docs: make Stripe badge issuance event-driven

This commit is contained in:
shum committed 2026-07-21 15:25:26 +00:00
1 parent 67bc10d47e
commit 4defe642d2
2 files changed
+87 -60

No files matched your search

@@ -188,8 +188,8 @@ Rules:
- 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; a later `ExistingPayment + IssueBadge` issues.
- Pending response has no grant/result and includes `retryAfter`.
- 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.
- `grantId` selects only; bot rechecks payment, owner, product, and eligibility.
- Before signing, orchestrator recomputes `BadgeKeyCommitment` from `BadgeMasterKey`; mismatch returns `ownership_conflict` without fulfilling grant.
@@ -209,7 +209,7 @@ Order:
4. verify the request key matches the payment `BadgeKeyCommitment`;
5. pass only grant + request to badge service;
6. cache issuance and mark grant fulfilled atomically;
7. return one final response.
7. return the single response.
### Idempotency and audit
@@ -289,7 +289,7 @@ sequenceDiagram
Commit entitlement before outbox acknowledgement/consume. RTDN triggers provider GET; never create a service grant from the notification payload.
### Stripe Checkout and webhook
### Stripe Checkout and waiting `IssueBadge`
```mermaid
sequenceDiagram
@@ -298,44 +298,60 @@ sequenceDiagram
participant S as Stripe
Note over C: CPPreparing
C->>B: RPC Prepare Stripe
Note over B: Payment: BPPrepared
Note over B: BPPrepared
B->>S: Create Checkout Session
S-->>B: Session ID + URL
Note over B: Payment: BPCheckoutOpen
Note over B: BPCheckoutOpen
B-->>C: RPC payment ID + capability + URL
Note over C: CPCheckoutReady
C->>S: Open Checkout
Note over C: CPAwaitingPayment
C->>B: RPC IssueBadge(requestId, paymentId, key)
Note over C: CPAwaitingPayment + CBRequesting
B->>B: Register waiter and recheck payment under lock
Note over B: BPAwaitingPayment<br/>No response yet
S-->>B: Signed webhook
Note over B: Payment: BPVerifying
Note over B: BPVerifying
B->>S: Retrieve current payment
S-->>B: Canonical payment
Note over B: Payment: BPPaidOneTime or BPSubscriptionActive
B->>B: Create or load due grant period
Note over B: Grant: GrantReady
Note over C: Still CPAwaitingPayment, no bot push
S-->>B: Canonical paid payment
Note over B: BPPaidOneTime or BPSubscriptionActive
B->>B: Create GrantReady and wake waiter
B->>B: Verify key, sign badge, mark GrantFulfilled
B-->>C: RPC credential
Note over C: CPEntitled + CBReceived
C->>C: Verify and install
Note over C: CBInstalled
```
Webhook handler verifies the raw body, persists/deduplicates event ID, returns `2xx`, then workers reconcile. Client remains pending until its next RPC.
The second RPC has exactly one response. The bot sends it only after verified payment allows issuance, or after a terminal event such as Checkout expiry. Register-and-recheck under the payment lock prevents a webhook/request race. If the webhook completed first, `IssueBadge` responds immediately.
### Stripe status while pending
If Stripe retrieval fails transiently after the webhook, the bot keeps the call open and retries internally. It does not send an intermediate response; only the RPC deadline or a terminal payment result ends the wait.
Persist request ID, hash, and result state; keep the live waiter and raw `BadgeMasterKey` only in memory. Webhook commit wakes live waiters after `GrantReady` is durable. After bot restart, the repeated request rechecks persisted payment/grant state and either returns immediately or installs a new waiter.
### Stripe wait interruption
```mermaid
sequenceDiagram
participant C as Client
participant B as Bot
participant S as Stripe
Note over C: CPVerifying
C->>B: RPC ExistingPayment
Note over B: Payment: BPVerifying
B->>S: Retrieve Checkout
S-->>B: Pending / unpaid
Note over B: Payment: BPAwaitingPayment
B-->>C: RPC pending + retryAfter
Note over C: CPAwaitingPayment
Note over C: CPAwaitingPayment + CBRequesting
C->>B: RPC IssueBadge with stable requestId
B->>B: Register waiter, payment still pending
Note over B: Hold call, no response
Note over C: RPC deadline / app restart
C-xB: Exchange removed
Note over C: CPAwaitingPayment + CBRetryableFailure
Note over B: Remove waiter, preserve payment state
S-->>B: Webhook may complete payment later
Note over B: Persist payment + GrantReady
C->>B: Repeat same IssueBadge on foreground
Note over C: CPAwaitingPayment + CBRequesting
B-->>C: Credential immediately if ready<br/>otherwise hold this call
```
Poll from Checkout open and on return/foreground at 5, 15, 30, 60, 120 seconds. Then use normal reconciliation. Deep links are optional; no localhost listener.
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.
### Cancellation
@@ -379,7 +395,7 @@ Join by payment/grant ID only. Update active profile only after core installatio
| `payments` | provider-object ownership + `BadgeKeyCommitment`; canonical payment sum |
| `service_grants` | payment + product + grant period |
| `badge_issuances` | grant + master-key hash; cached credential |
| `rpc_requests` | request ID; request hash + final response |
| `rpc_requests` | request ID; pending waiter/result + request hash + response |
| `provider_events` | provider event ID; dedupe/result |
| `outbox` | acknowledge, consume, reconciliation, cleanup |
@@ -396,7 +412,8 @@ reconcile(paymentId):
coalesce to one worker
render cached payment + installed badge
submit unseen Apple/Google evidence
request status for nonterminal payment
for pending Stripe Checkout: ensure one IssueBadge call is waiting
otherwise request status for nonterminal payment
if GrantReady and no badge covers its grant period: CBNeeded -> request IssueBadge
if credential returned: CBReceived -> CBInstalling -> CBInstalled
schedule next check
@@ -415,9 +432,10 @@ Every input is one of:
| Input/result | Class | Client | Bot |
|---|---|---|---|
| payment pending | retry | `CPAwaitingPayment`; schedule | `BPAwaitingPayment`; no grant |
| timeout/429/5xx | retry | `CPPaymentProblem` with prior snapshot | preserve current `BP…`; return `retryAfter` |
| lost response | retry | preserve state; repeat same ID/body | return cached result/state |
| Stripe awaiting webhook | wait | `CPAwaitingPayment` + `CBRequesting` | hold `IssueBadge`; no response |
| timeout/429/5xx in a non-waiting operation | retry | `CPPaymentProblem` with prior snapshot | preserve current `BP…`; return `retryAfter` |
| Stripe verification timeout/429/5xx while `IssueBadge` waits | wait; retry internally | `CPAwaitingPayment` + `CBRequesting` | preserve canonical payment state; retry Stripe; send no response |
| 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 |
@@ -440,6 +458,7 @@ Stable codes: `bad_request`, `unsupported_version`, `payment_pending`, `payment_
### Crash recovery
- Before provider call: repeat request.
- Waiting `IssueBadge` lost on deadline/restart: remove waiter; repeat the same logical request on foreground.
- Provider succeeds before commit: retrieve by idempotency key/object binding.
- Payment committed before issuance: `GrantReady` remains unchanged.
- Credential cached before response loss: repeat returns it.
@@ -496,7 +515,7 @@ Rules:
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, completion page, webhook, reconciliation, cancel RPC, restricted Portal.
3. **Stripe:** Checkout, waiting `IssueBadge`, webhook wake-up, reconciliation, cancel RPC, restricted Portal.
4. **UX/hardening:** scheduler, all Product states, rollout compatibility, telemetry, cleanup.
Tests:
+39 -31
View File
@@ -50,7 +50,7 @@ Show **Badge valid until** separately from **Renews on** or **Subscription ends
- Core signature and expiry decide badge validity.
- Bot/provider verification decides payment status and grant eligibility.
- Store state and Stripe redirects are hints only.
- Client asks through RPC; bot returns one final response and never pushes.
- 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.
@@ -208,48 +208,56 @@ sequenceDiagram
participant C as Client
participant B as Bot
participant S as Stripe
C->>B: Prepare Stripe payment
C->>B: RPC Prepare Stripe
B->>S: Create Checkout Session
B-->>C: Checkout URL + capability
B-->>C: RPC Checkout URL + capability
C->>S: Open Checkout
Note over C: Payment pending, start polling
Note over C: CPAwaitingPayment
C->>B: RPC IssueBadge
Note over B: Hold call, no response yet
S-->>B: Signed webhook
Note over B: Payment entitled, grant available
C->>B: Status + badge request
B-->>C: Status + badge
Note over C: Entitled, badge installed
B->>S: Retrieve and verify payment
S-->>B: Paid
Note over B: GrantReady → badge issued
B-->>C: RPC badge credential
Note over C: CPEntitled + CBReceived
C->>C: Verify and install
Note over C: CBInstalled
```
#### Still pending
#### RPC ends before payment
```mermaid
sequenceDiagram
participant C as Client
participant B as Bot
C->>B: RPC IssueBadge
B->>B: Persisted payment is still pending
Note over B: Hold call, no response
Note over C: RPC deadline / app closes
C-xB: Cancel waiting call
Note over C: CPAwaitingPayment + CBRetryableFailure
C->>B: Repeat same IssueBadge on foreground
Note over C: CPAwaitingPayment + CBRequesting
Note over B: Respond immediately if webhook already completed<br/>otherwise wait again
```
#### Checkout expired
```mermaid
sequenceDiagram
participant C as Client
participant B as Bot
participant S as Stripe
C->>B: Status request
B->>S: Retrieve Checkout
S-->>B: Pending
Note over B: Pending, no grant
B-->>C: Pending + retry time
Note over C: Poll later, badge unchanged
C->>B: RPC IssueBadge
Note over B: Hold call
S-->>B: Signed Checkout expired event
Note over B: BPExpired, no grant
B-->>C: RPC checkout expired
Note over C: CPExpired, new Checkout requires user action
```
#### Expired
```mermaid
sequenceDiagram
participant C as Client
participant B as Bot
participant S as Stripe
S-->>B: Checkout expired
Note over B: Expired, no grant
C->>B: Status request
B-->>C: Checkout expired
Note over C: New checkout requires user action
```
Start polling when Checkout opens and on return/foreground: 5, 15, 30, 60, 120 seconds, then normal refresh. The return link is optional routing, never proof.
There is no payment polling. The pending `IssueBadge` call is the completion signal. A deep link may return the user to the app but is not required and is never payment proof.
### Cancel subscription
@@ -312,7 +320,7 @@ If `GrantReady` exists without the current badge, request issuance. Cache the re
| Condition | Client action |
|---|---|
| Store canceled | restore previous screen |
| Payment pending | show pending; poll/schedule |
| Payment pending | keep `IssueBadge` waiting; retry the same call after deadline/restart |
| Network/provider failure | keep cached state and active badge; retry |
| Paid, issuance failed | show “Payment confirmed. Badge is being prepared”; retry |
| Cancel failed | keep **Renews on**; retry |