Skip to content

List and revoke paired browsers from Pocket - #1528

Closed
nedtwigg wants to merge 11 commits into
mainfrom
issue-331-stage2-pocket-devices
Closed

nedtwigg wants to merge 11 commits into
mainfrom
issue-331-stage2-pocket-devices

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Stage 2 of the design merged in #1473, stacked on #1527: from Pocket, a person can see the browsers paired with each online Burrow and revoke one. Its base is issue-331-stage1-passkey-revocation; it retargets main when #1527 merges.

Wire. Protocol-v1 gains devices.list and devices.revoke, inside the Noise session. The Relay sees only ciphertext: no record's label, deliveryId, approval time, or deviceIdFor id reaches it. One-time sessions get neither verb. An older Burrow or Relay reads as "too old", never as "no devices".

Listing takes no prompt. Any paired session can list, a window ride included.

Revoking takes a fresh presence proof. A new session presence binding carries no target, and the Burrow remembers which id each challenge answers. Neither a window ride nor the connection's own proof counts. Revocation goes by deviceIdFor id through BurrowService.revoke under requester pocket, which the service sets; a Client can't name it.

UI. Burrow rows gain "Paired browsers", and wall Settings show the same list on opening. Revoking your own browser is answered before the goodbye, and the row then offers Pair again.

Specs. The stage is promoted, and "Account-wide devices" is deleted from ## Future since nothing is left. New rules S302-S304, and a new e2e-lint rule with self-tests. remote-api.md's terminal-only scope now names these verbs.

Verified on this head: test:audit (197), test:checks, the specs, md, e2e, outbound and public-docs lints, tsc in lib, standalone, vscode-ext and hosted, remote-lib-common (363), the Relay reauth, e2e-ceremony and malicious-relay tests (30), lib's src/remote, src/host/remote and src/components suites (3504, no unhandled errors), and these stories on Chromium through the Argos config. Before the list change: Hosted relay.test.ts, lint:deploy and lint:workflows. Every mutation went red: 35 before the list change, 10 on the replacement, and 7 on the ended-session fix, the five survivors resolved with one line deleted and four tests added or tightened. Not verified: a live app, a real Relay with a phone, or WebKit stories.

iPhone checklist, required before shipping (Home Screen install and Safari tab):

  1. Row "Paired browsers" after a window ride: the list opens with no passkey prompt.
  2. Settings: the list shows on opening, with no prompt.
  3. Revoke another browser: the passkey prompt appears on confirm, with activation surviving the round trips before it; its session ends, and the list refreshes.
  4. Revoke this browser: returns to Burrows with the message and Pair again.
  5. Dismiss the prompt: nothing changes, and fixed copy shows.
  6. Keepalives and the session survive a slow Face ID sheet.
  7. Layout at small widths, and row touch targets.

Deploy: either order is safe; the verbs are unavailable until both ends are new. Relay and Pocket first, for self-host and Hosted, then Burrows.

Argos: new Pocket/PairedBrowsers stories, InSettings now showing the list rather than a button, and Pocket/BurrowsView moves because rows gain "Paired browsers".

Closes #331 together with #1527.

🤖 Generated with Claude Code

nedtwigg and others added 8 commits November 2, 2026 18:15
…roof

Stage 2 of #331, the Burrow half. A paired session may list the ACL's records
and revoke one by its device id, inside the Noise session, so the Relay holds
no device directory.

A revoke always needs a proof over a challenge that request was just answered
with, in a new `session` presence binding, verified by the Burrow's one verifier
against its own state and the session's own record. The list needs a proof the
session has shown, at its connection or since, so a window ride shows one first.
Revocations go through the service under a `pocket` requester no Client and no
command can claim. A one-time session gets no port, so the verbs are the unknown
method they are on an older Burrow, and hello says which is which.

A session revoking itself is answered before the revocation runs, since the
goodbye ends it and nothing after that is read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each paired, online Burrow row reaches its list on a session of its own, with no
wall behind it, and the wall's Settings reach the same panel on the session they
have. Every action starts on a tap, since each may raise the passkey prompt, and
the confirmation is inline: this is touch UI.

A Burrow older than the verbs, and a Relay older than the binding, each read as
what they are, never as no devices. A browser revoking its own pairing drops its
authorization as an authenticated `pairing-required` does, keeping the pin, and
its row then offers Pair again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 2 of #331 is built, so "Account-wide devices" leaves `## Future` for
"Revocation from Pocket", and remote-api's stage 10 for "Paired browsers", where
protocol-v1's scope rule now names them. New audited rules S302-S304 sit under
Trust boundary, with the e2e-lint rule that holds the one-time phone to no verb
and a self-test for each spelling. security.md states the limit: the prompt
guards Pocket's Revoke, not an attached terminal's `dor remote revoke`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ttings

Mutation found three lines nothing held. Clearing the session's challenge on
spend was unobservable, since consuming it is the single use, so it goes. Voiding
an older challenge only bounds the issuer, and resolving the id again at the
revoke only matters against another revocation: each now has a test.

Hosted's reauth routes get the `session` binding's case beside the self-host
Relay's, and the revoke guarantee names Pocket.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Beside a Revoke button, Last seen was the part cut off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Wrapping hello in an async method put its answer a turn behind a later request's,
which the direct path's drain test reads as out of order. Read the flag off the
core's own promise instead. A panel test's key generation also ran inside a mock
the screen settles on, so it failed under load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t-devices

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: df06ce2
Status: ✅  Deploy successful!
Preview URL: https://8cf36a49.mouseterm.pages.dev
Branch Preview URL: https://issue-331-stage2-pocket-devi.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new devices.* declarations were inserted between ConnectResult's doc comment and its type. As a result, DEVICE_REFUSAL_MESSAGES now carries two stacked JSDoc blocks, and ConnectResult lost its doc, so hovers show the wrong text for both. The two suggestions below move the comment back.

Comment thread lib/src/remote/client/pocket-client.ts Resolved
Comment thread lib/src/remote/client/pocket-client.ts Resolved
nedtwigg and others added 2 commits November 2, 2026 19:32
Ned's decision on #1528: a window ride lists with no prompt. An authorized
session reads the same through `dor remote list` in an attached terminal, so a
prompt there would cost a tap and protect nothing. Revoking still takes a fresh
proof, never a window ride or the connection's.

With no prompt to wait on a tap for, the panel reads the list on opening, in
Settings as on a Burrow's row, and offers Try again only after a failure. The
`presence-required` answer and a list's proof param leave the wire, since nothing
ever sent them.

Also moves the `devices.*` declarations out from under `ConnectResult`'s doc
comment, which had left `DEVICE_REFUSAL_MESSAGES` with two and `ConnectResult`
with none; a scan of this branch's source finds no other stacked doc comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ned is removing `dor remote` before it ships, in its own PR, so the security.md
gap about typing `dor remote revoke` closes with it, and the rationale gives the
list's reason on its own terms. Also pins why each proof is refused and that a
malformed list reads as the Burrow failing, which mutation found nothing held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from issue-331-stage1-passkey-revocation to main November 3, 2026 03:58
@nedtwigg
nedtwigg marked this pull request as ready for review November 3, 2026 03:58

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the new standalone "Paired browsers" screen (phase.at === 'devices'), nothing watches the session the way the wall's adapter does. If the session ends while the screen is open (Burrow restart, Relay drop, the reaper), #core.establishedRoute becomes null, so #servesDevices() is false. The next "Try again" or Revoke then answers DEVICES_UNSUPPORTED_MESSAGE. listDevices also sets unsupported: true, which switches the panel to its permanent "too old" view. The person is told to update a Dormouse that is current, when reconnecting would fix it. The suggestions below report an ended session as ended.

Comment thread lib/src/remote/client/pocket-client.ts Resolved
Comment thread lib/src/remote/client/pocket-client.ts Resolved
`#servesDevices` answered one question for two facts: no session, and a session
whose hello said no `devices`. So a list or revoke after a session ended read
"too old", and the panel stuck on that view. Only a live session's own answered
hello now speaks for the Burrow; with none, either verb reports the connection
ended, which connecting again mends. A test had pinned the old reading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nedtwigg

nedtwigg commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Closing per the maintainer's decision: not building stage 2. #1527 covers the lost-phone case by revoking a removed passkey's pairings on every computer, and Settings revokes one browser on the computer itself. The branch stays if this is ever revisited.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

remote: no account-wide device list — pairing, revocation, and push targets are all per-Host

2 participants