@stardeck-customer-apps/testing 0.23.0 → 0.24.0
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.
- package/SKILL.md +8 -0
- package/dist/index.js +65 -6
- package/dist/index.mjs +65 -6
- package/dist/next/headers-shim.js +1 -0
- package/dist/next/headers-shim.mjs +1 -0
- package/dist/setup.js +64 -6
- package/dist/setup.mjs +64 -6
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -208,6 +208,14 @@ app.payments.failNext("stripe.getCheckoutSessionStatus", {
|
|
|
208
208
|
message: "connection reset",
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
+
// Stripe createCheckout idempotency + expiry (payments-sdk >= 4.11.0) is simulated like the
|
|
212
|
+
// platform: same idempotencyKey + same payload replays the same session (one captured
|
|
213
|
+
// checkout); same key + different payload -> PaymentError "idempotency_conflict" (409); keys
|
|
214
|
+
// are scoped per account; expiresAt (unix seconds) must be 30 min - 24 h out or
|
|
215
|
+
// "expiry_out_of_range" (400); the default is 24 h and session.expiresAt is returned.
|
|
216
|
+
// Beam payment links reject idempotencyKey ("idempotency_unsupported") and echo expiresAt.
|
|
217
|
+
// The simulator does not model Stripe's 24 h key retention.
|
|
218
|
+
|
|
211
219
|
// File upload
|
|
212
220
|
const storage = new StorageClient();
|
|
213
221
|
await storage.upload(new File(["hi"], "note.txt", { type: "text/plain" }));
|
package/dist/index.js
CHANGED
|
@@ -340,6 +340,7 @@ var state = globalSingleton("state", () => ({
|
|
|
340
340
|
checkouts: [],
|
|
341
341
|
checkoutCounter: 0,
|
|
342
342
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
343
|
+
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
343
344
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
344
345
|
paymentFailures: [],
|
|
345
346
|
products: [],
|
|
@@ -2131,6 +2132,8 @@ function signEventDelivery(secret, context, rawBody) {
|
|
|
2131
2132
|
}
|
|
2132
2133
|
|
|
2133
2134
|
// src/simulator/payments.ts
|
|
2135
|
+
var STRIPE_MIN_EXPIRY_SECONDS = 30 * 60;
|
|
2136
|
+
var STRIPE_MAX_EXPIRY_SECONDS = 24 * 60 * 60;
|
|
2134
2137
|
var BEAM_BOLT_PAYMENT_METHODS = [
|
|
2135
2138
|
"CARD",
|
|
2136
2139
|
"CARD_INSTALLMENTS",
|
|
@@ -2200,6 +2203,15 @@ function isBoltPaymentMethod(value) {
|
|
|
2200
2203
|
function isBoltIntentStatus(value) {
|
|
2201
2204
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
2202
2205
|
}
|
|
2206
|
+
function stableStringify(value) {
|
|
2207
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
2208
|
+
if (value && typeof value === "object") {
|
|
2209
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2210
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
2211
|
+
}
|
|
2212
|
+
return JSON.stringify(value);
|
|
2213
|
+
}
|
|
2214
|
+
var ISO_8601_WITH_ZONE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
|
|
2203
2215
|
function nextCheckoutId() {
|
|
2204
2216
|
state.checkoutCounter += 1;
|
|
2205
2217
|
return `cs_test_${state.checkoutCounter}`;
|
|
@@ -2220,7 +2232,7 @@ function nextBoltChargeId() {
|
|
|
2220
2232
|
state.boltChargeCounter += 1;
|
|
2221
2233
|
return `chrg_${state.boltChargeCounter}`;
|
|
2222
2234
|
}
|
|
2223
|
-
function seedStripeSession(id, body) {
|
|
2235
|
+
function seedStripeSession(id, body, expiresAt) {
|
|
2224
2236
|
const lineItems = body.lineItems ?? [];
|
|
2225
2237
|
let amountTotal = null;
|
|
2226
2238
|
let currency = null;
|
|
@@ -2240,7 +2252,7 @@ function seedStripeSession(id, body) {
|
|
|
2240
2252
|
currency,
|
|
2241
2253
|
customerEmail: body.customerEmail ? String(body.customerEmail) : null,
|
|
2242
2254
|
metadata: body.metadata ?? {},
|
|
2243
|
-
expiresAt
|
|
2255
|
+
expiresAt
|
|
2244
2256
|
});
|
|
2245
2257
|
}
|
|
2246
2258
|
function seedBeamLink(id, body, merchantId) {
|
|
@@ -2260,7 +2272,8 @@ function seedBeamLink(id, body, merchantId) {
|
|
|
2260
2272
|
},
|
|
2261
2273
|
redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
|
|
2262
2274
|
linkSettings: body.linkSettings,
|
|
2263
|
-
collectDeliveryAddress: body.collectDeliveryAddress === true
|
|
2275
|
+
collectDeliveryAddress: body.collectDeliveryAddress === true,
|
|
2276
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
2264
2277
|
});
|
|
2265
2278
|
}
|
|
2266
2279
|
function deriveBoltIntentStatus(storedStatus, expiresAt, now3 = /* @__PURE__ */ new Date()) {
|
|
@@ -2562,6 +2575,16 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2562
2575
|
const linkId = beamProductsMatch[2];
|
|
2563
2576
|
if (!linkId && request.method === "POST") {
|
|
2564
2577
|
const body = await readJsonBody(request);
|
|
2578
|
+
if ("idempotencyKey" in body) {
|
|
2579
|
+
return payErr(
|
|
2580
|
+
"idempotencyKey is not supported for Beam payment links.",
|
|
2581
|
+
400,
|
|
2582
|
+
"idempotency_unsupported"
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
if (typeof body.expiresAt === "string" && (!ISO_8601_WITH_ZONE.test(body.expiresAt) || Number.isNaN(Date.parse(body.expiresAt)))) {
|
|
2586
|
+
return payErr("expiresAt must be an ISO 8601 timestamp", 400);
|
|
2587
|
+
}
|
|
2565
2588
|
const id = nextPaymentLinkId();
|
|
2566
2589
|
const checkoutUrl = `https://beam.test/pay/${id}`;
|
|
2567
2590
|
const captured = {
|
|
@@ -2574,7 +2597,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2574
2597
|
};
|
|
2575
2598
|
state.checkouts.push(captured);
|
|
2576
2599
|
seedBeamLink(id, body, merchantId);
|
|
2577
|
-
return json({
|
|
2600
|
+
return json({
|
|
2601
|
+
id,
|
|
2602
|
+
url: checkoutUrl,
|
|
2603
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
2604
|
+
});
|
|
2578
2605
|
}
|
|
2579
2606
|
if (linkId && request.method === "GET") {
|
|
2580
2607
|
const link = state.paymentLinks.get(linkId);
|
|
@@ -2600,6 +2627,34 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2600
2627
|
}
|
|
2601
2628
|
if (subPath === "checkout" && request.method === "POST") {
|
|
2602
2629
|
const body = await readJsonBody(request);
|
|
2630
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
2631
|
+
const requestedExpiry = typeof body.expiresAt === "number" ? body.expiresAt : void 0;
|
|
2632
|
+
const callerKey = typeof body.idempotencyKey === "string" ? body.idempotencyKey : void 0;
|
|
2633
|
+
const scopedKey = callerKey ? JSON.stringify(["stripe", accountId, callerKey]) : void 0;
|
|
2634
|
+
const comparable = { ...body };
|
|
2635
|
+
delete comparable.deploymentId;
|
|
2636
|
+
delete comparable.idempotencyKey;
|
|
2637
|
+
const fingerprint = stableStringify(comparable);
|
|
2638
|
+
if (scopedKey) {
|
|
2639
|
+
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
2640
|
+
if (saved) {
|
|
2641
|
+
if (saved.fingerprint !== fingerprint) {
|
|
2642
|
+
return payErr(
|
|
2643
|
+
"This idempotency key was already used with different checkout parameters. Use a new key for a different checkout.",
|
|
2644
|
+
409,
|
|
2645
|
+
"idempotency_conflict"
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
return json(saved.response);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
if (requestedExpiry !== void 0 && (requestedExpiry < nowSeconds + STRIPE_MIN_EXPIRY_SECONDS || requestedExpiry > nowSeconds + STRIPE_MAX_EXPIRY_SECONDS)) {
|
|
2652
|
+
return payErr(
|
|
2653
|
+
"expiresAt must be between 30 minutes and 24 hours after the checkout is created (Unix seconds).",
|
|
2654
|
+
400,
|
|
2655
|
+
"expiry_out_of_range"
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2603
2658
|
const id = nextCheckoutId();
|
|
2604
2659
|
const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
|
|
2605
2660
|
const captured = {
|
|
@@ -2612,8 +2667,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2612
2667
|
createdAt: /* @__PURE__ */ new Date()
|
|
2613
2668
|
};
|
|
2614
2669
|
state.checkouts.push(captured);
|
|
2615
|
-
|
|
2616
|
-
|
|
2670
|
+
const expiresAt = requestedExpiry ?? nowSeconds + STRIPE_MAX_EXPIRY_SECONDS;
|
|
2671
|
+
seedStripeSession(id, body, expiresAt);
|
|
2672
|
+
const response = { id, url: checkoutUrl, expiresAt };
|
|
2673
|
+
if (scopedKey) state.idempotentCheckouts.set(scopedKey, { fingerprint, response });
|
|
2674
|
+
return json(response);
|
|
2617
2675
|
}
|
|
2618
2676
|
const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
|
|
2619
2677
|
if (sessionMatch && request.method === "GET") {
|
|
@@ -2778,6 +2836,7 @@ function createPayments() {
|
|
|
2778
2836
|
state.checkouts = [];
|
|
2779
2837
|
state.checkoutCounter = 0;
|
|
2780
2838
|
state.sessionStatuses.clear();
|
|
2839
|
+
state.idempotentCheckouts.clear();
|
|
2781
2840
|
state.paymentLinks.clear();
|
|
2782
2841
|
state.paymentFailures = [];
|
|
2783
2842
|
state.products = [];
|
package/dist/index.mjs
CHANGED
|
@@ -301,6 +301,7 @@ var state = globalSingleton("state", () => ({
|
|
|
301
301
|
checkouts: [],
|
|
302
302
|
checkoutCounter: 0,
|
|
303
303
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
304
|
+
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
304
305
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
305
306
|
paymentFailures: [],
|
|
306
307
|
products: [],
|
|
@@ -2092,6 +2093,8 @@ function signEventDelivery(secret, context, rawBody) {
|
|
|
2092
2093
|
}
|
|
2093
2094
|
|
|
2094
2095
|
// src/simulator/payments.ts
|
|
2096
|
+
var STRIPE_MIN_EXPIRY_SECONDS = 30 * 60;
|
|
2097
|
+
var STRIPE_MAX_EXPIRY_SECONDS = 24 * 60 * 60;
|
|
2095
2098
|
var BEAM_BOLT_PAYMENT_METHODS = [
|
|
2096
2099
|
"CARD",
|
|
2097
2100
|
"CARD_INSTALLMENTS",
|
|
@@ -2161,6 +2164,15 @@ function isBoltPaymentMethod(value) {
|
|
|
2161
2164
|
function isBoltIntentStatus(value) {
|
|
2162
2165
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
2163
2166
|
}
|
|
2167
|
+
function stableStringify(value) {
|
|
2168
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
2169
|
+
if (value && typeof value === "object") {
|
|
2170
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2171
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
2172
|
+
}
|
|
2173
|
+
return JSON.stringify(value);
|
|
2174
|
+
}
|
|
2175
|
+
var ISO_8601_WITH_ZONE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
|
|
2164
2176
|
function nextCheckoutId() {
|
|
2165
2177
|
state.checkoutCounter += 1;
|
|
2166
2178
|
return `cs_test_${state.checkoutCounter}`;
|
|
@@ -2181,7 +2193,7 @@ function nextBoltChargeId() {
|
|
|
2181
2193
|
state.boltChargeCounter += 1;
|
|
2182
2194
|
return `chrg_${state.boltChargeCounter}`;
|
|
2183
2195
|
}
|
|
2184
|
-
function seedStripeSession(id, body) {
|
|
2196
|
+
function seedStripeSession(id, body, expiresAt) {
|
|
2185
2197
|
const lineItems = body.lineItems ?? [];
|
|
2186
2198
|
let amountTotal = null;
|
|
2187
2199
|
let currency = null;
|
|
@@ -2201,7 +2213,7 @@ function seedStripeSession(id, body) {
|
|
|
2201
2213
|
currency,
|
|
2202
2214
|
customerEmail: body.customerEmail ? String(body.customerEmail) : null,
|
|
2203
2215
|
metadata: body.metadata ?? {},
|
|
2204
|
-
expiresAt
|
|
2216
|
+
expiresAt
|
|
2205
2217
|
});
|
|
2206
2218
|
}
|
|
2207
2219
|
function seedBeamLink(id, body, merchantId) {
|
|
@@ -2221,7 +2233,8 @@ function seedBeamLink(id, body, merchantId) {
|
|
|
2221
2233
|
},
|
|
2222
2234
|
redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
|
|
2223
2235
|
linkSettings: body.linkSettings,
|
|
2224
|
-
collectDeliveryAddress: body.collectDeliveryAddress === true
|
|
2236
|
+
collectDeliveryAddress: body.collectDeliveryAddress === true,
|
|
2237
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
2225
2238
|
});
|
|
2226
2239
|
}
|
|
2227
2240
|
function deriveBoltIntentStatus(storedStatus, expiresAt, now3 = /* @__PURE__ */ new Date()) {
|
|
@@ -2523,6 +2536,16 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2523
2536
|
const linkId = beamProductsMatch[2];
|
|
2524
2537
|
if (!linkId && request.method === "POST") {
|
|
2525
2538
|
const body = await readJsonBody(request);
|
|
2539
|
+
if ("idempotencyKey" in body) {
|
|
2540
|
+
return payErr(
|
|
2541
|
+
"idempotencyKey is not supported for Beam payment links.",
|
|
2542
|
+
400,
|
|
2543
|
+
"idempotency_unsupported"
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
if (typeof body.expiresAt === "string" && (!ISO_8601_WITH_ZONE.test(body.expiresAt) || Number.isNaN(Date.parse(body.expiresAt)))) {
|
|
2547
|
+
return payErr("expiresAt must be an ISO 8601 timestamp", 400);
|
|
2548
|
+
}
|
|
2526
2549
|
const id = nextPaymentLinkId();
|
|
2527
2550
|
const checkoutUrl = `https://beam.test/pay/${id}`;
|
|
2528
2551
|
const captured = {
|
|
@@ -2535,7 +2558,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2535
2558
|
};
|
|
2536
2559
|
state.checkouts.push(captured);
|
|
2537
2560
|
seedBeamLink(id, body, merchantId);
|
|
2538
|
-
return json({
|
|
2561
|
+
return json({
|
|
2562
|
+
id,
|
|
2563
|
+
url: checkoutUrl,
|
|
2564
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
2565
|
+
});
|
|
2539
2566
|
}
|
|
2540
2567
|
if (linkId && request.method === "GET") {
|
|
2541
2568
|
const link = state.paymentLinks.get(linkId);
|
|
@@ -2561,6 +2588,34 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2561
2588
|
}
|
|
2562
2589
|
if (subPath === "checkout" && request.method === "POST") {
|
|
2563
2590
|
const body = await readJsonBody(request);
|
|
2591
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
2592
|
+
const requestedExpiry = typeof body.expiresAt === "number" ? body.expiresAt : void 0;
|
|
2593
|
+
const callerKey = typeof body.idempotencyKey === "string" ? body.idempotencyKey : void 0;
|
|
2594
|
+
const scopedKey = callerKey ? JSON.stringify(["stripe", accountId, callerKey]) : void 0;
|
|
2595
|
+
const comparable = { ...body };
|
|
2596
|
+
delete comparable.deploymentId;
|
|
2597
|
+
delete comparable.idempotencyKey;
|
|
2598
|
+
const fingerprint = stableStringify(comparable);
|
|
2599
|
+
if (scopedKey) {
|
|
2600
|
+
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
2601
|
+
if (saved) {
|
|
2602
|
+
if (saved.fingerprint !== fingerprint) {
|
|
2603
|
+
return payErr(
|
|
2604
|
+
"This idempotency key was already used with different checkout parameters. Use a new key for a different checkout.",
|
|
2605
|
+
409,
|
|
2606
|
+
"idempotency_conflict"
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2609
|
+
return json(saved.response);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
if (requestedExpiry !== void 0 && (requestedExpiry < nowSeconds + STRIPE_MIN_EXPIRY_SECONDS || requestedExpiry > nowSeconds + STRIPE_MAX_EXPIRY_SECONDS)) {
|
|
2613
|
+
return payErr(
|
|
2614
|
+
"expiresAt must be between 30 minutes and 24 hours after the checkout is created (Unix seconds).",
|
|
2615
|
+
400,
|
|
2616
|
+
"expiry_out_of_range"
|
|
2617
|
+
);
|
|
2618
|
+
}
|
|
2564
2619
|
const id = nextCheckoutId();
|
|
2565
2620
|
const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
|
|
2566
2621
|
const captured = {
|
|
@@ -2573,8 +2628,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2573
2628
|
createdAt: /* @__PURE__ */ new Date()
|
|
2574
2629
|
};
|
|
2575
2630
|
state.checkouts.push(captured);
|
|
2576
|
-
|
|
2577
|
-
|
|
2631
|
+
const expiresAt = requestedExpiry ?? nowSeconds + STRIPE_MAX_EXPIRY_SECONDS;
|
|
2632
|
+
seedStripeSession(id, body, expiresAt);
|
|
2633
|
+
const response = { id, url: checkoutUrl, expiresAt };
|
|
2634
|
+
if (scopedKey) state.idempotentCheckouts.set(scopedKey, { fingerprint, response });
|
|
2635
|
+
return json(response);
|
|
2578
2636
|
}
|
|
2579
2637
|
const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
|
|
2580
2638
|
if (sessionMatch && request.method === "GET") {
|
|
@@ -2739,6 +2797,7 @@ function createPayments() {
|
|
|
2739
2797
|
state.checkouts = [];
|
|
2740
2798
|
state.checkoutCounter = 0;
|
|
2741
2799
|
state.sessionStatuses.clear();
|
|
2800
|
+
state.idempotentCheckouts.clear();
|
|
2742
2801
|
state.paymentLinks.clear();
|
|
2743
2802
|
state.paymentFailures = [];
|
|
2744
2803
|
state.products = [];
|
|
@@ -62,6 +62,7 @@ var state = globalSingleton("state", () => ({
|
|
|
62
62
|
checkouts: [],
|
|
63
63
|
checkoutCounter: 0,
|
|
64
64
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
65
|
+
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
65
66
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
66
67
|
paymentFailures: [],
|
|
67
68
|
products: [],
|
|
@@ -35,6 +35,7 @@ var state = globalSingleton("state", () => ({
|
|
|
35
35
|
checkouts: [],
|
|
36
36
|
checkoutCounter: 0,
|
|
37
37
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
38
|
+
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
38
39
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
39
40
|
paymentFailures: [],
|
|
40
41
|
products: [],
|
package/dist/setup.js
CHANGED
|
@@ -67,6 +67,7 @@ var state = globalSingleton("state", () => ({
|
|
|
67
67
|
checkouts: [],
|
|
68
68
|
checkoutCounter: 0,
|
|
69
69
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
70
|
+
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
70
71
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
71
72
|
paymentFailures: [],
|
|
72
73
|
products: [],
|
|
@@ -1536,6 +1537,8 @@ var requestScopeStorage = globalSingleton(
|
|
|
1536
1537
|
var import_node_crypto4 = __toESM(require("crypto"));
|
|
1537
1538
|
|
|
1538
1539
|
// src/simulator/payments.ts
|
|
1540
|
+
var STRIPE_MIN_EXPIRY_SECONDS = 30 * 60;
|
|
1541
|
+
var STRIPE_MAX_EXPIRY_SECONDS = 24 * 60 * 60;
|
|
1539
1542
|
var BEAM_BOLT_PAYMENT_METHODS = [
|
|
1540
1543
|
"CARD",
|
|
1541
1544
|
"CARD_INSTALLMENTS",
|
|
@@ -1605,6 +1608,15 @@ function isBoltPaymentMethod(value) {
|
|
|
1605
1608
|
function isBoltIntentStatus(value) {
|
|
1606
1609
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
1607
1610
|
}
|
|
1611
|
+
function stableStringify(value) {
|
|
1612
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1613
|
+
if (value && typeof value === "object") {
|
|
1614
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1615
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
1616
|
+
}
|
|
1617
|
+
return JSON.stringify(value);
|
|
1618
|
+
}
|
|
1619
|
+
var ISO_8601_WITH_ZONE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
|
|
1608
1620
|
function nextCheckoutId() {
|
|
1609
1621
|
state.checkoutCounter += 1;
|
|
1610
1622
|
return `cs_test_${state.checkoutCounter}`;
|
|
@@ -1621,7 +1633,7 @@ function nextBoltIntentId() {
|
|
|
1621
1633
|
state.boltIntentCounter += 1;
|
|
1622
1634
|
return `bolti_${state.boltIntentCounter}`;
|
|
1623
1635
|
}
|
|
1624
|
-
function seedStripeSession(id, body) {
|
|
1636
|
+
function seedStripeSession(id, body, expiresAt) {
|
|
1625
1637
|
const lineItems = body.lineItems ?? [];
|
|
1626
1638
|
let amountTotal = null;
|
|
1627
1639
|
let currency = null;
|
|
@@ -1641,7 +1653,7 @@ function seedStripeSession(id, body) {
|
|
|
1641
1653
|
currency,
|
|
1642
1654
|
customerEmail: body.customerEmail ? String(body.customerEmail) : null,
|
|
1643
1655
|
metadata: body.metadata ?? {},
|
|
1644
|
-
expiresAt
|
|
1656
|
+
expiresAt
|
|
1645
1657
|
});
|
|
1646
1658
|
}
|
|
1647
1659
|
function seedBeamLink(id, body, merchantId) {
|
|
@@ -1661,7 +1673,8 @@ function seedBeamLink(id, body, merchantId) {
|
|
|
1661
1673
|
},
|
|
1662
1674
|
redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
|
|
1663
1675
|
linkSettings: body.linkSettings,
|
|
1664
|
-
collectDeliveryAddress: body.collectDeliveryAddress === true
|
|
1676
|
+
collectDeliveryAddress: body.collectDeliveryAddress === true,
|
|
1677
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
1665
1678
|
});
|
|
1666
1679
|
}
|
|
1667
1680
|
function deriveBoltIntentStatus(storedStatus, expiresAt, now3 = /* @__PURE__ */ new Date()) {
|
|
@@ -1932,6 +1945,16 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1932
1945
|
const linkId = beamProductsMatch[2];
|
|
1933
1946
|
if (!linkId && request.method === "POST") {
|
|
1934
1947
|
const body = await readJsonBody(request);
|
|
1948
|
+
if ("idempotencyKey" in body) {
|
|
1949
|
+
return payErr(
|
|
1950
|
+
"idempotencyKey is not supported for Beam payment links.",
|
|
1951
|
+
400,
|
|
1952
|
+
"idempotency_unsupported"
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
if (typeof body.expiresAt === "string" && (!ISO_8601_WITH_ZONE.test(body.expiresAt) || Number.isNaN(Date.parse(body.expiresAt)))) {
|
|
1956
|
+
return payErr("expiresAt must be an ISO 8601 timestamp", 400);
|
|
1957
|
+
}
|
|
1935
1958
|
const id = nextPaymentLinkId();
|
|
1936
1959
|
const checkoutUrl = `https://beam.test/pay/${id}`;
|
|
1937
1960
|
const captured = {
|
|
@@ -1944,7 +1967,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1944
1967
|
};
|
|
1945
1968
|
state.checkouts.push(captured);
|
|
1946
1969
|
seedBeamLink(id, body, merchantId);
|
|
1947
|
-
return json({
|
|
1970
|
+
return json({
|
|
1971
|
+
id,
|
|
1972
|
+
url: checkoutUrl,
|
|
1973
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
1974
|
+
});
|
|
1948
1975
|
}
|
|
1949
1976
|
if (linkId && request.method === "GET") {
|
|
1950
1977
|
const link = state.paymentLinks.get(linkId);
|
|
@@ -1970,6 +1997,34 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1970
1997
|
}
|
|
1971
1998
|
if (subPath === "checkout" && request.method === "POST") {
|
|
1972
1999
|
const body = await readJsonBody(request);
|
|
2000
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
2001
|
+
const requestedExpiry = typeof body.expiresAt === "number" ? body.expiresAt : void 0;
|
|
2002
|
+
const callerKey = typeof body.idempotencyKey === "string" ? body.idempotencyKey : void 0;
|
|
2003
|
+
const scopedKey = callerKey ? JSON.stringify(["stripe", accountId, callerKey]) : void 0;
|
|
2004
|
+
const comparable = { ...body };
|
|
2005
|
+
delete comparable.deploymentId;
|
|
2006
|
+
delete comparable.idempotencyKey;
|
|
2007
|
+
const fingerprint = stableStringify(comparable);
|
|
2008
|
+
if (scopedKey) {
|
|
2009
|
+
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
2010
|
+
if (saved) {
|
|
2011
|
+
if (saved.fingerprint !== fingerprint) {
|
|
2012
|
+
return payErr(
|
|
2013
|
+
"This idempotency key was already used with different checkout parameters. Use a new key for a different checkout.",
|
|
2014
|
+
409,
|
|
2015
|
+
"idempotency_conflict"
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
return json(saved.response);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
if (requestedExpiry !== void 0 && (requestedExpiry < nowSeconds + STRIPE_MIN_EXPIRY_SECONDS || requestedExpiry > nowSeconds + STRIPE_MAX_EXPIRY_SECONDS)) {
|
|
2022
|
+
return payErr(
|
|
2023
|
+
"expiresAt must be between 30 minutes and 24 hours after the checkout is created (Unix seconds).",
|
|
2024
|
+
400,
|
|
2025
|
+
"expiry_out_of_range"
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
1973
2028
|
const id = nextCheckoutId();
|
|
1974
2029
|
const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
|
|
1975
2030
|
const captured = {
|
|
@@ -1982,8 +2037,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1982
2037
|
createdAt: /* @__PURE__ */ new Date()
|
|
1983
2038
|
};
|
|
1984
2039
|
state.checkouts.push(captured);
|
|
1985
|
-
|
|
1986
|
-
|
|
2040
|
+
const expiresAt = requestedExpiry ?? nowSeconds + STRIPE_MAX_EXPIRY_SECONDS;
|
|
2041
|
+
seedStripeSession(id, body, expiresAt);
|
|
2042
|
+
const response = { id, url: checkoutUrl, expiresAt };
|
|
2043
|
+
if (scopedKey) state.idempotentCheckouts.set(scopedKey, { fingerprint, response });
|
|
2044
|
+
return json(response);
|
|
1987
2045
|
}
|
|
1988
2046
|
const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
|
|
1989
2047
|
if (sessionMatch && request.method === "GET") {
|
package/dist/setup.mjs
CHANGED
|
@@ -43,6 +43,7 @@ var state = globalSingleton("state", () => ({
|
|
|
43
43
|
checkouts: [],
|
|
44
44
|
checkoutCounter: 0,
|
|
45
45
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
46
|
+
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
46
47
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
47
48
|
paymentFailures: [],
|
|
48
49
|
products: [],
|
|
@@ -1512,6 +1513,8 @@ var requestScopeStorage = globalSingleton(
|
|
|
1512
1513
|
import crypto4 from "crypto";
|
|
1513
1514
|
|
|
1514
1515
|
// src/simulator/payments.ts
|
|
1516
|
+
var STRIPE_MIN_EXPIRY_SECONDS = 30 * 60;
|
|
1517
|
+
var STRIPE_MAX_EXPIRY_SECONDS = 24 * 60 * 60;
|
|
1515
1518
|
var BEAM_BOLT_PAYMENT_METHODS = [
|
|
1516
1519
|
"CARD",
|
|
1517
1520
|
"CARD_INSTALLMENTS",
|
|
@@ -1581,6 +1584,15 @@ function isBoltPaymentMethod(value) {
|
|
|
1581
1584
|
function isBoltIntentStatus(value) {
|
|
1582
1585
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
1583
1586
|
}
|
|
1587
|
+
function stableStringify(value) {
|
|
1588
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1589
|
+
if (value && typeof value === "object") {
|
|
1590
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1591
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
1592
|
+
}
|
|
1593
|
+
return JSON.stringify(value);
|
|
1594
|
+
}
|
|
1595
|
+
var ISO_8601_WITH_ZONE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
|
|
1584
1596
|
function nextCheckoutId() {
|
|
1585
1597
|
state.checkoutCounter += 1;
|
|
1586
1598
|
return `cs_test_${state.checkoutCounter}`;
|
|
@@ -1597,7 +1609,7 @@ function nextBoltIntentId() {
|
|
|
1597
1609
|
state.boltIntentCounter += 1;
|
|
1598
1610
|
return `bolti_${state.boltIntentCounter}`;
|
|
1599
1611
|
}
|
|
1600
|
-
function seedStripeSession(id, body) {
|
|
1612
|
+
function seedStripeSession(id, body, expiresAt) {
|
|
1601
1613
|
const lineItems = body.lineItems ?? [];
|
|
1602
1614
|
let amountTotal = null;
|
|
1603
1615
|
let currency = null;
|
|
@@ -1617,7 +1629,7 @@ function seedStripeSession(id, body) {
|
|
|
1617
1629
|
currency,
|
|
1618
1630
|
customerEmail: body.customerEmail ? String(body.customerEmail) : null,
|
|
1619
1631
|
metadata: body.metadata ?? {},
|
|
1620
|
-
expiresAt
|
|
1632
|
+
expiresAt
|
|
1621
1633
|
});
|
|
1622
1634
|
}
|
|
1623
1635
|
function seedBeamLink(id, body, merchantId) {
|
|
@@ -1637,7 +1649,8 @@ function seedBeamLink(id, body, merchantId) {
|
|
|
1637
1649
|
},
|
|
1638
1650
|
redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
|
|
1639
1651
|
linkSettings: body.linkSettings,
|
|
1640
|
-
collectDeliveryAddress: body.collectDeliveryAddress === true
|
|
1652
|
+
collectDeliveryAddress: body.collectDeliveryAddress === true,
|
|
1653
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
1641
1654
|
});
|
|
1642
1655
|
}
|
|
1643
1656
|
function deriveBoltIntentStatus(storedStatus, expiresAt, now3 = /* @__PURE__ */ new Date()) {
|
|
@@ -1908,6 +1921,16 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1908
1921
|
const linkId = beamProductsMatch[2];
|
|
1909
1922
|
if (!linkId && request.method === "POST") {
|
|
1910
1923
|
const body = await readJsonBody(request);
|
|
1924
|
+
if ("idempotencyKey" in body) {
|
|
1925
|
+
return payErr(
|
|
1926
|
+
"idempotencyKey is not supported for Beam payment links.",
|
|
1927
|
+
400,
|
|
1928
|
+
"idempotency_unsupported"
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
if (typeof body.expiresAt === "string" && (!ISO_8601_WITH_ZONE.test(body.expiresAt) || Number.isNaN(Date.parse(body.expiresAt)))) {
|
|
1932
|
+
return payErr("expiresAt must be an ISO 8601 timestamp", 400);
|
|
1933
|
+
}
|
|
1911
1934
|
const id = nextPaymentLinkId();
|
|
1912
1935
|
const checkoutUrl = `https://beam.test/pay/${id}`;
|
|
1913
1936
|
const captured = {
|
|
@@ -1920,7 +1943,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1920
1943
|
};
|
|
1921
1944
|
state.checkouts.push(captured);
|
|
1922
1945
|
seedBeamLink(id, body, merchantId);
|
|
1923
|
-
return json({
|
|
1946
|
+
return json({
|
|
1947
|
+
id,
|
|
1948
|
+
url: checkoutUrl,
|
|
1949
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
1950
|
+
});
|
|
1924
1951
|
}
|
|
1925
1952
|
if (linkId && request.method === "GET") {
|
|
1926
1953
|
const link = state.paymentLinks.get(linkId);
|
|
@@ -1946,6 +1973,34 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1946
1973
|
}
|
|
1947
1974
|
if (subPath === "checkout" && request.method === "POST") {
|
|
1948
1975
|
const body = await readJsonBody(request);
|
|
1976
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
1977
|
+
const requestedExpiry = typeof body.expiresAt === "number" ? body.expiresAt : void 0;
|
|
1978
|
+
const callerKey = typeof body.idempotencyKey === "string" ? body.idempotencyKey : void 0;
|
|
1979
|
+
const scopedKey = callerKey ? JSON.stringify(["stripe", accountId, callerKey]) : void 0;
|
|
1980
|
+
const comparable = { ...body };
|
|
1981
|
+
delete comparable.deploymentId;
|
|
1982
|
+
delete comparable.idempotencyKey;
|
|
1983
|
+
const fingerprint = stableStringify(comparable);
|
|
1984
|
+
if (scopedKey) {
|
|
1985
|
+
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
1986
|
+
if (saved) {
|
|
1987
|
+
if (saved.fingerprint !== fingerprint) {
|
|
1988
|
+
return payErr(
|
|
1989
|
+
"This idempotency key was already used with different checkout parameters. Use a new key for a different checkout.",
|
|
1990
|
+
409,
|
|
1991
|
+
"idempotency_conflict"
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
return json(saved.response);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
if (requestedExpiry !== void 0 && (requestedExpiry < nowSeconds + STRIPE_MIN_EXPIRY_SECONDS || requestedExpiry > nowSeconds + STRIPE_MAX_EXPIRY_SECONDS)) {
|
|
1998
|
+
return payErr(
|
|
1999
|
+
"expiresAt must be between 30 minutes and 24 hours after the checkout is created (Unix seconds).",
|
|
2000
|
+
400,
|
|
2001
|
+
"expiry_out_of_range"
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
1949
2004
|
const id = nextCheckoutId();
|
|
1950
2005
|
const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
|
|
1951
2006
|
const captured = {
|
|
@@ -1958,8 +2013,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1958
2013
|
createdAt: /* @__PURE__ */ new Date()
|
|
1959
2014
|
};
|
|
1960
2015
|
state.checkouts.push(captured);
|
|
1961
|
-
|
|
1962
|
-
|
|
2016
|
+
const expiresAt = requestedExpiry ?? nowSeconds + STRIPE_MAX_EXPIRY_SECONDS;
|
|
2017
|
+
seedStripeSession(id, body, expiresAt);
|
|
2018
|
+
const response = { id, url: checkoutUrl, expiresAt };
|
|
2019
|
+
if (scopedKey) state.idempotentCheckouts.set(scopedKey, { fingerprint, response });
|
|
2020
|
+
return json(response);
|
|
1963
2021
|
}
|
|
1964
2022
|
const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
|
|
1965
2023
|
if (sessionMatch && request.method === "GET") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Vitest test harness for Stardeck customer apps — in-process Postgres (PGlite) plus a control-plane simulator so the real Stardeck SDKs run unmodified in tests",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|