@stardeck-customer-apps/testing 0.23.0 → 0.25.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 +12 -1
- package/dist/index.d.mts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +170 -7
- package/dist/index.mjs +170 -7
- package/dist/next/headers-shim.js +3 -0
- package/dist/next/headers-shim.mjs +3 -0
- package/dist/setup.js +160 -6
- package/dist/setup.mjs +160 -6
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -208,6 +208,14 @@ app.payments.failNext("stripe.getCheckoutSessionStatus", {
|
|
|
208
208
|
message: "connection reset",
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
+
// Stripe createCheckout idempotency + expiry (payments-sdk >= 4.11.0) is simulated like the
|
|
212
|
+
// platform: same idempotencyKey + same payload replays the same session (one captured
|
|
213
|
+
// checkout); same key + different payload -> PaymentError "idempotency_conflict" (409); keys
|
|
214
|
+
// are scoped per account; expiresAt (unix seconds) must be 30 min - 24 h out or
|
|
215
|
+
// "expiry_out_of_range" (400); the default is 24 h and session.expiresAt is returned.
|
|
216
|
+
// Beam payment links reject idempotencyKey ("idempotency_unsupported") and echo expiresAt.
|
|
217
|
+
// The simulator does not model Stripe's 24 h key retention.
|
|
218
|
+
|
|
211
219
|
// File upload
|
|
212
220
|
const storage = new StorageClient();
|
|
213
221
|
await storage.upload(new File(["hi"], "note.txt", { type: "text/plain" }));
|
|
@@ -557,7 +565,10 @@ cursor, limit })`; `list()` intentionally keeps its previous array shape.
|
|
|
557
565
|
in `app.storage` against the simulated `STORAGE_URL` host.
|
|
558
566
|
- `client.slack` / `client.line` / `client.facebook` send endpoints — captured
|
|
559
567
|
in `app.messages`.
|
|
560
|
-
- `createEdgeClient()` (print,
|
|
568
|
+
- `createEdgeClient()` (print, `enqueueReceipt()`/`getPrintJob()` durable receipts: idempotent per key,
|
|
569
|
+
conflicts and refused options mirrored, `app.edge.durablePrints`, and
|
|
570
|
+
`app.edge.settleNextDurableJob({ status: "needs_attention", reason, outcome })` to drive the
|
|
571
|
+
uncertain/not-printed UI; pair/unpair, list devices/peripherals, show/clear
|
|
561
572
|
display, test print, `getCapabilities()`) — captured in `app.edge`; seed
|
|
562
573
|
devices/peripherals for pairing pickers via `app.edge.seedDevices()` /
|
|
563
574
|
`seedPeripherals()`.
|
package/dist/index.d.mts
CHANGED
|
@@ -542,6 +542,27 @@ interface CapturedPrint {
|
|
|
542
542
|
logo?: boolean;
|
|
543
543
|
copies: number;
|
|
544
544
|
}
|
|
545
|
+
/** One `edge.enqueueReceipt()` the app made, as the simulator accepted it. */
|
|
546
|
+
interface CapturedDurablePrint {
|
|
547
|
+
jobId: string;
|
|
548
|
+
idempotencyKey: string;
|
|
549
|
+
alias?: string;
|
|
550
|
+
deviceId?: string;
|
|
551
|
+
peripheralId?: string;
|
|
552
|
+
receipt: ReceiptPayload;
|
|
553
|
+
logo?: boolean;
|
|
554
|
+
}
|
|
555
|
+
/** How a simulated durable job settles. Mirrors the platform's status contract. */
|
|
556
|
+
type SimulatedDurableOutcome = {
|
|
557
|
+
status: "sent_to_printer";
|
|
558
|
+
} | {
|
|
559
|
+
status: "queued";
|
|
560
|
+
} | {
|
|
561
|
+
status: "needs_attention";
|
|
562
|
+
reason: string;
|
|
563
|
+
outcome: "not_printed" | "uncertain";
|
|
564
|
+
message?: string;
|
|
565
|
+
};
|
|
545
566
|
interface CapturedDisplay {
|
|
546
567
|
action: "show" | "clear";
|
|
547
568
|
alias?: string;
|
|
@@ -557,6 +578,15 @@ interface TestEdge {
|
|
|
557
578
|
get prints(): CapturedPrint[];
|
|
558
579
|
get displays(): CapturedDisplay[];
|
|
559
580
|
get testPrints(): CapturedTestPrint[];
|
|
581
|
+
/** Durable receipts accepted by `edge.enqueueReceipt()` (one per distinct idempotency key). */
|
|
582
|
+
get durablePrints(): CapturedDurablePrint[];
|
|
583
|
+
/**
|
|
584
|
+
* Decide how the NEXT accepted durable job settles when the app reads it with
|
|
585
|
+
* `edge.getPrintJob()`. Without this every job reads `sent_to_printer`. Queue
|
|
586
|
+
* several to model a run of jobs; use `needs_attention` with `uncertain` to
|
|
587
|
+
* test the "check the printer before reprinting" path.
|
|
588
|
+
*/
|
|
589
|
+
settleNextDurableJob(outcome: SimulatedDurableOutcome): void;
|
|
560
590
|
latestPrint(): CapturedPrint | undefined;
|
|
561
591
|
latestDisplay(): CapturedDisplay | undefined;
|
|
562
592
|
get bindings(): BindingInfo[];
|
|
@@ -742,4 +772,4 @@ declare const TEST_ENV_DEFAULTS: {
|
|
|
742
772
|
/** Default location of the DDL snapshot written by `generate-types`. */
|
|
743
773
|
declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
|
|
744
774
|
|
|
745
|
-
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedStarlensCall, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, type StarlensReply, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStarlens, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
|
775
|
+
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedDurablePrint, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedStarlensCall, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, type SimulatedDurableOutcome, type StarlensReply, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStarlens, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
package/dist/index.d.ts
CHANGED
|
@@ -542,6 +542,27 @@ interface CapturedPrint {
|
|
|
542
542
|
logo?: boolean;
|
|
543
543
|
copies: number;
|
|
544
544
|
}
|
|
545
|
+
/** One `edge.enqueueReceipt()` the app made, as the simulator accepted it. */
|
|
546
|
+
interface CapturedDurablePrint {
|
|
547
|
+
jobId: string;
|
|
548
|
+
idempotencyKey: string;
|
|
549
|
+
alias?: string;
|
|
550
|
+
deviceId?: string;
|
|
551
|
+
peripheralId?: string;
|
|
552
|
+
receipt: ReceiptPayload;
|
|
553
|
+
logo?: boolean;
|
|
554
|
+
}
|
|
555
|
+
/** How a simulated durable job settles. Mirrors the platform's status contract. */
|
|
556
|
+
type SimulatedDurableOutcome = {
|
|
557
|
+
status: "sent_to_printer";
|
|
558
|
+
} | {
|
|
559
|
+
status: "queued";
|
|
560
|
+
} | {
|
|
561
|
+
status: "needs_attention";
|
|
562
|
+
reason: string;
|
|
563
|
+
outcome: "not_printed" | "uncertain";
|
|
564
|
+
message?: string;
|
|
565
|
+
};
|
|
545
566
|
interface CapturedDisplay {
|
|
546
567
|
action: "show" | "clear";
|
|
547
568
|
alias?: string;
|
|
@@ -557,6 +578,15 @@ interface TestEdge {
|
|
|
557
578
|
get prints(): CapturedPrint[];
|
|
558
579
|
get displays(): CapturedDisplay[];
|
|
559
580
|
get testPrints(): CapturedTestPrint[];
|
|
581
|
+
/** Durable receipts accepted by `edge.enqueueReceipt()` (one per distinct idempotency key). */
|
|
582
|
+
get durablePrints(): CapturedDurablePrint[];
|
|
583
|
+
/**
|
|
584
|
+
* Decide how the NEXT accepted durable job settles when the app reads it with
|
|
585
|
+
* `edge.getPrintJob()`. Without this every job reads `sent_to_printer`. Queue
|
|
586
|
+
* several to model a run of jobs; use `needs_attention` with `uncertain` to
|
|
587
|
+
* test the "check the printer before reprinting" path.
|
|
588
|
+
*/
|
|
589
|
+
settleNextDurableJob(outcome: SimulatedDurableOutcome): void;
|
|
560
590
|
latestPrint(): CapturedPrint | undefined;
|
|
561
591
|
latestDisplay(): CapturedDisplay | undefined;
|
|
562
592
|
get bindings(): BindingInfo[];
|
|
@@ -742,4 +772,4 @@ declare const TEST_ENV_DEFAULTS: {
|
|
|
742
772
|
/** Default location of the DDL snapshot written by `generate-types`. */
|
|
743
773
|
declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
|
|
744
774
|
|
|
745
|
-
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedStarlensCall, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, type StarlensReply, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStarlens, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
|
775
|
+
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedDurablePrint, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedStarlensCall, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, type SimulatedDurableOutcome, type StarlensReply, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStarlens, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
package/dist/index.js
CHANGED
|
@@ -340,6 +340,7 @@ var state = globalSingleton("state", () => ({
|
|
|
340
340
|
checkouts: [],
|
|
341
341
|
checkoutCounter: 0,
|
|
342
342
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
343
|
+
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
343
344
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
344
345
|
paymentFailures: [],
|
|
345
346
|
products: [],
|
|
@@ -364,6 +365,8 @@ var state = globalSingleton("state", () => ({
|
|
|
364
365
|
edgeDisplays: [],
|
|
365
366
|
edgeTestPrints: [],
|
|
366
367
|
edgePrintCounter: 0,
|
|
368
|
+
edgeDurableJobs: /* @__PURE__ */ new Map(),
|
|
369
|
+
edgeDurableOutcomes: [],
|
|
367
370
|
edgeDevices: [],
|
|
368
371
|
edgePeripherals: [],
|
|
369
372
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
@@ -2131,6 +2134,8 @@ function signEventDelivery(secret, context, rawBody) {
|
|
|
2131
2134
|
}
|
|
2132
2135
|
|
|
2133
2136
|
// src/simulator/payments.ts
|
|
2137
|
+
var STRIPE_MIN_EXPIRY_SECONDS = 30 * 60;
|
|
2138
|
+
var STRIPE_MAX_EXPIRY_SECONDS = 24 * 60 * 60;
|
|
2134
2139
|
var BEAM_BOLT_PAYMENT_METHODS = [
|
|
2135
2140
|
"CARD",
|
|
2136
2141
|
"CARD_INSTALLMENTS",
|
|
@@ -2200,6 +2205,15 @@ function isBoltPaymentMethod(value) {
|
|
|
2200
2205
|
function isBoltIntentStatus(value) {
|
|
2201
2206
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
2202
2207
|
}
|
|
2208
|
+
function stableStringify(value) {
|
|
2209
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
2210
|
+
if (value && typeof value === "object") {
|
|
2211
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2212
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
2213
|
+
}
|
|
2214
|
+
return JSON.stringify(value);
|
|
2215
|
+
}
|
|
2216
|
+
var ISO_8601_WITH_ZONE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
|
|
2203
2217
|
function nextCheckoutId() {
|
|
2204
2218
|
state.checkoutCounter += 1;
|
|
2205
2219
|
return `cs_test_${state.checkoutCounter}`;
|
|
@@ -2220,7 +2234,7 @@ function nextBoltChargeId() {
|
|
|
2220
2234
|
state.boltChargeCounter += 1;
|
|
2221
2235
|
return `chrg_${state.boltChargeCounter}`;
|
|
2222
2236
|
}
|
|
2223
|
-
function seedStripeSession(id, body) {
|
|
2237
|
+
function seedStripeSession(id, body, expiresAt) {
|
|
2224
2238
|
const lineItems = body.lineItems ?? [];
|
|
2225
2239
|
let amountTotal = null;
|
|
2226
2240
|
let currency = null;
|
|
@@ -2240,7 +2254,7 @@ function seedStripeSession(id, body) {
|
|
|
2240
2254
|
currency,
|
|
2241
2255
|
customerEmail: body.customerEmail ? String(body.customerEmail) : null,
|
|
2242
2256
|
metadata: body.metadata ?? {},
|
|
2243
|
-
expiresAt
|
|
2257
|
+
expiresAt
|
|
2244
2258
|
});
|
|
2245
2259
|
}
|
|
2246
2260
|
function seedBeamLink(id, body, merchantId) {
|
|
@@ -2260,7 +2274,8 @@ function seedBeamLink(id, body, merchantId) {
|
|
|
2260
2274
|
},
|
|
2261
2275
|
redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
|
|
2262
2276
|
linkSettings: body.linkSettings,
|
|
2263
|
-
collectDeliveryAddress: body.collectDeliveryAddress === true
|
|
2277
|
+
collectDeliveryAddress: body.collectDeliveryAddress === true,
|
|
2278
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
2264
2279
|
});
|
|
2265
2280
|
}
|
|
2266
2281
|
function deriveBoltIntentStatus(storedStatus, expiresAt, now3 = /* @__PURE__ */ new Date()) {
|
|
@@ -2562,6 +2577,16 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2562
2577
|
const linkId = beamProductsMatch[2];
|
|
2563
2578
|
if (!linkId && request.method === "POST") {
|
|
2564
2579
|
const body = await readJsonBody(request);
|
|
2580
|
+
if ("idempotencyKey" in body) {
|
|
2581
|
+
return payErr(
|
|
2582
|
+
"idempotencyKey is not supported for Beam payment links.",
|
|
2583
|
+
400,
|
|
2584
|
+
"idempotency_unsupported"
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
if (typeof body.expiresAt === "string" && (!ISO_8601_WITH_ZONE.test(body.expiresAt) || Number.isNaN(Date.parse(body.expiresAt)))) {
|
|
2588
|
+
return payErr("expiresAt must be an ISO 8601 timestamp", 400);
|
|
2589
|
+
}
|
|
2565
2590
|
const id = nextPaymentLinkId();
|
|
2566
2591
|
const checkoutUrl = `https://beam.test/pay/${id}`;
|
|
2567
2592
|
const captured = {
|
|
@@ -2574,7 +2599,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2574
2599
|
};
|
|
2575
2600
|
state.checkouts.push(captured);
|
|
2576
2601
|
seedBeamLink(id, body, merchantId);
|
|
2577
|
-
return json({
|
|
2602
|
+
return json({
|
|
2603
|
+
id,
|
|
2604
|
+
url: checkoutUrl,
|
|
2605
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
2606
|
+
});
|
|
2578
2607
|
}
|
|
2579
2608
|
if (linkId && request.method === "GET") {
|
|
2580
2609
|
const link = state.paymentLinks.get(linkId);
|
|
@@ -2600,6 +2629,34 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2600
2629
|
}
|
|
2601
2630
|
if (subPath === "checkout" && request.method === "POST") {
|
|
2602
2631
|
const body = await readJsonBody(request);
|
|
2632
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
2633
|
+
const requestedExpiry = typeof body.expiresAt === "number" ? body.expiresAt : void 0;
|
|
2634
|
+
const callerKey = typeof body.idempotencyKey === "string" ? body.idempotencyKey : void 0;
|
|
2635
|
+
const scopedKey = callerKey ? JSON.stringify(["stripe", accountId, callerKey]) : void 0;
|
|
2636
|
+
const comparable = { ...body };
|
|
2637
|
+
delete comparable.deploymentId;
|
|
2638
|
+
delete comparable.idempotencyKey;
|
|
2639
|
+
const fingerprint = stableStringify(comparable);
|
|
2640
|
+
if (scopedKey) {
|
|
2641
|
+
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
2642
|
+
if (saved) {
|
|
2643
|
+
if (saved.fingerprint !== fingerprint) {
|
|
2644
|
+
return payErr(
|
|
2645
|
+
"This idempotency key was already used with different checkout parameters. Use a new key for a different checkout.",
|
|
2646
|
+
409,
|
|
2647
|
+
"idempotency_conflict"
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
return json(saved.response);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
if (requestedExpiry !== void 0 && (requestedExpiry < nowSeconds + STRIPE_MIN_EXPIRY_SECONDS || requestedExpiry > nowSeconds + STRIPE_MAX_EXPIRY_SECONDS)) {
|
|
2654
|
+
return payErr(
|
|
2655
|
+
"expiresAt must be between 30 minutes and 24 hours after the checkout is created (Unix seconds).",
|
|
2656
|
+
400,
|
|
2657
|
+
"expiry_out_of_range"
|
|
2658
|
+
);
|
|
2659
|
+
}
|
|
2603
2660
|
const id = nextCheckoutId();
|
|
2604
2661
|
const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
|
|
2605
2662
|
const captured = {
|
|
@@ -2612,8 +2669,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2612
2669
|
createdAt: /* @__PURE__ */ new Date()
|
|
2613
2670
|
};
|
|
2614
2671
|
state.checkouts.push(captured);
|
|
2615
|
-
|
|
2616
|
-
|
|
2672
|
+
const expiresAt = requestedExpiry ?? nowSeconds + STRIPE_MAX_EXPIRY_SECONDS;
|
|
2673
|
+
seedStripeSession(id, body, expiresAt);
|
|
2674
|
+
const response = { id, url: checkoutUrl, expiresAt };
|
|
2675
|
+
if (scopedKey) state.idempotentCheckouts.set(scopedKey, { fingerprint, response });
|
|
2676
|
+
return json(response);
|
|
2617
2677
|
}
|
|
2618
2678
|
const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
|
|
2619
2679
|
if (sessionMatch && request.method === "GET") {
|
|
@@ -2778,6 +2838,7 @@ function createPayments() {
|
|
|
2778
2838
|
state.checkouts = [];
|
|
2779
2839
|
state.checkoutCounter = 0;
|
|
2780
2840
|
state.sessionStatuses.clear();
|
|
2841
|
+
state.idempotentCheckouts.clear();
|
|
2781
2842
|
state.paymentLinks.clear();
|
|
2782
2843
|
state.paymentFailures = [];
|
|
2783
2844
|
state.products = [];
|
|
@@ -3323,6 +3384,93 @@ async function handlePrint(request) {
|
|
|
3323
3384
|
status: "completed"
|
|
3324
3385
|
});
|
|
3325
3386
|
}
|
|
3387
|
+
var IDEMPOTENCY_KEY = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
3388
|
+
async function handleEnqueue(request) {
|
|
3389
|
+
const body = await readJsonBody(request);
|
|
3390
|
+
const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : "";
|
|
3391
|
+
if (!IDEMPOTENCY_KEY.test(key)) {
|
|
3392
|
+
return edgeFailure("idempotencyKey must be 1-128 characters of A-Z a-z 0-9 _ . : -");
|
|
3393
|
+
}
|
|
3394
|
+
if (body.openDrawer === true) {
|
|
3395
|
+
return edgeFailure("Durable receipts cannot open the cash drawer; a retry would kick it again");
|
|
3396
|
+
}
|
|
3397
|
+
if (body.copies !== void 0 && body.copies !== 1) {
|
|
3398
|
+
return edgeFailure(
|
|
3399
|
+
"Durable receipts print exactly one copy; submit a second job for another copy"
|
|
3400
|
+
);
|
|
3401
|
+
}
|
|
3402
|
+
const receipt = body.receipt ?? {};
|
|
3403
|
+
if (receipt.rawEscPos !== void 0) {
|
|
3404
|
+
return edgeFailure("rawEscPos is not supported for durable receipts");
|
|
3405
|
+
}
|
|
3406
|
+
const requestHash = JSON.stringify([
|
|
3407
|
+
body.alias,
|
|
3408
|
+
body.deviceId,
|
|
3409
|
+
body.peripheralId,
|
|
3410
|
+
receipt,
|
|
3411
|
+
body.logo
|
|
3412
|
+
]);
|
|
3413
|
+
const existing = [...state.edgeDurableJobs.values()].find(
|
|
3414
|
+
(j) => j.captured.idempotencyKey === key
|
|
3415
|
+
);
|
|
3416
|
+
if (existing) {
|
|
3417
|
+
if (existing.requestHash !== requestHash) {
|
|
3418
|
+
return edgeFailure(
|
|
3419
|
+
"This idempotencyKey was already used for a different receipt, target or options.",
|
|
3420
|
+
409,
|
|
3421
|
+
"IDEMPOTENCY_KEY_CONFLICT"
|
|
3422
|
+
);
|
|
3423
|
+
}
|
|
3424
|
+
return json({ success: true, data: durableView(existing.captured.jobId, "queued") }, 200);
|
|
3425
|
+
}
|
|
3426
|
+
const jobId = nextJobId();
|
|
3427
|
+
const captured = {
|
|
3428
|
+
jobId,
|
|
3429
|
+
idempotencyKey: key,
|
|
3430
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
3431
|
+
deviceId: body.deviceId ? String(body.deviceId) : void 0,
|
|
3432
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
|
|
3433
|
+
receipt,
|
|
3434
|
+
logo: body.logo === true ? true : body.logo === false ? false : void 0
|
|
3435
|
+
};
|
|
3436
|
+
const outcome = state.edgeDurableOutcomes.shift() ?? { status: "sent_to_printer" };
|
|
3437
|
+
state.edgeDurableJobs.set(jobId, { requestHash, captured, outcome });
|
|
3438
|
+
return json({ success: true, data: durableView(jobId, "queued") }, 202);
|
|
3439
|
+
}
|
|
3440
|
+
function durableView(jobId, as) {
|
|
3441
|
+
const job = state.edgeDurableJobs.get(jobId);
|
|
3442
|
+
const createdAt = now2();
|
|
3443
|
+
const base = {
|
|
3444
|
+
jobId,
|
|
3445
|
+
attempts: as === "queued" ? 0 : 1,
|
|
3446
|
+
createdAt,
|
|
3447
|
+
expiresAt: new Date(Date.parse(createdAt) + 10 * 6e4).toISOString(),
|
|
3448
|
+
target: {
|
|
3449
|
+
deviceId: job.captured.deviceId ?? "sim-device",
|
|
3450
|
+
peripheralId: job.captured.peripheralId ?? "sim-peripheral"
|
|
3451
|
+
}
|
|
3452
|
+
};
|
|
3453
|
+
const outcome = as === "queued" ? { status: "queued" } : job.outcome;
|
|
3454
|
+
if (outcome.status === "needs_attention") {
|
|
3455
|
+
return {
|
|
3456
|
+
...base,
|
|
3457
|
+
status: "needs_attention",
|
|
3458
|
+
attention: {
|
|
3459
|
+
reason: outcome.reason,
|
|
3460
|
+
outcome: outcome.outcome,
|
|
3461
|
+
message: outcome.message ?? "Delivery stopped."
|
|
3462
|
+
}
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
if (outcome.status === "sent_to_printer") {
|
|
3466
|
+
return { ...base, status: "sent_to_printer", sentToPrinterAt: createdAt };
|
|
3467
|
+
}
|
|
3468
|
+
return { ...base, status: "queued" };
|
|
3469
|
+
}
|
|
3470
|
+
function handleGetDurable(jobId) {
|
|
3471
|
+
if (!state.edgeDurableJobs.has(jobId)) return edgeFailure("Print job not found", 404);
|
|
3472
|
+
return success(durableView(jobId));
|
|
3473
|
+
}
|
|
3326
3474
|
async function handleShowDisplay(request) {
|
|
3327
3475
|
const body = await readJsonBody(request);
|
|
3328
3476
|
const captured = {
|
|
@@ -3372,6 +3520,13 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
3372
3520
|
if (subPath === "/print" && method === "POST") {
|
|
3373
3521
|
return handlePrint(request);
|
|
3374
3522
|
}
|
|
3523
|
+
if (subPath === "/print-jobs" && method === "POST") {
|
|
3524
|
+
return handleEnqueue(request);
|
|
3525
|
+
}
|
|
3526
|
+
const durableMatch = subPath.match(/^\/print-jobs\/([^/]+)$/);
|
|
3527
|
+
if (durableMatch && method === "GET") {
|
|
3528
|
+
return handleGetDurable(decodeURIComponent(durableMatch[1]));
|
|
3529
|
+
}
|
|
3375
3530
|
if (subPath === "/capabilities" && method === "GET") {
|
|
3376
3531
|
return handleCapabilities(request);
|
|
3377
3532
|
}
|
|
@@ -3415,6 +3570,12 @@ function createEdge() {
|
|
|
3415
3570
|
get testPrints() {
|
|
3416
3571
|
return [...state.edgeTestPrints];
|
|
3417
3572
|
},
|
|
3573
|
+
get durablePrints() {
|
|
3574
|
+
return [...state.edgeDurableJobs.values()].map((j) => j.captured);
|
|
3575
|
+
},
|
|
3576
|
+
settleNextDurableJob(outcome) {
|
|
3577
|
+
state.edgeDurableOutcomes.push(outcome);
|
|
3578
|
+
},
|
|
3418
3579
|
latestPrint() {
|
|
3419
3580
|
return state.edgePrints[state.edgePrints.length - 1];
|
|
3420
3581
|
},
|
|
@@ -3451,12 +3612,14 @@ function createEdge() {
|
|
|
3451
3612
|
state.edgeDisplays = [];
|
|
3452
3613
|
state.edgeTestPrints = [];
|
|
3453
3614
|
state.edgePrintCounter = 0;
|
|
3615
|
+
state.edgeDurableJobs.clear();
|
|
3616
|
+
state.edgeDurableOutcomes = [];
|
|
3454
3617
|
state.edgeDevices = [];
|
|
3455
3618
|
state.edgePeripherals = [];
|
|
3456
3619
|
state.edgeBindings.clear();
|
|
3457
3620
|
},
|
|
3458
3621
|
get count() {
|
|
3459
|
-
return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length;
|
|
3622
|
+
return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length + state.edgeDurableJobs.size;
|
|
3460
3623
|
}
|
|
3461
3624
|
};
|
|
3462
3625
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -301,6 +301,7 @@ var state = globalSingleton("state", () => ({
|
|
|
301
301
|
checkouts: [],
|
|
302
302
|
checkoutCounter: 0,
|
|
303
303
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
304
|
+
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
304
305
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
305
306
|
paymentFailures: [],
|
|
306
307
|
products: [],
|
|
@@ -325,6 +326,8 @@ var state = globalSingleton("state", () => ({
|
|
|
325
326
|
edgeDisplays: [],
|
|
326
327
|
edgeTestPrints: [],
|
|
327
328
|
edgePrintCounter: 0,
|
|
329
|
+
edgeDurableJobs: /* @__PURE__ */ new Map(),
|
|
330
|
+
edgeDurableOutcomes: [],
|
|
328
331
|
edgeDevices: [],
|
|
329
332
|
edgePeripherals: [],
|
|
330
333
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
@@ -2092,6 +2095,8 @@ function signEventDelivery(secret, context, rawBody) {
|
|
|
2092
2095
|
}
|
|
2093
2096
|
|
|
2094
2097
|
// src/simulator/payments.ts
|
|
2098
|
+
var STRIPE_MIN_EXPIRY_SECONDS = 30 * 60;
|
|
2099
|
+
var STRIPE_MAX_EXPIRY_SECONDS = 24 * 60 * 60;
|
|
2095
2100
|
var BEAM_BOLT_PAYMENT_METHODS = [
|
|
2096
2101
|
"CARD",
|
|
2097
2102
|
"CARD_INSTALLMENTS",
|
|
@@ -2161,6 +2166,15 @@ function isBoltPaymentMethod(value) {
|
|
|
2161
2166
|
function isBoltIntentStatus(value) {
|
|
2162
2167
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
2163
2168
|
}
|
|
2169
|
+
function stableStringify(value) {
|
|
2170
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
2171
|
+
if (value && typeof value === "object") {
|
|
2172
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2173
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
2174
|
+
}
|
|
2175
|
+
return JSON.stringify(value);
|
|
2176
|
+
}
|
|
2177
|
+
var ISO_8601_WITH_ZONE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
|
|
2164
2178
|
function nextCheckoutId() {
|
|
2165
2179
|
state.checkoutCounter += 1;
|
|
2166
2180
|
return `cs_test_${state.checkoutCounter}`;
|
|
@@ -2181,7 +2195,7 @@ function nextBoltChargeId() {
|
|
|
2181
2195
|
state.boltChargeCounter += 1;
|
|
2182
2196
|
return `chrg_${state.boltChargeCounter}`;
|
|
2183
2197
|
}
|
|
2184
|
-
function seedStripeSession(id, body) {
|
|
2198
|
+
function seedStripeSession(id, body, expiresAt) {
|
|
2185
2199
|
const lineItems = body.lineItems ?? [];
|
|
2186
2200
|
let amountTotal = null;
|
|
2187
2201
|
let currency = null;
|
|
@@ -2201,7 +2215,7 @@ function seedStripeSession(id, body) {
|
|
|
2201
2215
|
currency,
|
|
2202
2216
|
customerEmail: body.customerEmail ? String(body.customerEmail) : null,
|
|
2203
2217
|
metadata: body.metadata ?? {},
|
|
2204
|
-
expiresAt
|
|
2218
|
+
expiresAt
|
|
2205
2219
|
});
|
|
2206
2220
|
}
|
|
2207
2221
|
function seedBeamLink(id, body, merchantId) {
|
|
@@ -2221,7 +2235,8 @@ function seedBeamLink(id, body, merchantId) {
|
|
|
2221
2235
|
},
|
|
2222
2236
|
redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
|
|
2223
2237
|
linkSettings: body.linkSettings,
|
|
2224
|
-
collectDeliveryAddress: body.collectDeliveryAddress === true
|
|
2238
|
+
collectDeliveryAddress: body.collectDeliveryAddress === true,
|
|
2239
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
2225
2240
|
});
|
|
2226
2241
|
}
|
|
2227
2242
|
function deriveBoltIntentStatus(storedStatus, expiresAt, now3 = /* @__PURE__ */ new Date()) {
|
|
@@ -2523,6 +2538,16 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2523
2538
|
const linkId = beamProductsMatch[2];
|
|
2524
2539
|
if (!linkId && request.method === "POST") {
|
|
2525
2540
|
const body = await readJsonBody(request);
|
|
2541
|
+
if ("idempotencyKey" in body) {
|
|
2542
|
+
return payErr(
|
|
2543
|
+
"idempotencyKey is not supported for Beam payment links.",
|
|
2544
|
+
400,
|
|
2545
|
+
"idempotency_unsupported"
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
if (typeof body.expiresAt === "string" && (!ISO_8601_WITH_ZONE.test(body.expiresAt) || Number.isNaN(Date.parse(body.expiresAt)))) {
|
|
2549
|
+
return payErr("expiresAt must be an ISO 8601 timestamp", 400);
|
|
2550
|
+
}
|
|
2526
2551
|
const id = nextPaymentLinkId();
|
|
2527
2552
|
const checkoutUrl = `https://beam.test/pay/${id}`;
|
|
2528
2553
|
const captured = {
|
|
@@ -2535,7 +2560,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2535
2560
|
};
|
|
2536
2561
|
state.checkouts.push(captured);
|
|
2537
2562
|
seedBeamLink(id, body, merchantId);
|
|
2538
|
-
return json({
|
|
2563
|
+
return json({
|
|
2564
|
+
id,
|
|
2565
|
+
url: checkoutUrl,
|
|
2566
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
2567
|
+
});
|
|
2539
2568
|
}
|
|
2540
2569
|
if (linkId && request.method === "GET") {
|
|
2541
2570
|
const link = state.paymentLinks.get(linkId);
|
|
@@ -2561,6 +2590,34 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2561
2590
|
}
|
|
2562
2591
|
if (subPath === "checkout" && request.method === "POST") {
|
|
2563
2592
|
const body = await readJsonBody(request);
|
|
2593
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
2594
|
+
const requestedExpiry = typeof body.expiresAt === "number" ? body.expiresAt : void 0;
|
|
2595
|
+
const callerKey = typeof body.idempotencyKey === "string" ? body.idempotencyKey : void 0;
|
|
2596
|
+
const scopedKey = callerKey ? JSON.stringify(["stripe", accountId, callerKey]) : void 0;
|
|
2597
|
+
const comparable = { ...body };
|
|
2598
|
+
delete comparable.deploymentId;
|
|
2599
|
+
delete comparable.idempotencyKey;
|
|
2600
|
+
const fingerprint = stableStringify(comparable);
|
|
2601
|
+
if (scopedKey) {
|
|
2602
|
+
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
2603
|
+
if (saved) {
|
|
2604
|
+
if (saved.fingerprint !== fingerprint) {
|
|
2605
|
+
return payErr(
|
|
2606
|
+
"This idempotency key was already used with different checkout parameters. Use a new key for a different checkout.",
|
|
2607
|
+
409,
|
|
2608
|
+
"idempotency_conflict"
|
|
2609
|
+
);
|
|
2610
|
+
}
|
|
2611
|
+
return json(saved.response);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
if (requestedExpiry !== void 0 && (requestedExpiry < nowSeconds + STRIPE_MIN_EXPIRY_SECONDS || requestedExpiry > nowSeconds + STRIPE_MAX_EXPIRY_SECONDS)) {
|
|
2615
|
+
return payErr(
|
|
2616
|
+
"expiresAt must be between 30 minutes and 24 hours after the checkout is created (Unix seconds).",
|
|
2617
|
+
400,
|
|
2618
|
+
"expiry_out_of_range"
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2564
2621
|
const id = nextCheckoutId();
|
|
2565
2622
|
const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
|
|
2566
2623
|
const captured = {
|
|
@@ -2573,8 +2630,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2573
2630
|
createdAt: /* @__PURE__ */ new Date()
|
|
2574
2631
|
};
|
|
2575
2632
|
state.checkouts.push(captured);
|
|
2576
|
-
|
|
2577
|
-
|
|
2633
|
+
const expiresAt = requestedExpiry ?? nowSeconds + STRIPE_MAX_EXPIRY_SECONDS;
|
|
2634
|
+
seedStripeSession(id, body, expiresAt);
|
|
2635
|
+
const response = { id, url: checkoutUrl, expiresAt };
|
|
2636
|
+
if (scopedKey) state.idempotentCheckouts.set(scopedKey, { fingerprint, response });
|
|
2637
|
+
return json(response);
|
|
2578
2638
|
}
|
|
2579
2639
|
const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
|
|
2580
2640
|
if (sessionMatch && request.method === "GET") {
|
|
@@ -2739,6 +2799,7 @@ function createPayments() {
|
|
|
2739
2799
|
state.checkouts = [];
|
|
2740
2800
|
state.checkoutCounter = 0;
|
|
2741
2801
|
state.sessionStatuses.clear();
|
|
2802
|
+
state.idempotentCheckouts.clear();
|
|
2742
2803
|
state.paymentLinks.clear();
|
|
2743
2804
|
state.paymentFailures = [];
|
|
2744
2805
|
state.products = [];
|
|
@@ -3284,6 +3345,93 @@ async function handlePrint(request) {
|
|
|
3284
3345
|
status: "completed"
|
|
3285
3346
|
});
|
|
3286
3347
|
}
|
|
3348
|
+
var IDEMPOTENCY_KEY = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
3349
|
+
async function handleEnqueue(request) {
|
|
3350
|
+
const body = await readJsonBody(request);
|
|
3351
|
+
const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : "";
|
|
3352
|
+
if (!IDEMPOTENCY_KEY.test(key)) {
|
|
3353
|
+
return edgeFailure("idempotencyKey must be 1-128 characters of A-Z a-z 0-9 _ . : -");
|
|
3354
|
+
}
|
|
3355
|
+
if (body.openDrawer === true) {
|
|
3356
|
+
return edgeFailure("Durable receipts cannot open the cash drawer; a retry would kick it again");
|
|
3357
|
+
}
|
|
3358
|
+
if (body.copies !== void 0 && body.copies !== 1) {
|
|
3359
|
+
return edgeFailure(
|
|
3360
|
+
"Durable receipts print exactly one copy; submit a second job for another copy"
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
const receipt = body.receipt ?? {};
|
|
3364
|
+
if (receipt.rawEscPos !== void 0) {
|
|
3365
|
+
return edgeFailure("rawEscPos is not supported for durable receipts");
|
|
3366
|
+
}
|
|
3367
|
+
const requestHash = JSON.stringify([
|
|
3368
|
+
body.alias,
|
|
3369
|
+
body.deviceId,
|
|
3370
|
+
body.peripheralId,
|
|
3371
|
+
receipt,
|
|
3372
|
+
body.logo
|
|
3373
|
+
]);
|
|
3374
|
+
const existing = [...state.edgeDurableJobs.values()].find(
|
|
3375
|
+
(j) => j.captured.idempotencyKey === key
|
|
3376
|
+
);
|
|
3377
|
+
if (existing) {
|
|
3378
|
+
if (existing.requestHash !== requestHash) {
|
|
3379
|
+
return edgeFailure(
|
|
3380
|
+
"This idempotencyKey was already used for a different receipt, target or options.",
|
|
3381
|
+
409,
|
|
3382
|
+
"IDEMPOTENCY_KEY_CONFLICT"
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3385
|
+
return json({ success: true, data: durableView(existing.captured.jobId, "queued") }, 200);
|
|
3386
|
+
}
|
|
3387
|
+
const jobId = nextJobId();
|
|
3388
|
+
const captured = {
|
|
3389
|
+
jobId,
|
|
3390
|
+
idempotencyKey: key,
|
|
3391
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
3392
|
+
deviceId: body.deviceId ? String(body.deviceId) : void 0,
|
|
3393
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
|
|
3394
|
+
receipt,
|
|
3395
|
+
logo: body.logo === true ? true : body.logo === false ? false : void 0
|
|
3396
|
+
};
|
|
3397
|
+
const outcome = state.edgeDurableOutcomes.shift() ?? { status: "sent_to_printer" };
|
|
3398
|
+
state.edgeDurableJobs.set(jobId, { requestHash, captured, outcome });
|
|
3399
|
+
return json({ success: true, data: durableView(jobId, "queued") }, 202);
|
|
3400
|
+
}
|
|
3401
|
+
function durableView(jobId, as) {
|
|
3402
|
+
const job = state.edgeDurableJobs.get(jobId);
|
|
3403
|
+
const createdAt = now2();
|
|
3404
|
+
const base = {
|
|
3405
|
+
jobId,
|
|
3406
|
+
attempts: as === "queued" ? 0 : 1,
|
|
3407
|
+
createdAt,
|
|
3408
|
+
expiresAt: new Date(Date.parse(createdAt) + 10 * 6e4).toISOString(),
|
|
3409
|
+
target: {
|
|
3410
|
+
deviceId: job.captured.deviceId ?? "sim-device",
|
|
3411
|
+
peripheralId: job.captured.peripheralId ?? "sim-peripheral"
|
|
3412
|
+
}
|
|
3413
|
+
};
|
|
3414
|
+
const outcome = as === "queued" ? { status: "queued" } : job.outcome;
|
|
3415
|
+
if (outcome.status === "needs_attention") {
|
|
3416
|
+
return {
|
|
3417
|
+
...base,
|
|
3418
|
+
status: "needs_attention",
|
|
3419
|
+
attention: {
|
|
3420
|
+
reason: outcome.reason,
|
|
3421
|
+
outcome: outcome.outcome,
|
|
3422
|
+
message: outcome.message ?? "Delivery stopped."
|
|
3423
|
+
}
|
|
3424
|
+
};
|
|
3425
|
+
}
|
|
3426
|
+
if (outcome.status === "sent_to_printer") {
|
|
3427
|
+
return { ...base, status: "sent_to_printer", sentToPrinterAt: createdAt };
|
|
3428
|
+
}
|
|
3429
|
+
return { ...base, status: "queued" };
|
|
3430
|
+
}
|
|
3431
|
+
function handleGetDurable(jobId) {
|
|
3432
|
+
if (!state.edgeDurableJobs.has(jobId)) return edgeFailure("Print job not found", 404);
|
|
3433
|
+
return success(durableView(jobId));
|
|
3434
|
+
}
|
|
3287
3435
|
async function handleShowDisplay(request) {
|
|
3288
3436
|
const body = await readJsonBody(request);
|
|
3289
3437
|
const captured = {
|
|
@@ -3333,6 +3481,13 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
3333
3481
|
if (subPath === "/print" && method === "POST") {
|
|
3334
3482
|
return handlePrint(request);
|
|
3335
3483
|
}
|
|
3484
|
+
if (subPath === "/print-jobs" && method === "POST") {
|
|
3485
|
+
return handleEnqueue(request);
|
|
3486
|
+
}
|
|
3487
|
+
const durableMatch = subPath.match(/^\/print-jobs\/([^/]+)$/);
|
|
3488
|
+
if (durableMatch && method === "GET") {
|
|
3489
|
+
return handleGetDurable(decodeURIComponent(durableMatch[1]));
|
|
3490
|
+
}
|
|
3336
3491
|
if (subPath === "/capabilities" && method === "GET") {
|
|
3337
3492
|
return handleCapabilities(request);
|
|
3338
3493
|
}
|
|
@@ -3376,6 +3531,12 @@ function createEdge() {
|
|
|
3376
3531
|
get testPrints() {
|
|
3377
3532
|
return [...state.edgeTestPrints];
|
|
3378
3533
|
},
|
|
3534
|
+
get durablePrints() {
|
|
3535
|
+
return [...state.edgeDurableJobs.values()].map((j) => j.captured);
|
|
3536
|
+
},
|
|
3537
|
+
settleNextDurableJob(outcome) {
|
|
3538
|
+
state.edgeDurableOutcomes.push(outcome);
|
|
3539
|
+
},
|
|
3379
3540
|
latestPrint() {
|
|
3380
3541
|
return state.edgePrints[state.edgePrints.length - 1];
|
|
3381
3542
|
},
|
|
@@ -3412,12 +3573,14 @@ function createEdge() {
|
|
|
3412
3573
|
state.edgeDisplays = [];
|
|
3413
3574
|
state.edgeTestPrints = [];
|
|
3414
3575
|
state.edgePrintCounter = 0;
|
|
3576
|
+
state.edgeDurableJobs.clear();
|
|
3577
|
+
state.edgeDurableOutcomes = [];
|
|
3415
3578
|
state.edgeDevices = [];
|
|
3416
3579
|
state.edgePeripherals = [];
|
|
3417
3580
|
state.edgeBindings.clear();
|
|
3418
3581
|
},
|
|
3419
3582
|
get count() {
|
|
3420
|
-
return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length;
|
|
3583
|
+
return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length + state.edgeDurableJobs.size;
|
|
3421
3584
|
}
|
|
3422
3585
|
};
|
|
3423
3586
|
}
|
|
@@ -62,6 +62,7 @@ var state = globalSingleton("state", () => ({
|
|
|
62
62
|
checkouts: [],
|
|
63
63
|
checkoutCounter: 0,
|
|
64
64
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
65
|
+
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
65
66
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
66
67
|
paymentFailures: [],
|
|
67
68
|
products: [],
|
|
@@ -86,6 +87,8 @@ var state = globalSingleton("state", () => ({
|
|
|
86
87
|
edgeDisplays: [],
|
|
87
88
|
edgeTestPrints: [],
|
|
88
89
|
edgePrintCounter: 0,
|
|
90
|
+
edgeDurableJobs: /* @__PURE__ */ new Map(),
|
|
91
|
+
edgeDurableOutcomes: [],
|
|
89
92
|
edgeDevices: [],
|
|
90
93
|
edgePeripherals: [],
|
|
91
94
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
@@ -35,6 +35,7 @@ var state = globalSingleton("state", () => ({
|
|
|
35
35
|
checkouts: [],
|
|
36
36
|
checkoutCounter: 0,
|
|
37
37
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
38
|
+
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
38
39
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
39
40
|
paymentFailures: [],
|
|
40
41
|
products: [],
|
|
@@ -59,6 +60,8 @@ var state = globalSingleton("state", () => ({
|
|
|
59
60
|
edgeDisplays: [],
|
|
60
61
|
edgeTestPrints: [],
|
|
61
62
|
edgePrintCounter: 0,
|
|
63
|
+
edgeDurableJobs: /* @__PURE__ */ new Map(),
|
|
64
|
+
edgeDurableOutcomes: [],
|
|
62
65
|
edgeDevices: [],
|
|
63
66
|
edgePeripherals: [],
|
|
64
67
|
edgeBindings: /* @__PURE__ */ new Map(),
|
package/dist/setup.js
CHANGED
|
@@ -67,6 +67,7 @@ var state = globalSingleton("state", () => ({
|
|
|
67
67
|
checkouts: [],
|
|
68
68
|
checkoutCounter: 0,
|
|
69
69
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
70
|
+
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
70
71
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
71
72
|
paymentFailures: [],
|
|
72
73
|
products: [],
|
|
@@ -91,6 +92,8 @@ var state = globalSingleton("state", () => ({
|
|
|
91
92
|
edgeDisplays: [],
|
|
92
93
|
edgeTestPrints: [],
|
|
93
94
|
edgePrintCounter: 0,
|
|
95
|
+
edgeDurableJobs: /* @__PURE__ */ new Map(),
|
|
96
|
+
edgeDurableOutcomes: [],
|
|
94
97
|
edgeDevices: [],
|
|
95
98
|
edgePeripherals: [],
|
|
96
99
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
@@ -1536,6 +1539,8 @@ var requestScopeStorage = globalSingleton(
|
|
|
1536
1539
|
var import_node_crypto4 = __toESM(require("crypto"));
|
|
1537
1540
|
|
|
1538
1541
|
// src/simulator/payments.ts
|
|
1542
|
+
var STRIPE_MIN_EXPIRY_SECONDS = 30 * 60;
|
|
1543
|
+
var STRIPE_MAX_EXPIRY_SECONDS = 24 * 60 * 60;
|
|
1539
1544
|
var BEAM_BOLT_PAYMENT_METHODS = [
|
|
1540
1545
|
"CARD",
|
|
1541
1546
|
"CARD_INSTALLMENTS",
|
|
@@ -1605,6 +1610,15 @@ function isBoltPaymentMethod(value) {
|
|
|
1605
1610
|
function isBoltIntentStatus(value) {
|
|
1606
1611
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
1607
1612
|
}
|
|
1613
|
+
function stableStringify(value) {
|
|
1614
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1615
|
+
if (value && typeof value === "object") {
|
|
1616
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1617
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
1618
|
+
}
|
|
1619
|
+
return JSON.stringify(value);
|
|
1620
|
+
}
|
|
1621
|
+
var ISO_8601_WITH_ZONE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
|
|
1608
1622
|
function nextCheckoutId() {
|
|
1609
1623
|
state.checkoutCounter += 1;
|
|
1610
1624
|
return `cs_test_${state.checkoutCounter}`;
|
|
@@ -1621,7 +1635,7 @@ function nextBoltIntentId() {
|
|
|
1621
1635
|
state.boltIntentCounter += 1;
|
|
1622
1636
|
return `bolti_${state.boltIntentCounter}`;
|
|
1623
1637
|
}
|
|
1624
|
-
function seedStripeSession(id, body) {
|
|
1638
|
+
function seedStripeSession(id, body, expiresAt) {
|
|
1625
1639
|
const lineItems = body.lineItems ?? [];
|
|
1626
1640
|
let amountTotal = null;
|
|
1627
1641
|
let currency = null;
|
|
@@ -1641,7 +1655,7 @@ function seedStripeSession(id, body) {
|
|
|
1641
1655
|
currency,
|
|
1642
1656
|
customerEmail: body.customerEmail ? String(body.customerEmail) : null,
|
|
1643
1657
|
metadata: body.metadata ?? {},
|
|
1644
|
-
expiresAt
|
|
1658
|
+
expiresAt
|
|
1645
1659
|
});
|
|
1646
1660
|
}
|
|
1647
1661
|
function seedBeamLink(id, body, merchantId) {
|
|
@@ -1661,7 +1675,8 @@ function seedBeamLink(id, body, merchantId) {
|
|
|
1661
1675
|
},
|
|
1662
1676
|
redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
|
|
1663
1677
|
linkSettings: body.linkSettings,
|
|
1664
|
-
collectDeliveryAddress: body.collectDeliveryAddress === true
|
|
1678
|
+
collectDeliveryAddress: body.collectDeliveryAddress === true,
|
|
1679
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
1665
1680
|
});
|
|
1666
1681
|
}
|
|
1667
1682
|
function deriveBoltIntentStatus(storedStatus, expiresAt, now3 = /* @__PURE__ */ new Date()) {
|
|
@@ -1932,6 +1947,16 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1932
1947
|
const linkId = beamProductsMatch[2];
|
|
1933
1948
|
if (!linkId && request.method === "POST") {
|
|
1934
1949
|
const body = await readJsonBody(request);
|
|
1950
|
+
if ("idempotencyKey" in body) {
|
|
1951
|
+
return payErr(
|
|
1952
|
+
"idempotencyKey is not supported for Beam payment links.",
|
|
1953
|
+
400,
|
|
1954
|
+
"idempotency_unsupported"
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
if (typeof body.expiresAt === "string" && (!ISO_8601_WITH_ZONE.test(body.expiresAt) || Number.isNaN(Date.parse(body.expiresAt)))) {
|
|
1958
|
+
return payErr("expiresAt must be an ISO 8601 timestamp", 400);
|
|
1959
|
+
}
|
|
1935
1960
|
const id = nextPaymentLinkId();
|
|
1936
1961
|
const checkoutUrl = `https://beam.test/pay/${id}`;
|
|
1937
1962
|
const captured = {
|
|
@@ -1944,7 +1969,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1944
1969
|
};
|
|
1945
1970
|
state.checkouts.push(captured);
|
|
1946
1971
|
seedBeamLink(id, body, merchantId);
|
|
1947
|
-
return json({
|
|
1972
|
+
return json({
|
|
1973
|
+
id,
|
|
1974
|
+
url: checkoutUrl,
|
|
1975
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
1976
|
+
});
|
|
1948
1977
|
}
|
|
1949
1978
|
if (linkId && request.method === "GET") {
|
|
1950
1979
|
const link = state.paymentLinks.get(linkId);
|
|
@@ -1970,6 +1999,34 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1970
1999
|
}
|
|
1971
2000
|
if (subPath === "checkout" && request.method === "POST") {
|
|
1972
2001
|
const body = await readJsonBody(request);
|
|
2002
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
2003
|
+
const requestedExpiry = typeof body.expiresAt === "number" ? body.expiresAt : void 0;
|
|
2004
|
+
const callerKey = typeof body.idempotencyKey === "string" ? body.idempotencyKey : void 0;
|
|
2005
|
+
const scopedKey = callerKey ? JSON.stringify(["stripe", accountId, callerKey]) : void 0;
|
|
2006
|
+
const comparable = { ...body };
|
|
2007
|
+
delete comparable.deploymentId;
|
|
2008
|
+
delete comparable.idempotencyKey;
|
|
2009
|
+
const fingerprint = stableStringify(comparable);
|
|
2010
|
+
if (scopedKey) {
|
|
2011
|
+
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
2012
|
+
if (saved) {
|
|
2013
|
+
if (saved.fingerprint !== fingerprint) {
|
|
2014
|
+
return payErr(
|
|
2015
|
+
"This idempotency key was already used with different checkout parameters. Use a new key for a different checkout.",
|
|
2016
|
+
409,
|
|
2017
|
+
"idempotency_conflict"
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
return json(saved.response);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
if (requestedExpiry !== void 0 && (requestedExpiry < nowSeconds + STRIPE_MIN_EXPIRY_SECONDS || requestedExpiry > nowSeconds + STRIPE_MAX_EXPIRY_SECONDS)) {
|
|
2024
|
+
return payErr(
|
|
2025
|
+
"expiresAt must be between 30 minutes and 24 hours after the checkout is created (Unix seconds).",
|
|
2026
|
+
400,
|
|
2027
|
+
"expiry_out_of_range"
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
1973
2030
|
const id = nextCheckoutId();
|
|
1974
2031
|
const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
|
|
1975
2032
|
const captured = {
|
|
@@ -1982,8 +2039,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1982
2039
|
createdAt: /* @__PURE__ */ new Date()
|
|
1983
2040
|
};
|
|
1984
2041
|
state.checkouts.push(captured);
|
|
1985
|
-
|
|
1986
|
-
|
|
2042
|
+
const expiresAt = requestedExpiry ?? nowSeconds + STRIPE_MAX_EXPIRY_SECONDS;
|
|
2043
|
+
seedStripeSession(id, body, expiresAt);
|
|
2044
|
+
const response = { id, url: checkoutUrl, expiresAt };
|
|
2045
|
+
if (scopedKey) state.idempotentCheckouts.set(scopedKey, { fingerprint, response });
|
|
2046
|
+
return json(response);
|
|
1987
2047
|
}
|
|
1988
2048
|
const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
|
|
1989
2049
|
if (sessionMatch && request.method === "GET") {
|
|
@@ -2469,6 +2529,93 @@ async function handlePrint(request) {
|
|
|
2469
2529
|
status: "completed"
|
|
2470
2530
|
});
|
|
2471
2531
|
}
|
|
2532
|
+
var IDEMPOTENCY_KEY = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
2533
|
+
async function handleEnqueue(request) {
|
|
2534
|
+
const body = await readJsonBody(request);
|
|
2535
|
+
const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : "";
|
|
2536
|
+
if (!IDEMPOTENCY_KEY.test(key)) {
|
|
2537
|
+
return edgeFailure("idempotencyKey must be 1-128 characters of A-Z a-z 0-9 _ . : -");
|
|
2538
|
+
}
|
|
2539
|
+
if (body.openDrawer === true) {
|
|
2540
|
+
return edgeFailure("Durable receipts cannot open the cash drawer; a retry would kick it again");
|
|
2541
|
+
}
|
|
2542
|
+
if (body.copies !== void 0 && body.copies !== 1) {
|
|
2543
|
+
return edgeFailure(
|
|
2544
|
+
"Durable receipts print exactly one copy; submit a second job for another copy"
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
const receipt = body.receipt ?? {};
|
|
2548
|
+
if (receipt.rawEscPos !== void 0) {
|
|
2549
|
+
return edgeFailure("rawEscPos is not supported for durable receipts");
|
|
2550
|
+
}
|
|
2551
|
+
const requestHash = JSON.stringify([
|
|
2552
|
+
body.alias,
|
|
2553
|
+
body.deviceId,
|
|
2554
|
+
body.peripheralId,
|
|
2555
|
+
receipt,
|
|
2556
|
+
body.logo
|
|
2557
|
+
]);
|
|
2558
|
+
const existing = [...state.edgeDurableJobs.values()].find(
|
|
2559
|
+
(j) => j.captured.idempotencyKey === key
|
|
2560
|
+
);
|
|
2561
|
+
if (existing) {
|
|
2562
|
+
if (existing.requestHash !== requestHash) {
|
|
2563
|
+
return edgeFailure(
|
|
2564
|
+
"This idempotencyKey was already used for a different receipt, target or options.",
|
|
2565
|
+
409,
|
|
2566
|
+
"IDEMPOTENCY_KEY_CONFLICT"
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
return json({ success: true, data: durableView(existing.captured.jobId, "queued") }, 200);
|
|
2570
|
+
}
|
|
2571
|
+
const jobId = nextJobId();
|
|
2572
|
+
const captured = {
|
|
2573
|
+
jobId,
|
|
2574
|
+
idempotencyKey: key,
|
|
2575
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
2576
|
+
deviceId: body.deviceId ? String(body.deviceId) : void 0,
|
|
2577
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
|
|
2578
|
+
receipt,
|
|
2579
|
+
logo: body.logo === true ? true : body.logo === false ? false : void 0
|
|
2580
|
+
};
|
|
2581
|
+
const outcome = state.edgeDurableOutcomes.shift() ?? { status: "sent_to_printer" };
|
|
2582
|
+
state.edgeDurableJobs.set(jobId, { requestHash, captured, outcome });
|
|
2583
|
+
return json({ success: true, data: durableView(jobId, "queued") }, 202);
|
|
2584
|
+
}
|
|
2585
|
+
function durableView(jobId, as) {
|
|
2586
|
+
const job = state.edgeDurableJobs.get(jobId);
|
|
2587
|
+
const createdAt = now2();
|
|
2588
|
+
const base = {
|
|
2589
|
+
jobId,
|
|
2590
|
+
attempts: as === "queued" ? 0 : 1,
|
|
2591
|
+
createdAt,
|
|
2592
|
+
expiresAt: new Date(Date.parse(createdAt) + 10 * 6e4).toISOString(),
|
|
2593
|
+
target: {
|
|
2594
|
+
deviceId: job.captured.deviceId ?? "sim-device",
|
|
2595
|
+
peripheralId: job.captured.peripheralId ?? "sim-peripheral"
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
const outcome = as === "queued" ? { status: "queued" } : job.outcome;
|
|
2599
|
+
if (outcome.status === "needs_attention") {
|
|
2600
|
+
return {
|
|
2601
|
+
...base,
|
|
2602
|
+
status: "needs_attention",
|
|
2603
|
+
attention: {
|
|
2604
|
+
reason: outcome.reason,
|
|
2605
|
+
outcome: outcome.outcome,
|
|
2606
|
+
message: outcome.message ?? "Delivery stopped."
|
|
2607
|
+
}
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
if (outcome.status === "sent_to_printer") {
|
|
2611
|
+
return { ...base, status: "sent_to_printer", sentToPrinterAt: createdAt };
|
|
2612
|
+
}
|
|
2613
|
+
return { ...base, status: "queued" };
|
|
2614
|
+
}
|
|
2615
|
+
function handleGetDurable(jobId) {
|
|
2616
|
+
if (!state.edgeDurableJobs.has(jobId)) return edgeFailure("Print job not found", 404);
|
|
2617
|
+
return success(durableView(jobId));
|
|
2618
|
+
}
|
|
2472
2619
|
async function handleShowDisplay(request) {
|
|
2473
2620
|
const body = await readJsonBody(request);
|
|
2474
2621
|
const captured = {
|
|
@@ -2518,6 +2665,13 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
2518
2665
|
if (subPath === "/print" && method === "POST") {
|
|
2519
2666
|
return handlePrint(request);
|
|
2520
2667
|
}
|
|
2668
|
+
if (subPath === "/print-jobs" && method === "POST") {
|
|
2669
|
+
return handleEnqueue(request);
|
|
2670
|
+
}
|
|
2671
|
+
const durableMatch = subPath.match(/^\/print-jobs\/([^/]+)$/);
|
|
2672
|
+
if (durableMatch && method === "GET") {
|
|
2673
|
+
return handleGetDurable(decodeURIComponent(durableMatch[1]));
|
|
2674
|
+
}
|
|
2521
2675
|
if (subPath === "/capabilities" && method === "GET") {
|
|
2522
2676
|
return handleCapabilities(request);
|
|
2523
2677
|
}
|
package/dist/setup.mjs
CHANGED
|
@@ -43,6 +43,7 @@ var state = globalSingleton("state", () => ({
|
|
|
43
43
|
checkouts: [],
|
|
44
44
|
checkoutCounter: 0,
|
|
45
45
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
46
|
+
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
46
47
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
47
48
|
paymentFailures: [],
|
|
48
49
|
products: [],
|
|
@@ -67,6 +68,8 @@ var state = globalSingleton("state", () => ({
|
|
|
67
68
|
edgeDisplays: [],
|
|
68
69
|
edgeTestPrints: [],
|
|
69
70
|
edgePrintCounter: 0,
|
|
71
|
+
edgeDurableJobs: /* @__PURE__ */ new Map(),
|
|
72
|
+
edgeDurableOutcomes: [],
|
|
70
73
|
edgeDevices: [],
|
|
71
74
|
edgePeripherals: [],
|
|
72
75
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
@@ -1512,6 +1515,8 @@ var requestScopeStorage = globalSingleton(
|
|
|
1512
1515
|
import crypto4 from "crypto";
|
|
1513
1516
|
|
|
1514
1517
|
// src/simulator/payments.ts
|
|
1518
|
+
var STRIPE_MIN_EXPIRY_SECONDS = 30 * 60;
|
|
1519
|
+
var STRIPE_MAX_EXPIRY_SECONDS = 24 * 60 * 60;
|
|
1515
1520
|
var BEAM_BOLT_PAYMENT_METHODS = [
|
|
1516
1521
|
"CARD",
|
|
1517
1522
|
"CARD_INSTALLMENTS",
|
|
@@ -1581,6 +1586,15 @@ function isBoltPaymentMethod(value) {
|
|
|
1581
1586
|
function isBoltIntentStatus(value) {
|
|
1582
1587
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
1583
1588
|
}
|
|
1589
|
+
function stableStringify(value) {
|
|
1590
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1591
|
+
if (value && typeof value === "object") {
|
|
1592
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1593
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
1594
|
+
}
|
|
1595
|
+
return JSON.stringify(value);
|
|
1596
|
+
}
|
|
1597
|
+
var ISO_8601_WITH_ZONE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
|
|
1584
1598
|
function nextCheckoutId() {
|
|
1585
1599
|
state.checkoutCounter += 1;
|
|
1586
1600
|
return `cs_test_${state.checkoutCounter}`;
|
|
@@ -1597,7 +1611,7 @@ function nextBoltIntentId() {
|
|
|
1597
1611
|
state.boltIntentCounter += 1;
|
|
1598
1612
|
return `bolti_${state.boltIntentCounter}`;
|
|
1599
1613
|
}
|
|
1600
|
-
function seedStripeSession(id, body) {
|
|
1614
|
+
function seedStripeSession(id, body, expiresAt) {
|
|
1601
1615
|
const lineItems = body.lineItems ?? [];
|
|
1602
1616
|
let amountTotal = null;
|
|
1603
1617
|
let currency = null;
|
|
@@ -1617,7 +1631,7 @@ function seedStripeSession(id, body) {
|
|
|
1617
1631
|
currency,
|
|
1618
1632
|
customerEmail: body.customerEmail ? String(body.customerEmail) : null,
|
|
1619
1633
|
metadata: body.metadata ?? {},
|
|
1620
|
-
expiresAt
|
|
1634
|
+
expiresAt
|
|
1621
1635
|
});
|
|
1622
1636
|
}
|
|
1623
1637
|
function seedBeamLink(id, body, merchantId) {
|
|
@@ -1637,7 +1651,8 @@ function seedBeamLink(id, body, merchantId) {
|
|
|
1637
1651
|
},
|
|
1638
1652
|
redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
|
|
1639
1653
|
linkSettings: body.linkSettings,
|
|
1640
|
-
collectDeliveryAddress: body.collectDeliveryAddress === true
|
|
1654
|
+
collectDeliveryAddress: body.collectDeliveryAddress === true,
|
|
1655
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
1641
1656
|
});
|
|
1642
1657
|
}
|
|
1643
1658
|
function deriveBoltIntentStatus(storedStatus, expiresAt, now3 = /* @__PURE__ */ new Date()) {
|
|
@@ -1908,6 +1923,16 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1908
1923
|
const linkId = beamProductsMatch[2];
|
|
1909
1924
|
if (!linkId && request.method === "POST") {
|
|
1910
1925
|
const body = await readJsonBody(request);
|
|
1926
|
+
if ("idempotencyKey" in body) {
|
|
1927
|
+
return payErr(
|
|
1928
|
+
"idempotencyKey is not supported for Beam payment links.",
|
|
1929
|
+
400,
|
|
1930
|
+
"idempotency_unsupported"
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
if (typeof body.expiresAt === "string" && (!ISO_8601_WITH_ZONE.test(body.expiresAt) || Number.isNaN(Date.parse(body.expiresAt)))) {
|
|
1934
|
+
return payErr("expiresAt must be an ISO 8601 timestamp", 400);
|
|
1935
|
+
}
|
|
1911
1936
|
const id = nextPaymentLinkId();
|
|
1912
1937
|
const checkoutUrl = `https://beam.test/pay/${id}`;
|
|
1913
1938
|
const captured = {
|
|
@@ -1920,7 +1945,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1920
1945
|
};
|
|
1921
1946
|
state.checkouts.push(captured);
|
|
1922
1947
|
seedBeamLink(id, body, merchantId);
|
|
1923
|
-
return json({
|
|
1948
|
+
return json({
|
|
1949
|
+
id,
|
|
1950
|
+
url: checkoutUrl,
|
|
1951
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
1952
|
+
});
|
|
1924
1953
|
}
|
|
1925
1954
|
if (linkId && request.method === "GET") {
|
|
1926
1955
|
const link = state.paymentLinks.get(linkId);
|
|
@@ -1946,6 +1975,34 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1946
1975
|
}
|
|
1947
1976
|
if (subPath === "checkout" && request.method === "POST") {
|
|
1948
1977
|
const body = await readJsonBody(request);
|
|
1978
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
1979
|
+
const requestedExpiry = typeof body.expiresAt === "number" ? body.expiresAt : void 0;
|
|
1980
|
+
const callerKey = typeof body.idempotencyKey === "string" ? body.idempotencyKey : void 0;
|
|
1981
|
+
const scopedKey = callerKey ? JSON.stringify(["stripe", accountId, callerKey]) : void 0;
|
|
1982
|
+
const comparable = { ...body };
|
|
1983
|
+
delete comparable.deploymentId;
|
|
1984
|
+
delete comparable.idempotencyKey;
|
|
1985
|
+
const fingerprint = stableStringify(comparable);
|
|
1986
|
+
if (scopedKey) {
|
|
1987
|
+
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
1988
|
+
if (saved) {
|
|
1989
|
+
if (saved.fingerprint !== fingerprint) {
|
|
1990
|
+
return payErr(
|
|
1991
|
+
"This idempotency key was already used with different checkout parameters. Use a new key for a different checkout.",
|
|
1992
|
+
409,
|
|
1993
|
+
"idempotency_conflict"
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
return json(saved.response);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
if (requestedExpiry !== void 0 && (requestedExpiry < nowSeconds + STRIPE_MIN_EXPIRY_SECONDS || requestedExpiry > nowSeconds + STRIPE_MAX_EXPIRY_SECONDS)) {
|
|
2000
|
+
return payErr(
|
|
2001
|
+
"expiresAt must be between 30 minutes and 24 hours after the checkout is created (Unix seconds).",
|
|
2002
|
+
400,
|
|
2003
|
+
"expiry_out_of_range"
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
1949
2006
|
const id = nextCheckoutId();
|
|
1950
2007
|
const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
|
|
1951
2008
|
const captured = {
|
|
@@ -1958,8 +2015,11 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1958
2015
|
createdAt: /* @__PURE__ */ new Date()
|
|
1959
2016
|
};
|
|
1960
2017
|
state.checkouts.push(captured);
|
|
1961
|
-
|
|
1962
|
-
|
|
2018
|
+
const expiresAt = requestedExpiry ?? nowSeconds + STRIPE_MAX_EXPIRY_SECONDS;
|
|
2019
|
+
seedStripeSession(id, body, expiresAt);
|
|
2020
|
+
const response = { id, url: checkoutUrl, expiresAt };
|
|
2021
|
+
if (scopedKey) state.idempotentCheckouts.set(scopedKey, { fingerprint, response });
|
|
2022
|
+
return json(response);
|
|
1963
2023
|
}
|
|
1964
2024
|
const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
|
|
1965
2025
|
if (sessionMatch && request.method === "GET") {
|
|
@@ -2445,6 +2505,93 @@ async function handlePrint(request) {
|
|
|
2445
2505
|
status: "completed"
|
|
2446
2506
|
});
|
|
2447
2507
|
}
|
|
2508
|
+
var IDEMPOTENCY_KEY = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
2509
|
+
async function handleEnqueue(request) {
|
|
2510
|
+
const body = await readJsonBody(request);
|
|
2511
|
+
const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : "";
|
|
2512
|
+
if (!IDEMPOTENCY_KEY.test(key)) {
|
|
2513
|
+
return edgeFailure("idempotencyKey must be 1-128 characters of A-Z a-z 0-9 _ . : -");
|
|
2514
|
+
}
|
|
2515
|
+
if (body.openDrawer === true) {
|
|
2516
|
+
return edgeFailure("Durable receipts cannot open the cash drawer; a retry would kick it again");
|
|
2517
|
+
}
|
|
2518
|
+
if (body.copies !== void 0 && body.copies !== 1) {
|
|
2519
|
+
return edgeFailure(
|
|
2520
|
+
"Durable receipts print exactly one copy; submit a second job for another copy"
|
|
2521
|
+
);
|
|
2522
|
+
}
|
|
2523
|
+
const receipt = body.receipt ?? {};
|
|
2524
|
+
if (receipt.rawEscPos !== void 0) {
|
|
2525
|
+
return edgeFailure("rawEscPos is not supported for durable receipts");
|
|
2526
|
+
}
|
|
2527
|
+
const requestHash = JSON.stringify([
|
|
2528
|
+
body.alias,
|
|
2529
|
+
body.deviceId,
|
|
2530
|
+
body.peripheralId,
|
|
2531
|
+
receipt,
|
|
2532
|
+
body.logo
|
|
2533
|
+
]);
|
|
2534
|
+
const existing = [...state.edgeDurableJobs.values()].find(
|
|
2535
|
+
(j) => j.captured.idempotencyKey === key
|
|
2536
|
+
);
|
|
2537
|
+
if (existing) {
|
|
2538
|
+
if (existing.requestHash !== requestHash) {
|
|
2539
|
+
return edgeFailure(
|
|
2540
|
+
"This idempotencyKey was already used for a different receipt, target or options.",
|
|
2541
|
+
409,
|
|
2542
|
+
"IDEMPOTENCY_KEY_CONFLICT"
|
|
2543
|
+
);
|
|
2544
|
+
}
|
|
2545
|
+
return json({ success: true, data: durableView(existing.captured.jobId, "queued") }, 200);
|
|
2546
|
+
}
|
|
2547
|
+
const jobId = nextJobId();
|
|
2548
|
+
const captured = {
|
|
2549
|
+
jobId,
|
|
2550
|
+
idempotencyKey: key,
|
|
2551
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
2552
|
+
deviceId: body.deviceId ? String(body.deviceId) : void 0,
|
|
2553
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
|
|
2554
|
+
receipt,
|
|
2555
|
+
logo: body.logo === true ? true : body.logo === false ? false : void 0
|
|
2556
|
+
};
|
|
2557
|
+
const outcome = state.edgeDurableOutcomes.shift() ?? { status: "sent_to_printer" };
|
|
2558
|
+
state.edgeDurableJobs.set(jobId, { requestHash, captured, outcome });
|
|
2559
|
+
return json({ success: true, data: durableView(jobId, "queued") }, 202);
|
|
2560
|
+
}
|
|
2561
|
+
function durableView(jobId, as) {
|
|
2562
|
+
const job = state.edgeDurableJobs.get(jobId);
|
|
2563
|
+
const createdAt = now2();
|
|
2564
|
+
const base = {
|
|
2565
|
+
jobId,
|
|
2566
|
+
attempts: as === "queued" ? 0 : 1,
|
|
2567
|
+
createdAt,
|
|
2568
|
+
expiresAt: new Date(Date.parse(createdAt) + 10 * 6e4).toISOString(),
|
|
2569
|
+
target: {
|
|
2570
|
+
deviceId: job.captured.deviceId ?? "sim-device",
|
|
2571
|
+
peripheralId: job.captured.peripheralId ?? "sim-peripheral"
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
const outcome = as === "queued" ? { status: "queued" } : job.outcome;
|
|
2575
|
+
if (outcome.status === "needs_attention") {
|
|
2576
|
+
return {
|
|
2577
|
+
...base,
|
|
2578
|
+
status: "needs_attention",
|
|
2579
|
+
attention: {
|
|
2580
|
+
reason: outcome.reason,
|
|
2581
|
+
outcome: outcome.outcome,
|
|
2582
|
+
message: outcome.message ?? "Delivery stopped."
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
if (outcome.status === "sent_to_printer") {
|
|
2587
|
+
return { ...base, status: "sent_to_printer", sentToPrinterAt: createdAt };
|
|
2588
|
+
}
|
|
2589
|
+
return { ...base, status: "queued" };
|
|
2590
|
+
}
|
|
2591
|
+
function handleGetDurable(jobId) {
|
|
2592
|
+
if (!state.edgeDurableJobs.has(jobId)) return edgeFailure("Print job not found", 404);
|
|
2593
|
+
return success(durableView(jobId));
|
|
2594
|
+
}
|
|
2448
2595
|
async function handleShowDisplay(request) {
|
|
2449
2596
|
const body = await readJsonBody(request);
|
|
2450
2597
|
const captured = {
|
|
@@ -2494,6 +2641,13 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
2494
2641
|
if (subPath === "/print" && method === "POST") {
|
|
2495
2642
|
return handlePrint(request);
|
|
2496
2643
|
}
|
|
2644
|
+
if (subPath === "/print-jobs" && method === "POST") {
|
|
2645
|
+
return handleEnqueue(request);
|
|
2646
|
+
}
|
|
2647
|
+
const durableMatch = subPath.match(/^\/print-jobs\/([^/]+)$/);
|
|
2648
|
+
if (durableMatch && method === "GET") {
|
|
2649
|
+
return handleGetDurable(decodeURIComponent(durableMatch[1]));
|
|
2650
|
+
}
|
|
2497
2651
|
if (subPath === "/capabilities" && method === "GET") {
|
|
2498
2652
|
return handleCapabilities(request);
|
|
2499
2653
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Vitest test harness for Stardeck customer apps — in-process Postgres (PGlite) plus a control-plane simulator so the real Stardeck SDKs run unmodified in tests",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|