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 @@ -46,7 +46,7 @@ Be adversarial, and go past the `FAIL IF` list; a bare section name is `docs/spe
- **Can a Hosted login become terminal access, or an account become someone else's?** Trace linking, a revoked login's callback, an unused or unknown provider credential, and every path from a login toward a Burrow ACL grant ("Account boundary"; the relay Worker's cookie rule is "Relay boundary"). Pocket and `/connect/` share the relay origin; check what each page's policy lets it reach of the other.
- **Can one account's relay socket reach another's?** Trace an upgrade through `relaySocketRoutes` (`hosted/server/relay-sockets.ts`) into `RelayRoom` (`hosted/server/relay-room.ts`) against "Relay boundary". Look for routing state kept in memory that a hibernated object would lose, a socket torn down twice or routed after its close began, a frame bounded in characters rather than bytes, a `ct` touched outside the shared frame layer's field copy, a Client cap one socket can evict past, a session that outlives its alarm, a ping that wakes the object, a Burrow socket accepted on a row removed after the token check, and a removed or de-entitled Burrow's socket outliving the sweep.
- **Can the rendezvous become more than a handshake pipe?** Trace a frame through `OneTimeRoom` against "Rendezvous boundary". Look for a second phone admitted across an await or a hibernation, a room that outlives its alarm, a web page that can mint a room, a join from another origin, a room id a caller can choose, and a limit a caller can step around. The relay is same-site with the account, so account cookies can ride the phone's upgrade.
- **Can billing grant what Stripe did not sell, or end what it should only suspend?** Trace checkout, confirm, the webhook, and the resync in `hosted/server/billing-routes.ts` and `hosted/server/billing.ts` against "Account boundary": a browser-chosen Price, owner, or return URL, an unsigned or replayed event, a 2xx before the commit, a count a redelivery repeats, a stale dispute event read as current, a partial refund, or a refund of an earlier charge, that ends a subscription, a failed count that fails the event, and a lapsed or disputed subscription that still admits; and a checkout URL answered before its consent row, or a founders row for a login without a current founding subscription. The founders row is public: what can a reader learn from `/api/hosted/*` beyond the names founders chose, and can a stored profile-image URL make the account Worker fetch anything but a provider's picture?
- **Can billing grant what Stripe did not sell, or end what it should only suspend?** Trace checkout, confirm, the webhook, and the resync in `hosted/server/billing-routes.ts` and `hosted/server/billing.ts` against "Account boundary": a browser-chosen Price, owner, or return URL, an unsigned or replayed event, a 2xx before the commit, a count a redelivery repeats, a stale dispute event read as current, a partial refund, or a refund of an earlier charge, that ends a subscription, a failed count that fails the event, and a lapsed or disputed subscription that still admits; and a checkout URL answered before its consent row, or a founders row for a login without a current founding subscription. The founders row is public: what can a reader learn from `/api/hosted/*` beyond the names founders chose, and can a stored profile-image URL make the account Worker fetch anything but a provider's picture? Trace a withdrawal and an account closure too: a withdrawal answered before its row, refused to an eligible contract, or stamped done before its email, cancellation, or refunds; and a closure that deletes an account whose subscription Stripe still renews.
- **Does anything from the test or preview build reach production?** Trace the production entries and the preview configuration and workers against "Deployment boundary". Preview credentials are `ci-and-secrets`' (`docs/specs/security-ci.md` -> "Hosted Deployments").

Does the shipped code still match what the spec and this section claim? Spec drift is a finding; say which side is wrong.
21 changes: 18 additions & 3 deletions docs/specs/hosted.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,12 @@ The account Worker bills through `@pgstencil/stripe` (`docs/specs/pricing.md` ->

| Route | Credential | Success |
|---|---|---|
| `GET /api/billing` | login cookie | 200 `{ plan, price, active, until, renews, entitled, suspended, founding, founder, consent }`; `plan` names a subscription Stripe still holds |
| `GET /api/billing` | login cookie | 200 `{ plan, price, active, until, renews, entitled, suspended, founding, founder, consent, withdrawal, withdrawn }`; `plan` names a subscription Stripe still holds |
| `POST /api/billing/checkout` | login cookie, exact `Origin`, JSON `{ plan, ref?, terms, autoRenew, price }` | 200 `{ url }` of Stripe Checkout |
| `POST /api/billing/confirm` | login cookie, exact `Origin`, JSON `{ checkout }` | 200, the `GET` body |
| `POST /api/billing/portal` | login cookie, exact `Origin` | 200 `{ url }` of the customer portal |
| `PUT /api/billing/founder` | login cookie, exact `Origin`, JSON `{ shown, name? }` | 200, the `GET` body |
| `POST /api/billing/withdraw` | login cookie, exact `Origin`, JSON `{ subscription }` | 200, the `GET` body |
| `POST /api/billing/webhook` | `Stripe-Signature` over the raw body | 200 once `webhook()` committed |
| `GET /api/hosted/cohorts` | none | 200 `{ cohort?, seatsLeft?, founders: { total, shown } }` |
| `GET /api/hosted/founders/<token>` | none | 200, a shown founder's picture |
Expand All @@ -98,13 +99,27 @@ Errors are JSON `{ message }`. The cookie routes need no entitlement, answer 401
- **Must add or rename a founders-row name (`dormouse_founders`) only while `subscribedSql` holds on the founding Prices** (else 409) and `isFounderName` accepts it; leaving always works, a rename keeps `shownSince`. Checks: `docs/specs/security-hosted.md` -> "Account boundary".
- **Must verify the webhook's signature over the raw body before any write** and answer 2xx only once `webhook()` has committed, any other failure 503, which Stripe retries.
- **Must end at once the subscription a fully refunded charge or a lost dispute's charge paid for**, found through its invoice, while it is the owner's current one: commented `ENDED_FOR_REFUND`, after the commit and on every delivery until ended. A partial refund ends nothing, nor does a charge for an earlier subscription or none, which is logged for the operator.
- **Must offer the online withdrawal only from the held subscription while `withdrawalOffer` holds**, once per subscription; else withdraw answers 409 `WITHDRAWAL_CLOSED`. A recorded withdrawal (`dormouse_withdrawals`) is then acknowledged by email to the Stripe customer's address and settled: its subscription ended at once (`ENDED_FOR_WITHDRAWAL`), every paid invoice payment refunded in full. Each step is stamped once done and safe to repeat; the cron retries a failed one for 30 days. Checks: `docs/specs/security-hosted.md` -> "Account boundary".
- **Must suspend, never end, on an open dispute**, by its current status read from Stripe, not the event (`dormouse_billing_disputes`); closing in the account's favor restores it.
- **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 answer `/api/hosted/*` without resolving a login**, each answer reused for `COHORT_CACHE_MS` per isolate; `total` counts founding purchases net of refunds. The site's Worker forwards it from `dormouse.sh` to this origin, so the 421 gate needs no exception. What it answers and fetches: `docs/specs/security-hosted.md` -> "Account boundary".
- **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 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, and finish a bounded batch of unfinished withdrawals; a failure fails the invocation after every one 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`; `billingRoutes` and `disputeAction` in `hosted/server/billing-routes.ts`; `cohortRoutes` in `hosted/server/cohort-routes.ts`; `forwardHosted` in `website/public/_worker.js`. Pinned by `hosted/server/tests/billing.test.ts` and `hosted/server/tests/billing-routes.test.ts`, on StripeDev.
Source of truth: `billingSetup`, `withBilling`, and `resyncBilling` in `hosted/server/billing.ts`; `billingRoutes` and `disputeAction` in `hosted/server/billing-routes.ts`; `withdrawalOffer` and `completeWithdrawal` in `hosted/server/withdrawal.ts`; `cohortRoutes` in `hosted/server/cohort-routes.ts`; `forwardHosted` in `website/public/_worker.js`. Pinned by `hosted/server/tests/billing.test.ts` and `hosted/server/tests/billing-routes.test.ts`, on StripeDev.

## Account closure

| Route | Credential | Success |
|---|---|---|
| `POST /api/account/close` | login cookie, exact `Origin`, JSON `{ close: true }` | 204, the account deleted |

Errors are JSON `{ message }`; it needs no entitlement and works with billing closed. What the page says first: `docs/specs/pricing.md` -> "Checkout and entitlement".

- **Must refuse a login older than `LOGIN_FRESH_AGE_MS`** with 403 `CLOSE_RECENT_LOGIN`, and, billing closed, an account with a current subscription row with 503 `CLOSE_UNAVAILABLE`.
- **Must expire an open checkout, then cancel every current subscription in Stripe (`ENDED_FOR_CLOSURE`), before one statement records the closure (`dormouse_account_closures`) and deletes the `"user"` row**; rows without a foreign key stay. Its Burrows' sockets then close as a removal's do ("Relay sockets"). Checks: `docs/specs/security-hosted.md` -> "Account boundary".

Source of truth: `closureRoutes` in `hosted/server/closure.ts`; `CloseAccount` in `hosted/src/Closure.tsx`.

## Managed voice

Expand Down
13 changes: 9 additions & 4 deletions docs/specs/pricing.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,14 @@ Source of truth: `Hosted` in `website/src/pages/Hosted.tsx`; `fetchCohort` in `w
- **Must obtain affirmative agreement to the versioned terms and express consent to automatic renewal at the purchase action**, two unticked boxes, disclosing beside it the price and tax, the interval and renewal price, cancellation, the refund conditions, and the material limits: one individual including work use, no device limit, and managed voice's daily cap. Checkout refuses a Price other than the one shown and records the consent with it (`docs/specs/hosted.md` -> "Billing").
- **Must keep a durable confirmation at `/billing` while the plan is held**: the plan and price, renewal, the terms version agreed and when, how to cancel, and refunds; Stripe's receipt emails carry each payment.
- **Founding offers the founders-row opt-in, unticked, once bought**; the account can leave at any time.
- **Must support EEA and UK consumer withdrawal**; the voluntary guarantee is additional (rationale):
- **Must disclose the statutory withdrawal right, where the online withdrawal is, the support path, and the model form at checkout and in the `/billing` confirmation.**
- **Must offer an online withdrawal function on `/billing` throughout the statutory period, distinct from cancelling renewal**: it names the contract and where the acknowledgement goes, takes a confirmation, and gives a durable acknowledgement with the statement and its date and time, on the page and by email (rationale).
- **Must end a withdrawn contract at once and refund every payment for it in full, tax included, within 14 days**, with no use deduction or waiver for immediate access. An unambiguous notice by email or post needs no account access; support refunds it in Stripe, which ends it as a full refund does.
- **Must let a member cancel without account recovery**: in the customer portal, or by email to support when access is lost.
- **Closing an account cancels its subscription at once** (`docs/specs/hosted.md` -> "Account closure"). **Must say before closing** that access ends now and renewal stops, that the time left is not refunded, which refunds and withdrawal stay open through support, and that cancelling renewal keeps access instead.

Source of truth: `withBilling` and `openCohort` in `hosted/server/billing.ts`; `Checkout` in `hosted/src/Checkout.tsx`; `PlanConfirmation` in `hosted/src/Plan.tsx`; `docs/specs/hosted.md` -> "Billing" owns the rest.
Source of truth: `withBilling` and `openCohort` in `hosted/server/billing.ts`; `Checkout` in `hosted/src/Checkout.tsx`; `PlanConfirmation` in `hosted/src/Plan.tsx`; `WithdrawalFunction` in `hosted/src/Withdrawal.tsx`; `closureTerms` in `hosted/src/Closure.tsx`; `docs/specs/hosted.md` -> "Billing" owns the rest.

## Future

Expand Down Expand Up @@ -127,14 +133,13 @@ Part of **hosted-sales**; these remain unimplemented launch gates, not claims ab
EEA/UK representative appointment is excluded from the internal release gate by operator decision; applicable legal obligations remain unchanged (rationale).

- **Must confirm Stripe's receipt emails are on, and that they and the `/billing` confirmation meet each sales territory's durable-confirmation rule**, before charging; consent at the purchase action is "Checkout and entitlement".
- **Must provide direct online cancellation and a support cancellation path when account access is lost.** **Must cancel future renewals as part of account closure**, explaining remaining access and refund eligibility before completing closure; verification cannot require account recovery.
- **Must turn on cancellation in Stripe's customer portal before charging**, the online cancellation "Checkout and entitlement" relies on.
- **Must send jurisdiction-required renewal, annual, and price-change notices with cancellation instructions.** For California consumers, annual-term renewal notices are 15–45 days before renewal and fee-change notices 7–30 days before effectiveness; annual reminders also apply to monthly plans (rationale).
- **Must route subscription notices to the maintained billing email**, including provider-only accounts without a sign-in email; use the account contact email for other notices, or show them at sign-in when none exists. **Must record notice delivery or presentation before starting a notice period**, and provide any additional legally required notice or consent process.
- **Must notify the customer of a payment-dispute suspension** and preserve refund and dispute rights; suspension, its restoration, and final reversal are "Checkout and entitlement".
- **Must refund unused prepaid service, including corresponding collected tax, on permanent discontinuation or termination unrelated to customer breach**, and offer the same remedy for a material service reduction or rejected material terms change during a prepaid term. **Must give at least 30 days' advance notice of discontinuation, material reductions, or material terms changes**, except urgent legal or security requirements; no retroactive terms changes for disputes.
- **Must verify the permitted sales territories and tax setup before accepting payment**, and confirm refund, cancellation, failed-payment, and founder-lock behavior against the published offer. Stripe Billing does not transfer the seller's tax obligations.
- **Must support worldwide sales only where lawful, including EEA and UK consumer rights**: disclose the statutory withdrawal right and model form before purchase and in the confirmation, accept an unambiguous notice without account recovery, and refund withdrawal payments within 14 days without a use deduction or waiver for immediate access. The voluntary guarantee is additional (rationale).
- **Must provide a prominent online withdrawal function on the account billing page throughout the statutory withdrawal period**, distinct from cancelling renewal; allow the consumer to identify the contract, confirm submission, and receive a durable acknowledgement with the statement and its date and time. **Must disclose its location before purchase and in the confirmation** (rationale).
- **Must support worldwide sales only where lawful**; EEA and UK withdrawal is "Checkout and entitlement".
- **Must complete the EEA and UK provider privacy arrangements before launch**: execute applicable processor agreements, document provider transfer safeguards and assessments, and publish the applicable contacts and means to obtain the safeguards. **Never claim a transfer certification, contract, or representative that has not been verified** (rationale).
- **Must recheck ElevenLabs' training opt-out or contractual no-training protection and the applicable processing agreement before enabling paid voices**, and keep the public disclosure consistent with the verified practice. History deletion alone is not evidence of either protection (rationale).
- **Must obtain acceptance of the managed-voice customer provisions before granting paid voice access**, archive the incorporated provider requirements with the accepted terms, preserve mandatory consumer rights, and relay relevant provider notices. **Must apply the terms' age and government-use restrictions to managed voices** and verify the permitted voice selection. **Must obtain ElevenLabs' written approval before marketing that names it**; the Terms and Privacy policy name it because the provider requirements and disclosure law need them to. Provider requirement changes follow the published notice, renewed-agreement, service-reduction, and refund process (rationale).
Expand Down
Loading
Loading