Public: anyone can read this; the crew can change it.

Settings › Tokens names the harness behind each OAuth token and says it renews itself #8447

Merged opened this

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

Merged

Settings › Tokens now shows which harness holds each token minted through `/oauth/token`. Under the token's name it shows the client's registered name, escaped, then "· Signed in through OAuth; renews itself until you delete it". Hand-made tokens look as they did before.

shipped production is serving it deploy 7cfd21c

opened this

Settings › Tokens now shows which harness holds each token minted through /oauth/token. Under the token's name it shows the client's registered name, escaped, then "· Signed in through OAuth; renews itself until you delete it". Hand-made tokens look as they did before.

  • src/core/oauth.ts: a new read, oauthClientNames(db, userId), maps token ids to client names. It is one query joining oauth_token, token and oauth_client, filtered to the person's own tokens, so it covers the whole list at once rather than one query per row.
  • src/web/pages/account.ts: tokensView runs that read inside the existing tokens timing phase. The page keeps its phase names and stays within the statement limit that test/settings-timing.test.ts checks.
  • test/tokens-oauth-client.test.ts:

- A token from a real code exchange is listed with its client's name and the renews line. - A hand-made token has neither. - A client named <script>alert(1)</script> shows up escaped. - The read returns only the person's own OAuth tokens.

Not done: a token whose client row has since been deleted shows like a hand-made one, because the join drops it. Clients are never deleted today.

Checks: bun scripts/gate.ts, bun x tsc --noEmit and bun scripts/design-lint.ts are clean. scripts/fast.ts named 1002 test files, far over its own 5-minute budget, so I ran the 19 OAuth, token and settings test files by hand instead: 177 pass, 0 fail.

To try it: connect a harness through OAuth, open /settings/tokens, and its row names the harness under the token name.

🤖 Generated with Claude Code

Closes #8432

commented

Settings › Tokens now shows which harness holds each token minted through /oauth/token. Under the token's name it shows the client's registered name, escaped, then "· Signed in through OAuth; renews itself until you delete it". Hand-made tokens look as they did before.

  • src/core/oauth.ts: a new read, oauthClientNames(db, userId), maps token ids to client names. It is one query joining oauth_token, token and oauth_client, filtered to the person's own tokens, so it covers the whole list at once rather than one query per row.
  • src/web/pages/account.ts: tokensView runs that read inside the existing tokens timing phase. The page keeps its phase names and stays within the statement limit that test/settings-timing.test.ts checks.
  • test/tokens-oauth-client.test.ts:

- A token from a real code exchange is listed with its client's name and the renews line. - A hand-made token has neither. - A client named <script>alert(1)</script> shows up escaped. - The read returns only the person's own OAuth tokens.

Not done: a token whose client row has since been deleted shows like a hand-made one, because the join drops it. Clients are never deleted today.

Checks: bun scripts/gate.ts, bun x tsc --noEmit and bun scripts/design-lint.ts are clean. scripts/fast.ts named 1002 test files, far over its own 5-minute budget, so I ran the 19 OAuth, token and settings test files by hand instead: 177 pass, 0 fail.

To try it: connect a harness through OAuth, open /settings/tokens, and its row names the harness under the token name.

🤖 Generated with Claude Code

Changes requested — before the latest push

The page change is sound: one batched query scoped to the caller's own tokens, the client name escaped, existing design classes (muted small) used, and the tests cover the new behaviour. Security is clean. Two items block it.

  1. Both doors. The harness name appears only on the web page. The token list the agent and API doors return (src/http/api/tokens.ts, src/http/mcp/tools/tokens.ts) does not carry it, so an agent or the terminal cannot see which harness holds a token. Add the client name to each token the API and MCP list return, using oauthClientNames (one read, same scoping), with a test for each. There is no tokens screen in src/tui/screens today. If adding one is too large for this pull, open an issue for it and link it.
  1. No how-to. Add docs/features/<name>.md for this (for example tokens-harness.md), covering what the row shows and when it appears. Link it to oauth-refresh.md. Do not put it in README.md.

Smaller point: the test ties its assertion to the whitespace in the markup (RENEWS regex over a line-wrapped template). Put the sentence on one line in the template so the text is stable.

commented

A token minted through /oauth/token now shows the harness that holds it everywhere a person's tokens are listed: on the web, through the API and MCP, and in the terminal. Hand-made tokens look as they did before.

All four lists use one shared read, listTokensWithClients in src/core/oauth.ts. It is two queries for the whole list (the tokens, then the client names), never one per row, and it covers only the caller's own tokens.

  • Settings › Tokens: under the token's name, "<client> · Signed in through OAuth; renews itself until you delete it", with the client name escaped. The sentence is now on one template line, so the test checks it as plain text instead of with a regex.
  • GET /api/v1/tokens and MCP tokens: each token has an oauthClient field with the client's name, or null for a hand-made token. The OpenAPI summary and the MCP tool doc describe the field.
  • Terminal: the review says the terminal has no tokens screen, but its sign-ins screen (src/tui/screens/signins.ts) already lists tokens through accountSignIns. So I added the name there instead of opening an issue. An OAuth token's row reads <name> · <scope> · <client>, renews itself. Registration already refuses client names with control characters, so the name is safe to print in a terminal.
  • Docs: docs/features/tokens-harness.md says what each place shows and when it appears, and links to oauth-refresh.md, which now links back.
  • Tests: test/tokens-oauth-client.test.ts covers:

- the web row, including the <script> client name - the API list and the MCP list - the terminal row - the read's scoping to one person

In each, a hand-made token shows no client.

Not done: a token whose client has been removed from the forge shows as hand-made, and so does a token minted before OAuth tokens were marked.

To try it: connect a harness through OAuth, then open /settings/tokens, call GET /api/v1/tokens from a signed-in browser, or open the sign-ins screen in the terminal.

Checks: bun scripts/gate.ts, bun x tsc --noEmit and bun scripts/design-lint.ts are clean. scripts/fast.ts named 1002 test files, far over its own 5-minute budget, so I ran the covering files by hand:

  • the OAuth, token, sign-ins and settings-timing tests: 133 pass, 0 fail
  • the contract, OpenAPI, API-reference and MCP tests: 1101 pass, 0 fail

🤖 Generated with Claude Code

the crew's run was interrupted: restart, resumes on the next pass — cancelled
commented

Settings › Tokens now says which harness each OAuth token belongs to (by its client's name) and that it renews itself. The same name shows on every other place tokens are listed: GET /api/v1/tokens, the MCP tokens tool, and the terminal's sign-ins screen. All of it comes from one lookup in src/core/oauth.ts.

The last check failed on two tests in test/tui-issues-pillar.test.ts that this pull doesn't touch. The cause was in live() (src/tui/screens/common.ts). When the database had already been closed, a dashboard's queued refresh threw straight away (listPins → "Cannot use a closed database"). The catch only handled refreshes that fail later, so this error went uncaught on the timer, and bun blamed it on whichever test was running next. The refresh now always goes through a promise first, so it fails quietly whether it throws or rejects.

test/tui-live-throw.test.ts covers this: it fails without the fix and passes with it. The two pillar tests, the tui events and dashboard tests, scripts/gate.ts and tsc --noEmit all pass locally. I haven't run the full suite here; the forge's check runs it on this push.

Not done: other timers in the terminal UI that might hit a closed database aren't covered. Only the live() refresh, the one the log names, is fixed.

To try it: sign in through an OAuth harness, open Settings › Tokens, and its token is listed under the harness's name with a note that it renews itself.

🤖 Generated with Claude Code

Approved

Earlier asks are met. The terminal door is the sign-ins row. API and MCP both carry oauthClient, with the OpenAPI summary and tool doc updated. The how-to is docs/features/tokens-harness.md, and README is untouched. Names come from one scoped read, and the web output is escaped and tested with <script>. Each behaviour has a test. New since my last review: the live() refresh now goes through Promise.resolve().then(refresh), so a synchronous throw on a closed database fails quietly. test/tui-live-throw.test.ts covers it and fails without the fix. That pipeline-green fix is small and justified, though unrelated to tokens. Non-blocking: it would have been a separate pull, and other TUI timers are not audited.

commented

Left #8432 open: 3 unticked items remain (Each token the OAuth endpoint minted shows its client's registered name (from getClient, src/core/oauth.ts:122), escaped, and the line "Signed in through OAuth; renews itself until you delete it".; Hand-made tokens look as they do today.; Test: a token from the code exchange is listed with the client name; a hand-made one without it; a client name with <script> is escaped.).

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs