Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/audit/canaries/burrow-fetch-global-fallback.patch
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ diff --git a/lib/src/remote/burrow/burrow-fetch.ts b/lib/src/remote/burrow/burro
index a4661ebc1..6abfae1f6 100644
--- a/lib/src/remote/burrow/burrow-fetch.ts
+++ b/lib/src/remote/burrow/burrow-fetch.ts
@@ -105,11 +105,8 @@ export function describingFetchFailures(fetch: typeof globalThis.fetch): typeof
@@ -106,11 +106,8 @@ export function describingFetchFailures(fetch: typeof globalThis.fetch): typeof
export interface BurrowFetchOptions {
/** Who this Burrow is to that Relay, and the bearer that proves it. */
readonly enrollment: Pick<BurrowEnrollment, 'relayUrl' | 'burrowToken'>;
Expand All @@ -63,10 +63,10 @@ index a4661ebc1..6abfae1f6 100644
readonly timeoutMs?: number;
/** Leads the non-2xx message, which always ends in ` (<status>)`. */
readonly errorPrefix?: string;
@@ -121,7 +118,8 @@ export interface BurrowFetchOptions {
@@ -122,7 +119,8 @@ export interface BurrowFetchOptions {
* endpoint whose only input is the bearer still posts, with `{}`.
*/
function burrowRequest(options: BurrowFetchOptions, route: string, body?: unknown): Promise<Response> {
export function burrowRequest(options: BurrowFetchOptions, route: string, body?: unknown): Promise<Response> {
- return options.fetch(`${options.enrollment.relayUrl}${route}`, {
+ const doFetch = options.fetch ?? globalThis.fetch;
+ return doFetch(`${options.enrollment.relayUrl}${route}`, {
Expand Down
2 changes: 1 addition & 1 deletion .github/audit/remote.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This is a code-and-specs audit of remote control: the Burrow, the Relay, the pho

Read, at minimum: `docs/specs/remote-security-model.md` **and its paired `docs/specs/remote-security-model.rationale.md`**, `docs/specs/relay.md`, `docs/specs/remote-api.md`, `docs/specs/pocket-app.md`, `docs/specs/one-time.md` ("Link", "Wire contract", "Burrow runtime", "Phone client"), `docs/specs/remote-network.md` ("Local networks", "Anywhere"), `SELF_HOST.md`, and then the code they point at — `remote-lib-common/src/security/`, `relay/src/`, `lib/src/remote/`, `lib/src/host/remote/`, `vscode-ext/src/burrow*.ts`, `scripts/relay-origin.mjs`, and all three installers — `deploy/local/install-macos.sh`, `deploy/local/install-windows.ps1`, and `deploy/local/install-linux.sh`. The three hold the same invariants through different native mechanisms, so read them against each other: a control present in one and quietly absent from another is a finding.

The end-to-end boundary is where the depth goes. Its modules are `remote-lib-common/src/security/noise.ts`, `noise-transport.ts`, `e2e-ceremony.ts`, `e2e-bounds.ts`, `token-bucket.ts`, `push-seal.ts`, `pairing-invitation.ts`, `one-time-link.ts`, `link-url.ts`, `presence.ts`, `presence-window.ts`, `acl.ts` and `direct-path.ts`; `remote-lib-common/src/remote/wire.ts` (the frame shapes and their guards) and `one-time-wire.ts` (the one-time rendezvous family — "One-time connection"); `lib/src/remote/direct/direct-endpoint.ts` and `direct-peer.ts` (the data channel the same session may move onto, and the one switching policy both ends run — "Direct path"); `lib/src/remote/burrow/burrow-runtime.ts` (both ceremonies, every Burrow bound), `established-session.ts` (an authorized session's decrypt, idle clock, and direct path), and `one-time-runtime.ts` (the one-time ceremony — "One-time connection"); `lib/src/host/relay-origin.ts` and the one-time half of `service.ts` ("Relay origin", "One-time connection", and the approval routing under "Trust boundary"); the revocation and access-log half of `service.ts`, with `lib/src/remote/burrow/acl.ts` and `access-log.ts` ("Trust boundary"); `lib/src/host/remote/local-networks.ts` and `native-direct-peer.ts` (Local networks' hold on a direct path — "One-time connection" and "Direct path"); `lib/src/remote/burrow/push-delivery.ts`; `lib/src/remote/burrow/frame-tunnel.ts`, `lib/src/remote/burrow/client-close.ts`, `lib/src/components/wall/close-for-client.ts` and `lib/src/host/remote/frame-host.ts` (the frame tunnel — "What crosses the boundary"); `lib/src/remote/client/pocket-client.ts` and `session-core.ts` (the phone's ceremonies, and the established session they promote), `one-time-client.ts` (the one-time phone), and `lib/src/remote/one-time-rendezvous.ts` (the frame bound both one-time ends read the room under); `lib/src/remote/pocket-app/sw.ts` (the render sink); `relay/src/relay.ts` and `relay/src/app.ts` ("What crosses the boundary"). The harnesses that already exercise this are `lib/src/remote/burrow/burrow-bounds.test.ts`, `lib/src/remote/burrow/one-time-runtime.test.ts`, `lib/src/remote/client/one-time-e2e.test.ts`, `lib/src/host/remote/local-networks.test.ts`, `lib/src/host/remote/service.test.ts`, `relay/test/malicious-relay.test.mjs`, `remote-lib-common/test/security-guarantees.test.mjs`, `remote-lib-common/test/noise.test.mjs`, and `remote-lib-common/test/push-seal.test.mjs` — read what they *do not* cover, and say so.
The end-to-end boundary is where the depth goes. Its modules are `remote-lib-common/src/security/noise.ts`, `noise-transport.ts`, `e2e-ceremony.ts`, `e2e-bounds.ts`, `token-bucket.ts`, `push-seal.ts`, `pairing-invitation.ts`, `one-time-link.ts`, `link-url.ts`, `presence.ts`, `presence-window.ts`, `acl.ts` and `direct-path.ts`; `remote-lib-common/src/remote/wire.ts` (the frame shapes and their guards) and `one-time-wire.ts` (the one-time rendezvous family — "One-time connection"); `lib/src/remote/direct/direct-endpoint.ts` and `direct-peer.ts` (the data channel the same session may move onto, and the one switching policy both ends run — "Direct path"); `lib/src/remote/burrow/burrow-runtime.ts` (both ceremonies, every Burrow bound), `established-session.ts` (an authorized session's decrypt, idle clock, and direct path), and `one-time-runtime.ts` (the one-time ceremony — "One-time connection"); `lib/src/host/relay-origin.ts` and the one-time half of `service.ts` ("Relay origin", "One-time connection", and the approval routing under "Trust boundary"); the revocation and access-log half of `service.ts`, with `lib/src/remote/burrow/acl.ts`, `access-log.ts`, and `passkey-sync.ts` ("Trust boundary"); `lib/src/host/remote/local-networks.ts` and `native-direct-peer.ts` (Local networks' hold on a direct path — "One-time connection" and "Direct path"); `lib/src/remote/burrow/push-delivery.ts`; `lib/src/remote/burrow/frame-tunnel.ts`, `lib/src/remote/burrow/client-close.ts`, `lib/src/components/wall/close-for-client.ts` and `lib/src/host/remote/frame-host.ts` (the frame tunnel — "What crosses the boundary"); `lib/src/remote/client/pocket-client.ts` and `session-core.ts` (the phone's ceremonies, and the established session they promote), `one-time-client.ts` (the one-time phone), and `lib/src/remote/one-time-rendezvous.ts` (the frame bound both one-time ends read the room under); `lib/src/remote/pocket-app/sw.ts` (the render sink); `relay/src/relay.ts` and `relay/src/app.ts` ("What crosses the boundary"). The harnesses that already exercise this are `lib/src/remote/burrow/burrow-bounds.test.ts`, `lib/src/remote/burrow/one-time-runtime.test.ts`, `lib/src/remote/client/one-time-e2e.test.ts`, `lib/src/host/remote/local-networks.test.ts`, `lib/src/host/remote/service.test.ts`, `relay/test/malicious-relay.test.mjs`, `remote-lib-common/test/security-guarantees.test.mjs`, `remote-lib-common/test/noise.test.mjs`, and `remote-lib-common/test/push-seal.test.mjs` — read what they *do not* cover, and say so.

For `## Auxiliary helpers`, read `docs/specs/terminal-context.md` -> "Helper lifecycle" and "Promotion and source closure".

Expand Down
7 changes: 4 additions & 3 deletions docs/specs/hosted.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ The relay Worker serves the self-host Relay's HTTP API to many accounts: the pat
| `POST /api/reauth/begin`, `/finish` | Only the session's account's credentials and nonces |
| `GET /api/burrows` | The session's account's Burrows, each `online` while it holds a live socket in the account's `RelayRoom` ("Relay sockets") |
| `POST /api/burrow/setup-token` | 401 for a removed Burrow, 403 `NOT_ENTITLED_ERROR` for an owner not entitled |
| `POST /api/burrow/passkeys/removed` | As `setup-token`; held means by the token's owner, so another account's passkey reads as removed |
| `POST /api/burrow/enroll` | Always 401 `UNAUTHORIZED_ERROR`: Hosted has no setup password; a Burrow enrolls by device code ("Burrow enrollment") |
| `GET /api/push/config` | The VAPID public key, or `null` when push is off ("Push") |
| `POST /api/push/subscribe` | 404 for a Burrow not the session's account's; 400 `endpoint must be a known push service` off the allowlist ("Push") |
Expand Down Expand Up @@ -241,9 +242,9 @@ The relay Worker serves `GET /ws/burrow` and `GET /ws/client` at the self-host p
- **Must recheck a Burrow's row in the object before accepting its socket**, under `blockConcurrencyWhile`: enrolled, the account's, its owner entitled, else the Worker's 401 or 403, so a removal after the Worker's check is refused; **and a Client's session likewise** (live, the account's, owner entitled), else 1008. **Never open a database connection in the object** (rationale): it reads rows through `RelayRows`. **Must bound every row read at `RELAY_ROW_READ_TIMEOUT_MS`**: past it the upgrade answers 503 and a sweep fails.
- **Must accept every socket through the Hibernation API**, its routing state in the socket's attachment.
- **Must measure a frame against `MAX_RELAY_FRAME_BYTES` before parsing it**, a text frame in UTF-8 bytes as the self-host `maxPayload` counts, closing 1009 past it; an under-cap binary frame is dropped. The Client cap is per account object.
- **Must close, from the object's alarm, every Client whose session expired, went, or lost the entitlement 1008 `unauthorized`** (its Burrow told `client-gone`) **and every removed or another account's Burrow 4001 (`WS_CLOSE_BURROW_REVOKED`) and de-entitled one 4002 (`WS_CLOSE_BURROW_NOT_ENTITLED`)** (its Clients told `burrow-gone`): the alarm fires at the earliest held Client's `expiresAt` and, while any socket is held, at most `RELAY_ROOM_SWEEP_MS` apart, or `RELAY_ROOM_SWEEP_RETRY_MS` after a failed read. **Must close 1013, at a failed read, every socket no read has found live for `RELAY_ROOM_SWEEP_MS` plus `RELAY_ROOM_SWEEP_GRACE_MS`**, to reconnect through the upgrade (rationale). It replaces the self-host expiry and revocation sweeps.
- **Must close, from the object's alarm, every Client whose session expired, went, or lost the entitlement 1008 `unauthorized`** (its Burrow told `client-gone`) **and every removed or another account's Burrow 4001 (`WS_CLOSE_BURROW_REVOKED`) and de-entitled one 4002 (`WS_CLOSE_BURROW_NOT_ENTITLED`)** (its Clients told `burrow-gone`), **sending every other held Burrow `passkeys-changed`**: the alarm fires at the earliest held Client's `expiresAt` and, while any socket is held, at most `RELAY_ROOM_SWEEP_MS` apart, or `RELAY_ROOM_SWEEP_RETRY_MS` after a failed read. **Must close 1013, at a failed read, every socket no read has found live for `RELAY_ROOM_SWEEP_MS` plus `RELAY_ROOM_SWEEP_GRACE_MS`**, to reconnect through the upgrade (rationale). It replaces the self-host expiry and revocation sweeps.
- **Must answer `RELAY_PING` with `RELAY_PONG` as the object's auto-response**, which never wakes it, reaches no handler, and is never forwarded. **A socket unheard from for longer than `RELAY_IDLE_TIMEOUT_MS` is gone**, retired with 1001, counted from its last auto-response or, before its first ping, from its opening. It replaces the self-host heartbeat. One gap is accepted: at the Client cap a backgrounded Pocket, its pings paused, counts as gone once silent that long, the Burrow reaping its session at 120 s anyway.
- **Must close a removed Burrow's socket from the account Worker's `DELETE /api/relay/burrows/:burrowId`**, through its `RELAY_ROOM` binding calling `closeBurrow`: 4001; **and a removed passkey's sessions' Client sockets from `DELETE /api/relay/passkeys/:credentialId`** calling `closeSessions`: 1008, as at expiry. **Must answer 204 once the row is gone, even if the close fails**, logging it; the sweep closes the socket. `GET /api/burrows` reads `online` through account-scoped `onlineBurrows`. **Never expose an RPC or `RelayRows` as a public endpoint.**
- **Must close a removed Burrow's socket from the account Worker's `DELETE /api/relay/burrows/:burrowId`**, through its `RELAY_ROOM` binding calling `closeBurrow`: 4001; **and a removed passkey's sessions' Client sockets from `DELETE /api/relay/passkeys/:credentialId`** calling `closeSessions`: 1008, as at expiry; **it must then call `nudgeBurrows`, whatever sessions there were**, so every Burrow socket the account holds gets `passkeys-changed`. **Must answer 204 once the row is gone, even if the close or the nudge fails**, logging it; the sweep does it instead. `GET /api/burrows` reads `online` through account-scoped `onlineBurrows`. **Never expose an RPC or `RelayRows` as a public endpoint.**

Source of truth: `relaySocketRoutes` in `hosted/server/relay-sockets.ts`; `RelayRoom` in `hosted/server/relay-room.ts`. Pinned by `hosted/server/tests/relay-room.test.ts`, which also runs the routing cases every Relay passes (`remote-lib-common/test/harness/relay-parity.mjs`).

Expand Down Expand Up @@ -308,7 +309,7 @@ Errors are the managed-voice cookie routes' ("Managed voice"), except that their
- **Must count every approval attempt** against `RELAY_APPROVE_LIMIT` (10 a minute per account) before reading the body: 429 with `Retry-After` past it.
- **Must answer a malformed code or digits 400**, the code forgiving case, spaces, and dashes, and a second approval of a live code 409 `ALREADY_APPROVED`, whichever account sends it: a live approval never moves. An expired one is replaced.
- **Must remove by deleting the Burrow's row**, its setup tokens and setup challenges cascading, so its token authenticates nothing on any relay route, **then close its live socket** ("Relay sockets"). **Never remove a passkey with its Burrow**: it is left with no Burrow (rationale).
- **Must remove a passkey by deleting its row**, every session it signed in cascading, so it signs in to nothing again, **then close their sockets** ("Relay sockets"); every session names the passkey that signed it in, its `"credentialId"` `NOT NULL`.
- **Must remove a passkey by deleting its row**, every session it signed in cascading, so it signs in to nothing again, **then close their sockets and nudge the account's Burrows** ("Relay sockets"), each of which revokes its pairings with that passkey, one offline at its next connection (`docs/specs/remote-security-model.md` -> "Relay-named revocation"); every session names the passkey that signed it in, its `"credentialId"` `NOT NULL`.
- **Must take the `/enroll` fragment before render and erase it from history, and hold the code in memory only**: through email sign-in and back, never into storage. Provider sign-in leaves the page, so the user opens the link again.
- **Must preserve newer enrollments when earlier approvals complete.**
- **Never offer the digits to an account the server says is unentitled**, and never conclude that from a failed read; the page offers another account instead.
Expand Down
Loading
Loading