@stardeck-customer-apps/testing 0.26.0 → 0.28.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 +51 -1
- package/dist/index.d.mts +117 -2
- package/dist/index.d.ts +117 -2
- package/dist/index.js +569 -17
- package/dist/index.mjs +569 -17
- package/dist/next/headers-shim.js +13 -0
- package/dist/next/headers-shim.mjs +13 -0
- package/dist/setup.js +418 -16
- package/dist/setup.mjs +418 -16
- package/package.json +10 -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(),
|
|
@@ -376,6 +381,14 @@ var state = globalSingleton("state", () => ({
|
|
|
376
381
|
starlensCalls: [],
|
|
377
382
|
starlensReplies: [],
|
|
378
383
|
starlensDefaultReply: null,
|
|
384
|
+
controls: {
|
|
385
|
+
seed: {},
|
|
386
|
+
values: {},
|
|
387
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
388
|
+
lastUsage: null,
|
|
389
|
+
writes: [],
|
|
390
|
+
refusal: null
|
|
391
|
+
},
|
|
379
392
|
allowNetwork: false
|
|
380
393
|
}));
|
|
381
394
|
function requireDb() {
|
|
@@ -823,8 +836,8 @@ async function handleNeonSql(db, request) {
|
|
|
823
836
|
rowAsArray: arrayMode
|
|
824
837
|
});
|
|
825
838
|
} catch (error) {
|
|
826
|
-
const
|
|
827
|
-
return json({ message:
|
|
839
|
+
const err2 = error;
|
|
840
|
+
return json({ message: err2.message, code: err2.code }, 400);
|
|
828
841
|
}
|
|
829
842
|
}
|
|
830
843
|
|
|
@@ -2117,6 +2130,348 @@ function decodeNextRedirect(error) {
|
|
|
2117
2130
|
return new Response(null, { status, headers: { location } });
|
|
2118
2131
|
}
|
|
2119
2132
|
|
|
2133
|
+
// src/simulator/stripe-payment-intents.ts
|
|
2134
|
+
var SIM_STRIPE_PUBLISHABLE_KEY = "pk_test_sim_stripe";
|
|
2135
|
+
var SIM_STRIPE_DEFAULT_ACCOUNT = "acct_sim_default";
|
|
2136
|
+
var METHODS = ["card", "promptpay"];
|
|
2137
|
+
var RESERVED_METADATA_KEYS = ["projectId", "stardeckReferenceId", "stardeckStagingDeploymentId"];
|
|
2138
|
+
var PROMPTPAY_QR_TTL_SECONDS = 15 * 60;
|
|
2139
|
+
var PATH_RE = /^\/api\/store\/(?!beam\/)([^/]+)\/(payment-intents(?:\/([^/]+)(?:\/(cancel))?)?|elements-config|readiness)$/;
|
|
2140
|
+
function err(error, status, code) {
|
|
2141
|
+
return json({ error, code }, status);
|
|
2142
|
+
}
|
|
2143
|
+
function decode(segment) {
|
|
2144
|
+
try {
|
|
2145
|
+
return decodeURIComponent(segment);
|
|
2146
|
+
} catch {
|
|
2147
|
+
return segment;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
function isPlayground(account) {
|
|
2151
|
+
return account === "playground";
|
|
2152
|
+
}
|
|
2153
|
+
function resolveAccount(account) {
|
|
2154
|
+
if (isPlayground(account)) return null;
|
|
2155
|
+
if (account === "default") {
|
|
2156
|
+
return process.env.STRIPE_CONNECT_ACCOUNT_ID || SIM_STRIPE_DEFAULT_ACCOUNT;
|
|
2157
|
+
}
|
|
2158
|
+
return account;
|
|
2159
|
+
}
|
|
2160
|
+
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;
|
|
2161
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2162
|
+
var APP_METADATA_MAX_KEYS = 50 - RESERVED_METADATA_KEYS.length;
|
|
2163
|
+
function isStringRecord(value) {
|
|
2164
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
2165
|
+
}
|
|
2166
|
+
function isValidMetadata(value) {
|
|
2167
|
+
if (!isStringRecord(value)) return false;
|
|
2168
|
+
const entries = Object.entries(value);
|
|
2169
|
+
return entries.length <= APP_METADATA_MAX_KEYS && entries.every(([k, v]) => k.length >= 1 && k.length <= 40 && v.length <= 500);
|
|
2170
|
+
}
|
|
2171
|
+
function isOptional(value, test) {
|
|
2172
|
+
return value === void 0 || test(value);
|
|
2173
|
+
}
|
|
2174
|
+
var isEmail = (v) => typeof v === "string" && EMAIL_RE.test(v);
|
|
2175
|
+
function validateCreate(body) {
|
|
2176
|
+
const { amount, currency, method, referenceId, idempotencyKey } = body;
|
|
2177
|
+
const checks = [
|
|
2178
|
+
[
|
|
2179
|
+
typeof amount === "number" && Number.isInteger(amount) && amount > 0,
|
|
2180
|
+
"amount must be a positive integer in minor units"
|
|
2181
|
+
],
|
|
2182
|
+
[typeof currency === "string" && currency.length === 3, "currency must be a 3-letter ISO code"],
|
|
2183
|
+
[
|
|
2184
|
+
typeof method === "string" && METHODS.includes(method),
|
|
2185
|
+
`method must be one of: ${METHODS.join(", ")}`
|
|
2186
|
+
],
|
|
2187
|
+
[
|
|
2188
|
+
typeof referenceId === "string" && referenceId.length >= 1 && referenceId.length <= 200,
|
|
2189
|
+
"referenceId must be 1 to 200 characters"
|
|
2190
|
+
],
|
|
2191
|
+
[
|
|
2192
|
+
typeof idempotencyKey === "string" && idempotencyKey.length >= 1 && idempotencyKey.length <= 200,
|
|
2193
|
+
"idempotencyKey is required (1 to 200 characters)"
|
|
2194
|
+
],
|
|
2195
|
+
[
|
|
2196
|
+
isOptional(body.description, (v) => typeof v === "string" && v.length <= 1e3),
|
|
2197
|
+
"description must be at most 1000 characters"
|
|
2198
|
+
],
|
|
2199
|
+
[isOptional(body.receiptEmail, isEmail), "receiptEmail must be an email address"],
|
|
2200
|
+
[isOptional(body.billingEmail, isEmail), "billingEmail must be an email address"],
|
|
2201
|
+
[
|
|
2202
|
+
isOptional(body.returnUrl, (v) => typeof v === "string" && URL.canParse(v)),
|
|
2203
|
+
"returnUrl must be an absolute URL"
|
|
2204
|
+
],
|
|
2205
|
+
[
|
|
2206
|
+
isOptional(body.metadata, isValidMetadata),
|
|
2207
|
+
`metadata must map keys (1-40 chars) to strings (max 500), at most ${APP_METADATA_MAX_KEYS} keys`
|
|
2208
|
+
],
|
|
2209
|
+
[
|
|
2210
|
+
typeof body.deploymentId === "string" && UUID_RE2.test(body.deploymentId),
|
|
2211
|
+
"Invalid deployment ID"
|
|
2212
|
+
]
|
|
2213
|
+
];
|
|
2214
|
+
const failed = checks.find(([ok]) => !ok);
|
|
2215
|
+
return failed ? failed[1] : null;
|
|
2216
|
+
}
|
|
2217
|
+
function appMetadata(raw) {
|
|
2218
|
+
if (!isStringRecord(raw)) return {};
|
|
2219
|
+
const out = {};
|
|
2220
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2221
|
+
if (!RESERVED_METADATA_KEYS.includes(key)) out[key] = value;
|
|
2222
|
+
}
|
|
2223
|
+
return out;
|
|
2224
|
+
}
|
|
2225
|
+
function stableStringify(value) {
|
|
2226
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
2227
|
+
if (value && typeof value === "object") {
|
|
2228
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2229
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
2230
|
+
}
|
|
2231
|
+
return JSON.stringify(value);
|
|
2232
|
+
}
|
|
2233
|
+
function toStripePaymentIntentSummary(intent) {
|
|
2234
|
+
const open = intent.status !== "succeeded" && intent.status !== "canceled";
|
|
2235
|
+
let actionRequired = "NONE";
|
|
2236
|
+
if (open && intent.method === "card") actionRequired = "CLIENT_CONFIRM";
|
|
2237
|
+
if (open && intent.method === "promptpay" && intent.status === "requires_action") {
|
|
2238
|
+
actionRequired = "ENCODED_IMAGE";
|
|
2239
|
+
}
|
|
2240
|
+
return {
|
|
2241
|
+
id: intent.id,
|
|
2242
|
+
status: intent.status,
|
|
2243
|
+
amount: intent.amount,
|
|
2244
|
+
currency: intent.currency,
|
|
2245
|
+
method: intent.method,
|
|
2246
|
+
referenceId: intent.referenceId,
|
|
2247
|
+
metadata: { ...intent.appMetadata },
|
|
2248
|
+
actionRequired,
|
|
2249
|
+
clientSecret: intent.method === "card" ? intent.clientSecret : null,
|
|
2250
|
+
qr: actionRequired === "ENCODED_IMAGE" ? { ...intent.qr } : null,
|
|
2251
|
+
latestChargeId: intent.latestChargeId,
|
|
2252
|
+
amountRefunded: intent.amountRefunded,
|
|
2253
|
+
lastPaymentError: intent.lastPaymentError ? { ...intent.lastPaymentError } : null,
|
|
2254
|
+
created: intent.created,
|
|
2255
|
+
livemode: false,
|
|
2256
|
+
playground: intent.playground
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
function promptPayQr(id, created) {
|
|
2260
|
+
return {
|
|
2261
|
+
imageUrlPng: `https://qr.stripe.test/${id}.png`,
|
|
2262
|
+
imageUrlSvg: `https://qr.stripe.test/${id}.svg`,
|
|
2263
|
+
data: `00020101021230SIM${id}`,
|
|
2264
|
+
hostedInstructionsUrl: `https://payments.stripe.test/promptpay/${id}`,
|
|
2265
|
+
expiresAt: created + PROMPTPAY_QR_TTL_SECONDS
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
function createIntent(body, account, projectId) {
|
|
2269
|
+
const problem = validateCreate(body);
|
|
2270
|
+
if (problem) return err(problem, 400, "invalid_request");
|
|
2271
|
+
const method = body.method;
|
|
2272
|
+
const currency = body.currency.toLowerCase();
|
|
2273
|
+
if (!state.stripeAllowedMethods.includes(method)) {
|
|
2274
|
+
return err(
|
|
2275
|
+
`The ${method} payment method is turned off for this account`,
|
|
2276
|
+
422,
|
|
2277
|
+
"method_not_allowed"
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2280
|
+
if (method === "promptpay" && currency !== "thb") {
|
|
2281
|
+
return err("PromptPay only takes THB", 422, "currency_not_supported");
|
|
2282
|
+
}
|
|
2283
|
+
if (!state.stripeChargesEnabled) {
|
|
2284
|
+
return err("This Stripe account can't take payments yet", 503, "account_not_ready");
|
|
2285
|
+
}
|
|
2286
|
+
const scopedKey = JSON.stringify(["pi", projectId, account, body.idempotencyKey]);
|
|
2287
|
+
const comparable = { ...body };
|
|
2288
|
+
delete comparable.deploymentId;
|
|
2289
|
+
delete comparable.idempotencyKey;
|
|
2290
|
+
const fingerprint = stableStringify(comparable);
|
|
2291
|
+
const saved = state.stripeIdempotentIntents.get(scopedKey);
|
|
2292
|
+
if (saved) {
|
|
2293
|
+
if (saved.fingerprint !== fingerprint) {
|
|
2294
|
+
return err(
|
|
2295
|
+
"This idempotency key was already used with different parameters. Use a new key for a different payment.",
|
|
2296
|
+
409,
|
|
2297
|
+
"idempotency_conflict"
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
const existing = state.stripePaymentIntents.get(saved.id);
|
|
2301
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(existing) }, 201);
|
|
2302
|
+
}
|
|
2303
|
+
state.stripePaymentIntentCounter += 1;
|
|
2304
|
+
const id = `pi_sim_${state.stripePaymentIntentCounter}`;
|
|
2305
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
2306
|
+
const referenceId = body.referenceId;
|
|
2307
|
+
const metadata = appMetadata(body.metadata);
|
|
2308
|
+
const intent = {
|
|
2309
|
+
id,
|
|
2310
|
+
projectId,
|
|
2311
|
+
account,
|
|
2312
|
+
connectedAccountId: resolveAccount(account),
|
|
2313
|
+
playground: isPlayground(account),
|
|
2314
|
+
status: method === "card" ? "requires_payment_method" : "requires_action",
|
|
2315
|
+
amount: body.amount,
|
|
2316
|
+
currency,
|
|
2317
|
+
method,
|
|
2318
|
+
referenceId,
|
|
2319
|
+
appMetadata: metadata,
|
|
2320
|
+
metadata: { ...metadata, projectId, stardeckReferenceId: referenceId },
|
|
2321
|
+
description: typeof body.description === "string" ? body.description : null,
|
|
2322
|
+
receiptEmail: typeof body.receiptEmail === "string" ? body.receiptEmail : null,
|
|
2323
|
+
returnUrl: typeof body.returnUrl === "string" ? body.returnUrl : null,
|
|
2324
|
+
clientSecret: `${id}_secret_sim`,
|
|
2325
|
+
qr: method === "promptpay" ? promptPayQr(id, created) : null,
|
|
2326
|
+
latestChargeId: null,
|
|
2327
|
+
amountRefunded: 0,
|
|
2328
|
+
lastPaymentError: null,
|
|
2329
|
+
created
|
|
2330
|
+
};
|
|
2331
|
+
state.stripePaymentIntents.set(id, intent);
|
|
2332
|
+
state.stripeIdempotentIntents.set(scopedKey, { fingerprint, id });
|
|
2333
|
+
console.log(
|
|
2334
|
+
`[stardeck-testing] stripe payment intent ${id} created method=${method} amount=${intent.amount} ${currency}`
|
|
2335
|
+
);
|
|
2336
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) }, 201);
|
|
2337
|
+
}
|
|
2338
|
+
function ownedIntent(id, projectId) {
|
|
2339
|
+
const intent = state.stripePaymentIntents.get(id);
|
|
2340
|
+
return intent && intent.projectId === projectId ? intent : void 0;
|
|
2341
|
+
}
|
|
2342
|
+
function cancelIntent(intent) {
|
|
2343
|
+
if (intent.status === "succeeded") {
|
|
2344
|
+
return err("This payment already succeeded and can't be canceled", 409, "already_succeeded");
|
|
2345
|
+
}
|
|
2346
|
+
intent.status = "canceled";
|
|
2347
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) });
|
|
2348
|
+
}
|
|
2349
|
+
function handleIntents(request, url, account, intentId, action, projectId) {
|
|
2350
|
+
if (!intentId && request.method === "POST") {
|
|
2351
|
+
return readJsonBody(request).then((body) => createIntent(body, account, projectId));
|
|
2352
|
+
}
|
|
2353
|
+
if (!intentId && request.method === "GET") {
|
|
2354
|
+
const referenceId = url.searchParams.get("referenceId");
|
|
2355
|
+
if (!referenceId) return err("referenceId is required", 400, "invalid_request");
|
|
2356
|
+
const paymentIntents = [...state.stripePaymentIntents.values()].filter((i) => i.projectId === projectId && i.referenceId === referenceId).map(toStripePaymentIntentSummary);
|
|
2357
|
+
return json({ paymentIntents });
|
|
2358
|
+
}
|
|
2359
|
+
if (!intentId) return null;
|
|
2360
|
+
const intent = ownedIntent(decode(intentId), projectId);
|
|
2361
|
+
if (!action && request.method === "GET") {
|
|
2362
|
+
if (!intent) return err("Payment intent not found", 404, "not_found");
|
|
2363
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) });
|
|
2364
|
+
}
|
|
2365
|
+
if (action === "cancel" && request.method === "POST") {
|
|
2366
|
+
return readJsonBody(request).then((body) => {
|
|
2367
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE2.test(body.deploymentId)) {
|
|
2368
|
+
return err("deploymentId is required", 400, "invalid_request");
|
|
2369
|
+
}
|
|
2370
|
+
if (!intent) return err("Payment intent not found", 404, "not_found");
|
|
2371
|
+
return cancelIntent(intent);
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
return null;
|
|
2375
|
+
}
|
|
2376
|
+
async function handleStripePaymentIntentRequest(request, url, projectId) {
|
|
2377
|
+
const match = url.pathname.match(PATH_RE);
|
|
2378
|
+
if (!match) return null;
|
|
2379
|
+
if (!projectId) return err("Missing authentication", 401, "unauthorized");
|
|
2380
|
+
const account = decode(match[1]);
|
|
2381
|
+
const resource = match[2];
|
|
2382
|
+
if (request.method === "GET" && !url.searchParams.get("deploymentId")) {
|
|
2383
|
+
return err("deploymentId is required", 400, "invalid_request");
|
|
2384
|
+
}
|
|
2385
|
+
if (resource === "elements-config" && request.method === "GET") {
|
|
2386
|
+
return json({
|
|
2387
|
+
publishableKey: SIM_STRIPE_PUBLISHABLE_KEY,
|
|
2388
|
+
stripeAccount: resolveAccount(account),
|
|
2389
|
+
playground: isPlayground(account)
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
if (resource === "readiness" && request.method === "GET") {
|
|
2393
|
+
const methods = [...state.stripeAllowedMethods];
|
|
2394
|
+
return json({
|
|
2395
|
+
provider: "stripe",
|
|
2396
|
+
ready: state.stripeChargesEnabled && methods.length > 0,
|
|
2397
|
+
chargesEnabled: state.stripeChargesEnabled,
|
|
2398
|
+
methods,
|
|
2399
|
+
playground: isPlayground(account)
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
if (resource.startsWith("payment-intents")) {
|
|
2403
|
+
return handleIntents(request, url, account, match[3], match[4], projectId);
|
|
2404
|
+
}
|
|
2405
|
+
return null;
|
|
2406
|
+
}
|
|
2407
|
+
function requireIntent(id) {
|
|
2408
|
+
const intent = state.stripePaymentIntents.get(id);
|
|
2409
|
+
if (!intent) throw new Error(`[stardeck-testing] Unknown payment intent id: ${id}`);
|
|
2410
|
+
return intent;
|
|
2411
|
+
}
|
|
2412
|
+
function requireOpenIntent(id, verb) {
|
|
2413
|
+
const intent = requireIntent(id);
|
|
2414
|
+
if (intent.status === "succeeded" || intent.status === "canceled") {
|
|
2415
|
+
throw new Error(
|
|
2416
|
+
`[stardeck-testing] Can't ${verb} payment intent ${id}: it is ${intent.status}`
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
return intent;
|
|
2420
|
+
}
|
|
2421
|
+
function succeedStripePaymentIntent(id) {
|
|
2422
|
+
const intent = requireOpenIntent(id, "mark paid");
|
|
2423
|
+
intent.status = "succeeded";
|
|
2424
|
+
intent.latestChargeId = `ch_sim_${id}`;
|
|
2425
|
+
intent.lastPaymentError = null;
|
|
2426
|
+
}
|
|
2427
|
+
function failStripePaymentIntent(id, code) {
|
|
2428
|
+
const intent = requireOpenIntent(id, "fail");
|
|
2429
|
+
intent.status = "requires_payment_method";
|
|
2430
|
+
intent.latestChargeId = `ch_sim_${id}_failed`;
|
|
2431
|
+
intent.lastPaymentError = {
|
|
2432
|
+
code,
|
|
2433
|
+
declineCode: code === "card_declined" ? "generic_decline" : null,
|
|
2434
|
+
message: `Simulated payment failure: ${code}`
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
function cancelStripePaymentIntentById(id) {
|
|
2438
|
+
const intent = requireOpenIntent(id, "cancel");
|
|
2439
|
+
intent.status = "canceled";
|
|
2440
|
+
}
|
|
2441
|
+
function stripePaymentIntentEvent(id) {
|
|
2442
|
+
const intent = requireIntent(id);
|
|
2443
|
+
let type;
|
|
2444
|
+
if (intent.status === "succeeded") type = "payment_intent.succeeded";
|
|
2445
|
+
else if (intent.status === "canceled") type = "payment_intent.canceled";
|
|
2446
|
+
else if (intent.lastPaymentError) type = "payment_intent.payment_failed";
|
|
2447
|
+
else {
|
|
2448
|
+
throw new Error(
|
|
2449
|
+
`[stardeck-testing] Payment intent ${id} is ${intent.status}; call markPaid, failPayment or cancelPayment first`
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
const error = intent.lastPaymentError;
|
|
2453
|
+
return {
|
|
2454
|
+
type,
|
|
2455
|
+
accountId: intent.connectedAccountId ?? "acct_stardeck_playground",
|
|
2456
|
+
data: {
|
|
2457
|
+
id: intent.id,
|
|
2458
|
+
object: "payment_intent",
|
|
2459
|
+
amount: intent.amount,
|
|
2460
|
+
amount_received: intent.status === "succeeded" ? intent.amount : 0,
|
|
2461
|
+
currency: intent.currency,
|
|
2462
|
+
status: intent.status,
|
|
2463
|
+
metadata: { ...intent.metadata },
|
|
2464
|
+
payment_method_types: [intent.method],
|
|
2465
|
+
latest_charge: intent.latestChargeId,
|
|
2466
|
+
last_payment_error: error ? { code: error.code, decline_code: error.declineCode, message: error.message } : null,
|
|
2467
|
+
description: intent.description,
|
|
2468
|
+
receipt_email: intent.receiptEmail,
|
|
2469
|
+
created: intent.created,
|
|
2470
|
+
livemode: false
|
|
2471
|
+
}
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2120
2475
|
// src/simulator/webhook-signing.ts
|
|
2121
2476
|
var import_node_crypto5 = __toESM(require("crypto"));
|
|
2122
2477
|
function signEventDelivery(secret, context, rawBody) {
|
|
@@ -2150,7 +2505,7 @@ var BEAM_BOLT_PAYMENT_METHODS = [
|
|
|
2150
2505
|
"SPAY_LATER"
|
|
2151
2506
|
];
|
|
2152
2507
|
var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
|
|
2153
|
-
var
|
|
2508
|
+
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
2509
|
function payErr(error, status = 400, code, details) {
|
|
2155
2510
|
const body = { error };
|
|
2156
2511
|
if (code) body.code = code;
|
|
@@ -2176,7 +2531,23 @@ function paymentOperationForRequest(request, pathname) {
|
|
|
2176
2531
|
if (request.method === "GET" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout-sessions\/[^/]+$/.test(pathname)) {
|
|
2177
2532
|
return "stripe.getCheckoutSessionStatus";
|
|
2178
2533
|
}
|
|
2179
|
-
return
|
|
2534
|
+
return stripeInlineOperation(request.method, pathname);
|
|
2535
|
+
}
|
|
2536
|
+
var STRIPE_INLINE_OPERATIONS = [
|
|
2537
|
+
["POST", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents$/, "stripe.createPaymentIntent"],
|
|
2538
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents$/, "stripe.findPaymentIntents"],
|
|
2539
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents\/[^/]+$/, "stripe.getPaymentIntent"],
|
|
2540
|
+
[
|
|
2541
|
+
"POST",
|
|
2542
|
+
/^\/api\/store\/(?!beam\/)[^/]+\/payment-intents\/[^/]+\/cancel$/,
|
|
2543
|
+
"stripe.cancelPaymentIntent"
|
|
2544
|
+
],
|
|
2545
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/elements-config$/, "stripe.getElementsConfig"],
|
|
2546
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/readiness$/, "stripe.getReadiness"]
|
|
2547
|
+
];
|
|
2548
|
+
function stripeInlineOperation(method, pathname) {
|
|
2549
|
+
const hit = STRIPE_INLINE_OPERATIONS.find(([m, re]) => m === method && re.test(pathname));
|
|
2550
|
+
return hit ? hit[2] : null;
|
|
2180
2551
|
}
|
|
2181
2552
|
function consumePaymentFailure(request, pathname) {
|
|
2182
2553
|
const operation = paymentOperationForRequest(request, pathname);
|
|
@@ -2206,11 +2577,11 @@ function isBoltPaymentMethod(value) {
|
|
|
2206
2577
|
function isBoltIntentStatus(value) {
|
|
2207
2578
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
2208
2579
|
}
|
|
2209
|
-
function
|
|
2210
|
-
if (Array.isArray(value)) return `[${value.map(
|
|
2580
|
+
function stableStringify2(value) {
|
|
2581
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify2).join(",")}]`;
|
|
2211
2582
|
if (value && typeof value === "object") {
|
|
2212
2583
|
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)}:${
|
|
2584
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify2(v)}`).join(",")}}`;
|
|
2214
2585
|
}
|
|
2215
2586
|
return JSON.stringify(value);
|
|
2216
2587
|
}
|
|
@@ -2351,10 +2722,16 @@ async function deliverEvent(envelope, handler, options) {
|
|
|
2351
2722
|
body: envelope
|
|
2352
2723
|
});
|
|
2353
2724
|
}
|
|
2354
|
-
async function handlePaymentsRequest(request, url) {
|
|
2725
|
+
async function handlePaymentsRequest(request, url, signedProjectId = null) {
|
|
2355
2726
|
const pathname = url.pathname;
|
|
2356
2727
|
const injectedFailure = consumePaymentFailure(request, pathname);
|
|
2357
2728
|
if (injectedFailure) return injectedFailure;
|
|
2729
|
+
const inlineStripeResponse = await handleStripePaymentIntentRequest(
|
|
2730
|
+
request,
|
|
2731
|
+
url,
|
|
2732
|
+
signedProjectId
|
|
2733
|
+
);
|
|
2734
|
+
if (inlineStripeResponse) return inlineStripeResponse;
|
|
2358
2735
|
const directChargeResponse = await handleDirectChargeRequest(request, pathname);
|
|
2359
2736
|
if (directChargeResponse) return directChargeResponse;
|
|
2360
2737
|
if (/\/billing-portal$/.test(pathname)) {
|
|
@@ -2453,7 +2830,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2453
2830
|
code: "too_small"
|
|
2454
2831
|
});
|
|
2455
2832
|
}
|
|
2456
|
-
if (typeof body.deploymentId !== "string" || !
|
|
2833
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE3.test(body.deploymentId)) {
|
|
2457
2834
|
issues.push({
|
|
2458
2835
|
path: ["deploymentId"],
|
|
2459
2836
|
message: "Invalid uuid",
|
|
@@ -2637,7 +3014,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2637
3014
|
const comparable = { ...body };
|
|
2638
3015
|
delete comparable.deploymentId;
|
|
2639
3016
|
delete comparable.idempotencyKey;
|
|
2640
|
-
const fingerprint =
|
|
3017
|
+
const fingerprint = stableStringify2(comparable);
|
|
2641
3018
|
if (scopedKey) {
|
|
2642
3019
|
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
2643
3020
|
if (saved) {
|
|
@@ -2712,6 +3089,10 @@ function createPayments() {
|
|
|
2712
3089
|
state.paymentFailures.push({ operation, failure: failure2 });
|
|
2713
3090
|
},
|
|
2714
3091
|
markPaid(id) {
|
|
3092
|
+
if (state.stripePaymentIntents.has(id)) {
|
|
3093
|
+
succeedStripePaymentIntent(id);
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
2715
3096
|
const session = state.sessionStatuses.get(id);
|
|
2716
3097
|
if (session) {
|
|
2717
3098
|
session.status = "complete";
|
|
@@ -2739,7 +3120,26 @@ function createPayments() {
|
|
|
2739
3120
|
}
|
|
2740
3121
|
link.status = status;
|
|
2741
3122
|
},
|
|
2742
|
-
|
|
3123
|
+
failPayment(paymentIntentId, code = "card_declined") {
|
|
3124
|
+
failStripePaymentIntent(paymentIntentId, code);
|
|
3125
|
+
},
|
|
3126
|
+
cancelPayment(paymentIntentId) {
|
|
3127
|
+
cancelStripePaymentIntentById(paymentIntentId);
|
|
3128
|
+
},
|
|
3129
|
+
get stripePaymentIntents() {
|
|
3130
|
+
return [...state.stripePaymentIntents.values()];
|
|
3131
|
+
},
|
|
3132
|
+
getStripePaymentIntent(paymentIntentId) {
|
|
3133
|
+
return state.stripePaymentIntents.get(paymentIntentId);
|
|
3134
|
+
},
|
|
3135
|
+
setStripePaymentMethods(methods) {
|
|
3136
|
+
state.stripeAllowedMethods = [...methods];
|
|
3137
|
+
},
|
|
3138
|
+
setStripeChargesEnabled(enabled) {
|
|
3139
|
+
state.stripeChargesEnabled = enabled;
|
|
3140
|
+
},
|
|
3141
|
+
async deliverStripeEvent(handler, input, options) {
|
|
3142
|
+
const event = "paymentIntentId" in input ? stripePaymentIntentEvent(input.paymentIntentId) : input;
|
|
2743
3143
|
const envelope = {
|
|
2744
3144
|
id: `evt_test_${import_node_crypto6.default.randomUUID()}`,
|
|
2745
3145
|
kind: "stripe_webhook",
|
|
@@ -2855,6 +3255,11 @@ function createPayments() {
|
|
|
2855
3255
|
state.cardTokens.clear();
|
|
2856
3256
|
state.cardTokenCounter = 0;
|
|
2857
3257
|
state.cardTokenizationEnabled = true;
|
|
3258
|
+
state.stripePaymentIntents.clear();
|
|
3259
|
+
state.stripePaymentIntentCounter = 0;
|
|
3260
|
+
state.stripeIdempotentIntents.clear();
|
|
3261
|
+
state.stripeAllowedMethods = ["card", "promptpay"];
|
|
3262
|
+
state.stripeChargesEnabled = true;
|
|
2858
3263
|
},
|
|
2859
3264
|
get count() {
|
|
2860
3265
|
return state.checkouts.length;
|
|
@@ -3627,6 +4032,141 @@ function createEdge() {
|
|
|
3627
4032
|
};
|
|
3628
4033
|
}
|
|
3629
4034
|
|
|
4035
|
+
// src/simulator/controls.ts
|
|
4036
|
+
var CONTROLS_STATE_SYMBOL = /* @__PURE__ */ Symbol.for("stardeck.controls.state");
|
|
4037
|
+
var USER_TOKEN_HEADER = "x-stardeck-user-token";
|
|
4038
|
+
var DEFAULTS = {
|
|
4039
|
+
"flag.paymentsSetupStep": false,
|
|
4040
|
+
"plan.locations": { included: 3, max: null, extraMonthly: { amount: 990, currency: "THB" } },
|
|
4041
|
+
"plan.multiEntity": true,
|
|
4042
|
+
"payments.provider": "beam",
|
|
4043
|
+
"payments.methods": { card: true, promptPay: true }
|
|
4044
|
+
};
|
|
4045
|
+
var REFUSAL_STATUS = {
|
|
4046
|
+
NOT_WRITABLE: 400,
|
|
4047
|
+
INVALID_VALUE: 400,
|
|
4048
|
+
UNAUTHENTICATED: 401,
|
|
4049
|
+
FORBIDDEN: 403,
|
|
4050
|
+
WRITE_NOT_ALLOWED_HERE: 403,
|
|
4051
|
+
PROVIDER_NOT_READY: 409,
|
|
4052
|
+
UNAVAILABLE: 503
|
|
4053
|
+
};
|
|
4054
|
+
var controlsModule = null;
|
|
4055
|
+
function loadControls() {
|
|
4056
|
+
controlsModule ??= import("@stardeck-customer-apps/controls");
|
|
4057
|
+
return controlsModule;
|
|
4058
|
+
}
|
|
4059
|
+
function resetControlsClientCache() {
|
|
4060
|
+
const clientState = globalThis[CONTROLS_STATE_SYMBOL];
|
|
4061
|
+
clientState?.reset?.();
|
|
4062
|
+
}
|
|
4063
|
+
function initControls(seed) {
|
|
4064
|
+
state.controls.seed = { ...seed };
|
|
4065
|
+
restoreControls();
|
|
4066
|
+
}
|
|
4067
|
+
function restoreControls() {
|
|
4068
|
+
state.controls.values = { ...DEFAULTS, ...state.controls.seed };
|
|
4069
|
+
state.controls.unavailable = /* @__PURE__ */ new Set();
|
|
4070
|
+
state.controls.lastUsage = null;
|
|
4071
|
+
state.controls.writes = [];
|
|
4072
|
+
state.controls.refusal = null;
|
|
4073
|
+
resetControlsClientCache();
|
|
4074
|
+
}
|
|
4075
|
+
function clearControls() {
|
|
4076
|
+
restoreControls();
|
|
4077
|
+
}
|
|
4078
|
+
function currentControlsBody() {
|
|
4079
|
+
const controls = {};
|
|
4080
|
+
for (const [key, value] of Object.entries(state.controls.values)) {
|
|
4081
|
+
controls[key] = state.controls.unavailable.has(key) ? { ok: false, reason: "unavailable (simulated)" } : { ok: true, value };
|
|
4082
|
+
}
|
|
4083
|
+
return controls;
|
|
4084
|
+
}
|
|
4085
|
+
function refusalResponse(refusal) {
|
|
4086
|
+
return json({ error: refusal }, REFUSAL_STATUS[refusal.code]);
|
|
4087
|
+
}
|
|
4088
|
+
function handleSync(body) {
|
|
4089
|
+
const usage = body.usage;
|
|
4090
|
+
if (typeof usage === "object" && usage !== null) {
|
|
4091
|
+
const locations = usage.locations;
|
|
4092
|
+
state.controls.lastUsage = typeof locations === "number" && Number.isInteger(locations) && locations >= 0 ? { locations } : {};
|
|
4093
|
+
}
|
|
4094
|
+
return json({ controls: currentControlsBody() });
|
|
4095
|
+
}
|
|
4096
|
+
async function handleWrite(request, key) {
|
|
4097
|
+
const { CONTROLS, isWritableControl } = await loadControls();
|
|
4098
|
+
if (!isWritableControl(key)) {
|
|
4099
|
+
return refusalResponse({
|
|
4100
|
+
code: "NOT_WRITABLE",
|
|
4101
|
+
message: `${key} cannot be changed from the app`
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
const userToken = request.headers.get(USER_TOKEN_HEADER);
|
|
4105
|
+
if (!userToken) {
|
|
4106
|
+
return refusalResponse({
|
|
4107
|
+
code: "UNAUTHENTICATED",
|
|
4108
|
+
message: "A signed-in person is required (X-Stardeck-User-Token)"
|
|
4109
|
+
});
|
|
4110
|
+
}
|
|
4111
|
+
const body = await readJsonBody(request);
|
|
4112
|
+
const parsed = CONTROLS[key].schema.safeParse(body.value);
|
|
4113
|
+
if (!parsed.success) {
|
|
4114
|
+
return refusalResponse({
|
|
4115
|
+
code: "INVALID_VALUE",
|
|
4116
|
+
message: parsed.error.issues[0]?.message ?? "Invalid value"
|
|
4117
|
+
});
|
|
4118
|
+
}
|
|
4119
|
+
const refusal = state.controls.refusal;
|
|
4120
|
+
if (refusal) {
|
|
4121
|
+
state.controls.refusal = null;
|
|
4122
|
+
return refusalResponse(refusal);
|
|
4123
|
+
}
|
|
4124
|
+
state.controls.values[key] = parsed.data;
|
|
4125
|
+
state.controls.unavailable.delete(key);
|
|
4126
|
+
state.controls.writes.push({ key, value: parsed.data, userToken });
|
|
4127
|
+
return json({ controls: currentControlsBody() });
|
|
4128
|
+
}
|
|
4129
|
+
var CONTROLS_PATH = /^\/api\/deployments\/[^/]+\/controls(?:\/([^/]+))?$/;
|
|
4130
|
+
async function handleControlsRequest(request, url) {
|
|
4131
|
+
const match = url.pathname.match(CONTROLS_PATH);
|
|
4132
|
+
const key = match?.[1] ? decodeURIComponent(match[1]) : null;
|
|
4133
|
+
if (!key && request.method === "POST") {
|
|
4134
|
+
return handleSync(await readJsonBody(request));
|
|
4135
|
+
}
|
|
4136
|
+
if (key && request.method === "PUT") {
|
|
4137
|
+
return handleWrite(request, key);
|
|
4138
|
+
}
|
|
4139
|
+
return json(
|
|
4140
|
+
{
|
|
4141
|
+
error: { code: "UNAVAILABLE", message: `No simulator for ${request.method} ${url.pathname}` }
|
|
4142
|
+
},
|
|
4143
|
+
404
|
|
4144
|
+
);
|
|
4145
|
+
}
|
|
4146
|
+
function createControls() {
|
|
4147
|
+
return {
|
|
4148
|
+
set(key, value) {
|
|
4149
|
+
state.controls.values[key] = value;
|
|
4150
|
+
state.controls.unavailable.delete(key);
|
|
4151
|
+
resetControlsClientCache();
|
|
4152
|
+
},
|
|
4153
|
+
setUnavailable(key) {
|
|
4154
|
+
state.controls.unavailable.add(key);
|
|
4155
|
+
resetControlsClientCache();
|
|
4156
|
+
},
|
|
4157
|
+
lastUsage() {
|
|
4158
|
+
return state.controls.lastUsage;
|
|
4159
|
+
},
|
|
4160
|
+
writes() {
|
|
4161
|
+
return state.controls.writes;
|
|
4162
|
+
},
|
|
4163
|
+
refuseNextWrite(refusal) {
|
|
4164
|
+
state.controls.refusal = refusal;
|
|
4165
|
+
},
|
|
4166
|
+
clear: restoreControls
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
|
|
3630
4170
|
// src/simulator/schedules.ts
|
|
3631
4171
|
async function handleScheduleWake(request, scheduleKey) {
|
|
3632
4172
|
if (request.method !== "POST") return failure(`Method ${request.method} not allowed`, 405);
|
|
@@ -3809,7 +4349,7 @@ function requiresDeploymentHmac(request, url) {
|
|
|
3809
4349
|
return !isPresignedPut;
|
|
3810
4350
|
}
|
|
3811
4351
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
3812
|
-
const
|
|
4352
|
+
const isEmail2 = url.pathname === "/api/email/send";
|
|
3813
4353
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
3814
4354
|
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
3815
4355
|
const messagingMatch = url.pathname.match(
|
|
@@ -3818,8 +4358,10 @@ function requiresDeploymentHmac(request, url) {
|
|
|
3818
4358
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
3819
4359
|
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
3820
4360
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
3821
|
-
|
|
4361
|
+
const controlsMatch = url.pathname.match(CONTROLS_PATH2);
|
|
4362
|
+
return !!(controlsMatch || isStarlens || scheduleWakeMatch || dataStoreMatch || isEmail2 || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
3822
4363
|
}
|
|
4364
|
+
var CONTROLS_PATH2 = /^\/api\/deployments\/[^/]+\/controls(\/[^/]+)?$/;
|
|
3823
4365
|
var SCHEDULE_WAKE_PATH = /^\/api\/deployments\/[^/]+\/schedules\/([^/]+)\/wake$/;
|
|
3824
4366
|
function deploymentIdFromPath(url) {
|
|
3825
4367
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
@@ -3841,7 +4383,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3841
4383
|
return authMatch[1].endsWith("verify") ? handleAuthVerify(request) : handleAuthRefresh(request);
|
|
3842
4384
|
}
|
|
3843
4385
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
3844
|
-
const
|
|
4386
|
+
const isEmail2 = url.pathname === "/api/email/send";
|
|
3845
4387
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
3846
4388
|
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
3847
4389
|
const messagingMatch = url.pathname.match(
|
|
@@ -3850,6 +4392,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3850
4392
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
3851
4393
|
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
3852
4394
|
let signedDeploymentId = null;
|
|
4395
|
+
let signedProjectId = null;
|
|
3853
4396
|
if (requiresDeploymentHmac(request, url)) {
|
|
3854
4397
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
3855
4398
|
if (!authHeader) {
|
|
@@ -3862,6 +4405,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3862
4405
|
return failure("Invalid authentication", 401);
|
|
3863
4406
|
}
|
|
3864
4407
|
signedDeploymentId = authPayload.deploymentId;
|
|
4408
|
+
signedProjectId = authPayload.projectId;
|
|
3865
4409
|
}
|
|
3866
4410
|
if (isStorageHost) {
|
|
3867
4411
|
return handleStorageRequest(request, url);
|
|
@@ -3869,14 +4413,14 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3869
4413
|
if (url.hostname === SIM_BEAM_HOST) {
|
|
3870
4414
|
return handleCardTokenRequest(request, url);
|
|
3871
4415
|
}
|
|
3872
|
-
if (
|
|
4416
|
+
if (isEmail2 && request.method === "POST") {
|
|
3873
4417
|
return handleEmailSend(request);
|
|
3874
4418
|
}
|
|
3875
4419
|
if (identitiesMatch) {
|
|
3876
4420
|
return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
|
|
3877
4421
|
}
|
|
3878
4422
|
if (storeMatch) {
|
|
3879
|
-
return handlePaymentsRequest(request, url);
|
|
4423
|
+
return handlePaymentsRequest(request, url, signedProjectId);
|
|
3880
4424
|
}
|
|
3881
4425
|
if (messagingMatch) {
|
|
3882
4426
|
const channel = messagingMatch[1];
|
|
@@ -3885,6 +4429,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3885
4429
|
if (edgeMatch) {
|
|
3886
4430
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
3887
4431
|
}
|
|
4432
|
+
if (CONTROLS_PATH2.test(url.pathname)) {
|
|
4433
|
+
return handleControlsRequest(request, url);
|
|
4434
|
+
}
|
|
3888
4435
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
3889
4436
|
if (scheduleWakeMatch) {
|
|
3890
4437
|
return handleScheduleWake(request, decodeURIComponent(scheduleWakeMatch[1]));
|
|
@@ -3913,7 +4460,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3913
4460
|
}
|
|
3914
4461
|
}
|
|
3915
4462
|
return failure(
|
|
3916
|
-
`[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.`,
|
|
4463
|
+
`[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, app controls sync/write, starlens extract, schedule wakes, auth verify/refresh, Neon /sql.`,
|
|
3917
4464
|
404
|
|
3918
4465
|
);
|
|
3919
4466
|
}
|
|
@@ -4062,6 +4609,7 @@ async function createTestApp(options = {}) {
|
|
|
4062
4609
|
const initialLiffId = options.liffId ?? null;
|
|
4063
4610
|
state.liffId = initialLiffId;
|
|
4064
4611
|
installFetchRouter();
|
|
4612
|
+
initControls(options.controls);
|
|
4065
4613
|
const schemaSql = loadSchemaSql(options) ?? "";
|
|
4066
4614
|
const db = await bootWithSchema(schemaSql);
|
|
4067
4615
|
state.db = db;
|
|
@@ -4093,6 +4641,7 @@ async function createTestApp(options = {}) {
|
|
|
4093
4641
|
const edge = createEdge();
|
|
4094
4642
|
const starlens = createStarlens();
|
|
4095
4643
|
const schedules = createSchedules();
|
|
4644
|
+
const controls = createControls();
|
|
4096
4645
|
const app = {
|
|
4097
4646
|
db,
|
|
4098
4647
|
inbox,
|
|
@@ -4103,6 +4652,7 @@ async function createTestApp(options = {}) {
|
|
|
4103
4652
|
edge,
|
|
4104
4653
|
starlens,
|
|
4105
4654
|
schedules,
|
|
4655
|
+
controls,
|
|
4106
4656
|
async query(sql, params = []) {
|
|
4107
4657
|
const result = await db.query(sql, params);
|
|
4108
4658
|
return result.rows;
|
|
@@ -4152,6 +4702,7 @@ async function createTestApp(options = {}) {
|
|
|
4152
4702
|
edge.clear();
|
|
4153
4703
|
starlens.clear();
|
|
4154
4704
|
schedules.clear();
|
|
4705
|
+
clearControls();
|
|
4155
4706
|
},
|
|
4156
4707
|
async close() {
|
|
4157
4708
|
state.db = null;
|
|
@@ -4168,6 +4719,7 @@ async function createTestApp(options = {}) {
|
|
|
4168
4719
|
edge.clear();
|
|
4169
4720
|
starlens.clear();
|
|
4170
4721
|
schedules.clear();
|
|
4722
|
+
clearControls();
|
|
4171
4723
|
uninstallFetchRouter();
|
|
4172
4724
|
await db.close();
|
|
4173
4725
|
}
|