Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9373ac2
test(billing): specify provider transport boundary
seonghobae Aug 15, 2026
4bdfd1b
fix(billing): enforce Checkout provider boundary
seonghobae Aug 15, 2026
2abef79
test(billing): preserve Stripe hosted URL fragments
seonghobae Aug 15, 2026
2d18157
fix(billing): preserve documented Stripe Checkout fragments
seonghobae Aug 15, 2026
357ad04
test(billing): require direct bounded Checkout transport
seonghobae Aug 15, 2026
9f137ef
fix(billing): reconcile provider trust with direct transport
seonghobae Aug 15, 2026
bfd8718
test(billing): preserve parent direct-transport regression
seonghobae Aug 15, 2026
e71d9f4
merge: reconcile Stripe provider boundary with parent head
seonghobae Aug 15, 2026
ecf0cd2
docs(billing): align provider evidence with direct Stripe API
seonghobae Aug 15, 2026
4eb153f
docs(billing): operationalize direct provider boundary
seonghobae Aug 15, 2026
ca050f7
refactor(billing): keep provider failure path coverage-exact
seonghobae Aug 15, 2026
e6ebaa7
test(billing): bound Stripe provider response bytes
seonghobae Aug 15, 2026
3e65a73
fix(billing): cap Stripe provider response bytes
seonghobae Aug 15, 2026
f1b403c
test(billing): cover bounded provider-body failures
seonghobae Aug 15, 2026
a060354
docs(billing): record bounded provider response contract
seonghobae Aug 15, 2026
e30be48
docs(changelog): note bounded Stripe response bytes
seonghobae Aug 15, 2026
c3d6a65
docs(billing): align operator response-size boundary
seonghobae Aug 15, 2026
770e69f
test(billing): make stream-cancel assertion deterministic
seonghobae Aug 15, 2026
342c2fd
merge(billing): reconcile provider boundary with trusted checkout
seonghobae Aug 16, 2026
c156e5d
fix(billing): reconcile provider test manifest with parent
seonghobae Aug 16, 2026
f990f65
merge(billing): reconcile provider boundary with current parent
seonghobae Aug 16, 2026
bd384a6
merge(billing): reconcile provider boundary with current trusted-orig…
seonghobae Aug 16, 2026
2c0c4b4
merge(billing): reconcile provider boundary with current trusted-conf…
seonghobae Aug 17, 2026
299b029
merge(billing): reconcile provider boundary with current trusted-conf…
seonghobae Aug 17, 2026
22bdccd
merge(billing): reconcile provider boundary with current trusted-conf…
seonghobae Aug 17, 2026
f1ca84b
fix(stack): reconcile provider boundary with current billing parent
seonghobae Aug 19, 2026
3ab1a93
fix(stack): inherit current billing root in provider boundary
seonghobae Aug 20, 2026
54c3dfd
fix(stack): inherit Playwright-updated billing root in provider boundary
seonghobae Aug 20, 2026
6c39fb1
test(billing): reproduce unread Stripe response leak
seonghobae Aug 20, 2026
54bbacf
test(billing): preserve provider error across cleanup failure
seonghobae Aug 20, 2026
5d1bf1b
fix(billing): cancel unread Stripe failure bodies
seonghobae Aug 20, 2026
12fd922
fix: tolerate empty Stripe error bodies
seonghobae Aug 28, 2026
6df68ad
merge: reconcile Stripe checkout provider boundary
seonghobae Aug 28, 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Security

- Made contextual-orchestrator briefing requests fail closed unless an authenticated endpoint is configured. Deterministic generated text is restricted to explicit `SCOPEWEAVE_DEV=1`, message/provider responses are bounded and validated, and non-loopback HTTP transport is rejected.
- Bounded hosted Stripe Checkout provider calls to one 15-second, no-retry
attempt with a 1 MiB response ceiling before JSON parsing until durable
idempotency exists; validated returned destinations as exact HTTPS
`checkout.stripe.com` URLs without credentials or non-standard ports while
preserving Stripe-issued client fragments; and mapped provider failures to
sanitized no-store 502 responses.
- Bound Stripe Checkout success/cancel redirects to an operator-configured
canonical public origin instead of request authority, rejected partial or
ambiguous billing configuration at startup, and confined successful mock
Expand Down
101 changes: 86 additions & 15 deletions docs/billing-production.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,72 @@ For local integration tests, `SCOPEWEAVE_DEV=1` plus a valid loopback
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.
## Provider trust boundary

> Active PR state: this section describes the stacked provider-boundary work in
> PR #507. It is not protected-`develop` shipped truth until its parent PR #505
> and this PR are independently approved and integrated.

The live hosted-Checkout adapter performs one direct server-side HTTPS request to
Stripe until a later lifecycle slice introduces durable checkout-attempt and
idempotency state:

- endpoint: exact constant `https://api.stripe.com/v1/checkout/sessions`;
- method/body: one POST with `application/x-www-form-urlencoded` fields;
- authentication: `Authorization: Bearer <STRIPE_SECRET_KEY>` sent only to the
constant Stripe API authority;
- total request budget: 15,000 ms using an abort signal;
- redirect policy: provider HTTP redirects are rejected;
- automatic application retries: none;
- successful response budget: at most 1 MiB before UTF-8 decoding and JSON
parsing; invalid, negative, or oversized `Content-Length` declarations are
rejected, and a streamed body that crosses the ceiling is cancelled;
- network, abort, or non-2xx provider failures: stable HTTP 502
`billing_provider_unavailable` with `Cache-Control: no-store`;
- successful non-JSON, bodyless, unreadable, malformed JSON, or oversized
responses: stable HTTP 502 `billing_provider_invalid_response` with
`Cache-Control: no-store`;
- malformed or untrusted Checkout destinations: stable HTTP 502
`billing_provider_invalid_response` with `Cache-Control: no-store`;
- browser destination: parsed with `URL` and accepted only for HTTPS, exact
hostname `checkout.stripe.com`, default HTTPS port, and no URL credentials.

This exact-host check deliberately rejects suffix-confusion names such as
`checkout.stripe.com.evil.example`. Stripe's current Checkout Session API
reference shows a standard hosted `checkout.stripe.com` URL containing an opaque
`#fidk...` fragment. ScopeWeave therefore preserves provider-issued fragments
verbatim after the authority checks instead of treating a fragment as an origin
or hostname decision.

Stripe Checkout custom domains are not silently trusted. Supporting one requires
a separate operator-owned allowlist or canonical-domain configuration contract
and its own regression evidence.

Provider exception text, network addresses, non-2xx response bodies, stream-read
errors, and credentials are never copied into the browser error payload. The
customer receives a retry/diagnostic next action rather than downstream internals.

The provider boundary intentionally uses the documented Stripe HTTPS API instead
of dynamically importing an undeclared runtime SDK. A clean deployment therefore
does not depend on a hidden `stripe` package merely to create the hosted Session.
Package provenance remains part of the normal application supply-chain gate, but
there is no Stripe SDK package gate for this direct adapter.

## Current lifecycle boundary

The trusted-configuration and provider-trust slices do **not** declare the Stripe
subscription lifecycle production complete. Before production billing can be
release-approved, ScopeWeave still needs the remaining #488 controls, including:

a durable checkout-attempt UUID and stable idempotency key; raw-body webhook
signature verification and streaming size limits; durable event deduplication;
out-of-order reconciliation; normalized customer/subscription/payment/entitlement
state; transactional reversible entitlement changes; migration and restore
evidence; privacy/incident runbooks; and provider smoke plus release acceptance.

No automatic provider retry should be enabled before durable idempotency exists.
No custom Checkout domain should be accepted before an operator-owned trust
configuration exists.

## Operator verification

Expand All @@ -73,10 +128,26 @@ Before a billing-enabled rollout:
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
4. Capture the canary's outbound request destination and verify exactly one POST
goes to `api.stripe.com/v1/checkout/sessions`, redirects are not followed, and
the request aborts within the configured 15-second total budget.
5. Exercise network failure, non-2xx response, non-JSON success, malformed JSON,
bodyless success, invalid/oversized declared response length, streamed
response overflow, stream-read failure, and provider timeout handling.
Confirm response bodies above 1 MiB are not buffered/parsed and callers
receive only the stable no-store 502 contract without provider body, network,
stream, or credential detail.
6. Reject null, malformed, plaintext, credential-bearing, non-standard-port,
and hostname-confusion Checkout destinations; accept and preserve the exact
standard `https://checkout.stripe.com/...#...` hosted destination, including
its provider-issued fragment.
7. 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.
Rollback for the trusted-configuration/provider-boundary stack is data-neutral:
revert the validation and provider-boundary source, tests, documentation, and
CHANGELOG entries together. No database migration or persisted billing state is
introduced by these slices. If billing must be disabled while investigating a
provider outage, remove the complete live provider tuple and restart; never
substitute a production mock.
187 changes: 187 additions & 0 deletions docs/doctoring/stripe-checkout-provider-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# Stripe Checkout provider trust boundary

Status: **active stacked PR design/implementation evidence; not protected-`develop`
shipped truth until its parent and this slice are independently approved and
integrated.**

## Buyer-visible failure being closed

The trusted-origin billing slice prevents request authority from controlling
success/cancel redirects, but a provider boundary also has to constrain how long
Checkout can block a request, how many provider bytes ScopeWeave will accept,
whether an unsafe automatic retry can duplicate a side effect, what
provider-returned URL the browser may follow, and what failure detail can leave
the server.

Without those controls a buyer cannot distinguish a controlled billing outage
from an indefinite provider wait, and a compromised or malformed provider can
turn a small Checkout call into unbounded process memory consumption or browser
redirect authority.

## Decision

ScopeWeave uses a small, explicit HTTPS adapter for this bounded Checkout call
instead of an undeclared runtime SDK. The adapter follows Stripe's published REST
contract and keeps the production dependency surface auditable.

For hosted Stripe Checkout, this slice applies these controls:

1. The server sends exactly one `POST https://api.stripe.com/v1/checkout/sessions`
attempt using `application/x-www-form-urlencoded` request data. The request has
a 15,000 ms total abort budget and redirects are rejected by the fetch layer.
2. ScopeWeave implements no provider retry loop until durable checkout-attempt
identifiers and idempotency state exist. A later lifecycle slice may introduce
retries only together with that durable reconciliation model.
3. A non-2xx provider response or network/abort failure becomes stable HTTP 502
`billing_provider_unavailable`. The provider body is not exposed to callers.
4. A successful response must declare JSON and is consumed as a byte stream with
a hard 1 MiB ceiling before UTF-8 decoding and JSON parsing. A present
`Content-Length` must be a non-negative safe integer no greater than that
ceiling; an invalid or oversized declaration fails before parsing. When an
actual streamed body crosses the ceiling, its reader is cancelled and the
response fails as `billing_provider_invalid_response` rather than buffering
the remaining bytes.
5. Empty, unreadable, malformed-JSON, unsupported-media, invalid-size, and
oversized successful responses become stable HTTP 502
`billing_provider_invalid_response`. Downstream stream/read errors remain
sanitized and do not expose provider details.
6. The returned Checkout Session `url` is parsed with the platform `URL` parser
and accepted only when it uses HTTPS, has exact hostname
`checkout.stripe.com`, uses the default HTTPS port, and contains no URL
credentials. Provider-issued fragments are preserved verbatim because Stripe's
Checkout Session examples include opaque client fragments; fragments do not
participate in HTTPS authority selection.
7. All provider-facing buyer errors use `Cache-Control: no-store` and omit
downstream exception text, network addresses, response bodies, and secrets.

The validation uses exact parsed authority fields rather than string-prefix or
suffix matching, so `checkout.stripe.com.evil.example` is not trusted.

## Authentication, response consumption, and wire contract

Stripe's API requires HTTPS and authenticates secret API calls with an API key.
Its API reference permits bearer authorization for HTTP clients. ScopeWeave sends
`Authorization: Bearer <STRIPE_SECRET_KEY>` only to the constant Stripe API
origin and never derives that origin from request input. Checkout parameters are
form encoded using the same field names documented by Stripe, including
`line_items[0][price]`, `line_items[0][quantity]`, `success_url`, `cancel_url`,
`client_reference_id`, and organization metadata.

Stripe documents conventional HTTP status semantics: 2xx is successful while
4xx/5xx responses represent provider/request failures. ScopeWeave therefore does
not parse a non-2xx response as a successful Checkout Session and does not reflect
provider error text to the browser.

The WHATWG Fetch Standard models a response body as a readable stream and its
body-consuming algorithms operate on the received byte sequence. ScopeWeave uses
that streaming boundary directly instead of calling `Response.json()`, because
`Response.json()` would consume the complete body before the application can
enforce its own byte ceiling. The 1 MiB application limit is deliberately far
above the fields ScopeWeave needs from a Checkout Session while still bounding a
malicious or malfunctioning provider response.

## Compatibility boundary

Stripe documents hosted Checkout Session URLs as nullable and present only while
the Session is active. Without a configured Checkout custom domain, Stripe uses
`checkout.stripe.com`; configured custom domains use the merchant's subdomain.
The current Stripe API reference returns an example hosted Checkout URL with an
opaque fragment after the session path, so rejecting all fragments would reject
a documented provider response. ScopeWeave therefore validates the URL authority
and preserves the provider-issued URL, including its fragment, without parsing
or rewriting that fragment.

ScopeWeave does **not** silently trust arbitrary custom domains in this slice.
Supporting one requires a future operator-owned allowlist/configuration contract
and tests proving the configured authority cannot be replaced by provider or
request input.

This slice does not add durable checkout attempts, webhook verification,
subscription/payment/entitlement persistence, or reconciliation. Those remain
explicit lifecycle work rather than being silently implied by this
provider-boundary slice.

## TDD and branch-reconciliation evidence

The original test-only commit `9373ac2719600d0e159b22557733a4c75def8744`
specified provider timing/authority controls before production changes. A later
parent-head review found that the parent branch dynamically imported an
undeclared `stripe` package, so a real default live request could fail with
`ERR_MODULE_NOT_FOUND` despite injected-factory tests passing.

Parent commit `ad81eb52a6f0e3448e6a17f0e500e1f140993c92` added a realistic
regression that invokes the default live path and requires a direct HTTPS Stripe
request. Parent commit `0b5e1d9a25a986546efa79d6b0a62d7b1e8395fe` implemented that
transport without adding an undeclared runtime dependency.

This stacked branch then received regression-only commit
`357ad04523f20415ca996b965943000c414c8809`, which changed the provider tests to
exercise the real default transport, non-2xx handling, invalid successful media,
malformed JSON, exact hosted-URL authority, and sanitized network failure. The
pre-repair stacked source still depended on the absent SDK and therefore could
not satisfy the default-transport contract. Commit
`9f137ef4aa5486b9b3cafe6ec1f986191bc560d0` reconciled the production provider
boundary with the parent's direct transport while preserving the stricter URL
and failure validation. Commit `bfd8718452946c514ca17009894097ce5dcf937e`
preserved the parent's default-transport regression on the child branch.

Merge commit `e71d9f41893d57b69297d70c7d6a42af1763b297` records the exact
parent `0b5e1d9...` as ancestry without force-pushing or rebasing. The stacked
comparison is therefore ahead-only from the current parent and cannot silently
discard the parent's transport repair.

Response-size regression commit `e6ebaa70b40046fe3b80d244603f66aac8850d86`
was written before the byte-bound implementation. Its declared-oversize case is
deterministically RED against that predecessor source because the predecessor
called `Response.json()` directly and would accept the small valid JSON body
regardless of the oversized declaration; the streamed-overflow case also
requires cancellation that the predecessor never performed. That queued hosted
run was superseded by the work-conserving production push rather than being
promoted as passing evidence. Production commit
`3e65a73a5b3c82b57eb9cee6fa1e7b1aad179d59` replaced unbounded JSON consumption
with the 1 MiB streamed boundary. Commit
`f1b403c9faa1ae4c8d603a3500113017ebca3732` expanded realistic regressions across
valid, malformed, negative, and oversized size declarations, bodyless success,
stream overflow/cancellation, and downstream stream failure so statement/branch
coverage cannot hide an untested failure path.

The focused provider regression remains registered under the canonical c8
coverage producer; no test or deterministic gate is removed to obtain GREEN.
Hosted exact-head CI remains authoritative after every source or documentation
movement.

## Rollback

Revert the provider-boundary source change, focused tests, documentation, and
CHANGELOG entry together. No database schema or persisted billing state is
introduced, so rollback has no data migration. If billing must be disabled while
investigating a provider outage, remove the complete live provider tuple and
restart so the existing fail-closed 503 configuration path applies; do not
replace the provider error with a production mock.

## Traceability

- Issue: #488
- Parent trusted-configuration slice: PR #505
- Provider-boundary slice: PR #507
- Owned production: `server/billing.mjs`
- Parent regression: `tests/unit/billing-checkout.test.mjs`
- Provider regression: `tests/unit/billing-provider-boundary.test.mjs`
- Operator contract: `docs/billing-production.md`

## References

Stripe, Inc. (n.d.). *Authentication*. Stripe API Reference. Retrieved August 15,
2026, from https://docs.stripe.com/api/authentication

Stripe, Inc. (n.d.). *Create a Checkout Session*. Stripe API Reference. Retrieved
August 15, 2026, from https://docs.stripe.com/api/checkout/sessions/create

Stripe, Inc. (n.d.). *Errors*. Stripe API Reference. Retrieved August 15, 2026,
from https://docs.stripe.com/api/errors

Stripe, Inc. (n.d.). *The Checkout Session object*. Stripe API Reference.
Retrieved August 15, 2026, from https://docs.stripe.com/api/checkout/sessions/object

WHATWG. (n.d.). *Fetch standard*. Retrieved August 16, 2026, from
https://fetch.spec.whatwg.org/
Loading