@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/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
- function payErr(error, status = 400, code) {
1246
- return json(code ? { error, code } : { error }, status);
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 (/\/bolt-connections/.test(pathname) || /\/bolt-intents/.test(pathname) || /\/charges(\/|$)/.test(pathname) || /\/billing-portal$/.test(pathname)) {
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,