Hosted enrollment from the desktop, and paired phones under Local networks and Anywhere - #872
Conversation
Deploying mouseterm with
|
| Latest commit: |
7ad70b3
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://8a68066f.mouseterm.pages.dev |
| Branch Preview URL: | https://remote-network-hosted-client.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
Three enrollment edge cases are inline. Some text outside the diff still describes Hosted's STUN as one-time-only, but after this PR, Pocket served by Hosted always gathers through Cloudflare STUN, Local networks included (deploymentDirectPeer → hostedDirectPeer):
docs/specs/security.md, the "Traffic analysis" bullet: "Cloudflare's STUN server sees the one-time phone's public address, and under Anywhere this computer's". That page is published at/security, so it now understates whose addresses Cloudflare sees. A paired phone in Hosted-served Pocket belongs there too.lib/src/remote/client/browser-direct-peer.ts: the doc comments onhostedDirectPeer("The one-time page's, which Hosted serves.") andselfHostDirectPeer("Pocket's, which a self-host Relay serves.") no longer hold.docs/specs/remote-api.md, the## Futuresentence "A paired phone's network levels follow the remote-network scope". This PR builds those levels, so the pointer reads as if they were still unbuilt.
I can push these doc fixes as a commit if you'd like.
The service begins and polls the device-code enrollment itself, holding the device code out of every webview, and composes the approval link at the fixed hosted.dormouse.sh (a dev Hosted build follows the Relay's checked link). The Phones section replaces the disabled Hosted button with the code, a link to approve, and the enrolled view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nswers Review fixes: the run stays redeeming until its save lands, a late redemption is adopted rather than dropped, a lost poll answer reads as redeemed from a marker the Relay keeps until expiry, a second window joins the waiting code instead of replacing it, a failed begin keeps the old code, and the account links follow one origin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Hosted build runs its persistent Burrow under both levels. Under Local networks a paired phone's session is direct-only: the outcome says so, Pocket waits for the switch before protocol-v1, and the Burrow ends the session unread on an application message over the Relay, a given-up attempt, or the deadline. Under Anywhere it may fall back to relaying through Hosted. Pocket served by Hosted gathers through Cloudflare STUN, read from a deployment file Hosted stages, and Settings lists the paired-phone connections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes: EstablishedE2eSession owns the direct-only rule both runtimes use, with a deadline of the phone's setup plus handoff so it cannot end an attempt the phone still counts live; Pocket's failure copy follows the cause and reports an ending once; stop() leaves no flush timer; the redeemed answer names the Burrow to remove; one deployment marker; and the built scope leaves the specs' Future sections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t an unanswered request could not reach
When the path ends a direct-only session under Local networks — a refused
pair, a given-up attempt, or the deadline once the phone offered — the shared
EstablishedE2eSession records { at, kind, address?, addressSource }: the
refused pair's remote end (observed), else the first public address the
phone's offer reported (a diagnostic that decides nothing). The goodbye now
carries reason network-not-allowed with that address, so Pocket and the
one-time page show one sentence naming it; the laptop shows the same record
in Settings → Network, with Dismiss, and in the one-time ending. The Burrow
service's fetch now names the host and why a request got no answer instead
of a bare "fetch failed".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Burrow closed 4001 (removed) or 4002 (Hosted, owner no longer entitled) now latches `removed` / `not-entitled` instead of reconnecting forever, and a refused upgrade earns one standing probe of GET /api/push/devices per failure streak. Settings names the state, offering Enroll again (Hosted removal) or Reconnect (not entitled), and lists no relay connection while latched; Pocket marks a record the Relay's list no longer names as removed, with Forget, and says so for a Connect that went unanswered for that reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s to what was answered A path refusal now records `end`: `local` where the policy refused this machine's end (the pair's local end, checked first, or no candidate of this end), naming only its own address and sending the phone no address, so the phone reads the generic direct-failure copy; else `remote`, naming the phone's observed address, or the first public address outside the allowed networks its offer reported. The laptop blames the phone's network only for `remote`, words a reported address as the phone's claim rather than as off-network, and dates a refusal from another day. Dismiss clears only the refusal held when it arrived. Pocket marks removed only a record of the signed-in account its list no longer names. Only a 2xx/401/403 answer spends the standing probe's streak; a 5xx or other status counts as no answer. One `relayRefuses` predicate serves the service and both panels; the described fetch keeps its cause; relay.md's socket-policy pointer joins the section's own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 2xx whose body stream breaks is a transport failure like one before the headers, so the next poll can still hear the redemption it spent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every Connect and pairing awaits the deployment read, bounded by POCKET_DEPLOYMENT_READ_TIMEOUT_MS. A complete answer is cached for the page; a failed, timed-out, 5xx, or truncated read is not, and that Connect fails retryably, so Hosted's Pocket never gathers through no ICE server because its origin was slow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
002_relay.sql returns to exactly what #868 merged; 004 adds redeemedBurrowId, redeemedAt, and their CHECK by ALTER TABLE, since a migrated database never reruns a migration. A test pins every merged migration's content. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…client # Conflicts: # scripts/spec-word-budgets.json
4eabb83 to
10e26a7
Compare
|
@dormouse-bot Please review and approve the current head f12c502 if the findings are resolved. The cancelled-begin and saved-startup-failure fixes are pushed, the lost-body recovery fix is present, and all three stale documentation points from your review summary are corrected. Validation: 172 enrollment/service tests, lib typecheck, and spec lint/self-tests passed. The maintainer explicitly says to IGNORE ARGOS; all other CI must pass. Please report any remaining issue and do not merge. |
dormouse-bot
left a comment
There was a problem hiding this comment.
One finding is inline, in Pocket's removed-marking.
|
Not approving f12c502 yet: the review of this head (posted a minute after your comment) left one open finding. In The three earlier findings are fixed at this head and their threads are resolved: cancel and disposal clear |
|
@dormouse-bot Please review and approve latest head 837783d if clear. The account-switch removal race from your latest review is fixed with a regression; previous findings remain resolved. PR 870's latest fixes are integrated. Validation: 77 Pocket scan/view tests, 172 enrollment/service tests, 9 shared web-push tests, lib typecheck, spec lint/self-tests pass. Maintainer says IGNORE ARGOS; all other CI must pass. Please report any remaining issue and do not merge. |
dormouse-bot
left a comment
There was a problem hiding this comment.
Build & Test is red at this head, and at f12c502 before it, on the new starts a fresh begin after %s while the old request is pending test. Each run failed a different variant: cancel at f12c502 (one of two begin requests had arrived), Nothing at this head (none had). That points to timing, not the fix. Approval withdrawn until it's green.
| async function drain(until: () => boolean): Promise<void> { | ||
| for (let turn = 0; turn < 500 && !until(); turn += 1) { | ||
| await new Promise((resolve) => setImmediate(resolve)); | ||
| } | ||
| } |
There was a problem hiding this comment.
drain gives up silently after 500 setImmediate turns. Before its fetch, each begin awaits #networkPolicy() and mintNoiseStatic(), and WebCrypto key generation answers from the threadpool, not within a set number of turns. On a loaded runner the 500 turns run out first, so expect(settle).toHaveLength(2) sees 0 or 1. The begins left pending then fail after the test ends, which is the unhandled no result for beginHostedEnrollment in the same log. Only setTimeout and Date are faked here, so a wall-clock bound on performance.now() still works:
| async function drain(until: () => boolean): Promise<void> { | |
| for (let turn = 0; turn < 500 && !until(); turn += 1) { | |
| await new Promise((resolve) => setImmediate(resolve)); | |
| } | |
| } | |
| async function drain(until: () => boolean): Promise<void> { | |
| const deadline = performance.now() + 5_000; | |
| while (!until() && performance.now() < deadline) { | |
| await new Promise((resolve) => setImmediate(resolve)); | |
| } | |
| } |
CI failed — Build & Test red on a timing-dependent enrollment test.
|
@dormouse-bot Please review and approve latest head 7ad70b3 if clear. CI exposed a timing flaw in my new cancelled-begin regression: bounded event-loop spins could finish before real WebCrypto reached fetch under suite load. The test now awaits explicit first/second fetch-arrival promises, synchronizes the pending join through the cached-policy read, and fails any unexpected third begin immediately. Production code is unchanged. The full lib pnpm test passed: 286 files, 5190 tests; typecheck passed, 2 files/5 tests skipped. Prior review fixes remain present. Maintainer says IGNORE ARGOS; all other CI must pass. Please report any remaining issue and do not merge. |
|
The 7ad70b3 diff is test-only. It replaces the event-loop spins with fetch-arrival promises and a third-begin tripwire, which removes the timing dependence behind the red runs on the |
Fifth and last of the Hosted persistent stack, on #870. It is the desktop half; with it,
docs/specs/remote-network.mdhas no## FutureHosted persistent scope left.Enroll a Hosted build (Settings → Network → Phones):
https://hosted.dormouse.sh(a dev Hosted build follows the Relay's checked link).Paired phones:
runsBurrowis every level but Nothing), and restarts it on any path change.EstablishedE2eSessionnow serves both this rule and the one-time session's.deployment.jsonthat Hosted stages; anything else reads as self-host.Changed shared bound: the direct-only deadline went from 15 s to 30 s, so
ONE_TIME_EXPIRY_GRACE_MSwent from 30 s to 45 s. A room must outlive a session confirmed in a link's last second. This affects the one-time room on Hosted too, so deploy the relay Worker with or before desktop builds carrying this.Test plan
pnpm testgreen; Hosted Docker suite 154/154, including an end-to-end device-code enroll → pair → connect through the account's Durable Object, ended unread on a relayed application message; Storybook play for the changed stories.🤖 Generated with Claude Code