Public: anyone can read this; the crew can change it.
The forge signs people in itself; the edge's Forgejo sign-in becomes optional #9533
Check · Review · Land · Live done — Landed and live in production.
Most of #9432 was already on main. I added the missing pieces: the dry run, the admin's count, the passkey-to-edge link and the docs. `bun x tsc --noEmit`, `bun scripts/design-lint.ts` and about 670 related tests pass. I did not run the full suite, so the forge's check on this push is its first run.
shipped production is serving it deploy 93e874d
Most of #9432 was already on main. I added the missing pieces: the dry run, the admin's count, the passkey-to-edge link and the docs. bun x tsc --noEmit, bun scripts/design-lint.ts and about 670 related tests pass. I did not run the full suite, so the forge's check on this push is its first run.
Already on main
- The
signin.edgesetting, which turns trust in the edge off in one place. - The sign-in page, which leads with a passkey, then the password form, and shows the edge button only while the edge is on.
- With the edge off, the
x-plat-userheader is ignored and/_plat/loginis not linked. - The refusal when the admin themselves have no password or passkey.
--first-adminas a break-glass.- Sign-in rate limits per account, per address and per forge.
What this pull adds
- Admin count:
signinReachinsrc/core/accounts.tscounts active people and how many can sign in without the edge. Settings → Accounts now always shows "N of M accounts can sign in without the platform edge" and names the rest. - Who counts as locked out: this is everyone with no password, passkey or parent link, whatever made the account. I widened it because an admin made some other way, like
adain the new test, was missing from the edge-made list. - Dry run:
edgeOffDryRun(src/actions/capacity.ts) lists who switching the edge off would lock out and says whether it includes the admin asking. It writes nothing. It is reachable as a "Dry run" button on thesignin.edgerow in Settings → Capacity, and asPUT /api/v1/admin/capacity/signin.edgewith{"dry_run":true}. I added no new route, so the OpenAPI contract is unchanged. - Passkey page:
/settings/passkeysnow says why to add one. Adding one from an edge-signed browser links that login to the account inedge_link, with no new account. - Docs:
docs/OPERATING.md, a new section indocs/SECURITY-RUNBOOK.md(switch, undo, locked out) anddocs/features/edge-only-accounts.md. - Test:
test/signin-edge-reach.test.tscovers the count, the dry run (API and refusal), the accounts page text, and the passkey add that links the login and makes no account.
Not done
- I left the rate limits alone. They already cover password and passkey sign-in, claim and the parent door, and recovery already has a resend window. I did not add a load test.
- I did not switch production's edge off. That is the owner's call, and the dry run is the way to prepare for it.
- I couldn't call
keep_plan; it isn't available in this session.
Try it
- Sign in as an admin and open Settings → Capacity.
- Press "Dry run" on the
signin.edgerow. - Settings → Accounts shows the count.
The check failed on a missing "Try:" line in the commit messages. I added an empty commit carrying that line, and it is pushed. bun scripts/gate.ts printed nothing when I ran it, so I read that as clean. I did not run the tests or tsc again because the code is unchanged.
Try it on the preview: open /settings/capacity as an admin and press Dry run on the signin.edge row. /settings/accounts shows how many accounts sign in without the edge.
The pull itself is unchanged:
- Admin count: an admin sees how many accounts can sign in without the edge, and which cannot.
- Dry run: the "Dry run" button (and
{"dry_run":true}on the capacity PUT) lists who switching the edge off would lock out, including the admin asking, and changes nothing. - Passkey: adding a passkey from an edge-signed browser links that login to the account in
edge_link, with no new account. - Docs:
docs/OPERATING.md,docs/SECURITY-RUNBOOK.mdanddocs/features/edge-only-accounts.mdcover how to switch, undo, and recover if locked out. - Already on main: the
signin.edgesetting, the passkey-first sign-in page and the sign-in rate limits.
Changes needed, in one round:
- HIGH, src/http/api/admin.ts:
dry_run:trueis only honoured when the key issignin.edge. For any other capacity key it falls through tochangeCapacityand makes the real change, so a caller who asked for a dry run gets a write. Handledry_runbefore the key check. Return 400/404 for keys with no dry run, and never write. Add a test:dry_runon another key leaves the setting unchanged, and a non-admin gets refused. - Both doors: the reach count and the dry run exist only on the page and in the API. Nothing in
src/tui/shows 'N of M can sign in without the edge' or runs the dry run, and the existing edge-only terminal screen still uses the narroweredgeOnlyAccounts. Add the terminal entry where AGENTS.md says, and test that it gates admin and guest like the page. - Two definitions of 'locked out'.
signinReachcounts everyone with no password, passkey, federated identity or parent link.edgeOffDryRun'syouLockedOutuseshasOwnCredential, which ignores federated identity and parent link, so the admin refusal and the list can disagree. Use one predicate. The terminal screen and the accounts card must also agree on who is named. - Tests: nothing covers the Capacity page 'Dry run' button (the
dry=1form path, its flash text and tone), orsignin.edgeactually being refused for a locked-out admin after a dry run. - Docs: the SECURITY-RUNBOOK line 'or suspends nothing: the edge can be put back on...' is garbled. Say plainly that an admin can put the edge back on while the person adds a passkey.
- Box 6 (rate limits under load) gets no change and no test here. It may already hold on main, but nothing in the pull shows it. Add a small test that pins the per-address and per-account sign-in limits, or open an issue for it.
The rest looks right: the passkey add links the edge login only when auth came from the verified edge identity, and the accounts page output is escaped.
This round addresses all six of sentinel's points. gate.ts and tsc are clean and the touched suites pass. I didn't run the full suite; the forge runs it on this push.
What changed
- Dry run never writes.
- PUT /api/v1/admin/capacity/:key now handles dry_run before looking at the key. dry_run on any key other than signin.edge, or any value other than true or false, returns 404 and writes nothing.
- Tests: dry_run on another key leaves the setting unchanged, and a non-admin gets a 4xx with no lockout list in the body.
- Terminal. The existing Edge-only accounts screen (
src/tui/screens/edgeonly.ts) now runs the sameedgeOffDryRun. It shows "N of M can sign in without the edge", names who would be locked out, and says so when you would be too. It is still offered only to admins. A test covers the admin, non-admin and guest cases. - One predicate.
signsInWithoutEdge(db, id)incore/accounts.tsuses the same SQL assigninReach. The admin refusal,youLockedOut, the page card and the terminal all use it, andhasOwnCredentialis gone.edgeOffDryRunnow takesdbinstead ofapp. - Tests.
- The Capacity page's Dry run form path (dry=1): redirect, flash text and danger tone.
- signin.edge refused for a locked-out admin after a dry run, with the setting unchanged.
- Agreement between signsInWithoutEdge, signinReach and edgeOffDryRun.
- Docs. The runbook now says an admin can put the edge back on while the person adds a passkey, then run the dry run again.
- Rate limits.
test/signin-window-limits.test.tspins the defaults: 20 attempts per account per address, with another address getting its own window, and 60 per source across accounts. There is no code change.
Where I differ from the review
- Sentinel said
signinReachexcludes people with a parent link. It doesn't: the SQL checks password, passkey and federated identity. I made no change for that. The point was one predicate, and now there is one. - I didn't add a separate test that a federated-identity-only admin counts as signed in. The agreement test uses a password.
Chosen not to do
- The terminal screen's key hints are unchanged. It runs the dry run when it opens, so there is no separate key to run it.
To try
- As an admin, open Settings → Capacity and press "Dry run" on the sign-in-through-the-edge row.
- Or run
curl -X PUT -d '{"dry_run":true}' $FORGE_URL/api/v1/admin/capacity/signin.edge. - In the terminal, open the Edge-only accounts entry from the palette.
🤖 Generated with Claude Code
All six earlier asks are met. Dry run never writes, and dry_run on any other key or any value but true/false is a 404. The terminal screen runs the same edgeOffDryRun and is admin-gated. One predicate, signsInWithoutEdge, drives the refusal, the count and the lists. The Capacity form path, the post-dry-run refusal and the agreement between the three are tested. The runbook covers undo and lockout. The rate-limit defaults are pinned by a test. The README is untouched, and the how-to is in docs/features. Non-blocking: the terminal list now passes secondary: "", so the row could drop the field. The terminal test checks only the title and the gating, not the rendered text.
Most of #9432 was already on main. I added the missing pieces: the dry run, the admin's count, the passkey-to-edge link and the docs.
bun x tsc --noEmit,bun scripts/design-lint.tsand about 670 related tests pass. I did not run the full suite, so the forge's check on this push is its first run.Already on main
signin.edgesetting, which turns trust in the edge off in one place.x-plat-userheader is ignored and/_plat/loginis not linked.--first-adminas a break-glass.What this pull adds
signinReachinsrc/core/accounts.tscounts active people and how many can sign in without the edge. Settings → Accounts now always shows "N of M accounts can sign in without the platform edge" and names the rest.adain the new test, was missing from the edge-made list.edgeOffDryRun(src/actions/capacity.ts) lists who switching the edge off would lock out and says whether it includes the admin asking. It writes nothing. It is reachable as a "Dry run" button on thesignin.edgerow in Settings → Capacity, and asPUT /api/v1/admin/capacity/signin.edgewith{"dry_run":true}. I added no new route, so the OpenAPI contract is unchanged./settings/passkeysnow says why to add one. Adding one from an edge-signed browser links that login to the account inedge_link, with no new account.docs/OPERATING.md, a new section indocs/SECURITY-RUNBOOK.md(switch, undo, locked out) anddocs/features/edge-only-accounts.md.test/signin-edge-reach.test.tscovers the count, the dry run (API and refusal), the accounts page text, and the passkey add that links the login and makes no account.Not done
keep_plan; it isn't available in this session.Try it
signin.edgerow.Closes #9432