# 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) ```go type RequestContext struct { Workflow string // PARAM1 classification UserID int64 // the resolved user (0 when anonymous) Principal string // "user:" — 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). --- ## 6. Session cookie & CSRF - 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. |