-
Notifications
You must be signed in to change notification settings - Fork 0
fix(billing): bind Checkout redirects to trusted configuration #505
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
seonghobae
wants to merge
34
commits into
develop
Choose a base branch
from
feat/stripe-trusted-checkout-config-488
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
fb1a7c0
test(billing): specify trusted checkout configuration
seonghobae f2ac4fa
feat(billing): validate trusted checkout configuration
seonghobae fb40d2a
feat(billing): fail closed and use operator redirect origin
seonghobae 520f1b7
test(billing): cover trusted checkout authority
seonghobae 1b3594c
test(api): prove checkout ignores request host authority
seonghobae 311f084
test(billing): register checkout coverage evidence
seonghobae 303e4c6
docs(billing): define trusted checkout configuration
seonghobae be5dbd7
docs(doctoring): trace trusted checkout origin evidence
seonghobae 931d828
docs(changelog): record trusted billing origin
seonghobae 956742d
test(billing): inspect rejected response after sync validation
seonghobae 47049dd
test(api): configure trusted billing origin for smoke
seonghobae 3ae911e
test(api): load billing origin in smoke harness
seonghobae ad81eb5
test(billing): prove default live transport needs no undeclared SDK
seonghobae 0b5e1d9
fix(billing): use declared provider transport for checkout
seonghobae 53b040a
merge(billing): reconcile trusted checkout with protected develop
seonghobae 6cb4385
fix(test): remove unrelated attribution registrations
seonghobae b55347b
merge(billing): reconcile trusted checkout with current develop
seonghobae f9ba6b6
merge(billing): reconcile trusted checkout with current develop
seonghobae 0b46b90
test(billing): fail closed on Stripe provider errors
seonghobae 1ff56bc
fix(billing): fail closed on provider checkout errors
seonghobae 8363ad8
test(billing): cover provider transport failure envelope
seonghobae 35264a8
docs(billing): record fail-closed Stripe response boundary
seonghobae d848bbb
docs(billing): record provider failure handling
seonghobae 58b3480
fix(billing): normalize all provider exceptions
seonghobae 3be747d
test(billing): cover absent provider redirect shapes
seonghobae 5477a34
merge(billing): reconcile trusted checkout root with current develop
seonghobae 000731d
merge(billing): reconcile trusted checkout root with protected develop
seonghobae 4c0592d
fix(stack): preserve protected develop in billing reconciliation
seonghobae da14bb4
fix(stack): reconcile billing root with current develop
seonghobae b80d502
fix(stack): reconcile billing root with Playwright develop update
seonghobae 3329a17
docs: align billing deployment guidance
seonghobae 23a6998
fix: normalize Stripe configuration values
seonghobae 51418c0
fix: make billing smoke boundary explicit
seonghobae b8435c7
fix(billing): restrict checkout redirect host
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| # Billing production configuration | ||
|
|
||
| ScopeWeave treats billing as a separately deployable capability. An absent Stripe | ||
| configuration does **not** imply a successful production checkout. The only | ||
| successful mock path is explicit development mode. | ||
|
|
||
| ## Configuration contract | ||
|
|
||
| A live checkout process requires all of the following values together: | ||
|
|
||
| - `STRIPE_SECRET_KEY` | ||
| - `STRIPE_PRICE_ID` | ||
| - `STRIPE_WEBHOOK_SECRET` | ||
| - `SCOPEWEAVE_PUBLIC_ORIGIN` | ||
|
|
||
| The three Stripe values are an all-or-none startup tuple. A partial tuple stops | ||
| application startup with `billing_configuration_incomplete`. A complete Stripe | ||
| tuple without `SCOPEWEAVE_PUBLIC_ORIGIN` stops startup with | ||
| `billing_public_origin_required`. | ||
|
|
||
| `SCOPEWEAVE_PUBLIC_ORIGIN` is the operator-owned browser origin used to construct | ||
| Checkout success and cancellation URLs. ScopeWeave parses it with the platform | ||
| `URL` implementation and accepts a root HTTPS origin only. URL credentials, | ||
| paths, query strings, fragments, unsupported schemes, and remote plaintext HTTP | ||
| are rejected. Explicit `SCOPEWEAVE_DEV=1` may use HTTP only on `localhost`, | ||
| `127.0.0.1`, or `::1`. | ||
|
|
||
| Example production shape: | ||
|
|
||
| ```text | ||
| SCOPEWEAVE_PUBLIC_ORIGIN=https://planner.example.com | ||
| STRIPE_SECRET_KEY=<secret-manager reference> | ||
| STRIPE_PRICE_ID=price_... | ||
| STRIPE_WEBHOOK_SECRET=<secret-manager reference> | ||
| ``` | ||
|
|
||
| Do not derive `SCOPEWEAVE_PUBLIC_ORIGIN` from `Host`, `Forwarded`, | ||
| `X-Forwarded-Host`, or the incoming request URL. Proxy headers describe a request | ||
| path through infrastructure; they are not billing redirect authority. | ||
|
|
||
| ## Disabled and development behavior | ||
|
|
||
| With no Stripe tuple, production billing is disabled. A checkout attempt fails | ||
| closed with HTTP 503 and `billing_not_configured` rather than generating a fake | ||
| success URL. The response tells the operator to configure the complete Stripe | ||
| settings and public origin, then restart ScopeWeave. | ||
|
|
||
| For local integration tests, `SCOPEWEAVE_DEV=1` plus a valid loopback | ||
| `SCOPEWEAVE_PUBLIC_ORIGIN` enables the mock checkout. The mock URL is built from | ||
| the configured origin and a percent-encoded organization identifier; a different | ||
| request host cannot replace that origin. | ||
|
|
||
| ## Current slice boundary | ||
|
|
||
| This document describes only the trusted-configuration and redirect-authority | ||
| slice of issue #488. It does **not** declare the Stripe lifecycle production | ||
| complete. Before production billing can be release-approved, ScopeWeave still | ||
| needs the remaining #488 controls, including durable checkout attempts and stable | ||
| idempotency keys, a packaged/pinned provider SDK and bounded provider transport, | ||
| validated returned Checkout destinations, raw-body webhook verification and | ||
| size limits, durable event deduplication, out-of-order reconciliation, normalized | ||
| subscription/payment/entitlement state, rollback/recovery procedures, and | ||
| end-to-end operational acceptance evidence. | ||
|
|
||
| ## Operator verification | ||
|
|
||
| Before a billing-enabled rollout: | ||
|
|
||
| 1. Start a canary with the complete Stripe tuple and the exact public browser | ||
| origin intended for customer redirects. | ||
| 2. Confirm malformed, partial, path-bearing, query-bearing, credential-bearing, | ||
| and plaintext remote origins stop startup. | ||
| 3. Send a checkout request through the same reverse proxy used in production | ||
| while varying the request authority; success/cancel URLs must still use only | ||
| `SCOPEWEAVE_PUBLIC_ORIGIN`. | ||
| 4. Keep the rollout blocked until the remaining #488 lifecycle controls are | ||
| implemented and their exact-head security, coverage, review, rollback, and | ||
| recovery gates pass together. | ||
|
|
||
| Rollback for this slice is configuration-neutral: revert the validation module, | ||
| checkout authority change, and tests together. No database migration or | ||
| persisted billing state is introduced here. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| # Stripe checkout trusted-origin evidence | ||
|
|
||
| ## Decision | ||
|
|
||
| ScopeWeave separates request authority from billing redirect authority. Checkout | ||
| success/cancel URLs derive only from the operator-owned | ||
| `SCOPEWEAVE_PUBLIC_ORIGIN`; an inbound request URL, `Host`, or forwarded host is | ||
| not a trusted redirect source. | ||
|
|
||
| A Stripe-enabled process must also receive `STRIPE_SECRET_KEY`, | ||
| `STRIPE_PRICE_ID`, and `STRIPE_WEBHOOK_SECRET` as one complete startup tuple. | ||
| Partial provider configuration fails startup. A complete tuple without the | ||
| public origin fails startup. Without the tuple, production billing remains | ||
| disabled; only explicit `SCOPEWEAVE_DEV=1` plus a valid public loopback origin | ||
| may select the mock checkout path. | ||
|
|
||
| The configured public origin is parsed with the WHATWG `URL` API and is accepted | ||
| only as a root HTTPS origin. Credentials, a configured path, query, fragment, | ||
| unsupported scheme, and remote plaintext HTTP are rejected. Development HTTP is | ||
| limited to `localhost`, `127.0.0.1`, and WHATWG-serialized IPv6 loopback `[::1]`. | ||
|
|
||
| The default live Checkout transport uses the platform HTTPS `fetch` boundary, | ||
| not an undeclared Stripe runtime SDK. A provider response is accepted only when | ||
| HTTP reports success, JSON parsing succeeds, and the resulting hosted Checkout | ||
| Session contains a non-empty HTTPS URL without URL credentials. Network errors, | ||
| timeouts, non-2xx provider responses, malformed JSON, missing URLs, plaintext | ||
| URLs, and credential-bearing URLs fail closed as a stable HTTP 502 response. | ||
| Provider response bodies and transport details are never copied into that | ||
| customer-facing failure payload. | ||
|
|
||
| ## Threat and standards rationale | ||
|
|
||
| Stripe Checkout sessions are created server-side and carry success/cancel URLs. | ||
| Using request authority to populate those URLs would let reverse-proxy or | ||
| host-header misconfiguration influence a security-sensitive customer redirect. | ||
| The operator origin is therefore explicit configuration rather than request | ||
| derived data. | ||
|
|
||
| Stripe's API error contract uses conventional HTTP status classes: successful | ||
| requests are represented by 2xx responses, while 4xx and 5xx responses represent | ||
| request/provider failures. Treating an error document as a successful Checkout | ||
| Session can return an undefined or otherwise unusable redirect to the buyer, so | ||
| the direct transport validates HTTP success before parsing the session. Stripe's | ||
| Checkout Session API returns a Checkout Session object after successful | ||
| creation; ScopeWeave additionally validates the returned hosted URL before | ||
| exposing it to the caller. | ||
|
|
||
| The WHATWG URL Standard defines the parsed URL components and tuple origin used | ||
| by the JavaScript `URL` implementation. Parsing first and then applying | ||
| component-level policy avoids ambiguous prefix/string matching. | ||
|
|
||
| Stripe documents idempotency keys for safely retrying POST requests and webhook | ||
| handling requirements including raw-body signature verification, duplicate | ||
| events, and non-guaranteed event ordering. Those requirements are intentionally | ||
| recorded here as the next lifecycle boundary; this root slice does not claim to | ||
| have implemented them. | ||
|
|
||
| ## Executable evidence | ||
|
|
||
| `tests/unit/billing-configuration.test.mjs` proves: | ||
|
|
||
| - no provider tuple in production resolves to a disabled capability, not a mock; | ||
| - explicit development mode plus loopback origin enables only the mock; | ||
| - partial Stripe tuples fail closed; | ||
| - a live tuple requires a canonical public origin; | ||
| - credentials, path, query, fragment, malformed URLs, unsupported schemes, and | ||
| remote HTTP are rejected; and | ||
| - development loopback HTTP and canonical HTTPS serialization behave exactly as | ||
| documented. | ||
|
|
||
| `tests/unit/billing-checkout.test.mjs` proves: | ||
|
|
||
| - disabled production checkout raises an actionable HTTP 503 response; | ||
| - a caller-supplied/request-derived `origin` property is ignored by the checkout | ||
| implementation; | ||
| - mock organization identifiers are percent encoded; | ||
| - an injected deterministic Stripe client receives success/cancel URLs built | ||
| from the configured public origin rather than a request host; | ||
| - the default provider path posts only to Stripe's HTTPS Checkout Sessions API; | ||
| - provider non-2xx responses and network failures collapse to a non-leaking HTTP | ||
| 502 failure envelope; | ||
| - malformed success JSON and missing hosted URLs are rejected; | ||
| - plaintext, malformed, or URL-credential-bearing provider redirects are | ||
| rejected; and | ||
| - unexpected injected-provider failures use the same safe failure envelope. | ||
|
|
||
| `tests/api/billing-checkout.test.mjs` drives the real Hono route with requests | ||
| addressed to `https://attacker.example` while the operator origin is | ||
| `http://127.0.0.1:8787`; the returned mock Checkout URL remains bound to the | ||
| operator origin. The package coverage producer includes both billing production | ||
| modules and these regressions. | ||
|
|
||
| ## Scope limit and remaining acquisition gap | ||
|
|
||
| This is the first bounded vertical slice of issue #488 and **does not close it**. | ||
| It introduces no billing database schema and makes no claim that subscription | ||
| entitlements are production complete. The following remain blocking work: | ||
|
|
||
| - durable checkout-attempt UUIDs and stable Stripe idempotency keys; | ||
| - bounded provider response-size enforcement and retry policy that distinguishes | ||
| safe transient failure from permanent configuration/request failure; | ||
| - exact raw-body webhook signature verification with bounded timestamp | ||
| tolerance and body size; | ||
| - durable event-ID deduplication and non-sensitive audit metadata; | ||
| - out-of-order event reconciliation against authoritative provider state or a | ||
| monotonic per-object cursor; | ||
| - 3NF customer/subscription/payment/organization-entitlement state machines; | ||
| - transactional, reversible entitlement transitions; and | ||
| - migration, incident, recovery, privacy, test-mode provider smoke, and release | ||
| acceptance evidence. | ||
|
|
||
| ## Rollback | ||
|
|
||
| Rollback reverts `server/billing_configuration.mjs`, the checkout authority and | ||
| provider-response validation in `server/billing.mjs`, the registered unit/API | ||
| coverage cases, billing operations documentation, and this evidence record | ||
| together. No database migration or persisted billing record is introduced by | ||
| this slice. | ||
|
|
||
| ## References | ||
|
|
||
| Stripe. (n.d.). *Create a Checkout Session*. Stripe API Reference. | ||
| https://docs.stripe.com/api/checkout/sessions/create | ||
|
|
||
| Stripe. (n.d.). *Errors*. Stripe API Reference. | ||
| https://docs.stripe.com/api/errors | ||
|
|
||
| Stripe. (n.d.). *Error handling*. Stripe Documentation. | ||
| https://docs.stripe.com/error-handling | ||
|
|
||
| Stripe. (n.d.). *Idempotent requests*. Stripe API Reference. | ||
| https://docs.stripe.com/api/idempotent_requests | ||
|
|
||
| Stripe. (n.d.). *Receive Stripe events in your webhook endpoint*. Stripe | ||
| Documentation. https://docs.stripe.com/webhooks | ||
|
|
||
| WHATWG. (2026). *URL Standard*. https://url.spec.whatwg.org/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.