- docs/stories/porch/ — a TRACK folder, not a status folder: status still
lives only in frontmatter. Adds `track: porch` so a query over
docs/stories/ can tell a porch 3 from a language 3
- 00-story.md carries the sequence, the dependency graph, and a table of
what the track explicitly does NOT own (binding -> 29, cache -> 18,
proxy -> 38, metrics -> 30, TLS/templates -> doctrine)
- eight iterations, each with phases, per-phase tasks, Given/When/Then
criteria, out-of-scope and the forks a spec must settle:
1 store-backed middleware (limiter + idempotency — needs nothing new,
first on purpose so the store pattern is proven cheaply)
2 randomness + cookies (phase A is language-track: a CSPRNG builtin;
`Resp.headers` being a map cannot emit two Set-Cookie lines)
3 sessions 4 CSRF 5 routing/response ergonomics (independent)
6 streaming core (the seam 7 and 8 wait on; chunked-request refusal
must survive) 7 SSE + compression 8 static + lifecycle hooks
- language iteration 39 -> status: hold, retitled superseded, with a row
mapping each of its goals to the porch iteration that took it. Kept, not
deleted: the Fiber study cites it and its randomness argument is what
this track is built on
- board gains a porch section; board-views gains porch and both-track
Dataview queries; porch README and the Fiber study §7 point at the track
- no code blocks in any story (plans carry concept and actions in words);
linkcheck 0 broken / 0 anchors
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.5 KiB
6.5 KiB
| track | iteration | status |
|---|---|---|
| porch | 3 | refine |
porch 3 — sessions: server-side state, revocable, durable
Part of Story —
porch, the writeonce web framework. Source: the Fiber parity study §2. Needs iteration 2 (a random session id and a signed cookie to carry it) and inherits the store convention from iteration 1.
Goals
- A session is a
@tablerow keyed by a random id; the cookie carries only the id. The alternative — a signed cookie carrying the whole payload — needs no store but cannot be revoked, and revocation is not optional for a real login. This is the fork iteration 1's store convention exists to answer. - Both timeouts, because they answer different questions. Idle timeout
bounds "how long since you did anything"; absolute timeout bounds "how long
since you authenticated". Fiber ships both (
IdleTimeout,AbsoluteTimeout) and a session with only the first never expires for an active attacker. - Durability is the differentiator. Fiber's default store is in-memory: a restart logs everyone out. porch's sessions ride the WAL, so they survive — and the gate proves it, because an unexercised durability claim is not a claim.
- Revocation that works. Logout, and "log out everywhere for this principal" — the second is what a password change needs, and it is nearly free once sessions are rows with an owner column.
Phases
Phase A — the session table and its lifecycle
- The
@tableclass: random id, principal, created-at, last-seen, and whatever payload shape fork 1 settles. Secondary index on principal, because "revoke all for this user" is an equality probe and that is the only index shape the engine has. - Create, touch, expire and delete, with expiry evaluated on access (the lazy discipline iteration 1 established — there is still no timer).
- Verify: rows replay across a restart; an expired row is pruned when touched.
Phase B — the middleware
- A
Sessionmiddleware whosebeforereads the signed cookie, loads the row, checks both timeouts, and attaches identity. Decide whether it setsreq.principalor writes intoreq.ctx—principalis the field the framework already means for identity, so it should be that unless the session carries more than identity. - Rotate the id on privilege change (login especially). Session fixation is the attack that a login which reuses the pre-login id walks straight into.
- A missing, expired, unknown or tampered cookie all end at the same place — unauthenticated — but for distinguishable reasons, because debugging a login loop without that distinction is miserable.
- Verify: each of those four cases behaves; the id changes across login.
Phase C — login, logout, revoke-all
- The three flows end to end in a sample, using porch's existing auth pieces
(
bearer_token,basic_credentials,ct_eq) for the credential check — sessions are about keeping identity, not establishing it. - Logout deletes the row and clears the cookie with matching attributes.
- Revoke-all deletes by principal and is proven with two concurrent sessions.
- Verify: after logout the old cookie is inert even though it is still well-signed — the point of server-side state.
Phase D — the gate and the ledger
- Extend
scripts/site-accept.shrather than only the storefront: the site has an admin route that currently checks a bearer token per request, which is exactly the thing sessions replace. - Ledger rows, status board entry, and the standup questions answered.
- Verify:
just web-app,just site,just linkcheckgreen.
Acceptance Criteria
- Given a valid session cookie, when a request arrives, then identity is attached and the row's last-seen advances.
- Given a session whose signature is valid but whose row was deleted, when it is presented, then the request is unauthenticated — a well-formed cookie is not authority.
- Given an idle timeout of T, when a session is unused for longer than T, then it is rejected and its row pruned on that access.
- Given an absolute timeout of A, when a session is continuously active past A, then it is still rejected — the criterion an idle-only implementation fails.
- Given an anonymous session id, when the user logs in, then the id is rotated and the pre-login id is inert.
- Given active sessions and a SIGTERM plus restart, when the same cookies return, then the users are still logged in, replayed from the WAL.
- Given two sessions for one principal, when revoke-all runs, then both are inert and other principals are untouched.
- Given logout, when the cleared cookie is compared to the one that set it, then the attributes match, so the browser actually drops it.
Out Of Scope
- Flash messages and a general session bag. A typed session with a known
shape first; an untyped
map<Text,Text>payload is a decision to defer, not a default to adopt. - OAuth, OIDC, SSO, "log in with X". Every one needs an outbound socket, which does not exist — language iteration 38.
- Remember-me tokens — a second, longer-lived credential class with its own rotation story. Its own slice.
- CSRF — iteration 4. Sessions make CSRF possible to do properly; they do not provide it.
- Sliding-window renewal of the absolute timeout. That is not what absolute means.
Info
Forks the spec must settle:
- What does the session row carry beyond identity? Just principal, or a
payload? A payload wants a shape, and without generics the shape is either a
declared class per app or
Text. The framework's cache already storesTextand that is the compromise this repo dislikes most — so leaning: identity plus declared columns, and apps that want more keep their own table keyed by session id. - Idle-timeout writes on every request. Touching last-seen means a WAL
write per request, which turns every read into a durable write. Options:
write at a coarser granularity, or accept the cost and say so with a number
from
just db-bench. This is a real performance fork, not a detail. - Does
Sessionsetreq.principalorreq.ctx?principalis the framework's declared home for identity and auth middleware already writes it, so two writers of one field need a documented precedence.