hsa-app/plan.md
Jean-Michel Tremblay 8b7252dad4 Initial commit: HSA receipt tracker
Go app for capturing and archiving HSA-eligible receipts: OIDC/PKCE auth
against Authelia, SQLite storage with dual-write (filesystem + DB blob),
mobile-first upload, and DB export.

Adds AI receipt classification: a config.json catalog of people and
categories (seeded into the DB on startup), a prompt builder that derives
name-order/initial variants from the data (with same-surname ambiguity
handling), and an Anthropic tool-use client behind POST /classify. Tests
run against a mock endpoint; a live integration test is env-gated to the
cheapest model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:40:12 -04:00

7.6 KiB
Raw Permalink Blame History

HSA Receipt Tracker — Implementation Plan (v1)

Companion to spec.md. This is the how and the order; spec.md is the what. Strategy: red/green (write a failing test, make it pass) wherever the logic is pure and deterministic. Where behaviour depends on the live world (the real OIDC round-trip against Authelia, a camera, a browser), we verify manually instead and say so explicitly.


Decisions locked (from consultation)

Topic Decision
Stack Go — single static binary, systemd in LXC, SQLite
App domain hsa.maisym.com
Auth issuer auth.maisym.com
OIDC client Confidential + PKCE
Auth group hsa-users (gates access; not in group → 403)
Session Encrypted, signed cookie (stateless, no server-side store)
Edit Designed-for, not built in v1
Export /export/db only, with include-blobs yes/no toggle
Files PDF + images, accepted as-is; client compression is phase 2
View list/detail Out of scope for v1
Auth testing Unit-test pure pieces; verify live round-trip manually

Build order (milestones)

  • M0 — Auth flow only. App authenticates against live Authelia and shows a "you're logged in" result, including the group gate. This is the first thing JM wants to see running. Detailed below.
  • M1 — Storage layer. SQLite schema + receipts data access (insert, soft-delete, fetch). Dual-write (filesystem + BLOB) in one transaction.
  • M2 — Upload flow. Mobile-first capture/upload form (amount, date, category), PDF+image handling, validation, store both copies, confirmation page.
  • M3 — Export. GET /export/db with include-blobs toggle, via point-in-time snapshot (never the live DB file).
  • Phase 2 / later. Client-side image compression; edit functionality.

Only M0 is planned in detail below — we plan the next milestone when we get there, so the plan stays honest.


Milestone 0 — Auth flow

Goal / acceptance: Open hsa.maisym.com in a phone/browser → redirected to auth.maisym.com → log in → land back on a page that reads roughly:

Logged in as jeanmi.tremblay@gmail.com · groups: [hsa-users] · access: GRANTED

…and a user who is not in hsa-users gets a 403. That single screen proves the entire chain: OIDC+PKCE round-trip, identity claims, group claim, and the authorization gate.

Step 0.1 — Prerequisites (setup, not tested)

  • Install Go toolchain (not currently present on this machine).
  • git init the repo.
  • go mod initmodule path TBD (see Open Questions). Default proposal: maisym.com/hsa.
  • Project skeleton: cmd/hsa/main.go, internal/auth/, internal/web/, internal/config/.

Step 0.2 — JM's Authelia-side config (your action; app can't do this)

Enumerated here so the app and Authelia agree on every value:

  • Create group hsa-users; add both users.
  • Register a confidential OIDC client in identity_providers.oidc.clients:
    • client_id: e.g. hsa-tracker
    • client_secret: random, stored hashed (via authelia crypto hash generate)
    • redirect_uris: https://hsa.maisym.com/callback (+ a dev redirect, e.g. http://localhost:8080/callback — see Open Questions)
    • scopes: openid, profile, email, groups
    • response_types: code; grant_types: authorization_code
    • PKCE: require S256
    • token endpoint auth method: client_secret_basic (confirm at execution)
  • Reload Authelia.

Step 0.3 — App config (env vars)

ISSUER_URL, CLIENT_ID, CLIENT_SECRET, REDIRECT_URL, REQUIRED_GROUP (=hsa-users), SESSION_KEY (cookie encryption key), LISTEN_ADDR. Loaded and validated at startup (fail fast if any missing).

Step 0.4 — Red/green units (pure logic)

Each is a failing test first, then the implementation:

  1. PKCEGenerateVerifier() (43128 URL-safe chars) and ChallengeS256(verifier). Test with the RFC 7636 Appendix B known vector (fixed verifier → known challenge) so the encoding is provably correct.
  2. State / nonce — sufficient length, two calls differ (CSRF + replay defense).
  3. Session codecEncode(session)/Decode(cookie) round-trips; a tampered value is rejected; a value signed with the wrong key is rejected.
  4. Authorization decisionIsAuthorized(groups, required) bool. Table tests: in-group → true; empty groups → false; other-group-only → false. This is the 403 rule, tested in isolation.
  5. Authorize-URL builder — construct oauth2.Config with fixed endpoints (no network/discovery) and assert AuthCodeURL carries state, code_challenge, code_challenge_method=S256, and the right scopes.

Step 0.5 — Handlers tested via httptest (no live Authelia)

  • GET /healthz → 200.
  • GET / (protected) behind RequireAuth: forged valid session cookie → 200 and the page shows identity + groups; no/invalid cookie → 302 to /login.
  • GET /logout → clears cookie, 302.

Step 0.6 — Live round-trip (manual verification — the M0 acceptance)

Not unit-testable (needs real Authelia + a human clicking login):

  • GET /login → builds PKCE+state, stashes verifier/state in a short-lived cookie, 302 to Authelia's authorize endpoint.
  • GET /callback → validate state, exchange code (with PKCE verifier), verify the ID token, extract claims, run IsAuthorized, set the session cookie, redirect to /. (The pure sub-parts — claim extraction, group gate — are already red/green from 0.4.)

Manual test script:

  1. Run the app with real config (locally with the dev redirect, or deployed in the LXC behind Caddy).
  2. Browser → app → redirected to auth.maisym.com → log in → returned to success page showing identity + [hsa-users] + GRANTED.
  3. Negative path: an account not in hsa-users403.

M0 done =

All red/green units green, httptest handler tests green, and the manual round-trip (both positive and 403 paths) confirmed against live Authelia.


Open questions (revisit before/at execution — not guessing)

  1. Module path / repo namemaisym.com/hsa? a GitHub path? Will there be a GitHub remote, or local-only for now?
  2. Dev redirect URI — do you want to test M0 locally first (needs a http://localhost:8080/callback redirect added to the Authelia client), or deploy-to-LXC-first and test only at https://hsa.maisym.com?
  3. Token endpoint auth methodclient_secret_basic vs _post. Pick when we wire it; basic is the default assumption.
  4. Infra ownership — Caddy vhost + systemd unit: you handle, or you want me to draft the unit file / Caddyfile snippet as part of M0?

Deferred to later milestones (noted so we don't design against them)

  • SQLite driver choice — lean modernc.org/sqlite (pure Go, keeps the static binary, no cgo). Decide at M1.
  • Edit (v1: schema/storage must not preclude it — on a future edit, update FS file and BLOB in one transaction, keep file_path UUID stable).
  • Client-side image compression (phase 2): re-encode camera images to JPEG/WebP at a quality factor before upload; PDFs pass through untouched.