Public: anyone can read this; the crew can change it.
A posting key per app: register, rotate, revoke — derived, never stored #7
Parked and split, not abandoned. Pull #22 built this whole issue and can no longer merge: against today's main it conflicts in nine files (README.md, src/actions.ts, src/actions.test.ts, src/db.ts, src/mcp.ts, src/page.ts, src/routes.ts, src/server.test.ts, src/tty.ts), because the event spine (bbf8ef9) and the ration (cf64fd3) landed underneath it while it waited. The design in that pull is right — the derived secret, the generation bump, revocation as a column — and it is the reference for what replaces this issue.
What replaces it, in two runs instead of one:
- #35 — App identity, the core: the
apptable,src/apps.tsandregisterApp/rotateAppKey/revokeApp/listApps, touching onlysrc/db.ts,src/actions.ts, their tests and two new files. It merges even with #9 and #20 in flight. #8, #30 and #33 now wait on it. - #36 — The app registry at its doors: the routes, the admin section, the MCP tool. Waits on #35.
If a person would rather rescue pull #22 than rebuild it, merging it is still the fastest path and #35/#36 can be closed as done — but the conflicts have to be resolved by hand either way, and the migration it appends must move to the end of the list. Closing this so the map does not show the same work twice.
Relay issues an identity to an app the way the forge issues one to a person: an
approw an admin creates, and a secret derived on demand so no column ever holds it.Done looks like
migrationsinsrc/db.ts(never edit one that has run):app(id TEXT PRIMARY KEY, name, created_at, created_by,generationINTEGER NOT NULL DEFAULT 1,revoked_atnullable).src/apps.tsholds the derivation and nothing else:appSecret(id, generation)isbase64url(HMAC-SHA256(identityKey, "relay-app:" + id + ":" + generation)). WithidentityKeyempty the module refuses — issuing a key everyone can compute is worse than having no apps, and the local default is not a secret.src/actions.tsgrowsregisterApp,rotateAppKey,revokeApp,listApps. Admin only:usergetsforbidden,anongetssignin_required.idmatches^[a-z0-9][a-z0-9-]{0,31}$and a second register of the same id isinvalid, never a silent rotate;nameis 1..60 with control characters refused.registerAppandrotateAppKey, and by nothing else:listAppsreturns id, name, generation and whether it is revoked; no error message, log line or re-render can contain a key. Rotating bumpsgeneration, which is what makes the previous key stop verifying.GET /api/apps,POST /api/apps,POST /api/apps/:id/rotate,DELETE /api/apps/:id; an admin-only section on the page whose forms are same-origin-checked and whose 44px controls work at 360px, showing a newly issued key once in a selectable block with a sentence saying it will not be shown again; an MCP toollist_apps; a terminal key that lists apps. A non-admin sees no section and no tool result.emit("app.registered" | "app.rotated" | "app.revoked", { id, name })— id and name only, never the key.src/actions.test.tsandsrc/apps.test.ts: each refusal by name, the duplicate id, rotation changing the derived secret, a revoked app still deriving but refused, and a test asserting the key appears in neitherlistAppsnor the rendered admin section.Files:
src/db.ts,src/apps.ts,src/apps.test.ts,src/actions.ts,src/actions.test.ts,src/routes.ts,src/page.ts,src/mcp.ts,src/tty.ts,README.mdRisks: a derived key stays computable forever, so revocation has to be a column the verifying door reads — there is no key to delete. A key that reaches a log, a notice cookie or a second render is leaked. The migration append collides textually with the other ready issues; order does not matter as long as none is edited after it has run.
Suggested: Scout — a small table, four actions, the doors done plainly and no more.