Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c5ee177
test(billing): define Stripe webhook trust boundary
seonghobae Aug 16, 2026
4317f3f
test(billing): run Stripe webhook boundary regression
seonghobae Aug 16, 2026
26afb04
test(billing): prove webhook replay window independently
seonghobae Aug 16, 2026
493ae4f
fix(billing): verify raw Stripe webhook signatures
seonghobae Aug 16, 2026
62b4900
test(api): require authenticated Stripe webhook delivery
seonghobae Aug 16, 2026
d081175
test(api): run Stripe webhook trust regression
seonghobae Aug 16, 2026
20cd2eb
fix(billing): enforce verified Stripe webhook boundary
seonghobae Aug 16, 2026
54e96c0
test(coverage): require Stripe webhook instrumentation
seonghobae Aug 16, 2026
5845898
test(coverage): instrument Stripe webhook boundary
seonghobae Aug 16, 2026
2e5dbcc
docs(billing): trace Stripe webhook trust boundary
seonghobae Aug 16, 2026
f627bc8
merge(billing): reconcile webhook trust with current operations parent
seonghobae Aug 16, 2026
b952c32
merge(billing): inherit Checkout rollback regression
seonghobae Aug 16, 2026
1dd032d
Merge current reconciliation parent into Stripe webhook trust boundary
seonghobae Aug 16, 2026
0a503e2
merge(billing): reconcile Stripe webhook trust with current reconcili…
seonghobae Aug 16, 2026
a1a5176
merge(billing): reconcile webhook trust with current reconciliation p…
seonghobae Aug 17, 2026
a0c0933
fix(billing): restore webhook child to exact parent tree
seonghobae Aug 18, 2026
9c72a3a
fix(stack): preserve toast delivery in webhook trust reconciliation
seonghobae Aug 19, 2026
6ad097b
fix(stack): preserve tenant cost attribution in webhook route graph
seonghobae Aug 19, 2026
4b8b87a
fix(stack): inherit current reconciliation ops in webhook boundary
seonghobae Aug 20, 2026
0178984
fix(stack): inherit Playwright-updated reconciliation ops in webhook …
seonghobae Aug 20, 2026
ea6c2c4
chore(stack): sync current reconciliation parent
seonghobae Aug 20, 2026
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
51 changes: 51 additions & 0 deletions docs/doctoring/stripe-webhook-trust-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Stripe webhook trust boundary

## Status

**Active PR only — not protected-`develop` shipped truth.** This note describes the bounded implementation on PR #520, stacked on PR #516 for issue #488. The protected `develop` branch does not contain this behavior until the stack is independently reviewed and merged through live repository protection.

## Buyer-visible risk closed by this slice

A billing webhook is an unauthenticated Internet ingress unless the application proves that the exact bytes were signed by the payment provider. Before this slice, `/api/stripe/webhook` parsed arbitrary JSON and could promote an organization to Pro from caller-controlled fields. That made entitlement state writable by an untrusted request.

PR #520 changes the boundary so a webhook is acknowledged only after verification of the exact raw request bytes. It also removes direct entitlement mutation from the webhook handler. A cryptographically valid delivery therefore proves delivery authenticity, but it does **not** by itself grant billing authority. Durable event deduplication, authoritative provider-state reconciliation, and out-of-order lifecycle handling remain separate follow-on work under #488.

## Normative evidence and design decisions

Stripe's webhook documentation requires signature verification against the raw, unmodified request body and warns that JSON parsing, whitespace changes, key reordering, or encoding changes invalidate verification. Stripe also documents a five-minute default timestamp tolerance to mitigate replay and recommends quickly returning a `2xx` response before complex processing. These requirements drive the implementation rather than model judgment.

The ScopeWeave verifier therefore:

- reads and signs the exact raw request bytes before JSON parsing;
- bounds both declared and streamed body size at 256 KiB and bounds the signature header at 4 KiB;
- requires one valid signed timestamp plus at least one SHA-256 `v1` signature;
- computes HMAC-SHA-256 over `timestamp + "." + raw_body` and compares candidate digests in constant time;
- applies a symmetric 300-second recency window using an injected clock in tests;
- rejects invalid UTF-8, malformed JSON, and missing or oversized event identity before acknowledging the delivery;
- maps expected verifier failures to sanitized stable error codes and returns `Cache-Control: no-store`;
- acknowledges a valid event without directly changing plan entitlements.

The body/header limits are ScopeWeave defense-in-depth limits, not claims about Stripe protocol maxima. They bound memory and parser work at an Internet-facing trust boundary.

## TDD and acceptance trace

The implementation is covered by two realistic regression layers:

1. `tests/unit/stripe-webhook-boundary.test.mjs` exercises exact-byte signatures, payload mutation, multiple `v1` signatures, stale/future timestamps, malformed headers, oversized declared and streamed bodies, invalid UTF-8/JSON, configuration errors, and event-identity bounds.
2. `tests/api/stripe-webhook.test.mjs` exercises the real Hono route and database state. It proves unsigned, stale, and mutated deliveries cannot upgrade an organization; a correctly signed delivery is acknowledged; and even a valid `checkout.session.completed` delivery cannot directly mutate the entitlement.

`package.json` registers both the API regression and the webhook module/unit suite in the canonical c8 coverage path. `tests/unit/coverage-script-contract.test.mjs` fails if that instrumentation or regression registration is later removed.

Hosted CI on the implementation head must be treated as head-specific evidence. A predecessor run is never sufficient after any subsequent source, test, or documentation commit.

## Authority boundary still open

This slice intentionally stops before durable billing lifecycle processing. The next billing authority layer must, at minimum, persist provider event identity for idempotent processing, tolerate duplicate and out-of-order delivery, reconcile against authoritative Stripe state before releasing held Checkout attempts, and produce auditable tenant-scoped evidence. PR #516 provides the adjacent reconciliation persistence boundary but remains a separate stacked dependency.

No ScopeWeave document should describe webhook-driven Pro activation as shipped until those authority layers and the protected-stack merge are complete.

## References (APA 7th)

Stripe. (n.d.). *Receive Stripe events in your webhook endpoint*. Stripe Documentation. Retrieved August 16, 2026, from https://docs.stripe.com/webhooks

Stripe. (n.d.). *Resolve webhook signature verification errors*. Stripe Documentation. Retrieved August 16, 2026, from https://docs.stripe.com/webhooks/signature
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
"check:python-docstrings": "node scripts/ci/static_coverage_evidence.mjs docstrings",
"coverage": "npm run test:coverage",
"server": "node server/server.mjs",
"test:api": "node tests/api/auth-secret.test.mjs && node --env-file=tests/api/smoke.env tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs && node tests/api/session-revocation.test.mjs && node tests/api/orchestrator-attribution.test.mjs && node tests/api/billing-checkout.test.mjs && node tests/api/billing-live-checkout.test.mjs",
"test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/billing-configuration.test.mjs && node tests/unit/billing-checkout-attempt.test.mjs && node tests/unit/billing-checkout-attempt-authority.test.mjs && node tests/unit/billing-checkout-reconciliation.test.mjs && node tests/unit/billing-checkout-reconciliation-authority.test.mjs && node tests/unit/billing-checkout.test.mjs && node tests/unit/billing-provider-boundary.test.mjs && node tests/unit/billing-checkout-review-regressions.test.mjs && node tests/unit/toast-accessibility.test.mjs",
"test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/auth.mjs --include=server/billing.mjs --include=server/billing_checkout_attempt.mjs --include=server/billing_configuration.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases",
"test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/billing-configuration.test.mjs && node tests/unit/billing-checkout-attempt.test.mjs && node tests/unit/billing-checkout-attempt-authority.test.mjs && node tests/unit/billing-checkout-reconciliation.test.mjs && node tests/unit/billing-checkout-reconciliation-authority.test.mjs && node tests/unit/billing-checkout.test.mjs && node tests/unit/billing-provider-boundary.test.mjs && node tests/unit/billing-checkout-review-regressions.test.mjs && npm run test:api",
"test:api": "node tests/api/auth-secret.test.mjs && node --env-file=tests/api/smoke.env tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs && node tests/api/session-revocation.test.mjs && node tests/api/orchestrator-attribution.test.mjs && node tests/api/billing-checkout.test.mjs && node tests/api/billing-live-checkout.test.mjs && node tests/api/stripe-webhook.test.mjs",
"test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/billing-configuration.test.mjs && node tests/unit/billing-checkout-attempt.test.mjs && node tests/unit/billing-checkout-attempt-authority.test.mjs && node tests/unit/billing-checkout-reconciliation.test.mjs && node tests/unit/billing-checkout-reconciliation-authority.test.mjs && node tests/unit/billing-checkout.test.mjs && node tests/unit/billing-provider-boundary.test.mjs && node tests/unit/billing-checkout-review-regressions.test.mjs && node tests/unit/stripe-webhook-boundary.test.mjs && node tests/unit/toast-accessibility.test.mjs",
"test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/application_routes.mjs --include=server/auth.mjs --include=server/billing.mjs --include=server/billing_checkout_attempt.mjs --include=server/billing_configuration.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --include=server/stripe_webhook.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases",
"test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/billing-configuration.test.mjs && node tests/unit/billing-checkout-attempt.test.mjs && node tests/unit/billing-checkout-attempt-authority.test.mjs && node tests/unit/billing-checkout-reconciliation.test.mjs && node tests/unit/billing-checkout-reconciliation-authority.test.mjs && node tests/unit/billing-checkout.test.mjs && node tests/unit/billing-provider-boundary.test.mjs && node tests/unit/billing-checkout-review-regressions.test.mjs && node tests/unit/stripe-webhook-boundary.test.mjs && npm run test:api",
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed",
"test:e2e:cloud": "playwright install chromium && playwright test tests/e2e/cloud.spec.js tests/e2e/toast-accessibility.spec.js",
Expand Down
Loading
Loading