Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8bbf58e
feat(chat): AI chat agent page with tool-based data access
alon710 May 27, 2026
d76a386
refactor: rename middleware.ts to proxy.ts for Next.js 16
alon710 May 27, 2026
8431779
Merge pull request #3 from alon710/alon710/middleware-to-proxy-rename
alon710 May 27, 2026
5f0ca8c
feat(chat): persist chat sessions with history sidebar and markdown r…
alon710 May 27, 2026
81e0e0d
Merge pull request #6 from alon710/alon710/ai-chat-agent-page
alon710 May 27, 2026
ba541f2
fix(transactions): exclude credit card transfers from expense totals
alon710 May 27, 2026
cfcf338
Merge pull request #8 from alon710/alon710/fix-credit-card-double-cou…
alon710 May 27, 2026
8eb519b
feat(ai): add Gemini as a third AI provider (#7)
alon710 May 27, 2026
89bb2d6
feat(ci): add strict CI gate and migrate from npm to bun (#9)
alon710 May 27, 2026
a891ced
docs + sync: refresh README and surface full sync error message (#10)
alon710 May 27, 2026
2ed3035
fix(ai): support Gemini in chat and trim provider API keys (#11)
alon710 Jun 5, 2026
e131237
feat: locale URL routing, double-counting fixes, and pre-launch polis…
alon710 Jun 6, 2026
6914e4b
feat: add financial-event transaction deduplication engine
alon710 Jun 6, 2026
6b321e4
feat(db): introduce Drizzle ORM as the typed query layer
alon710 Jun 6, 2026
28978bb
refactor(db): migrate query layer to Drizzle ORM (hybrid)
alon710 Jun 6, 2026
a711b21
chore: remove menubar apps and service scripts, slim tooling
alon710 Jun 6, 2026
c654e71
Merge remote-tracking branch 'upstream/main' into alon710/drizzle-orm…
alon710 Jun 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Full history so `lint:changed` can resolve the merge-base with main.
fetch-depth: 0

- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3"

- name: Install dependencies
env:
PUPPETEER_SKIP_DOWNLOAD: "true"
run: bun install --frozen-lockfile

- name: Strict gate (format / lint / typecheck / i18n / knip / react-compiler / security / tests)
run: bun run ci
6 changes: 0 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,6 @@ yarn-error.log*
/.claude/
/.superpowers/

# menu bar app build outputs
/menubar/.build/
/menubar/build/
/menubar/.swiftpm/
/menubar/Package.resolved

# vercel
.vercel

Expand Down
10 changes: 9 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,17 @@ Key priorities (in order):
## Testing the app

```bash
npm run dev # starts on 127.0.0.1:3000
bun dev # starts on 127.0.0.1:3000
```

The full CI gate (formatting, typecheck, i18n, knip, react-doctor, tests) is `bun run ci`. GitHub Actions runs the same script on every PR via `.github/workflows/ci.yml`. The five strict checks the project enforces:

- `bun run format:check` — Biome formatter
- `bun run i18n:check` — `@lingual/i18n-check` for missing / orphan i18n keys (next-intl-recommended; wrapped in `scripts/check-i18n.mjs` with a baseline ignore list)
- `bun run knip` — dead code (files, deps, unlisted deps)
- `bun run react:doctor` — `react-compiler-healthcheck`
- `bun test` — Bun's built-in Jest-compatible test runner, with `--conditions react-server` so `server-only` resolves as a no-op

For end-to-end testing without real credentials, call the setup API directly:

```typescript
Expand Down
166 changes: 72 additions & 94 deletions README.md

Large diffs are not rendered by default.

66 changes: 22 additions & 44 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ sandbox on.

## CSRF defense

Next.js middleware (`src/middleware.ts`) rejects any mutating API
Next.js proxy (`src/proxy.ts`) rejects any mutating API
request (POST/PUT/PATCH/DELETE) whose `Origin` or `Referer` header
doesn't match the app's own host. This prevents a malicious tab in
your browser from triggering syncs / category changes against your
Expand Down Expand Up @@ -140,64 +140,42 @@ defense.
key only exposes one credential at a time.
- **Audit log** of every API mutation with timestamps.

## Always-on service

If you install Spent as a background service (`npm run service:install`),
the server runs from login to logout (or from boot to shutdown). This
changes how some surfaces look. The guarantees:

**The server still binds only to `127.0.0.1`.** The `npm run start`
script hardcodes `-H 127.0.0.1 -p 41234`, and every per-OS template
(LaunchAgent plist, systemd unit, Task Scheduler XML) invokes it with
those flags. The installer runs a post-install check and refuses to
finish if it detects the server listening on a non-loopback address.

**No new daemon runs as root or SYSTEM.** The installer refuses to run
as root. The LaunchAgent and systemd user unit run under your user.
The Windows scheduled task uses `LeastPrivilege` and runs as the
installing user, not as `SYSTEM`.

**The hostname is loopback-only.** Spent uses `spent.localhost`, which
RFC 6761 reserves as loopback. macOS and Linux resolve `*.localhost`
to `127.0.0.1` natively through the system resolver, so no hosts file
edit is needed there. On Windows, `npm run service:install` appends
`127.0.0.1 spent.localhost` to the hosts file as a compatibility
fallback (the only step that requires elevation, and it prompts
interactively). The block is bracketed with markers and removed
cleanly by `npm run service:uninstall`. No mDNS / Bonjour service is
ever registered, and the loopback address never resolves from another
device on your network.
## Running it always-on

Spent does not ship a service installer. You run the server yourself with
`bun start` and, if you want it always-on, wrap that command in your own
service manager (a macOS LaunchAgent, a systemd user unit, a Windows
scheduled task, `tmux`, etc.). Whatever you use, these properties hold:

**The server binds only to `127.0.0.1`.** The `bun start` script hardcodes
`-H 127.0.0.1 -p 2412`, so the dashboard is reachable from your machine
only — never from your LAN or the internet. Do not change the host flag to
a non-loopback address. Run your service manager under your own user, not
as root / SYSTEM.

**The health endpoint discloses minimum information.** `GET /api/health`
returns `{ok, version, hasDb}` and nothing else. No transaction counts,
no provider names, no setup status. Add to it only if you have thought
carefully about what a local cross-app attacker could learn.

**The macOS menu bar app has no network entitlements beyond loopback.**
`Spent.app` ships with an `NSAppTransportSecurity > NSExceptionDomains`
entry whitelisting `127.0.0.1` and nothing else. It cannot reach the
internet even if its code were modified to try, without re-signing the
bundle with a different Info.plist.

**Logs do not leak credentials.** macOS LaunchAgent stdout/stderr go to
`~/Library/Logs/Spent/{out,err}.log` (directory mode `0700`). Linux
systemd writes to `~/.local/state/spent/log/`. The app itself already
avoids logging credentials (see "What's protected at rest" above);
the always-on service does not change that.
**Logs do not leak credentials.** The app avoids logging credentials (see
"What's protected at rest" above). If you redirect `bun start` output to a
file, store it somewhere only your user can read (e.g. directory mode
`0700`).

**The encryption key file's permissions are now asserted at startup.**
**The encryption key file's permissions are asserted at startup.**
`src/server/lib/encryption.ts` reads `data/.encryption-key` and refuses
to start if the file mode is not `0600` (POSIX only; Windows relies on
the user profile ACL). If you ever `chmod 644` the key file by accident,
the server will fail loudly with the fix command.

**What the always-on service does not protect against:**
**What running always-on does not protect against:**

- A local attacker who can already run code as your user. They can read
the DB and key file with or without the service running.
the DB and key file with or without the server running.
- A malicious browser tab on your machine doing a CSRF against
`127.0.0.1:41234`. The same-origin middleware in
`src/middleware.ts` already blocks this on every mutating request,
`127.0.0.1:2412`. The same-origin proxy in
`src/proxy.ts` already blocks this on every mutating request,
and that protection works the same whether the server runs on demand
or always-on.

Expand Down
29 changes: 0 additions & 29 deletions Spent.sln

This file was deleted.

59 changes: 59 additions & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": true,
"includes": [
"**",
"!.context",
"!**/.next",
"!**/out",
"!**/build",
"!**/data",
"!**/coverage",
"!website",
"!**/*.lockb",
"!**/bun.lock",
"!src/components/ui"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"linter": {
"enabled": false
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"trailingCommas": "all",
"semicolons": "always",
"arrowParentheses": "always"
}
},
"json": {
"formatter": {
"trailingCommas": "none"
}
},
"css": {
"parser": {
"tailwindDirectives": true
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
Loading