@volter/twin-stripe 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,11 +6,13 @@
6
6
  // regression. Grow this toward Stripe's *full* surface every cycle — a missing entry is a
7
7
  // hidden gap, and a high % against a thin list is a misleading metric (the bug this fixes).
8
8
  import { mkdtempSync, rmSync } from 'node:fs';
9
+ import { emitTwinEvent } from '@volter/twin';
10
+ import { stripeEmitter } from './stripe-emit.ts';
9
11
  import { tmpdir } from 'node:os';
10
12
  import { join } from 'node:path';
11
13
  import { createElement } from 'react';
12
14
  import { renderToStaticMarkup } from 'react-dom/server';
13
- import { checkCapabilities, type CapabilityReport, type CapabilitySpec } from '@volter/twin-tooling';
15
+ import { checkCapabilities, type CapabilityReport, type CapabilitySpec, verifyBoundary, isInfrastructureError, harnessError } from '@volter/twin-tooling';
14
16
  import { ListPane, SECTIONS, PaymentErrorBanner, ConnectAccountPanel, BalanceSummary, type Section } from '../client/stripe-mirror.tsx';
15
17
  import type { StripeRow } from './stripe-mirror-ui.ts';
16
18
  import {
@@ -58,7 +60,8 @@ function uiDataCoupled(opts: {
58
60
  const get: ServerFetch = (path) => fetch(`${base}${path}`).then((r) => r.json() as Promise<Body>);
59
61
  const post = (path: string, body?: string) => fetch(`${base}${path}`, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: body ?? '' }).then((r) => r.json() as Promise<Body>);
60
62
  return await opts.check({ get, post });
61
- } catch {
63
+ } catch (err) {
64
+ if (isInfrastructureError(err)) throw harnessError('stripe.uiDataCoupled', err);
62
65
  return false;
63
66
  } finally {
64
67
  server.stop();
@@ -96,9 +99,7 @@ async function withRoot(steps: (h: (s: Step) => Promise<StripeResponse>) => Prom
96
99
  const root = mkdtempSync(join(tmpdir(), 'stp-cap-'));
97
100
  const h = (s: Step) => handleStripeTwinRequest({ method: s.m, path: s.p, body: s.b, root });
98
101
  try {
99
- return await steps(h);
100
- } catch {
101
- return false;
102
+ return await verifyBoundary('stripe.withRoot', () => steps(h));
102
103
  } finally {
103
104
  rmSync(root, { recursive: true, force: true });
104
105
  }
@@ -704,6 +705,15 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
704
705
  if (!ok(cus) || !ok(pm) || field(pm, 'type') !== 'card' || field(pm, 'customer') !== null) return false;
705
706
  const card = (pm.body as Body).card as Body | undefined;
706
707
  if (!card || card.last4 !== '4242') return false;
708
+ // last4 FIDELITY (not just presence): a raw test PAN must surface its TRUE last4 —
709
+ // the '4242' check above can't prove that, since '4242' equals the synthesized
710
+ // fallback. parseStripeForm coerces an all-digit card[number] to a JS number (like
711
+ // any numeric-looking form field), so cover BOTH wire shapes: the numeric-coerced
712
+ // bare PAN and a string PAN (spaces keep it a string through the form parser).
713
+ const pmNum = await h({ m: 'POST', p: '/v1/payment_methods', b: 'type=card&card[number]=5555555555554444&card[exp_month]=4&card[exp_year]=2030&card[cvc]=314' });
714
+ if (!ok(pmNum) || ((pmNum.body as Body).card as Body)?.last4 !== '4444') return false;
715
+ const pmStr = await h({ m: 'POST', p: '/v1/payment_methods', b: 'type=card&card[number]=5555+5555+5555+4444&card[exp_month]=4&card[exp_year]=2030&card[cvc]=314' });
716
+ if (!ok(pmStr) || ((pmStr.body as Body).card as Body)?.last4 !== '4444') return false;
707
717
  const att = await h({ m: 'POST', p: `/v1/payment_methods/${id(pm)}/attach`, b: `customer=${id(cus)}` });
708
718
  if (!ok(att) || field(att, 'customer') !== id(cus)) return false;
709
719
  const g = await h({ m: 'GET', p: `/v1/payment_methods/${id(pm)}` });
@@ -761,7 +771,8 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
761
771
  // DELETE expires it early (retrievable shape; expires bumped).
762
772
  const del = await hv({ m: 'DELETE', p: `/v1/ephemeral_keys/${id(key)}` });
763
773
  return badCust.status === 400 && noScope.status === 400 && ok(del);
764
- } catch {
774
+ } catch (err) {
775
+ if (isInfrastructureError(err)) throw harnessError('stripe.ephemeral_keys', err);
765
776
  return false;
766
777
  } finally {
767
778
  rmSync(root, { recursive: true, force: true });
@@ -905,9 +916,33 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
905
916
  const inv = await h({ m: 'POST', p: '/v1/invoices', b: `customer=${id(cust)}` });
906
917
  if (!ok(inv) || field(inv, 'object') !== 'invoice') return false;
907
918
  const fin = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/finalize` });
908
- const pay = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/pay` });
919
+ const pay = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/pay`, b: 'payment_method=pm_card_visa' });
909
920
  const l = await h({ m: 'GET', p: '/v1/invoices' });
910
- return ok(fin) && field(fin, 'status') === 'open' && ok(pay) && field(pay, 'status') === 'paid' && field(l, 'object') === 'list';
921
+ if (!(ok(fin) && field(fin, 'status') === 'open' && ok(pay) && field(pay, 'status') === 'paid' && field(l, 'object') === 'list')) return false;
922
+ // Card-decline fidelity on the pay edge (the off-session / saved-card retry shape):
923
+ // a card created from a DECLINING test PAN, attached, then referenced ONLY by its
924
+ // PaymentMethod id (never the raw PAN again) must 402 card_error and leave the
925
+ // invoice OPEN — decline state must survive past the creating request. A different
926
+ // NON-declining saved card on a fresh invoice must still pay to 'paid', proving the
927
+ // decline is per-card, not a blanket pay failure.
928
+ const bad = await h({ m: 'POST', p: '/v1/payment_methods', b: 'type=card&card[number]=4000000000000002&card[exp_month]=4&card[exp_year]=2030&card[cvc]=314' });
929
+ const badAtt = await h({ m: 'POST', p: `/v1/payment_methods/${id(bad)}/attach`, b: `customer=${id(cust)}` });
930
+ if (!ok(bad) || !ok(badAtt)) return false;
931
+ await h({ m: 'POST', p: '/v1/invoiceitems', b: `customer=${id(cust)}&amount=1800&currency=usd` });
932
+ const inv2 = await h({ m: 'POST', p: '/v1/invoices', b: `customer=${id(cust)}` });
933
+ await h({ m: 'POST', p: `/v1/invoices/${id(inv2)}/finalize` });
934
+ const payBad = await h({ m: 'POST', p: `/v1/invoices/${id(inv2)}/pay`, b: `payment_method=${id(bad)}` });
935
+ if (payBad.status !== 402 || ((payBad.body as Body).error as Body)?.type !== 'card_error') return false;
936
+ const after = await h({ m: 'GET', p: `/v1/invoices/${id(inv2)}` });
937
+ if (!ok(after) || field(after, 'status') !== 'open') return false;
938
+ const good = await h({ m: 'POST', p: '/v1/payment_methods', b: 'type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2030&card[cvc]=314' });
939
+ const goodAtt = await h({ m: 'POST', p: `/v1/payment_methods/${id(good)}/attach`, b: `customer=${id(cust)}` });
940
+ if (!ok(good) || !ok(goodAtt)) return false;
941
+ await h({ m: 'POST', p: '/v1/invoiceitems', b: `customer=${id(cust)}&amount=1900&currency=usd` });
942
+ const inv3 = await h({ m: 'POST', p: '/v1/invoices', b: `customer=${id(cust)}` });
943
+ await h({ m: 'POST', p: `/v1/invoices/${id(inv3)}/finalize` });
944
+ const payGood = await h({ m: 'POST', p: `/v1/invoices/${id(inv3)}/pay`, b: `payment_method=${id(good)}` });
945
+ return ok(payGood) && field(payGood, 'status') === 'paid';
911
946
  }),
912
947
  ),
913
948
  done('stripe.invoiceitems.crud', 'invoices', 'InvoiceItems: create + list', 'api', 'core', () =>
@@ -971,7 +1006,7 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
971
1006
  await h({ m: 'POST', p: '/v1/invoiceitems', b: `customer=${id(cust)}&amount=5000&currency=usd` });
972
1007
  const inv = await h({ m: 'POST', p: '/v1/invoices', b: `customer=${id(cust)}` });
973
1008
  await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/finalize` });
974
- await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/pay` });
1009
+ await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/pay`, b: 'payment_method=pm_card_visa' });
975
1010
  // preview computes the object WITHOUT persisting it
976
1011
  const prev = await h({ m: 'GET', p: `/v1/credit_notes/preview?invoice=${id(inv)}&amount=2000` });
977
1012
  if (!ok(prev) || field(prev, 'object') !== 'credit_note' || field(prev, 'amount') !== 2000) return false;
@@ -1005,6 +1040,37 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
1005
1040
  return ok(g) && ok(li) && field(li, 'object') === 'list' && ok(exp);
1006
1041
  }),
1007
1042
  ),
1043
+ // Completion copies the session's create-only subscription_data onto the created
1044
+ // subscription (metadata is how apps bind the checkout attempt → subscription; trial
1045
+ // becomes a real trialing window) — and the Session itself never echoes the param.
1046
+ done('stripe.checkout.subscription_data', 'checkout', 'Checkout completion copies subscription_data (metadata + trial) onto the created subscription', 'api', 'core', () =>
1047
+ withRoot(async (h) => {
1048
+ const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=subdata@twin.test' });
1049
+ const cs = await h({ m: 'POST', p: '/v1/checkout/sessions', b: `mode=subscription&success_url=https://x.test&customer=${id(cust)}&line_items[0][price_data][currency]=usd&line_items[0][price_data][unit_amount]=999&line_items[0][quantity]=1&subscription_data[metadata][attemptId]=att_cap_1&subscription_data[trial_period_days]=7` });
1050
+ if (!ok(cs) || field(cs, 'subscription_data') !== undefined || field(cs, '_subscription_data') !== undefined) return false;
1051
+ const completed = await h({ m: 'POST', p: `/v1/checkout/sessions/${id(cs)}`, b: 'status=complete' });
1052
+ if (!ok(completed) || !field(completed, 'subscription')) return false;
1053
+ const sub = await h({ m: 'GET', p: `/v1/subscriptions/${field(completed, 'subscription')}` });
1054
+ const md = field(sub, 'metadata') as Body | undefined;
1055
+ return ok(sub) && md?.attemptId === 'att_cap_1' && field(sub, 'status') === 'trialing'
1056
+ && typeof field(sub, 'trial_end') === 'number' && field(sub, 'trial_end') === field(sub, 'current_period_end');
1057
+ }),
1058
+ ),
1059
+ // Completing a session fires checkout.session.completed (data.object = the completed
1060
+ // Session) and stores it in the Events API — the event most billing flows fulfill from.
1061
+ done('stripe.checkout.completed_event', 'checkout', 'Checkout completion emits checkout.session.completed with the session payload', 'api', 'core', () =>
1062
+ withRoot(async (h) => {
1063
+ const cs = await h({ m: 'POST', p: '/v1/checkout/sessions', b: 'mode=payment&success_url=https://x.test&line_items[0][price_data][currency]=usd&line_items[0][price_data][unit_amount]=2500&line_items[0][quantity]=1' });
1064
+ if (!ok(cs)) return false;
1065
+ const completed = await h({ m: 'POST', p: `/v1/checkout/sessions/${id(cs)}`, b: 'status=complete' });
1066
+ if (!ok(completed) || field(completed, 'status') !== 'complete') return false;
1067
+ const ev = await h({ m: 'GET', p: '/v1/events?type=checkout.session.completed' });
1068
+ const rows = ((ev.body as Body)?.data ?? []) as Body[];
1069
+ const payload = (rows[0]?.data as Body | undefined)?.object as Body | undefined;
1070
+ return ok(ev) && rows.length === 1 && payload?.object === 'checkout.session'
1071
+ && payload?.id === id(cs) && payload?.status === 'complete' && payload?.payment_status === 'paid';
1072
+ }),
1073
+ ),
1008
1074
  done('stripe.billing_portal.session', 'checkout', 'Billing Portal Session create (referential to customer)', 'api', 'common', () =>
1009
1075
  withRoot(async (h) => {
1010
1076
  const cust = await h({ m: 'POST', p: '/v1/customers', b: 'email=portal@twin.test' });
@@ -1120,7 +1186,8 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
1120
1186
  if (platData.length !== 1 || platData[0]!.id !== id(plat)) return false;
1121
1187
  const badAcct = await h({ m: 'POST', p: '/v1/payouts', b: 'amount=500&currency=usd', acct: 'acct_nope' });
1122
1188
  return badAcct.status === 400;
1123
- } catch {
1189
+ } catch (err) {
1190
+ if (isInfrastructureError(err)) throw harnessError('stripe.connect.connect_payouts', err);
1124
1191
  return false;
1125
1192
  } finally {
1126
1193
  rmSync(root, { recursive: true, force: true });
@@ -1195,6 +1262,36 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
1195
1262
  return ok(g) && id(g) === id(e) && field(l, 'object') === 'list';
1196
1263
  }),
1197
1264
  ),
1265
+ done('stripe.events.emit_verb', 'events', 'DELIVER verb (world-stripe emit): synthesizes the event envelope from twin state and signs it with the destination endpoint\'s own whsec; unknown subject + missing endpoint fail loudly', 'api', 'common', async () => {
1266
+ const root = mkdtempSync(join(tmpdir(), 'stp-emit-cap-'));
1267
+ const H = (m: string, p: string, b?: string) => handleStripeTwinRequest({ method: m, path: p, body: b, root, occurredAt: '2026-08-01T00:00:00Z' });
1268
+ try {
1269
+ return await verifyBoundary('stripe.events.emit_verb', async () => {
1270
+ // no endpoint registered → LOUD, not a silent no-op
1271
+ const noEndpoint = await emitTwinEvent(stripeEmitter, { type: 'invoice.paid', subjectId: 'in_twin_1', root }).then(() => false, (e: unknown) => /no stripe webhook endpoint/.test(String(e)));
1272
+ if (!noEndpoint) return false;
1273
+ const cust = await H('POST', '/v1/customers', 'email=e@x.co');
1274
+ await H('POST', '/v1/invoiceitems', `customer=${id(cust)}&amount=700&currency=usd`);
1275
+ const inv = await H('POST', '/v1/invoices', `customer=${id(cust)}`);
1276
+ const we = await H('POST', '/v1/webhook_endpoints', `url=${encodeURIComponent('http://127.0.0.1:1/hook')}&enabled_events[0]=invoice.*`);
1277
+ const secret = field(we, 'secret') as string;
1278
+ // delivery seam injected — offline + deterministic; the signature is still REAL
1279
+ const posts: Array<{ headers: Record<string, string>; body: string }> = [];
1280
+ const report = await emitTwinEvent(stripeEmitter, {
1281
+ type: 'invoice.paid', subjectId: id(inv), root,
1282
+ fetchFn: async (_url, init) => { posts.push({ headers: init.headers, body: init.body }); return { status: 200 }; },
1283
+ });
1284
+ if (!report.ok || posts.length !== 1) return false;
1285
+ // verified with the same scheme stripe.webhooks.constructEvent implements
1286
+ const evt = constructEvent(posts[0]!.body, posts[0]!.headers['stripe-signature']!, secret);
1287
+ if (evt.type !== 'invoice.paid' || (evt.data.object as Body).id !== id(inv)) return false;
1288
+ // unknown subject → LOUD, listing what exists
1289
+ return await emitTwinEvent(stripeEmitter, { type: 'invoice.paid', subjectId: 'in_twin_999', root }).then(() => false, (e: unknown) => /no invoice "in_twin_999"/.test(String(e)));
1290
+ });
1291
+ } finally {
1292
+ rmSync(root, { recursive: true, force: true });
1293
+ }
1294
+ }),
1198
1295
  done('stripe.idempotency', 'core', 'Idempotency-Key: POST replay returns the same resource', 'api', 'core', async () => {
1199
1296
  const root = mkdtempSync(join(tmpdir(), 'stp-cap-'));
1200
1297
  const H = (idk: string) => handleStripeTwinRequest({ method: 'POST', path: '/v1/customers', body: 'email=idem@twin.test', root, idempotencyKey: idk });
@@ -1244,7 +1341,8 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
1244
1341
  const c = await H({ m: 'POST', p: `/v1/payment_intents/${id(pi)}/confirm`, b: 'payment_method=pm_card_visa' });
1245
1342
  if (!ok(c) || field(c, 'status') !== 'succeeded') return false;
1246
1343
  return hits.some((e) => e.type === 'payment_intent.succeeded' && (e.data.object as Body)?.status === 'succeeded');
1247
- } catch {
1344
+ } catch (err) {
1345
+ if (isInfrastructureError(err)) throw harnessError('stripe.webhooks.emit', err);
1248
1346
  return false;
1249
1347
  } finally {
1250
1348
  setStripeEventDelivery(null);
@@ -1280,7 +1378,8 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
1280
1378
  // stale timestamp beyond tolerance → throws
1281
1379
  let rejStale = false; try { constructEvent(payload, header, secret, { tolerance: 300, now: ts + 1000 }); } catch { rejStale = true; }
1282
1380
  return rejWrongSecret && rejTampered && rejMalformed && rejStale;
1283
- } catch {
1381
+ } catch (err) {
1382
+ if (isInfrastructureError(err)) throw harnessError('stripe.webhooks.signature', err);
1284
1383
  return false;
1285
1384
  }
1286
1385
  }),
@@ -1417,7 +1516,7 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
1417
1516
  const ev2 = (((await handleStripeTwinRequest({ method: 'GET', path: '/v1/events', root })).body as Body).data as Body[])
1418
1517
  .find((e) => e.type === 'payment_intent.created' && ((e.data as Body)?.object as Body)?.id === id(pi2));
1419
1518
  return ev2 ? ev2.api_version === '2024-06-20' : false;
1420
- } catch { return false; } finally { rmSync(root, { recursive: true, force: true }); }
1519
+ } catch (err) { if (isInfrastructureError(err)) throw harnessError('stripe.api.versioning', err); return false; } finally { rmSync(root, { recursive: true, force: true }); }
1421
1520
  }),
1422
1521
 
1423
1522
  // ── Test helpers ──────────────────────────────────────────────────────────────────
@@ -1456,7 +1555,7 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
1456
1555
  const ch = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=Jane&type=individual&billing[address][line1]=1 Main&billing[address][city]=SF&billing[address][country]=US&billing[address][postal_code]=94105&billing[address][state]=CA' });
1457
1556
  const card = await h({ m: 'POST', p: `/v1/issuing/cards`, b: `cardholder=${id(ch)}&currency=usd&type=virtual` });
1458
1557
  const auth = await h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: `card=${id(card)}&amount=2500` });
1459
- if (!ok(auth) || field(auth, 'status') !== 'pending' || field(auth, 'card') !== id(card) || field(auth, 'cardholder') !== id(ch)) return false;
1558
+ if (!ok(auth) || field(auth, 'status') !== 'pending' || (field(auth, 'card') as Body)?.id !== id(card) || field(auth, 'cardholder') !== id(ch)) return false;
1460
1559
  const forced = await h({ m: 'POST', p: '/v1/test_helpers/issuing/transactions/create_force_capture', b: `card=${id(card)}&amount=1000` });
1461
1560
  if (!ok(forced) || field(forced, 'type') !== 'capture' || field(forced, 'amount') !== -1000) return false;
1462
1561
  const noAmt = await h({ m: 'POST', p: '/v1/test_helpers/issuing/fund_balance', b: 'currency=usd' });
@@ -1618,6 +1717,225 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
1618
1717
  return twice.status === 400 && noReason.status === 400 && badTxn.status === 400;
1619
1718
  }),
1620
1719
  ),
1720
+ // Real-time authorization — the SYNCHRONOUS issuing_authorization.request leg: when a
1721
+ // registered webhook endpoint subscribes to it, presenting an authorization delivers the
1722
+ // request event synchronously (data.object carries a populated pending_request) and the
1723
+ // endpoint's JSON response ({approved: bool}) decides the authorization; the decision
1724
+ // lands in request_history with reason webhook_approved/webhook_declined. The request
1725
+ // event is also stored in GET /v1/events. An endpoint enrolled for OTHER events is never
1726
+ // consulted (the presentment stays pending awaiting the approve/decline API). Offline via
1727
+ // the injected deliverer.
1728
+ done('stripe.issuing.realtime_auth_webhook', 'issuing', 'Real-time authorization: synchronous issuing_authorization.request webhook decides the presentment', 'api', 'niche', async () => {
1729
+ const root = mkdtempSync(join(tmpdir(), 'stp-cap-'));
1730
+ try {
1731
+ const h = (s: Step) => handleStripeTwinRequest({ method: s.m, path: s.p, body: s.b, root, occurredAt: '2026-06-14T00:00:00Z' });
1732
+ const delivered: StripeEvent[] = [];
1733
+ setStripeEventDelivery((_url, event) => {
1734
+ delivered.push(event);
1735
+ if (event.type === 'issuing_authorization.request') return { status: 200, body: '{"approved":true}' };
1736
+ });
1737
+ const ch = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=RT&type=individual&billing[address][country]=US' });
1738
+ const card = await h({ m: 'POST', p: '/v1/issuing/cards', b: `cardholder=${id(ch)}&currency=usd&type=virtual` });
1739
+ // Family enrollment: the synchronous .request leg matches via the family form, and the
1740
+ // organic .created asserted below reaches this endpoint only because the family covers
1741
+ // it — the fan-out filters by enabled_events, so an exact .request enrollment would
1742
+ // (correctly, like the vendor) receive nothing but .request.
1743
+ await h({ m: 'POST', p: '/v1/webhook_endpoints', b: `url=${encodeURIComponent('https://rt.twin.test/auth')}&enabled_events[]=issuing_authorization.*` });
1744
+ const auth = await h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: `card=${id(card)}&amount=2500` });
1745
+ if (!ok(auth) || field(auth, 'approved') !== true || field(auth, 'status') !== 'pending' || field(auth, 'pending_request') !== null) return false;
1746
+ const history = field(auth, 'request_history') as Body[];
1747
+ if (!Array.isArray(history) || history.length !== 1 || history[0]!.reason !== 'webhook_approved' || history[0]!.approved !== true) return false;
1748
+ // the endpoint saw exactly one synchronous request event, pending_request populated
1749
+ const req = delivered.filter((e) => e.type === 'issuing_authorization.request');
1750
+ if (req.length !== 1) return false;
1751
+ const reqObj = req[0]!.data.object as Body;
1752
+ if ((reqObj.pending_request as Body)?.amount !== 2500 || reqObj.status !== 'pending') return false;
1753
+ // the organic issuing lifecycle events flow too (created for the card + authorization)
1754
+ if (!delivered.some((e) => e.type === 'issuing_authorization.created')) return false;
1755
+ // stored Events API carries the request event
1756
+ const events = await h({ m: 'GET', p: '/v1/events?limit=100' });
1757
+ if (!((events.body as Body).data as Body[]).some((e) => e.type === 'issuing_authorization.request')) return false;
1758
+ // an endpoint enrolled for OTHER events is never consulted → presentment stays pending
1759
+ const root2 = mkdtempSync(join(tmpdir(), 'stp-cap-'));
1760
+ try {
1761
+ const h2 = (s: Step) => handleStripeTwinRequest({ method: s.m, path: s.p, body: s.b, root: root2, occurredAt: '2026-06-14T00:00:00Z' });
1762
+ let consulted = false;
1763
+ setStripeEventDelivery((_url, event) => {
1764
+ if (event.type === 'issuing_authorization.request') consulted = true;
1765
+ return { status: 200, body: '{"approved":false}' };
1766
+ });
1767
+ const ch2 = await h2({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=RT2&type=individual&billing[address][country]=US' });
1768
+ const card2 = await h2({ m: 'POST', p: '/v1/issuing/cards', b: `cardholder=${id(ch2)}&currency=usd&type=virtual` });
1769
+ await h2({ m: 'POST', p: '/v1/webhook_endpoints', b: `url=${encodeURIComponent('https://other.twin.test/hook')}&enabled_events[]=invoice.paid` });
1770
+ const auth2 = await h2({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: `card=${id(card2)}&amount=100` });
1771
+ return !consulted && field(auth2, 'status') === 'pending' && field(auth2, 'approved') === false;
1772
+ } finally {
1773
+ rmSync(root2, { recursive: true, force: true });
1774
+ }
1775
+ } catch (err) {
1776
+ if (isInfrastructureError(err)) throw harnessError('stripe.issuing.realtime_auth_webhook', err);
1777
+ return false;
1778
+ } finally {
1779
+ setStripeEventDelivery(null);
1780
+ clearStripeWebhooks();
1781
+ rmSync(root, { recursive: true, force: true });
1782
+ }
1783
+ }),
1784
+ // Real-time authorization fallbacks (the 2s-window posture, fail-closed): a webhook
1785
+ // decline closes the authorization (reason webhook_declined); a TIMEOUT declines with
1786
+ // reason webhook_timeout; an invalid response (non-2xx, non-JSON, missing `approved`)
1787
+ // declines with reason webhook_error + a reason_message; a partial `amount` in the
1788
+ // response is held ONLY when the presentment was is_amount_controllable.
1789
+ done('stripe.issuing.realtime_auth_fallbacks', 'issuing', 'Real-time authorization: decline honored; timeout → webhook_timeout; invalid response → webhook_error; partial amount only when controllable', 'api', 'niche', async () => {
1790
+ const root = mkdtempSync(join(tmpdir(), 'stp-cap-'));
1791
+ try {
1792
+ const h = (s: Step) => handleStripeTwinRequest({ method: s.m, path: s.p, body: s.b, root, occurredAt: '2026-06-14T00:00:00Z' });
1793
+ const ch = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=FB&type=individual&billing[address][country]=US' });
1794
+ const card = await h({ m: 'POST', p: '/v1/issuing/cards', b: `cardholder=${id(ch)}&currency=usd&type=virtual` });
1795
+ await h({ m: 'POST', p: '/v1/webhook_endpoints', b: `url=${encodeURIComponent('https://rt.twin.test/auth')}&enabled_events[]=issuing_authorization.request` });
1796
+ const present = (b: string) => h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b });
1797
+ const reasonOf = (r: StripeResponse) => ((field(r, 'request_history') as Body[])?.[0] ?? {}) as Body;
1798
+ const respond = (res: { status: number; body: string } | (() => never)) =>
1799
+ setStripeEventDelivery((_url, event) => {
1800
+ if (event.type !== 'issuing_authorization.request') return;
1801
+ if (typeof res === 'function') return res();
1802
+ return res;
1803
+ });
1804
+ // decline honored
1805
+ respond({ status: 200, body: '{"approved":false}' });
1806
+ const dec = await present(`card=${id(card)}&amount=900`);
1807
+ if (field(dec, 'approved') !== false || field(dec, 'status') !== 'closed' || reasonOf(dec).reason !== 'webhook_declined') return false;
1808
+ // timeout → webhook_timeout (the exact TimeoutError AbortSignal.timeout raises)
1809
+ respond(() => { const e = new Error('timed out'); e.name = 'TimeoutError'; throw e; });
1810
+ const to = await present(`card=${id(card)}&amount=900`);
1811
+ if (field(to, 'approved') !== false || reasonOf(to).reason !== 'webhook_timeout') return false;
1812
+ // invalid responses → webhook_error with a reason_message
1813
+ for (const bad of [{ status: 500, body: 'oops' }, { status: 200, body: 'not-json' }, { status: 200, body: '{"ok":true}' }]) {
1814
+ respond(bad);
1815
+ const er = await present(`card=${id(card)}&amount=900`);
1816
+ if (field(er, 'approved') !== false || reasonOf(er).reason !== 'webhook_error' || typeof reasonOf(er).reason_message !== 'string') return false;
1817
+ }
1818
+ // partial amount: honored when controllable, ignored when not
1819
+ respond({ status: 200, body: '{"approved":true,"amount":1200}' });
1820
+ const partial = await present(`card=${id(card)}&amount=5000&is_amount_controllable=true`);
1821
+ if (field(partial, 'amount') !== 1200 || field(partial, 'approved') !== true) return false;
1822
+ const fixed = await present(`card=${id(card)}&amount=5000`);
1823
+ return field(fixed, 'amount') === 5000 && field(fixed, 'approved') === true;
1824
+ } catch (err) {
1825
+ if (isInfrastructureError(err)) throw harnessError('stripe.issuing.realtime_auth_fallbacks', err);
1826
+ return false;
1827
+ } finally {
1828
+ setStripeEventDelivery(null);
1829
+ clearStripeWebhooks();
1830
+ rmSync(root, { recursive: true, force: true });
1831
+ }
1832
+ }),
1833
+ // spending_controls enforcement at present time: per_authorization and all_time limits,
1834
+ // blocked categories, and card-state declines all refuse the presentment with the
1835
+ // vendor's request_history reasons ('spending_controls', 'card_inactive'); an invalid
1836
+ // spending_controls dictionary 400s at write time (closed interval enum).
1837
+ done('stripe.issuing.spending_controls', 'issuing', 'spending_controls enforced at authorization time (limits, categories, card state) with vendor decline reasons', 'api', 'niche', async () => {
1838
+ const root = mkdtempSync(join(tmpdir(), 'stp-cap-'));
1839
+ try {
1840
+ const h = (s: Step) => handleStripeTwinRequest({ method: s.m, path: s.p, body: s.b, root, occurredAt: '2026-06-14T00:00:00Z' });
1841
+ setStripeEventDelivery((_url, event) => {
1842
+ if (event.type === 'issuing_authorization.request') return { status: 200, body: '{"approved":true}' };
1843
+ });
1844
+ const reason = (r: StripeResponse) => ((field(r, 'request_history') as Body[])?.[0] ?? {}) as Body;
1845
+ const ch = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=SC&type=individual&billing[address][country]=US' });
1846
+ // per_authorization
1847
+ const perAuth = await h({ m: 'POST', p: '/v1/issuing/cards', b: `cardholder=${id(ch)}&currency=usd&type=virtual&spending_controls[spending_limits][0][amount]=5000&spending_controls[spending_limits][0][interval]=per_authorization` });
1848
+ const over = await h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: `card=${id(perAuth)}&amount=5001` });
1849
+ if (field(over, 'approved') !== false || field(over, 'status') !== 'closed' || reason(over).reason !== 'spending_controls') return false;
1850
+ const under = await h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: `card=${id(perAuth)}&amount=5000` });
1851
+ if (field(under, 'status') !== 'pending') return false; // within the limit → not a controls decline
1852
+ // all_time accumulates approved authorizations (webhook approvals via the enrolled endpoint)
1853
+ await h({ m: 'POST', p: '/v1/webhook_endpoints', b: `url=${encodeURIComponent('https://rt.twin.test/auth')}&enabled_events[]=issuing_authorization.request` });
1854
+ const allTime = await h({ m: 'POST', p: '/v1/issuing/cards', b: `cardholder=${id(ch)}&currency=usd&type=virtual&spending_controls[spending_limits][0][amount]=10000&spending_controls[spending_limits][0][interval]=all_time` });
1855
+ const first = await h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: `card=${id(allTime)}&amount=3000` });
1856
+ if (field(first, 'approved') !== true) return false;
1857
+ const second = await h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: `card=${id(allTime)}&amount=8000` });
1858
+ if (field(second, 'approved') !== false || reason(second).reason !== 'spending_controls') return false;
1859
+ // blocked category
1860
+ const blocked = await h({ m: 'POST', p: '/v1/issuing/cards', b: `cardholder=${id(ch)}&currency=usd&type=virtual&spending_controls[blocked_categories][]=airlines_air_carriers` });
1861
+ const blockedHit = await h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: `card=${id(blocked)}&amount=100&merchant_data[category]=airlines_air_carriers` });
1862
+ if (field(blockedHit, 'approved') !== false || reason(blockedHit).reason !== 'spending_controls') return false;
1863
+ // card state: a physical (inactive) card declines card_inactive
1864
+ const phys = await h({ m: 'POST', p: '/v1/issuing/cards', b: `cardholder=${id(ch)}&currency=usd&type=physical` });
1865
+ const inactive = await h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: `card=${id(phys)}&amount=100` });
1866
+ if (reason(inactive).reason !== 'card_inactive') return false;
1867
+ // vendor 400: invalid interval enum on write
1868
+ const badInterval = await h({ m: 'POST', p: `/v1/issuing/cards/${id(perAuth)}`, b: 'spending_controls[spending_limits][0][amount]=100&spending_controls[spending_limits][0][interval]=fortnightly' });
1869
+ return badInterval.status === 400;
1870
+ } catch (err) {
1871
+ if (isInfrastructureError(err)) throw harnessError('stripe.issuing.spending_controls', err);
1872
+ return false;
1873
+ } finally {
1874
+ setStripeEventDelivery(null);
1875
+ clearStripeWebhooks();
1876
+ rmSync(root, { recursive: true, force: true });
1877
+ }
1878
+ }),
1879
+ // Capture debits the issuing balance: fund_balance accrues the float, a webhook-approved
1880
+ // authorization is captured via POST /v1/test_helpers/issuing/authorizations/:id/capture
1881
+ // (partial capture_amount honored, refused above the held amount / on a non-approved or
1882
+ // closed authorization), the materialized transaction is type 'capture' with a NEGATIVE
1883
+ // amount linked to a balance_transaction {type: 'issuing_transaction', balance_type:
1884
+ // 'issuing'}, and GET /v1/balance serves the reduced issuing.available. force_capture
1885
+ // debits too.
1886
+ done('stripe.issuing.capture_debits_balance', 'issuing', 'Capture (test-helper + force_capture + API approve) debits the issuing balance; GET /v1/balance serves the issuing section', 'api', 'niche', async () => {
1887
+ const root = mkdtempSync(join(tmpdir(), 'stp-cap-'));
1888
+ try {
1889
+ const h = (s: Step) => handleStripeTwinRequest({ method: s.m, path: s.p, body: s.b, root, occurredAt: '2026-06-14T00:00:00Z' });
1890
+ setStripeEventDelivery((_url, event) => {
1891
+ if (event.type === 'issuing_authorization.request') return { status: 200, body: '{"approved":true}' };
1892
+ });
1893
+ const issuingAvailable = async () => {
1894
+ const bal = await h({ m: 'GET', p: '/v1/balance' });
1895
+ return (((field(bal, 'issuing') as Body)?.available as Body[])?.[0] ?? {}).amount;
1896
+ };
1897
+ const ch = await h({ m: 'POST', p: '/v1/issuing/cardholders', b: 'name=Cap&type=individual&billing[address][country]=US' });
1898
+ const card = await h({ m: 'POST', p: '/v1/issuing/cards', b: `cardholder=${id(ch)}&currency=usd&type=virtual` });
1899
+ await h({ m: 'POST', p: '/v1/webhook_endpoints', b: `url=${encodeURIComponent('https://rt.twin.test/auth')}&enabled_events[]=issuing_authorization.request` });
1900
+ const fund = await h({ m: 'POST', p: '/v1/test_helpers/issuing/fund_balance', b: 'amount=100000&currency=usd' });
1901
+ if (((field(fund, 'issuing') as Body).available as Body[])[0]!.amount !== 100000) return false;
1902
+ if (await issuingAvailable() !== 100000) return false;
1903
+ const auth = await h({ m: 'POST', p: '/v1/test_helpers/issuing/authorizations', b: `card=${id(card)}&amount=30000` });
1904
+ if (field(auth, 'approved') !== true) return false;
1905
+ // refuse a capture above the held amount
1906
+ const overCap = await h({ m: 'POST', p: `/v1/test_helpers/issuing/authorizations/${id(auth)}/capture`, b: 'capture_amount=30001' });
1907
+ if (overCap.status !== 400) return false;
1908
+ const captured = await h({ m: 'POST', p: `/v1/test_helpers/issuing/authorizations/${id(auth)}/capture` });
1909
+ if (!ok(captured) || field(captured, 'status') !== 'closed') return false;
1910
+ // the materialized transaction: capture, negative amount, linked balance_transaction
1911
+ const txns = await h({ m: 'GET', p: '/v1/issuing/transactions' });
1912
+ const txn = ((txns.body as Body).data as Body[]).find((t) => t.authorization === id(auth));
1913
+ if (!txn || txn.type !== 'capture' || txn.amount !== -30000 || typeof txn.balance_transaction !== 'string') return false;
1914
+ const bt = await h({ m: 'GET', p: `/v1/balance_transactions/${txn.balance_transaction}` });
1915
+ if (field(bt, 'amount') !== -30000 || field(bt, 'type') !== 'issuing_transaction' || field(bt, 'balance_type') !== 'issuing') return false;
1916
+ // the float went down; issuing funds never leak into the payments buckets
1917
+ if (await issuingAvailable() !== 70000) return false;
1918
+ const bal = await h({ m: 'GET', p: '/v1/balance' });
1919
+ if (((field(bal, 'available') as Body[])[0] ?? {}).amount !== 0) return false;
1920
+ // a second capture on the closed authorization 400s
1921
+ const again = await h({ m: 'POST', p: `/v1/test_helpers/issuing/authorizations/${id(auth)}/capture` });
1922
+ if (again.status !== 400) return false;
1923
+ // force_capture debits too
1924
+ const forced = await h({ m: 'POST', p: '/v1/test_helpers/issuing/transactions/create_force_capture', b: `card=${id(card)}&amount=5000` });
1925
+ if (field(forced, 'amount') !== -5000 || typeof field(forced, 'balance_transaction') !== 'string') return false;
1926
+ return await issuingAvailable() === 65000;
1927
+ } catch (err) {
1928
+ if (isInfrastructureError(err)) throw harnessError('stripe.issuing.capture_debits_balance', err);
1929
+ return false;
1930
+ } finally {
1931
+ setStripeEventDelivery(null);
1932
+ clearStripeWebhooks();
1933
+ rmSync(root, { recursive: true, force: true });
1934
+ }
1935
+ }),
1936
+ // Known-unmodeled Issuing surface (kept honest as todos):
1937
+ todo('stripe.issuing.authorization_holds', 'issuing', 'Authorization holds/releases as balance_transactions (type issuing_authorization_hold/_release) while an approved authorization is open', 'api', 'niche'),
1938
+ todo('stripe.issuing.test_helpers.lifecycle', 'issuing', 'Test-helper authorization lifecycle: expire / increment / reverse / finalize_amount / respond', 'api', 'niche'),
1621
1939
 
1622
1940
  // ── Terminal ─────────────────────────────────────────────────────────────────────
1623
1941
  // Readers (+process_payment_intent): a reader registers with a registration_code (status
@@ -1857,7 +2175,7 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
1857
2175
  // re-sync of identical state appends no deltas (idempotent shadow-diff).
1858
2176
  const again = await fullSyncStripe(execute, { root, occurredAt: '2024-01-01T00:00:00.000Z' });
1859
2177
  return again.deltasAppended === 0;
1860
- } catch { return false; } finally { rmSync(root, { recursive: true, force: true }); }
2178
+ } catch (err) { if (isInfrastructureError(err)) throw harnessError('stripe.connector.full_sync', err); return false; } finally { rmSync(root, { recursive: true, force: true }); }
1861
2179
  }),
1862
2180
 
1863
2181
  // ── DASHBOARD UI screens (the real Stripe Dashboard) ─────────────────────────────
@@ -2660,10 +2978,10 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
2660
2978
  // finalizing an already-finalized invoice 400s.
2661
2979
  const finAgain = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/finalize` });
2662
2980
  if (finAgain.status !== 400) return false;
2663
- const pay = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/pay` });
2981
+ const pay = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/pay`, b: 'payment_method=pm_card_visa' });
2664
2982
  if (!ok(pay) || field(pay, 'status') !== 'paid') return false;
2665
2983
  // paying an already-paid invoice 400s; voiding a paid invoice 400s.
2666
- const payAgain = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/pay` });
2984
+ const payAgain = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/pay`, b: 'payment_method=pm_card_visa' });
2667
2985
  const voidPaid = await h({ m: 'POST', p: `/v1/invoices/${id(inv)}/void` });
2668
2986
  const missing = await h({ m: 'POST', p: '/v1/invoices/in_nope/void' });
2669
2987
  return payAgain.status === 400 && voidPaid.status === 400 && missing.status === 404;
@@ -3291,9 +3609,9 @@ export const STRIPE_CAPABILITIES: CapabilitySpec[] = [
3291
3609
  todo('stripe.treasury.reversals', 'treasury', 'Treasury: credit reversals / debit reversals', 'api', 'niche'),
3292
3610
  todo('stripe.treasury.financial_addresses', 'treasury', 'Treasury: financial-account ABA/address activation', 'api', 'niche'),
3293
3611
  todo('stripe.financial_connections.refresh', 'financial_connections', 'Financial Connections: balance/ownership/transaction refresh + inferred balances', 'api', 'niche'),
3294
- // ── Pull-surface coverage audit gaps (TWIN-46 / G2) — filed as manifest todos so
3295
- // scripts/seed-conformance-issues.ts (A1) turns them into real backlog work. See
3296
- // pull-audit.json (repo root) for the per-pack pull-vs-read-surface evidence.
3612
+ // ── Pull-surface coverage audit gaps (TWIN-46 / G2) — filed as manifest todos, which is
3613
+ // what the demand-ordered build list is drawn from. See pull-audit.json (repo root) for the
3614
+ // per-pack pull-vs-read-surface evidence.
3297
3615
  todo('stripe.connector.pull_payment_methods', 'connector', 'Connector: pull payment methods from the real account (already pushable via COLLECTION, never pulled)', 'connector', 'core'),
3298
3616
  todo('stripe.connector.pull_setup_intents', 'connector', 'Connector: pull setup intents from the real account (already pushable via COLLECTION, never pulled)', 'connector', 'common'),
3299
3617
 
@@ -30,6 +30,8 @@ const TYPE_TO_OBJECT: Record<string, string> = {
30
30
  account: 'account', transfer: 'transfer',
31
31
  tax_rate: 'tax_rate', tax_calculation: 'tax.calculation', tax_registration: 'tax.registration',
32
32
  credit_note: 'credit_note', tax_id: 'tax_id', customer_balance_transaction: 'customer_balance_transaction',
33
+ issuing_authorization: 'issuing.authorization', issuing_card: 'issuing.card',
34
+ issuing_cardholder: 'issuing.cardholder', issuing_transaction: 'issuing.transaction',
33
35
  };
34
36
 
35
37
  export type StripeSchemas = Record<string, JsonSchema>;
@@ -16,8 +16,9 @@
16
16
  // - live runs pass `liveStripeExecute(apiKey)` (the user's own secret key).
17
17
  // Same code path either way, so the connector is fully exercisable offline AND
18
18
  // runnable against a real account.
19
- import { confirmAction, pendingActions, syncPull } from '@volter/twin';
19
+ import { assertBudgetGuardIntact, confirmAction, pendingActions, syncPull } from '@volter/twin';
20
20
  import type { SyncResource, TwinAction } from '@volter/twin';
21
+ import { StripeBudget, stripeCallWeight, type StripeBudgetOptions } from './stripe-budget.ts';
21
22
 
22
23
  const SERVICE = 'stripe';
23
24
 
@@ -40,12 +41,62 @@ export type StripeExecute = (
40
41
  params?: Record<string, unknown>,
41
42
  ) => Promise<{ object?: string; data?: any; error?: { message?: string; type?: string }; [k: string]: unknown }>;
42
43
 
44
+ /** Construction options for the live executor. `budget` cannot be null and cannot be loosened. */
45
+ export type LiveStripeOptions = {
46
+ /** Injected `fetch`, so a test can COUNT the requests the guard did or did not let through. */
47
+ fetchImpl?: typeof fetch;
48
+ /** An existing budget to share across executors. Omit and one is constructed. Cannot be null. */
49
+ budget?: StripeBudget;
50
+ /** Construction options for the default budget (ledger path, clock). Cannot loosen it. */
51
+ budgetOptions?: StripeBudgetOptions;
52
+ };
53
+
43
54
  /**
44
55
  * A live executor against the real Stripe REST API (secret key = the user's own).
45
56
  * Form-encodes POST params; sends GET filters as a query string. Never imported by
46
57
  * the pack's own code path — only constructed by a caller that opts into real I/O.
58
+ *
59
+ * THIS IS THE ONE PLACE this pack issues a live `api.stripe.com` request, and therefore the one
60
+ * place the rate budget has to be enforced — and Stripe is the pack where a runaway loop does not
61
+ * merely annoy a vendor, it MOVES MONEY. EVERY call is guarded: the budget is charged BEFORE the
62
+ * request goes out (`checkBudget`, which THROWS `StripeBudgetError` instead of returning when the
63
+ * ceiling or a cooldown says stop) and the response is fed back (`recordCall`) so a `Retry-After` /
64
+ * 429 signal becomes a persisted cooldown that makes every later call fail fast WITHOUT touching
65
+ * Stripe. There is deliberately no OPTION to disable the guard, and no
66
+ * value a caller can pass for `budget` that yields an unguarded client. That is NOT immunity from
67
+ * a caller who WANTS one: a fresh `budgetOptions.path` per construction, or an injected clock,
68
+ * restores the allowance, because the seam tests need cannot be denied to a determined caller in
69
+ * the same process. The kernel header states that limit and this does not upgrade it. See `stripe-budget.ts` for why,
70
+ * and for the limits of the guarantee.
47
71
  */
48
- export function liveStripeExecute(apiKey: string, base = 'https://api.stripe.com'): StripeExecute {
72
+ export function liveStripeExecute(
73
+ apiKey: string,
74
+ base = 'https://api.stripe.com',
75
+ opts: LiveStripeOptions = {},
76
+ ): StripeExecute {
77
+ // `null`/`undefined` (or omitting it) build the default budget. Anything else must be an
78
+ // UNMODIFIED StripeBudget: a duck-typed stand-in, a SUBCLASS that overrides `checkBudget`, and a
79
+ // Proxy that traps it are all refused, because all three are one-liners that would otherwise
80
+ // hand back a client with no ceiling at all (§9 finding, 2026-07-26 — `instanceof` alone was
81
+ // not a check). What this cannot stop is deliberate sabotage from inside the process (an
82
+ // injected clock, a throwaway ledger path); the kernel's header says so rather than pretending
83
+ // otherwise, and this guards the accident and the one-liner, which are the shapes that happen.
84
+ const doFetch = opts.fetchImpl ?? fetch;
85
+ // The default ledger is keyed by a hash of THIS key — Stripe's limits attach to the account
86
+ // behind it, so a cwd-scoped ledger would hand it a fresh allowance per checkout/worktree/CI leg.
87
+ // A live key and a test key hash differently, which matches Stripe's separate 100/s and 25/s.
88
+ // ONE expression decides which budget is used, so there is no second, weaker test that could
89
+ // disagree with the first. `null`/`undefined` (or omitting it) build the default; anything else
90
+ // must be an UNMODIFIED StripeBudget — a duck-typed stand-in, a SUBCLASS overriding
91
+ // `checkBudget`, and a Proxy trapping it are ALL refused, because each is a one-liner that
92
+ // would otherwise hand back a client with no ceiling (§9 finding, 2026-07-26: `instanceof`
93
+ // alone was not a check — a subclass satisfied it). What this cannot stop is deliberate
94
+ // sabotage from inside the process (an injected clock, a throwaway ledger path); the kernel
95
+ // header states that limit rather than pretending otherwise. This closes the accident and the
96
+ // one-liner, which are the shapes that actually happen.
97
+ const budget = opts.budget !== undefined && opts.budget !== null
98
+ ? assertBudgetGuardIntact(opts.budget, StripeBudget, 'liveStripeExecute')
99
+ : new StripeBudget({ token: apiKey, ...(opts.budgetOptions ?? {}) });
49
100
  return async (method, path, params) => {
50
101
  const headers: Record<string, string> = { Authorization: `Bearer ${apiKey}` };
51
102
  let url = `${base}${path}`;
@@ -57,8 +108,18 @@ export function liveStripeExecute(apiKey: string, base = 'https://api.stripe.com
57
108
  headers['Content-Type'] = 'application/x-www-form-urlencoded';
58
109
  init.body = form;
59
110
  }
60
- const res = await fetch(url, init);
61
- return (await res.json()) as { object?: string; data?: any; error?: { message?: string } };
111
+ const weight = stripeCallWeight(method, path);
112
+ // THROWS instead of calling. Nothing below this line runs when the budget refuses.
113
+ const reservation = budget.checkBudget(weight);
114
+ const res = await doFetch(url, init);
115
+ const resHeaders: Record<string, string> = {};
116
+ res.headers.forEach((v: string, k: string) => { resHeaders[k.toLowerCase()] = v; });
117
+ const parsed = (await res.json()) as { object?: string; data?: any; error?: { message?: string } };
118
+ // Settles the reservation and, on a back-off signal, arms the cooldown. May itself throw (a
119
+ // `Retry-After` beyond the cap is not something to sleep off) — the cooldown is persisted
120
+ // first either way, so the refusal survives the throw.
121
+ budget.recordCall(weight, resHeaders, { status: res.status, reservation });
122
+ return parsed;
62
123
  };
63
124
  }
64
125