@tokenoftrust/storefront-runner 2.4.47 → 2.4.49

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.
@@ -1,19 +1,15 @@
1
1
  /**
2
- * Order READ DTO — the FROZEN v1 contract shape the storefront consumes from
3
- * the checkout order API's `GET /commerce/orders[/:orderId]` (card 8585). Kept in
4
- * `@tot/public-runtime` (types only, no secrets, edge-safe) so the server-side
5
- * order client, the Orders admin route, and the view-model mapping
6
- * all code against one source of truth.
2
+ * Admin order-list DTO plus the commerce contract.
7
3
  *
8
- * Authoritative API shape lives with the checkout order service; this file
9
- * freezes the storefront-facing v1 DTO.
4
+ * `CheckoutOrder*` is the list shape the orders workspace reads. Null
5
+ * verification is absent evidence, not clearance. It is not the commerce
6
+ * state machine and it is not the measured Foxy read body.
10
7
  *
11
- * READ-ONLY (scope-locks): no write/mutation shapes here. Two fields are
12
- * ENRICHED-later by U4 (the tot20 identity/age join) and are `null`/`[]` until
13
- * then — `verification` (summary) and `verificationDetail` (detail). Consumers
14
- * MUST tolerate that (render risk "pending", readiness/evidence empty) so no
15
- * contract change is needed when U4 lands.
8
+ * Commerce money is integer minor units. Payment, compliance, stock,
9
+ * fulfillment, and label are independent. Public events carry no customer
10
+ * identifier and no address. Caller admission is asymmetric JWKS only.
16
11
  */
12
+ import { sha256Hex } from "./hash.js";
17
13
 
18
14
  /** Payment state of an order. */
19
15
  export type CheckoutOrderPayment = "authorized" | "captured" | "refunded";
@@ -49,13 +45,12 @@ export interface CheckoutOrderSummary {
49
45
  /** Why the order is held, or null. */
50
46
  blocker: string | null;
51
47
  /**
52
- * Identity/age verification summary — ENRICHED by U4 (tot20 join). `null` from
53
- * U1; consumers render risk as "pending" until it is filled.
48
+ * Verification summary. Null is absent evidence, not a pass and not clearance.
54
49
  */
55
50
  verification: CheckoutOrderVerification | null;
56
51
  }
57
52
 
58
- /** U4-filled verification summary (shape reserved; opaque to U1/U2/U3). */
53
+ /** Verification summary carried on the admin list. Null stays null. */
59
54
  export interface CheckoutOrderVerification {
60
55
  riskLevel?: string;
61
56
  [k: string]: unknown;
@@ -87,7 +82,7 @@ export interface CheckoutOrderShippingAddress {
87
82
  name: string;
88
83
  }
89
84
 
90
- /** U4-filled per-order verification detail; `readinessChecks`/`evidenceTimeline` empty from U1. */
85
+ /** Per-order verification detail. An empty timeline is not clearance. */
91
86
  export interface CheckoutOrderVerificationDetail {
92
87
  readinessChecks: unknown[];
93
88
  evidenceTimeline: unknown[];
@@ -135,3 +130,594 @@ export interface CheckoutOrderListFilters {
135
130
  /** Opaque cursor from a prior page's `nextCursor`. */
136
131
  cursor?: string;
137
132
  }
133
+
134
+ /**
135
+ * Measured `GET /commerce/orders` body at Foxy `origin/main`
136
+ * `0d6037d563f5f33a057e7797b925455cefd1d90f` (`buildOrderPayload`).
137
+ * Offset paging. Not a cursor page. No translator into `CheckoutOrder*`.
138
+ */
139
+ export interface MeasuredFoxyOrderRead {
140
+ source: "foxycart";
141
+ externalId: string;
142
+ externalNumber: string;
143
+ orderedAt: string;
144
+ currency: string;
145
+ status: string;
146
+ customerEmail?: string;
147
+ customerName?: string;
148
+ subtotal: number;
149
+ shipping: number;
150
+ tax: number;
151
+ total: number;
152
+ shippingAddress: MeasuredFoxyAddress | null;
153
+ billingAddress: MeasuredFoxyAddress | null;
154
+ paymentRef: string;
155
+ paymentMetadata: Record<string, unknown>;
156
+ lineItems: readonly MeasuredFoxyLine[];
157
+ exciseTax: { amount: number; status: string | null };
158
+ }
159
+
160
+ export interface MeasuredFoxyAddress {
161
+ firstName: string;
162
+ lastName: string;
163
+ company: string;
164
+ line1: string;
165
+ line2: string | null;
166
+ locality: string;
167
+ regionCode: string;
168
+ postalCode: string;
169
+ countryCode: string;
170
+ }
171
+
172
+ export interface MeasuredFoxyLine {
173
+ sku: string;
174
+ name: string;
175
+ quantity: number;
176
+ unitPrice: number;
177
+ totalPrice?: number;
178
+ options: Record<string, unknown>;
179
+ flavor?: string;
180
+ }
181
+
182
+ export interface MeasuredFoxyOrderList {
183
+ orders: readonly MeasuredFoxyOrderRead[];
184
+ limit: number;
185
+ offset: number;
186
+ total: number;
187
+ hasMore: boolean;
188
+ }
189
+
190
+ export const COMMERCE_STAGES = ["preview", "publication"] as const;
191
+ export type CommerceStage = (typeof COMMERCE_STAGES)[number];
192
+
193
+ export const PAYMENT_STATES = [
194
+ "UNKNOWN",
195
+ "pending",
196
+ "authorized",
197
+ "captured",
198
+ "capture_unknown",
199
+ "voided",
200
+ "refund_requested",
201
+ "refunded",
202
+ "refund_unknown",
203
+ ] as const;
204
+ export type PaymentState = (typeof PAYMENT_STATES)[number];
205
+
206
+ export const COMPLIANCE_STATES = ["UNKNOWN", "pending", "fresh", "stale", "unreadable", "refused"] as const;
207
+ export type ComplianceState = (typeof COMPLIANCE_STATES)[number];
208
+
209
+ export const STOCK_STATES = ["UNKNOWN", "unallocated", "reserved", "confirmed", "released", "expired"] as const;
210
+ export type StockState = (typeof STOCK_STATES)[number];
211
+
212
+ export const FULFILLMENT_STATES = ["UNKNOWN", "held", "ready", "handed_off", "cancelled"] as const;
213
+ export type FulfillmentState = (typeof FULFILLMENT_STATES)[number];
214
+
215
+ export const LABEL_STATES = [
216
+ "UNKNOWN",
217
+ "none",
218
+ "intent",
219
+ "purchased",
220
+ "printed",
221
+ "void_requested",
222
+ "voided",
223
+ ] as const;
224
+ export type LabelState = (typeof LABEL_STATES)[number];
225
+
226
+ export const EVIDENCE_STATES = ["fresh", "UNKNOWN", "stale", "unreadable", "absent"] as const;
227
+ export type EvidenceState = (typeof EVIDENCE_STATES)[number];
228
+
229
+ export const PROVIDER_OUTCOMES = ["intent", "accepted", "unknown", "voided", "refunded"] as const;
230
+ export type ProviderOutcome = (typeof PROVIDER_OUTCOMES)[number];
231
+
232
+ export const COMMERCE_SCHEDULER = "tenant-pipeline-alarm";
233
+
234
+ /** Same path patterns as `WORKER_TIER3_PATH_PATTERNS`. Both providers, both phases. */
235
+ export const WORKER_ROUTE_PATTERNS = [
236
+ "/api/*",
237
+ "/auth/*",
238
+ "/dashboard",
239
+ "/dashboard/*",
240
+ "/search",
241
+ "/cart",
242
+ ] as const;
243
+
244
+ export const SCRAPPY_ORDERS_FIXTURE_DOMAIN = "scrappy-orders-v1";
245
+ export const FINAL_CART_DOMAIN = "scrappy-final-cart-v1";
246
+ export const CALLER_TOKEN_MAX_LIFETIME_MS = 15 * 60 * 1000;
247
+
248
+ export interface CommerceEvidence {
249
+ verification: EvidenceState;
250
+ tax: EvidenceState;
251
+ risk: EvidenceState;
252
+ stock: EvidenceState;
253
+ }
254
+
255
+ export interface MinorTotals {
256
+ subtotalMinor: number;
257
+ shippingMinor: number;
258
+ taxMinor: number;
259
+ totalMinor: number;
260
+ }
261
+
262
+ export interface PublicCommerceEvent {
263
+ eventVersion: number;
264
+ type: "order.observed";
265
+ orderId: string;
266
+ placedAt: string;
267
+ currency: string;
268
+ payment: PaymentState;
269
+ compliance: ComplianceState;
270
+ stock: StockState;
271
+ fulfillment: FulfillmentState;
272
+ label: LabelState;
273
+ totals: MinorTotals;
274
+ verificationDetail: CommerceEvidence;
275
+ }
276
+
277
+ export type CommerceRole = "owner" | "caller" | "denied";
278
+
279
+ export interface CommerceActor {
280
+ role: CommerceRole;
281
+ tenantId: string;
282
+ stage: CommerceStage;
283
+ }
284
+
285
+ export interface CommerceScope {
286
+ tenantId: string;
287
+ stage: CommerceStage;
288
+ }
289
+
290
+ export type CommerceCommand =
291
+ | "authorize-final-cart"
292
+ | "allocate"
293
+ | "refund"
294
+ | "cancel"
295
+ | "purchase-label"
296
+ | "print-label"
297
+ | "hand-off"
298
+ | "reconcile";
299
+
300
+ export interface CommerceCommandInput {
301
+ command: CommerceCommand;
302
+ payment: PaymentState;
303
+ stock: StockState;
304
+ fulfillment: FulfillmentState;
305
+ label: LabelState;
306
+ evidence: CommerceEvidence;
307
+ providerOutcome: ProviderOutcome | null;
308
+ }
309
+
310
+ export interface CommerceCommandDecision {
311
+ admit: boolean;
312
+ reason: "admitted" | "evidence" | "payment" | "stock" | "fulfillment" | "label" | "reconcile" | "reuse";
313
+ restock: false;
314
+ releaseReservation: boolean;
315
+ blindRepurchase: false;
316
+ }
317
+
318
+ export interface FinalCartLine {
319
+ sku: string;
320
+ quantity: number;
321
+ unitMinor: number;
322
+ }
323
+
324
+ export interface FinalCartBinding {
325
+ tenantId: string;
326
+ stage: CommerceStage;
327
+ currency: string;
328
+ lines: readonly FinalCartLine[];
329
+ destinationDigest: string;
330
+ verificationExpiresAt: string;
331
+ taxExpiresAt: string;
332
+ }
333
+
334
+ export interface AsymmetricCallerToken {
335
+ scheme: "asymmetric-jwks";
336
+ signatureValid: boolean;
337
+ kid: string;
338
+ issuer: string;
339
+ audience: string;
340
+ expiresAt: string;
341
+ jti: string;
342
+ tenantId: string;
343
+ stage: CommerceStage;
344
+ }
345
+
346
+ export type CallerToken = AsymmetricCallerToken | { scheme: "shared-secret" };
347
+
348
+ export type CallerTokenRefusal =
349
+ | "shared-secret"
350
+ | "signature"
351
+ | "kid"
352
+ | "issuer"
353
+ | "audience"
354
+ | "expired"
355
+ | "lifetime"
356
+ | "jti"
357
+ | "tenant"
358
+ | "stage";
359
+
360
+ export function evidencePermitsRelease(evidence: CommerceEvidence): boolean {
361
+ return (
362
+ evidence.verification === "fresh" &&
363
+ evidence.tax === "fresh" &&
364
+ evidence.risk === "fresh" &&
365
+ evidence.stock === "fresh"
366
+ );
367
+ }
368
+
369
+ export function paymentIsCaptured(payment: PaymentState): boolean {
370
+ return payment === "captured";
371
+ }
372
+
373
+ export function fulfillmentIsHandedOff(fulfillment: FulfillmentState): boolean {
374
+ return fulfillment === "handed_off";
375
+ }
376
+
377
+ export function assertMinor(value: number, label: string): number {
378
+ if (!Number.isSafeInteger(value)) throw new Error(`${label} is not a minor-unit integer`);
379
+ return value;
380
+ }
381
+
382
+ export function sumMinor(values: readonly number[]): number {
383
+ let total = 0;
384
+ for (const value of values) {
385
+ assertMinor(value, "amount");
386
+ total += value;
387
+ }
388
+ if (!Number.isSafeInteger(total)) throw new Error("minor-unit sum overflowed");
389
+ return total;
390
+ }
391
+
392
+ export function totalsMatch(totals: MinorTotals): boolean {
393
+ return (
394
+ sumMinor([totals.subtotalMinor, totals.shippingMinor, totals.taxMinor]) ===
395
+ assertMinor(totals.totalMinor, "totalMinor")
396
+ );
397
+ }
398
+
399
+ export function canonicalJson(value: unknown): string {
400
+ if (value === undefined) throw new Error("canonical JSON rejects undefined");
401
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
402
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
403
+ const record = value as Record<string, unknown>;
404
+ const keys = Object.keys(record).sort();
405
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
406
+ }
407
+
408
+ export async function digestCommerceFixture(document: Record<string, unknown>): Promise<string> {
409
+ const rest: Record<string, unknown> = {};
410
+ for (const key of Object.keys(document)) {
411
+ if (key !== "digest") rest[key] = document[key];
412
+ }
413
+ return sha256Hex(`${SCRAPPY_ORDERS_FIXTURE_DOMAIN}\n${canonicalJson(rest)}`);
414
+ }
415
+
416
+ export function idempotencyKey(
417
+ tenantId: string,
418
+ stage: CommerceStage,
419
+ source: string,
420
+ sourceEventId: string,
421
+ ): string {
422
+ return `${tenantId}\n${stage}\n${source}\n${sourceEventId}`;
423
+ }
424
+
425
+ export function allocationIdentity(input: {
426
+ tenantId: string;
427
+ stage: CommerceStage;
428
+ orderId: string;
429
+ sku: string;
430
+ locationId: string;
431
+ }): string {
432
+ return `${input.tenantId}\n${input.stage}\n${input.orderId}\n${input.sku}\n${input.locationId}`;
433
+ }
434
+
435
+ function deny(
436
+ reason: CommerceCommandDecision["reason"],
437
+ releaseReservation = false,
438
+ ): CommerceCommandDecision {
439
+ return { admit: false, reason, restock: false, releaseReservation, blindRepurchase: false };
440
+ }
441
+
442
+ function allow(releaseReservation = false): CommerceCommandDecision {
443
+ return { admit: true, reason: "admitted", restock: false, releaseReservation, blindRepurchase: false };
444
+ }
445
+
446
+ /** Compliance state is not an input. Evidence that is not fresh never clears. */
447
+ export function decideCommerceCommand(input: CommerceCommandInput): CommerceCommandDecision {
448
+ switch (input.command) {
449
+ case "reconcile":
450
+ return allow();
451
+ case "authorize-final-cart":
452
+ if (!evidencePermitsRelease(input.evidence)) return deny("evidence");
453
+ if (input.stock === "UNKNOWN") return deny("stock");
454
+ if (input.fulfillment === "cancelled") return deny("fulfillment");
455
+ if (input.payment === "capture_unknown") return deny("reconcile");
456
+ if (
457
+ input.payment === "refunded" ||
458
+ input.payment === "voided" ||
459
+ input.payment === "refund_requested" ||
460
+ input.payment === "refund_unknown"
461
+ ) {
462
+ return deny("payment");
463
+ }
464
+ return allow();
465
+ case "allocate":
466
+ if (!evidencePermitsRelease(input.evidence)) return deny("evidence");
467
+ if (input.stock === "UNKNOWN") return deny("stock");
468
+ if (input.fulfillment === "cancelled" || input.fulfillment === "handed_off") return deny("fulfillment");
469
+ return allow();
470
+ case "refund":
471
+ if (input.payment === "capture_unknown" || input.payment === "refund_unknown") return deny("reconcile");
472
+ if (input.payment !== "captured" && input.payment !== "refund_requested") return deny("payment");
473
+ return allow();
474
+ case "cancel":
475
+ if (input.fulfillment === "handed_off" || input.fulfillment === "UNKNOWN" || input.fulfillment === "cancelled") {
476
+ return deny("fulfillment");
477
+ }
478
+ if (input.payment === "capture_unknown") return deny("reconcile");
479
+ return allow(input.stock === "reserved" || input.stock === "confirmed");
480
+ case "purchase-label":
481
+ if (
482
+ input.label === "UNKNOWN" ||
483
+ input.label === "intent" ||
484
+ input.label === "void_requested" ||
485
+ input.providerOutcome === "unknown" ||
486
+ input.providerOutcome === "intent"
487
+ ) {
488
+ return deny("reconcile");
489
+ }
490
+ if (input.label === "purchased" || input.label === "printed") return deny("reuse");
491
+ if (!evidencePermitsRelease(input.evidence)) return deny("evidence");
492
+ if (input.payment !== "captured") return deny("payment");
493
+ if (input.stock !== "confirmed") return deny("stock");
494
+ if (input.fulfillment !== "ready") return deny("fulfillment");
495
+ if (input.label !== "none" && input.label !== "voided") return deny("label");
496
+ return allow();
497
+ case "print-label":
498
+ if (input.fulfillment === "cancelled") return deny("fulfillment");
499
+ if (input.label === "purchased" || input.label === "printed") return allow();
500
+ return deny("label");
501
+ case "hand-off":
502
+ if (input.providerOutcome === "unknown" || input.providerOutcome === "intent") return deny("reconcile");
503
+ if (!evidencePermitsRelease(input.evidence)) return deny("evidence");
504
+ if (input.payment !== "captured") return deny("payment");
505
+ if (input.stock !== "confirmed") return deny("stock");
506
+ if (input.fulfillment !== "ready") return deny("fulfillment");
507
+ if (input.label !== "purchased" && input.label !== "printed") return deny("label");
508
+ return allow();
509
+ default: {
510
+ const unreachable: never = input.command;
511
+ throw new Error(`unknown commerce command ${String(unreachable)}`);
512
+ }
513
+ }
514
+ }
515
+
516
+ export function actorMayCommand(actor: CommerceActor, resource: CommerceScope): boolean {
517
+ return (
518
+ (actor.role === "owner" || actor.role === "caller") &&
519
+ actor.tenantId === resource.tenantId &&
520
+ actor.stage === resource.stage
521
+ );
522
+ }
523
+
524
+ export function actorMayReadPrivate(actor: CommerceActor, resource: CommerceScope): boolean {
525
+ return actor.role === "owner" && actor.tenantId === resource.tenantId && actor.stage === resource.stage;
526
+ }
527
+
528
+ export function readOwnerAddress<T>(
529
+ actor: CommerceActor,
530
+ resource: CommerceScope,
531
+ read: () => T,
532
+ ): { ok: true; address: T } | { ok: false; reason: "denied" } {
533
+ if (!actorMayReadPrivate(actor, resource)) return { ok: false, reason: "denied" };
534
+ return { ok: true, address: read() };
535
+ }
536
+
537
+ export function admitTrackingRead(input: {
538
+ actor: CommerceActor;
539
+ resource: CommerceScope;
540
+ trackingUrl: string;
541
+ allowedHosts: readonly string[];
542
+ }): { ok: true } | { ok: false; reason: "denied" | "malformed-url" | "authority" } {
543
+ if (!actorMayReadPrivate(input.actor, input.resource)) return { ok: false, reason: "denied" };
544
+ let url: URL;
545
+ try {
546
+ url = new URL(input.trackingUrl);
547
+ } catch {
548
+ return { ok: false, reason: "malformed-url" };
549
+ }
550
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "") {
551
+ return { ok: false, reason: "authority" };
552
+ }
553
+ if (!input.allowedHosts.includes(url.host)) return { ok: false, reason: "authority" };
554
+ return { ok: true };
555
+ }
556
+
557
+ export function publicCommerceEvent(
558
+ input: {
559
+ eventVersion: number;
560
+ orderId: string;
561
+ placedAt: string;
562
+ currency: string;
563
+ payment: PaymentState;
564
+ compliance: ComplianceState;
565
+ stock: StockState;
566
+ fulfillment: FulfillmentState;
567
+ label: LabelState;
568
+ totals: MinorTotals;
569
+ verificationDetail: CommerceEvidence;
570
+ } & Record<string, unknown>,
571
+ ): PublicCommerceEvent {
572
+ if (!Number.isSafeInteger(input.eventVersion) || input.eventVersion < 1) {
573
+ throw new Error("eventVersion must be a positive integer");
574
+ }
575
+ if (!totalsMatch(input.totals)) throw new Error("totals do not match minor units");
576
+ return {
577
+ eventVersion: input.eventVersion,
578
+ type: "order.observed",
579
+ orderId: input.orderId,
580
+ placedAt: input.placedAt,
581
+ currency: input.currency,
582
+ payment: input.payment,
583
+ compliance: input.compliance,
584
+ stock: input.stock,
585
+ fulfillment: input.fulfillment,
586
+ label: input.label,
587
+ totals: {
588
+ subtotalMinor: input.totals.subtotalMinor,
589
+ shippingMinor: input.totals.shippingMinor,
590
+ taxMinor: input.totals.taxMinor,
591
+ totalMinor: input.totals.totalMinor,
592
+ },
593
+ verificationDetail: {
594
+ verification: input.verificationDetail.verification,
595
+ tax: input.verificationDetail.tax,
596
+ risk: input.verificationDetail.risk,
597
+ stock: input.verificationDetail.stock,
598
+ },
599
+ };
600
+ }
601
+
602
+ function encodeCursor(order: { placedAt: string; id: string }): string {
603
+ if (order.placedAt.includes("\n") || order.id.includes("\n")) {
604
+ throw new Error("cursor fields reject newlines");
605
+ }
606
+ const bytes = new TextEncoder().encode(`${order.placedAt}\n${order.id}`);
607
+ let binary = "";
608
+ for (const byte of bytes) binary += String.fromCharCode(byte);
609
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
610
+ }
611
+
612
+ function decodeCursor(cursor: string): { placedAt: string; id: string } | null {
613
+ try {
614
+ const padded = cursor.replaceAll("-", "+").replaceAll("_", "/") + "===".slice((cursor.length + 3) % 4);
615
+ const binary = atob(padded);
616
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
617
+ const text = new TextDecoder().decode(bytes);
618
+ const split = text.indexOf("\n");
619
+ if (split <= 0 || split === text.length - 1) return null;
620
+ return { placedAt: text.slice(0, split), id: text.slice(split + 1) };
621
+ } catch {
622
+ return null;
623
+ }
624
+ }
625
+
626
+ export function paginateOrders<T extends { id: string; placedAt: string }>(
627
+ orders: readonly T[],
628
+ limit: number,
629
+ cursor: string | null,
630
+ ): { orders: T[]; nextCursor: string | null; invalidCursor: boolean } {
631
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new Error("limit must be a positive integer");
632
+ const sorted = [...orders].sort((left, right) => {
633
+ if (left.placedAt !== right.placedAt) return left.placedAt < right.placedAt ? 1 : -1;
634
+ if (left.id === right.id) return 0;
635
+ return left.id < right.id ? 1 : -1;
636
+ });
637
+ let start = 0;
638
+ if (cursor !== null) {
639
+ const decoded = decodeCursor(cursor);
640
+ const index = decoded === null ? -1 : sorted.findIndex((order) => order.id === decoded.id && order.placedAt === decoded.placedAt);
641
+ if (index < 0) return { orders: [], nextCursor: null, invalidCursor: true };
642
+ start = index + 1;
643
+ }
644
+ const page = sorted.slice(start, start + limit);
645
+ const last = page[page.length - 1];
646
+ const nextCursor = page.length === limit && start + limit < sorted.length && last !== undefined ? encodeCursor(last) : null;
647
+ return { orders: page, nextCursor, invalidCursor: false };
648
+ }
649
+
650
+ export async function finalCartDigest(cart: FinalCartBinding): Promise<string> {
651
+ const lines = [...cart.lines].sort(
652
+ (left, right) => left.sku.localeCompare(right.sku) || left.unitMinor - right.unitMinor || left.quantity - right.quantity,
653
+ );
654
+ for (const line of lines) {
655
+ assertMinor(line.quantity, "quantity");
656
+ assertMinor(line.unitMinor, "unitMinor");
657
+ if (line.quantity < 1) throw new Error("quantity must be positive");
658
+ }
659
+ return sha256Hex(
660
+ `${FINAL_CART_DOMAIN}\n${canonicalJson({
661
+ currency: cart.currency,
662
+ destinationDigest: cart.destinationDigest,
663
+ lines,
664
+ stage: cart.stage,
665
+ taxExpiresAt: cart.taxExpiresAt,
666
+ tenantId: cart.tenantId,
667
+ verificationExpiresAt: cart.verificationExpiresAt,
668
+ })}`,
669
+ );
670
+ }
671
+
672
+ export function finalCartStillBound(
673
+ boundDigest: string,
674
+ actualDigest: string,
675
+ now: string,
676
+ expiresAt: string,
677
+ ): boolean {
678
+ return boundDigest === actualDigest && expiresAt > now;
679
+ }
680
+
681
+ export function admitCallerToken(input: {
682
+ token: CallerToken;
683
+ trustedIssuers: readonly string[];
684
+ expectedAudience: string;
685
+ activeKids: readonly string[];
686
+ now: string;
687
+ tenantId: string;
688
+ stage: CommerceStage;
689
+ }): { ok: true; replayKey: string } | { ok: false; reason: CallerTokenRefusal } {
690
+ if (input.token.scheme !== "asymmetric-jwks") return { ok: false, reason: "shared-secret" };
691
+ const token = input.token;
692
+ if (!token.signatureValid) return { ok: false, reason: "signature" };
693
+ if (!input.activeKids.includes(token.kid)) return { ok: false, reason: "kid" };
694
+ if (!input.trustedIssuers.includes(token.issuer)) return { ok: false, reason: "issuer" };
695
+ if (token.audience !== input.expectedAudience) return { ok: false, reason: "audience" };
696
+ const expiresAt = Date.parse(token.expiresAt);
697
+ const now = Date.parse(input.now);
698
+ if (!Number.isFinite(expiresAt) || !Number.isFinite(now) || expiresAt <= now) return { ok: false, reason: "expired" };
699
+ if (expiresAt - now > CALLER_TOKEN_MAX_LIFETIME_MS) return { ok: false, reason: "lifetime" };
700
+ if (token.jti.trim() === "") return { ok: false, reason: "jti" };
701
+ if (token.tenantId !== input.tenantId) return { ok: false, reason: "tenant" };
702
+ if (token.stage !== input.stage) return { ok: false, reason: "stage" };
703
+ return { ok: true, replayKey: `${token.issuer}\n${token.jti}` };
704
+ }
705
+
706
+ function matchWorkerPattern(path: string, pattern: string): boolean {
707
+ const expression = `^${pattern
708
+ .split("*")
709
+ .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
710
+ .join(".*")}$`;
711
+ return new RegExp(expression).test(path);
712
+ }
713
+
714
+ export function commerceApiOrigin(
715
+ provider: string,
716
+ phase: string,
717
+ path: string,
718
+ ): "worker" | "storage" | "refuse" {
719
+ if (provider !== "cloudflare" && provider !== "cloudfront") return "refuse";
720
+ if (phase !== "preview" && phase !== "publication") return "refuse";
721
+ if (WORKER_ROUTE_PATTERNS.some((pattern) => matchWorkerPattern(path, pattern))) return "worker";
722
+ return "storage";
723
+ }