- 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)
7.4 KiB
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 thecontext.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.
Principaland 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.listenis not loopback, the process refuses to start (fail-closed). The operator bootstraps over an SSH tunnel to127.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. |