From 017b53555f4ba69d95b1f2f526db41332ef812fc Mon Sep 17 00:00:00 2001 From: shum Date: Wed, 22 Jul 2026 16:41:02 +0000 Subject: [PATCH] docs: rework badge auth, cancel, and recovery model --- ...7-20-supporter-badges-v2-implementation.md | 138 +++++++++++++----- .../2026-07-20-supporter-badges-v2-product.md | 28 +++- plans/assets/badge-v2-providers.svg | 2 +- plans/assets/badge-v2-roles.svg | 4 +- plans/assets/badge-v2-screen-s2.svg | 2 +- 5 files changed, 127 insertions(+), 47 deletions(-) diff --git a/plans/2026-07-20-supporter-badges-v2-implementation.md b/plans/2026-07-20-supporter-badges-v2-implementation.md index 59667176ac..0acf1fa015 100644 --- a/plans/2026-07-20-supporter-badges-v2-implementation.md +++ b/plans/2026-07-20-supporter-badges-v2-implementation.md @@ -17,10 +17,11 @@ Payment verification creates a provider-neutral `ServiceGrant`. Badge issuance f - [5. Persistence and `CallState` pattern](#5-persistence-and-callstate-pattern) - [6. Reconciliation and errors](#6-reconciliation-and-errors) - [7. Provider rules](#7-provider-rules) -- [8. Security and concurrency](#8-security-and-concurrency) -- [9. Delivery and tests](#9-delivery-and-tests) -- [10. API references](#10-api-references) -- [11. Open question](#11-open-question) +- [8. Recovery](#8-recovery) +- [9. Security and concurrency](#9-security-and-concurrency) +- [10. Delivery and tests](#10-delivery-and-tests) +- [11. API references](#11-api-references) +- [12. Open questions](#12-open-questions) ## 1. Architecture @@ -34,7 +35,7 @@ flowchart LR | Component | Owns | Must not own | |---|---|---| -| Client payment | BBS master key, capability, purchase UI, cached status, retry schedule | bot/provider truth | +| Client payment | BBS master key, purchase UI, cached status, retry schedule | bot/provider truth | | Client badge | credential receipt and installation | billing state | | Payment service | BBS owner commitment, proof verification, billing state, grant schedule | raw master key, credential | | Service grant | product and eligible monthly grant period | provider proof, credential | @@ -51,7 +52,7 @@ Treat these as separate programs with typed interfaces. 4. Only `GrantReady` plus the matching raw master key enters badge signing. 5. Grant fulfillment and cached issuance result are atomic/idempotent. 6. Payment never activates perks; verified credential does. -7. RPC has no caller identity or bot push. Capability authorizes each payment request. +7. RPC has no caller identity, no bot-issued token, and no bot push. Each request carries its own credential — a fresh provider proof (Apple/Google) or a `BadgeMasterKey` possession proof (Stripe) — verified against the payment binding. 8. Duplicate RPCs/events return the same result. Unknown states preserve prior state. 9. Provider dates create eligibility; retry/request time never changes badge expiry. @@ -112,7 +113,7 @@ These names are canonical. Every transition is validated against the current con | State | Meaning | |---|---| -| `BPPrepared` | payment/capability/binding stored | +| `BPPrepared` | payment/commitment/binding stored | | `BPCheckoutOpen` | Stripe Session stored | | `BPAwaitingPayment` | provider not complete | | `BPVerifying` | reconciliation lease active | @@ -166,12 +167,22 @@ data ServiceCall = ServiceCall data PaymentInput = Prepare Provider ServiceProductId PurchaseKind BadgeKeyCommitment - | AppleEvidence PaymentId Capability SignedTransactionJWS - | GoogleEvidence PaymentId Capability PurchaseToken - | ExistingPayment PaymentId Capability - | CancelSubscription PaymentId Capability - | CreatePortal PaymentId Capability + | AppleEvidence BadgeKeyCommitment SignedTransactionJWS + | GoogleEvidence BadgeKeyCommitment PurchaseToken + | StripeStatus BadgeKeyProof + | StripeCancel BadgeKeyProof + | StripePortal BadgeKeyProof +-- BadgeKeyProof is the raw BadgeMasterKey, revealed over the E2E channel; +-- the bot re-derives BadgeKeyCommitment = SHA-256(domain || key) and matches. +-- This reuses issuance, which already sends the key, so it exposes nothing new. +-- +-- No PaymentId on the wire: each Prepare uses a fresh BadgeMasterKey, so +-- BadgeKeyCommitment is 1:1 with the payment and selects it. Apple/Google +-- carry the commitment and authenticate by fresh store proof; Stripe calls +-- carry a BadgeKeyProof, which both selects (via commitment) and authenticates. +-- PaymentId (below) is an internal primary/foreign key only. +type BadgeKeyProof = BadgeMasterKey data ServiceRequest = IssueBadge BadgeMasterKey (Maybe GrantId) data ServiceResponse = ServiceResponse @@ -185,13 +196,16 @@ data ServiceResponse = ServiceResponse Rules: +- Each `Prepare` uses a fresh `BadgeMasterKey`; renewals of the same subscription reuse it without a new `Prepare`. So `BadgeKeyCommitment` is unique per payment and is the wire selector; `PaymentId` never appears on the wire. - Client calls `generateMasterKey` once before `Prepare`, persists it encrypted, computes `BadgeKeyCommitment`, and reuses the key for all renewal badges. - `Prepare` stores `BadgeKeyCommitment` but cannot issue a badge. - Apple/Google evidence may include `IssueBadge`. - Stripe prepare returns Checkout data. The client then sends `IssueBadge`; while payment is pending the bot holds that call and sends no response. - The waiting call responds once after verified payment and issuance, or with a terminal payment error. Other retryable operations may still return `retryAfter`. -- Capability and `BadgeKeyCommitment` never enter Stripe metadata or a return URL. +- The `BadgeMasterKey`, its `BadgeKeyProof`, and `BadgeKeyCommitment` never enter Stripe metadata or a return URL. - `grantId` selects only; bot rechecks payment, owner, product, and eligibility. +- `StripeCancel`/`StripePortal` return a portal URL in the payment snapshot (like the Checkout URL); the bot never cancels silently. `StripeCancel` deep-links to the cancel flow; `StripePortal` opens general management. When the request carries no valid `BadgeKeyProof` (customer cannot be identified), the bot returns the account-wide login page instead of a session. +- There is no bot-issued authorization token and no `PaymentId` on the wire. The payment is selected by `BadgeKeyCommitment` (Apple/Google carry it explicitly; a Stripe `BadgeKeyProof` references it). Each RPC also authenticates: Apple/Google by fresh store proof, Stripe by the `BadgeKeyProof`. The bot resolves and verifies before acting. - Before signing, orchestrator recomputes `BadgeKeyCommitment` from `BadgeMasterKey`; mismatch returns `ownership_conflict` without fulfilling grant. ### Internal interface @@ -203,7 +217,7 @@ fulfillBadge :: ServiceGrant -> BadgeRequest -> Transaction BadgeResult Order: -1. authorize capability; +1. select the payment by `BadgeKeyCommitment` and authenticate the request credential (store proof, or `BadgeKeyProof` against that commitment); 2. resolve/verify payment; 3. commit payment and create/load due grant; 4. verify the request key matches the payment `BadgeKeyCommitment`; @@ -217,7 +231,7 @@ Order: - Transport replay dedupe is separate and shorter-lived. - Stripe mutation idempotency key derives from request ID + operation. - Developer Tools → Chat Console records start/result, request ID, method, payment suffix, before/after states, retry class, and duration. -- Redact capability, JWS/token, Checkout query/return token, master key, credential, and provider/customer IDs. +- Redact JWS/token, `BadgeKeyProof`, Checkout query/return token, master key, credential, and provider/customer IDs. ## 4. Provider flows @@ -302,11 +316,11 @@ sequenceDiagram B->>S: Create Checkout Session S-->>B: Session ID + URL Note over B: BPCheckoutOpen - B-->>C: RPC payment ID + capability + URL + B-->>C: RPC Checkout URL Note over C: CPCheckoutReady C->>S: Open Checkout Note over C: CPAwaitingPayment - C->>B: RPC IssueBadge(requestId, paymentId, key) + C->>B: RPC IssueBadge(requestId, key) Note over C: CPAwaitingPayment + CBRequesting B->>B: Register waiter and recheck payment under lock Note over B: BPAwaitingPayment
No response yet @@ -351,7 +365,7 @@ sequenceDiagram B-->>C: Credential immediately if ready
otherwise hold this call ``` -The client persists `requestId`, payment capability, and `BadgeMasterKey` before opening Checkout. It retries only after an interrupted exchange, foreground, or explicit user action—never on a polling timer. A deep link is optional UX; no localhost listener is used. +The client persists `requestId` and `BadgeMasterKey` before opening Checkout. It retries only after an interrupted exchange, foreground, or explicit user action—never on a polling timer. A deep link is optional UX; no localhost listener is used. ### Cancellation @@ -359,9 +373,18 @@ The client persists `requestId`, payment capability, and `BadgeMasterKey` before |---|---|---|---| | Apple | open Apple management UI; status RPC on return | App Store Server API status | `BPEndsAtPeriodEnd` | | Google | open Play management UI; status RPC on return | `subscriptionsv2.get` | `BPEndsAtPeriodEnd` | -| Stripe | `CancelSubscription` RPC | set `cancel_at_period_end=true`, retrieve Subscription | `BPEndsAtPeriodEnd` | +| Stripe | open a browser Customer Portal from a bot-provided link | return a portal link (session or login page); the portal performs the cancel, reconciled via `customer.subscription.updated` webhook | `BPEndsAtPeriodEnd` | -Failure preserves previous state; client shows Retry and still says **Renews on**. “Already canceled” is success. Stripe Portal cancellation is disabled. +Failure preserves previous state; client shows Retry and still says **Renews on**. “Already canceled” is success. The bot never cancels a Stripe subscription itself: the hosted Customer Portal calls `cancel_at_period_end`, and the bot reconciles it from the webhook. + +**Stripe cancel-link selection.** Cancellation is always in the browser portal; the bot chooses which link it returns based on whether the request identifies the customer: + +| Client presents | Portal link the bot returns | +|---|---| +| valid `BadgeKeyProof` (matches stored `BadgeKeyCommitment`) | authenticated `billing_portal.Session` with `flow_data.type=subscription_cancel` — opens straight to the cancel flow, no email code | +| no valid proof (master key lost with the app) | the account-wide hosted portal **login page** (`prefilled_email` when the customer email is known), authenticated by email OTP | + +The authenticated session link is short-lived and per-customer; the login page is the operator-config account-wide URL and returns no per-customer secret. The bot carries whichever link applies in Stripe status responses so a cancel path is always reachable. Because possession of the `BadgeMasterKey` is the sole client credential, there is no intermediate "capability lost" state: the client either can prove key possession (session) or cannot (login page). ## 5. Persistence and `CallState` pattern @@ -382,7 +405,7 @@ Define five separate sums: client payment, client badge, bot payment, grant, bot ### Client tables -`badge_payments`: provider/product/plan, payment state payload, encrypted capability and `BadgeMasterKey`, `BadgeKeyCommitment`, binding/proof reference, `paidThrough`, `willRenew`, checked/retry time, version. +`badge_payments`: provider/product/plan, payment state payload, encrypted `BadgeMasterKey`, `BadgeKeyCommitment`, binding/proof reference, `paidThrough`, `willRenew`, checked/retry time, version. `badges`: payment/grant/grant-period/key hash, badge state payload, cached credential, expiry, attempt/error, version. @@ -408,7 +431,7 @@ Provider calls/signing run outside long transactions. Leases and compare-and-swa Triggers: launch, foreground, profile switch, network restore, store update, Stripe browser return, manual retry, six-hour jittered timer, and date boundaries. ```text -reconcile(paymentId): +reconcile(payment): coalesce to one worker render cached payment + installed badge submit unseen Apple/Google evidence @@ -438,7 +461,7 @@ Every input is one of: | deadline/restart/lost response | retry on foreground | preserve state; repeat same ID/body | remove waiter; return cached result or wait again | | duplicate event/request | idempotent | accept same state/result | preserve state; dedupe/re-fetch | | ID reused with new body | reject | preserve state; new ID only for new action | preserve state; telemetry | -| invalid capability/binding | reject | preserve state; restore/support | preserve state; rate-limit | +| invalid proof/binding | reject | preserve state; restore/support | preserve state; rate-limit | | invalid proof/product | reject | `CPPaymentProblem`; no blind retry | preserve `BP…`; quarantine/alert | | unknown provider state | quarantine | `CPPaymentProblem`; retry later | preserve `BP…`; re-fetch, never guess | | `GrantReady` | apply | `CBNeeded` → `CBRequesting` | `BBRequested` when requested | @@ -471,7 +494,7 @@ Stable codes: `bad_request`, `unsupported_version`, `payment_pending`, `payment_ |---|---|---|---|---| | Apple | offline signed initial transaction; server API later | subscription: original transaction + renewal transaction | Notifications V2 → re-fetch | store UI | | Google | products v2 / subscriptions v2 GET | linked token chain + order/period | RTDN → re-fetch | Play UI | -| Stripe | retrieve Session/Intent/Invoice/Subscription | one-time intent/session; subscription paid invoice | signed webhook → re-fetch | bot RPC | +| Stripe | retrieve Session/Intent/Invoice/Subscription | one-time intent/session; subscription paid invoice | signed webhook → re-fetch | browser portal (bot-provided link) | Provider-state mapping: @@ -495,15 +518,60 @@ Google linked-token replacement changes subscription identity/period data, then Rules: - Google initial subscription acknowledgement and one-time consumption run from durable outbox. -- Stripe uses server-selected Price, mode, Customer, `client_reference_id=paymentId`, metadata, and redirect URLs. +- Stripe uses server-selected Price, mode, Customer, `client_reference_id=paymentId`, metadata, redirect URLs, and collects customer email so the hosted portal login works. - Stripe subscription grant requires a paid invoice, not merely active Subscription status. - Webhook/status/completion page use one reconciliation function; redirects never fulfill. -- Portal is for invoices/payment methods only. Apple/Google normal cancellation is store UI. +- All Stripe cancellation, invoices, and payment methods go through the browser Customer Portal — an authenticated `billing_portal.Session` when the customer is identifiable, else the account-wide login page (email OTP) which is also the app-removed path; the bot reconciles portal cancellation from the webhook. Apple/Google normal cancellation is store UI. -## 8. Security and concurrency +## 8. Recovery + +Recovery re-establishes payment control and the badge after reinstall, device transfer, or local data loss. There is no bot-issued token and no caller identity, so a reinstalled client is a new contact; it re-attaches by presenting a credential the bot matches to a stored payment. + +### State ownership + +| Side | Durable | Lost on client wipe without backup | +|---|---|---| +| Bot | payment (keyed by `BadgeKeyCommitment`), provider bindings, `sub_`/`cus_`, grants, cached credential | — | +| Client | — | `BadgeMasterKey` (commitment re-derives from it), cached credential, installed badge | + +The bot never loses the payment. `BadgeMasterKey` is the only client secret to protect; all other client state is re-derivable. + +### Restore from backup + +SimpleX encrypted-profile backup or migration restores `BadgeMasterKey`, `BadgeKeyCommitment`, and the cached badge. No recovery RPC runs. This is the primary path. + +### Re-attach when only the master key survives + +With `BadgeMasterKey` retained, the client re-derives `BadgeKeyCommitment` and re-attaches by presenting a provider credential (the store proof for Apple/Google, a key-possession proof for Stripe): + +| Provider | Credential | Bot match | Result | +|---|---|---|---| +| Apple | signed transaction (`Transaction.currentEntitlements`) | original-transaction binding | re-attached; status/grant refreshed | +| Google | purchase token (`queryPurchases`) | linked-token/order binding | re-attached; status/grant refreshed | +| Stripe | `BadgeKeyProof` | `BadgeKeyCommitment` → `payments` row | re-attached; status/grant refreshed (portal session if requested) | + +### Badge re-issuance + +Re-attaching does not install a badge. Reconciliation then issues from the eligible or fulfilled grant. Issuance is idempotent on `(grant, master-key hash)`: the same `BadgeMasterKey` returns the same cached credential — no new charge, no duplicate. A one-time grant already `GrantFulfilled` returns the cached credential. + +### Lost master key + +If `BadgeMasterKey` is lost with no backup, the payment cannot be re-attached: + +- Apple/Google: unaffected; the client re-presents store proof and cancellation is store-side. +- Stripe: cancellation falls back to the hosted portal login page (email OTP); billing continues until canceled there or the card lapses. +- Installed badges remain valid to signed expiry; no new issuance against the lost key. + +### Abuse controls + +- Verify the credential before re-attaching; never re-attach on an unauthenticated selector such as a bare `BadgeKeyCommitment`. +- Rate-limit attempts per `BadgeKeyCommitment` and per provider binding; the `BadgeKeyProof` nonce prevents replay. +- Re-attaching changes only the client association; it never mutates provider or billing state. + +## 9. Security and concurrency - Verify provider signatures/objects server-side; never trust decoded client/redirect fields. -- Hash capabilities; encrypt retained proofs/provider IDs; rotate keys. +- Encrypt retained proofs/provider IDs; rotate keys. Select the payment by `BadgeKeyCommitment` and authorize only on a verified proof (store proof or `BadgeKeyProof`); never act on an unauthenticated selector. - Keep raw `BadgeMasterKey` client-encrypted and bot-memory-only during ownership verification/signing; persist only domain-separated `BadgeKeyCommitment`. - Allowlist product, app/package, environment, currency/price, and account binding. - Rate-limit operation/payment and cap payload sizes. @@ -511,11 +579,11 @@ Rules: - Use outbox for provider actions/events. Alert on stale leases, acknowledgement deadline, webhook lag, and signing failures. - Trust client-shipped issuer keys; unknown key/protocol requires update. -## 9. Delivery and tests +## 10. Delivery and tests 1. **Schema/protocol:** five sums/codecs, migrations, grant boundary, request ledger, Chat Console audit, core install API. 2. **Apple/Google:** bindings, verification/status, Notifications V2/RTDN, acknowledge/consume, native UI. -3. **Stripe:** Checkout, waiting `IssueBadge`, webhook wake-up, reconciliation, cancel RPC, restricted Portal. +3. **Stripe:** Checkout, waiting `IssueBadge`, webhook wake-up, reconciliation, portal link (authenticated session + login-page fallback), portal cancellation + webhook reconciliation. 4. **UX/hardening:** scheduler, all Product states, rollout compatibility, telemetry, cleanup. Tests: @@ -527,7 +595,7 @@ Tests: - Stripe async payment, invoice renewal, cancellation, closed app/browser, delayed/duplicate/reordered webhook; - monthly/yearly grant periods and 21 July → 31 August expiry; - crash/replay at every side-effect boundary; -- capability/grant/BBS-owner isolation and wrong-`BadgeMasterKey` rejection; +- `BadgeKeyProof`/grant/BBS-owner isolation and wrong-`BadgeMasterKey` rejection; - Chat Console coverage and redaction snapshots. Release gates: provider sandbox E2E, webhook signature/replay, schema rollback, store-policy review, complete error handling, operational dashboards. @@ -548,15 +616,15 @@ Release gates: provider sandbox E2E, webhook signature/replay, schema rollback, | `badge-service/stripe_api.py` | Checkout/webhook/status/cancel/Portal | | `badge-service/wire.py` | versioned call/response; keep existing badge request compatibility during rollout | -## 10. API references +## 11. API references | Provider | References | |---|---| | Apple | [StoreKit](https://developer.apple.com/storekit/), [subscription statuses](https://developer.apple.com/documentation/appstoreserverapi/get-all-subscription-statuses), Notifications V2 | | Google | [Play Billing](https://developer.android.com/google/play/billing/integrate), [`productsv2.getproductpurchasev2`](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.productsv2/getproductpurchasev2), [`subscriptionsv2.get`](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptionsv2/get), RTDN | -| Stripe | [Checkout](https://docs.stripe.com/api/checkout/sessions/create), [fulfillment](https://docs.stripe.com/checkout/fulfillment), [webhooks](https://docs.stripe.com/webhooks), [subscription events](https://docs.stripe.com/billing/subscriptions/webhooks), [cancel](https://docs.stripe.com/billing/subscriptions/cancel), [Portal](https://docs.stripe.com/customer-management/integrate-customer-portal) | +| Stripe | [Checkout](https://docs.stripe.com/api/checkout/sessions/create), [fulfillment](https://docs.stripe.com/checkout/fulfillment), [webhooks](https://docs.stripe.com/webhooks), [subscription events](https://docs.stripe.com/billing/subscriptions/webhooks), [cancel](https://docs.stripe.com/billing/subscriptions/cancel), [Portal](https://docs.stripe.com/customer-management/integrate-customer-portal), [hosted portal login](https://docs.stripe.com/customer-management/activate-no-code-customer-portal) | | RPC | [`simplexmq` service RPC RFC](https://github.com/simplex-chat/simplexmq/blob/rpc/rfcs/2026-07-11-service-rpc.md) | -## 11. Open question +## 12. Open questions -**Capability recovery:** if the client still has its BBS `BadgeMasterKey` but loses the payment capability after reinstall or device transfer, should key possession recover/rotate that capability? Define the recovery RPC, provider re-verification, rate limits, and user confirmation before implementation. Until then, do not automatically reassign a payment. +**Lost key.** Recovery §8 defines the behavior when `BadgeMasterKey` is lost with no backup (Stripe cancellation falls back to the portal login page; badge expires normally; Apple/Google unaffected). Decision: accept this, or add an optional user-held recovery code as a second Stripe recovery credential. diff --git a/plans/2026-07-20-supporter-badges-v2-product.md b/plans/2026-07-20-supporter-badges-v2-product.md index 9335640894..6c28ea1b91 100644 --- a/plans/2026-07-20-supporter-badges-v2-product.md +++ b/plans/2026-07-20-supporter-badges-v2-product.md @@ -25,7 +25,7 @@ Payment and badge are separate: payment creates a service grant for an eligible |---|---|---| | iOS | StoreKit | Apple subscription UI | | Android Play | Play Billing | Google Play subscription UI | -| F-Droid / desktop | Stripe Checkout | cancel RPC; Customer Portal for invoices/payment methods | +| F-Droid / desktop | Stripe Checkout | browser Customer Portal — authenticated session, or login page (email code) after app removal | Choices: **One-time**, **Monthly**, **Yearly**. There is no Extend action. @@ -52,7 +52,7 @@ Show **Badge valid until** separately from **Renews on** or **Subscription ends - Store state and Stripe redirects are hints only. - Each client RPC call receives exactly one bot response; the bot never initiates a call. - Before payment, the client generates one 32-byte BBS `BadgeMasterKey`. The payment and every badge issued from it are bound to that key; renewals reuse it. -- Payment capability authorizes bot requests. The raw BBS key, capability, and provider proofs are redacted. +- There is no bot-issued token. Each request carries its own credential — a fresh store proof (Apple/Google) or a `BadgeMasterKey` possession proof (Stripe). The raw BBS key and provider proofs are redacted. ## 2. UX states @@ -210,7 +210,7 @@ sequenceDiagram participant S as Stripe C->>B: RPC Prepare Stripe B->>S: Create Checkout Session - B-->>C: RPC Checkout URL + capability + B-->>C: RPC Checkout URL C->>S: Open Checkout Note over C: CPAwaitingPayment C->>B: RPC IssueBadge @@ -301,16 +301,28 @@ sequenceDiagram sequenceDiagram participant C as Client participant B as Bot + participant P as Stripe Portal participant S as Stripe API - C->>B: Cancel RPC - B->>S: Cancel at period end - S-->>B: Renewal off + end date - B-->>C: Updated status + C->>B: Request cancel link + B-->>C: Portal URL (session or login page) + C->>P: Open portal, confirm cancel + P->>S: Cancel at period end + S-->>B: Signed webhook (renewal off + end date) + B-->>C: Updated status on next check Note over C: Canceled, active until end date ``` Never show canceled until the bot confirms renewal is off. +Cancellation also works after the app is removed: Apple/Google via the store subscription UI; Stripe via the hosted Customer Portal login page (`billing.stripe.com/p/login/…`), where the user signs in with the email they paid with. The bot reconciles the resulting cancellation from the provider webhook/status. + +Stripe cancellation always happens in the browser Customer Portal (the portal cancels; the bot reconciles from the webhook). The bot only chooses which link it sends, based on what the client can still prove: + +| Client still holds | Cancel link the bot sends | +|---|---| +| the master key (proves ownership) | authenticated portal session — opens straight to the cancel flow, no email code | +| nothing (key lost with the app) | generic hosted portal login page — user signs in with the email they paid with (email code) | + ## 5. Refresh and errors Refresh on launch, foreground, profile switch, network restore, store update, Stripe browser return, manual retry, six-hour jittered timer, and payment/badge date boundaries. @@ -341,6 +353,6 @@ Errors preserve the last payment snapshot and installed badge. The implementatio - Payment verification creates a provider-neutral service grant; badge service has no provider logic. - Client and bot payment/badge states are separate. - RPC is client-request/bot-response only and idempotent. -- Stripe needs no localhost/deep-link success and cancels through bot RPC. +- Stripe needs no localhost/deep-link success; cancellation is always via the browser Customer Portal — an authenticated session, or the login page when the client cannot identify the payment. - Every error category has an owner, state-preserving action, and retry/final result. - RPC attempts/results appear redacted in Developer Tools → Chat Console. diff --git a/plans/assets/badge-v2-providers.svg b/plans/assets/badge-v2-providers.svg index d842d2ef4d..a04d492359 100644 --- a/plans/assets/badge-v2-providers.svg +++ b/plans/assets/badge-v2-providers.svg @@ -1 +1 @@ -Provider capability mapProviderPurchaseCanonical statusCancel renewalAppleStoreKit 2signed JWSStoreKit hint +App Store Server APIApple management UI(no app cancel API)GooglePlay Billingpurchase tokensubscriptionsv2.getRTDN triggers re-fetchPlay subscriptions UIAPI for operator recoveryStripeCheckout Sessionhosted browsersigned webhooks +server API refreshcancel_at_period_endbot RPC only \ No newline at end of file +Provider capability mapProviderPurchaseCanonical statusCancel renewalAppleStoreKit 2signed JWSStoreKit hint +App Store Server APIApple management UI(no app cancel API)GooglePlay Billingpurchase tokensubscriptionsv2.getRTDN triggers re-fetchPlay subscriptions UIAPI for operator recoveryStripeCheckout Sessionhosted browsersigned webhooks +server API refreshhosted Customer Portalbrowser, bot link \ No newline at end of file diff --git a/plans/assets/badge-v2-roles.svg b/plans/assets/badge-v2-roles.svg index 4b05e4ec98..050beb2d64 100644 --- a/plans/assets/badge-v2-roles.svg +++ b/plans/assets/badge-v2-roles.svg @@ -1,6 +1,6 @@ Responsibility and trust boundaries -CLIENT ORCHESTRATOR• separate payment + badge machines• native store / hosted browser UI• owns capability + master key• asks through one-off RPC• core verifies and installs badge• reconciliation and local audit -PAYMENT SERVICE• capability authorization• provider verification/status• billing and grant schedule• cancellation where supported• never sees badge master key +CLIENT ORCHESTRATOR• separate payment + badge machines• native store / hosted browser UI• owns master key (sole credential)• asks through one-off RPC• core verifies and installs badge• reconciliation and local audit +PAYMENT SERVICE• store proof / key-possession auth• provider verification/status• billing and grant schedule• cancellation where supported• never sees badge master key SERVICE GRANT• provider-neutral authorization• one monthly grant period BADGE SERVICE• accepts grant + badge request• signs and caches credential• fulfills grant atomically• never imports provider logic• never changes payment state PROVIDER ADAPTERSApple · Google · Stripe diff --git a/plans/assets/badge-v2-screen-s2.svg b/plans/assets/badge-v2-screen-s2.svg index da00d74108..6e08212f89 100644 --- a/plans/assets/badge-v2-screen-s2.svg +++ b/plans/assets/badge-v2-screen-s2.svg @@ -25,5 +25,5 @@ Cancel subscription - Apple/Google: store · Stripe: bot RPC + Apple/Google: store · Stripe: browser portal