@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.mjs
CHANGED
|
@@ -299,6 +299,7 @@ var state = globalSingleton("state", () => ({
|
|
|
299
299
|
identitySuppressClaimDelivery: false,
|
|
300
300
|
identityAllowedReturnOrigins: /* @__PURE__ */ new Set([APP_TEST_ORIGIN]),
|
|
301
301
|
checkouts: [],
|
|
302
|
+
scheduleWakes: [],
|
|
302
303
|
checkoutCounter: 0,
|
|
303
304
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
304
305
|
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
@@ -317,6 +318,11 @@ var state = globalSingleton("state", () => ({
|
|
|
317
318
|
cardTokens: /* @__PURE__ */ new Map(),
|
|
318
319
|
cardTokenCounter: 0,
|
|
319
320
|
cardTokenizationEnabled: true,
|
|
321
|
+
stripePaymentIntents: /* @__PURE__ */ new Map(),
|
|
322
|
+
stripePaymentIntentCounter: 0,
|
|
323
|
+
stripeIdempotentIntents: /* @__PURE__ */ new Map(),
|
|
324
|
+
stripeAllowedMethods: ["card", "promptpay"],
|
|
325
|
+
stripeChargesEnabled: true,
|
|
320
326
|
uploads: [],
|
|
321
327
|
uploadCounter: 0,
|
|
322
328
|
storageFiles: /* @__PURE__ */ new Map(),
|
|
@@ -783,8 +789,8 @@ async function handleNeonSql(db, request) {
|
|
|
783
789
|
rowAsArray: arrayMode
|
|
784
790
|
});
|
|
785
791
|
} catch (error) {
|
|
786
|
-
const
|
|
787
|
-
return json({ message:
|
|
792
|
+
const err2 = error;
|
|
793
|
+
return json({ message: err2.message, code: err2.code }, 400);
|
|
788
794
|
}
|
|
789
795
|
}
|
|
790
796
|
|
|
@@ -2077,6 +2083,348 @@ function decodeNextRedirect(error) {
|
|
|
2077
2083
|
return new Response(null, { status, headers: { location } });
|
|
2078
2084
|
}
|
|
2079
2085
|
|
|
2086
|
+
// src/simulator/stripe-payment-intents.ts
|
|
2087
|
+
var SIM_STRIPE_PUBLISHABLE_KEY = "pk_test_sim_stripe";
|
|
2088
|
+
var SIM_STRIPE_DEFAULT_ACCOUNT = "acct_sim_default";
|
|
2089
|
+
var METHODS = ["card", "promptpay"];
|
|
2090
|
+
var RESERVED_METADATA_KEYS = ["projectId", "stardeckReferenceId", "stardeckStagingDeploymentId"];
|
|
2091
|
+
var PROMPTPAY_QR_TTL_SECONDS = 15 * 60;
|
|
2092
|
+
var PATH_RE = /^\/api\/store\/(?!beam\/)([^/]+)\/(payment-intents(?:\/([^/]+)(?:\/(cancel))?)?|elements-config|readiness)$/;
|
|
2093
|
+
function err(error, status, code) {
|
|
2094
|
+
return json({ error, code }, status);
|
|
2095
|
+
}
|
|
2096
|
+
function decode(segment) {
|
|
2097
|
+
try {
|
|
2098
|
+
return decodeURIComponent(segment);
|
|
2099
|
+
} catch {
|
|
2100
|
+
return segment;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
function isPlayground(account) {
|
|
2104
|
+
return account === "playground";
|
|
2105
|
+
}
|
|
2106
|
+
function resolveAccount(account) {
|
|
2107
|
+
if (isPlayground(account)) return null;
|
|
2108
|
+
if (account === "default") {
|
|
2109
|
+
return process.env.STRIPE_CONNECT_ACCOUNT_ID || SIM_STRIPE_DEFAULT_ACCOUNT;
|
|
2110
|
+
}
|
|
2111
|
+
return account;
|
|
2112
|
+
}
|
|
2113
|
+
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;
|
|
2114
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2115
|
+
var APP_METADATA_MAX_KEYS = 50 - RESERVED_METADATA_KEYS.length;
|
|
2116
|
+
function isStringRecord(value) {
|
|
2117
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
2118
|
+
}
|
|
2119
|
+
function isValidMetadata(value) {
|
|
2120
|
+
if (!isStringRecord(value)) return false;
|
|
2121
|
+
const entries = Object.entries(value);
|
|
2122
|
+
return entries.length <= APP_METADATA_MAX_KEYS && entries.every(([k, v]) => k.length >= 1 && k.length <= 40 && v.length <= 500);
|
|
2123
|
+
}
|
|
2124
|
+
function isOptional(value, test) {
|
|
2125
|
+
return value === void 0 || test(value);
|
|
2126
|
+
}
|
|
2127
|
+
var isEmail = (v) => typeof v === "string" && EMAIL_RE.test(v);
|
|
2128
|
+
function validateCreate(body) {
|
|
2129
|
+
const { amount, currency, method, referenceId, idempotencyKey } = body;
|
|
2130
|
+
const checks = [
|
|
2131
|
+
[
|
|
2132
|
+
typeof amount === "number" && Number.isInteger(amount) && amount > 0,
|
|
2133
|
+
"amount must be a positive integer in minor units"
|
|
2134
|
+
],
|
|
2135
|
+
[typeof currency === "string" && currency.length === 3, "currency must be a 3-letter ISO code"],
|
|
2136
|
+
[
|
|
2137
|
+
typeof method === "string" && METHODS.includes(method),
|
|
2138
|
+
`method must be one of: ${METHODS.join(", ")}`
|
|
2139
|
+
],
|
|
2140
|
+
[
|
|
2141
|
+
typeof referenceId === "string" && referenceId.length >= 1 && referenceId.length <= 200,
|
|
2142
|
+
"referenceId must be 1 to 200 characters"
|
|
2143
|
+
],
|
|
2144
|
+
[
|
|
2145
|
+
typeof idempotencyKey === "string" && idempotencyKey.length >= 1 && idempotencyKey.length <= 200,
|
|
2146
|
+
"idempotencyKey is required (1 to 200 characters)"
|
|
2147
|
+
],
|
|
2148
|
+
[
|
|
2149
|
+
isOptional(body.description, (v) => typeof v === "string" && v.length <= 1e3),
|
|
2150
|
+
"description must be at most 1000 characters"
|
|
2151
|
+
],
|
|
2152
|
+
[isOptional(body.receiptEmail, isEmail), "receiptEmail must be an email address"],
|
|
2153
|
+
[isOptional(body.billingEmail, isEmail), "billingEmail must be an email address"],
|
|
2154
|
+
[
|
|
2155
|
+
isOptional(body.returnUrl, (v) => typeof v === "string" && URL.canParse(v)),
|
|
2156
|
+
"returnUrl must be an absolute URL"
|
|
2157
|
+
],
|
|
2158
|
+
[
|
|
2159
|
+
isOptional(body.metadata, isValidMetadata),
|
|
2160
|
+
`metadata must map keys (1-40 chars) to strings (max 500), at most ${APP_METADATA_MAX_KEYS} keys`
|
|
2161
|
+
],
|
|
2162
|
+
[
|
|
2163
|
+
typeof body.deploymentId === "string" && UUID_RE2.test(body.deploymentId),
|
|
2164
|
+
"Invalid deployment ID"
|
|
2165
|
+
]
|
|
2166
|
+
];
|
|
2167
|
+
const failed = checks.find(([ok]) => !ok);
|
|
2168
|
+
return failed ? failed[1] : null;
|
|
2169
|
+
}
|
|
2170
|
+
function appMetadata(raw) {
|
|
2171
|
+
if (!isStringRecord(raw)) return {};
|
|
2172
|
+
const out = {};
|
|
2173
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2174
|
+
if (!RESERVED_METADATA_KEYS.includes(key)) out[key] = value;
|
|
2175
|
+
}
|
|
2176
|
+
return out;
|
|
2177
|
+
}
|
|
2178
|
+
function stableStringify(value) {
|
|
2179
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
2180
|
+
if (value && typeof value === "object") {
|
|
2181
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2182
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
2183
|
+
}
|
|
2184
|
+
return JSON.stringify(value);
|
|
2185
|
+
}
|
|
2186
|
+
function toStripePaymentIntentSummary(intent) {
|
|
2187
|
+
const open = intent.status !== "succeeded" && intent.status !== "canceled";
|
|
2188
|
+
let actionRequired = "NONE";
|
|
2189
|
+
if (open && intent.method === "card") actionRequired = "CLIENT_CONFIRM";
|
|
2190
|
+
if (open && intent.method === "promptpay" && intent.status === "requires_action") {
|
|
2191
|
+
actionRequired = "ENCODED_IMAGE";
|
|
2192
|
+
}
|
|
2193
|
+
return {
|
|
2194
|
+
id: intent.id,
|
|
2195
|
+
status: intent.status,
|
|
2196
|
+
amount: intent.amount,
|
|
2197
|
+
currency: intent.currency,
|
|
2198
|
+
method: intent.method,
|
|
2199
|
+
referenceId: intent.referenceId,
|
|
2200
|
+
metadata: { ...intent.appMetadata },
|
|
2201
|
+
actionRequired,
|
|
2202
|
+
clientSecret: intent.method === "card" ? intent.clientSecret : null,
|
|
2203
|
+
qr: actionRequired === "ENCODED_IMAGE" ? { ...intent.qr } : null,
|
|
2204
|
+
latestChargeId: intent.latestChargeId,
|
|
2205
|
+
amountRefunded: intent.amountRefunded,
|
|
2206
|
+
lastPaymentError: intent.lastPaymentError ? { ...intent.lastPaymentError } : null,
|
|
2207
|
+
created: intent.created,
|
|
2208
|
+
livemode: false,
|
|
2209
|
+
playground: intent.playground
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
function promptPayQr(id, created) {
|
|
2213
|
+
return {
|
|
2214
|
+
imageUrlPng: `https://qr.stripe.test/${id}.png`,
|
|
2215
|
+
imageUrlSvg: `https://qr.stripe.test/${id}.svg`,
|
|
2216
|
+
data: `00020101021230SIM${id}`,
|
|
2217
|
+
hostedInstructionsUrl: `https://payments.stripe.test/promptpay/${id}`,
|
|
2218
|
+
expiresAt: created + PROMPTPAY_QR_TTL_SECONDS
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
function createIntent(body, account, projectId) {
|
|
2222
|
+
const problem = validateCreate(body);
|
|
2223
|
+
if (problem) return err(problem, 400, "invalid_request");
|
|
2224
|
+
const method = body.method;
|
|
2225
|
+
const currency = body.currency.toLowerCase();
|
|
2226
|
+
if (!state.stripeAllowedMethods.includes(method)) {
|
|
2227
|
+
return err(
|
|
2228
|
+
`The ${method} payment method is turned off for this account`,
|
|
2229
|
+
422,
|
|
2230
|
+
"method_not_allowed"
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
if (method === "promptpay" && currency !== "thb") {
|
|
2234
|
+
return err("PromptPay only takes THB", 422, "currency_not_supported");
|
|
2235
|
+
}
|
|
2236
|
+
if (!state.stripeChargesEnabled) {
|
|
2237
|
+
return err("This Stripe account can't take payments yet", 503, "account_not_ready");
|
|
2238
|
+
}
|
|
2239
|
+
const scopedKey = JSON.stringify(["pi", projectId, account, body.idempotencyKey]);
|
|
2240
|
+
const comparable = { ...body };
|
|
2241
|
+
delete comparable.deploymentId;
|
|
2242
|
+
delete comparable.idempotencyKey;
|
|
2243
|
+
const fingerprint = stableStringify(comparable);
|
|
2244
|
+
const saved = state.stripeIdempotentIntents.get(scopedKey);
|
|
2245
|
+
if (saved) {
|
|
2246
|
+
if (saved.fingerprint !== fingerprint) {
|
|
2247
|
+
return err(
|
|
2248
|
+
"This idempotency key was already used with different parameters. Use a new key for a different payment.",
|
|
2249
|
+
409,
|
|
2250
|
+
"idempotency_conflict"
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
const existing = state.stripePaymentIntents.get(saved.id);
|
|
2254
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(existing) }, 201);
|
|
2255
|
+
}
|
|
2256
|
+
state.stripePaymentIntentCounter += 1;
|
|
2257
|
+
const id = `pi_sim_${state.stripePaymentIntentCounter}`;
|
|
2258
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
2259
|
+
const referenceId = body.referenceId;
|
|
2260
|
+
const metadata = appMetadata(body.metadata);
|
|
2261
|
+
const intent = {
|
|
2262
|
+
id,
|
|
2263
|
+
projectId,
|
|
2264
|
+
account,
|
|
2265
|
+
connectedAccountId: resolveAccount(account),
|
|
2266
|
+
playground: isPlayground(account),
|
|
2267
|
+
status: method === "card" ? "requires_payment_method" : "requires_action",
|
|
2268
|
+
amount: body.amount,
|
|
2269
|
+
currency,
|
|
2270
|
+
method,
|
|
2271
|
+
referenceId,
|
|
2272
|
+
appMetadata: metadata,
|
|
2273
|
+
metadata: { ...metadata, projectId, stardeckReferenceId: referenceId },
|
|
2274
|
+
description: typeof body.description === "string" ? body.description : null,
|
|
2275
|
+
receiptEmail: typeof body.receiptEmail === "string" ? body.receiptEmail : null,
|
|
2276
|
+
returnUrl: typeof body.returnUrl === "string" ? body.returnUrl : null,
|
|
2277
|
+
clientSecret: `${id}_secret_sim`,
|
|
2278
|
+
qr: method === "promptpay" ? promptPayQr(id, created) : null,
|
|
2279
|
+
latestChargeId: null,
|
|
2280
|
+
amountRefunded: 0,
|
|
2281
|
+
lastPaymentError: null,
|
|
2282
|
+
created
|
|
2283
|
+
};
|
|
2284
|
+
state.stripePaymentIntents.set(id, intent);
|
|
2285
|
+
state.stripeIdempotentIntents.set(scopedKey, { fingerprint, id });
|
|
2286
|
+
console.log(
|
|
2287
|
+
`[stardeck-testing] stripe payment intent ${id} created method=${method} amount=${intent.amount} ${currency}`
|
|
2288
|
+
);
|
|
2289
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) }, 201);
|
|
2290
|
+
}
|
|
2291
|
+
function ownedIntent(id, projectId) {
|
|
2292
|
+
const intent = state.stripePaymentIntents.get(id);
|
|
2293
|
+
return intent && intent.projectId === projectId ? intent : void 0;
|
|
2294
|
+
}
|
|
2295
|
+
function cancelIntent(intent) {
|
|
2296
|
+
if (intent.status === "succeeded") {
|
|
2297
|
+
return err("This payment already succeeded and can't be canceled", 409, "already_succeeded");
|
|
2298
|
+
}
|
|
2299
|
+
intent.status = "canceled";
|
|
2300
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) });
|
|
2301
|
+
}
|
|
2302
|
+
function handleIntents(request, url, account, intentId, action, projectId) {
|
|
2303
|
+
if (!intentId && request.method === "POST") {
|
|
2304
|
+
return readJsonBody(request).then((body) => createIntent(body, account, projectId));
|
|
2305
|
+
}
|
|
2306
|
+
if (!intentId && request.method === "GET") {
|
|
2307
|
+
const referenceId = url.searchParams.get("referenceId");
|
|
2308
|
+
if (!referenceId) return err("referenceId is required", 400, "invalid_request");
|
|
2309
|
+
const paymentIntents = [...state.stripePaymentIntents.values()].filter((i) => i.projectId === projectId && i.referenceId === referenceId).map(toStripePaymentIntentSummary);
|
|
2310
|
+
return json({ paymentIntents });
|
|
2311
|
+
}
|
|
2312
|
+
if (!intentId) return null;
|
|
2313
|
+
const intent = ownedIntent(decode(intentId), projectId);
|
|
2314
|
+
if (!action && request.method === "GET") {
|
|
2315
|
+
if (!intent) return err("Payment intent not found", 404, "not_found");
|
|
2316
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) });
|
|
2317
|
+
}
|
|
2318
|
+
if (action === "cancel" && request.method === "POST") {
|
|
2319
|
+
return readJsonBody(request).then((body) => {
|
|
2320
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE2.test(body.deploymentId)) {
|
|
2321
|
+
return err("deploymentId is required", 400, "invalid_request");
|
|
2322
|
+
}
|
|
2323
|
+
if (!intent) return err("Payment intent not found", 404, "not_found");
|
|
2324
|
+
return cancelIntent(intent);
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
return null;
|
|
2328
|
+
}
|
|
2329
|
+
async function handleStripePaymentIntentRequest(request, url, projectId) {
|
|
2330
|
+
const match = url.pathname.match(PATH_RE);
|
|
2331
|
+
if (!match) return null;
|
|
2332
|
+
if (!projectId) return err("Missing authentication", 401, "unauthorized");
|
|
2333
|
+
const account = decode(match[1]);
|
|
2334
|
+
const resource = match[2];
|
|
2335
|
+
if (request.method === "GET" && !url.searchParams.get("deploymentId")) {
|
|
2336
|
+
return err("deploymentId is required", 400, "invalid_request");
|
|
2337
|
+
}
|
|
2338
|
+
if (resource === "elements-config" && request.method === "GET") {
|
|
2339
|
+
return json({
|
|
2340
|
+
publishableKey: SIM_STRIPE_PUBLISHABLE_KEY,
|
|
2341
|
+
stripeAccount: resolveAccount(account),
|
|
2342
|
+
playground: isPlayground(account)
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
if (resource === "readiness" && request.method === "GET") {
|
|
2346
|
+
const methods = [...state.stripeAllowedMethods];
|
|
2347
|
+
return json({
|
|
2348
|
+
provider: "stripe",
|
|
2349
|
+
ready: state.stripeChargesEnabled && methods.length > 0,
|
|
2350
|
+
chargesEnabled: state.stripeChargesEnabled,
|
|
2351
|
+
methods,
|
|
2352
|
+
playground: isPlayground(account)
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
if (resource.startsWith("payment-intents")) {
|
|
2356
|
+
return handleIntents(request, url, account, match[3], match[4], projectId);
|
|
2357
|
+
}
|
|
2358
|
+
return null;
|
|
2359
|
+
}
|
|
2360
|
+
function requireIntent(id) {
|
|
2361
|
+
const intent = state.stripePaymentIntents.get(id);
|
|
2362
|
+
if (!intent) throw new Error(`[stardeck-testing] Unknown payment intent id: ${id}`);
|
|
2363
|
+
return intent;
|
|
2364
|
+
}
|
|
2365
|
+
function requireOpenIntent(id, verb) {
|
|
2366
|
+
const intent = requireIntent(id);
|
|
2367
|
+
if (intent.status === "succeeded" || intent.status === "canceled") {
|
|
2368
|
+
throw new Error(
|
|
2369
|
+
`[stardeck-testing] Can't ${verb} payment intent ${id}: it is ${intent.status}`
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
2372
|
+
return intent;
|
|
2373
|
+
}
|
|
2374
|
+
function succeedStripePaymentIntent(id) {
|
|
2375
|
+
const intent = requireOpenIntent(id, "mark paid");
|
|
2376
|
+
intent.status = "succeeded";
|
|
2377
|
+
intent.latestChargeId = `ch_sim_${id}`;
|
|
2378
|
+
intent.lastPaymentError = null;
|
|
2379
|
+
}
|
|
2380
|
+
function failStripePaymentIntent(id, code) {
|
|
2381
|
+
const intent = requireOpenIntent(id, "fail");
|
|
2382
|
+
intent.status = "requires_payment_method";
|
|
2383
|
+
intent.latestChargeId = `ch_sim_${id}_failed`;
|
|
2384
|
+
intent.lastPaymentError = {
|
|
2385
|
+
code,
|
|
2386
|
+
declineCode: code === "card_declined" ? "generic_decline" : null,
|
|
2387
|
+
message: `Simulated payment failure: ${code}`
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
function cancelStripePaymentIntentById(id) {
|
|
2391
|
+
const intent = requireOpenIntent(id, "cancel");
|
|
2392
|
+
intent.status = "canceled";
|
|
2393
|
+
}
|
|
2394
|
+
function stripePaymentIntentEvent(id) {
|
|
2395
|
+
const intent = requireIntent(id);
|
|
2396
|
+
let type;
|
|
2397
|
+
if (intent.status === "succeeded") type = "payment_intent.succeeded";
|
|
2398
|
+
else if (intent.status === "canceled") type = "payment_intent.canceled";
|
|
2399
|
+
else if (intent.lastPaymentError) type = "payment_intent.payment_failed";
|
|
2400
|
+
else {
|
|
2401
|
+
throw new Error(
|
|
2402
|
+
`[stardeck-testing] Payment intent ${id} is ${intent.status}; call markPaid, failPayment or cancelPayment first`
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
const error = intent.lastPaymentError;
|
|
2406
|
+
return {
|
|
2407
|
+
type,
|
|
2408
|
+
accountId: intent.connectedAccountId ?? "acct_stardeck_playground",
|
|
2409
|
+
data: {
|
|
2410
|
+
id: intent.id,
|
|
2411
|
+
object: "payment_intent",
|
|
2412
|
+
amount: intent.amount,
|
|
2413
|
+
amount_received: intent.status === "succeeded" ? intent.amount : 0,
|
|
2414
|
+
currency: intent.currency,
|
|
2415
|
+
status: intent.status,
|
|
2416
|
+
metadata: { ...intent.metadata },
|
|
2417
|
+
payment_method_types: [intent.method],
|
|
2418
|
+
latest_charge: intent.latestChargeId,
|
|
2419
|
+
last_payment_error: error ? { code: error.code, decline_code: error.declineCode, message: error.message } : null,
|
|
2420
|
+
description: intent.description,
|
|
2421
|
+
receipt_email: intent.receiptEmail,
|
|
2422
|
+
created: intent.created,
|
|
2423
|
+
livemode: false
|
|
2424
|
+
}
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2080
2428
|
// src/simulator/webhook-signing.ts
|
|
2081
2429
|
import crypto4 from "crypto";
|
|
2082
2430
|
function signEventDelivery(secret, context, rawBody) {
|
|
@@ -2110,7 +2458,7 @@ var BEAM_BOLT_PAYMENT_METHODS = [
|
|
|
2110
2458
|
"SPAY_LATER"
|
|
2111
2459
|
];
|
|
2112
2460
|
var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
|
|
2113
|
-
var
|
|
2461
|
+
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;
|
|
2114
2462
|
function payErr(error, status = 400, code, details) {
|
|
2115
2463
|
const body = { error };
|
|
2116
2464
|
if (code) body.code = code;
|
|
@@ -2136,7 +2484,23 @@ function paymentOperationForRequest(request, pathname) {
|
|
|
2136
2484
|
if (request.method === "GET" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout-sessions\/[^/]+$/.test(pathname)) {
|
|
2137
2485
|
return "stripe.getCheckoutSessionStatus";
|
|
2138
2486
|
}
|
|
2139
|
-
return
|
|
2487
|
+
return stripeInlineOperation(request.method, pathname);
|
|
2488
|
+
}
|
|
2489
|
+
var STRIPE_INLINE_OPERATIONS = [
|
|
2490
|
+
["POST", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents$/, "stripe.createPaymentIntent"],
|
|
2491
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents$/, "stripe.findPaymentIntents"],
|
|
2492
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents\/[^/]+$/, "stripe.getPaymentIntent"],
|
|
2493
|
+
[
|
|
2494
|
+
"POST",
|
|
2495
|
+
/^\/api\/store\/(?!beam\/)[^/]+\/payment-intents\/[^/]+\/cancel$/,
|
|
2496
|
+
"stripe.cancelPaymentIntent"
|
|
2497
|
+
],
|
|
2498
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/elements-config$/, "stripe.getElementsConfig"],
|
|
2499
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/readiness$/, "stripe.getReadiness"]
|
|
2500
|
+
];
|
|
2501
|
+
function stripeInlineOperation(method, pathname) {
|
|
2502
|
+
const hit = STRIPE_INLINE_OPERATIONS.find(([m, re]) => m === method && re.test(pathname));
|
|
2503
|
+
return hit ? hit[2] : null;
|
|
2140
2504
|
}
|
|
2141
2505
|
function consumePaymentFailure(request, pathname) {
|
|
2142
2506
|
const operation = paymentOperationForRequest(request, pathname);
|
|
@@ -2166,11 +2530,11 @@ function isBoltPaymentMethod(value) {
|
|
|
2166
2530
|
function isBoltIntentStatus(value) {
|
|
2167
2531
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
2168
2532
|
}
|
|
2169
|
-
function
|
|
2170
|
-
if (Array.isArray(value)) return `[${value.map(
|
|
2533
|
+
function stableStringify2(value) {
|
|
2534
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify2).join(",")}]`;
|
|
2171
2535
|
if (value && typeof value === "object") {
|
|
2172
2536
|
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2173
|
-
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${
|
|
2537
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify2(v)}`).join(",")}}`;
|
|
2174
2538
|
}
|
|
2175
2539
|
return JSON.stringify(value);
|
|
2176
2540
|
}
|
|
@@ -2311,10 +2675,16 @@ async function deliverEvent(envelope, handler, options) {
|
|
|
2311
2675
|
body: envelope
|
|
2312
2676
|
});
|
|
2313
2677
|
}
|
|
2314
|
-
async function handlePaymentsRequest(request, url) {
|
|
2678
|
+
async function handlePaymentsRequest(request, url, signedProjectId = null) {
|
|
2315
2679
|
const pathname = url.pathname;
|
|
2316
2680
|
const injectedFailure = consumePaymentFailure(request, pathname);
|
|
2317
2681
|
if (injectedFailure) return injectedFailure;
|
|
2682
|
+
const inlineStripeResponse = await handleStripePaymentIntentRequest(
|
|
2683
|
+
request,
|
|
2684
|
+
url,
|
|
2685
|
+
signedProjectId
|
|
2686
|
+
);
|
|
2687
|
+
if (inlineStripeResponse) return inlineStripeResponse;
|
|
2318
2688
|
const directChargeResponse = await handleDirectChargeRequest(request, pathname);
|
|
2319
2689
|
if (directChargeResponse) return directChargeResponse;
|
|
2320
2690
|
if (/\/billing-portal$/.test(pathname)) {
|
|
@@ -2413,7 +2783,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2413
2783
|
code: "too_small"
|
|
2414
2784
|
});
|
|
2415
2785
|
}
|
|
2416
|
-
if (typeof body.deploymentId !== "string" || !
|
|
2786
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE3.test(body.deploymentId)) {
|
|
2417
2787
|
issues.push({
|
|
2418
2788
|
path: ["deploymentId"],
|
|
2419
2789
|
message: "Invalid uuid",
|
|
@@ -2597,7 +2967,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2597
2967
|
const comparable = { ...body };
|
|
2598
2968
|
delete comparable.deploymentId;
|
|
2599
2969
|
delete comparable.idempotencyKey;
|
|
2600
|
-
const fingerprint =
|
|
2970
|
+
const fingerprint = stableStringify2(comparable);
|
|
2601
2971
|
if (scopedKey) {
|
|
2602
2972
|
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
2603
2973
|
if (saved) {
|
|
@@ -2672,6 +3042,10 @@ function createPayments() {
|
|
|
2672
3042
|
state.paymentFailures.push({ operation, failure: failure2 });
|
|
2673
3043
|
},
|
|
2674
3044
|
markPaid(id) {
|
|
3045
|
+
if (state.stripePaymentIntents.has(id)) {
|
|
3046
|
+
succeedStripePaymentIntent(id);
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
2675
3049
|
const session = state.sessionStatuses.get(id);
|
|
2676
3050
|
if (session) {
|
|
2677
3051
|
session.status = "complete";
|
|
@@ -2699,7 +3073,26 @@ function createPayments() {
|
|
|
2699
3073
|
}
|
|
2700
3074
|
link.status = status;
|
|
2701
3075
|
},
|
|
2702
|
-
|
|
3076
|
+
failPayment(paymentIntentId, code = "card_declined") {
|
|
3077
|
+
failStripePaymentIntent(paymentIntentId, code);
|
|
3078
|
+
},
|
|
3079
|
+
cancelPayment(paymentIntentId) {
|
|
3080
|
+
cancelStripePaymentIntentById(paymentIntentId);
|
|
3081
|
+
},
|
|
3082
|
+
get stripePaymentIntents() {
|
|
3083
|
+
return [...state.stripePaymentIntents.values()];
|
|
3084
|
+
},
|
|
3085
|
+
getStripePaymentIntent(paymentIntentId) {
|
|
3086
|
+
return state.stripePaymentIntents.get(paymentIntentId);
|
|
3087
|
+
},
|
|
3088
|
+
setStripePaymentMethods(methods) {
|
|
3089
|
+
state.stripeAllowedMethods = [...methods];
|
|
3090
|
+
},
|
|
3091
|
+
setStripeChargesEnabled(enabled) {
|
|
3092
|
+
state.stripeChargesEnabled = enabled;
|
|
3093
|
+
},
|
|
3094
|
+
async deliverStripeEvent(handler, input, options) {
|
|
3095
|
+
const event = "paymentIntentId" in input ? stripePaymentIntentEvent(input.paymentIntentId) : input;
|
|
2703
3096
|
const envelope = {
|
|
2704
3097
|
id: `evt_test_${crypto5.randomUUID()}`,
|
|
2705
3098
|
kind: "stripe_webhook",
|
|
@@ -2815,6 +3208,11 @@ function createPayments() {
|
|
|
2815
3208
|
state.cardTokens.clear();
|
|
2816
3209
|
state.cardTokenCounter = 0;
|
|
2817
3210
|
state.cardTokenizationEnabled = true;
|
|
3211
|
+
state.stripePaymentIntents.clear();
|
|
3212
|
+
state.stripePaymentIntentCounter = 0;
|
|
3213
|
+
state.stripeIdempotentIntents.clear();
|
|
3214
|
+
state.stripeAllowedMethods = ["card", "promptpay"];
|
|
3215
|
+
state.stripeChargesEnabled = true;
|
|
2818
3216
|
},
|
|
2819
3217
|
get count() {
|
|
2820
3218
|
return state.checkouts.length;
|
|
@@ -3587,6 +3985,32 @@ function createEdge() {
|
|
|
3587
3985
|
};
|
|
3588
3986
|
}
|
|
3589
3987
|
|
|
3988
|
+
// src/simulator/schedules.ts
|
|
3989
|
+
async function handleScheduleWake(request, scheduleKey) {
|
|
3990
|
+
if (request.method !== "POST") return failure(`Method ${request.method} not allowed`, 405);
|
|
3991
|
+
const body = await readJsonBody(request);
|
|
3992
|
+
const at = typeof body.at === "string" ? new Date(body.at) : null;
|
|
3993
|
+
if (!at || Number.isNaN(at.getTime())) return failure("`at` must be an ISO date-time", 400);
|
|
3994
|
+
state.scheduleWakes.push({ scheduleKey, at, requestedAt: /* @__PURE__ */ new Date() });
|
|
3995
|
+
return success({ woken: true });
|
|
3996
|
+
}
|
|
3997
|
+
function createSchedules() {
|
|
3998
|
+
return {
|
|
3999
|
+
get wakes() {
|
|
4000
|
+
return state.scheduleWakes;
|
|
4001
|
+
},
|
|
4002
|
+
wakesFor(scheduleKey) {
|
|
4003
|
+
return state.scheduleWakes.filter((wake) => wake.scheduleKey === scheduleKey);
|
|
4004
|
+
},
|
|
4005
|
+
earliestWake(scheduleKey) {
|
|
4006
|
+
return this.wakesFor(scheduleKey).map((wake) => wake.at).sort((a, b) => a.getTime() - b.getTime())[0];
|
|
4007
|
+
},
|
|
4008
|
+
clear() {
|
|
4009
|
+
state.scheduleWakes = [];
|
|
4010
|
+
}
|
|
4011
|
+
};
|
|
4012
|
+
}
|
|
4013
|
+
|
|
3590
4014
|
// src/simulator/starlens.ts
|
|
3591
4015
|
var LATEST_VERSION = "v2";
|
|
3592
4016
|
function starlensFailure(error, status, code) {
|
|
@@ -3743,7 +4167,7 @@ function requiresDeploymentHmac(request, url) {
|
|
|
3743
4167
|
return !isPresignedPut;
|
|
3744
4168
|
}
|
|
3745
4169
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
3746
|
-
const
|
|
4170
|
+
const isEmail2 = url.pathname === "/api/email/send";
|
|
3747
4171
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
3748
4172
|
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
3749
4173
|
const messagingMatch = url.pathname.match(
|
|
@@ -3751,8 +4175,10 @@ function requiresDeploymentHmac(request, url) {
|
|
|
3751
4175
|
);
|
|
3752
4176
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
3753
4177
|
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
3754
|
-
|
|
4178
|
+
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
4179
|
+
return !!(isStarlens || scheduleWakeMatch || dataStoreMatch || isEmail2 || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
3755
4180
|
}
|
|
4181
|
+
var SCHEDULE_WAKE_PATH = /^\/api\/deployments\/[^/]+\/schedules\/([^/]+)\/wake$/;
|
|
3756
4182
|
function deploymentIdFromPath(url) {
|
|
3757
4183
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
3758
4184
|
if (!match) return null;
|
|
@@ -3773,7 +4199,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3773
4199
|
return authMatch[1].endsWith("verify") ? handleAuthVerify(request) : handleAuthRefresh(request);
|
|
3774
4200
|
}
|
|
3775
4201
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
3776
|
-
const
|
|
4202
|
+
const isEmail2 = url.pathname === "/api/email/send";
|
|
3777
4203
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
3778
4204
|
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
3779
4205
|
const messagingMatch = url.pathname.match(
|
|
@@ -3782,6 +4208,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3782
4208
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
3783
4209
|
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
3784
4210
|
let signedDeploymentId = null;
|
|
4211
|
+
let signedProjectId = null;
|
|
3785
4212
|
if (requiresDeploymentHmac(request, url)) {
|
|
3786
4213
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
3787
4214
|
if (!authHeader) {
|
|
@@ -3794,6 +4221,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3794
4221
|
return failure("Invalid authentication", 401);
|
|
3795
4222
|
}
|
|
3796
4223
|
signedDeploymentId = authPayload.deploymentId;
|
|
4224
|
+
signedProjectId = authPayload.projectId;
|
|
3797
4225
|
}
|
|
3798
4226
|
if (isStorageHost) {
|
|
3799
4227
|
return handleStorageRequest(request, url);
|
|
@@ -3801,14 +4229,14 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3801
4229
|
if (url.hostname === SIM_BEAM_HOST) {
|
|
3802
4230
|
return handleCardTokenRequest(request, url);
|
|
3803
4231
|
}
|
|
3804
|
-
if (
|
|
4232
|
+
if (isEmail2 && request.method === "POST") {
|
|
3805
4233
|
return handleEmailSend(request);
|
|
3806
4234
|
}
|
|
3807
4235
|
if (identitiesMatch) {
|
|
3808
4236
|
return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
|
|
3809
4237
|
}
|
|
3810
4238
|
if (storeMatch) {
|
|
3811
|
-
return handlePaymentsRequest(request, url);
|
|
4239
|
+
return handlePaymentsRequest(request, url, signedProjectId);
|
|
3812
4240
|
}
|
|
3813
4241
|
if (messagingMatch) {
|
|
3814
4242
|
const channel = messagingMatch[1];
|
|
@@ -3817,6 +4245,10 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3817
4245
|
if (edgeMatch) {
|
|
3818
4246
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
3819
4247
|
}
|
|
4248
|
+
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
4249
|
+
if (scheduleWakeMatch) {
|
|
4250
|
+
return handleScheduleWake(request, decodeURIComponent(scheduleWakeMatch[1]));
|
|
4251
|
+
}
|
|
3820
4252
|
if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
|
|
3821
4253
|
return handleStarlensExtract(request, signedDeploymentId);
|
|
3822
4254
|
}
|
|
@@ -3841,7 +4273,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3841
4273
|
}
|
|
3842
4274
|
}
|
|
3843
4275
|
return failure(
|
|
3844
|
-
`[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.`,
|
|
4276
|
+
`[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.`,
|
|
3845
4277
|
404
|
|
3846
4278
|
);
|
|
3847
4279
|
}
|
|
@@ -4020,6 +4452,7 @@ async function createTestApp(options = {}) {
|
|
|
4020
4452
|
const messages = createMessages();
|
|
4021
4453
|
const edge = createEdge();
|
|
4022
4454
|
const starlens = createStarlens();
|
|
4455
|
+
const schedules = createSchedules();
|
|
4023
4456
|
const app = {
|
|
4024
4457
|
db,
|
|
4025
4458
|
inbox,
|
|
@@ -4029,6 +4462,7 @@ async function createTestApp(options = {}) {
|
|
|
4029
4462
|
messages,
|
|
4030
4463
|
edge,
|
|
4031
4464
|
starlens,
|
|
4465
|
+
schedules,
|
|
4032
4466
|
async query(sql, params = []) {
|
|
4033
4467
|
const result = await db.query(sql, params);
|
|
4034
4468
|
return result.rows;
|
|
@@ -4077,6 +4511,7 @@ async function createTestApp(options = {}) {
|
|
|
4077
4511
|
messages.clear();
|
|
4078
4512
|
edge.clear();
|
|
4079
4513
|
starlens.clear();
|
|
4514
|
+
schedules.clear();
|
|
4080
4515
|
},
|
|
4081
4516
|
async close() {
|
|
4082
4517
|
state.db = null;
|
|
@@ -4092,6 +4527,7 @@ async function createTestApp(options = {}) {
|
|
|
4092
4527
|
messages.clear();
|
|
4093
4528
|
edge.clear();
|
|
4094
4529
|
starlens.clear();
|
|
4530
|
+
schedules.clear();
|
|
4095
4531
|
uninstallFetchRouter();
|
|
4096
4532
|
await db.close();
|
|
4097
4533
|
}
|
|
@@ -60,6 +60,7 @@ var state = globalSingleton("state", () => ({
|
|
|
60
60
|
identitySuppressClaimDelivery: false,
|
|
61
61
|
identityAllowedReturnOrigins: /* @__PURE__ */ new Set([APP_TEST_ORIGIN]),
|
|
62
62
|
checkouts: [],
|
|
63
|
+
scheduleWakes: [],
|
|
63
64
|
checkoutCounter: 0,
|
|
64
65
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
65
66
|
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
@@ -78,6 +79,11 @@ var state = globalSingleton("state", () => ({
|
|
|
78
79
|
cardTokens: /* @__PURE__ */ new Map(),
|
|
79
80
|
cardTokenCounter: 0,
|
|
80
81
|
cardTokenizationEnabled: true,
|
|
82
|
+
stripePaymentIntents: /* @__PURE__ */ new Map(),
|
|
83
|
+
stripePaymentIntentCounter: 0,
|
|
84
|
+
stripeIdempotentIntents: /* @__PURE__ */ new Map(),
|
|
85
|
+
stripeAllowedMethods: ["card", "promptpay"],
|
|
86
|
+
stripeChargesEnabled: true,
|
|
81
87
|
uploads: [],
|
|
82
88
|
uploadCounter: 0,
|
|
83
89
|
storageFiles: /* @__PURE__ */ new Map(),
|