Public: anyone can read this; the crew can change it.
A forge link says when it ends and its sender revokes it in one tap #10830
Check · Review · Land · Live done — Landed and live in production.
A sender can now revoke an open forge link and undo it within a minute. A revoked or expired link shows a plain "This link has ended" page with status 410 and sets no session.
shipped production is serving it deploy 951be3d
A sender can now revoke an open forge link and undo it within a minute. A revoked or expired link shows a plain "This link has ended" page with status 410 and sets no session.
The new test/claim-revoke.test.ts and the MCP size and table tests pass, and bun x tsc --noEmit is clean. I ran the related forges*, mcp*, spawn-api, spawn-events and audience tests; the only two failures were the ones fixed below. I did not run the full suite. scripts/fast.ts called the diff too broad to name its tests.
What changed
- Link state:
src/spawn/claim.tsstores a revoked timestamp (r) inside the existing claim setting. The link's secret is untouched and never logged.
- revokeClaim and restoreClaim flip that timestamp. A restore only works within 60 seconds of the revoke, while the link is unspent and still in date.
- linkStateIn reads a child's state (open, used or ended) without writing to it.
- Revoke action:
changeClaiminsrc/actions/spawn.tsis admin-only and same-origin checked. It writes aforge.claim_revokedorforge.claim_restoredevent whose payload holds only the forge name. - Sent-links list:
/forgesshows a state badge on each row, plus a Revoke button when the link is open.
- Revoke and Undo go through POST /forges/:name/revoke-link and /undo-link, and the confirmation page carries the Undo button.
- MCP:
claim_linktakesaction: "revoke" | "undo"instead of a new tool.docs/MCP.mdis regenerated, and no ceiling was raised. - Claim page: a revoked link answers 410 with "This link has ended". Expired links keep their existing page.
Tests
test/claim-revoke.test.tscovers revoke-then-use (410 on both/claim/optionsand/claim, noset-cookie) and use-then-revoke (refused). It also checks that a cross-origin revoke is refused, and that undo works and the receipts omit the secret.- I added
link: nullto hand-built rows intest/forges-attention.test.tsandtest/tui-forges.test.ts, so they type-check with the newClientRow.linkfield. test/mcp-forge-view.test.tsread only the first page oftools/list. Myactionenum pushedforge_viewonto page two. That test was wrong, so it now followsnextCursor.
Not done: a revoked link cannot be revived after the minute; send a fresh link instead. There is no new MCP tool.
To try: open /forges as an admin and press Revoke beside an open link. Open the link in another browser to see the 410 page, then press Undo within a minute.
Revoke/undo logic is sound (compare-and-set update, admin + same-origin check, secret kept out of events). Changes needed, all in this round:
- FIRST: both doors. Revoke/undo exists on the page and MCP but not in the terminal. Add it to the forges screen in src/tui/ (state per row, revoke, undo), entered where AGENTS.md says, gated like the page (admin only). tui-forges.test.ts only gained
link: null, so nothing covers it. Add docs/features/<name>.md for the how-to (nothing was added). - Tests missing: (a) undo after 60s must be refused (restoreClaim takes
now; test it at 61s), and undo of a used or expired link; (b) the web routes POST /forges/:name/revoke-link and /undo-link: a non-admin or guest gets nothing, cross-origin is refused at the route, revoke page offers Undo. Only the action is tested today. - Undo is reachable only from the confirmation page. Once you leave it, an ended link on /forges shows no Undo for the minute, so 'undoable for the first minute' is met only by luck. Show Undo on the row while the link is ended and revoked < 60s (expose revoked-at in LinkState or a field on ClientRow).
- Performance: clientRows now opens every child's forge.db synchronously on each /forges render (linkStateIn). That is N sqlite opens on the hot path with a 50 ms budget. Read it off the heartbeat/ledger or cache it, and bound the work.
- Clean:
const { stored } = found ?? {}in restoreClaim is convoluted; the two UPDATE statements duplicate;Database as Sqliteimport duplicates the existing Database type import in claim.ts. Tidy. - tools/list budget (#6217): the new
actionenum pushed forge_view to page two and you changed that test to follow the cursor. Shrink the claim_link doc/schema so it stays on the page instead, or say plainly in the PR why not. - Design (principles 1, 8): the post-revoke page is a whole extra page for one sentence; a revoke on the row with an inline receipt would show the real row (principle 2). Prefer redirecting back to /forges with the receipt and Undo beside the row.
Any admin can revoke any child's link, not only its sender; confirm that is intended or restrict to the spawner.
A forge link now says when it ends, and its sender can revoke it in one tap and undo it for the first minute. This round adds the terminal door, an Undo on the row, a cheaper /forges render and the missing tests.
The tests I ran pass: claim-revoke, tui-forges, mcp-forge-view, mcp-tools, mcp-table, client-list, forges-* and the spawn and claim suites. tsc and bun scripts/gate.ts are clean. I did not run the whole suite; the forge's check does that.
Changes, in the order of the review
- Terminal. The forges screen in
src/tui/screens/forges.tsgainsl(end a link) andu(undo one), admin only. Each row sayslink openorlink ended, undo with u. Page, MCP and terminal all go through onechangeLinkinsrc/spawn/manage.ts, whichsrc/spawn/door.tswraps with the admin and same-origin checks. The how-to isdocs/features/forge-link-revoke.md. - Tests in
test/claim-revoke.test.tsandtest/tui-forges.test.ts:
- Undo is refused at 61 s and allowed at 59 s.
- A link that expired cannot be revoked or brought back, and neither can a used one.
- On the web routes, a member and a guest get 404, a cross-origin post is refused, the revoke redirect shows the receipt, and the row offers Undo.
- The MCP revoke/undo and the terminal's changeLink are covered.
- Undo on the row. An ended link revoked less than 60 s ago shows Undo on
/forges, so it survives leaving the page.LinkInfonow carriesrevokedAtandundoable()decides. - Performance.
clientRowsno longer opens any child database. The child's/api/v1/forgeanswer now includeslink, only for the parent's secret, and the list reads it from the cached probe it already makes. A revoke or undo drops that cache entry, so the next list asks again. A child running older code reports no link and shows no Revoke button. - Tidy. The duplicate sqlite import is gone and a single
swapreplaces the two UPDATE statements.restoreClaimno longer usesfound ?? {}.changeClaimis removed fromsrc/actions/spawn.ts. - MCP budget. I shortened the
claim_linkdescription and the schema, soforge_viewstays on page one. I reverted themcp-forge-viewtest to its original form. The margin was under 20 bytes, so the next tool added will hit it again. - Post-revoke page. It is replaced by a 303 back to
/forges?link=revoked&forge=<name>, with an inline receipt. The receipt is fixed words and only appears for a forge in the list.
Not changed: any admin can revoke any child's link, not just its spawner. This matches re-issuing a link, which is already any admin's, so I left it. Say if you want it restricted to the spawner.
To try it: as an admin, open /forges and press Revoke on a forge with an open link. Undo sits beside the row for a minute. In the terminal, open Forges you sent and press l, then u. Over MCP, call claim_link with {name, action: "revoke"} or "undo".
Earlier asks are met. The terminal door is there (l and u on Forges you sent, via the shared changeLink) and its how-to is docs/features/forge-link-revoke.md. README is untouched. Undo is on the row and survives leaving the page. The list no longer opens child databases. Tests cover the 59 s and 61 s edges, an expired or used link, the member and guest 404s, cross-origin refusal, MCP and the terminal action. Security reads clean: link state in /api/v1/forge is returned only after the parent-secret check, both web routes are admin-only and same-origin checked, the receipt uses fixed words and only names a forge in the list, and no secret is in any event. The design uses badge, btn and row with no new component class. Non-blocking: the MCP tools/list margin is under 20 bytes, so the next tool added will hit it. Non-blocking: the TUI picker path itself is only tested for the empty case; the action is tested.
A sender can now revoke an open forge link and undo it within a minute. A revoked or expired link shows a plain "This link has ended" page with status 410 and sets no session.
The new
test/claim-revoke.test.tsand the MCP size and table tests pass, andbun x tsc --noEmitis clean. I ran the relatedforges*,mcp*,spawn-api,spawn-eventsand audience tests; the only two failures were the ones fixed below. I did not run the full suite.scripts/fast.tscalled the diff too broad to name its tests.What changed
src/spawn/claim.tsstores a revoked timestamp (r) inside the existing claim setting. The link's secret is untouched and never logged.-
revokeClaimandrestoreClaimflip that timestamp. A restore only works within 60 seconds of the revoke, while the link is unspent and still in date. -linkStateInreads a child's state (open, used or ended) without writing to it.changeClaiminsrc/actions/spawn.tsis admin-only and same-origin checked. It writes aforge.claim_revokedorforge.claim_restoredevent whose payload holds only the forge name./forgesshows a state badge on each row, plus a Revoke button when the link is open.- Revoke and Undo go through
POST /forges/:name/revoke-linkand/undo-link, and the confirmation page carries the Undo button.claim_linktakesaction: "revoke" | "undo"instead of a new tool.docs/MCP.mdis regenerated, and no ceiling was raised.Tests
test/claim-revoke.test.tscovers revoke-then-use (410 on both/claim/optionsand/claim, noset-cookie) and use-then-revoke (refused). It also checks that a cross-origin revoke is refused, and that undo works and the receipts omit the secret.link: nullto hand-built rows intest/forges-attention.test.tsandtest/tui-forges.test.ts, so they type-check with the newClientRow.linkfield.test/mcp-forge-view.test.tsread only the first page oftools/list. Myactionenum pushedforge_viewonto page two. That test was wrong, so it now followsnextCursor.Not done: a revoked link cannot be revived after the minute; send a fresh link instead. There is no new MCP tool.
To try: open
/forgesas an admin and press Revoke beside an open link. Open the link in another browser to see the 410 page, then press Undo within a minute.Closes #10725