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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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_link table (migration 20261019170000-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.ts holds the link logic.
  • Sign-in: ensureEdgeUser checks 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 /settings offers "Link <login>" using the login the current request carries. The card only appears while edge sign-in is on.
  • Routes: POST /settings/edge/link and POST /settings/edge/unlink are same-origin, post-redirect-get with a flash message, and emit account.edge_linked and account.edge_unlinked events.
  • 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>".

Closes #2596

commented

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_link table (migration 20261019170000-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.ts holds the link logic.
  • Sign-in: ensureEdgeUser checks 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 /settings offers "Link <login>" using the login the current request carries. The card only appears while edge sign-in is on.
  • Routes: POST /settings/edge/link and POST /settings/edge/unlink are same-origin, post-redirect-get with a flash message, and emit account.edge_linked and account.edge_unlinked events.
  • 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>".

Changes requested — before the latest push

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:

  1. 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.ts already shows the account), and a u key that calls unlinkEdgeLogin and appends account.edge_unlinked. Cover it with a TUI test, and add one README sentence for the terminal.
  1. 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.

commented

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.
  • u unlinks the login and appends account.edge_unlinked. A TUI test covers the status line, the key press, the bus event and linkedLogin being 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, Octo matching octo, and the card and its routes being absent when edge sign-in is off.

Import cycle

  • ensureEdgeUser in src/core/users.ts now runs the one edge_link lookup itself, so it no longer imports edge_links.ts.

Checks

  • bun test on the TUI, edge-link, edge-auth and edge sign-in suites passes (33 tests).
  • I could not run tsc --noEmit here: bun install and bun x tsc both 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

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:

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs