A deterministic TypeScript reference for repairing webhook-driven subscription access when deliveries can be forged, duplicated, delayed, reordered, or fail mid-processing.
Important
Reference-work disclosure: this is synthetic portfolio/reference work. It is not client work, a production incident, an official Stripe sample, or a complete Stripe emulator. Every ID, event, amount, secret, and account in the tests is invented. It never contacts Stripe or processes a real payment.
| Failure mode | Explicit behavior | Executable evidence |
|---|---|---|
| Forged or stale signature | Reject before parsing or state mutation | mutations=0 |
| Duplicate event ID | Return the prior disposition without applying twice | deduplicated=true |
| Older lifecycle snapshot | Keep the higher authoritative version | stale_lifecycle_ignored=true |
| Older invoice outcome | Keep the newer state for that invoice | stale_invoice_ignored=true |
| Payment failure then recovery | Revoke, then re-grant from server-side rules | Tested transition |
| Refund | Record separately; never imitate cancellation | refund_separate=true |
| Subscription deletion | Reconcile authoritative deletion and revoke | deleted_revoked=true |
| Processing failure | Retain a visible pending dead letter for explicit replay | explicit_replay=true |
Run the evidence with Node.js 20 or newer:
npm ci
npm run check
npm testExpected evidence markers:
SIGNATURE GUARD VERIFIED forged_rejected=true stale_rejected=true mutations=0
ENTITLEMENT RESCUE VERIFIED deduplicated=true stale_lifecycle_ignored=true stale_invoice_ignored=true refund_separate=true deleted_revoked=true
DEAD LETTER VISIBILITY VERIFIED resolved=1 pending=2 explicit_replay=true
tests 3 | pass 3 | fail 0
No Docker, webhook tunnel, Stripe account, environment values, hosted database, or real credentials are required.
flowchart TD
A["Raw delivery"] --> B{"HMAC-style signature and timestamp valid?"}
B -->|No| C["Reject; record body digest only"]
B -->|Yes| D{"JSON event valid?"}
D -->|No| E["Pending dead letter"]
D -->|Yes| F{"Event ID seen?"}
F -->|Yes| G["Return prior disposition"]
F -->|No| H{"Event family"}
H -->|Subscription updated or deleted| I["Fetch authoritative subscription"]
I --> J{"Snapshot version newer?"}
J -->|No| K["Ignore stale lifecycle snapshot"]
J -->|Yes| L["Replace lifecycle state"]
H -->|Invoice paid or failed| M{"Newer outcome for invoice?"}
M -->|No| N["Ignore stale invoice outcome"]
M -->|Yes| O["Update invoice ledger"]
H -->|Charge refunded| P["Update separate refund ledger"]
I -->|Lookup or validation fails| E
H -->|Unsupported| E
E --> Q["Explicit operator replay"]
Q --> H
L --> R["Derive entitlement server-side"]
O --> R
P --> R
The webhook is a notification, not the lifecycle source of truth. Subscription
events trigger the injected AuthoritativeBillingReader, and a
monotonic snapshot version prevents a stale read from rolling state backward.
Invoice outcomes are ordered independently per invoice. Refund records are a
separate accounting concern and never mutate subscription lifecycle.
The entitlement decision in entitlement.ts is computed
from server-held state:
- an active trial grants access until its reconciled period end;
- an active subscription requires the latest applicable invoice to be paid;
- a later payment failure revokes access and a later paid event can restore it;
- past-due, canceled, deleted, or expired lifecycle state denies access;
- refund amount is deliberately absent from the access decision.
These rules are illustrative product policy, not universal Stripe guidance.
signature.ts: HMAC-SHA256 construction, constant-time comparison, and replay-window enforcement.processor.ts: event-ID deduplication, ordering, authoritative reconciliation, ledgers, and dead-letter replay.entitlement.ts: server-side access derivation.webhook-rescue.test.ts: three deterministic end-to-end scenarios using only synthetic fixtures.findings.md: failure-to-control traceability.rollback-runbook.md: fail-closed recovery sequence for adapting this pattern.residual-risks.md: limits that a real integration must test.manifest.json: local verification record.
This repository models one customer with one relevant subscription and an in-memory state store. It does not implement Stripe's API schema, SDK, webhook endpoint framework, persistence, queue, Connect account context, invoice line items, proration, disputes, tax, multi-currency accounting, authorization, or operational alerting. The signature format is Stripe-like for teaching purposes but is not offered as a drop-in replacement for Stripe's official library.
For production adaptation, use Stripe's maintained server SDK to verify the raw request body, a transactional database with a unique event-ID constraint, an authorized server-side retrieval path, encrypted secret storage, and monitored queue/dead-letter infrastructure. Read the residual risks before reusing any design.
This reference is closest to a subscription application whose access has drifted from billing state, whose webhooks apply twice or arrive out of order, or whose failed deliveries disappear without an operator recovery path. Open a public redacted fit check with synthetic facts only.
GitHub issues are public. Never include API keys, webhook secrets, customer or
payment data, private URLs, proprietary code, or production event payloads.
Security-sensitive reports belong in the private flow described in
SECURITY.md.
MIT
