@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)
|
|
11
|
-
const CLIENT_CSS = () => new URL('../client/stripe-mirror.css', import.meta.url)
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
64
|
+
"@volter/world-core": "2.0.2"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@volter/world-core": "2.0.
|
|
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",
|
package/src/stripe-events.ts
CHANGED
|
@@ -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
|
});
|
package/src/stripe-mirror-ui.ts
CHANGED
|
@@ -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)
|
|
12
|
-
const CLIENT_CSS = () => new URL('../client/stripe-mirror.css', import.meta.url)
|
|
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.
|
package/src/stripe-server.ts
CHANGED
|
@@ -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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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 }> {
|