@stardeck-customer-apps/testing 0.8.0 → 0.10.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/LICENSE +21 -0
- package/SKILL.md +123 -5
- package/dist/config.d.mts +6 -2
- package/dist/config.d.ts +6 -2
- package/dist/config.js +75 -1
- package/dist/config.mjs +73 -1
- package/dist/data-store-manifest-Zwgcv0DJ.d.mts +25 -0
- package/dist/data-store-manifest-Zwgcv0DJ.d.ts +25 -0
- package/dist/index.d.mts +82 -2
- package/dist/index.d.ts +82 -2
- package/dist/index.js +453 -6
- package/dist/index.mjs +450 -6
- package/dist/next/headers-shim.js +8 -0
- package/dist/next/headers-shim.mjs +8 -0
- package/dist/setup.js +294 -4
- package/dist/setup.mjs +294 -4
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as _stardeck_customer_apps_payments_sdk from '@stardeck-customer-apps/payments-sdk';
|
|
2
2
|
import * as next_server from 'next/server';
|
|
3
3
|
import { PGlite } from '@electric-sql/pglite';
|
|
4
|
+
import { T as TestDataStoreInput } from './data-store-manifest-Zwgcv0DJ.js';
|
|
5
|
+
export { S as STARDECK_DATA_STORES_ENV, a as TestDataStoreManifestEntry, b as buildTestDataStoreManifest } from './data-store-manifest-Zwgcv0DJ.js';
|
|
4
6
|
|
|
5
7
|
type RouteHandler<Req extends Request = Request, P = Record<string, string | string[]>> = (request: Req, context: {
|
|
6
8
|
params: Promise<P>;
|
|
@@ -131,6 +133,53 @@ interface CapturedCheckout {
|
|
|
131
133
|
metadata?: Record<string, string>;
|
|
132
134
|
createdAt: Date;
|
|
133
135
|
}
|
|
136
|
+
/** Simulated Bolt+ connection as returned by the control-plane store API. */
|
|
137
|
+
interface SimBoltConnection {
|
|
138
|
+
id: string;
|
|
139
|
+
projectId: string;
|
|
140
|
+
beamConnectionId: string;
|
|
141
|
+
displayName: string | null;
|
|
142
|
+
pairingCode: string;
|
|
143
|
+
status: string;
|
|
144
|
+
isSandbox: boolean;
|
|
145
|
+
environments: Array<"sandbox" | "preview" | "production">;
|
|
146
|
+
createdAt: string;
|
|
147
|
+
updatedAt: string;
|
|
148
|
+
}
|
|
149
|
+
type SimBoltIntentStoredStatus = "PENDING" | "PAID" | "FAILED" | "CANCELED";
|
|
150
|
+
type SimBoltIntentStatus = SimBoltIntentStoredStatus | "EXPIRED";
|
|
151
|
+
/**
|
|
152
|
+
* Stored Bolt+ intent. `EXPIRED` is never written — derive it from
|
|
153
|
+
* `expiresAt` on read, matching the real control plane.
|
|
154
|
+
*/
|
|
155
|
+
type SimBoltPaymentMethod = _stardeck_customer_apps_payments_sdk.BeamBoltPaymentMethod;
|
|
156
|
+
/** Bolt intent as returned by list/get helpers (timestamps ISO; nullables are null). */
|
|
157
|
+
interface SimBoltIntentRecord {
|
|
158
|
+
id: string;
|
|
159
|
+
beamIntentId: string;
|
|
160
|
+
boltConnectionId: string;
|
|
161
|
+
amount: number;
|
|
162
|
+
currency: string;
|
|
163
|
+
paymentMethodType: SimBoltPaymentMethod;
|
|
164
|
+
referenceId: string | null;
|
|
165
|
+
internalNote: string | null;
|
|
166
|
+
status: SimBoltIntentStatus;
|
|
167
|
+
isVirtual: boolean;
|
|
168
|
+
environment: "sandbox" | "preview" | "production";
|
|
169
|
+
expiresAt: string;
|
|
170
|
+
settledAt: string | null;
|
|
171
|
+
chargeId: string | null;
|
|
172
|
+
failureReason: string | null;
|
|
173
|
+
createdAt: string;
|
|
174
|
+
}
|
|
175
|
+
interface SimBoltCharge {
|
|
176
|
+
id: string;
|
|
177
|
+
status: "SUCCEEDED" | "FAILED" | "PENDING";
|
|
178
|
+
amount?: number;
|
|
179
|
+
currency?: string;
|
|
180
|
+
sourceId?: string;
|
|
181
|
+
createdAt?: string;
|
|
182
|
+
}
|
|
134
183
|
interface TestPayments {
|
|
135
184
|
get checkouts(): CapturedCheckout[];
|
|
136
185
|
latest(): CapturedCheckout | undefined;
|
|
@@ -153,6 +202,30 @@ interface TestPayments {
|
|
|
153
202
|
path?: string;
|
|
154
203
|
deploymentSecret?: string;
|
|
155
204
|
}): Promise<Response>;
|
|
205
|
+
/**
|
|
206
|
+
* Mark a PENDING bolt intent PAID and record a SUCCEEDED charge. Does not
|
|
207
|
+
* deliver a webhook — call `deliverBeamEvent` with `charge.succeeded` (reuse
|
|
208
|
+
* the existing signing path) after approving, the same way `markPaid` pairs
|
|
209
|
+
* with `deliverStripeEvent` / `deliverBeamEvent`.
|
|
210
|
+
*/
|
|
211
|
+
approveBoltIntent(intentId: string, options?: {
|
|
212
|
+
chargeId?: string;
|
|
213
|
+
}): SimBoltCharge;
|
|
214
|
+
/**
|
|
215
|
+
* Mark a PENDING bolt intent FAILED and record a FAILED charge.
|
|
216
|
+
* Delivers no webhook: Beam's catalog has no charge-failure event for bolt
|
|
217
|
+
* payments, and the real product notifies the app of nothing on decline.
|
|
218
|
+
*/
|
|
219
|
+
declineBoltIntent(intentId: string, options?: {
|
|
220
|
+
failureReason?: string;
|
|
221
|
+
}): SimBoltCharge;
|
|
222
|
+
/**
|
|
223
|
+
* Backdate `expiresAt` so the intent derives as EXPIRED on the next read.
|
|
224
|
+
* Delivers no webhook: Beam does not notify apps when a bolt intent expires.
|
|
225
|
+
*/
|
|
226
|
+
expireBoltIntent(intentId: string): void;
|
|
227
|
+
listBoltIntents(): SimBoltIntentRecord[];
|
|
228
|
+
getBoltIntent(intentId: string): SimBoltIntentRecord | undefined;
|
|
156
229
|
clear(): void;
|
|
157
230
|
get count(): number;
|
|
158
231
|
}
|
|
@@ -366,6 +439,13 @@ interface TestEdge {
|
|
|
366
439
|
get count(): number;
|
|
367
440
|
}
|
|
368
441
|
interface TestAppOptions {
|
|
442
|
+
/** Data stores exposed through the production-shaped STARDECK_DATA_STORES manifest. */
|
|
443
|
+
dataStores?: TestDataStoreInput[];
|
|
444
|
+
/**
|
|
445
|
+
* Force the legacy single-store DATA_STORE_URL on or off. By default it is
|
|
446
|
+
* present only for the zero-config manifest, and omitted once a manifest is configured.
|
|
447
|
+
*/
|
|
448
|
+
legacyDataStoreUrl?: boolean;
|
|
369
449
|
/**
|
|
370
450
|
* Path to the schema.sql snapshot generated by
|
|
371
451
|
* `npx stardeck-data-store generate-types` (relative to the project root).
|
|
@@ -446,6 +526,7 @@ declare function parseWorkflowName(describeTitle: string): string | null;
|
|
|
446
526
|
|
|
447
527
|
declare const CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
448
528
|
declare const DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
529
|
+
declare const DATA_STORE_TEST_URL = "postgresql://test:test@db.stardeck.test/main";
|
|
449
530
|
declare const STORAGE_TEST_URL = "https://storage.stardeck.test";
|
|
450
531
|
declare const STORAGE_TEST_HOST = "storage.stardeck.test";
|
|
451
532
|
declare const TEST_ENV_DEFAULTS: {
|
|
@@ -454,10 +535,9 @@ declare const TEST_ENV_DEFAULTS: {
|
|
|
454
535
|
readonly ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a";
|
|
455
536
|
readonly PROJECT_ID: "00000000-0000-4000-8000-00000000000b";
|
|
456
537
|
readonly DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c";
|
|
457
|
-
readonly DATA_STORE_URL: "postgresql://test:test@db.stardeck.test/main";
|
|
458
538
|
readonly STORAGE_URL: "https://storage.stardeck.test";
|
|
459
539
|
};
|
|
460
540
|
/** Default location of the DDL snapshot written by `generate-types`. */
|
|
461
541
|
declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
|
|
462
542
|
|
|
463
|
-
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
|
543
|
+
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
package/dist/index.js
CHANGED
|
@@ -32,11 +32,14 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
CONTROL_PLANE_TEST_URL: () => CONTROL_PLANE_TEST_URL,
|
|
34
34
|
DATA_STORE_TEST_HOST: () => DATA_STORE_TEST_HOST,
|
|
35
|
+
DATA_STORE_TEST_URL: () => DATA_STORE_TEST_URL,
|
|
35
36
|
DEFAULT_SCHEMA_PATH: () => DEFAULT_SCHEMA_PATH,
|
|
37
|
+
STARDECK_DATA_STORES_ENV: () => STARDECK_DATA_STORES_ENV,
|
|
36
38
|
STORAGE_TEST_HOST: () => STORAGE_TEST_HOST,
|
|
37
39
|
STORAGE_TEST_URL: () => STORAGE_TEST_URL,
|
|
38
40
|
TEST_ENV_DEFAULTS: () => TEST_ENV_DEFAULTS,
|
|
39
41
|
WORKFLOW_NAME_PREFIX: () => WORKFLOW_NAME_PREFIX,
|
|
42
|
+
buildTestDataStoreManifest: () => buildTestDataStoreManifest,
|
|
40
43
|
callRoute: () => callRoute,
|
|
41
44
|
createTestApp: () => createTestApp,
|
|
42
45
|
describeWorkflow: () => describeWorkflow,
|
|
@@ -93,6 +96,7 @@ function globalSingleton(key, create) {
|
|
|
93
96
|
// src/state.ts
|
|
94
97
|
var state = globalSingleton("state", () => ({
|
|
95
98
|
db: null,
|
|
99
|
+
lastHarnessDataStoreManifest: null,
|
|
96
100
|
currentUser: null,
|
|
97
101
|
sessions: /* @__PURE__ */ new Map(),
|
|
98
102
|
refreshSessions: /* @__PURE__ */ new Map(),
|
|
@@ -105,6 +109,13 @@ var state = globalSingleton("state", () => ({
|
|
|
105
109
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
106
110
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
107
111
|
products: [],
|
|
112
|
+
boltConnections: /* @__PURE__ */ new Map(),
|
|
113
|
+
boltUsedPairingCodes: /* @__PURE__ */ new Set(),
|
|
114
|
+
boltConnectionCounter: 0,
|
|
115
|
+
boltIntents: /* @__PURE__ */ new Map(),
|
|
116
|
+
boltIntentCounter: 0,
|
|
117
|
+
boltCharges: [],
|
|
118
|
+
boltChargeCounter: 0,
|
|
108
119
|
uploads: [],
|
|
109
120
|
uploadCounter: 0,
|
|
110
121
|
storageFiles: /* @__PURE__ */ new Map(),
|
|
@@ -134,6 +145,7 @@ function requireDb() {
|
|
|
134
145
|
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
135
146
|
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
136
147
|
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
148
|
+
var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
|
|
137
149
|
var STORAGE_TEST_URL = "https://storage.stardeck.test";
|
|
138
150
|
var STORAGE_TEST_HOST = "storage.stardeck.test";
|
|
139
151
|
var TEST_ENV_DEFAULTS = {
|
|
@@ -142,7 +154,6 @@ var TEST_ENV_DEFAULTS = {
|
|
|
142
154
|
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
143
155
|
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
144
156
|
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
145
|
-
DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`,
|
|
146
157
|
STORAGE_URL: STORAGE_TEST_URL
|
|
147
158
|
};
|
|
148
159
|
var DEFAULT_TEST_USER = {
|
|
@@ -1242,8 +1253,31 @@ function signEventDelivery(secret, context, rawBody) {
|
|
|
1242
1253
|
}
|
|
1243
1254
|
|
|
1244
1255
|
// src/simulator/payments.ts
|
|
1245
|
-
|
|
1246
|
-
|
|
1256
|
+
var BEAM_BOLT_PAYMENT_METHODS = [
|
|
1257
|
+
"CARD",
|
|
1258
|
+
"CARD_INSTALLMENTS",
|
|
1259
|
+
"QR_PROMPT_PAY",
|
|
1260
|
+
"ALIPAY",
|
|
1261
|
+
"ALIPAY_PLUS",
|
|
1262
|
+
"LINE_PAY",
|
|
1263
|
+
"SHOPEE_PAY",
|
|
1264
|
+
"TRUE_MONEY",
|
|
1265
|
+
"WECHAT_PAY",
|
|
1266
|
+
"SPAY_LATER"
|
|
1267
|
+
];
|
|
1268
|
+
var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
|
|
1269
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
1270
|
+
function payErr(error, status = 400, code, details) {
|
|
1271
|
+
const body = { error };
|
|
1272
|
+
if (code) body.code = code;
|
|
1273
|
+
if (details !== void 0) body.details = details;
|
|
1274
|
+
return json(body, status);
|
|
1275
|
+
}
|
|
1276
|
+
function isBoltPaymentMethod(value) {
|
|
1277
|
+
return typeof value === "string" && BEAM_BOLT_PAYMENT_METHODS.includes(value);
|
|
1278
|
+
}
|
|
1279
|
+
function isBoltIntentStatus(value) {
|
|
1280
|
+
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
1247
1281
|
}
|
|
1248
1282
|
function nextCheckoutId() {
|
|
1249
1283
|
state.checkoutCounter += 1;
|
|
@@ -1253,6 +1287,18 @@ function nextPaymentLinkId() {
|
|
|
1253
1287
|
state.checkoutCounter += 1;
|
|
1254
1288
|
return `plink_test_${state.checkoutCounter}`;
|
|
1255
1289
|
}
|
|
1290
|
+
function nextBoltConnectionId() {
|
|
1291
|
+
state.boltConnectionCounter += 1;
|
|
1292
|
+
return `boltc_${state.boltConnectionCounter}`;
|
|
1293
|
+
}
|
|
1294
|
+
function nextBoltIntentId() {
|
|
1295
|
+
state.boltIntentCounter += 1;
|
|
1296
|
+
return `bolti_${state.boltIntentCounter}`;
|
|
1297
|
+
}
|
|
1298
|
+
function nextBoltChargeId() {
|
|
1299
|
+
state.boltChargeCounter += 1;
|
|
1300
|
+
return `chrg_${state.boltChargeCounter}`;
|
|
1301
|
+
}
|
|
1256
1302
|
function seedStripeSession(id, body) {
|
|
1257
1303
|
const lineItems = body.lineItems ?? [];
|
|
1258
1304
|
let amountTotal = null;
|
|
@@ -1296,6 +1342,58 @@ function seedBeamLink(id, body, merchantId) {
|
|
|
1296
1342
|
collectDeliveryAddress: body.collectDeliveryAddress === true
|
|
1297
1343
|
});
|
|
1298
1344
|
}
|
|
1345
|
+
function deriveBoltIntentStatus(storedStatus, expiresAt, now3 = /* @__PURE__ */ new Date()) {
|
|
1346
|
+
if (storedStatus !== "PENDING") return storedStatus;
|
|
1347
|
+
return expiresAt.getTime() <= now3.getTime() ? "EXPIRED" : "PENDING";
|
|
1348
|
+
}
|
|
1349
|
+
function toBoltIntentRecord(intent, now3 = /* @__PURE__ */ new Date()) {
|
|
1350
|
+
return {
|
|
1351
|
+
id: intent.id,
|
|
1352
|
+
beamIntentId: intent.beamIntentId,
|
|
1353
|
+
boltConnectionId: intent.boltConnectionId,
|
|
1354
|
+
amount: intent.amount,
|
|
1355
|
+
currency: intent.currency,
|
|
1356
|
+
paymentMethodType: intent.paymentMethodType,
|
|
1357
|
+
referenceId: intent.referenceId,
|
|
1358
|
+
internalNote: intent.internalNote,
|
|
1359
|
+
status: deriveBoltIntentStatus(intent.status, intent.expiresAt, now3),
|
|
1360
|
+
isVirtual: intent.isVirtual,
|
|
1361
|
+
environment: intent.environment,
|
|
1362
|
+
expiresAt: intent.expiresAt.toISOString(),
|
|
1363
|
+
settledAt: intent.settledAt ? intent.settledAt.toISOString() : null,
|
|
1364
|
+
chargeId: intent.chargeId,
|
|
1365
|
+
failureReason: intent.failureReason,
|
|
1366
|
+
createdAt: intent.createdAt.toISOString()
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
function findBoltConnection(connectionId) {
|
|
1370
|
+
return state.boltConnections.get(connectionId) ?? [...state.boltConnections.values()].find((c) => c.beamConnectionId === connectionId);
|
|
1371
|
+
}
|
|
1372
|
+
function findBoltIntent(intentId) {
|
|
1373
|
+
return state.boltIntents.get(intentId) ?? [...state.boltIntents.values()].find((i) => i.beamIntentId === intentId);
|
|
1374
|
+
}
|
|
1375
|
+
function requirePendingBoltIntent(intentId) {
|
|
1376
|
+
const intent = findBoltIntent(intentId);
|
|
1377
|
+
if (!intent) {
|
|
1378
|
+
throw new Error(`[stardeck-testing] Unknown bolt intent id: ${intentId}`);
|
|
1379
|
+
}
|
|
1380
|
+
const derived = deriveBoltIntentStatus(intent.status, intent.expiresAt);
|
|
1381
|
+
if (derived !== "PENDING") {
|
|
1382
|
+
throw new Error(`[stardeck-testing] Bolt intent ${intentId} is ${derived}, expected PENDING`);
|
|
1383
|
+
}
|
|
1384
|
+
return intent;
|
|
1385
|
+
}
|
|
1386
|
+
function storedStatusesForFilter(statuses) {
|
|
1387
|
+
const stored = /* @__PURE__ */ new Set();
|
|
1388
|
+
for (const status of statuses) {
|
|
1389
|
+
if (status === "EXPIRED" || status === "PENDING") {
|
|
1390
|
+
stored.add("PENDING");
|
|
1391
|
+
} else {
|
|
1392
|
+
stored.add(status);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return [...stored];
|
|
1396
|
+
}
|
|
1299
1397
|
async function deliverEvent(envelope, handler, options) {
|
|
1300
1398
|
const rawBody = JSON.stringify(envelope);
|
|
1301
1399
|
const secret = options?.deploymentSecret ?? TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET;
|
|
@@ -1318,9 +1416,219 @@ async function deliverEvent(envelope, handler, options) {
|
|
|
1318
1416
|
}
|
|
1319
1417
|
async function handlePaymentsRequest(request, url) {
|
|
1320
1418
|
const pathname = url.pathname;
|
|
1321
|
-
if (/\/
|
|
1419
|
+
if (/\/billing-portal$/.test(pathname)) {
|
|
1322
1420
|
return payErr("Not found", 404, "NOT_FOUND");
|
|
1323
1421
|
}
|
|
1422
|
+
const boltConnectionsMatch = pathname.match(
|
|
1423
|
+
/^\/api\/store\/beam\/([^/]+)\/bolt-connections(?:\/([^/]+))?$/
|
|
1424
|
+
);
|
|
1425
|
+
if (boltConnectionsMatch) {
|
|
1426
|
+
const connectionId = boltConnectionsMatch[2];
|
|
1427
|
+
if (!connectionId && request.method === "POST") {
|
|
1428
|
+
const body = await readJsonBody(request);
|
|
1429
|
+
const pairingCode = body.pairingCode ? String(body.pairingCode) : "";
|
|
1430
|
+
if (!pairingCode) {
|
|
1431
|
+
return payErr("pairingCode is required", 400);
|
|
1432
|
+
}
|
|
1433
|
+
if (state.boltUsedPairingCodes.has(pairingCode)) {
|
|
1434
|
+
return payErr("Pairing code has already been used", 400, "PAIRING_CODE_USED");
|
|
1435
|
+
}
|
|
1436
|
+
const id = nextBoltConnectionId();
|
|
1437
|
+
const now3 = (/* @__PURE__ */ new Date()).toISOString();
|
|
1438
|
+
const environments = Array.isArray(body.environments) ? body.environments.map(String) : ["sandbox"];
|
|
1439
|
+
const isSandbox = environments.some((e) => e === "sandbox" || e === "preview");
|
|
1440
|
+
const connection = {
|
|
1441
|
+
id,
|
|
1442
|
+
projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
|
|
1443
|
+
beamConnectionId: id,
|
|
1444
|
+
displayName: body.displayName != null ? String(body.displayName) : null,
|
|
1445
|
+
pairingCode,
|
|
1446
|
+
status: "ACTIVE",
|
|
1447
|
+
isSandbox,
|
|
1448
|
+
environments,
|
|
1449
|
+
createdAt: now3,
|
|
1450
|
+
updatedAt: now3
|
|
1451
|
+
};
|
|
1452
|
+
state.boltUsedPairingCodes.add(pairingCode);
|
|
1453
|
+
state.boltConnections.set(id, connection);
|
|
1454
|
+
return json({ connection });
|
|
1455
|
+
}
|
|
1456
|
+
if (!connectionId && request.method === "GET") {
|
|
1457
|
+
return json({ connections: [...state.boltConnections.values()] });
|
|
1458
|
+
}
|
|
1459
|
+
if (connectionId && request.method === "GET") {
|
|
1460
|
+
const connection = findBoltConnection(connectionId);
|
|
1461
|
+
if (!connection) return payErr("Bolt connection not found", 404, "NOT_FOUND");
|
|
1462
|
+
return json({ connection });
|
|
1463
|
+
}
|
|
1464
|
+
if (connectionId && request.method === "DELETE") {
|
|
1465
|
+
const connection = findBoltConnection(connectionId);
|
|
1466
|
+
if (!connection) return payErr("Bolt connection not found", 404, "NOT_FOUND");
|
|
1467
|
+
state.boltConnections.delete(connection.id);
|
|
1468
|
+
return json({ success: true });
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
const boltIntentsMatch = pathname.match(
|
|
1472
|
+
/^\/api\/store\/beam\/([^/]+)\/bolt-intents(?:\/([^/]+))?$/
|
|
1473
|
+
);
|
|
1474
|
+
if (boltIntentsMatch) {
|
|
1475
|
+
const intentId = boltIntentsMatch[2];
|
|
1476
|
+
if (!intentId && request.method === "POST") {
|
|
1477
|
+
const body = await readJsonBody(request);
|
|
1478
|
+
const issues = [];
|
|
1479
|
+
if (typeof body.amount !== "number" || !Number.isInteger(body.amount) || body.amount < 1) {
|
|
1480
|
+
issues.push({
|
|
1481
|
+
path: ["amount"],
|
|
1482
|
+
message: "Number must be greater than 0",
|
|
1483
|
+
code: "too_small"
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
const boltConnectionIdRaw = body.boltConnectionId;
|
|
1487
|
+
if (typeof boltConnectionIdRaw !== "string" || boltConnectionIdRaw.length < 1) {
|
|
1488
|
+
issues.push({
|
|
1489
|
+
path: ["boltConnectionId"],
|
|
1490
|
+
message: "String must contain at least 1 character(s)",
|
|
1491
|
+
code: "too_small"
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
const paymentMethod = body.paymentMethod;
|
|
1495
|
+
if (!paymentMethod || typeof paymentMethod !== "object") {
|
|
1496
|
+
issues.push({
|
|
1497
|
+
path: ["paymentMethod"],
|
|
1498
|
+
message: "Required",
|
|
1499
|
+
code: "invalid_type"
|
|
1500
|
+
});
|
|
1501
|
+
} else if (!isBoltPaymentMethod(paymentMethod.paymentMethodType)) {
|
|
1502
|
+
issues.push({
|
|
1503
|
+
path: ["paymentMethod", "paymentMethodType"],
|
|
1504
|
+
message: "Invalid enum value",
|
|
1505
|
+
code: "invalid_enum_value"
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
if (typeof body.expiryDurationInSec !== "number" || !Number.isInteger(body.expiryDurationInSec) || body.expiryDurationInSec < 90 || body.expiryDurationInSec > 600) {
|
|
1509
|
+
issues.push({
|
|
1510
|
+
path: ["expiryDurationInSec"],
|
|
1511
|
+
message: "Number must be between 90 and 600",
|
|
1512
|
+
code: "too_small"
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE.test(body.deploymentId)) {
|
|
1516
|
+
issues.push({
|
|
1517
|
+
path: ["deploymentId"],
|
|
1518
|
+
message: "Invalid uuid",
|
|
1519
|
+
code: "invalid_string"
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
if (issues.length > 0) {
|
|
1523
|
+
return payErr("Invalid request body", 400, void 0, issues);
|
|
1524
|
+
}
|
|
1525
|
+
const boltConnectionId = String(body.boltConnectionId);
|
|
1526
|
+
if (!findBoltConnection(boltConnectionId)) {
|
|
1527
|
+
return payErr("Bolt connection not found", 404, "NOT_FOUND");
|
|
1528
|
+
}
|
|
1529
|
+
const paymentMethodType = body.paymentMethod.paymentMethodType;
|
|
1530
|
+
const amount = body.amount;
|
|
1531
|
+
const currency = String(body.currency ?? "THB");
|
|
1532
|
+
const expiryDurationInSec = body.expiryDurationInSec;
|
|
1533
|
+
const id = nextBoltIntentId();
|
|
1534
|
+
const now3 = /* @__PURE__ */ new Date();
|
|
1535
|
+
const intent = {
|
|
1536
|
+
id,
|
|
1537
|
+
beamIntentId: id,
|
|
1538
|
+
boltConnectionId,
|
|
1539
|
+
amount,
|
|
1540
|
+
currency,
|
|
1541
|
+
paymentMethodType,
|
|
1542
|
+
referenceId: body.referenceId != null ? String(body.referenceId) : null,
|
|
1543
|
+
internalNote: body.internalNote != null ? String(body.internalNote) : null,
|
|
1544
|
+
status: "PENDING",
|
|
1545
|
+
isVirtual: false,
|
|
1546
|
+
environment: "sandbox",
|
|
1547
|
+
expiresAt: new Date(now3.getTime() + expiryDurationInSec * 1e3),
|
|
1548
|
+
settledAt: null,
|
|
1549
|
+
chargeId: null,
|
|
1550
|
+
failureReason: null,
|
|
1551
|
+
createdAt: now3
|
|
1552
|
+
};
|
|
1553
|
+
state.boltIntents.set(id, intent);
|
|
1554
|
+
return json({
|
|
1555
|
+
id,
|
|
1556
|
+
status: "PENDING",
|
|
1557
|
+
amount,
|
|
1558
|
+
currency,
|
|
1559
|
+
createdAt: now3.toISOString()
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
if (!intentId && request.method === "GET") {
|
|
1563
|
+
const deploymentId = url.searchParams.get("deploymentId");
|
|
1564
|
+
if (!deploymentId) {
|
|
1565
|
+
return payErr("Missing deploymentId parameter", 400);
|
|
1566
|
+
}
|
|
1567
|
+
const now3 = /* @__PURE__ */ new Date();
|
|
1568
|
+
const statusParam = url.searchParams.get("status");
|
|
1569
|
+
let requestedStatuses;
|
|
1570
|
+
if (statusParam) {
|
|
1571
|
+
const parts = statusParam.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1572
|
+
const parsed = [];
|
|
1573
|
+
for (const part of parts) {
|
|
1574
|
+
if (!isBoltIntentStatus(part)) {
|
|
1575
|
+
return payErr(`Invalid status: ${part}`, 400);
|
|
1576
|
+
}
|
|
1577
|
+
parsed.push(part);
|
|
1578
|
+
}
|
|
1579
|
+
requestedStatuses = parsed;
|
|
1580
|
+
}
|
|
1581
|
+
const boltConnectionId = url.searchParams.get("boltConnectionId") ?? void 0;
|
|
1582
|
+
const limitParam = url.searchParams.get("limit");
|
|
1583
|
+
let limit = 50;
|
|
1584
|
+
if (limitParam !== null) {
|
|
1585
|
+
const trimmed = limitParam.trim();
|
|
1586
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
1587
|
+
return payErr("limit must be an integer between 1 and 100", 400);
|
|
1588
|
+
}
|
|
1589
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
1590
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
|
|
1591
|
+
return payErr("limit must be an integer between 1 and 100", 400);
|
|
1592
|
+
}
|
|
1593
|
+
limit = parsed;
|
|
1594
|
+
}
|
|
1595
|
+
let intents = [...state.boltIntents.values()];
|
|
1596
|
+
if (boltConnectionId) {
|
|
1597
|
+
intents = intents.filter((i) => i.boltConnectionId === boltConnectionId);
|
|
1598
|
+
}
|
|
1599
|
+
if (requestedStatuses && requestedStatuses.length > 0) {
|
|
1600
|
+
const storedWanted = new Set(storedStatusesForFilter(requestedStatuses));
|
|
1601
|
+
intents = intents.filter((i) => storedWanted.has(i.status));
|
|
1602
|
+
}
|
|
1603
|
+
let records = intents.map((i) => toBoltIntentRecord(i, now3));
|
|
1604
|
+
if (requestedStatuses && requestedStatuses.length > 0) {
|
|
1605
|
+
const wanted = new Set(requestedStatuses);
|
|
1606
|
+
records = records.filter((r) => wanted.has(r.status));
|
|
1607
|
+
}
|
|
1608
|
+
records = records.slice(0, limit);
|
|
1609
|
+
return json({ intents: records });
|
|
1610
|
+
}
|
|
1611
|
+
if (intentId && request.method === "DELETE") {
|
|
1612
|
+
const intent = findBoltIntent(intentId);
|
|
1613
|
+
if (!intent) return payErr("Bolt intent not found", 404, "NOT_FOUND");
|
|
1614
|
+
const derived = deriveBoltIntentStatus(intent.status, intent.expiresAt);
|
|
1615
|
+
if (derived !== "PENDING") {
|
|
1616
|
+
return payErr(`Bolt intent is ${derived}, only PENDING intents can be canceled`, 409);
|
|
1617
|
+
}
|
|
1618
|
+
intent.status = "CANCELED";
|
|
1619
|
+
intent.settledAt = /* @__PURE__ */ new Date();
|
|
1620
|
+
return json({ success: true });
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
const chargesMatch = pathname.match(/^\/api\/store\/beam\/([^/]+)\/charges$/);
|
|
1624
|
+
if (chargesMatch && request.method === "GET") {
|
|
1625
|
+
const sourceId = url.searchParams.get("sourceId");
|
|
1626
|
+
if (!sourceId) {
|
|
1627
|
+
return payErr("Missing sourceId parameter", 400);
|
|
1628
|
+
}
|
|
1629
|
+
const charges = state.boltCharges.filter((c) => c.sourceId === sourceId);
|
|
1630
|
+
return json({ charges });
|
|
1631
|
+
}
|
|
1324
1632
|
const beamProductsMatch = pathname.match(
|
|
1325
1633
|
/^\/api\/store\/beam\/([^/]+)\/payment-links(?:\/([^/]+))?$/
|
|
1326
1634
|
);
|
|
@@ -1455,12 +1763,70 @@ function createPayments() {
|
|
|
1455
1763
|
};
|
|
1456
1764
|
return deliverEvent(envelope, handler, options);
|
|
1457
1765
|
},
|
|
1766
|
+
approveBoltIntent(intentId, options) {
|
|
1767
|
+
const intent = requirePendingBoltIntent(intentId);
|
|
1768
|
+
const chargeId = options?.chargeId ?? nextBoltChargeId();
|
|
1769
|
+
const charge = {
|
|
1770
|
+
id: chargeId,
|
|
1771
|
+
status: "SUCCEEDED",
|
|
1772
|
+
amount: intent.amount,
|
|
1773
|
+
currency: intent.currency,
|
|
1774
|
+
sourceId: intent.beamIntentId,
|
|
1775
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1776
|
+
};
|
|
1777
|
+
intent.status = "PAID";
|
|
1778
|
+
intent.chargeId = chargeId;
|
|
1779
|
+
intent.settledAt = /* @__PURE__ */ new Date();
|
|
1780
|
+
state.boltCharges.push(charge);
|
|
1781
|
+
return charge;
|
|
1782
|
+
},
|
|
1783
|
+
// No webhook on decline: Beam has no charge-failure event for bolt payments.
|
|
1784
|
+
declineBoltIntent(intentId, options) {
|
|
1785
|
+
const intent = requirePendingBoltIntent(intentId);
|
|
1786
|
+
const chargeId = nextBoltChargeId();
|
|
1787
|
+
const failureReason = options?.failureReason ?? "declined";
|
|
1788
|
+
const charge = {
|
|
1789
|
+
id: chargeId,
|
|
1790
|
+
status: "FAILED",
|
|
1791
|
+
amount: intent.amount,
|
|
1792
|
+
currency: intent.currency,
|
|
1793
|
+
sourceId: intent.beamIntentId,
|
|
1794
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1795
|
+
};
|
|
1796
|
+
intent.status = "FAILED";
|
|
1797
|
+
intent.chargeId = chargeId;
|
|
1798
|
+
intent.failureReason = failureReason;
|
|
1799
|
+
intent.settledAt = /* @__PURE__ */ new Date();
|
|
1800
|
+
state.boltCharges.push(charge);
|
|
1801
|
+
return charge;
|
|
1802
|
+
},
|
|
1803
|
+
// No webhook on expire: Beam does not notify apps when a bolt intent expires.
|
|
1804
|
+
expireBoltIntent(intentId) {
|
|
1805
|
+
const intent = requirePendingBoltIntent(intentId);
|
|
1806
|
+
intent.expiresAt = new Date(Date.now() - 1e3);
|
|
1807
|
+
},
|
|
1808
|
+
listBoltIntents() {
|
|
1809
|
+
const now3 = /* @__PURE__ */ new Date();
|
|
1810
|
+
return [...state.boltIntents.values()].map((i) => toBoltIntentRecord(i, now3));
|
|
1811
|
+
},
|
|
1812
|
+
getBoltIntent(intentId) {
|
|
1813
|
+
const intent = findBoltIntent(intentId);
|
|
1814
|
+
if (!intent) return void 0;
|
|
1815
|
+
return toBoltIntentRecord(intent);
|
|
1816
|
+
},
|
|
1458
1817
|
clear() {
|
|
1459
1818
|
state.checkouts = [];
|
|
1460
1819
|
state.checkoutCounter = 0;
|
|
1461
1820
|
state.sessionStatuses.clear();
|
|
1462
1821
|
state.paymentLinks.clear();
|
|
1463
1822
|
state.products = [];
|
|
1823
|
+
state.boltConnections.clear();
|
|
1824
|
+
state.boltUsedPairingCodes.clear();
|
|
1825
|
+
state.boltConnectionCounter = 0;
|
|
1826
|
+
state.boltIntents.clear();
|
|
1827
|
+
state.boltIntentCounter = 0;
|
|
1828
|
+
state.boltCharges = [];
|
|
1829
|
+
state.boltChargeCounter = 0;
|
|
1464
1830
|
},
|
|
1465
1831
|
get count() {
|
|
1466
1832
|
return state.checkouts.length;
|
|
@@ -2241,11 +2607,89 @@ function uninstallFetchRouter() {
|
|
|
2241
2607
|
}
|
|
2242
2608
|
}
|
|
2243
2609
|
|
|
2610
|
+
// src/data-store-manifest.ts
|
|
2611
|
+
var STARDECK_DATA_STORES_ENV = "STARDECK_DATA_STORES";
|
|
2612
|
+
function dataStoreSlug(name) {
|
|
2613
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2614
|
+
return slug || "store";
|
|
2615
|
+
}
|
|
2616
|
+
function buildTestDataStoreManifest(inputs = [{}]) {
|
|
2617
|
+
const takenSlugs = /* @__PURE__ */ new Set();
|
|
2618
|
+
const takenBindingKeys = /* @__PURE__ */ new Set();
|
|
2619
|
+
const takenIds = /* @__PURE__ */ new Set();
|
|
2620
|
+
return inputs.map((input, index) => {
|
|
2621
|
+
for (const field of ["bindingKey", "slug", "name"]) {
|
|
2622
|
+
if (input[field] === "") {
|
|
2623
|
+
throw new Error(`[stardeck-testing] dataStores[${index}].${field} cannot be empty.`);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
let slug;
|
|
2627
|
+
if (input.slug !== void 0) {
|
|
2628
|
+
slug = input.slug;
|
|
2629
|
+
if (takenSlugs.has(slug)) {
|
|
2630
|
+
throw new Error(`[stardeck-testing] dataStores[${index}].slug must be unique.`);
|
|
2631
|
+
}
|
|
2632
|
+
} else {
|
|
2633
|
+
const base = input.name !== void 0 ? dataStoreSlug(input.name) : index === 0 ? "main" : null;
|
|
2634
|
+
if (base === null) {
|
|
2635
|
+
throw new Error(
|
|
2636
|
+
`[stardeck-testing] dataStores[${index}] requires a slug or name. Only the first entry defaults to the "main" slug.`
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2639
|
+
slug = base;
|
|
2640
|
+
let suffix = 2;
|
|
2641
|
+
while (takenSlugs.has(slug)) {
|
|
2642
|
+
slug = `${base}_${suffix}`;
|
|
2643
|
+
suffix++;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
const id = input.id ?? `00000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}`;
|
|
2647
|
+
if (takenIds.has(id)) {
|
|
2648
|
+
throw new Error(`[stardeck-testing] dataStores[${index}].id must be unique.`);
|
|
2649
|
+
}
|
|
2650
|
+
if (input.bindingKey !== void 0 && takenBindingKeys.has(input.bindingKey)) {
|
|
2651
|
+
throw new Error(`[stardeck-testing] dataStores[${index}].bindingKey must be unique.`);
|
|
2652
|
+
}
|
|
2653
|
+
takenSlugs.add(slug);
|
|
2654
|
+
takenIds.add(id);
|
|
2655
|
+
if (input.bindingKey !== void 0) takenBindingKeys.add(input.bindingKey);
|
|
2656
|
+
const storeType = input.storeType ?? "database";
|
|
2657
|
+
return {
|
|
2658
|
+
id,
|
|
2659
|
+
name: input.name ?? slug,
|
|
2660
|
+
slug,
|
|
2661
|
+
storeType,
|
|
2662
|
+
accessLevel: input.accessLevel ?? "admin",
|
|
2663
|
+
...storeType === "database" ? { url: input.url ?? DATA_STORE_TEST_URL } : {},
|
|
2664
|
+
...input.bindingKey ? { bindingKey: input.bindingKey } : {}
|
|
2665
|
+
};
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2244
2669
|
// src/test-app.ts
|
|
2245
|
-
function applyTestEnv() {
|
|
2670
|
+
function applyTestEnv(options) {
|
|
2246
2671
|
for (const [key, value] of Object.entries(TEST_ENV_DEFAULTS)) {
|
|
2247
2672
|
process.env[key] = value;
|
|
2248
2673
|
}
|
|
2674
|
+
let manifestAdopted = true;
|
|
2675
|
+
if (options.dataStores !== void 0) {
|
|
2676
|
+
const manifest = JSON.stringify(buildTestDataStoreManifest(options.dataStores));
|
|
2677
|
+
process.env[STARDECK_DATA_STORES_ENV] = manifest;
|
|
2678
|
+
state.lastHarnessDataStoreManifest = manifest;
|
|
2679
|
+
} else {
|
|
2680
|
+
const currentManifest = process.env[STARDECK_DATA_STORES_ENV];
|
|
2681
|
+
if (!currentManifest || currentManifest === state.lastHarnessDataStoreManifest) {
|
|
2682
|
+
const manifest = JSON.stringify(buildTestDataStoreManifest());
|
|
2683
|
+
process.env[STARDECK_DATA_STORES_ENV] = manifest;
|
|
2684
|
+
state.lastHarnessDataStoreManifest = manifest;
|
|
2685
|
+
manifestAdopted = false;
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
if (options.legacyDataStoreUrl === true || !manifestAdopted && options.legacyDataStoreUrl === void 0) {
|
|
2689
|
+
process.env.DATA_STORE_URL = DATA_STORE_TEST_URL;
|
|
2690
|
+
} else {
|
|
2691
|
+
delete process.env.DATA_STORE_URL;
|
|
2692
|
+
}
|
|
2249
2693
|
delete process.env.DISABLE_AUTH;
|
|
2250
2694
|
delete process.env.NEXT_PUBLIC_DISABLE_AUTH;
|
|
2251
2695
|
delete process.env.SANDBOX_MODE;
|
|
@@ -2278,7 +2722,7 @@ async function createTestApp(options = {}) {
|
|
|
2278
2722
|
"[stardeck-testing] A test app is already active in this test file. Call `app.close()` first, or share one app per file."
|
|
2279
2723
|
);
|
|
2280
2724
|
}
|
|
2281
|
-
applyTestEnv();
|
|
2725
|
+
applyTestEnv(options);
|
|
2282
2726
|
state.allowNetwork = options.allowNetwork ?? false;
|
|
2283
2727
|
installFetchRouter();
|
|
2284
2728
|
const db = await createPgliteDatabase();
|
|
@@ -2380,11 +2824,14 @@ function parseWorkflowName(describeTitle) {
|
|
|
2380
2824
|
0 && (module.exports = {
|
|
2381
2825
|
CONTROL_PLANE_TEST_URL,
|
|
2382
2826
|
DATA_STORE_TEST_HOST,
|
|
2827
|
+
DATA_STORE_TEST_URL,
|
|
2383
2828
|
DEFAULT_SCHEMA_PATH,
|
|
2829
|
+
STARDECK_DATA_STORES_ENV,
|
|
2384
2830
|
STORAGE_TEST_HOST,
|
|
2385
2831
|
STORAGE_TEST_URL,
|
|
2386
2832
|
TEST_ENV_DEFAULTS,
|
|
2387
2833
|
WORKFLOW_NAME_PREFIX,
|
|
2834
|
+
buildTestDataStoreManifest,
|
|
2388
2835
|
callRoute,
|
|
2389
2836
|
createTestApp,
|
|
2390
2837
|
describeWorkflow,
|