@volter/twin-stripe 2.0.1 → 2.0.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.
@@ -1,4 +1,4 @@
1
- import { nodeBuiltin, worldNow } from '@volter/world-core';
1
+ import { nodeBuiltin, worldNow, deliveryTraceHeaders } from '@volter/world-core';
2
2
  import { appDestination, appFetch } from '@volter/world-core/app-route';
3
3
  import { worldEgressRefusal } from '@volter/world-core/network-policy';
4
4
  import vendorEvents from './generated/events.gen.json' with { type: 'json' };
@@ -240,7 +240,7 @@ async function postEvent(url, event, payload, secret) {
240
240
  for (let attempt = 1; attempt <= DELIVERY_ATTEMPTS; attempt += 1) {
241
241
  try {
242
242
  const header = generateTestHeaderString({ payload, secret });
243
- await appFetch(url, { method: 'POST', headers: { 'content-type': 'application/json', 'stripe-signature': header }, body: payload });
243
+ await appFetch(url, { method: 'POST', headers: { 'content-type': 'application/json', 'stripe-signature': header, ...deliveryTraceHeaders() }, body: payload });
244
244
  return;
245
245
  }
246
246
  catch (error) {
@@ -296,7 +296,7 @@ async function httpAuthRequestDelivery(url, event, secret) {
296
296
  async function postAuthRequest(url, payload, header) {
297
297
  const res = await appFetch(url, {
298
298
  method: 'POST',
299
- headers: { 'content-type': 'application/json', 'stripe-signature': header },
299
+ headers: { 'content-type': 'application/json', 'stripe-signature': header, ...deliveryTraceHeaders() },
300
300
  body: payload,
301
301
  signal: AbortSignal.timeout(STRIPE_REALTIME_AUTH_TIMEOUT_MS),
302
302
  });
@@ -4,11 +4,11 @@
4
4
  // twin internals; the client reads and writes only through Stripe's API (`/v1/...`, client/dashboard-api.ts),
5
5
  // so it renders a twin or a real account's test data unchanged, pointed at any origin by configuration.
6
6
  import { readFile } from 'node:fs/promises';
7
- import { bundleClient, fileResponse } from '@volter/world-core';
7
+ import { bundleClient, fileResponse, filePathOf } from '@volter/world-core';
8
8
  import { serveHttp } from '@volter/world-core';
9
9
  import { createStripeTwinFetch } from "./stripe-server.js";
10
- const CLIENT_ENTRY = () => new URL('../client/stripe-mirror.tsx', import.meta.url).pathname; // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
11
- const CLIENT_CSS = () => new URL('../client/stripe-mirror.css', import.meta.url).pathname; // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
10
+ const CLIENT_ENTRY = () => filePathOf(new URL('../client/stripe-mirror.tsx', import.meta.url)); // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
11
+ const CLIENT_CSS = () => filePathOf(new URL('../client/stripe-mirror.css', import.meta.url)); // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
12
12
  // ---------------------------------------------------------------------------
13
13
  // Pure, dependency-free render/format/resolution helpers.
14
14
  //
@@ -7,7 +7,7 @@
7
7
  // The surface is a plain `fetch` (`createStripeTwinFetch`) and the SERVER is one line of
8
8
  // `Bun.serve` around it — see that factory's docstring for why (a serverless entry has no
9
9
  // port to bind, so it mounts the fetch in-process).
10
- import { bindSemantics, coreFor, createDerivedFetch, crossCutting, readParams, semanticsContext, serveHttp, vendorError } from '@volter/world-core';
10
+ import { bindSemantics, coreFor, createDerivedFetch, crossCutting, derivedRequestScopes, readParams, runAsVendorMove, runWithRequestTrace, semanticsContext, serveHttp, vendorError } from '@volter/world-core';
11
11
  import { stripeCheckoutFlow } from "./screens/checkout.js";
12
12
  import { stripeJs } from "./stripe-js.js";
13
13
  import { stripeFinancialConnectionsFlow } from "./screens/financial-connections.js";
@@ -219,26 +219,29 @@ export function createStripeTwinFetch(options) {
219
219
  // later: semantics/renewals.ts; each account's automatic payouts: semantics/balance.ts) are caught up to the World's clock before anything is answered, so every door
220
220
  // (the API, the hosted pages) reads the account as it stands now
221
221
  const billingClock = surface.operations.find((o) => o.id === 'GetSubscriptions');
222
+ // time's moves are Stripe's own, not the caller's: they land under a read-only request too (runAsVendorMove)
222
223
  const catchUp = async (request) => {
223
224
  if (readOnly)
224
225
  return;
225
- const ctx = await semanticsContext(manifest, new Request(request.url), billingClock, scope);
226
- // a test clock advanced by the last request has reached its time (semantics/test-clocks.ts)
227
- await finishClockAdvances(ctx);
228
- await advanceBilling(ctx);
229
- // a submitted bank debit settles (in test mode at once) before anything is answered (semantics/payment-intents.ts)
230
- await settleBankDebits(ctx);
231
- // a coupon past its redeem_by is no longer valid (semantics/coupons.ts)
232
- await lapseCoupons(ctx);
233
- // a top-up's funds arrive five days after it is made (semantics/terminal.ts)
234
- await settleTopups(ctx);
235
- // a report run completes (semantics/platform.ts)
236
- await finishReportRuns(ctx);
237
- // a real-time authorization request no one answered is decided when its window ends (semantics/issuing.ts)
238
- await lapseRealtimeRequests(ctx);
239
- // a refund held for want of balance is made when funds cover it, before any payout takes them (semantics/refunds.ts)
240
- await settleHeldRefunds(ctx);
241
- await advancePayouts(ctx);
226
+ await runAsVendorMove(async () => {
227
+ const ctx = await semanticsContext(manifest, new Request(request.url), billingClock, scope);
228
+ // a test clock advanced by the last request has reached its time (semantics/test-clocks.ts)
229
+ await finishClockAdvances(ctx);
230
+ await advanceBilling(ctx);
231
+ // a submitted bank debit settles (in test mode at once) before anything is answered (semantics/payment-intents.ts)
232
+ await settleBankDebits(ctx);
233
+ // a coupon past its redeem_by is no longer valid (semantics/coupons.ts)
234
+ await lapseCoupons(ctx);
235
+ // a top-up's funds arrive five days after it is made (semantics/terminal.ts)
236
+ await settleTopups(ctx);
237
+ // a report run completes (semantics/platform.ts)
238
+ await finishReportRuns(ctx);
239
+ // a real-time authorization request no one answered is decided when its window ends (semantics/issuing.ts)
240
+ await lapseRealtimeRequests(ctx);
241
+ // a refund held for want of balance is made when funds cover it, before any payout takes them (semantics/refunds.ts)
242
+ await settleHeldRefunds(ctx);
243
+ await advancePayouts(ctx);
244
+ });
242
245
  };
243
246
  // Time's EVENTS, sent on the vendor's clock through a twin-only drain door, as the qstash and vercel twins send
244
247
  // theirs (a caller — a runner's drainer — drives each move). POST /_twin/drain catches time up as every request
@@ -330,7 +333,10 @@ export function createStripeTwinFetch(options) {
330
333
  const answered = creates ? body : withoutEndpointSecret(body);
331
334
  return new Response(JSON.stringify(servesCurrent(pinned) ? render(answered, pinned, await expand) : answered), { status: res.status, statusText: res.statusText, headers: res.headers });
332
335
  };
333
- return Object.assign(rendered, { owners: derived.owners });
336
+ // the request's W3C trace context scopes every door (the API, the hosted flows, the drain): their writes record it and
337
+ // the webhooks they cause continue it (world-core trace-context)
338
+ // a read-only request (x-volter-read-only) writes nothing, answered with Stripe's read-only error
339
+ return derivedRequestScopes(manifest, Object.assign((request) => runWithRequestTrace(request, () => rendered(request)), { owners: derived.owners }));
334
340
  }
335
341
  export async function createStripeTwinServer(options) {
336
342
  const server = await serveHttp({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/twin-stripe",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "Local Stripe twin — a faithful, stateful local Stripe API your real `stripe` SDK talks to unmodified. Mirror, simulate, and fork. Built on @volter/world-core.",
5
5
  "keywords": [
6
6
  "twin",
@@ -56,15 +56,15 @@
56
56
  "postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
57
57
  },
58
58
  "dependencies": {
59
- "@volter/world-ui": "0.1.1",
59
+ "@volter/world-ui": "0.1.2",
60
60
  "react": "^19.2.7",
61
61
  "react-dom": "^19.2.7"
62
62
  },
63
63
  "peerDependencies": {
64
- "@volter/world-core": "2.0.1"
64
+ "@volter/world-core": "2.0.2"
65
65
  },
66
66
  "devDependencies": {
67
- "@volter/world-core": "2.0.1",
67
+ "@volter/world-core": "2.0.2",
68
68
  "@volter/world-tooling": "0.1.0",
69
69
  "@types/bun": "^1.2.20",
70
70
  "@types/node": "^24.0.0",
@@ -1,4 +1,4 @@
1
- import { nodeBuiltin, worldNow } from '@volter/world-core';
1
+ import { nodeBuiltin, worldNow, deliveryTraceHeaders } from '@volter/world-core';
2
2
  import { appDestination, appFetch } from '@volter/world-core/app-route';
3
3
  import { worldEgressRefusal } from '@volter/world-core/network-policy';
4
4
  import vendorEvents from './generated/events.gen.json' with { type: 'json' };
@@ -274,7 +274,7 @@ async function postEvent(url: string, event: StripeEvent, payload: string, secre
274
274
  for (let attempt = 1; attempt <= DELIVERY_ATTEMPTS; attempt += 1) {
275
275
  try {
276
276
  const header = generateTestHeaderString({ payload, secret });
277
- await appFetch(url, { method: 'POST', headers: { 'content-type': 'application/json', 'stripe-signature': header }, body: payload });
277
+ await appFetch(url, { method: 'POST', headers: { 'content-type': 'application/json', 'stripe-signature': header, ...deliveryTraceHeaders() }, body: payload });
278
278
  return;
279
279
  } catch (error) {
280
280
  last = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
@@ -338,7 +338,7 @@ async function httpAuthRequestDelivery(url: string, event: StripeEvent, secret:
338
338
  async function postAuthRequest(url: string, payload: string, header: string): Promise<StripeWebhookEndpointResponse> {
339
339
  const res = await appFetch(url, {
340
340
  method: 'POST',
341
- headers: { 'content-type': 'application/json', 'stripe-signature': header },
341
+ headers: { 'content-type': 'application/json', 'stripe-signature': header, ...deliveryTraceHeaders() },
342
342
  body: payload,
343
343
  signal: AbortSignal.timeout(STRIPE_REALTIME_AUTH_TIMEOUT_MS),
344
344
  });
@@ -4,12 +4,12 @@
4
4
  // twin internals; the client reads and writes only through Stripe's API (`/v1/...`, client/dashboard-api.ts),
5
5
  // so it renders a twin or a real account's test data unchanged, pointed at any origin by configuration.
6
6
  import { readFile } from 'node:fs/promises';
7
- import { bundleClient, fileResponse } from '@volter/world-core';
7
+ import { bundleClient, fileResponse, filePathOf } from '@volter/world-core';
8
8
  import { serveHttp } from '@volter/world-core';
9
9
  import { createStripeTwinFetch } from './stripe-server.ts';
10
10
 
11
- const CLIENT_ENTRY = () => new URL('../client/stripe-mirror.tsx', import.meta.url).pathname; // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
12
- const CLIENT_CSS = () => new URL('../client/stripe-mirror.css', import.meta.url).pathname; // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
11
+ const CLIENT_ENTRY = () => filePathOf(new URL('../client/stripe-mirror.tsx', import.meta.url)); // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
12
+ const CLIENT_CSS = () => filePathOf(new URL('../client/stripe-mirror.css', import.meta.url)); // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
13
13
 
14
14
  // ---------------------------------------------------------------------------
15
15
  // Pure, dependency-free render/format/resolution helpers.
@@ -7,7 +7,7 @@
7
7
  // The surface is a plain `fetch` (`createStripeTwinFetch`) and the SERVER is one line of
8
8
  // `Bun.serve` around it — see that factory's docstring for why (a serverless entry has no
9
9
  // port to bind, so it mounts the fetch in-process).
10
- import { bindSemantics, coreFor, createDerivedFetch, crossCutting, readParams, semanticsContext, serveHttp, vendorError, type DerivedCall, type DerivedFetch } from '@volter/world-core';
10
+ import { bindSemantics, coreFor, createDerivedFetch, crossCutting, derivedRequestScopes, readParams, runAsVendorMove, runWithRequestTrace, semanticsContext, serveHttp, vendorError, type DerivedCall, type DerivedFetch } from '@volter/world-core';
11
11
  import { stripeCheckoutFlow } from './screens/checkout.tsx';
12
12
  import { stripeJs } from './stripe-js.ts';
13
13
  import { stripeFinancialConnectionsFlow } from './screens/financial-connections.tsx';
@@ -215,25 +215,28 @@ export function createStripeTwinFetch(options: StripeTwinOptions): DerivedFetch
215
215
  // later: semantics/renewals.ts; each account's automatic payouts: semantics/balance.ts) are caught up to the World's clock before anything is answered, so every door
216
216
  // (the API, the hosted pages) reads the account as it stands now
217
217
  const billingClock = (surface.operations as Array<{ id: string; method: string; path: string; class: string }>).find((o) => o.id === 'GetSubscriptions')!;
218
+ // time's moves are Stripe's own, not the caller's: they land under a read-only request too (runAsVendorMove)
218
219
  const catchUp = async (request: Request): Promise<void> => {
219
220
  if (readOnly) return;
220
- const ctx = await semanticsContext(manifest, new Request(request.url), billingClock, scope);
221
- // a test clock advanced by the last request has reached its time (semantics/test-clocks.ts)
222
- await finishClockAdvances(ctx);
223
- await advanceBilling(ctx);
224
- // a submitted bank debit settles (in test mode at once) before anything is answered (semantics/payment-intents.ts)
225
- await settleBankDebits(ctx);
226
- // a coupon past its redeem_by is no longer valid (semantics/coupons.ts)
227
- await lapseCoupons(ctx);
228
- // a top-up's funds arrive five days after it is made (semantics/terminal.ts)
229
- await settleTopups(ctx);
230
- // a report run completes (semantics/platform.ts)
231
- await finishReportRuns(ctx);
232
- // a real-time authorization request no one answered is decided when its window ends (semantics/issuing.ts)
233
- await lapseRealtimeRequests(ctx);
234
- // a refund held for want of balance is made when funds cover it, before any payout takes them (semantics/refunds.ts)
235
- await settleHeldRefunds(ctx);
236
- await advancePayouts(ctx);
221
+ await runAsVendorMove(async () => {
222
+ const ctx = await semanticsContext(manifest, new Request(request.url), billingClock, scope);
223
+ // a test clock advanced by the last request has reached its time (semantics/test-clocks.ts)
224
+ await finishClockAdvances(ctx);
225
+ await advanceBilling(ctx);
226
+ // a submitted bank debit settles (in test mode at once) before anything is answered (semantics/payment-intents.ts)
227
+ await settleBankDebits(ctx);
228
+ // a coupon past its redeem_by is no longer valid (semantics/coupons.ts)
229
+ await lapseCoupons(ctx);
230
+ // a top-up's funds arrive five days after it is made (semantics/terminal.ts)
231
+ await settleTopups(ctx);
232
+ // a report run completes (semantics/platform.ts)
233
+ await finishReportRuns(ctx);
234
+ // a real-time authorization request no one answered is decided when its window ends (semantics/issuing.ts)
235
+ await lapseRealtimeRequests(ctx);
236
+ // a refund held for want of balance is made when funds cover it, before any payout takes them (semantics/refunds.ts)
237
+ await settleHeldRefunds(ctx);
238
+ await advancePayouts(ctx);
239
+ });
237
240
  };
238
241
  // Time's EVENTS, sent on the vendor's clock through a twin-only drain door, as the qstash and vercel twins send
239
242
  // theirs (a caller — a runner's drainer — drives each move). POST /_twin/drain catches time up as every request
@@ -309,7 +312,10 @@ export function createStripeTwinFetch(options: StripeTwinOptions): DerivedFetch
309
312
  const answered = creates ? body : withoutEndpointSecret(body);
310
313
  return new Response(JSON.stringify(servesCurrent(pinned) ? render(answered, pinned, await expand) : answered), { status: res.status, statusText: res.statusText, headers: res.headers });
311
314
  };
312
- return Object.assign(rendered, { owners: derived.owners });
315
+ // the request's W3C trace context scopes every door (the API, the hosted flows, the drain): their writes record it and
316
+ // the webhooks they cause continue it (world-core trace-context)
317
+ // a read-only request (x-volter-read-only) writes nothing, answered with Stripe's read-only error
318
+ return derivedRequestScopes(manifest, Object.assign((request: Request) => runWithRequestTrace(request, () => rendered(request)), { owners: derived.owners }));
313
319
  }
314
320
 
315
321
  export async function createStripeTwinServer(options: StripeTwinOptions): Promise<{ port: number; stop: () => void }> {