Commit graph

8 commits

Author SHA1 Message Date
03a9db03dc fix(security): harden httpx — XFF hops, __Host- cookie, isolation headers
REVIEW-001 (M0) perimeter findings:

- H1: resolveForwarded walked X-Forwarded-For left→right and returned the
  leftmost untrusted entry. An append-style proxy (nginx's default) appends
  the peer it saw to the RIGHT of whatever the client sent, so the leftmost
  entry is attacker-controlled — a fresh rate-limit bucket per request (A3)
  and a forged client_ip in the audit trail (A11/D-031). Walk right→left and
  return the first untrusted entry, falling back to the peer.
- L1s: the session cookie now carries the `__Host-` prefix when it is Secure
  (https), so it is pinned to the exact origin and cannot be shadowed from a
  sibling subdomain. Conditional on purpose: a browser rejects a `__Host-`
  cookie without Secure, which would break the http loopback bootstrap.
- L4s: add Permissions-Policy, Cross-Origin-Opener-Policy and
  Cross-Origin-Resource-Policy.
- Add httpx.IsLoopbackIP: a RESOLVED client IP test that fails closed on an
  empty/unparseable value (the setup-mode gate, H2), sharing a classifier
  with IsLoopbackHost whose "unknown" direction is the opposite, each
  fail-closed for its own decision.

Tests updated: the unit test encoding the vulnerable leftmost expectation is
rewritten, plus right→left/forged-entry/host-classification coverage.
2026-10-14 11:29:52 +00:00
dd865d4f27 feat(web): add the web shell and the PWA (SPEC-009)
- One base layout (layout.html) with the header, the nav from Registry.Nav()
  (D-038), a content slot and the CSRF token; every page renders inside it and
  a module supplies content only
- Embedded same-origin assets (app.css · app.js · webauthn.js · sw.js ·
  manifest.webmanifest · icons) served by the reserved `static` workflow
- webauthn.js drives the three ceremonies from data-* attributes — no inline
  script, so the CSP `script-src 'self'` holds (AC-3)
- manifest start_url "/" (D-011); the service worker never caches an
  authenticated response or /{GUID} (A5)
2026-10-14 10:53:57 +00:00
d7d2c7d2b6 feat(module): add the module registry, discovery and the home module (SPEC-008)
- Build-time discovery (D-028, the Caddy pattern): a module self-registers from
  init(); internal/modules/imports_gen.go (go:generate) blank-imports every
  module directory, so THE DIRECTORY IS THE SOURCE OF TRUTH
- The registry: deterministic registration order, config-backed enable/disable
  (D-038), required modules, name-prefixed migrations, Mount() into the door
- The name-collision guard now also fails a module named for a reserved
  level-1 workflow (SPEC-009 §13 Q-1) — access · static · api · media · ws ·
  dev — derived from routing.ReservedWorkflows, `home` excepted (D-2)
- home: the reference dashboard module, and the core Modules page
2026-10-14 10:53:57 +00:00
4c839ff30d feat(recovery): add the admin recovery flow (SPEC-007)
Config-flag → email user 1 an enrolment link (D-033), with the approve/deny
actions and dual control.
2026-10-14 10:53:53 +00:00
65618c57db feat(setup): add the /setup bootstrap wizard (SPEC-005)
Reserved, setup-mode-only, loopback-only: the domain, the top admin, the
authenticator (TOTP), mail/locale — then Finish persists and the dashboard
becomes reachable. The wizard IS the bootstrap (D-026); the TOTP key is
generated at startup so TOTP enrollment precedes Finish (SPEC-009 D-1).
2026-10-14 10:53:53 +00:00
76a4a6950c feat(auth): add WebAuthn, TOTP and the enroll/login flow (SPEC-004/006)
- webauthn: the passkey ceremony service (a fake in tests)
- totp: the TOTP authenticator; the secret is AEAD-sealed at rest (A9)
- sealer: the AEAD TOTP sealer
- identities/sessions/credentials: the GUID state machine
- flow: the enroll/login screens + the passkey nudge (pre-session
  double-submit CSRF — A15), rendered inside the shell
2026-10-14 10:53:53 +00:00
d11e14295b feat(core): add the store, identity plumbing and the door (SPEC-003/004)
- store: SQLite (WAL, foreign keys ON, CGO-free driver) + the embedded
  migration runner; each module's migrations are name-prefixed (D-012)
- audit: the append-only audit log; the actor threads via context (R-1)
- users: the user_id/GUID/email anchors (D-013)
- config: the YAML settings file (D-035) + validation
- httpx: cookies · loopback detection · trusted-proxy XFF · CSRF · security
  headers · redaction
- middleware: correlation id · audit · rate limiting
- mail: the invite/recovery mailer seam
- server: the core DOOR — the resolution switch, the dispatch registry
  (RegisterModule), the role gates, /setup · enroll/login · recovery flows,
  the reserved `static` workflow, and the loopback-only bootstrap (D-025)
2026-10-14 10:53:51 +00:00
e114a2be61 feat(routing): add the pure routing model (SPEC-002)
Turns a request path plus the two routing fields (`do`/`action`) into a single
Resolution for BOTH doors (D-039): the RESERVED level-1 switch first
(home/access/setup/api/static/media/ws/dev — D-004), then the GUID classifier,
then the registry. Identity and the module registry are injected interfaces, so
the package is PURE — no HTTP stack, no DB (asserted by purity_test.go). All 22
spec rows are covered.
2026-10-14 10:53:48 +00:00