From f316a1ec44b0d08cea6bb1f451901f9082d8ec0e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 09:48:51 -0700 Subject: [PATCH 01/11] test(billing): define Checkout identity bootstrap contract --- ...tripe-checkout-identity-bootstrap.test.mjs | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 tests/unit/stripe-checkout-identity-bootstrap.test.mjs diff --git a/tests/unit/stripe-checkout-identity-bootstrap.test.mjs b/tests/unit/stripe-checkout-identity-bootstrap.test.mjs new file mode 100644 index 00000000..ab5b547c --- /dev/null +++ b/tests/unit/stripe-checkout-identity-bootstrap.test.mjs @@ -0,0 +1,270 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { DatabaseSync } from 'node:sqlite'; + +import { installBillingCheckoutAttemptSchema } from '../../server/billing_checkout_attempt.mjs'; +import { installStripeWebhookEventSchema } from '../../server/stripe_webhook_event_ledger.mjs'; +import { installStripeSubscriptionObservationSchema } from '../../server/stripe_subscription_observation_ledger.mjs'; +import { + StripeCheckoutIdentityBootstrapError, + bindVerifiedStripeCheckoutSessionIdentity, +} from '../../server/stripe_checkout_identity_bootstrap.mjs'; + +const RECEIVED_AT_MS = 1_787_100_000_123; + +function bootstrapDatabase() { + const database = new DatabaseSync(':memory:'); + database.exec('PRAGMA foreign_keys = ON'); + database.exec(` + CREATE TABLE users ( + id INTEGER PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + name TEXT NOT NULL DEFAULT '', + token_version INTEGER NOT NULL DEFAULT 0 + ); + CREATE TABLE orgs ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + owner_id INTEGER NOT NULL REFERENCES users(id), + plan TEXT NOT NULL DEFAULT 'free' + ); + INSERT INTO users(id, email, password_hash, name) VALUES + (1, 'owner-one@example.invalid', 'hash', 'Owner One'), + (2, 'owner-two@example.invalid', 'hash', 'Owner Two'); + INSERT INTO orgs(id, name, owner_id, plan) VALUES + (1, 'Org One', 1, 'free'), + (2, 'Org Two', 2, 'free'); + `); + installBillingCheckoutAttemptSchema(database); + installStripeWebhookEventSchema(database); + installStripeSubscriptionObservationSchema(database); + return database; +} + +function insertSucceededAttempt(database, { + attemptId = 'attempt_one', + organizationId = 1, + sessionId = 'cs_scopeweave', + priceId = 'price_scopeweave', +} = {}) { + database.prepare(` + INSERT INTO billing_checkout_attempts( + attempt_id, organization_id, price_id, idempotency_key, attempt_state, + provider_session_id, created_at_ms, updated_at_ms + ) VALUES(?,?,?,?,?,?,?,?) + `).run( + attemptId, + organizationId, + priceId, + `idem_${attemptId}`, + 'provider_succeeded', + sessionId, + RECEIVED_AT_MS - 10, + RECEIVED_AT_MS - 5, + ); +} + +function insertVerifiedEvent(database, { + eventId = 'evt_checkout_completed', + sessionId = 'cs_scopeweave', + eventType = 'checkout.session.completed', + objectType = 'checkout.session', +} = {}) { + database.prepare(` + INSERT INTO billing_stripe_webhook_events( + event_id, provider_created_at_sec, event_type, object_id, object_type, + api_version, request_id, payload_sha256, first_received_at_ms + ) VALUES(?,?,?,?,?,?,?,?,?) + `).run( + eventId, + 1_787_100_000, + eventType, + sessionId, + objectType, + '2025-03-31.basil', + null, + 'a'.repeat(64), + RECEIVED_AT_MS, + ); +} + +function checkoutEvent({ + eventId = 'evt_checkout_completed', + sessionId = 'cs_scopeweave', + customerId = 'cus_scopeweave', + subscriptionId = 'sub_scopeweave', + type = 'checkout.session.completed', +} = {}) { + return { + id: eventId, + object: 'event', + type, + data: { + object: { + id: sessionId, + object: 'checkout.session', + mode: 'subscription', + customer: customerId, + subscription: subscriptionId, + }, + }, + }; +} + +test('verified Checkout completion permanently bootstraps Customer and Subscription tenant identity from the local successful attempt', () => { + const database = bootstrapDatabase(); + insertSucceededAttempt(database); + insertVerifiedEvent(database); + + assert.deepEqual( + bindVerifiedStripeCheckoutSessionIdentity(database, checkoutEvent()), + { + eventId: 'evt_checkout_completed', + organizationId: 1, + customerId: 'cus_scopeweave', + subscriptionId: 'sub_scopeweave', + bound: true, + }, + ); + + assert.deepEqual({ ...database.prepare(` + SELECT customer_id, organization_id, first_observed_at_ms + FROM billing_stripe_customers + `).get() }, { + customer_id: 'cus_scopeweave', + organization_id: 1, + first_observed_at_ms: RECEIVED_AT_MS, + }); + assert.deepEqual({ ...database.prepare(` + SELECT subscription_id, customer_id, first_observed_at_ms + FROM billing_stripe_subscriptions + `).get() }, { + subscription_id: 'sub_scopeweave', + customer_id: 'cus_scopeweave', + first_observed_at_ms: RECEIVED_AT_MS, + }); + assert.equal(database.prepare('SELECT plan FROM orgs WHERE id = 1').get().plan, 'free'); +}); + +test('exact verified Checkout completion replay is idempotent and does not rewrite first-observed evidence', () => { + const database = bootstrapDatabase(); + insertSucceededAttempt(database); + insertVerifiedEvent(database); + + assert.equal(bindVerifiedStripeCheckoutSessionIdentity(database, checkoutEvent()).bound, true); + assert.deepEqual(bindVerifiedStripeCheckoutSessionIdentity(database, checkoutEvent()), { + eventId: 'evt_checkout_completed', + organizationId: 1, + customerId: 'cus_scopeweave', + subscriptionId: 'sub_scopeweave', + bound: false, + }); + assert.equal(database.prepare('SELECT COUNT(*) AS count FROM billing_stripe_customers').get().count, 1); + assert.equal(database.prepare('SELECT COUNT(*) AS count FROM billing_stripe_subscriptions').get().count, 1); + assert.equal( + database.prepare('SELECT first_observed_at_ms FROM billing_stripe_customers').get().first_observed_at_ms, + RECEIVED_AT_MS, + ); +}); + +test('bootstrap fails closed unless the verified event and successful local Checkout attempt agree on exact session identity', () => { + const database = bootstrapDatabase(); + insertSucceededAttempt(database); + insertVerifiedEvent(database); + + for (const event of [ + checkoutEvent({ sessionId: 'cs_other' }), + checkoutEvent({ eventId: 'evt_missing' }), + checkoutEvent({ type: 'invoice.paid' }), + ]) { + assert.throws( + () => bindVerifiedStripeCheckoutSessionIdentity(database, event), + (error) => error instanceof StripeCheckoutIdentityBootstrapError, + ); + } + assert.equal(database.prepare('SELECT COUNT(*) AS count FROM billing_stripe_customers').get().count, 0); +}); + +test('ambiguous local session ownership and cross-tenant provider identity rebinding are rejected', () => { + const ambiguous = bootstrapDatabase(); + insertSucceededAttempt(ambiguous); + insertSucceededAttempt(ambiguous, { + attemptId: 'attempt_two', + organizationId: 2, + sessionId: 'cs_scopeweave', + priceId: 'price_other', + }); + insertVerifiedEvent(ambiguous); + assert.throws( + () => bindVerifiedStripeCheckoutSessionIdentity(ambiguous, checkoutEvent()), + (error) => error instanceof StripeCheckoutIdentityBootstrapError + && error.code === 'stripe_checkout_identity_ambiguous', + ); + + const rebound = bootstrapDatabase(); + insertSucceededAttempt(rebound); + insertVerifiedEvent(rebound); + bindVerifiedStripeCheckoutSessionIdentity(rebound, checkoutEvent()); + insertSucceededAttempt(rebound, { + attemptId: 'attempt_two', + organizationId: 2, + sessionId: 'cs_other', + priceId: 'price_other', + }); + insertVerifiedEvent(rebound, { + eventId: 'evt_checkout_other', + sessionId: 'cs_other', + }); + assert.throws( + () => bindVerifiedStripeCheckoutSessionIdentity(rebound, checkoutEvent({ + eventId: 'evt_checkout_other', + sessionId: 'cs_other', + })), + (error) => error instanceof StripeCheckoutIdentityBootstrapError + && error.code === 'stripe_checkout_identity_conflict', + ); + assert.equal(databaseCount(rebound, 'billing_stripe_customers'), 1); + assert.equal(databaseCount(rebound, 'billing_stripe_subscriptions'), 1); +}); + +function databaseCount(database, table) { + return database.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get().count; +} + +test('expanded or malformed provider identities fail closed before persistence', () => { + const database = bootstrapDatabase(); + insertSucceededAttempt(database); + insertVerifiedEvent(database); + + const malformed = [ + checkoutEvent({ customerId: { id: 'cus_scopeweave' } }), + checkoutEvent({ subscriptionId: { id: 'sub_scopeweave' } }), + { ...checkoutEvent(), data: { object: { ...checkoutEvent().data.object, mode: 'payment' } } }, + ]; + for (const event of malformed) { + assert.throws( + () => bindVerifiedStripeCheckoutSessionIdentity(database, event), + (error) => error instanceof StripeCheckoutIdentityBootstrapError + && error.code === 'stripe_checkout_identity_invalid', + ); + } + assert.equal(databaseCount(database, 'billing_stripe_customers'), 0); +}); + +test('Customer and Subscription bootstrap roll back together when the second identity write fails', () => { + const database = bootstrapDatabase(); + insertSucceededAttempt(database); + insertVerifiedEvent(database); + database.exec(` + CREATE TEMP TRIGGER force_subscription_identity_failure + BEFORE INSERT ON billing_stripe_subscriptions + BEGIN + SELECT RAISE(ABORT, 'forced identity failure'); + END; + `); + + assert.throws(() => bindVerifiedStripeCheckoutSessionIdentity(database, checkoutEvent())); + assert.equal(databaseCount(database, 'billing_stripe_customers'), 0); + assert.equal(databaseCount(database, 'billing_stripe_subscriptions'), 0); +}); From d0e51bf77787336402244b4eaa5f52ab9223ac37 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 09:49:31 -0700 Subject: [PATCH 02/11] feat(billing): bind verified Checkout completion identity --- server/stripe_checkout_identity_bootstrap.mjs | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 server/stripe_checkout_identity_bootstrap.mjs diff --git a/server/stripe_checkout_identity_bootstrap.mjs b/server/stripe_checkout_identity_bootstrap.mjs new file mode 100644 index 00000000..a5e5e05a --- /dev/null +++ b/server/stripe_checkout_identity_bootstrap.mjs @@ -0,0 +1,204 @@ +const MAX_PROVIDER_ID_LENGTH = 255; +const EVENT_ID_PATTERN = /^[A-Za-z0-9_:-]+$/u; +const SESSION_ID_PATTERN = /^cs_[A-Za-z0-9_]+$/u; +const CUSTOMER_ID_PATTERN = /^cus_[A-Za-z0-9_]+$/u; +const SUBSCRIPTION_ID_PATTERN = /^sub_[A-Za-z0-9_]+$/u; +const SAVEPOINT_NAME = 'billing_stripe_checkout_identity_bootstrap'; + +/** Stable fail-closed error for Checkout-completion identity bootstrap. */ +export class StripeCheckoutIdentityBootstrapError extends Error { + /** + * Create one sanitized identity-bootstrap failure. + * @param {string} code stable machine-readable failure code + * @param {number} [status=400] HTTP-compatible adapter status + */ + constructor(code, status = 400) { + super(code); + this.name = 'StripeCheckoutIdentityBootstrapError'; + this.code = code; + this.status = status; + } +} + +function bootstrapError(code, status = 400) { + return new StripeCheckoutIdentityBootstrapError(code, status); +} + +function boundedIdentifier(value, pattern) { + if ( + typeof value !== 'string' + || value.length === 0 + || value.length > MAX_PROVIDER_ID_LENGTH + || !pattern.test(value) + ) { + throw bootstrapError('stripe_checkout_identity_invalid'); + } + return value; +} + +function normalizedCheckoutCompletion(event) { + if (!event || typeof event !== 'object' || Array.isArray(event)) { + throw bootstrapError('stripe_checkout_identity_invalid'); + } + const eventId = boundedIdentifier(event.id, EVENT_ID_PATTERN); + if (event.type !== 'checkout.session.completed') { + throw bootstrapError('stripe_checkout_identity_invalid'); + } + const session = event.data?.object; + if (!session || typeof session !== 'object' || Array.isArray(session)) { + throw bootstrapError('stripe_checkout_identity_invalid'); + } + if (session.object !== 'checkout.session' || session.mode !== 'subscription') { + throw bootstrapError('stripe_checkout_identity_invalid'); + } + return Object.freeze({ + eventId, + sessionId: boundedIdentifier(session.id, SESSION_ID_PATTERN), + customerId: boundedIdentifier(session.customer, CUSTOMER_ID_PATTERN), + subscriptionId: boundedIdentifier(session.subscription, SUBSCRIPTION_ID_PATTERN), + }); +} + +function withSavepoint(database, operation) { + database.exec(`SAVEPOINT ${SAVEPOINT_NAME}`); + try { + const result = operation(); + database.exec(`RELEASE SAVEPOINT ${SAVEPOINT_NAME}`); + return result; + } catch (error) { + let rollbackSucceeded = false; + try { + database.exec(`ROLLBACK TO SAVEPOINT ${SAVEPOINT_NAME}`); + rollbackSucceeded = true; + } catch { + // Leave an unconfirmed failed savepoint open rather than risk partial commit. + } + if (rollbackSucceeded) { + try { + database.exec(`RELEASE SAVEPOINT ${SAVEPOINT_NAME}`); + } catch { + // Cleanup after confirmed rollback must not replace the causal failure. + } + } + throw error; + } +} + +/** + * Permanently bind verified Checkout Subscription identities to local tenant authority. + * + * The tenant source of truth is the already-succeeded local Checkout attempt whose + * server-recorded Stripe Session ID exactly matches the verified + * `checkout.session.completed` object. Signed Checkout customer/subscription fields + * therefore cannot choose an organization by themselves. The immutable verified + * event ledger must independently agree on event type, object type, and Session ID. + * + * Customer and Subscription identities are inserted into the same normalized tables + * used by authoritative Subscription observations. Existing identical bindings are + * idempotent; ambiguous local Session ownership or any Customer/Subscription rebind + * attempt fails closed. Both identity writes share one SQLite savepoint and this + * boundary never changes `orgs.plan`, entitlement claims, sessions, or RBAC. + * + * @param {import('node:sqlite').DatabaseSync} database bootstrapped SQLite database + * @param {Record} event cryptographically verified Stripe event + * @returns {Readonly<{eventId:string, organizationId:number, customerId:string, subscriptionId:string, bound:boolean}>} + * durable tenant/provider identity receipt + * @throws {StripeCheckoutIdentityBootstrapError} when authority is malformed, absent, ambiguous, or conflicting + */ +export function bindVerifiedStripeCheckoutSessionIdentity(database, event) { + if (!database || typeof database.prepare !== 'function' || typeof database.exec !== 'function') { + throw new TypeError('database must provide SQLite prepare/exec operations'); + } + const completion = normalizedCheckoutCompletion(event); + + const selectVerifiedEvent = database.prepare(` + SELECT event_type, object_id, object_type, first_received_at_ms + FROM billing_stripe_webhook_events + WHERE event_id = ? + `); + const selectAttempts = database.prepare(` + SELECT organization_id + FROM billing_checkout_attempts + WHERE provider_session_id = ? + AND attempt_state = 'provider_succeeded' + ORDER BY attempt_id + LIMIT 2 + `); + const selectCustomer = database.prepare(` + SELECT organization_id + FROM billing_stripe_customers + WHERE customer_id = ? + `); + const selectSubscription = database.prepare(` + SELECT customer_id + FROM billing_stripe_subscriptions + WHERE subscription_id = ? + `); + const insertCustomer = database.prepare(` + INSERT INTO billing_stripe_customers(customer_id, organization_id, first_observed_at_ms) + VALUES(?,?,?) + `); + const insertSubscription = database.prepare(` + INSERT INTO billing_stripe_subscriptions(subscription_id, customer_id, first_observed_at_ms) + VALUES(?,?,?) + `); + + return withSavepoint(database, () => { + const verified = selectVerifiedEvent.get(completion.eventId); + if (!verified + || verified.event_type !== 'checkout.session.completed' + || verified.object_type !== 'checkout.session' + || verified.object_id !== completion.sessionId + || !Number.isSafeInteger(verified.first_received_at_ms) + || verified.first_received_at_ms < 0) { + throw bootstrapError('stripe_checkout_identity_unverified', 409); + } + + const attempts = selectAttempts.all(completion.sessionId); + if (attempts.length === 0) { + throw bootstrapError('stripe_checkout_identity_unmatched', 409); + } + if (attempts.length !== 1) { + throw bootstrapError('stripe_checkout_identity_ambiguous', 409); + } + const organizationId = Number(attempts[0].organization_id); + if (!Number.isSafeInteger(organizationId) || organizationId <= 0) { + throw bootstrapError('stripe_checkout_identity_ambiguous', 409); + } + + const existingCustomer = selectCustomer.get(completion.customerId); + if (existingCustomer && Number(existingCustomer.organization_id) !== organizationId) { + throw bootstrapError('stripe_checkout_identity_conflict', 409); + } + const existingSubscription = selectSubscription.get(completion.subscriptionId); + if (existingSubscription && existingSubscription.customer_id !== completion.customerId) { + throw bootstrapError('stripe_checkout_identity_conflict', 409); + } + + let bound = false; + if (!existingCustomer) { + insertCustomer.run( + completion.customerId, + organizationId, + verified.first_received_at_ms, + ); + bound = true; + } + if (!existingSubscription) { + insertSubscription.run( + completion.subscriptionId, + completion.customerId, + verified.first_received_at_ms, + ); + bound = true; + } + + return Object.freeze({ + eventId: completion.eventId, + organizationId, + customerId: completion.customerId, + subscriptionId: completion.subscriptionId, + bound, + }); + }); +} From 811ca9c85d50a26c8822dfbed60a7e8d5203c6a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 09:50:17 -0700 Subject: [PATCH 03/11] test(billing): require Checkout identity bootstrap in webhook path --- ...-reconciliation-queue-integration.test.mjs | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/tests/unit/stripe-webhook-reconciliation-queue-integration.test.mjs b/tests/unit/stripe-webhook-reconciliation-queue-integration.test.mjs index 1e5e49c5..5181c86b 100644 --- a/tests/unit/stripe-webhook-reconciliation-queue-integration.test.mjs +++ b/tests/unit/stripe-webhook-reconciliation-queue-integration.test.mjs @@ -47,6 +47,63 @@ function subscriptionEvent({ }; } +function checkoutCompletedEvent({ + eventId = 'evt_queue_checkout_completed', + sessionId = 'cs_queue_checkout', + customerId = 'cus_queue_checkout', + subscriptionId = 'sub_queue_checkout', +} = {}) { + return { + id: eventId, + object: 'event', + api_version: '2025-03-31.basil', + created: NOW_SECONDS - 1, + type: 'checkout.session.completed', + request: null, + data: { + object: { + id: sessionId, + object: 'checkout.session', + mode: 'subscription', + customer: customerId, + subscription: subscriptionId, + }, + }, + }; +} + +function prepareSucceededCheckout({ + userId = 901, + organizationId = 901, + attemptId = 'attempt_queue_checkout', + sessionId = 'cs_queue_checkout', + priceId = 'price_queue_checkout', +} = {}) { + db.prepare(` + INSERT OR IGNORE INTO users(id, email, password_hash, name) + VALUES(?,?,?,?) + `).run(userId, `owner-${userId}@example.invalid`, 'hash', `Owner ${userId}`); + db.prepare(` + INSERT OR IGNORE INTO orgs(id, name, owner_id, plan) + VALUES(?,?,?,'free') + `).run(organizationId, `Org ${organizationId}`, userId); + db.prepare(` + INSERT INTO billing_checkout_attempts( + attempt_id, organization_id, price_id, idempotency_key, attempt_state, + provider_session_id, created_at_ms, updated_at_ms + ) VALUES(?,?,?,?,?,?,?,?) + `).run( + attemptId, + organizationId, + priceId, + `idem_${attemptId}`, + 'provider_succeeded', + sessionId, + NOW_SECONDS * 1000 - 10, + NOW_SECONDS * 1000 - 5, + ); +} + function oneOffInvoiceEvent() { return { id: 'evt_queue_one_off_invoice', @@ -84,6 +141,99 @@ test('production webhook bootstrap durably queues a verified Subscription trigge }); }); +test('verified Checkout completion bootstraps tenant identity before queuing reconciliation work', async () => { + prepareSucceededCheckout(); + const event = checkoutCompletedEvent(); + + await verifyStripeWebhookRequest(signedRequest(event), { + secret: WEBHOOK_SECRET, + nowSeconds: NOW_SECONDS, + }); + + assert.deepEqual({ ...db.prepare(` + SELECT customer_id, organization_id + FROM billing_stripe_customers + WHERE customer_id = ? + `).get('cus_queue_checkout') }, { + customer_id: 'cus_queue_checkout', + organization_id: 901, + }); + assert.deepEqual({ ...db.prepare(` + SELECT subscription_id, customer_id + FROM billing_stripe_subscriptions + WHERE subscription_id = ? + `).get('sub_queue_checkout') }, { + subscription_id: 'sub_queue_checkout', + customer_id: 'cus_queue_checkout', + }); + assert.deepEqual({ ...db.prepare(` + SELECT event_id, subscription_id, processing_state + FROM billing_stripe_reconciliation_triggers + WHERE event_id = ? + `).get(event.id) }, { + event_id: event.id, + subscription_id: 'sub_queue_checkout', + processing_state: 'pending', + }); + assert.equal(db.prepare('SELECT plan FROM orgs WHERE id = 901').get().plan, 'free'); +}); + +test('Checkout identity bootstrap and trigger queue roll back with verified event evidence on downstream failure', async () => { + prepareSucceededCheckout({ + userId: 902, + organizationId: 902, + attemptId: 'attempt_queue_checkout_failure', + sessionId: 'cs_queue_checkout_failure', + priceId: 'price_queue_checkout_failure', + }); + const event = checkoutCompletedEvent({ + eventId: 'evt_queue_checkout_failure', + sessionId: 'cs_queue_checkout_failure', + customerId: 'cus_queue_checkout_failure', + subscriptionId: 'sub_queue_checkout_failure', + }); + db.exec(` + CREATE TEMP TRIGGER force_checkout_identity_failure + BEFORE INSERT ON billing_stripe_subscriptions + WHEN NEW.subscription_id = 'sub_queue_checkout_failure' + BEGIN + SELECT RAISE(ABORT, 'forced checkout identity failure'); + END; + `); + + try { + await assert.rejects( + verifyStripeWebhookRequest(signedRequest(event), { + secret: WEBHOOK_SECRET, + nowSeconds: NOW_SECONDS, + }), + ); + } finally { + db.exec('DROP TRIGGER force_checkout_identity_failure'); + } + + for (const [table, column] of [ + ['billing_stripe_webhook_events', 'event_id'], + ['billing_stripe_webhook_deliveries', 'event_id'], + ['billing_stripe_reconciliation_triggers', 'event_id'], + ]) { + assert.equal( + db.prepare(`SELECT COUNT(*) AS count FROM ${table} WHERE ${column} = ?`).get(event.id).count, + 0, + ); + } + assert.equal( + db.prepare('SELECT COUNT(*) AS count FROM billing_stripe_customers WHERE customer_id = ?') + .get('cus_queue_checkout_failure').count, + 0, + ); + assert.equal( + db.prepare('SELECT COUNT(*) AS count FROM billing_stripe_subscriptions WHERE subscription_id = ?') + .get('sub_queue_checkout_failure').count, + 0, + ); +}); + test('exact verified webhook redelivery records delivery evidence without duplicating queued work', async () => { const event = subscriptionEvent({ eventId: 'evt_queue_redelivery', From c9f0d0887e591b49f97e72b70ff385ad00bd67f2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 09:50:51 -0700 Subject: [PATCH 04/11] feat(billing): queue completed subscription Checkout events --- .../stripe_webhook_reconciliation_queue.mjs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/server/stripe_webhook_reconciliation_queue.mjs b/server/stripe_webhook_reconciliation_queue.mjs index 08f0b91d..168354f5 100644 --- a/server/stripe_webhook_reconciliation_queue.mjs +++ b/server/stripe_webhook_reconciliation_queue.mjs @@ -79,14 +79,24 @@ function currentInvoiceSubscription(object) { return optionalSubscriptionId(details.subscription); } +function checkoutSessionSubscription(object) { + if (object.object !== 'checkout.session') { + throw queueError('stripe_reconciliation_trigger_invalid'); + } + if (object.mode !== 'subscription') return null; + return normalizedSubscriptionId(object.subscription); +} + /** * Extract the Subscription identity that a verified Stripe event should reconcile. * * The returned identifier is only a trigger key. It is never treated as current * lifecycle or entitlement authority; the reconciliation service must re-fetch - * current provider state before evaluating durable claims. Irrelevant events and - * one-off invoices return `null`. Contradictory current/legacy Invoice provenance - * fails closed instead of selecting one representation. + * current provider state before evaluating durable claims. A completed subscription + * Checkout Session is eligible only after the runtime also binds that Session to a + * unique successful local Checkout attempt. Payment/setup Checkout Sessions, + * irrelevant events, and one-off invoices return `null`. Contradictory current/ + * legacy Invoice provenance fails closed instead of selecting one representation. * * @param {Record} event cryptographically verified Stripe event * @returns {string|null} bounded Subscription identity to reconcile @@ -95,6 +105,10 @@ function currentInvoiceSubscription(object) { export function extractStripeSubscriptionReconciliationCandidate(event) { const { type, object } = requireEventEnvelope(event); + if (type === 'checkout.session.completed') { + return checkoutSessionSubscription(object); + } + if (type.startsWith('customer.subscription.')) { if (object.object !== 'subscription') { throw queueError('stripe_reconciliation_trigger_invalid'); From f2e05f4250314787598526029edcea0fefbde0d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 09:53:04 -0700 Subject: [PATCH 05/11] feat(billing): bootstrap Checkout identity before reconciliation queue --- server/db.mjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/db.mjs b/server/db.mjs index 45886c27..7386762c 100644 --- a/server/db.mjs +++ b/server/db.mjs @@ -13,6 +13,7 @@ import { createSqliteStripeWebhookEventRepository, installStripeWebhookEventSchema, } from './stripe_webhook_event_ledger.mjs'; +import { bindVerifiedStripeCheckoutSessionIdentity } from './stripe_checkout_identity_bootstrap.mjs'; import { createSqliteStripeWebhookReconciliationQueue, extractStripeSubscriptionReconciliationCandidate, @@ -210,6 +211,8 @@ installBillingCheckoutAttemptSchema(db); export const billingCheckoutAttempts = createSqliteBillingCheckoutAttemptRepository(db); installStripeWebhookEventSchema(db); export const stripeWebhookEvents = createSqliteStripeWebhookEventRepository(db); +installStripeSubscriptionObservationSchema(db); +export const stripeSubscriptionObservations = createSqliteStripeSubscriptionObservationRepository(db); installStripeWebhookReconciliationQueueSchema(db); export const stripeWebhookReconciliationQueue = createSqliteStripeWebhookReconciliationQueue(db); configureStripeWebhookEventRecorder((evidence) => { @@ -217,6 +220,9 @@ configureStripeWebhookEventRecorder((evidence) => { try { const eventReceipt = stripeWebhookEvents.recordVerifiedEvent(evidence); const subscriptionId = extractStripeSubscriptionReconciliationCandidate(evidence.event); + if (subscriptionId && evidence.event?.type === 'checkout.session.completed') { + bindVerifiedStripeCheckoutSessionIdentity(db, evidence.event); + } if (subscriptionId) { stripeWebhookReconciliationQueue.enqueue({ eventId: eventReceipt.eventId, @@ -243,8 +249,6 @@ configureStripeWebhookEventRecorder((evidence) => { throw error; } }); -installStripeSubscriptionObservationSchema(db); -export const stripeSubscriptionObservations = createSqliteStripeSubscriptionObservationRepository(db); installStripeInvoiceObservationSchema(db); export const stripeInvoiceObservations = createSqliteStripeInvoiceObservationRepository(db); installStripeEntitlementClaimSchema(db); From ce98999c9b9ab223981f8a9f5d05d9a0a68ccf17 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 09:53:21 -0700 Subject: [PATCH 06/11] test(billing): lock Checkout identity coverage registration --- ...entity-bootstrap-package-contract.test.mjs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/unit/stripe-checkout-identity-bootstrap-package-contract.test.mjs diff --git a/tests/unit/stripe-checkout-identity-bootstrap-package-contract.test.mjs b/tests/unit/stripe-checkout-identity-bootstrap-package-contract.test.mjs new file mode 100644 index 00000000..8ae85419 --- /dev/null +++ b/tests/unit/stripe-checkout-identity-bootstrap-package-contract.test.mjs @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; + +const packageJson = JSON.parse(readFileSync( + new URL('../../package.json', import.meta.url), + 'utf8', +)); + +const behaviorTest = 'node tests/unit/stripe-checkout-identity-bootstrap.test.mjs'; +const productionInclude = '--include=server/stripe_checkout_identity_bootstrap.mjs'; + +assert.match( + packageJson.scripts['test:unit'], + new RegExp(behaviorTest.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), + 'normal unit CI must execute the Checkout identity bootstrap regression', +); +assert.match( + packageJson.scripts['test:coverage'], + new RegExp(productionInclude.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), + 'canonical coverage must instrument the Checkout identity bootstrap module', +); +assert.match( + packageJson.scripts['test:coverage:cases'], + new RegExp(behaviorTest.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), + 'canonical coverage cases must execute the Checkout identity bootstrap regression', +); + +console.log('✓ Checkout identity bootstrap package registration contract passed'); From f17c8dbc7479df0b0da69c36c82aa21b38d8c134 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 09:54:03 -0700 Subject: [PATCH 07/11] test(billing): register Checkout identity bootstrap coverage --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 3745029f..0deb26ee 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "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 && node tests/api/billing-effective-plan-status.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-effective-plan.test.mjs && node tests/unit/billing-effective-plan-bootstrap.test.mjs && node tests/unit/billing-status-package-contract.test.mjs && node tests/unit/billing-status-response.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/stripe-webhook-event-ledger.test.mjs && node tests/unit/stripe-webhook-recorder-integration.test.mjs && node tests/unit/stripe-subscription-provider.test.mjs && node tests/unit/stripe-subscription-metadata-propagation.test.mjs && node tests/unit/stripe-subscription-observation-ledger.test.mjs && node tests/unit/stripe-subscription-current-projection.test.mjs && node tests/unit/stripe-entitlement-policy.test.mjs && node tests/unit/stripe-entitlement-policy-edge.test.mjs && node tests/unit/stripe-entitlement-policy-duplicate-claims.test.mjs && node tests/unit/stripe-invoice-provider.test.mjs && node tests/unit/stripe-invoice-provider-edge.test.mjs && node tests/unit/stripe-invoice-observation-ledger.test.mjs && node tests/unit/stripe-invoice-current-projection-package-contract.test.mjs && node tests/unit/stripe-invoice-current-projection.test.mjs && node tests/unit/stripe-entitlement-claim-package-contract.test.mjs && node tests/unit/stripe-entitlement-claim-ledger.test.mjs && node tests/unit/stripe-billing-authoritative-reconciliation.test.mjs && node tests/unit/stripe-webhook-reconciliation-queue.test.mjs && node tests/unit/stripe-webhook-reconciliation-queue-integration.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/billing_status_response.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --include=server/stripe_webhook.mjs --include=server/stripe_webhook_event_ledger.mjs --include=server/stripe_subscription_provider.mjs --include=server/stripe_subscription_observation_ledger.mjs --include=server/stripe_subscription_current_projection.mjs --include=server/stripe_entitlement_policy.mjs --include=server/stripe_invoice_provider.mjs --include=server/stripe_invoice_observation_ledger.mjs --include=server/stripe_invoice_current_projection.mjs --include=server/stripe_entitlement_claim_ledger.mjs --include=server/stripe_billing_reconciliation.mjs --include=server/stripe_webhook_reconciliation_queue.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-effective-plan.test.mjs && node tests/unit/billing-effective-plan-bootstrap.test.mjs && node tests/unit/billing-status-package-contract.test.mjs && node tests/unit/billing-status-response.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/stripe-webhook-event-ledger.test.mjs && node tests/unit/stripe-webhook-recorder-integration.test.mjs && node tests/unit/stripe-subscription-provider.test.mjs && node tests/unit/stripe-subscription-metadata-propagation.test.mjs && node tests/unit/stripe-subscription-observation-ledger.test.mjs && node tests/unit/stripe-subscription-current-projection.test.mjs && node tests/unit/stripe-entitlement-policy.test.mjs && node tests/unit/stripe-entitlement-policy-edge.test.mjs && node tests/unit/stripe-entitlement-policy-duplicate-claims.test.mjs && node tests/unit/stripe-invoice-provider.test.mjs && node tests/unit/stripe-invoice-provider-edge.test.mjs && node tests/unit/stripe-invoice-observation-ledger.test.mjs && node tests/unit/stripe-invoice-current-projection-package-contract.test.mjs && node tests/unit/stripe-invoice-current-projection.test.mjs && node tests/unit/stripe-entitlement-claim-package-contract.test.mjs && node tests/unit/stripe-entitlement-claim-ledger.test.mjs && node tests/unit/stripe-billing-authoritative-reconciliation.test.mjs && node tests/unit/stripe-webhook-reconciliation-queue.test.mjs && node tests/unit/stripe-webhook-reconciliation-queue-integration.test.mjs && npm run test:api", + "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-effective-plan.test.mjs && node tests/unit/billing-effective-plan-bootstrap.test.mjs && node tests/unit/billing-status-package-contract.test.mjs && node tests/unit/billing-status-response.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/stripe-webhook-event-ledger.test.mjs && node tests/unit/stripe-webhook-recorder-integration.test.mjs && node tests/unit/stripe-subscription-provider.test.mjs && node tests/unit/stripe-subscription-metadata-propagation.test.mjs && node tests/unit/stripe-subscription-observation-ledger.test.mjs && node tests/unit/stripe-subscription-current-projection.test.mjs && node tests/unit/stripe-entitlement-policy.test.mjs && node tests/unit/stripe-entitlement-policy-edge.test.mjs && node tests/unit/stripe-entitlement-policy-duplicate-claims.test.mjs && node tests/unit/stripe-invoice-provider.test.mjs && node tests/unit/stripe-invoice-provider-edge.test.mjs && node tests/unit/stripe-invoice-observation-ledger.test.mjs && node tests/unit/stripe-invoice-current-projection-package-contract.test.mjs && node tests/unit/stripe-invoice-current-projection.test.mjs && node tests/unit/stripe-entitlement-claim-package-contract.test.mjs && node tests/unit/stripe-entitlement-claim-ledger.test.mjs && node tests/unit/stripe-billing-authoritative-reconciliation.test.mjs && node tests/unit/stripe-webhook-reconciliation-queue.test.mjs && node tests/unit/stripe-checkout-identity-bootstrap-package-contract.test.mjs && node tests/unit/stripe-checkout-identity-bootstrap.test.mjs && node tests/unit/stripe-webhook-reconciliation-queue-integration.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/billing_status_response.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --include=server/stripe_webhook.mjs --include=server/stripe_webhook_event_ledger.mjs --include=server/stripe_subscription_provider.mjs --include=server/stripe_subscription_observation_ledger.mjs --include=server/stripe_subscription_current_projection.mjs --include=server/stripe_entitlement_policy.mjs --include=server/stripe_invoice_provider.mjs --include=server/stripe_invoice_observation_ledger.mjs --include=server/stripe_invoice_current_projection.mjs --include=server/stripe_entitlement_claim_ledger.mjs --include=server/stripe_billing_reconciliation.mjs --include=server/stripe_webhook_reconciliation_queue.mjs --include=server/stripe_checkout_identity_bootstrap.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-effective-plan.test.mjs && node tests/unit/billing-effective-plan-bootstrap.test.mjs && node tests/unit/billing-status-package-contract.test.mjs && node tests/unit/billing-status-response.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/stripe-webhook-event-ledger.test.mjs && node tests/unit/stripe-webhook-recorder-integration.test.mjs && node tests/unit/stripe-subscription-provider.test.mjs && node tests/unit/stripe-subscription-metadata-propagation.test.mjs && node tests/unit/stripe-subscription-observation-ledger.test.mjs && node tests/unit/stripe-subscription-current-projection.test.mjs && node tests/unit/stripe-entitlement-policy.test.mjs && node tests/unit/stripe-entitlement-policy-edge.test.mjs && node tests/unit/stripe-entitlement-policy-duplicate-claims.test.mjs && node tests/unit/stripe-invoice-provider.test.mjs && node tests/unit/stripe-invoice-provider-edge.test.mjs && node tests/unit/stripe-invoice-observation-ledger.test.mjs && node tests/unit/stripe-invoice-current-projection-package-contract.test.mjs && node tests/unit/stripe-invoice-current-projection.test.mjs && node tests/unit/stripe-entitlement-claim-package-contract.test.mjs && node tests/unit/stripe-entitlement-claim-ledger.test.mjs && node tests/unit/stripe-billing-authoritative-reconciliation.test.mjs && node tests/unit/stripe-webhook-reconciliation-queue.test.mjs && node tests/unit/stripe-checkout-identity-bootstrap.test.mjs && node tests/unit/stripe-webhook-reconciliation-queue-integration.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", From 83a3fb79de38d8a4919e76b4da50d54d47b06b68 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 09:54:41 -0700 Subject: [PATCH 08/11] test(billing): protect Checkout identity coverage path --- tests/unit/coverage-script-contract.test.mjs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/unit/coverage-script-contract.test.mjs b/tests/unit/coverage-script-contract.test.mjs index 199596af..3b57d82e 100644 --- a/tests/unit/coverage-script-contract.test.mjs +++ b/tests/unit/coverage-script-contract.test.mjs @@ -64,6 +64,11 @@ assert.match( /--include=server\/stripe_webhook_reconciliation_queue\.mjs/, 'the verified Stripe reconciliation trigger queue is instrumented', ); +assert.match( + scripts['test:coverage'], + /--include=server\/stripe_checkout_identity_bootstrap\.mjs/, + 'the verified Checkout identity bootstrap boundary is instrumented', +); assert.match( scripts['test:coverage'], /--include=server\/stripe_subscription_provider\.mjs/, @@ -154,6 +159,11 @@ assert.match( /tests\/unit\/stripe-webhook-reconciliation-queue\.test\.mjs/, 'the durable Stripe reconciliation queue regression executes under c8', ); +assert.match( + scripts['test:coverage:cases'], + /tests\/unit\/stripe-checkout-identity-bootstrap\.test\.mjs/, + 'the verified Checkout identity bootstrap regression executes under c8', +); assert.match( scripts['test:coverage:cases'], /tests\/unit\/stripe-webhook-reconciliation-queue-integration\.test\.mjs/, @@ -164,6 +174,11 @@ assert.match( /tests\/unit\/stripe-webhook-reconciliation-queue\.test\.mjs/, 'normal unit CI executes the durable Stripe reconciliation queue regression', ); +assert.match( + scripts['test:unit'], + /tests\/unit\/stripe-checkout-identity-bootstrap\.test\.mjs/, + 'normal unit CI executes the verified Checkout identity bootstrap regression', +); assert.match( scripts['test:unit'], /tests\/unit\/stripe-webhook-reconciliation-queue-integration\.test\.mjs/, From 57c0f60b564002ef4dfae3e0756f37eef737d269 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 09:55:52 -0700 Subject: [PATCH 09/11] docs(billing): trace Checkout tenant identity bootstrap --- .../stripe-checkout-identity-bootstrap.md | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 docs/doctoring/stripe-checkout-identity-bootstrap.md diff --git a/docs/doctoring/stripe-checkout-identity-bootstrap.md b/docs/doctoring/stripe-checkout-identity-bootstrap.md new file mode 100644 index 00000000..b5cfc0e2 --- /dev/null +++ b/docs/doctoring/stripe-checkout-identity-bootstrap.md @@ -0,0 +1,64 @@ +# Stripe Checkout tenant-identity bootstrap + +## Status boundary + +This document describes active stacked work on `feat/stripe-checkout-identity-bootstrap-488`. It is **not protected-`develop` shipped truth** until the owning pull request and its prerequisite Stripe lifecycle stack integrate under the live ruleset. The parent slice queues verified provider events for later authoritative reconciliation; this child closes the missing first-subscription tenant-identity bootstrap needed before that reconciliation can resolve a ScopeWeave organization without trusting webhook metadata as authority. + +## Buyer and integrity problem + +The authoritative Subscription reader and reconciliation service deliberately require an expected ScopeWeave organization before accepting provider state. That fail-closed rule creates a bootstrap question for a newly purchased subscription: the first verified Subscription/Invoice event can name a Stripe Subscription, but it must not be allowed to choose the local organization by mutable metadata or arrival order. + +ScopeWeave already has a stronger server-owned anchor. A successful Checkout attempt records both its local `organization_id` and the exact Stripe Checkout `provider_session_id`. Stripe documents that a successfully completed Checkout Session contains Customer and, in subscription mode, Subscription references, and that successful completion emits `checkout.session.completed`. The bootstrap therefore joins the cryptographically verified Checkout Session event to one unique local successful attempt by the exact Session ID before persisting Customer/Subscription identity. + +## Authority chain + +```mermaid +flowchart LR + A[Local Checkout attempt\norganization_id + provider_session_id] -->|exact unique Session ID| C[Identity bootstrap] + B[Verified checkout.session.completed\nimmutable event ledger] -->|event/object/session match| C + C --> D[billing_stripe_customers\nCustomer → organization] + C --> E[billing_stripe_subscriptions\nSubscription → Customer] + E --> F[Pending reconciliation trigger] + F --> G[Authoritative Subscription / Invoice reads] + G --> H[Durable entitlement claim] +``` + +The signed event supplies provider identities, but the local successful attempt supplies tenant authority. Neither source is sufficient by itself. The event ledger must independently agree on event type, Checkout object type, and Session ID. Exactly one successful local attempt must own that Session ID. + +## Fail-closed rules + +- Only `checkout.session.completed` whose object is a `checkout.session` in `subscription` mode is eligible. +- Session, Customer, and Subscription identities must be bounded canonical provider identifiers; expanded objects or malformed identities are rejected in this slice rather than guessed. +- The verified event ledger row must exist and match the exact Session ID and object/event type. +- Zero successful local attempts for the Session fails as unmatched authority; two or more fail as ambiguous authority. +- An existing Customer can only remain bound to the same organization; an existing Subscription can only remain bound to the same Customer. +- Exact replay of the same verified binding is idempotent and does not rewrite first-observed time. +- The bootstrap never writes `orgs.plan`, entitlement claims, sessions, membership, or RBAC authority. + +## Atomicity and recovery + +The event recorder already owns an outer SQLite savepoint covering verified event evidence and reconciliation-trigger creation. This slice installs the normalized Customer/Subscription schema before recorder configuration, executes identity bootstrap inside that same outer savepoint, and then queues the trigger. The bootstrap itself uses a nested savepoint. A failure in Customer/Subscription binding or later trigger creation therefore rolls the event delivery, immutable event fact, identity rows, and trigger back together. + +SQLite documents that nested savepoints remain reversible by an enclosing rollback and that `ROLLBACK TO` rewinds changes after the savepoint while keeping the savepoint active until release. Cleanup releases only after rollback is confirmed; a cleanup failure does not replace the causal error. + +## Verification and traceability + +The focused behavior regression covers successful tenant bootstrap, exact replay, event/session mismatch, missing verified event, wrong event type, duplicate successful local Session ownership, cross-tenant Customer/Subscription rebinding, malformed or expanded identities, plan non-mutation, and a real SQLite trigger-induced second-write failure proving Customer and Subscription inserts roll back together. + +The production webhook integration regression uses the real bootstrapped `server/db.mjs` recorder path. It proves verified `checkout.session.completed` creates normalized tenant identity before pending reconciliation work and proves a forced identity-write failure removes the verified event, delivery, identity rows, and trigger together. `package.json`, the focused package contract, and the canonical coverage contract register the production module and regression in normal unit and c8 execution. + +## Rollback + +Before protected integration, rollback removes the bootstrap module/tests and restores the parent recorder ordering/extraction. After integration, rollback must preserve any already-created Customer/Subscription identity rows as historical provider identity unless an operator proves they were incorrectly bound; deleting durable identity as part of code rollback would discard audit evidence. No automatic plan or entitlement reversal is required because this slice never changes access authority. + +## References + +SQLite Consortium. (2026). *Savepoints*. SQLite. https://www.sqlite.org/lang_savepoint.html + +Stripe. (2026). *Checkout Sessions*. Stripe API Reference. https://docs.stripe.com/api/checkout/sessions + +Stripe. (2026). *The Checkout Session object*. Stripe API Reference. https://docs.stripe.com/api/checkout/sessions/object + +Stripe. (2026). *Types of events*. Stripe API Reference. https://docs.stripe.com/api/events/types + +Stripe. (2026). *Fulfill orders*. Stripe Documentation. https://docs.stripe.com/checkout/fulfillment From 7e480f0c10ac5598e641fff03d6f7d7a8afca8f2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 09:56:37 -0700 Subject: [PATCH 10/11] docs(changelog): record Checkout identity bootstrap --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6612d0ca..52f8e111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security +- Bound verified subscription-mode `checkout.session.completed` Customer and + Subscription identities to exactly one server-recorded successful local Checkout + Session before creating reconciliation work; missing or ambiguous Session + ownership and cross-tenant identity rebinding fail closed, exact replay is + idempotent, and event/identity/trigger writes roll back together without changing + `orgs.plan` or granting entitlement. - Added bounded authoritative Stripe billing reconciliation that treats webhook event identity as provenance only, re-reads current Subscription and Invoice state before appending evidence, and evaluates durable entitlement claims only From e6ce16e967c50615f31d28db0b81cb214827c610 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:01:01 -0700 Subject: [PATCH 11/11] test(billing): inherit queue row normalization --- .../stripe-webhook-reconciliation-queue-integration.test.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/stripe-webhook-reconciliation-queue-integration.test.mjs b/tests/unit/stripe-webhook-reconciliation-queue-integration.test.mjs index 5181c86b..f1da6506 100644 --- a/tests/unit/stripe-webhook-reconciliation-queue-integration.test.mjs +++ b/tests/unit/stripe-webhook-reconciliation-queue-integration.test.mjs @@ -130,11 +130,12 @@ test('production webhook bootstrap durably queues a verified Subscription trigge }); assert.equal(verified.id, event.id); - assert.deepEqual(db.prepare(` + const trigger = db.prepare(` SELECT event_id, subscription_id, processing_state FROM billing_stripe_reconciliation_triggers WHERE event_id = ? - `).get(event.id), { + `).get(event.id); + assert.deepEqual({ ...trigger }, { event_id: event.id, subscription_id: 'sub_queue_subscription', processing_state: 'pending',