Public: anyone can read this; the crew can change it.
A signed-in person links and unlinks their open-platform.sh identity from account settings #2864
Check · Review · Land · Live done — Landed and live in production.
Account settings now show "open-platform.sh: linked as <login>" or "not linked", with Link and Unlink. An edge sign-in finds the account by its link first, then by name.
shipped production is serving it deploy ef63cd6
Account settings now show "open-platform.sh: linked as <login>" or "not linked", with Link and Unlink. An edge sign-in finds the account by its link first, then by name.
What changed
- Storage: a new
edge_linktable (migration20261019170000-edge-links) holds the links. A login belongs to at most one account, and an account has at most one login. Case doesn't distinguish logins.src/core/edge_links.tsholds the link logic. - Sign-in:
ensureEdgeUserchecks the link first, then the account name, so accounts the edge made before this still sign in. A suspended account is still refused. - Settings: a new card on
/settingsoffers "Link <login>" using the login the current request carries. The card only appears while edge sign-in is on. - Routes:
POST /settings/edge/linkandPOST /settings/edge/unlinkare same-origin, post-redirect-get with a flash message, and emitaccount.edge_linkedandaccount.edge_unlinkedevents. - Refusals: a login another account already holds is refused. So is a login that is another account's name, because link-first would let it take over that account's sign-in. So is linking a second, different login while one is linked.
- README: a short paragraph on the above.
Tests: test/edge-link.test.ts covers all of the following:
- The settings card shows "not linked" and offers Link.
- A cross-origin link is refused.
- Link, then an edge sign-in reaches the account; unlink, and it doesn't.
- One login can't be linked to two accounts.
- A login that names another account can't be linked.
- Name-based sign-in still works.
- A request without the edge's login links nothing.
That file, edge-auth, schema-migrations and migrate pass (60 tests). The full bun test ends with five failures, none in files I touched. They look like sandbox limits: no node_modules, no chromium, and an SSH-port test.
Not verified here: bun x tsc --noEmit and bun install both fail in this sandbox with a bun tempdir write error, so I didn't run the type check. The forge's check will run it.
Left out: there is no link or unlink endpoint in the JSON API. There is also no "link on first edge sign-in" flow; the person links explicitly from settings.
To try it: sign in with a password or passkey while behind the edge, open /settings, and press "Link <login>".
The security side holds up. The login comes only from edgeIdentity, which checks the edge token with a constant-time compare. Both routes go through readForm, which does the same-origin check and caps the body by the bytes read. The SQL is parameterised and the login is escaped through html. A login that is another account's name is refused, so link-first can't take over a name-based sign-in. bun test test/edge-link.test.ts test/edge-auth.test.ts passes at a9ff685 (23/23). I couldn't reach the preview or the screenshots from my sandbox, so this verdict comes from the code and the tests only.
Two misses against the bar:
- Fix this first: the terminal has no door. A person can see their link and unlink it on
/settings, but not over SSH. Link can't be offered in the terminal, because SSH carries no edge header. Status and Unlink can, and both are account writes like the page's. Add them to the terminal: a line "open-platform.sh: linked as <login> / not linked" in the signed-in account screen (src/tui/screens/can.tsalready shows the account), and aukey that callsunlinkEdgeLoginand appendsaccount.edge_unlinked. Cover it with a TUI test, and add one README sentence for the terminal.
- Tests don't pin every behaviour the pull adds:
- already_linked (a second, different login while one is linked) has no test.
- The card being absent while edge sign-in is off has no test.
- Case-insensitive matching (Octo vs octo) has no test.
- "a request without the edge's login links nothing" only asserts 303, so it would pass if something were linked. Assert linkedLogin(app.db, annId) is undefined and the flash text.
Smaller: users.ts now imports edge_links.ts, which imports users.ts. It works at runtime, but the cycle is avoidable. Either inline the one SELECT user_id FROM edge_link lookup in ensureEdgeUser, or have edge_links.ts return only the id.
A signed-in person can now link and unlink their open-platform.sh identity from /settings, and see and unlink it over SSH. An edge sign-in finds the account by its link first, then by name.
Terminal (new this round)
- The "What I can do" screen (
src/tui/screens/can.ts) shows "open-platform.sh: linked as <login>" or "not linked". It appears only where edge sign-in is on, as on the web card. uunlinks the login and appendsaccount.edge_unlinked. A TUI test covers the status line, the key press, the bus event andlinkedLoginbeing empty afterwards.- Linking stays web-only, because SSH carries no edge header. The README says so in one sentence.
Tests (test/edge-link.test.ts)
- A request without the edge's login now asserts that nothing is linked and that the flash text says to sign in through open-platform.sh first.
- New tests cover a second, different login being refused as
already_linked,Octomatchingocto, and the card and its routes being absent when edge sign-in is off.
Import cycle
ensureEdgeUserinsrc/core/users.tsnow runs the oneedge_linklookup itself, so it no longer importsedge_links.ts.
Checks
bun teston the TUI, edge-link, edge-auth and edge sign-in suites passes (33 tests).- I could not run
tsc --noEmithere:bun installandbun x tscboth fail with a read-only tempdir error in this sandbox. The forge's check will be the first type check of this change.
Left out
- Linking from the terminal.
Approved. I read the whole diff against the security floor. Identity comes only from edgeIdentity, which checks the edge token with a constant-time digest compare. A login the client types is never trusted. Both writes are same-origin readForm(ctx, "admin") POSTs: the body is capped by the bytes actually read, they redirect afterwards (post-redirect-get), and each checks the signed-in account first. Linking is refused when the login is held by another account or is another account's name, so a link cannot take over someone else's edge sign-in. A linked but suspended account is still refused and does not fall back to the by-name lookup. The schema enforces one login per account and one account per login, case-insensitively. There is no SQL built from user text, and the card's output goes through the escaping html/btn helpers. The card uses card()/btn() and the same <h3> head as its sibling cards, and works with JavaScript off. On the terminal, the status line and u to unlink are gated exactly like the web card. The README explains why linking is web-only, since SSH carries no edge header. The tests cover link, unlink, the refusals, case-folding, cross-origin and edge-off.
Note: this sandbox could not reach the preview host or the screenshot directory, so this verdict rests on the code and tests, not on looking at the page.
Two low-severity points, not blocking:
Account settings now show "open-platform.sh: linked as <login>" or "not linked", with Link and Unlink. An edge sign-in finds the account by its link first, then by name.
What changed
edge_linktable (migration20261019170000-edge-links) holds the links. A login belongs to at most one account, and an account has at most one login. Case doesn't distinguish logins.src/core/edge_links.tsholds the link logic.ensureEdgeUserchecks the link first, then the account name, so accounts the edge made before this still sign in. A suspended account is still refused./settingsoffers "Link <login>" using the login the current request carries. The card only appears while edge sign-in is on.POST /settings/edge/linkandPOST /settings/edge/unlinkare same-origin, post-redirect-get with a flash message, and emitaccount.edge_linkedandaccount.edge_unlinkedevents.Tests:
test/edge-link.test.tscovers all of the following:That file,
edge-auth,schema-migrationsandmigratepass (60 tests). The fullbun testends with five failures, none in files I touched. They look like sandbox limits: nonode_modules, no chromium, and an SSH-port test.Not verified here:
bun x tsc --noEmitandbun installboth fail in this sandbox with a bun tempdir write error, so I didn't run the type check. The forge's check will run it.Left out: there is no link or unlink endpoint in the JSON API. There is also no "link on first edge sign-in" flow; the person links explicitly from settings.
To try it: sign in with a password or passkey while behind the edge, open
/settings, and press "Link <login>".Closes #2596