@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.mjs
CHANGED
|
@@ -318,6 +318,11 @@ var state = globalSingleton("state", () => ({
|
|
|
318
318
|
cardTokens: /* @__PURE__ */ new Map(),
|
|
319
319
|
cardTokenCounter: 0,
|
|
320
320
|
cardTokenizationEnabled: true,
|
|
321
|
+
stripePaymentIntents: /* @__PURE__ */ new Map(),
|
|
322
|
+
stripePaymentIntentCounter: 0,
|
|
323
|
+
stripeIdempotentIntents: /* @__PURE__ */ new Map(),
|
|
324
|
+
stripeAllowedMethods: ["card", "promptpay"],
|
|
325
|
+
stripeChargesEnabled: true,
|
|
321
326
|
uploads: [],
|
|
322
327
|
uploadCounter: 0,
|
|
323
328
|
storageFiles: /* @__PURE__ */ new Map(),
|
|
@@ -784,8 +789,8 @@ async function handleNeonSql(db, request) {
|
|
|
784
789
|
rowAsArray: arrayMode
|
|
785
790
|
});
|
|
786
791
|
} catch (error) {
|
|
787
|
-
const
|
|
788
|
-
return json({ message:
|
|
792
|
+
const err2 = error;
|
|
793
|
+
return json({ message: err2.message, code: err2.code }, 400);
|
|
789
794
|
}
|
|
790
795
|
}
|
|
791
796
|
|
|
@@ -2078,6 +2083,348 @@ function decodeNextRedirect(error) {
|
|
|
2078
2083
|
return new Response(null, { status, headers: { location } });
|
|
2079
2084
|
}
|
|
2080
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
|
+
|
|
2081
2428
|
// src/simulator/webhook-signing.ts
|
|
2082
2429
|
import crypto4 from "crypto";
|
|
2083
2430
|
function signEventDelivery(secret, context, rawBody) {
|
|
@@ -2111,7 +2458,7 @@ var BEAM_BOLT_PAYMENT_METHODS = [
|
|
|
2111
2458
|
"SPAY_LATER"
|
|
2112
2459
|
];
|
|
2113
2460
|
var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
|
|
2114
|
-
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;
|
|
2115
2462
|
function payErr(error, status = 400, code, details) {
|
|
2116
2463
|
const body = { error };
|
|
2117
2464
|
if (code) body.code = code;
|
|
@@ -2137,7 +2484,23 @@ function paymentOperationForRequest(request, pathname) {
|
|
|
2137
2484
|
if (request.method === "GET" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout-sessions\/[^/]+$/.test(pathname)) {
|
|
2138
2485
|
return "stripe.getCheckoutSessionStatus";
|
|
2139
2486
|
}
|
|
2140
|
-
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;
|
|
2141
2504
|
}
|
|
2142
2505
|
function consumePaymentFailure(request, pathname) {
|
|
2143
2506
|
const operation = paymentOperationForRequest(request, pathname);
|
|
@@ -2167,11 +2530,11 @@ function isBoltPaymentMethod(value) {
|
|
|
2167
2530
|
function isBoltIntentStatus(value) {
|
|
2168
2531
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
2169
2532
|
}
|
|
2170
|
-
function
|
|
2171
|
-
if (Array.isArray(value)) return `[${value.map(
|
|
2533
|
+
function stableStringify2(value) {
|
|
2534
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify2).join(",")}]`;
|
|
2172
2535
|
if (value && typeof value === "object") {
|
|
2173
2536
|
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2174
|
-
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${
|
|
2537
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify2(v)}`).join(",")}}`;
|
|
2175
2538
|
}
|
|
2176
2539
|
return JSON.stringify(value);
|
|
2177
2540
|
}
|
|
@@ -2312,10 +2675,16 @@ async function deliverEvent(envelope, handler, options) {
|
|
|
2312
2675
|
body: envelope
|
|
2313
2676
|
});
|
|
2314
2677
|
}
|
|
2315
|
-
async function handlePaymentsRequest(request, url) {
|
|
2678
|
+
async function handlePaymentsRequest(request, url, signedProjectId = null) {
|
|
2316
2679
|
const pathname = url.pathname;
|
|
2317
2680
|
const injectedFailure = consumePaymentFailure(request, pathname);
|
|
2318
2681
|
if (injectedFailure) return injectedFailure;
|
|
2682
|
+
const inlineStripeResponse = await handleStripePaymentIntentRequest(
|
|
2683
|
+
request,
|
|
2684
|
+
url,
|
|
2685
|
+
signedProjectId
|
|
2686
|
+
);
|
|
2687
|
+
if (inlineStripeResponse) return inlineStripeResponse;
|
|
2319
2688
|
const directChargeResponse = await handleDirectChargeRequest(request, pathname);
|
|
2320
2689
|
if (directChargeResponse) return directChargeResponse;
|
|
2321
2690
|
if (/\/billing-portal$/.test(pathname)) {
|
|
@@ -2414,7 +2783,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2414
2783
|
code: "too_small"
|
|
2415
2784
|
});
|
|
2416
2785
|
}
|
|
2417
|
-
if (typeof body.deploymentId !== "string" || !
|
|
2786
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE3.test(body.deploymentId)) {
|
|
2418
2787
|
issues.push({
|
|
2419
2788
|
path: ["deploymentId"],
|
|
2420
2789
|
message: "Invalid uuid",
|
|
@@ -2598,7 +2967,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2598
2967
|
const comparable = { ...body };
|
|
2599
2968
|
delete comparable.deploymentId;
|
|
2600
2969
|
delete comparable.idempotencyKey;
|
|
2601
|
-
const fingerprint =
|
|
2970
|
+
const fingerprint = stableStringify2(comparable);
|
|
2602
2971
|
if (scopedKey) {
|
|
2603
2972
|
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
2604
2973
|
if (saved) {
|
|
@@ -2673,6 +3042,10 @@ function createPayments() {
|
|
|
2673
3042
|
state.paymentFailures.push({ operation, failure: failure2 });
|
|
2674
3043
|
},
|
|
2675
3044
|
markPaid(id) {
|
|
3045
|
+
if (state.stripePaymentIntents.has(id)) {
|
|
3046
|
+
succeedStripePaymentIntent(id);
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
2676
3049
|
const session = state.sessionStatuses.get(id);
|
|
2677
3050
|
if (session) {
|
|
2678
3051
|
session.status = "complete";
|
|
@@ -2700,7 +3073,26 @@ function createPayments() {
|
|
|
2700
3073
|
}
|
|
2701
3074
|
link.status = status;
|
|
2702
3075
|
},
|
|
2703
|
-
|
|
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;
|
|
2704
3096
|
const envelope = {
|
|
2705
3097
|
id: `evt_test_${crypto5.randomUUID()}`,
|
|
2706
3098
|
kind: "stripe_webhook",
|
|
@@ -2816,6 +3208,11 @@ function createPayments() {
|
|
|
2816
3208
|
state.cardTokens.clear();
|
|
2817
3209
|
state.cardTokenCounter = 0;
|
|
2818
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;
|
|
2819
3216
|
},
|
|
2820
3217
|
get count() {
|
|
2821
3218
|
return state.checkouts.length;
|
|
@@ -3770,7 +4167,7 @@ function requiresDeploymentHmac(request, url) {
|
|
|
3770
4167
|
return !isPresignedPut;
|
|
3771
4168
|
}
|
|
3772
4169
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
3773
|
-
const
|
|
4170
|
+
const isEmail2 = url.pathname === "/api/email/send";
|
|
3774
4171
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
3775
4172
|
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
3776
4173
|
const messagingMatch = url.pathname.match(
|
|
@@ -3779,7 +4176,7 @@ function requiresDeploymentHmac(request, url) {
|
|
|
3779
4176
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
3780
4177
|
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
3781
4178
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
3782
|
-
return !!(isStarlens || scheduleWakeMatch || dataStoreMatch ||
|
|
4179
|
+
return !!(isStarlens || scheduleWakeMatch || dataStoreMatch || isEmail2 || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
3783
4180
|
}
|
|
3784
4181
|
var SCHEDULE_WAKE_PATH = /^\/api\/deployments\/[^/]+\/schedules\/([^/]+)\/wake$/;
|
|
3785
4182
|
function deploymentIdFromPath(url) {
|
|
@@ -3802,7 +4199,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3802
4199
|
return authMatch[1].endsWith("verify") ? handleAuthVerify(request) : handleAuthRefresh(request);
|
|
3803
4200
|
}
|
|
3804
4201
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
3805
|
-
const
|
|
4202
|
+
const isEmail2 = url.pathname === "/api/email/send";
|
|
3806
4203
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
3807
4204
|
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
3808
4205
|
const messagingMatch = url.pathname.match(
|
|
@@ -3811,6 +4208,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3811
4208
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
3812
4209
|
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
3813
4210
|
let signedDeploymentId = null;
|
|
4211
|
+
let signedProjectId = null;
|
|
3814
4212
|
if (requiresDeploymentHmac(request, url)) {
|
|
3815
4213
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
3816
4214
|
if (!authHeader) {
|
|
@@ -3823,6 +4221,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3823
4221
|
return failure("Invalid authentication", 401);
|
|
3824
4222
|
}
|
|
3825
4223
|
signedDeploymentId = authPayload.deploymentId;
|
|
4224
|
+
signedProjectId = authPayload.projectId;
|
|
3826
4225
|
}
|
|
3827
4226
|
if (isStorageHost) {
|
|
3828
4227
|
return handleStorageRequest(request, url);
|
|
@@ -3830,14 +4229,14 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3830
4229
|
if (url.hostname === SIM_BEAM_HOST) {
|
|
3831
4230
|
return handleCardTokenRequest(request, url);
|
|
3832
4231
|
}
|
|
3833
|
-
if (
|
|
4232
|
+
if (isEmail2 && request.method === "POST") {
|
|
3834
4233
|
return handleEmailSend(request);
|
|
3835
4234
|
}
|
|
3836
4235
|
if (identitiesMatch) {
|
|
3837
4236
|
return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
|
|
3838
4237
|
}
|
|
3839
4238
|
if (storeMatch) {
|
|
3840
|
-
return handlePaymentsRequest(request, url);
|
|
4239
|
+
return handlePaymentsRequest(request, url, signedProjectId);
|
|
3841
4240
|
}
|
|
3842
4241
|
if (messagingMatch) {
|
|
3843
4242
|
const channel = messagingMatch[1];
|
|
@@ -79,6 +79,11 @@ var state = globalSingleton("state", () => ({
|
|
|
79
79
|
cardTokens: /* @__PURE__ */ new Map(),
|
|
80
80
|
cardTokenCounter: 0,
|
|
81
81
|
cardTokenizationEnabled: true,
|
|
82
|
+
stripePaymentIntents: /* @__PURE__ */ new Map(),
|
|
83
|
+
stripePaymentIntentCounter: 0,
|
|
84
|
+
stripeIdempotentIntents: /* @__PURE__ */ new Map(),
|
|
85
|
+
stripeAllowedMethods: ["card", "promptpay"],
|
|
86
|
+
stripeChargesEnabled: true,
|
|
82
87
|
uploads: [],
|
|
83
88
|
uploadCounter: 0,
|
|
84
89
|
storageFiles: /* @__PURE__ */ new Map(),
|
|
@@ -52,6 +52,11 @@ var state = globalSingleton("state", () => ({
|
|
|
52
52
|
cardTokens: /* @__PURE__ */ new Map(),
|
|
53
53
|
cardTokenCounter: 0,
|
|
54
54
|
cardTokenizationEnabled: true,
|
|
55
|
+
stripePaymentIntents: /* @__PURE__ */ new Map(),
|
|
56
|
+
stripePaymentIntentCounter: 0,
|
|
57
|
+
stripeIdempotentIntents: /* @__PURE__ */ new Map(),
|
|
58
|
+
stripeAllowedMethods: ["card", "promptpay"],
|
|
59
|
+
stripeChargesEnabled: true,
|
|
55
60
|
uploads: [],
|
|
56
61
|
uploadCounter: 0,
|
|
57
62
|
storageFiles: /* @__PURE__ */ new Map(),
|