Skip to content

Hosted Relay accounts, device-code enrollment, and Pocket at relay.dormouse.sh - #868

Open
nedtwigg wants to merge 8 commits into
remote-network-hostedfrom
remote-network-hosted-relay
Open

nedtwigg wants to merge 8 commits into
remote-network-hostedfrom
remote-network-hosted-relay

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Oct 1, 2026

Copy link
Copy Markdown
Member

Second of the Hosted persistent stack, on #867. It adds the multi-tenant Relay's HTTP half (saas-multitenant in docs/specs/relay.md). Sockets (Durable Objects) are the next PR, and push the one after.

Relay on relay.dormouse.sh:

  • Same API_ROUTES, request/response shapes, and error strings as the self-host Relay, so Pocket and the Burrow need no Hosted special case. The account id on the wire is the Hosted user id; Pocket now uses the id its Relay answered instead of hard-coding 'owner'.
  • State lives in Postgres (002_relay.sql):
    • every bearer secret is stored only as SHA-256;
    • every query is scoped to the caller's account;
    • single-use secrets are spent in one statement;
    • every table a caller can grow has a cap keyed by that caller;
    • an hourly cron sweeps expired rows.
  • Every unauthenticated route that reaches Postgres has a per-address rate limit.
  • Paid gate, until billing exists: the owner's verified email is ADMIN_EMAIL. It is rechecked on every session and Burrow-token use, with one lookup.
  • The relay Worker never reads a cookie and never calls Better Auth. Hosted login never authorizes a terminal.
  • Pocket is served at the root under its own CSP, with the camera allowed only on Pocket paths. Its passkey is named "Dormouse Pocket ()".
  • Registration and sign-in verification now live once in remote-lib-common, shared by both Relays.

Device-code enrollment:

  1. The Burrow calls POST /api/burrow/enroll/begin, which stores nothing: the device code carries its own expiry, and the user code is an HMAC of the device code under RELAY_ENROLL_SECRET.
  2. You approve at hosted.dormouse.sh/enroll (recent login, exact Origin, admin, per-account limit). Approval is the only row written.
  3. The Burrow's poll redeems the approval atomically, creating a Burrow owned by the approver.

The account page lists enrolled computers, with Remove (which deletes the row). The desktop half is the client-integration PR.

Operator step before production: wrangler secret put RELAY_ENROLL_SECRET --config wrangler.relay.jsonc (production preflight requires it).

Test plan

  • Root pnpm test green; Hosted Docker suite 94/94 (Postgres route tests: tenant isolation, racing polls, forged device codes, recent-login refusal, removal cascade).
  • /enroll driven locally with dor agent-browser against dor tool hosted, before the begin-stores-nothing rework.
  • Manual, on the PR's preview: sign in as the admin at the account preview; open the relay preview root (Pocket loads, camera prompt on Scan); approve a code minted with curl -X POST <relay>/api/burrow/enroll/begin -d '{"origin":"<relay>"}', then poll it and see the computer listed.

Local note: dor tool hosted fails at start with "Applied migration changed or missing: 002_relay.sql" in a worktree whose dev database predates the in-place migration edit; drop that worktree's pgstencil_dev database to reset it.

🤖 Generated with Claude Code

nedtwigg and others added 5 commits October 1, 2026 00:37
Postgres relay state (hashed bearer secrets, single-use consumption, per-account
caps), the Pocket-facing setup/sign-in/reauth/burrows routes with the self-host
Relay's shapes and error strings, the ADMIN_EMAIL entitlement on every
Burrow-authenticated request, Pocket at the relay root under its own policy, and
Pocket using the account id its Relay answers instead of 'owner'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes on the Hosted Relay: per-address limits on every unauthenticated
route that reaches Postgres, entitlement rechecked with each session or Burrow
token in one lookup, a dead setup token restored without evicting a live one,
headers classified on the decoded path, one asset fetch per Pocket deep link,
no /api/hello, and Pocket's passkey named "Dormouse Pocket (<host>)".
Registration and sign-in checks now live once in remote-lib-common for both
Relays; capped inserts take four round trips and an hourly cron sweeps expiry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Burrow asks relay.dormouse.sh for a code, the person approves it at
hosted.dormouse.sh/enroll with a recent login, and the Burrow's poll redeems
the approval once, atomically, as a Burrow owned by the approver. The account
page lists enrolled computers with Remove.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes: the device code carries its own expiry and the user code is an
HMAC of it under RELAY_ENROLL_SECRET, so no unauthenticated route writes a row;
approval stores the only row, and poll redeems it atomically. Remove deletes
the Burrow row; Burrow-side response guards move to the desktop stage; the
cookie gate no longer needs the login's age outside approval; and /enroll takes
a new fragment without reloading the account page.

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

cloudflare-workers-and-pages Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2746583
Status: ✅  Deploy successful!
Preview URL: https://bd3b6100.mouseterm.pages.dev
Branch Preview URL: https://remote-network-hosted-relay.mouseterm.pages.dev

View logs

Miniflare counts each limit in fixed 60 s windows, so a test sending exactly
the limit then expecting 429 failed when a window rolled over mid-test.

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

@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.

Draft feedback on work in progress, not a merge verdict. Mark the PR ready when you want the full review.

The PR moves pocketContentSecurityPolicy from relay/src/app.ts into remote-lib-common/src/remote/relay-common.ts. It updates the pointers in pocket-app.md and security-hosted.md, but docs/specs/terminal-escapes.md still says pocketContentSecurityPolicy in relay/src/app.ts in its CSP Source of truth: line. That file still imports the function, so spec-lint's path check passes, but the pointer now leads to a caller rather than to the definition.

The Postgres suite drives the shared test harness, which imports the built
package; the Hosted verify job ran it before anything had built it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nedtwigg
nedtwigg added this pull request to stack #879 October 1, 2026 14:23
@nedtwigg
nedtwigg marked this pull request as ready for review October 1, 2026 18:36

@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.

No findings on the diff. I'm holding the approval only because of the Argos checks: argos/storybook-chromium and argos/storybook-webkit each report 33 changed snapshots awaiting a decision. #867 (this PR's base) and #869 report the same 33, and this PR touches no Storybook component, so the diffs most likely come from the base branch. Accepting them in Argos (or on #867) clears the gate.

This branch is waiting to be deployed

1 waiting (outdated) deployment
hosted-preview — f695f16d Waiting Oct 1, 2026 by nedtwigg via deploy #611
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.

2 participants