Skip to content

Split Hosted into account, relay, and voice origins - #867

Merged
nedtwigg merged 8 commits into
mainfrom
remote-network-hosted
Oct 1, 2026
Merged

nedtwigg merged 8 commits into
mainfrom
remote-network-hosted

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Oct 1, 2026

Copy link
Copy Markdown
Member

First of the Hosted persistent stack (docs/specs/remote-network.md → "Hosted persistent", with saas-multitenant in docs/specs/relay.md). It resolves the one-origin pin before any Relay code lands.

Origins (decided 2026-09-30):

Origin Worker Serves
hosted.dormouse.sh dormouse-hosted the account, Better Auth, voice-token minting; holds the login cookie
relay.dormouse.sh dormouse-relay (new) the one-time rendezvous and /connect/; the Relay and Pocket land here in the next PRs
voice.dormouse.sh dormouse-voice (new) /api/voice/speak and the ElevenLabs history sweep

Why separate: Pocket and /connect/ render untrusted terminal output, so they must never share an origin with the login cookie. Sibling origins are same-site, so every cookie route keeps its exact-Origin + CSRF check; that is now an audited FAIL IF.

Desktop: DEFAULT_RELAY_ORIGIN is now https://relay.dormouse.sh. A Hosted build speaks at the fixed https://voice.dormouse.sh; a self-host build reaches neither. No released build bakes hosted.dormouse.sh (v1.1.0 predates one-time and voice), so no compatibility shim.

Hosted:

  • One Worker registry (hosted/scripts/workers.mjs) derives preview and production configs from each wrangler*.jsonc.
  • One 421 gate, bindings mapper, and header policy per Worker. The relay and voice Workers carry no auth secret.
  • OneTimeRoom moves to dormouse-relay as its first migration (v1). The account Worker appends v2 deleted_classes. Production deploys relay → voice → account, so the rendezvous never lapses.
  • Previews deploy three Workers per PR.

Operator steps before the first production release (hosted/README.md):

  • Attach the relay.dormouse.sh and voice.dormouse.sh custom domains (created on deploy).
  • Put ELEVENLABS_API_KEY on dormouse-voice, then remove it from dormouse-hosted.
  • Provider callbacks stay on hosted.dormouse.sh.

Not verified against Cloudflare:

  • whether a deploy carrying both v1 and v2 deleted_classes is accepted for the existing dormouse-hosted script;
  • whether crons: [] removes the account's old schedule.

This PR's own preview is cleaned up by main's old script, which deletes only dormouse-hosted-pr-N. If it closes unmerged, delete dormouse-relay-pr-N and dormouse-voice-pr-N by hand.

Test plan

  • Root pnpm test green.
  • Hosted test:deploy, test:miniflare, and the Docker workers.test.ts suite.
  • Preview deploys all three Workers, and the preview smoke passes.
  • Manual: on the preview, sign in at the account origin; a cookie route refuses Origin: <relay preview>; the one-time link from an innerdogfood build baked with the relay preview origin connects a phone.

🤖 Generated with Claude Code

nedtwigg and others added 5 commits September 30, 2026 23:54
hosted.dormouse.sh keeps the account and its login cookie; relay.dormouse.sh
serves the one-time rendezvous and /connect/; voice.dormouse.sh serves speak and
the ElevenLabs sweep. Each Worker has its own 421 gate, bindings mapper, header
policy, previews, and production deploy.

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

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

Simplify pass on the split: one Worker registry and deploy loop for preview
and production, one smoke sequence, the bindings mapper applied to cron
handlers, per-Worker hashed-asset prefixes, dist/<worker>/ output, tests
derived from the configs, and specs that state each rule once.

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

Code-review fixes: the account preview carries no Durable Object migrations,
production deploys relay → voice → account so the rendezvous never lapses,
retries only the relay and voice smokes, runs the one-time smoke on relay
health alone, and the local account dev server serves no speak route.

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: 2b71f55
Status: ✅  Deploy successful!
Preview URL: https://7957ea89.mouseterm.pages.dev
Branch Preview URL: https://remote-network-hosted.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.

Feedback on the draft, not a merge verdict. Mark the PR ready when you want the full review.

Build & Test fails at the first step of the root pnpm test. spec-lint rejects the two backticked build-output paths that docs/specs/one-time.md → "Phone page" now names:

docs/specs/one-time.md:338: path does not exist -> hosted/dist/relay/
docs/specs/one-time.md:338: path does not exist -> hosted/dist/relay/connect/

The spec on main never named hosted/dist, so the lint has no exemption for it. The fix that matches the existing ones is to add 'hosted/dist' to SKIP_PATH_PREFIXES in scripts/spec-lint.mjs, next to lib/dist and standalone/dist. The other option is to drop the backticks on those two paths.

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

nedtwigg commented Oct 1, 2026 •

Copy link
Copy Markdown
Member Author

Manual test plan for the Hosted persistent stack (#867 → #868 → #869 → #870 → #872)

Automated coverage: root pnpm test is green on every PR. The Hosted Docker suite is 154/154 on #872. The self-host pairing walkthrough (scripts/pairing-walkthrough/run.mjs) passed on #872. What follows needs a real phone, Cloudflare, or your eyes.

0. Unblock the previews

  1. Approve the Hosted PR preview deploy for Hosted enrollment from the desktop, and paired phones under Local networks and Anywhere #872 (Actions → "Hosted PR preview" → waiting environment). Its preview contains the whole stack and deploys three Workers. The other PRs' previews can wait.

    • https://dormouse-hosted-pr-872.dormouse-hosted-preview.workers.dev — the account
    • https://dormouse-relay-pr-872.dormouse-hosted-preview.workers.dev — the Relay and Pocket
    • https://dormouse-voice-pr-872.dormouse-hosted-preview.workers.dev — voice
  2. The first preview deploy is itself a test of three things not yet tried on Cloudflare:

    • the relay Worker's v1 + v2 Durable Object migrations;
    • the account Worker's cross-script RELAY_ROOM binding;
    • RelayRoom reading Postgres through ctx.exports.RelayRows.

    If the deploy or its smoke fails, send me the log.

1. Enroll a Hosted desktop build (Local networks)

  1. From the Hosted enrollment from the desktop, and paired phones under Local networks and Anywhere #872 worktree, run DORMOUSE_RELAY_ORIGIN=https://dormouse-relay-pr-872.dormouse-hosted-preview.workers.dev DORMOUSE_RELAY_IS_HOSTED=1 pnpm dev:standalone.
  2. Settings → Network → Local networks, with your Wi-Fi allowed.
  3. Phones → Enroll with … shows an XXXX-XXXX code. Click Open … to approve; it should open the account preview's /enroll#code.
  4. Sign in as ned.twigg@diffplug.com, using the code from <account preview>/dev/emails. The code should still be on screen afterwards. Click Approve. The desktop should flip to enrolled and its connection to connected.
  5. On the account page, Computers lists it.

2. Pair a phone, then connect directly

  1. On the iPhone, open the relay preview origin in Safari and Add to Home Screen first. Open the Home Screen app.
  2. On the desktop, choose Set up a phone and scan its QR from inside the app.
    • The passkey prompt should read "Dormouse Pocket (dormouse-relay-pr-872…)".
    • Type the two digits on the laptop.
    • The terminal should open with the header showing direct.
  3. Turn Wi-Fi off on the phone (cellular only) and Connect. Within about 30 s it should fail with the "join one of those networks" copy, and no terminal byte should arrive.

3. Anywhere

  1. Switch the desktop to Anywhere (the Burrow restarts and the phone's session ends with a goodbye). On cellular, Connect again. Expect direct via Cloudflare STUN, or relay if your carrier's NAT blocks it. Either way the terminal works.
  2. Settings → Network's connection list should match what happened: the relay origin "Always", your phone, push if paired, voice if a token is saved.

4. Push

  1. In the Home Screen app, choose Enable push notifications. On the desktop, let a pane ring unattended (e.g. sleep 5; printf '\a' in a pane you leave). A notification should arrive, and tapping it should focus the app. The preview has its own VAPID pair, so no production key is involved.

5. Removal and liveness

  1. On the account page, Remove the computer. The desktop's connection should drop and its next reconnect be refused; the phone should lose the Burrow.
  2. Re-enroll. Leave the phone app backgrounded for a few minutes, then return; it should reconnect cleanly with one passkey prompt.

6. Regressions

  1. One-time link under Local networks still connects a phone on the same Wi-Fi. It is now served from the relay origin's /connect/.
  2. Managed voice: every Hosted desktop build sends speech to the fixed production origin https://voice.dormouse.sh, including builds with a preview relay origin. A token minted at the account preview is unknown to production voice: expect 401 (or an unreachable-origin error until the custom domain is attached) and system-voice fallback. Test successful managed speech separately with a token minted at the production account. To test the voice preview’s missing-provider 503 directly, call its speak route with a preview-minted token; desktop builds do not target the voice preview.
  3. Argos: Split Hosted into account, relay, and voice origins #867 shows about 33 snapshot changes. They should be copy only (relay.dormouse.sh / voice.dormouse.sh in Settings stories); approve them if so.

Before production

  • Attach the custom domains relay.dormouse.sh and voice.dormouse.sh.
  • Set the secrets: ELEVENLABS_API_KEY on dormouse-voice (then remove it from dormouse-hosted), RELAY_ENROLL_SECRET and the VAPID pair on dormouse-relay. Steps are in hosted/README.md.
  • Deploy the relay Worker no later than any desktop build carrying Hosted enrollment from the desktop, and paired phones under Local networks and Anywhere #872, because the one-time room's grace grew from 30 s to 45 s.
  • Paid security claims still need the independent review.

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

Step 17 of the stack's manual test plan won't behave as written. It expects a dev build's spoken alert to go to the voice preview and get a 503. But hostedVoiceOrigin returns the fixed HOSTED_VOICE_ORIGIN (https://voice.dormouse.sh) whatever the baked relay origin is, and #872 doesn't change that. So a dev build carrying a token minted at the account preview sends it to production voice. Production has no record of a preview-minted token, so expect a 401 "A valid voice token is required.", or no answer at all until that custom domain is attached. The preview's 503 never comes. That's correct under this PR's design ("never baked and never overridden"). Either step 17's expectation changes, or a dev Hosted build needs a way to target the voice preview before step 17 can test the preview at all.

Comment thread dormouse.yml Outdated
nedtwigg and others added 2 commits October 1, 2026 12:40
The account's v2 deletes the old OneTimeRoom, so it must never run until the
relay serving the replacement passes its revision check and oneTimeSmoke.
deployWorkers gains an afterDeploy hook; deployProduction runs relaySmoke
(with the relay's six bounded attempts) after the relay deploy, and a failure
stops before voice or account deploy. The full smoke still runs at the end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	dormouse.yml
#	scripts/spec-word-budgets.json
@nedtwigg

nedtwigg commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

@dormouse-bot The maintainer has explicitly instructed us to IGNORE ARGOS for this stack; they will handle the visual decisions separately. All other CI must pass, and we need your approval before merging. Please review the current head 2b71f55 and approve if there are no remaining findings; please do not merge, since I am handling the stack bottom to top.

I corrected step 17 of the manual test plan in place: desktop speech always targets production voice, so a preview-minted token gets 401/system fallback there; the voice-preview 503 needs a direct call. The current dormouse.yml comment also no longer claims that the local Hosted Tool serves speak. The spec-lint output-path exemption is present, and the new production deployment guard smokes the replacement relay before the account deletes its old room. The non-Argos CI checks are now green.

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

Approved at 2b71f55. The Argos checks are left out of this verdict, as requested; every other check is green.

@nedtwigg
nedtwigg merged commit d701099 into main Oct 1, 2026
9 of 12 checks passed
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