@stardeck-customer-apps/testing 0.26.0 → 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 +33 -1
- package/dist/index.d.mts +81 -2
- package/dist/index.d.ts +81 -2
- package/dist/index.js +415 -16
- package/dist/index.mjs +415 -16
- package/dist/next/headers-shim.js +5 -0
- package/dist/next/headers-shim.mjs +5 -0
- package/dist/setup.js +319 -15
- package/dist/setup.mjs +319 -15
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -357,6 +357,11 @@ var state = globalSingleton("state", () => ({
|
|
|
357
357
|
cardTokens: /* @__PURE__ */ new Map(),
|
|
358
358
|
cardTokenCounter: 0,
|
|
359
359
|
cardTokenizationEnabled: true,
|
|
360
|
+
stripePaymentIntents: /* @__PURE__ */ new Map(),
|
|
361
|
+
stripePaymentIntentCounter: 0,
|
|
362
|
+
stripeIdempotentIntents: /* @__PURE__ */ new Map(),
|
|
363
|
+
stripeAllowedMethods: ["card", "promptpay"],
|
|
364
|
+
stripeChargesEnabled: true,
|
|
360
365
|
uploads: [],
|
|
361
366
|
uploadCounter: 0,
|
|
362
367
|
storageFiles: /* @__PURE__ */ new Map(),
|
|
@@ -823,8 +828,8 @@ async function handleNeonSql(db, request) {
|
|
|
823
828
|
rowAsArray: arrayMode
|
|
824
829
|
});
|
|
825
830
|
} catch (error) {
|
|
826
|
-
const
|
|
827
|
-
return json({ message:
|
|
831
|
+
const err2 = error;
|
|
832
|
+
return json({ message: err2.message, code: err2.code }, 400);
|
|
828
833
|
}
|
|
829
834
|
}
|
|
830
835
|
|
|
@@ -2117,6 +2122,348 @@ function decodeNextRedirect(error) {
|
|
|
2117
2122
|
return new Response(null, { status, headers: { location } });
|
|
2118
2123
|
}
|
|
2119
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
|
+
|
|
2120
2467
|
// src/simulator/webhook-signing.ts
|
|
2121
2468
|
var import_node_crypto5 = __toESM(require("crypto"));
|
|
2122
2469
|
function signEventDelivery(secret, context, rawBody) {
|
|
@@ -2150,7 +2497,7 @@ var BEAM_BOLT_PAYMENT_METHODS = [
|
|
|
2150
2497
|
"SPAY_LATER"
|
|
2151
2498
|
];
|
|
2152
2499
|
var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
|
|
2153
|
-
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;
|
|
2154
2501
|
function payErr(error, status = 400, code, details) {
|
|
2155
2502
|
const body = { error };
|
|
2156
2503
|
if (code) body.code = code;
|
|
@@ -2176,7 +2523,23 @@ function paymentOperationForRequest(request, pathname) {
|
|
|
2176
2523
|
if (request.method === "GET" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout-sessions\/[^/]+$/.test(pathname)) {
|
|
2177
2524
|
return "stripe.getCheckoutSessionStatus";
|
|
2178
2525
|
}
|
|
2179
|
-
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;
|
|
2180
2543
|
}
|
|
2181
2544
|
function consumePaymentFailure(request, pathname) {
|
|
2182
2545
|
const operation = paymentOperationForRequest(request, pathname);
|
|
@@ -2206,11 +2569,11 @@ function isBoltPaymentMethod(value) {
|
|
|
2206
2569
|
function isBoltIntentStatus(value) {
|
|
2207
2570
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
2208
2571
|
}
|
|
2209
|
-
function
|
|
2210
|
-
if (Array.isArray(value)) return `[${value.map(
|
|
2572
|
+
function stableStringify2(value) {
|
|
2573
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify2).join(",")}]`;
|
|
2211
2574
|
if (value && typeof value === "object") {
|
|
2212
2575
|
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2213
|
-
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${
|
|
2576
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify2(v)}`).join(",")}}`;
|
|
2214
2577
|
}
|
|
2215
2578
|
return JSON.stringify(value);
|
|
2216
2579
|
}
|
|
@@ -2351,10 +2714,16 @@ async function deliverEvent(envelope, handler, options) {
|
|
|
2351
2714
|
body: envelope
|
|
2352
2715
|
});
|
|
2353
2716
|
}
|
|
2354
|
-
async function handlePaymentsRequest(request, url) {
|
|
2717
|
+
async function handlePaymentsRequest(request, url, signedProjectId = null) {
|
|
2355
2718
|
const pathname = url.pathname;
|
|
2356
2719
|
const injectedFailure = consumePaymentFailure(request, pathname);
|
|
2357
2720
|
if (injectedFailure) return injectedFailure;
|
|
2721
|
+
const inlineStripeResponse = await handleStripePaymentIntentRequest(
|
|
2722
|
+
request,
|
|
2723
|
+
url,
|
|
2724
|
+
signedProjectId
|
|
2725
|
+
);
|
|
2726
|
+
if (inlineStripeResponse) return inlineStripeResponse;
|
|
2358
2727
|
const directChargeResponse = await handleDirectChargeRequest(request, pathname);
|
|
2359
2728
|
if (directChargeResponse) return directChargeResponse;
|
|
2360
2729
|
if (/\/billing-portal$/.test(pathname)) {
|
|
@@ -2453,7 +2822,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2453
2822
|
code: "too_small"
|
|
2454
2823
|
});
|
|
2455
2824
|
}
|
|
2456
|
-
if (typeof body.deploymentId !== "string" || !
|
|
2825
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE3.test(body.deploymentId)) {
|
|
2457
2826
|
issues.push({
|
|
2458
2827
|
path: ["deploymentId"],
|
|
2459
2828
|
message: "Invalid uuid",
|
|
@@ -2637,7 +3006,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2637
3006
|
const comparable = { ...body };
|
|
2638
3007
|
delete comparable.deploymentId;
|
|
2639
3008
|
delete comparable.idempotencyKey;
|
|
2640
|
-
const fingerprint =
|
|
3009
|
+
const fingerprint = stableStringify2(comparable);
|
|
2641
3010
|
if (scopedKey) {
|
|
2642
3011
|
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
2643
3012
|
if (saved) {
|
|
@@ -2712,6 +3081,10 @@ function createPayments() {
|
|
|
2712
3081
|
state.paymentFailures.push({ operation, failure: failure2 });
|
|
2713
3082
|
},
|
|
2714
3083
|
markPaid(id) {
|
|
3084
|
+
if (state.stripePaymentIntents.has(id)) {
|
|
3085
|
+
succeedStripePaymentIntent(id);
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
2715
3088
|
const session = state.sessionStatuses.get(id);
|
|
2716
3089
|
if (session) {
|
|
2717
3090
|
session.status = "complete";
|
|
@@ -2739,7 +3112,26 @@ function createPayments() {
|
|
|
2739
3112
|
}
|
|
2740
3113
|
link.status = status;
|
|
2741
3114
|
},
|
|
2742
|
-
|
|
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;
|
|
2743
3135
|
const envelope = {
|
|
2744
3136
|
id: `evt_test_${import_node_crypto6.default.randomUUID()}`,
|
|
2745
3137
|
kind: "stripe_webhook",
|
|
@@ -2855,6 +3247,11 @@ function createPayments() {
|
|
|
2855
3247
|
state.cardTokens.clear();
|
|
2856
3248
|
state.cardTokenCounter = 0;
|
|
2857
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;
|
|
2858
3255
|
},
|
|
2859
3256
|
get count() {
|
|
2860
3257
|
return state.checkouts.length;
|
|
@@ -3809,7 +4206,7 @@ function requiresDeploymentHmac(request, url) {
|
|
|
3809
4206
|
return !isPresignedPut;
|
|
3810
4207
|
}
|
|
3811
4208
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
3812
|
-
const
|
|
4209
|
+
const isEmail2 = url.pathname === "/api/email/send";
|
|
3813
4210
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
3814
4211
|
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
3815
4212
|
const messagingMatch = url.pathname.match(
|
|
@@ -3818,7 +4215,7 @@ function requiresDeploymentHmac(request, url) {
|
|
|
3818
4215
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
3819
4216
|
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
3820
4217
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
3821
|
-
return !!(isStarlens || scheduleWakeMatch || dataStoreMatch ||
|
|
4218
|
+
return !!(isStarlens || scheduleWakeMatch || dataStoreMatch || isEmail2 || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
3822
4219
|
}
|
|
3823
4220
|
var SCHEDULE_WAKE_PATH = /^\/api\/deployments\/[^/]+\/schedules\/([^/]+)\/wake$/;
|
|
3824
4221
|
function deploymentIdFromPath(url) {
|
|
@@ -3841,7 +4238,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3841
4238
|
return authMatch[1].endsWith("verify") ? handleAuthVerify(request) : handleAuthRefresh(request);
|
|
3842
4239
|
}
|
|
3843
4240
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
3844
|
-
const
|
|
4241
|
+
const isEmail2 = url.pathname === "/api/email/send";
|
|
3845
4242
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
3846
4243
|
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
3847
4244
|
const messagingMatch = url.pathname.match(
|
|
@@ -3850,6 +4247,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3850
4247
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
3851
4248
|
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
3852
4249
|
let signedDeploymentId = null;
|
|
4250
|
+
let signedProjectId = null;
|
|
3853
4251
|
if (requiresDeploymentHmac(request, url)) {
|
|
3854
4252
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
3855
4253
|
if (!authHeader) {
|
|
@@ -3862,6 +4260,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3862
4260
|
return failure("Invalid authentication", 401);
|
|
3863
4261
|
}
|
|
3864
4262
|
signedDeploymentId = authPayload.deploymentId;
|
|
4263
|
+
signedProjectId = authPayload.projectId;
|
|
3865
4264
|
}
|
|
3866
4265
|
if (isStorageHost) {
|
|
3867
4266
|
return handleStorageRequest(request, url);
|
|
@@ -3869,14 +4268,14 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3869
4268
|
if (url.hostname === SIM_BEAM_HOST) {
|
|
3870
4269
|
return handleCardTokenRequest(request, url);
|
|
3871
4270
|
}
|
|
3872
|
-
if (
|
|
4271
|
+
if (isEmail2 && request.method === "POST") {
|
|
3873
4272
|
return handleEmailSend(request);
|
|
3874
4273
|
}
|
|
3875
4274
|
if (identitiesMatch) {
|
|
3876
4275
|
return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
|
|
3877
4276
|
}
|
|
3878
4277
|
if (storeMatch) {
|
|
3879
|
-
return handlePaymentsRequest(request, url);
|
|
4278
|
+
return handlePaymentsRequest(request, url, signedProjectId);
|
|
3880
4279
|
}
|
|
3881
4280
|
if (messagingMatch) {
|
|
3882
4281
|
const channel = messagingMatch[1];
|