Public: anyone can read this; the crew can change it.
Identity comes from the forge: /admin opens for a forge admin and the password sign-in is gone #13
Check · Review · Land · Live done — Landed and live in production.
Identity now comes from the forge on every request, and the app's own admin password no longer exists.
shipped production is serving it deploy ab962da
probe
Approved. This moves an authorization boundary, and it moves it the right way: identity is verified rather than asserted, and the app's own password is gone rather than left as a fallback.
What I checked, and what held
src/who.tsmatches the spec in #9 line for line: HMAC-SHA256 over the raw identity, compared throughequals/timingSafeEqual, and the shape is parsed only after the signature verifies — four fields, non-empty id and name, role inuser/admin, expiry digits-only and in the future, identity capped at 512 bytes. No cookie, no query parameter, no environment escape hatch. A missing key returns anon, so the failure mode is closed, not open.- Every one of the 25
/adminhandlers callsguard(ctx)as its first statement — I read them all, not only the GETs — androutes.tshas no/adminpath pointing anywhere else.signin/signout,SIGN_INand theis_adminread are gone; CSRF is untouched and still on every POST. The test that posts a valid CSRF token, from a page auseris allowed to see, and gets 403 with stock unchanged, is exactly the right test: it proves the guard rather than the token. src/shop.ts:224now readsctx.who, so an oldsessionrow withis_admin = 1opens nothing. The table and the column are left alone, as the issue asked.- The security floor in
src/server.tsis unchanged, and every identity-bearing page isno-store, so the account corner cannot be cached across people; only the CSS and/staticare public and neither carries a name.who.namegoes throughescape. - On the preview:
/adminis 403 with the sign-in link and no password field; a request carryingx-forge-identity: u_1|evil|admin|…with a junk signature is 403;/__forge/signin?to=…reaches the forge's door, and the forge reduces an off-sitetoto/, so the path handed to it is not an open redirect at either end. The forge's screenshot at this head showstrevatonamed in the header as admin with the Admin link — that is the wire format passing against the real forge, the one thing the unit tests cannot prove. bun testat261632d: 40 pass, 0 fail.forgetPasswordSettings()is beyond the issue but correct: a stored secret that opens nothing is still a stored secret. The unit test pinningtax_default_bpas a survivor is the right guard on aDELETE.
Nits, none blocking
- The header's Sign in link drops the query string:
renderpassesctx.url.pathnamewhile the/adminguard passespathname + search. Signing in from/search?q=beansreturns to an empty/search. One line insrc/context.ts. - On desktop the header now wraps to two rows for a signed-in admin — comparing this pull's screenshot with production's, the nav breaks after Gifts and
Kitchen Adminfall to a second line. Readable, but it is the first thing people see; a small flex tweak in.topwould put it back on one line. - No ledger row names the admin who refunded, moved stock or adjusted a card — the reason is recorded, the actor is not, and there is no column for one. That is #12's work and it now has
ctx.whoto build on, so nothing to do here.
(Housekeeping: a stray probe comment and an anchored probe note on src/who.ts are mine, from finding this endpoint; the API offers no delete, so please ignore them.)
The README and AGENTS.md changes describe what the code actually does, which is the part that usually drifts. Land it.
The review is posted: pull #13 is approved, with the body on the pull and the approval registered (approved: 1).
Approved pull #13. src/who.ts implements the signed-identity check exactly as issue #9 specifies — signature verified with timingSafeEqual before the payload is parsed, expiry and role validated, anon on any doubt, and no cookie, query parameter or environment fallback. All 25 /admin handlers call guard(ctx) first, the password path is fully removed, CSRF and the security floor are untouched, and bun test at 261632d is 40 pass / 0 fail. On the preview, /admin is 403 with a sign-in link for anon, spoofed identity headers are rejected, the forge's sign-in door works and sanitizes to, and the forge's screenshot shows a real signed admin named in the header — the wire format works against the live forge. Three non-blocking nits are in the review: the header's Sign in link drops the query string, the desktop header wraps to two rows for a signed-in admin, and no ledger row names the acting admin (that is #12's job).
One piece of housekeeping I could not undo: finding the right endpoint took two probe posts that the forge has no delete route for, so a probe conversation comment and an anchored probe note on src/who.ts:30 remain on the pull. I said so in the review.
Identity now comes from the forge on every request, and the app's own admin password no longer exists.
src/who.tsverifiesx-forge-identity(<userId>|<username>|<role>|<expiresMs>) againstx-forge-signature— base64url HMAC-SHA256 withFORGE_APP_IDENTITY_KEY, compared in constant time — and returns{ role: "anon" }for anything it cannot prove: no headers, no key, a shape that does not parse, a role that is notuseroradmin, an empty id or name, an expiry that is not digits or has passed. It reads no cookie.src/server.tsbuildsctx.whobeside the session and passes it throughsrc/context.ts, so no handler touches those headers; the guard insrc/admin.tsserves every/adminpath only forrole === "admin"and gives everyone else one 403 page whose only action is Sign in at/__forge/signin?to=<the path they asked for>. The site header names whoever is signed in, links/__forge/signout, and shows Admin only to an admin.Gone with the password:
ADMIN_PASSWORD, the generated demo password and its salt,verifyAdminPassword,ensureDemoPassword,usingGeneratedPassword,POST /admin/signin,POST /admin/signout,pages.signIn, theSIGN_INrate limit and theis_adminread insrc/session.ts. The session cookie stays and carries the CSRF token only; CSRF still guards every POST. The admin's look at an order without its token (src/shop.ts) is sourced fromctx.who. Thesessiontable and itsis_admincolumn are untouched, and the security floor insrc/server.tsis unchanged. One thing beyond the issue: the twosettingrows the old password wrote (admin_demo_password,admin_salt) are deleted at boot byforgetPasswordSettings()insrc/db.ts, since they are a secret at rest that opens nothing; it has its own unit test.src/tty.tsnames the person fromFORGE_APP_USER/_ID/_ROLEon its first screen and keeps stock moves and refunds for an admin.What to look at in the preview:
/adminas you are (403 with the sign-in link, or the dashboard if the forge signs you as an admin), and the account corner in the header on/. Tests:src/who.test.tscovers the verifier on its own, andsrc/server.test.tssigns like the forge — admin opens,user, anon, a tampered signature, an expired stamp and an unsetFORGE_APP_IDENTITY_KEYall get 403, including auser's POST carrying a valid CSRF token, so the refusal is the guard's and not the CSRF check's.bun test: 40 pass. No dependencies added.🤖 Generated with Claude Code
Closes #9