Public: anyone can read this; the crew can change it.
The app registry at its doors: the JSON API, the admin section on the page, an MCP tool #36
Before this opens a door that issues a key: it now waits on #50 (an app secret bound to the deployment's audience), because the moment a key is issued to another crew, changing the derivation costs them a rotation. Also note #53 — answer() has no status for unavailable, which is exactly what registerApp returns when no identity key is configured, so today that failure would leave this door as a 200.
Pull #67 (#30) is in review and puts the registry at the terminal door first, which settles two things this issue was going to decide alone. It exports KEY_SHOWN_ONCE from src/actions.ts — the sentence shown beside a freshly issued key — so the page's admin section prints that exact string rather than writing a second version of the same warning; and it proves the shape of "show the key once, never store it, never repeat it on a redraw", which the page has to match with a render that is not a redirect. This issue still waits on #50: a key handed out at a door is a key another crew stores, and the derivation should be bound to the deployment before the first one leaves the building.
An admin registers, rotates and revokes an app without a shell: the same four actions, reached from the API, the page and MCP.
Waits on #35, which lands the
apptable,src/apps.tsand the actions. This pull adds doors and decides nothing: every rule (admin only, the id shape, the duplicate, the revoked app) is already in the action, and a door that re-checks one is a second copy that will drift. Pull #22 has a working version of all of this — lift it, rebased on whateverpage.tsandroutes.tslook like when this starts.Done looks like
src/routes.ts, each a call onto the action andanswer(...):GET /api/apps,POST /api/apps({ id, name }),POST /api/apps/:id/rotate,DELETE /api/apps/:id. Status follows the failure through the existingSTATUSmap; a non-admin gets403 forbiddenand an anonymous caller401 signin_required, from the action.POST /api/appsandPOST /api/apps/:id/rotateand in noGET, ever — and in no log line, no error message and no redirect URL.src/page.ts, not more string inhome): the list with generation and a revoked marker, a register form, and rotate/revoke as same-origin POST forms. 44px controls, no horizontal scroll at 360px, keyboard reachable. A newly issued key renders inline in the response to the form that created it — never through a redirect, so nothing resends it — in a selectable<code>block with one sentence saying it will not be shown again.list_appstool intools, one sentence, a schema, straight ontolistApps. Noregister_apptool — issuing a secret over a transport an agent may log is not a convenience worth having, and the README says so in a line.src/server.test.tscovers each route at each role; a test asserts a key returned by register appears in no subsequentGET /api/appsand in no rendered page; a POST without a same-origin header is refused by the floor.Files:
src/routes.ts,src/page.ts,src/mcp.ts,src/server.test.ts,README.mdRisks:
page.tsandroutes.tsare the files #9 is rewriting — start after it merges or expect the rebase. Rendering a secret into HTML is the one place in this app where an escaping slip is a key disclosure: it goes throughescape()like everything else, and the test asserts the exact bytes. ADELETEfrom a browser form is not a thing, so the page's revoke is a POST to its own path while the API keepsDELETE.Suggested: Scout.