@stardeck-customer-apps/testing 0.25.1 → 0.27.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 +39 -1
- package/dist/index.d.mts +98 -3
- package/dist/index.d.ts +98 -3
- package/dist/index.js +453 -17
- package/dist/index.mjs +453 -17
- package/dist/next/headers-shim.js +6 -0
- package/dist/next/headers-shim.mjs +6 -0
- package/dist/setup.js +337 -16
- package/dist/setup.mjs +337 -16
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -338,6 +338,7 @@ var state = globalSingleton("state", () => ({
|
|
|
338
338
|
identitySuppressClaimDelivery: false,
|
|
339
339
|
identityAllowedReturnOrigins: /* @__PURE__ */ new Set([APP_TEST_ORIGIN]),
|
|
340
340
|
checkouts: [],
|
|
341
|
+
scheduleWakes: [],
|
|
341
342
|
checkoutCounter: 0,
|
|
342
343
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
343
344
|
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
@@ -356,6 +357,11 @@ var state = globalSingleton("state", () => ({
|
|
|
356
357
|
cardTokens: /* @__PURE__ */ new Map(),
|
|
357
358
|
cardTokenCounter: 0,
|
|
358
359
|
cardTokenizationEnabled: true,
|
|
360
|
+
stripePaymentIntents: /* @__PURE__ */ new Map(),
|
|
361
|
+
stripePaymentIntentCounter: 0,
|
|
362
|
+
stripeIdempotentIntents: /* @__PURE__ */ new Map(),
|
|
363
|
+
stripeAllowedMethods: ["card", "promptpay"],
|
|
364
|
+
stripeChargesEnabled: true,
|
|
359
365
|
uploads: [],
|
|
360
366
|
uploadCounter: 0,
|
|
361
367
|
storageFiles: /* @__PURE__ */ new Map(),
|
|
@@ -822,8 +828,8 @@ async function handleNeonSql(db, request) {
|
|
|
822
828
|
rowAsArray: arrayMode
|
|
823
829
|
});
|
|
824
830
|
} catch (error) {
|
|
825
|
-
const
|
|
826
|
-
return json({ message:
|
|
831
|
+
const err2 = error;
|
|
832
|
+
return json({ message: err2.message, code: err2.code }, 400);
|
|
827
833
|
}
|
|
828
834
|
}
|
|
829
835
|
|
|
@@ -2116,6 +2122,348 @@ function decodeNextRedirect(error) {
|
|
|
2116
2122
|
return new Response(null, { status, headers: { location } });
|
|
2117
2123
|
}
|
|
2118
2124
|
|
|
2125
|
+
// src/simulator/stripe-payment-intents.ts
|
|
2126
|
+
var SIM_STRIPE_PUBLISHABLE_KEY = "pk_test_sim_stripe";
|
|
2127
|
+
var SIM_STRIPE_DEFAULT_ACCOUNT = "acct_sim_default";
|
|
2128
|
+
var METHODS = ["card", "promptpay"];
|
|
2129
|
+
var RESERVED_METADATA_KEYS = ["projectId", "stardeckReferenceId", "stardeckStagingDeploymentId"];
|
|
2130
|
+
var PROMPTPAY_QR_TTL_SECONDS = 15 * 60;
|
|
2131
|
+
var PATH_RE = /^\/api\/store\/(?!beam\/)([^/]+)\/(payment-intents(?:\/([^/]+)(?:\/(cancel))?)?|elements-config|readiness)$/;
|
|
2132
|
+
function err(error, status, code) {
|
|
2133
|
+
return json({ error, code }, status);
|
|
2134
|
+
}
|
|
2135
|
+
function decode(segment) {
|
|
2136
|
+
try {
|
|
2137
|
+
return decodeURIComponent(segment);
|
|
2138
|
+
} catch {
|
|
2139
|
+
return segment;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
function isPlayground(account) {
|
|
2143
|
+
return account === "playground";
|
|
2144
|
+
}
|
|
2145
|
+
function resolveAccount(account) {
|
|
2146
|
+
if (isPlayground(account)) return null;
|
|
2147
|
+
if (account === "default") {
|
|
2148
|
+
return process.env.STRIPE_CONNECT_ACCOUNT_ID || SIM_STRIPE_DEFAULT_ACCOUNT;
|
|
2149
|
+
}
|
|
2150
|
+
return account;
|
|
2151
|
+
}
|
|
2152
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2153
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2154
|
+
var APP_METADATA_MAX_KEYS = 50 - RESERVED_METADATA_KEYS.length;
|
|
2155
|
+
function isStringRecord(value) {
|
|
2156
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
2157
|
+
}
|
|
2158
|
+
function isValidMetadata(value) {
|
|
2159
|
+
if (!isStringRecord(value)) return false;
|
|
2160
|
+
const entries = Object.entries(value);
|
|
2161
|
+
return entries.length <= APP_METADATA_MAX_KEYS && entries.every(([k, v]) => k.length >= 1 && k.length <= 40 && v.length <= 500);
|
|
2162
|
+
}
|
|
2163
|
+
function isOptional(value, test) {
|
|
2164
|
+
return value === void 0 || test(value);
|
|
2165
|
+
}
|
|
2166
|
+
var isEmail = (v) => typeof v === "string" && EMAIL_RE.test(v);
|
|
2167
|
+
function validateCreate(body) {
|
|
2168
|
+
const { amount, currency, method, referenceId, idempotencyKey } = body;
|
|
2169
|
+
const checks = [
|
|
2170
|
+
[
|
|
2171
|
+
typeof amount === "number" && Number.isInteger(amount) && amount > 0,
|
|
2172
|
+
"amount must be a positive integer in minor units"
|
|
2173
|
+
],
|
|
2174
|
+
[typeof currency === "string" && currency.length === 3, "currency must be a 3-letter ISO code"],
|
|
2175
|
+
[
|
|
2176
|
+
typeof method === "string" && METHODS.includes(method),
|
|
2177
|
+
`method must be one of: ${METHODS.join(", ")}`
|
|
2178
|
+
],
|
|
2179
|
+
[
|
|
2180
|
+
typeof referenceId === "string" && referenceId.length >= 1 && referenceId.length <= 200,
|
|
2181
|
+
"referenceId must be 1 to 200 characters"
|
|
2182
|
+
],
|
|
2183
|
+
[
|
|
2184
|
+
typeof idempotencyKey === "string" && idempotencyKey.length >= 1 && idempotencyKey.length <= 200,
|
|
2185
|
+
"idempotencyKey is required (1 to 200 characters)"
|
|
2186
|
+
],
|
|
2187
|
+
[
|
|
2188
|
+
isOptional(body.description, (v) => typeof v === "string" && v.length <= 1e3),
|
|
2189
|
+
"description must be at most 1000 characters"
|
|
2190
|
+
],
|
|
2191
|
+
[isOptional(body.receiptEmail, isEmail), "receiptEmail must be an email address"],
|
|
2192
|
+
[isOptional(body.billingEmail, isEmail), "billingEmail must be an email address"],
|
|
2193
|
+
[
|
|
2194
|
+
isOptional(body.returnUrl, (v) => typeof v === "string" && URL.canParse(v)),
|
|
2195
|
+
"returnUrl must be an absolute URL"
|
|
2196
|
+
],
|
|
2197
|
+
[
|
|
2198
|
+
isOptional(body.metadata, isValidMetadata),
|
|
2199
|
+
`metadata must map keys (1-40 chars) to strings (max 500), at most ${APP_METADATA_MAX_KEYS} keys`
|
|
2200
|
+
],
|
|
2201
|
+
[
|
|
2202
|
+
typeof body.deploymentId === "string" && UUID_RE2.test(body.deploymentId),
|
|
2203
|
+
"Invalid deployment ID"
|
|
2204
|
+
]
|
|
2205
|
+
];
|
|
2206
|
+
const failed = checks.find(([ok]) => !ok);
|
|
2207
|
+
return failed ? failed[1] : null;
|
|
2208
|
+
}
|
|
2209
|
+
function appMetadata(raw) {
|
|
2210
|
+
if (!isStringRecord(raw)) return {};
|
|
2211
|
+
const out = {};
|
|
2212
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2213
|
+
if (!RESERVED_METADATA_KEYS.includes(key)) out[key] = value;
|
|
2214
|
+
}
|
|
2215
|
+
return out;
|
|
2216
|
+
}
|
|
2217
|
+
function stableStringify(value) {
|
|
2218
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
2219
|
+
if (value && typeof value === "object") {
|
|
2220
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2221
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
2222
|
+
}
|
|
2223
|
+
return JSON.stringify(value);
|
|
2224
|
+
}
|
|
2225
|
+
function toStripePaymentIntentSummary(intent) {
|
|
2226
|
+
const open = intent.status !== "succeeded" && intent.status !== "canceled";
|
|
2227
|
+
let actionRequired = "NONE";
|
|
2228
|
+
if (open && intent.method === "card") actionRequired = "CLIENT_CONFIRM";
|
|
2229
|
+
if (open && intent.method === "promptpay" && intent.status === "requires_action") {
|
|
2230
|
+
actionRequired = "ENCODED_IMAGE";
|
|
2231
|
+
}
|
|
2232
|
+
return {
|
|
2233
|
+
id: intent.id,
|
|
2234
|
+
status: intent.status,
|
|
2235
|
+
amount: intent.amount,
|
|
2236
|
+
currency: intent.currency,
|
|
2237
|
+
method: intent.method,
|
|
2238
|
+
referenceId: intent.referenceId,
|
|
2239
|
+
metadata: { ...intent.appMetadata },
|
|
2240
|
+
actionRequired,
|
|
2241
|
+
clientSecret: intent.method === "card" ? intent.clientSecret : null,
|
|
2242
|
+
qr: actionRequired === "ENCODED_IMAGE" ? { ...intent.qr } : null,
|
|
2243
|
+
latestChargeId: intent.latestChargeId,
|
|
2244
|
+
amountRefunded: intent.amountRefunded,
|
|
2245
|
+
lastPaymentError: intent.lastPaymentError ? { ...intent.lastPaymentError } : null,
|
|
2246
|
+
created: intent.created,
|
|
2247
|
+
livemode: false,
|
|
2248
|
+
playground: intent.playground
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
function promptPayQr(id, created) {
|
|
2252
|
+
return {
|
|
2253
|
+
imageUrlPng: `https://qr.stripe.test/${id}.png`,
|
|
2254
|
+
imageUrlSvg: `https://qr.stripe.test/${id}.svg`,
|
|
2255
|
+
data: `00020101021230SIM${id}`,
|
|
2256
|
+
hostedInstructionsUrl: `https://payments.stripe.test/promptpay/${id}`,
|
|
2257
|
+
expiresAt: created + PROMPTPAY_QR_TTL_SECONDS
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
function createIntent(body, account, projectId) {
|
|
2261
|
+
const problem = validateCreate(body);
|
|
2262
|
+
if (problem) return err(problem, 400, "invalid_request");
|
|
2263
|
+
const method = body.method;
|
|
2264
|
+
const currency = body.currency.toLowerCase();
|
|
2265
|
+
if (!state.stripeAllowedMethods.includes(method)) {
|
|
2266
|
+
return err(
|
|
2267
|
+
`The ${method} payment method is turned off for this account`,
|
|
2268
|
+
422,
|
|
2269
|
+
"method_not_allowed"
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
if (method === "promptpay" && currency !== "thb") {
|
|
2273
|
+
return err("PromptPay only takes THB", 422, "currency_not_supported");
|
|
2274
|
+
}
|
|
2275
|
+
if (!state.stripeChargesEnabled) {
|
|
2276
|
+
return err("This Stripe account can't take payments yet", 503, "account_not_ready");
|
|
2277
|
+
}
|
|
2278
|
+
const scopedKey = JSON.stringify(["pi", projectId, account, body.idempotencyKey]);
|
|
2279
|
+
const comparable = { ...body };
|
|
2280
|
+
delete comparable.deploymentId;
|
|
2281
|
+
delete comparable.idempotencyKey;
|
|
2282
|
+
const fingerprint = stableStringify(comparable);
|
|
2283
|
+
const saved = state.stripeIdempotentIntents.get(scopedKey);
|
|
2284
|
+
if (saved) {
|
|
2285
|
+
if (saved.fingerprint !== fingerprint) {
|
|
2286
|
+
return err(
|
|
2287
|
+
"This idempotency key was already used with different parameters. Use a new key for a different payment.",
|
|
2288
|
+
409,
|
|
2289
|
+
"idempotency_conflict"
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
const existing = state.stripePaymentIntents.get(saved.id);
|
|
2293
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(existing) }, 201);
|
|
2294
|
+
}
|
|
2295
|
+
state.stripePaymentIntentCounter += 1;
|
|
2296
|
+
const id = `pi_sim_${state.stripePaymentIntentCounter}`;
|
|
2297
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
2298
|
+
const referenceId = body.referenceId;
|
|
2299
|
+
const metadata = appMetadata(body.metadata);
|
|
2300
|
+
const intent = {
|
|
2301
|
+
id,
|
|
2302
|
+
projectId,
|
|
2303
|
+
account,
|
|
2304
|
+
connectedAccountId: resolveAccount(account),
|
|
2305
|
+
playground: isPlayground(account),
|
|
2306
|
+
status: method === "card" ? "requires_payment_method" : "requires_action",
|
|
2307
|
+
amount: body.amount,
|
|
2308
|
+
currency,
|
|
2309
|
+
method,
|
|
2310
|
+
referenceId,
|
|
2311
|
+
appMetadata: metadata,
|
|
2312
|
+
metadata: { ...metadata, projectId, stardeckReferenceId: referenceId },
|
|
2313
|
+
description: typeof body.description === "string" ? body.description : null,
|
|
2314
|
+
receiptEmail: typeof body.receiptEmail === "string" ? body.receiptEmail : null,
|
|
2315
|
+
returnUrl: typeof body.returnUrl === "string" ? body.returnUrl : null,
|
|
2316
|
+
clientSecret: `${id}_secret_sim`,
|
|
2317
|
+
qr: method === "promptpay" ? promptPayQr(id, created) : null,
|
|
2318
|
+
latestChargeId: null,
|
|
2319
|
+
amountRefunded: 0,
|
|
2320
|
+
lastPaymentError: null,
|
|
2321
|
+
created
|
|
2322
|
+
};
|
|
2323
|
+
state.stripePaymentIntents.set(id, intent);
|
|
2324
|
+
state.stripeIdempotentIntents.set(scopedKey, { fingerprint, id });
|
|
2325
|
+
console.log(
|
|
2326
|
+
`[stardeck-testing] stripe payment intent ${id} created method=${method} amount=${intent.amount} ${currency}`
|
|
2327
|
+
);
|
|
2328
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) }, 201);
|
|
2329
|
+
}
|
|
2330
|
+
function ownedIntent(id, projectId) {
|
|
2331
|
+
const intent = state.stripePaymentIntents.get(id);
|
|
2332
|
+
return intent && intent.projectId === projectId ? intent : void 0;
|
|
2333
|
+
}
|
|
2334
|
+
function cancelIntent(intent) {
|
|
2335
|
+
if (intent.status === "succeeded") {
|
|
2336
|
+
return err("This payment already succeeded and can't be canceled", 409, "already_succeeded");
|
|
2337
|
+
}
|
|
2338
|
+
intent.status = "canceled";
|
|
2339
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) });
|
|
2340
|
+
}
|
|
2341
|
+
function handleIntents(request, url, account, intentId, action, projectId) {
|
|
2342
|
+
if (!intentId && request.method === "POST") {
|
|
2343
|
+
return readJsonBody(request).then((body) => createIntent(body, account, projectId));
|
|
2344
|
+
}
|
|
2345
|
+
if (!intentId && request.method === "GET") {
|
|
2346
|
+
const referenceId = url.searchParams.get("referenceId");
|
|
2347
|
+
if (!referenceId) return err("referenceId is required", 400, "invalid_request");
|
|
2348
|
+
const paymentIntents = [...state.stripePaymentIntents.values()].filter((i) => i.projectId === projectId && i.referenceId === referenceId).map(toStripePaymentIntentSummary);
|
|
2349
|
+
return json({ paymentIntents });
|
|
2350
|
+
}
|
|
2351
|
+
if (!intentId) return null;
|
|
2352
|
+
const intent = ownedIntent(decode(intentId), projectId);
|
|
2353
|
+
if (!action && request.method === "GET") {
|
|
2354
|
+
if (!intent) return err("Payment intent not found", 404, "not_found");
|
|
2355
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) });
|
|
2356
|
+
}
|
|
2357
|
+
if (action === "cancel" && request.method === "POST") {
|
|
2358
|
+
return readJsonBody(request).then((body) => {
|
|
2359
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE2.test(body.deploymentId)) {
|
|
2360
|
+
return err("deploymentId is required", 400, "invalid_request");
|
|
2361
|
+
}
|
|
2362
|
+
if (!intent) return err("Payment intent not found", 404, "not_found");
|
|
2363
|
+
return cancelIntent(intent);
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
return null;
|
|
2367
|
+
}
|
|
2368
|
+
async function handleStripePaymentIntentRequest(request, url, projectId) {
|
|
2369
|
+
const match = url.pathname.match(PATH_RE);
|
|
2370
|
+
if (!match) return null;
|
|
2371
|
+
if (!projectId) return err("Missing authentication", 401, "unauthorized");
|
|
2372
|
+
const account = decode(match[1]);
|
|
2373
|
+
const resource = match[2];
|
|
2374
|
+
if (request.method === "GET" && !url.searchParams.get("deploymentId")) {
|
|
2375
|
+
return err("deploymentId is required", 400, "invalid_request");
|
|
2376
|
+
}
|
|
2377
|
+
if (resource === "elements-config" && request.method === "GET") {
|
|
2378
|
+
return json({
|
|
2379
|
+
publishableKey: SIM_STRIPE_PUBLISHABLE_KEY,
|
|
2380
|
+
stripeAccount: resolveAccount(account),
|
|
2381
|
+
playground: isPlayground(account)
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
if (resource === "readiness" && request.method === "GET") {
|
|
2385
|
+
const methods = [...state.stripeAllowedMethods];
|
|
2386
|
+
return json({
|
|
2387
|
+
provider: "stripe",
|
|
2388
|
+
ready: state.stripeChargesEnabled && methods.length > 0,
|
|
2389
|
+
chargesEnabled: state.stripeChargesEnabled,
|
|
2390
|
+
methods,
|
|
2391
|
+
playground: isPlayground(account)
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
if (resource.startsWith("payment-intents")) {
|
|
2395
|
+
return handleIntents(request, url, account, match[3], match[4], projectId);
|
|
2396
|
+
}
|
|
2397
|
+
return null;
|
|
2398
|
+
}
|
|
2399
|
+
function requireIntent(id) {
|
|
2400
|
+
const intent = state.stripePaymentIntents.get(id);
|
|
2401
|
+
if (!intent) throw new Error(`[stardeck-testing] Unknown payment intent id: ${id}`);
|
|
2402
|
+
return intent;
|
|
2403
|
+
}
|
|
2404
|
+
function requireOpenIntent(id, verb) {
|
|
2405
|
+
const intent = requireIntent(id);
|
|
2406
|
+
if (intent.status === "succeeded" || intent.status === "canceled") {
|
|
2407
|
+
throw new Error(
|
|
2408
|
+
`[stardeck-testing] Can't ${verb} payment intent ${id}: it is ${intent.status}`
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
return intent;
|
|
2412
|
+
}
|
|
2413
|
+
function succeedStripePaymentIntent(id) {
|
|
2414
|
+
const intent = requireOpenIntent(id, "mark paid");
|
|
2415
|
+
intent.status = "succeeded";
|
|
2416
|
+
intent.latestChargeId = `ch_sim_${id}`;
|
|
2417
|
+
intent.lastPaymentError = null;
|
|
2418
|
+
}
|
|
2419
|
+
function failStripePaymentIntent(id, code) {
|
|
2420
|
+
const intent = requireOpenIntent(id, "fail");
|
|
2421
|
+
intent.status = "requires_payment_method";
|
|
2422
|
+
intent.latestChargeId = `ch_sim_${id}_failed`;
|
|
2423
|
+
intent.lastPaymentError = {
|
|
2424
|
+
code,
|
|
2425
|
+
declineCode: code === "card_declined" ? "generic_decline" : null,
|
|
2426
|
+
message: `Simulated payment failure: ${code}`
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
function cancelStripePaymentIntentById(id) {
|
|
2430
|
+
const intent = requireOpenIntent(id, "cancel");
|
|
2431
|
+
intent.status = "canceled";
|
|
2432
|
+
}
|
|
2433
|
+
function stripePaymentIntentEvent(id) {
|
|
2434
|
+
const intent = requireIntent(id);
|
|
2435
|
+
let type;
|
|
2436
|
+
if (intent.status === "succeeded") type = "payment_intent.succeeded";
|
|
2437
|
+
else if (intent.status === "canceled") type = "payment_intent.canceled";
|
|
2438
|
+
else if (intent.lastPaymentError) type = "payment_intent.payment_failed";
|
|
2439
|
+
else {
|
|
2440
|
+
throw new Error(
|
|
2441
|
+
`[stardeck-testing] Payment intent ${id} is ${intent.status}; call markPaid, failPayment or cancelPayment first`
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
const error = intent.lastPaymentError;
|
|
2445
|
+
return {
|
|
2446
|
+
type,
|
|
2447
|
+
accountId: intent.connectedAccountId ?? "acct_stardeck_playground",
|
|
2448
|
+
data: {
|
|
2449
|
+
id: intent.id,
|
|
2450
|
+
object: "payment_intent",
|
|
2451
|
+
amount: intent.amount,
|
|
2452
|
+
amount_received: intent.status === "succeeded" ? intent.amount : 0,
|
|
2453
|
+
currency: intent.currency,
|
|
2454
|
+
status: intent.status,
|
|
2455
|
+
metadata: { ...intent.metadata },
|
|
2456
|
+
payment_method_types: [intent.method],
|
|
2457
|
+
latest_charge: intent.latestChargeId,
|
|
2458
|
+
last_payment_error: error ? { code: error.code, decline_code: error.declineCode, message: error.message } : null,
|
|
2459
|
+
description: intent.description,
|
|
2460
|
+
receipt_email: intent.receiptEmail,
|
|
2461
|
+
created: intent.created,
|
|
2462
|
+
livemode: false
|
|
2463
|
+
}
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2119
2467
|
// src/simulator/webhook-signing.ts
|
|
2120
2468
|
var import_node_crypto5 = __toESM(require("crypto"));
|
|
2121
2469
|
function signEventDelivery(secret, context, rawBody) {
|
|
@@ -2149,7 +2497,7 @@ var BEAM_BOLT_PAYMENT_METHODS = [
|
|
|
2149
2497
|
"SPAY_LATER"
|
|
2150
2498
|
];
|
|
2151
2499
|
var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
|
|
2152
|
-
var
|
|
2500
|
+
var UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2153
2501
|
function payErr(error, status = 400, code, details) {
|
|
2154
2502
|
const body = { error };
|
|
2155
2503
|
if (code) body.code = code;
|
|
@@ -2175,7 +2523,23 @@ function paymentOperationForRequest(request, pathname) {
|
|
|
2175
2523
|
if (request.method === "GET" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout-sessions\/[^/]+$/.test(pathname)) {
|
|
2176
2524
|
return "stripe.getCheckoutSessionStatus";
|
|
2177
2525
|
}
|
|
2178
|
-
return
|
|
2526
|
+
return stripeInlineOperation(request.method, pathname);
|
|
2527
|
+
}
|
|
2528
|
+
var STRIPE_INLINE_OPERATIONS = [
|
|
2529
|
+
["POST", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents$/, "stripe.createPaymentIntent"],
|
|
2530
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents$/, "stripe.findPaymentIntents"],
|
|
2531
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents\/[^/]+$/, "stripe.getPaymentIntent"],
|
|
2532
|
+
[
|
|
2533
|
+
"POST",
|
|
2534
|
+
/^\/api\/store\/(?!beam\/)[^/]+\/payment-intents\/[^/]+\/cancel$/,
|
|
2535
|
+
"stripe.cancelPaymentIntent"
|
|
2536
|
+
],
|
|
2537
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/elements-config$/, "stripe.getElementsConfig"],
|
|
2538
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/readiness$/, "stripe.getReadiness"]
|
|
2539
|
+
];
|
|
2540
|
+
function stripeInlineOperation(method, pathname) {
|
|
2541
|
+
const hit = STRIPE_INLINE_OPERATIONS.find(([m, re]) => m === method && re.test(pathname));
|
|
2542
|
+
return hit ? hit[2] : null;
|
|
2179
2543
|
}
|
|
2180
2544
|
function consumePaymentFailure(request, pathname) {
|
|
2181
2545
|
const operation = paymentOperationForRequest(request, pathname);
|
|
@@ -2205,11 +2569,11 @@ function isBoltPaymentMethod(value) {
|
|
|
2205
2569
|
function isBoltIntentStatus(value) {
|
|
2206
2570
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
2207
2571
|
}
|
|
2208
|
-
function
|
|
2209
|
-
if (Array.isArray(value)) return `[${value.map(
|
|
2572
|
+
function stableStringify2(value) {
|
|
2573
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify2).join(",")}]`;
|
|
2210
2574
|
if (value && typeof value === "object") {
|
|
2211
2575
|
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2212
|
-
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${
|
|
2576
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify2(v)}`).join(",")}}`;
|
|
2213
2577
|
}
|
|
2214
2578
|
return JSON.stringify(value);
|
|
2215
2579
|
}
|
|
@@ -2350,10 +2714,16 @@ async function deliverEvent(envelope, handler, options) {
|
|
|
2350
2714
|
body: envelope
|
|
2351
2715
|
});
|
|
2352
2716
|
}
|
|
2353
|
-
async function handlePaymentsRequest(request, url) {
|
|
2717
|
+
async function handlePaymentsRequest(request, url, signedProjectId = null) {
|
|
2354
2718
|
const pathname = url.pathname;
|
|
2355
2719
|
const injectedFailure = consumePaymentFailure(request, pathname);
|
|
2356
2720
|
if (injectedFailure) return injectedFailure;
|
|
2721
|
+
const inlineStripeResponse = await handleStripePaymentIntentRequest(
|
|
2722
|
+
request,
|
|
2723
|
+
url,
|
|
2724
|
+
signedProjectId
|
|
2725
|
+
);
|
|
2726
|
+
if (inlineStripeResponse) return inlineStripeResponse;
|
|
2357
2727
|
const directChargeResponse = await handleDirectChargeRequest(request, pathname);
|
|
2358
2728
|
if (directChargeResponse) return directChargeResponse;
|
|
2359
2729
|
if (/\/billing-portal$/.test(pathname)) {
|
|
@@ -2452,7 +2822,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2452
2822
|
code: "too_small"
|
|
2453
2823
|
});
|
|
2454
2824
|
}
|
|
2455
|
-
if (typeof body.deploymentId !== "string" || !
|
|
2825
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE3.test(body.deploymentId)) {
|
|
2456
2826
|
issues.push({
|
|
2457
2827
|
path: ["deploymentId"],
|
|
2458
2828
|
message: "Invalid uuid",
|
|
@@ -2636,7 +3006,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2636
3006
|
const comparable = { ...body };
|
|
2637
3007
|
delete comparable.deploymentId;
|
|
2638
3008
|
delete comparable.idempotencyKey;
|
|
2639
|
-
const fingerprint =
|
|
3009
|
+
const fingerprint = stableStringify2(comparable);
|
|
2640
3010
|
if (scopedKey) {
|
|
2641
3011
|
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
2642
3012
|
if (saved) {
|
|
@@ -2711,6 +3081,10 @@ function createPayments() {
|
|
|
2711
3081
|
state.paymentFailures.push({ operation, failure: failure2 });
|
|
2712
3082
|
},
|
|
2713
3083
|
markPaid(id) {
|
|
3084
|
+
if (state.stripePaymentIntents.has(id)) {
|
|
3085
|
+
succeedStripePaymentIntent(id);
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
2714
3088
|
const session = state.sessionStatuses.get(id);
|
|
2715
3089
|
if (session) {
|
|
2716
3090
|
session.status = "complete";
|
|
@@ -2738,7 +3112,26 @@ function createPayments() {
|
|
|
2738
3112
|
}
|
|
2739
3113
|
link.status = status;
|
|
2740
3114
|
},
|
|
2741
|
-
|
|
3115
|
+
failPayment(paymentIntentId, code = "card_declined") {
|
|
3116
|
+
failStripePaymentIntent(paymentIntentId, code);
|
|
3117
|
+
},
|
|
3118
|
+
cancelPayment(paymentIntentId) {
|
|
3119
|
+
cancelStripePaymentIntentById(paymentIntentId);
|
|
3120
|
+
},
|
|
3121
|
+
get stripePaymentIntents() {
|
|
3122
|
+
return [...state.stripePaymentIntents.values()];
|
|
3123
|
+
},
|
|
3124
|
+
getStripePaymentIntent(paymentIntentId) {
|
|
3125
|
+
return state.stripePaymentIntents.get(paymentIntentId);
|
|
3126
|
+
},
|
|
3127
|
+
setStripePaymentMethods(methods) {
|
|
3128
|
+
state.stripeAllowedMethods = [...methods];
|
|
3129
|
+
},
|
|
3130
|
+
setStripeChargesEnabled(enabled) {
|
|
3131
|
+
state.stripeChargesEnabled = enabled;
|
|
3132
|
+
},
|
|
3133
|
+
async deliverStripeEvent(handler, input, options) {
|
|
3134
|
+
const event = "paymentIntentId" in input ? stripePaymentIntentEvent(input.paymentIntentId) : input;
|
|
2742
3135
|
const envelope = {
|
|
2743
3136
|
id: `evt_test_${import_node_crypto6.default.randomUUID()}`,
|
|
2744
3137
|
kind: "stripe_webhook",
|
|
@@ -2854,6 +3247,11 @@ function createPayments() {
|
|
|
2854
3247
|
state.cardTokens.clear();
|
|
2855
3248
|
state.cardTokenCounter = 0;
|
|
2856
3249
|
state.cardTokenizationEnabled = true;
|
|
3250
|
+
state.stripePaymentIntents.clear();
|
|
3251
|
+
state.stripePaymentIntentCounter = 0;
|
|
3252
|
+
state.stripeIdempotentIntents.clear();
|
|
3253
|
+
state.stripeAllowedMethods = ["card", "promptpay"];
|
|
3254
|
+
state.stripeChargesEnabled = true;
|
|
2857
3255
|
},
|
|
2858
3256
|
get count() {
|
|
2859
3257
|
return state.checkouts.length;
|
|
@@ -3626,6 +4024,32 @@ function createEdge() {
|
|
|
3626
4024
|
};
|
|
3627
4025
|
}
|
|
3628
4026
|
|
|
4027
|
+
// src/simulator/schedules.ts
|
|
4028
|
+
async function handleScheduleWake(request, scheduleKey) {
|
|
4029
|
+
if (request.method !== "POST") return failure(`Method ${request.method} not allowed`, 405);
|
|
4030
|
+
const body = await readJsonBody(request);
|
|
4031
|
+
const at = typeof body.at === "string" ? new Date(body.at) : null;
|
|
4032
|
+
if (!at || Number.isNaN(at.getTime())) return failure("`at` must be an ISO date-time", 400);
|
|
4033
|
+
state.scheduleWakes.push({ scheduleKey, at, requestedAt: /* @__PURE__ */ new Date() });
|
|
4034
|
+
return success({ woken: true });
|
|
4035
|
+
}
|
|
4036
|
+
function createSchedules() {
|
|
4037
|
+
return {
|
|
4038
|
+
get wakes() {
|
|
4039
|
+
return state.scheduleWakes;
|
|
4040
|
+
},
|
|
4041
|
+
wakesFor(scheduleKey) {
|
|
4042
|
+
return state.scheduleWakes.filter((wake) => wake.scheduleKey === scheduleKey);
|
|
4043
|
+
},
|
|
4044
|
+
earliestWake(scheduleKey) {
|
|
4045
|
+
return this.wakesFor(scheduleKey).map((wake) => wake.at).sort((a, b) => a.getTime() - b.getTime())[0];
|
|
4046
|
+
},
|
|
4047
|
+
clear() {
|
|
4048
|
+
state.scheduleWakes = [];
|
|
4049
|
+
}
|
|
4050
|
+
};
|
|
4051
|
+
}
|
|
4052
|
+
|
|
3629
4053
|
// src/simulator/starlens.ts
|
|
3630
4054
|
var LATEST_VERSION = "v2";
|
|
3631
4055
|
function starlensFailure(error, status, code) {
|
|
@@ -3782,7 +4206,7 @@ function requiresDeploymentHmac(request, url) {
|
|
|
3782
4206
|
return !isPresignedPut;
|
|
3783
4207
|
}
|
|
3784
4208
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
3785
|
-
const
|
|
4209
|
+
const isEmail2 = url.pathname === "/api/email/send";
|
|
3786
4210
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
3787
4211
|
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
3788
4212
|
const messagingMatch = url.pathname.match(
|
|
@@ -3790,8 +4214,10 @@ function requiresDeploymentHmac(request, url) {
|
|
|
3790
4214
|
);
|
|
3791
4215
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
3792
4216
|
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
3793
|
-
|
|
4217
|
+
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
4218
|
+
return !!(isStarlens || scheduleWakeMatch || dataStoreMatch || isEmail2 || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
3794
4219
|
}
|
|
4220
|
+
var SCHEDULE_WAKE_PATH = /^\/api\/deployments\/[^/]+\/schedules\/([^/]+)\/wake$/;
|
|
3795
4221
|
function deploymentIdFromPath(url) {
|
|
3796
4222
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
3797
4223
|
if (!match) return null;
|
|
@@ -3812,7 +4238,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3812
4238
|
return authMatch[1].endsWith("verify") ? handleAuthVerify(request) : handleAuthRefresh(request);
|
|
3813
4239
|
}
|
|
3814
4240
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
3815
|
-
const
|
|
4241
|
+
const isEmail2 = url.pathname === "/api/email/send";
|
|
3816
4242
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
3817
4243
|
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
3818
4244
|
const messagingMatch = url.pathname.match(
|
|
@@ -3821,6 +4247,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3821
4247
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
3822
4248
|
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
3823
4249
|
let signedDeploymentId = null;
|
|
4250
|
+
let signedProjectId = null;
|
|
3824
4251
|
if (requiresDeploymentHmac(request, url)) {
|
|
3825
4252
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
3826
4253
|
if (!authHeader) {
|
|
@@ -3833,6 +4260,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3833
4260
|
return failure("Invalid authentication", 401);
|
|
3834
4261
|
}
|
|
3835
4262
|
signedDeploymentId = authPayload.deploymentId;
|
|
4263
|
+
signedProjectId = authPayload.projectId;
|
|
3836
4264
|
}
|
|
3837
4265
|
if (isStorageHost) {
|
|
3838
4266
|
return handleStorageRequest(request, url);
|
|
@@ -3840,14 +4268,14 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3840
4268
|
if (url.hostname === SIM_BEAM_HOST) {
|
|
3841
4269
|
return handleCardTokenRequest(request, url);
|
|
3842
4270
|
}
|
|
3843
|
-
if (
|
|
4271
|
+
if (isEmail2 && request.method === "POST") {
|
|
3844
4272
|
return handleEmailSend(request);
|
|
3845
4273
|
}
|
|
3846
4274
|
if (identitiesMatch) {
|
|
3847
4275
|
return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
|
|
3848
4276
|
}
|
|
3849
4277
|
if (storeMatch) {
|
|
3850
|
-
return handlePaymentsRequest(request, url);
|
|
4278
|
+
return handlePaymentsRequest(request, url, signedProjectId);
|
|
3851
4279
|
}
|
|
3852
4280
|
if (messagingMatch) {
|
|
3853
4281
|
const channel = messagingMatch[1];
|
|
@@ -3856,6 +4284,10 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3856
4284
|
if (edgeMatch) {
|
|
3857
4285
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
3858
4286
|
}
|
|
4287
|
+
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
4288
|
+
if (scheduleWakeMatch) {
|
|
4289
|
+
return handleScheduleWake(request, decodeURIComponent(scheduleWakeMatch[1]));
|
|
4290
|
+
}
|
|
3859
4291
|
if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
|
|
3860
4292
|
return handleStarlensExtract(request, signedDeploymentId);
|
|
3861
4293
|
}
|
|
@@ -3880,7 +4312,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3880
4312
|
}
|
|
3881
4313
|
}
|
|
3882
4314
|
return failure(
|
|
3883
|
-
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, auth verify/refresh, Neon /sql.`,
|
|
4315
|
+
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, schedule wakes, auth verify/refresh, Neon /sql.`,
|
|
3884
4316
|
404
|
|
3885
4317
|
);
|
|
3886
4318
|
}
|
|
@@ -4059,6 +4491,7 @@ async function createTestApp(options = {}) {
|
|
|
4059
4491
|
const messages = createMessages();
|
|
4060
4492
|
const edge = createEdge();
|
|
4061
4493
|
const starlens = createStarlens();
|
|
4494
|
+
const schedules = createSchedules();
|
|
4062
4495
|
const app = {
|
|
4063
4496
|
db,
|
|
4064
4497
|
inbox,
|
|
@@ -4068,6 +4501,7 @@ async function createTestApp(options = {}) {
|
|
|
4068
4501
|
messages,
|
|
4069
4502
|
edge,
|
|
4070
4503
|
starlens,
|
|
4504
|
+
schedules,
|
|
4071
4505
|
async query(sql, params = []) {
|
|
4072
4506
|
const result = await db.query(sql, params);
|
|
4073
4507
|
return result.rows;
|
|
@@ -4116,6 +4550,7 @@ async function createTestApp(options = {}) {
|
|
|
4116
4550
|
messages.clear();
|
|
4117
4551
|
edge.clear();
|
|
4118
4552
|
starlens.clear();
|
|
4553
|
+
schedules.clear();
|
|
4119
4554
|
},
|
|
4120
4555
|
async close() {
|
|
4121
4556
|
state.db = null;
|
|
@@ -4131,6 +4566,7 @@ async function createTestApp(options = {}) {
|
|
|
4131
4566
|
messages.clear();
|
|
4132
4567
|
edge.clear();
|
|
4133
4568
|
starlens.clear();
|
|
4569
|
+
schedules.clear();
|
|
4134
4570
|
uninstallFetchRouter();
|
|
4135
4571
|
await db.close();
|
|
4136
4572
|
}
|