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? 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.
- **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. Trace a price change too: a subscriber moved to the new Price without a notice sent in time, a founding Price changed, or a notice sent to an address that is not the Stripe billing email.
- **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: 20 additions & 1 deletion docs/specs/hosted.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,30 @@ Errors are JSON `{ message }`. The cookie routes need no entitlement, answer 401
- **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, and finish a bounded batch of unfinished withdrawals; a failure fails the invocation after every one 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, finish a bounded batch of unfinished withdrawals, and run the notices ("Billing notices"); 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`; `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.

## Billing notices

Each notice is a `dormouse_billing_notices` row, recorded before it is sent and stamped once Postmark accepts it, one per kind and renewal or dispute.

| Kind | Due | Still sent while |
|---|---|---|
| `renewal` (yearly and founding, doubling as their annual reminder) | 30 days before the renewal | the subscription is `active` and renews then |
| `annual` (monthly) | 30 days before each renewal ending a year since its start | the same |
| `price_change` | 21 days before the first renewal on or after the change's `effectiveAt` | the same, on the old Price, over 8 days ahead |
| `dispute_suspended` | a dispute opens a suspension | the dispute is open, for 30 days |
| `dispute_restored` | it closes in the account's favor, no other open | 30 days |

- **Must send every notice to the Stripe customer's email**, never the sign-in email, so a provider-only account gets them.
- **Must record a dispute's notice in the webhook's transaction** and send it after the commit; a failed send never fails the event.
- **A price change is an operator's `dormouse_price_changes` row** (`hosted/README.md`); both its Prices grant the plan once inserted. **Never act on one naming a founding Price or another plan's**, or whose Prices Stripe does not hold at its amounts, in USD, at the plan's interval.
- **Must move a subscription to the new Price, without proration, only after its notice was sent at least 7 days before the renewal it names**, while Stripe still renews it then on the old Price; a later notice waits for the next renewal. Checks: `docs/specs/security-hosted.md` -> "Account boundary".

Source of truth: `runNotices` and `sendNotices` in `hosted/server/notices.ts`; `recordDispute` in `hosted/server/billing-routes.ts`; `hosted/server/dormouse-migrations/018_billing_notices.sql`.

## Account closure

| Route | Credential | Success |
Expand Down
7 changes: 4 additions & 3 deletions docs/specs/pricing.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Source of truth: `Hosted` in `website/src/pages/Hosted.tsx`; `fetchCohort` in `w
- **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.
- **Must email renewal notices, annual reminders, and price-change notices with cancellation instructions to the billing email, each recorded before it is sent, inside California's windows**; a price change applies only from a renewal its notice reached in time, never to a founding Price (`docs/specs/hosted.md` -> "Billing notices"; rationale).
- **Must email the customer when a payment dispute suspends the subscription and when it is restored**, preserving refund and dispute rights.
- **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`; `WithdrawalFunction` in `hosted/src/Withdrawal.tsx`; `closureTerms` in `hosted/src/Closure.tsx`; `docs/specs/hosted.md` -> "Billing" owns the rest.
Expand Down Expand Up @@ -134,9 +136,8 @@ EEA/UK representative appointment is excluded from the internal release gate by

- **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 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 confirm the built notices ("Checkout and entitlement") meet each sales territory's rules beyond California's**, and provide any additional legally required notice or consent process (rationale).
- **Must send notices other than subscription notices to the account contact email, or show them at sign-in when none exists**, recording delivery or presentation before a notice period starts.
- **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**; EEA and UK withdrawal is "Checkout and entitlement".
Expand Down
6 changes: 4 additions & 2 deletions docs/specs/pricing.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
- [Directive 2023/2673](https://eur-lex.europa.eu/eli/dir/2023/2673/oj/eng) added the Consumer Rights Directive's Article 11a online withdrawal function, with national measures applying from 2026-06-19. Despite the amending directive's financial-services title, this function also covers other distance contracts concluded through an online interface (reviewed 2026-10-07).
- The online function is offered per Stripe subscription, each a new contract, for three weeks from its start: the statutory period ends at the end of the 14th day after the contract in the consumer's time zone, later when that falls on a weekend or holiday (Regulation 1182/71), so a fixed 14 × 24 hours could close it early. Eligibility reads the billing country Stripe Tax saved; an unknown country or an unreachable Stripe offers it, as do the Crown Dependencies and Gibraltar, where the doubt was resolved toward offering.

- California notice windows were checked against the [Attorney General's September 2025 guidance](https://oag.ca.gov/node/608083) on 2026-10-07: an annual term's renewal notice 15 to 45 days before renewal, a fee change 7 to 30 days before it takes effect, and an annual reminder that monthly plans get too. The chosen offsets sit inside them with room for cron retries: a price-change notice due at 21 days can be retried for two weeks before the 7-day floor. Subscription notices use the Stripe billing email because provider-only accounts can lack a sign-in email, and Checkout always collects one.

## Paid-launch requirements

- The September 29 account terms promised reasonable advance notice of material changes. The October 8 revision adds a liability cap, business indemnity, and forum clause, so publication is distinguished from acceptance and the notified transition for existing accounts. Its 30-day transition is the chosen contractual process, not a claim that every jurisdiction requires that exact period. Subscription notices use the checkout/billing email because provider-only accounts can lack a sign-in email; an undisplayed account notice is not treated as delivered.
- California notice windows were checked against the [Attorney General's September 2025 guidance](https://oag.ca.gov/node/608083) on 2026-10-07. The applicable sales jurisdictions need review before launch; a generic promise of enough time to cancel does not configure the required notices.
- The September 29 account terms promised reasonable advance notice of material changes. The October 8 revision adds a liability cap, business indemnity, and forum clause, so publication is distinguished from acceptance and the notified transition for existing accounts. Its 30-day transition is the chosen contractual process, not a claim that every jurisdiction requires that exact period. An undisplayed account notice is not treated as delivered.
- The California windows are the only ones checked so far; the other sales jurisdictions need review before launch, and a generic promise of enough time to cancel does not configure their notices.
- ElevenLabs' [retention documentation](https://elevenlabs.io/docs/eleven-api/resources/zero-retention-mode) and [model-training guidance](https://elevenlabs.io/docs/help-center/legal/is-my-data-used-to-improve-eleven-labs-ai-models), checked 2026-10-07, distinguish visible history deletion from residual records, backups, and training settings. On 2026-10-07 the workspace training opt-out was saved in the system browser and verified off after a full reload. The Dormouse Hosted service-account key was restricted to Text to Speech and History write, with a 10,000-credit refresh-period limit and leak auto-disable enabled. The operator separately confirmed history isolation. These facts do not establish zero retention or retroactively undo training; section 4(i) of the [provider terms](https://elevenlabs.io/terms-of-use) makes the opt-out prospective after processing.
- The [ICO's representative guidance](https://ico.org.uk/for-organisations/uk-gdpr-guidance-and-resources/international-transfers/receiving-personal-information-from-the-eea/) describes separate EEA and UK representation duties and the narrow occasional-processing exception; [its transfer guidance](https://ico.org.uk/for-organisations/uk-gdpr-guidance-and-resources/international-transfers/a-brief-guide-to-international-transfers/) distinguishes adequacy from other safeguards. Provider locations alone do not establish either arrangement (reviewed 2026-10-07).
- On 2026-10-08 the operator declined to appoint EEA or UK representatives at this stage and accepted the resulting compliance risk. Appointment is therefore excluded from the internal launch gate; this records a business decision, not a legal exemption. The published privacy contact is DiffPlug's support address and does not purport to be a local representative.
Expand Down
Loading
Loading