From 5b698f6fdcedb9ba46bb18a4bb4698daaaeebee7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 10:14:54 -0700 Subject: [PATCH 1/8] test(billing): require claim-backed effective plan limits --- tests/unit/billing-effective-plan.test.mjs | 115 +++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 tests/unit/billing-effective-plan.test.mjs diff --git a/tests/unit/billing-effective-plan.test.mjs b/tests/unit/billing-effective-plan.test.mjs new file mode 100644 index 00000000..aa5dd6d1 --- /dev/null +++ b/tests/unit/billing-effective-plan.test.mjs @@ -0,0 +1,115 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DatabaseSync } from 'node:sqlite'; + +import { PLANS, effectivePlanOf, wouldExceed } from '../../server/billing.mjs'; + +function database({ withClaims = true } = {}) { + const db = new DatabaseSync(':memory:'); + db.exec(` + CREATE TABLE projects(id INTEGER PRIMARY KEY, org_id INTEGER NOT NULL); + CREATE TABLE memberships(id INTEGER PRIMARY KEY, org_id INTEGER NOT NULL); + INSERT INTO projects(org_id) VALUES(42),(42); + INSERT INTO memberships(org_id) VALUES(42),(42),(42); + `); + if (withClaims) { + db.exec(` + CREATE TABLE billing_stripe_customers(customer_id TEXT PRIMARY KEY, organization_id INTEGER NOT NULL); + CREATE TABLE billing_stripe_subscriptions(subscription_id TEXT PRIMARY KEY, customer_id TEXT NOT NULL); + CREATE TABLE billing_stripe_entitlement_decisions( + decision_id INTEGER PRIMARY KEY, + subscription_id TEXT NOT NULL, + entitled INTEGER NOT NULL, + valid_until_sec INTEGER + ); + CREATE TABLE billing_stripe_entitlement_claim_heads( + subscription_id TEXT PRIMARY KEY, + decision_id INTEGER NOT NULL + ); + INSERT INTO billing_stripe_customers VALUES('cus_42',42),('cus_77',77); + INSERT INTO billing_stripe_subscriptions VALUES('sub_42','cus_42'),('sub_other','cus_77'); + `); + } + return db; +} + +function freeOrg() { return { id: 42, plan: 'free' }; } + +test('free limits remain enforced when there is no current Stripe claim', () => { + const db = database(); + assert.equal(effectivePlanOf(db, freeOrg(), { nowSec: 1000 }), PLANS.free); + assert.equal(wouldExceed(db, freeOrg(), 'projects', { nowSec: 1000 }), true); + assert.equal(wouldExceed(db, freeOrg(), 'members', { nowSec: 1000 }), true); + db.close(); +}); + +test('one unexpired current entitled claim reversibly unlocks Pro resource limits', () => { + const db = database(); + db.exec(` + INSERT INTO billing_stripe_entitlement_decisions VALUES(1,'sub_42',1,2000); + INSERT INTO billing_stripe_entitlement_claim_heads VALUES('sub_42',1); + `); + assert.equal(effectivePlanOf(db, freeOrg(), { nowSec: 1000 }), PLANS.pro); + assert.equal(wouldExceed(db, freeOrg(), 'projects', { nowSec: 1000 }), false); + assert.equal(wouldExceed(db, freeOrg(), 'members', { nowSec: 1000 }), false); + db.close(); +}); + +test('claim expiry and a later revoke head re-lock Pro limits without mutating org.plan', () => { + const db = database(); + db.exec(` + INSERT INTO billing_stripe_entitlement_decisions VALUES(1,'sub_42',1,2000); + INSERT INTO billing_stripe_entitlement_claim_heads VALUES('sub_42',1); + `); + assert.equal(effectivePlanOf(db, freeOrg(), { nowSec: 2000 }), PLANS.free); + assert.equal(wouldExceed(db, freeOrg(), 'projects', { nowSec: 2000 }), true); + db.exec(` + INSERT INTO billing_stripe_entitlement_decisions VALUES(2,'sub_42',0,NULL); + UPDATE billing_stripe_entitlement_claim_heads SET decision_id=2 WHERE subscription_id='sub_42'; + `); + assert.equal(effectivePlanOf(db, freeOrg(), { nowSec: 1500 }), PLANS.free); + assert.equal(freeOrg().plan, 'free'); + db.close(); +}); + +test('foreign-tenant claims never unlock this organization and one valid local claim among many is sufficient', () => { + const db = database(); + db.exec(` + INSERT INTO billing_stripe_entitlement_decisions VALUES(1,'sub_other',1,5000); + INSERT INTO billing_stripe_entitlement_claim_heads VALUES('sub_other',1); + `); + assert.equal(effectivePlanOf(db, freeOrg(), { nowSec: 1000 }), PLANS.free); + db.exec(` + INSERT INTO billing_stripe_entitlement_decisions VALUES(2,'sub_42',1,5000); + INSERT INTO billing_stripe_entitlement_claim_heads VALUES('sub_42',2); + `); + assert.equal(effectivePlanOf(db, freeOrg(), { nowSec: 1000 }), PLANS.pro); + db.close(); +}); + +test('static Pro remains an explicit non-Stripe override and never depends on claim-table health', () => { + const db = database({ withClaims: false }); + const org = { id: 42, plan: 'pro' }; + assert.equal(effectivePlanOf(db, org, { nowSec: 1000 }), PLANS.pro); + assert.equal(wouldExceed(db, org, 'projects', { nowSec: 1000 }), false); + db.close(); +}); + +test('missing or unreadable claim tables fail closed to the stored plan rather than manufacturing Pro', () => { + const db = database({ withClaims: false }); + assert.equal(effectivePlanOf(db, freeOrg(), { nowSec: 1000 }), PLANS.free); + assert.equal(wouldExceed(db, freeOrg(), 'projects', { nowSec: 1000 }), true); + db.close(); +}); + +test('effective-plan authority and clock inputs are bounded before entitlement lookup', () => { + const db = database(); + for (const org of [null, {}, { id: 0, plan: 'free' }, { id: '42', plan: 'free' }]) { + assert.throws(() => effectivePlanOf(db, org, { nowSec: 1000 }), TypeError); + } + for (const nowSec of [-1, 1.5, '1000', Number.MAX_SAFE_INTEGER + 1]) { + assert.throws(() => effectivePlanOf(db, freeOrg(), { nowSec }), TypeError); + } + assert.throws(() => effectivePlanOf(null, freeOrg(), { nowSec: 1000 }), TypeError); + db.close(); +}); From 260eb18b6bd0f586cd8cbef7bc14c12e90b7b6d3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 10:17:49 -0700 Subject: [PATCH 2/8] feat(billing): apply claim-backed effective plan limits --- CHANGELOG.md | 4 ++ .../stripe-effective-plan-authorization.md | 50 ++++++++++++++++ package.json | 4 +- server/billing.mjs | 58 +++++++++++++++++-- 4 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 docs/doctoring/stripe-effective-plan-authorization.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c4749c58..353e6be4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security +- Applied current durable Stripe entitlement claims to project/member limit + authorization without mutating `orgs.plan`: one unexpired tenant-owned claim + unlocks Pro limits, expiry or a revoke head re-locks them, foreign claims are + isolated, and missing claim storage never manufactures paid authority. - Persist deterministic Stripe entitlement-policy decisions as append-only, tenant-bound claim evidence with optimistic previous-decision concurrency, automatic current Subscription/Invoice evidence selection, exact source diff --git a/docs/doctoring/stripe-effective-plan-authorization.md b/docs/doctoring/stripe-effective-plan-authorization.md new file mode 100644 index 00000000..96a7f9b3 --- /dev/null +++ b/docs/doctoring/stripe-effective-plan-authorization.md @@ -0,0 +1,50 @@ +# Claim-backed effective plan authorization + +## Status and authority + +**Status: active stacked PR evidence, not protected-`develop` shipped truth.** + +This bounded #488 slice is stacked on the transactional entitlement-claim ledger. Protected `develop` remains shipped authority until the prerequisite billing stack is independently reviewed and integrated. + +The slice applies durable Stripe claim state to ScopeWeave's existing project/member plan-limit authorization. It does not change membership roles, authentication, Stripe provider state, claim derivation, or `orgs.plan` persistence. + +## Buyer objective + +A durable paid claim is not commercially useful if product limits still read only the legacy `orgs.plan` column. Conversely, copying Stripe state into that column would destroy provenance and make revocation/recovery ambiguous. ScopeWeave therefore resolves an **effective access plan** at the resource-limit boundary without mutating the stored plan. + +`effectivePlanOf` in `server/billing.mjs` treats an explicit stored `pro` plan as a manual/legacy non-Stripe override. Otherwise, an organization receives Pro limits only while at least one current Stripe entitlement claim head for that exact organization is both `entitled = 1` and has `valid_until_sec` strictly greater than the current epoch second. + +When that claim expires or the current head advances to a revoke/deny claim, the organization automatically returns to its stored Free limits. No plan-row rewrite or compensating revocation mutation is required. + +## Tenant and failure boundary + +The claim query joins current claim head → decision → Subscription → Customer → organization and filters on the exact server-owned organization ID. A claim from another tenant cannot unlock this organization. + +The helper validates a positive safe-integer organization ID, a SQLite-like database boundary, and a non-negative safe-integer clock before entitlement lookup. Claim-table absence or read failure does not manufacture paid authority: resolution fails closed to the explicit stored plan. This permits rolling schema deployment and incident containment without turning persistence failure into Pro access. + +An explicit stored Pro plan remains independent of Stripe claim-table health. This preserves pre-existing/manual commercial authority and avoids silently revoking a non-Stripe contract during Stripe reconciliation incidents. + +## Resource-limit application + +`wouldExceed` now evaluates project/member limits from `effectivePlanOf`. Existing callers require no new browser-provided authority and cannot supply claim IDs. Consequently: + +- Free organizations at their current limit remain blocked without a claim; +- one unexpired tenant-owned claim unlocks Pro's unlimited project/member limits; +- exact expiry re-locks the limits; +- a later false/revoked current claim re-locks immediately; +- a foreign-tenant claim has no effect; and +- stored Pro remains unlimited even if claim tables are absent. + +This is an authorization application, but it is intentionally narrow: it governs the existing commercial resource limits only. A later UI/API truth-status slice should surface claim-backed effective billing status so buyers see the same authority the server enforces. + +## TDD and rollback + +Test-only commit introduces `tests/unit/billing-effective-plan.test.mjs` before production changes and requires claim-backed unlock, expiry/revoke re-lock, tenant isolation, stored-Pro override, fail-closed missing-table behavior, and bounded authority inputs. The production change keeps `server/billing.mjs` in the existing canonical c8 producer and registers the focused regression in normal unit CI and coverage cases. + +Rollback restores `wouldExceed` to stored-plan-only behavior and removes the effective-plan regression, doctoring record, and matching Unreleased changelog entry. It does not alter claim evidence or require a database migration. + +## References + +SQLite. (n.d.). *SELECT*. https://www.sqlite.org/lang_select.html + +Stripe. (n.d.). *The Subscription object*. Stripe API Reference. https://docs.stripe.com/api/subscriptions/object diff --git a/package.json b/package.json index 27af2659..4b1f966d 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/stripe-webhook.test.mjs", - "test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/billing-configuration.test.mjs && node tests/unit/billing-checkout-attempt.test.mjs && node tests/unit/billing-checkout-attempt-authority.test.mjs && node tests/unit/billing-checkout-reconciliation.test.mjs && node tests/unit/billing-checkout-reconciliation-authority.test.mjs && node tests/unit/billing-checkout.test.mjs && node tests/unit/billing-provider-boundary.test.mjs && node tests/unit/billing-checkout-review-regressions.test.mjs && node tests/unit/stripe-webhook-boundary.test.mjs && node tests/unit/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/toast-accessibility.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-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/toast-accessibility.test.mjs", "test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/application_routes.mjs --include=server/auth.mjs --include=server/billing.mjs --include=server/billing_checkout_attempt.mjs --include=server/billing_configuration.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --include=server/stripe_webhook.mjs --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 --reporter=json --reporter=json-summary npm run test:coverage:cases", - "test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/billing-configuration.test.mjs && node tests/unit/billing-checkout-attempt.test.mjs && node tests/unit/billing-checkout-attempt-authority.test.mjs && node tests/unit/billing-checkout-reconciliation.test.mjs && node tests/unit/billing-checkout-reconciliation-authority.test.mjs && node tests/unit/billing-checkout.test.mjs && node tests/unit/billing-provider-boundary.test.mjs && node tests/unit/billing-checkout-review-regressions.test.mjs && node tests/unit/stripe-webhook-boundary.test.mjs && 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 && npm run test:api", + "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-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 && 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", diff --git a/server/billing.mjs b/server/billing.mjs index 21cf28e7..acc9d3ac 100644 --- a/server/billing.mjs +++ b/server/billing.mjs @@ -15,11 +15,61 @@ export const PLANS = { pro: { name: 'Pro', limits: { projects: null, members: null }, priceKrw: 19000 }, // null = unlimited }; -/** Return the effective plan definition for an organization-like record. */ +/** Return the stored/manual plan definition for an organization-like record. */ export function planOf(org) { return PLANS[org?.plan] || PLANS.free; } +function requireEffectivePlanAuthority(db, org, nowSec) { + if (!db || typeof db.prepare !== 'function') throw new TypeError('db must provide SQLite prepare operations'); + if (!org || !Number.isSafeInteger(org.id) || org.id <= 0) throw new TypeError('org.id must be a positive safe integer'); + if (!Number.isSafeInteger(nowSec) || nowSec < 0) throw new TypeError('nowSec must be a non-negative safe integer'); +} + +function hasCurrentStripeProClaim(db, organizationId, nowSec) { + try { + return Boolean(db.prepare(` + SELECT 1 AS active + FROM billing_stripe_entitlement_claim_heads h + JOIN billing_stripe_entitlement_decisions d ON d.decision_id = h.decision_id + JOIN billing_stripe_subscriptions s ON s.subscription_id = h.subscription_id + JOIN billing_stripe_customers c ON c.customer_id = s.customer_id + WHERE c.organization_id = ? + AND d.entitled = 1 + AND d.valid_until_sec IS NOT NULL + AND d.valid_until_sec > ? + LIMIT 1 + `).get(organizationId, nowSec)); + } catch { + // Claim storage can be absent during rolling migration or unavailable during + // an incident. Never manufacture paid authority from missing evidence; fall + // back to the explicit stored/manual plan instead. + return false; + } +} + +/** + * Return the plan that currently authorizes resource-limit behavior. + * + * An explicit stored Pro plan remains a non-Stripe/manual override. Otherwise a + * free organization becomes Pro only while at least one current Stripe claim head + * is both entitled and unexpired. Revocation or expiry therefore re-locks Pro + * limits without mutating `orgs.plan`. Claim-table absence or read failure fails + * closed to the stored plan and never creates paid authority. + * + * @param {object} db SQLite-like database with prepare/get support + * @param {{id:number, plan?:string}} org organization authority + * @param {{nowSec?:number}} [options] deterministic current epoch seconds + * @returns {{name:string,limits:{projects:number|null,members:number|null},priceKrw:number}} + * effective plan definition + */ +export function effectivePlanOf(db, org, { nowSec = Math.floor(Date.now() / 1000) } = {}) { + requireEffectivePlanAuthority(db, org, nowSec); + const stored = planOf(org); + if (stored === PLANS.pro) return PLANS.pro; + return hasCurrentStripeProClaim(db, org.id, nowSec) ? PLANS.pro : stored; +} + /** Return current project/member counts for one organization. */ export function orgUsage(db, orgId) { const projects = db.prepare('SELECT COUNT(*) AS n FROM projects WHERE org_id = ?').get(orgId).n; @@ -27,9 +77,9 @@ export function orgUsage(db, orgId) { return { projects, members }; } -/** Return whether adding one resource would exceed the organization's plan limit. */ -export function wouldExceed(db, org, kind) { - const limit = planOf(org).limits[kind]; +/** Return whether adding one resource would exceed the organization's effective plan limit. */ +export function wouldExceed(db, org, kind, options = {}) { + const limit = effectivePlanOf(db, org, options).limits[kind]; if (limit == null) return false; // unlimited return orgUsage(db, org.id)[kind] >= limit; } From 4e2bff0136a8221a6a934b4b70add5a1876cbe51 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 10:21:37 -0700 Subject: [PATCH 3/8] test(billing): require effective plan truth for planOf consumers --- tests/unit/billing-effective-plan.test.mjs | 27 +++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/unit/billing-effective-plan.test.mjs b/tests/unit/billing-effective-plan.test.mjs index aa5dd6d1..5e88cb36 100644 --- a/tests/unit/billing-effective-plan.test.mjs +++ b/tests/unit/billing-effective-plan.test.mjs @@ -2,7 +2,13 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { DatabaseSync } from 'node:sqlite'; -import { PLANS, effectivePlanOf, wouldExceed } from '../../server/billing.mjs'; +import { + PLANS, + configureBillingEntitlementDatabase, + effectivePlanOf, + planOf, + wouldExceed, +} from '../../server/billing.mjs'; function database({ withClaims = true } = {}) { const db = new DatabaseSync(':memory:'); @@ -102,8 +108,27 @@ test('missing or unreadable claim tables fail closed to the stored plan rather t db.close(); }); +test('configured claim database makes existing planOf consumers report the same reversible effective plan', () => { + const db = database(); + db.exec(` + INSERT INTO billing_stripe_entitlement_decisions VALUES(1,'sub_42',1,4102444800); + INSERT INTO billing_stripe_entitlement_claim_heads VALUES('sub_42',1); + `); + configureBillingEntitlementDatabase(db); + assert.equal(planOf(freeOrg()), PLANS.pro); + assert.equal(planOf({ id: 42, plan: 'pro' }), PLANS.pro); + assert.equal(planOf({ plan: 'free' }), PLANS.free); + db.exec(` + INSERT INTO billing_stripe_entitlement_decisions VALUES(2,'sub_42',0,NULL); + UPDATE billing_stripe_entitlement_claim_heads SET decision_id=2 WHERE subscription_id='sub_42'; + `); + assert.equal(planOf(freeOrg()), PLANS.free); + db.close(); +}); + test('effective-plan authority and clock inputs are bounded before entitlement lookup', () => { const db = database(); + assert.throws(() => configureBillingEntitlementDatabase(null), TypeError); for (const org of [null, {}, { id: 0, plan: 'free' }, { id: '42', plan: 'free' }]) { assert.throws(() => effectivePlanOf(db, org, { nowSec: 1000 }), TypeError); } From 73b24eb527c7deeb9f527d1967860bfb095017d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 10:24:53 -0700 Subject: [PATCH 4/8] feat(billing): make planOf reflect current durable claim authority --- server/billing.mjs | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/server/billing.mjs b/server/billing.mjs index acc9d3ac..888b6272 100644 --- a/server/billing.mjs +++ b/server/billing.mjs @@ -15,11 +15,46 @@ export const PLANS = { pro: { name: 'Pro', limits: { projects: null, members: null }, priceKrw: 19000 }, // null = unlimited }; -/** Return the stored/manual plan definition for an organization-like record. */ -export function planOf(org) { +let billingEntitlementDatabase = null; + +function storedPlanOf(org) { return PLANS[org?.plan] || PLANS.free; } +/** + * Bind the bootstrapped billing database used by synchronous plan-reporting + * consumers. The binding is server-owned and never accepts caller-selected + * claim or observation identities. + * + * @param {object} database SQLite-like database with prepare/get support + * @returns {void} + */ +export function configureBillingEntitlementDatabase(database) { + if (!database || typeof database.prepare !== 'function') { + throw new TypeError('database must provide SQLite prepare operations'); + } + billingEntitlementDatabase = database; +} + +/** + * Return the plan currently visible to existing synchronous plan consumers. + * + * Explicit stored Pro remains a manual/non-Stripe override. For a stored Free + * organization with a valid ID, a bootstrap-configured claim database can + * upgrade the returned plan only while current Stripe claim evidence is both + * entitled and unexpired. Claim storage failure or malformed legacy caller data + * fails closed to the stored plan. + */ +export function planOf(org) { + const stored = storedPlanOf(org); + if (stored === PLANS.pro || billingEntitlementDatabase == null) return stored; + try { + return effectivePlanOf(billingEntitlementDatabase, org); + } catch { + return stored; + } +} + function requireEffectivePlanAuthority(db, org, nowSec) { if (!db || typeof db.prepare !== 'function') throw new TypeError('db must provide SQLite prepare operations'); if (!org || !Number.isSafeInteger(org.id) || org.id <= 0) throw new TypeError('org.id must be a positive safe integer'); @@ -65,7 +100,7 @@ function hasCurrentStripeProClaim(db, organizationId, nowSec) { */ export function effectivePlanOf(db, org, { nowSec = Math.floor(Date.now() / 1000) } = {}) { requireEffectivePlanAuthority(db, org, nowSec); - const stored = planOf(org); + const stored = storedPlanOf(org); if (stored === PLANS.pro) return PLANS.pro; return hasCurrentStripeProClaim(db, org.id, nowSec) ? PLANS.pro : stored; } From f7e55cdf2bda36b49f37725dcc24199433035a96 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 23:58:03 -0700 Subject: [PATCH 5/8] test(billing): reproduce missing effective-plan bootstrap binding --- .../billing-effective-plan-bootstrap.test.mjs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/unit/billing-effective-plan-bootstrap.test.mjs diff --git a/tests/unit/billing-effective-plan-bootstrap.test.mjs b/tests/unit/billing-effective-plan-bootstrap.test.mjs new file mode 100644 index 00000000..becce082 --- /dev/null +++ b/tests/unit/billing-effective-plan-bootstrap.test.mjs @@ -0,0 +1,66 @@ +import assert from 'node:assert/strict'; + +process.env.SCOPEWEAVE_DB = ':memory:'; +process.env.SCOPEWEAVE_DEV = '1'; +process.env.SCOPEWEAVE_JWT_SECRET = '0123456789abcdef0123456789abcdef'; +delete process.env.ORCHESTRATOR_URL; + +const { db } = await import('../../server/db.mjs'); +const { PLANS, planOf } = await import('../../server/billing.mjs'); + +// This exercises the real production bootstrap boundary rather than manually +// calling configureBillingEntitlementDatabase from the test. A claim-backed plan +// is not usable by legacy planOf consumers unless server/db.mjs binds the +// bootstrapped database into billing.mjs itself. +db.prepare('INSERT INTO users(id,email,password_hash,name) VALUES(?,?,?,?)') + .run(9001, 'bootstrap-plan@example.test', 'hash', 'Bootstrap Plan'); +db.prepare('INSERT INTO orgs(id,name,owner_id,plan) VALUES(?,?,?,?)') + .run(9101, 'Bootstrap Org', 9001, 'free'); + +db.prepare(` + INSERT INTO billing_stripe_customers(customer_id, organization_id, first_observed_at_ms) + VALUES(?,?,?) +`).run('cus_bootstrap', 9101, 1); +db.prepare(` + INSERT INTO billing_stripe_subscriptions(subscription_id, customer_id, first_observed_at_ms) + VALUES(?,?,?) +`).run('sub_bootstrap', 'cus_bootstrap', 1); + +const nowSec = Math.floor(Date.now() / 1000); +const validUntilSec = nowSec + 3600; +const observationId = Number(db.prepare(` + INSERT INTO billing_stripe_subscription_observations( + subscription_id, source_event_id, observed_at_ms, subscription_status, + cancel_at_period_end, current_period_start_sec, current_period_end_sec, + canceled_at_sec, ended_at_sec, trial_end_sec, latest_invoice_id + ) VALUES(?,?,?,?,?,?,?,?,?,?,?) +`).run( + 'sub_bootstrap', null, 1, 'active', 0, nowSec - 60, validUntilSec, + null, null, null, null, +).lastInsertRowid); + +const decisionId = Number(db.prepare(` + INSERT INTO billing_stripe_entitlement_decisions( + subscription_id, evaluated_subscription_observation_id, evaluated_invoice_observation_id, + previous_decision_id, decision_action, decision_reason, entitled, valid_until_sec, + claim_subscription_observation_id, claim_invoice_observation_id, + evaluated_at_sec, recorded_at_ms + ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?) +`).run( + 'sub_bootstrap', observationId, null, null, 'grant', 'bootstrap-binding-regression', 1, + validUntilSec, observationId, null, nowSec, Date.now(), +).lastInsertRowid); +db.prepare(` + INSERT INTO billing_stripe_entitlement_claim_heads(subscription_id, decision_id) + VALUES(?,?) +`).run('sub_bootstrap', decisionId); + +const organization = db.prepare('SELECT id, plan FROM orgs WHERE id = ?').get(9101); +assert.equal( + planOf(organization), + PLANS.pro, + 'production database bootstrap must bind current claim evidence into planOf consumers', +); + +db.close(); +console.log('✓ billing effective-plan bootstrap binding passed'); From b3f7481a8bb8c424dede67e8334f43bc30b637dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 23:59:04 -0700 Subject: [PATCH 6/8] test(billing): register bootstrap binding regression --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c7c3cbc1..828ea047 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/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-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/toast-accessibility.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-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/toast-accessibility.test.mjs", "test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/application_routes.mjs --include=server/auth.mjs --include=server/billing.mjs --include=server/billing_checkout_attempt.mjs --include=server/billing_configuration.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --include=server/stripe_webhook.mjs --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 --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-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 && npm run test:api", + "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-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 && 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 cbee4c55b722278194d4d216ca34dfc9f5889b8f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:00:58 -0700 Subject: [PATCH 7/8] test(billing): bind regression to real application bootstrap --- tests/unit/billing-effective-plan-bootstrap.test.mjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/unit/billing-effective-plan-bootstrap.test.mjs b/tests/unit/billing-effective-plan-bootstrap.test.mjs index becce082..00fbc5df 100644 --- a/tests/unit/billing-effective-plan-bootstrap.test.mjs +++ b/tests/unit/billing-effective-plan-bootstrap.test.mjs @@ -5,13 +5,15 @@ process.env.SCOPEWEAVE_DEV = '1'; process.env.SCOPEWEAVE_JWT_SECRET = '0123456789abcdef0123456789abcdef'; delete process.env.ORCHESTRATOR_URL; +// Import the real public application bootstrap first. It is responsible for +// composing the shared database and billing authority before requests can use +// the legacy route graph. +await import('../../server/app.mjs'); const { db } = await import('../../server/db.mjs'); const { PLANS, planOf } = await import('../../server/billing.mjs'); -// This exercises the real production bootstrap boundary rather than manually -// calling configureBillingEntitlementDatabase from the test. A claim-backed plan -// is not usable by legacy planOf consumers unless server/db.mjs binds the -// bootstrapped database into billing.mjs itself. +// Do not manually call configureBillingEntitlementDatabase here: this contract +// proves production application bootstrap wires claim-backed plan authority. db.prepare('INSERT INTO users(id,email,password_hash,name) VALUES(?,?,?,?)') .run(9001, 'bootstrap-plan@example.test', 'hash', 'Bootstrap Plan'); db.prepare('INSERT INTO orgs(id,name,owner_id,plan) VALUES(?,?,?,?)') @@ -59,7 +61,7 @@ const organization = db.prepare('SELECT id, plan FROM orgs WHERE id = ?').get(91 assert.equal( planOf(organization), PLANS.pro, - 'production database bootstrap must bind current claim evidence into planOf consumers', + 'production application bootstrap must bind current claim evidence into planOf consumers', ); db.close(); From 30d28a54543e05e9c54ca6db96fdfacef32a6885 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:01:52 -0700 Subject: [PATCH 8/8] fix(billing): bind effective plan authority at app bootstrap --- server/app.mjs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/app.mjs b/server/app.mjs index 3492d48f..95d1598b 100644 --- a/server/app.mjs +++ b/server/app.mjs @@ -1,9 +1,18 @@ import { Hono } from 'hono'; import { readFile } from 'node:fs/promises'; import { app as applicationRoutes } from './application_routes.mjs'; +import { configureBillingEntitlementDatabase } from './billing.mjs'; +import { db } from './db.mjs'; const toastStylesheetUrl = new URL('../toast-state.css', import.meta.url); +// Bind the already-bootstrapped server-owned database before the public route +// graph can serve any request that reports plan authority. This keeps legacy +// synchronous planOf consumers aligned with the same tenant-scoped current +// entitlement claims used by resource-limit authorization, without mutating +// orgs.plan or accepting caller-selected claim identities. +configureBillingEntitlementDatabase(db); + /** * ScopeWeave's public HTTP application entry point. *