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
2 changes: 1 addition & 1 deletion .github/audit/hosted.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 Hosted's three Workers — the account applica

Read `docs/specs/hosted.md`, `docs/specs/one-time.md` (its "Wire contract", "Hosted rendezvous", and "Phone page"), `docs/specs/relay.md` (its "HTTP API", "Setup tokens and the pairing QR", "WebAuthn without a WebAuthn library", "Routing", "Web Push", and "State files", whose semantics the Hosted Relay keeps), `hosted/server/`, `hosted/src/`, `hosted/scripts/`, `hosted/wrangler.jsonc`, `hosted/wrangler.relay.jsonc`, `hosted/wrangler.voice.jsonc`, `remote-lib-common/src/remote/one-time-wire.ts`, `remote-lib-common/src/remote/relay-common.ts`, `remote-lib-common/src/remote/relay-routing.ts`, `remote-lib-common/src/remote/enroll-code.ts`, `remote-lib-common/src/remote/wire.ts`, `scripts/e2e-lint.mjs`, `lib/package.json`, `remote-lib-common/src/remote/web-push.ts` and its test `remote-lib-common/test/web-push.test.mjs` with the vectors under `remote-lib-common/test/vectors/`, the Pocket build the relay serves (`lib/vite.pocket.config.ts`, `lib/pocket/`), the phone page it serves — `lib/vite.one-time.config.ts`, `lib/one-time/`, `lib/src/remote/one-time-app/`, and `lib/scripts/assert-pocket-worker.mjs` — and `.github/workflows/hosted-preview.yml` and `.github/workflows/hosted-production.yml` — `docs/specs/security-hosted.md`'s Deployment boundary quantifies over the preview and production paths, which live in those scripts and workflows rather than in the Worker.

Check the installed `pgstencil` and `@pgstencil/auth` packages against "Deployment boundary" by reading each `dist/provenance.json`, `pnpm-lock.yaml` and `pnpm-workspace.yaml` (no `.npmrc` is tracked, so the registry is npm's default), and Hosted's runtime imports. Audit no package code, except that the `FAIL IF`s which name the packed adapter (CSRF and CORS, cookie attributes and JSON scrubbing, the revoked-login callback) may read `@pgstencil/auth`'s `dist/` to verify how Hosted configures it.
Check the installed `pgstencil`, `@pgstencil/auth`, and `@pgstencil/stripe` packages against "Deployment boundary" by reading each `dist/provenance.json`, `pnpm-lock.yaml` and `pnpm-workspace.yaml` (no `.npmrc` is tracked, so the registry is npm's default), and Hosted's runtime imports. Audit no package code, except that the `FAIL IF`s which name the packed adapter (CSRF and CORS, cookie attributes and JSON scrubbing, the revoked-login callback) may read `@pgstencil/auth`'s `dist/` to verify how Hosted configures it.

Verify npm's signed SLSA provenance for each installed package/version. Run `npm audit signatures --json --include-attestations` in `hosted/`, as the production `verify` job does. A package counts only from `verified`, never from `invalid` or `missing`. Decode the verified SLSA DSSE payload and the Fulcio certificate in that bundle and judge S119's certificate and payload clauses against them, and its job clause against that step, the payload's `externalParameters.workflow` and `resolvedDependencies` included. npm verifies the signature and subject digest, but the payload's workflow claim alone is not the signer identity. Then check the commit against pgstencil `main` and its audit:

Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/hosted-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ jobs:
working-directory: hosted
run: |
npm audit signatures --json --include-attestations > "$RUNNER_TEMP/signatures.json" || true
jq -e '[.invalid[], .missing[] | select(.name == "pgstencil" or .name == "@pgstencil/auth")] == []
and (["pgstencil", "@pgstencil/auth"] - [.verified[] | select(.attestations.provenance) | .name] == [])' \
jq -e '["pgstencil", "@pgstencil/auth", "@pgstencil/stripe"] as $ours
| [.invalid[], .missing[] | select(.name as $name | $ours | index($name))] == []
and ($ours - [.verified[] | select(.attestations.provenance) | .name] == [])' \
"$RUNNER_TEMP/signatures.json"
# Every Worker's static files, each under its own `dist/<worker>/`.
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
Expand Down
25 changes: 20 additions & 5 deletions docs/specs/hosted.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

| Worker | Origin | Serves | Holds |
|---|---|---|---|
| `dormouse-hosted` | `https://hosted.dormouse.sh` | the account frontend, `/api/auth/*`, `/api/providers`, Natural Voice on or off, the Relay's account routes ("Burrow enrollment") | the login cookie, auth secrets, Hyperdrive, the approval rate limit, a binding to the relay's `RelayRoom` |
| `dormouse-hosted` | `https://hosted.dormouse.sh` | the account frontend, `/api/auth/*`, `/api/providers`, Natural Voice on or off, the Relay's account routes ("Burrow enrollment"), billing's resync ("Billing") | the login cookie, auth secrets, Hyperdrive, the approval rate limit, a binding to the relay's `RelayRoom`, the Stripe bindings |
| `dormouse-relay` | `https://relay.dormouse.sh` | the Hosted Relay, its sockets, and Pocket ("Relay", "Relay sockets"), the one-time rendezvous and `/connect/` (`docs/specs/one-time.md` -> "Hosted rendezvous") | Hyperdrive, `OneTimeRoom`, `RelayRoom`, the one-time, sign-in, setup, and enrollment rate limits, `ACCOUNT_ORIGIN`, `RELAY_ENROLL_SECRET`, the VAPID pair |
| `dormouse-voice` | `https://voice.dormouse.sh` | speak, a token's own delete, and the history sweep ("Managed voice") | `ELEVENLABS_API_KEY`, Hyperdrive |

Expand All @@ -19,9 +19,9 @@ Every Worker answers `/api/health` and `/api/ready`, 404s anything else under it

**Must run committed Better Auth migrations before deploying code that needs them, never during a Worker request.** Postgres is reached through an uncached Hyperdrive binding, one per Worker.

**Must grant `dormouse_relay` and `dormouse_voice` only what `hosted/server/runtime-roles.sql` lists, never default privileges**, so a new table needs a grant there; each reaches only its Worker's tables and the entitlement's user columns, the relay inserts approved voice tokens ("Managed voice"), and both add to the daily counts ("Metrics"). **Must bind each production Worker to its own Hyperdrive connecting as its role**: account `dormouse_app`, relay `dormouse_relay`, voice `dormouse_voice` (`PRODUCTION` in `hosted/scripts/production.mjs`).
**Must grant `dormouse_relay` and `dormouse_voice` only what `hosted/server/runtime-roles.sql` lists, never default privileges**, so a new table needs a grant there; each reaches only its Worker's tables and the entitlement's user and subscription columns, the relay inserts approved voice tokens ("Managed voice"), and both add to the daily counts ("Metrics"). **Must bind each production Worker to its own Hyperdrive connecting as its role**: account `dormouse_app`, relay `dormouse_relay`, voice `dormouse_voice` (`PRODUCTION` in `hosted/scripts/production.mjs`).

**Must install released core/auth packages from npm and commit their lockfile integrity hashes.** The installed packages' `dist/provenance.json` must name the same clean pgstencil commit; no runtime import depends on a sibling checkout. The auth migrations remain owned by the package; Dormouse's own tables migrate from `hosted/server/dormouse-migrations/`. **Never edit a merged migration**: a migrated database never reruns one, so append the next number (pinned by `hosted/server/tests/migrations.test.ts`).
**Must install released core, auth, and stripe packages from npm and commit their lockfile integrity hashes.** The installed packages' `dist/provenance.json` must name the same clean pgstencil commit; no runtime import depends on a sibling checkout. The auth and billing migrations remain owned by their packages; Dormouse's own tables migrate from `hosted/server/dormouse-migrations/`. **Never edit a merged migration**: a migrated database never reruns one, so append the next number (pinned by `hosted/server/tests/migrations.test.ts`).

**Must declare every required peer dependency of the installed packages in `hosted/package.json`'s `dependencies`**, so they share Hosted's copy and Renovate updates them. **Never put an optional peer (pgstencil's tooling) in `dependencies`.**

Expand Down Expand Up @@ -67,10 +67,25 @@ Source of truth: `termsRoutes` in `hosted/server/terms.ts`; `TERMS_VERSION` in `

## Entitlement

**Must read the entitlement (`docs/specs/pricing.md` -> "Checkout and entitlement") on the server, per request, through one SQL predicate over the account's `"user"` row**, so a bearer and its owner's entitlement resolve in one query. Until billing ships it admits only `ADMIN_EMAIL` while that is the account's verified email, one of the exceptions to "never email" ("Identity and login"); nothing else may key on an address.
**Must read the entitlement (`docs/specs/pricing.md` -> "Checkout and entitlement") on the server, per request, through one SQL predicate over the account's `"user"` row**, so a bearer and its owner's entitlement resolve in one query.

- **Must entitle an account only while it holds exactly one current subscription, `active` before its period end or `trialing` before its trial end** by the database's clock, as `@pgstencil/stripe`'s `status()` grants access; no other status grants anything.
- **Must entitle `ADMIN_EMAIL` as a standing comp** while it is the account's verified email, one of the exceptions to "never email" ("Identity and login"); nothing else may key on an address.

Source of truth: `entitledSql` and `entitled` in `hosted/server/entitlement.ts`; `cookieEntitled` in `hosted/server/account-gate.ts`.

## Billing

The account Worker bills through `@pgstencil/stripe` (`docs/specs/pricing.md` -> "Checkout and entitlement"); its routes are unbuilt.

- **Must keep billing closed without all five bindings**: the secrets `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET`, and the vars `STRIPE_PRICE_MONTHLY`, `STRIPE_PRICE_YEARLY`, and `STRIPE_PRICES_FOUNDING` (cohort order); closed, the resync reads nothing. **Must refuse a founding list whose length differs from `FOUNDING_LADDER`**, or a malformed or repeated Price. Live mode follows the key's prefix.
- **Never offer a trial or Managed Payments; must enable Stripe Tax on every Checkout.**
- **Must offer founding at the first ladder step whose completed purchases, refunds included, are short of `FOUNDING_COHORT_SIZE`**; its seats left exclude purchases ended within `REFUND_DAYS`. Past the last step, founding checkout answers 409.
- **Must resync, every 10 minutes from the account's Cron Trigger, a bounded random batch of owners whose `active` or `trialing` row no longer grants access**, so a missed renewal webhook lapses a member only until a run picks them; a failure fails the invocation after every owner was tried.
- **Must grant the account's role the `pgstencil_billing` schema from `runtime-roles.sql`**, additively: the package's migration creates it.

Source of truth: `billingSetup`, `withBilling`, and `resyncBilling` in `hosted/server/billing.ts`. Pinned by `hosted/server/tests/billing.test.ts`, on StripeDev.

## Managed voice

A desktop with Natural Voice on exchanges its voice token for ElevenLabs speech in a voice of the curated set (`MANAGED_VOICES` in `remote-lib-common/src/remote/managed-voice.ts`). A voice-purpose device-code approval ("Burrow enrollment") mints only the token at redemption.
Expand Down Expand Up @@ -304,5 +319,5 @@ Source of truth: `.github/workflows/hosted-production.yml`; `hosted/scripts/prod

1. Deploy the configured providers and pass real production acceptance. pgstencil includes the Microsoft fix; personal and work/school callbacks need acceptance.
2. Add per-browser login listing/revocation, sign-out-everywhere, and account recovery before broad paid use. Revisit the fixed 24-hour login lifetime for daily voice use.
3. Managed voice for every member: the subscription as the entitlement in place of `ADMIN_EMAIL` (`docs/specs/pricing.md` -> "Checkout and entitlement"), per-account quotas, usage accounting, spending bounds beyond the fixed daily cap, and explicit text/redaction disclosure.
3. Managed voice for every member: per-account quotas, usage accounting, spending bounds beyond the fixed daily cap, and explicit text/redaction disclosure.
4. Open sign-in: delete `hosted/server/prelaunch.ts` and the frontend's pre-launch notice.
Loading
Loading