NextMyDay/docs/specs/spec-003-core-door.md
Claus Lohmar 57610cdbae docs: add the project plan, M0/M0.1 specs, agent guide and README
- PROJECT_PLAN.md: the decision log of record through D-042 and the M0.1 slice
- docs/specs: SPEC-002…009 — routing · door · identity/sessions · /setup ·
  enroll/login · roles/chain/recovery · module registry · the web layer & PWA
  (plus docs/specs/README.md, the index)
- AGENT.md: the working rules — commit AND push at every submilestone (gate
  green), Conventional Commits, never commit secrets
- README.md: the quickstart, the first-run/SSH-tunnel steps and the
  localhost/loopback caveat (SPEC-009 §13 Q-3): with an https base URL the
  CSRF/session cookies are Secure, so reach the app at localhost/127.0.0.1
- config.example.yaml: the settings template (D-035)
2026-10-14 10:53:44 +00:00

7.4 KiB

SPEC-003 — the core door (internal/core/server + httpx)

Field Value
Status Implemented (2026-10-13) — see §10; build/vet/test/-race green · static binary · live-smoked
Created 2026-10-13
Author OpenArchitect
Target milestone M0
Governing decisions D-002 · D-003 · D-025 · D-031 · D-039 · PROJECT_PLAN.md §4/§12
Related SPEC-002 (routing) · SPEC-004 (identity)

1. Goal

The single HTTP entry — the door — that turns an inbound request into a dispatched module function, applying identity / session / role and all cross-cutting concerns. The door resolves and gates; a module never sees a request it shouldn't (D-002, D-039).


2. Non-goals

  • The routing model → SPEC-002.
  • Identity/session storage, users, roles, the chain → SPEC-004.
  • The /setup, enroll, login, recovery flows → SPEC-005+.

3. Structural changes

Path Purpose
cmd/myday/main.go assemble config → core → discover modules → serve
internal/core/server/ the door pipeline + dispatch
internal/core/httpx/ session cookie · client IP (trusted proxies) · RedactPath · security headers · CSRF
internal/core/middleware/ correlation id · rate limit · audit

4. The door pipeline

request
  1. correlation id      — generate a UUID; put it on the context and in every log line
  2. client IP           — resolve from `server.trusted_proxies`; SET `X-Forwarded-For` (never append)  [A6]
  3. security headers    — X-Content-Type-Options · Referrer-Policy · a strict CSP · HSTS (when https)
  4. routing.Dispatch()  — the pure model (SPEC-002)
  5. switch on Kind:
       Redirect   → 302
       NotFound   → 404
       Setup      → the setup wizard, ONLY in setup mode; else 404
       Enroll     → the enrollment screen (if already enrolled → redirect)
       Route/Action →  [the identity gate]
            a. load the session cookie → validate:
                 exists · not expired · not revoked · **non-empty session id**  [A2]
            b. session.user_id == the resolved GUID's user   (the identity match)
            c. missing/invalid → KindLogin (login screen)
            d. ROLE gate: does the user's role permit this function?
            e. build the RequestContext → dispatch to the module function
  6. audit               — every auth event + every mutation (A11)
  7. rate limit          — `/{GUID}`, `/access`, `/setup` (A3)
  • Before dispatching, the door attaches audit.Meta{ActorUserID · ClientIP · CorrelationID} to the context.Context, so services can write their audit rows (SPEC-004 §5, ruling R-1).

The door renders no dynamic HTML — it resolves, gates, and hands off.


5. RequestContext (in-process hand-over)

type RequestContext struct {
    Workflow    string   // PARAM1 classification
    UserID      int64    // the resolved user (0 when anonymous)
    Principal   string   // "user:<id>" — IDENTIFICATION only, never a credential
    Function    string   // the resolved module function / action
    Page        string   // the joined remainder
    Correlation string   // the request's correlation id
    ClientIP    string   // resolved client IP
    Location    string   // IP-derived, AUDIT ONLY (D-031) — no tracking
}
  • Carries identification, never a credential.
  • Principal and the credential (session cookie) are separate concerns (D-001).

  • Cookie myday_session: HttpOnly · Secure · SameSite=Lax · Path=/.
  • Opaque random id; the server-side row is authoritative (SPEC-004).
  • CSRF: a per-session token on every action (do/action) — hidden field, verified server-side (A8). The action fields are not a security control on their own.
  • Logout revokes the session row.

7. Loopback enforcement (D-025)

  • In setup mode, if server.listen is not loopback, the process refuses to start (fail-closed). The operator bootstraps over an SSH tunnel to 127.0.0.1:8080.
  • Once the top admin exists (setup: false), a public bind is allowed.

8. Logging & redaction

  • slog, structured, with the correlation id on every line.
  • The access log records the raw path + PARAMS; the GUID is public (a username) but any credential that rides the path is redacted (RedactPath) — A8/A11.
  • The door logs the resolved (workflow, module, function); a module logs its own steps.

9. Acceptance criteria

# Criterion
AC-1 / → 302 /home; /holy-shit → 404
AC-2 /home renders with no session and no DB lookup
AC-3 /{GUID} with no session → login; with a valid session → the module function
AC-4 A session whose user_id ≠ the GUID's user is rejected
AC-5 An empty / missing session id is rejected on every path (A2)
AC-6 A do/action request without a valid CSRF token is rejected
AC-7 In setup mode with a non-loopback listen, the process exits with an error (D-025)
AC-8 Every response carries a correlation id, echoed in the log; path credentials are redacted
AC-9 The rate limiter returns 429 on the auth-sensitive routes (A3)

10. Implementation notes (accepted rulings)

Built 2026-10-13. build · vet · test · -race green; static CGO-free binary; live smoke verified (/→302 · /home→200 · /setup→200 · /holy-shit→404 · correlation id in header and log · loopback enforcement · fail-closed secrets). internal/core/routing purity intact.

# Builder choice Ruling
D-1 gopkg.in/yaml.v3 + KnownFields(true) (unknown config key = error) Accepted — pure-Go, static-safe, fail-closed.
D-2 Secret strength: session_key/cookie_secret/csrf_key ≥ 32 bytes; totp_encryption_key = 32 bytes; CHANGE_ME rejected in productive mode Accepted — to be documented in the config reference.
D-3 CSRF = stateless HMAC-SHA256(csrf_key, "myday.csrf.v1:"+session_id) Accepted — no extra table; dies with the session.
D-4 In-memory fixed-window rate limiter (30/min; classes access·setup·guid) Accepted.
D-5 RoleGate seam, defaulting to allow any authenticated user Accepted — the policy lands in SPEC-007/008.
D-6 Env: config path via MYDAY_CONFIG only Accepted; per-key env override deferred.
D-7 RequestContext = exactly §5 (no Module field) Accepted — module identity stays on Resolution.

Rulings on the flagged items:

# Item Ruling
R-4 auth.Mint/Rotate/Revoke hard-code the owning user as actor. Use the context actor (creator/approver); the affected user is the subject — see SPEC-004 §8.
R-5 Pre-session CSRF — /setup, /access and the enroll/login actions run before a session, so §6's session-bound CSRF doesn't cover them. Must be added — a signed double-submit cookie for anonymous forms. Applies to SPEC-005/006/007; tracked as A15.
— RequestContext.Location always empty (D-031) Accepted — the seam exists; the IP-geo lookup is deferred.
— RedactPath over-redacts a segment after a credential-like name Accepted — log-only; no leak.