@tokenoftrust/storefront-runner 2.4.50 → 2.4.51

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.
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Final-cart authorization decision. Payment is admitted only when the caller
3
+ * token, the bound cart, fresh evidence, the verification seam, and mirror stock
4
+ * all agree. Catalog availability and a browser confirmation are not authority.
5
+ */
6
+ import {
7
+ admitCallerToken,
8
+ assertMinor,
9
+ decideCommerceCommand,
10
+ finalCartDigest,
11
+ finalCartStillBound,
12
+ sumMinor,
13
+ type CallerToken,
14
+ type CommerceEvidence,
15
+ type CommerceStage,
16
+ type EvidenceState,
17
+ type FinalCartBinding,
18
+ type FulfillmentState,
19
+ type LabelState,
20
+ type PaymentState,
21
+ type StockState,
22
+ } from "@tot/public-runtime";
23
+ import { readMirrorAuthority, VERIFICATION_PROVIDER_OWNER, type ReserveHold } from "@/lib/inventory/allocations.js";
24
+
25
+ export interface SeamView {
26
+ status: "clear" | "absent" | "blocked";
27
+ failedClosed?: boolean;
28
+ codes?: readonly string[];
29
+ }
30
+
31
+ export interface FinalCartRequest {
32
+ now: string;
33
+ routedTenantId: string;
34
+ quoted: FinalCartBinding;
35
+ presented: FinalCartBinding;
36
+ evidence: CommerceEvidence | null;
37
+ seam: SeamView | null;
38
+ catalogAvailable: boolean;
39
+ browserConfirmed: boolean;
40
+ token: CallerToken | null;
41
+ trustedIssuers: readonly string[];
42
+ expectedAudience: string;
43
+ activeKids: readonly string[];
44
+ authorities: readonly unknown[];
45
+ payment?: PaymentState;
46
+ stock?: StockState;
47
+ fulfillment?: FulfillmentState;
48
+ label?: LabelState;
49
+ }
50
+
51
+ export type FinalCartDecision =
52
+ | {
53
+ ok: true;
54
+ digest: string;
55
+ expiresAt: string;
56
+ amountMinor: number;
57
+ holds: ReserveHold[];
58
+ issuer: string;
59
+ jti: string;
60
+ tenantId: string;
61
+ stage: CommerceStage;
62
+ }
63
+ | { ok: false; finding: string; owner?: string };
64
+
65
+ const EVIDENCE_FIELDS = ["verification", "tax", "risk", "stock"] as const;
66
+
67
+ function sortedLines(cart: FinalCartBinding) {
68
+ return [...cart.lines].sort(
69
+ (left, right) => left.sku.localeCompare(right.sku) || left.unitMinor - right.unitMinor || left.quantity - right.quantity,
70
+ );
71
+ }
72
+
73
+ function cartFinding(quoted: FinalCartBinding, presented: FinalCartBinding, routedTenantId: string): string | null {
74
+ if (quoted.tenantId !== routedTenantId || presented.tenantId !== routedTenantId || quoted.tenantId !== presented.tenantId) {
75
+ return "tenant-changed";
76
+ }
77
+ if (quoted.stage !== presented.stage) return "stage-changed";
78
+ if (quoted.currency !== presented.currency) return "currency-changed";
79
+ if (quoted.destinationDigest !== presented.destinationDigest) return "destination-changed";
80
+ const left = sortedLines(quoted);
81
+ const right = sortedLines(presented);
82
+ if (left.length !== right.length || left.some((line, index) => line.sku !== right[index]?.sku)) return "product-changed";
83
+ if (left.some((line, index) => line.unitMinor !== right[index]?.unitMinor)) return "price-changed";
84
+ if (left.some((line, index) => line.quantity !== right[index]?.quantity)) return "quantity-changed";
85
+ if (quoted.verificationExpiresAt !== presented.verificationExpiresAt || quoted.taxExpiresAt !== presented.taxExpiresAt) {
86
+ return "evidence-window-changed";
87
+ }
88
+ return null;
89
+ }
90
+
91
+ function evidenceFinding(evidence: CommerceEvidence | null): string | null {
92
+ if (!evidence) return "missing-evidence";
93
+ for (const field of EVIDENCE_FIELDS) {
94
+ const state: EvidenceState | string = evidence[field];
95
+ if (state === "fresh") continue;
96
+ if (state === "UNKNOWN") return `evidence-${field}-unknown`;
97
+ if (state === "stale" || state === "unreadable" || state === "absent") return `evidence-${field}-${state}`;
98
+ return `evidence-${field}-unreadable`;
99
+ }
100
+ return null;
101
+ }
102
+
103
+ function seamFinding(seam: SeamView | null): { finding: string; owner?: string } | null {
104
+ if (!seam || seam.status === "absent") return { finding: "verification-seam-absent" };
105
+ if (seam.status === "clear") return null;
106
+ const codes = (seam.codes ?? []).filter((code) => /^[a-z0-9_-]+$/.test(code));
107
+ if (seam.failedClosed && codes.some((code) => code === "verification_unconfigured" || code === "ruleset_unavailable")) {
108
+ return { finding: "verification-provider-unconfigured", owner: VERIFICATION_PROVIDER_OWNER };
109
+ }
110
+ return { finding: codes[0] ? `verification-refused:${codes[0]}` : "verification-refused" };
111
+ }
112
+
113
+ function isCatalogShape(value: unknown): boolean {
114
+ return value == null || (typeof value === "object" && value !== null && "available" in value && !("provider" in value));
115
+ }
116
+
117
+ function commandFinding(reason: string): string {
118
+ switch (reason) {
119
+ case "stock":
120
+ return "stock-unknown";
121
+ case "fulfillment":
122
+ return "fulfillment-refused";
123
+ case "reconcile":
124
+ return "payment-reconcile";
125
+ case "payment":
126
+ return "payment-refused";
127
+ default:
128
+ return "evidence-refused";
129
+ }
130
+ }
131
+
132
+ function earlierExpiry(left: string, right: string): string {
133
+ return left <= right ? left : right;
134
+ }
135
+
136
+ /** Deny with one specific finding, or clear a mirror hold for each sku. */
137
+ export async function evaluateFinalCart(input: FinalCartRequest): Promise<FinalCartDecision> {
138
+ if (input.browserConfirmed && (!input.token || input.token.scheme !== "asymmetric-jwks" || !input.token.signatureValid)) {
139
+ return { ok: false, finding: "browser-not-authority" };
140
+ }
141
+ if (!input.token) return { ok: false, finding: "signature" };
142
+ const admitted = admitCallerToken({
143
+ token: input.token,
144
+ trustedIssuers: input.trustedIssuers,
145
+ expectedAudience: input.expectedAudience,
146
+ activeKids: input.activeKids,
147
+ now: input.now,
148
+ tenantId: input.routedTenantId,
149
+ stage: input.presented.stage,
150
+ });
151
+ if (!admitted.ok) return { ok: false, finding: admitted.reason };
152
+
153
+ const changed = cartFinding(input.quoted, input.presented, input.routedTenantId);
154
+ if (changed) return { ok: false, finding: changed };
155
+
156
+ if (input.presented.lines.length < 1) return { ok: false, finding: "malformed-request" };
157
+
158
+ const evidence = evidenceFinding(input.evidence);
159
+ if (evidence) return { ok: false, finding: evidence };
160
+
161
+ const expiresAt = earlierExpiry(input.presented.verificationExpiresAt, input.presented.taxExpiresAt);
162
+ if (
163
+ input.presented.verificationExpiresAt <= input.now ||
164
+ input.presented.taxExpiresAt <= input.now ||
165
+ expiresAt <= input.now
166
+ ) {
167
+ return { ok: false, finding: "evidence-expired" };
168
+ }
169
+
170
+ let digest: string;
171
+ let amountMinor: number;
172
+ try {
173
+ for (const line of input.presented.lines) {
174
+ assertMinor(line.quantity, "quantity");
175
+ assertMinor(line.unitMinor, "unitMinor");
176
+ }
177
+ amountMinor = sumMinor(input.presented.lines.map((line) => line.unitMinor * line.quantity));
178
+ digest = await finalCartDigest(input.presented);
179
+ const quotedDigest = await finalCartDigest(input.quoted);
180
+ if (!finalCartStillBound(quotedDigest, digest, input.now, expiresAt)) return { ok: false, finding: "cart-unbound" };
181
+ } catch {
182
+ return { ok: false, finding: "malformed-request" };
183
+ }
184
+
185
+ const seam = seamFinding(input.seam);
186
+ if (seam) return { ok: false, finding: seam.finding, ...(seam.owner ? { owner: seam.owner } : {}) };
187
+
188
+ const command = decideCommerceCommand({
189
+ command: "authorize-final-cart",
190
+ payment: input.payment ?? "pending",
191
+ stock: input.stock ?? "unallocated",
192
+ fulfillment: input.fulfillment ?? "held",
193
+ label: input.label ?? "none",
194
+ evidence: input.evidence!,
195
+ providerOutcome: null,
196
+ });
197
+ if (!command.admit) return { ok: false, finding: commandFinding(command.reason) };
198
+
199
+ const quantities = new Map<string, number>();
200
+ for (const line of input.presented.lines) {
201
+ quantities.set(line.sku, (quantities.get(line.sku) ?? 0) + line.quantity);
202
+ }
203
+ const holds: ReserveHold[] = [];
204
+ for (const sku of [...quantities.keys()].sort()) {
205
+ const quantity = quantities.get(sku)!;
206
+ const named =
207
+ input.authorities.find((value) => {
208
+ if (!value || typeof value !== "object") return false;
209
+ return (value as { sku?: unknown }).sku === sku;
210
+ }) ?? (quantities.size === 1 ? (input.authorities[0] ?? null) : null);
211
+ if (named && typeof named === "object" && "provider" in named && (named as { provider: unknown }).provider !== "mirror") {
212
+ return { ok: false, finding: "authority-unavailable" };
213
+ }
214
+ const mirror = readMirrorAuthority(named);
215
+ if (!mirror || mirror.sku !== sku) {
216
+ return { ok: false, finding: input.catalogAvailable && isCatalogShape(named) ? "catalog-not-authority" : "authority-unavailable" };
217
+ }
218
+ if (mirror.onHand < quantity) return { ok: false, finding: "stock-unavailable" };
219
+ holds.push({ sku, locationId: mirror.locationId, quantity, onHand: mirror.onHand });
220
+ }
221
+
222
+ if (input.token.scheme !== "asymmetric-jwks") return { ok: false, finding: "shared-secret" };
223
+ return {
224
+ ok: true,
225
+ digest,
226
+ expiresAt,
227
+ amountMinor,
228
+ holds,
229
+ issuer: input.token.issuer,
230
+ jti: input.token.jti,
231
+ tenantId: input.routedTenantId,
232
+ stage: input.presented.stage,
233
+ };
234
+ }
@@ -0,0 +1,749 @@
1
+ /**
2
+ * Mirror-stock allocations for a final cart. Catalog availability is not a hold.
3
+ * One allocation per tenant, stage, order, sku, and location. Payment confirmation
4
+ * moves `reserved` to `confirmed` once; that quantity stays the decrement.
5
+ * Expiry and timeout release only `reserved`. Cancel before handoff releases once.
6
+ * A refund does not restock. There is no on-hand ledger in this schema; `onHand`
7
+ * is the mirror balance the caller resolved, never a catalog quantity.
8
+ */
9
+ import {
10
+ decideCommerceCommand,
11
+ type CommerceEvidence,
12
+ type CommerceStage,
13
+ type FulfillmentState,
14
+ type LabelState,
15
+ type PaymentState,
16
+ type StockState,
17
+ } from "@tot/public-runtime";
18
+
19
+ export const RESERVATION_AUTHORITY_HEADER = "x-tot-reservation-authority";
20
+ export const RESERVATION_AUTHORITY_NONE = "none";
21
+ /** Catalog `available` / `quantity` never authorizes a hold. */
22
+ export const CATALOG_IS_RESERVATION_AUTHORITY = false;
23
+ export const VERIFICATION_PROVIDER_OWNER = "tot-9577-tot-verification-provider";
24
+
25
+ const SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,80}$/;
26
+ const ID_RE = /^[a-z0-9][a-z0-9-]{0,80}$/;
27
+ const LOCATION_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,80}$/;
28
+ const ISSUER_RE = /^[A-Za-z0-9._:/?#&=%+-]{1,200}$/;
29
+ const JTI_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,128}$/;
30
+ const DIGEST_RE = /^[a-f0-9]{64}$/;
31
+
32
+ export interface SqlStatement {
33
+ sql: string;
34
+ params: readonly unknown[];
35
+ }
36
+
37
+ export interface StatementRunner {
38
+ all(sql: string, params: readonly unknown[]): Promise<readonly Record<string, unknown>[]>;
39
+ batch(statements: readonly SqlStatement[]): Promise<readonly (readonly Record<string, unknown>[])[]>;
40
+ }
41
+
42
+ interface SqliteStatement {
43
+ all(...params: unknown[]): unknown[];
44
+ }
45
+
46
+ export interface SqliteDatabase {
47
+ exec(sql: string): void;
48
+ prepare(sql: string): SqliteStatement;
49
+ }
50
+
51
+ interface D1Bound {
52
+ all(): Promise<unknown>;
53
+ }
54
+
55
+ export interface D1CommerceDatabase {
56
+ prepare(sql: string): { bind(...vals: unknown[]): D1Bound };
57
+ batch?(statements: unknown[]): Promise<unknown[]>;
58
+ }
59
+
60
+ export interface MirrorStockAuthority {
61
+ provider: "mirror";
62
+ readable: true;
63
+ locationId: string;
64
+ sku: string;
65
+ onHand: number;
66
+ asOf: string;
67
+ }
68
+
69
+ export interface ReserveHold {
70
+ sku: string;
71
+ locationId: string;
72
+ quantity: number;
73
+ onHand: number;
74
+ }
75
+
76
+ export interface ReserveInput {
77
+ tenantId: string;
78
+ stage: CommerceStage;
79
+ orderId: string;
80
+ cartDigest: string;
81
+ expiresAt: string;
82
+ now: string;
83
+ currency: string;
84
+ amountMinor: number;
85
+ issuer: string;
86
+ jti: string;
87
+ holds: readonly ReserveHold[];
88
+ }
89
+
90
+ export type ReserveResult =
91
+ | { ok: true; authorizationId: string; repeated: boolean; expiresAt: string }
92
+ | { ok: false; finding: "caller-replay" | "stock-unavailable" | "malformed-request" };
93
+
94
+ export interface PolicyResult {
95
+ released: number;
96
+ restock: false;
97
+ finding: string;
98
+ /** Units moved from reserved to confirmed. Present only for payment confirmation. */
99
+ decremented?: number;
100
+ }
101
+
102
+ type PolicyKind = "refund" | "cancel" | "payment-failure" | "confirm" | "expiry" | "timeout" | "restart";
103
+
104
+ export interface PolicyInput {
105
+ kind: PolicyKind;
106
+ tenantId: string;
107
+ stage: CommerceStage;
108
+ orderId?: string;
109
+ now?: string;
110
+ evidence?: CommerceEvidence;
111
+ providerOutcome?: "intent" | "accepted" | "unknown" | "voided" | "refunded" | null;
112
+ }
113
+
114
+ const FRESH_EVIDENCE: CommerceEvidence = {
115
+ verification: "fresh",
116
+ tax: "fresh",
117
+ risk: "fresh",
118
+ stock: "fresh",
119
+ };
120
+
121
+ let testOnHand: ((sku: string, locationId: string) => number | null) | null = null;
122
+
123
+ /**
124
+ * Non-production mirror balance. Production has no on-hand ledger in this schema,
125
+ * so the read stays empty and authorization fails closed.
126
+ */
127
+ export function __setMirrorOnHandForTests(reader: ((sku: string, locationId: string) => number | null) | null): void {
128
+ testOnHand = reader;
129
+ }
130
+
131
+ export function mirrorOnHand(sku: string, locationId: string): number | null {
132
+ if (process.env.NODE_ENV === "production") return null;
133
+ if (!testOnHand) return null;
134
+ const onHand = testOnHand(sku, locationId);
135
+ if (typeof onHand !== "number" || !Number.isSafeInteger(onHand) || onHand < 0) return null;
136
+ return onHand;
137
+ }
138
+
139
+ export function reservationAuthorityHeader(): typeof RESERVATION_AUTHORITY_NONE {
140
+ if (CATALOG_IS_RESERVATION_AUTHORITY) {
141
+ throw new Error("catalog availability is not reservation authority");
142
+ }
143
+ return RESERVATION_AUTHORITY_NONE;
144
+ }
145
+
146
+ export function readMirrorAuthority(value: unknown): MirrorStockAuthority | null {
147
+ if (!value || typeof value !== "object") return null;
148
+ const record = value as Record<string, unknown>;
149
+ if (record.provider !== "mirror" || record.readable !== true) return null;
150
+ if (typeof record.locationId !== "string" || !LOCATION_RE.test(record.locationId)) return null;
151
+ if (typeof record.sku !== "string" || !SKU_RE.test(record.sku)) return null;
152
+ if (typeof record.onHand !== "number" || !Number.isSafeInteger(record.onHand) || record.onHand < 0) return null;
153
+ if (typeof record.asOf !== "string" || record.asOf.trim() === "" || record.asOf.length > 40) return null;
154
+ return {
155
+ provider: "mirror",
156
+ readable: true,
157
+ locationId: record.locationId,
158
+ sku: record.sku,
159
+ onHand: record.onHand,
160
+ asOf: record.asOf,
161
+ };
162
+ }
163
+
164
+ const chains = new WeakMap<object, Promise<unknown>>();
165
+
166
+ function enqueue<T>(key: object, job: () => Promise<T> | T): Promise<T> {
167
+ const previous = chains.get(key) ?? Promise.resolve();
168
+ const run = previous.then(job, job);
169
+ chains.set(
170
+ key,
171
+ run.then(
172
+ () => undefined,
173
+ () => undefined,
174
+ ),
175
+ );
176
+ return run;
177
+ }
178
+
179
+ function asRows(value: unknown): Record<string, unknown>[] {
180
+ if (Array.isArray(value)) return value as Record<string, unknown>[];
181
+ if (value && typeof value === "object" && Array.isArray((value as { results?: unknown }).results)) {
182
+ return (value as { results: Record<string, unknown>[] }).results;
183
+ }
184
+ return [];
185
+ }
186
+
187
+ export function sqliteCommerceRunner(db: SqliteDatabase): StatementRunner {
188
+ return {
189
+ all(sql, params) {
190
+ return enqueue(db, () => db.prepare(sql).all(...params) as Record<string, unknown>[]);
191
+ },
192
+ batch(statements) {
193
+ return enqueue(db, () => {
194
+ db.exec("BEGIN IMMEDIATE");
195
+ try {
196
+ const results = statements.map(
197
+ (statement) => db.prepare(statement.sql).all(...statement.params) as Record<string, unknown>[],
198
+ );
199
+ db.exec("COMMIT");
200
+ return results;
201
+ } catch (error) {
202
+ try {
203
+ db.exec("ROLLBACK");
204
+ } catch {
205
+ /* the failed statement already aborted the transaction */
206
+ }
207
+ throw error;
208
+ }
209
+ });
210
+ },
211
+ };
212
+ }
213
+
214
+ export function d1CommerceRunner(db: D1CommerceDatabase): StatementRunner {
215
+ return {
216
+ async all(sql, params) {
217
+ return asRows(await db.prepare(sql).bind(...params).all());
218
+ },
219
+ async batch(statements) {
220
+ if (!db.batch) {
221
+ const results: Record<string, unknown>[][] = [];
222
+ for (const statement of statements) {
223
+ results.push(asRows(await db.prepare(statement.sql).bind(...statement.params).all()));
224
+ }
225
+ return results;
226
+ }
227
+ const bound: unknown[] = statements.map((statement) => db.prepare(statement.sql).bind(...statement.params));
228
+ const results = await db.batch(bound);
229
+ return results.map((result) => asRows(result));
230
+ },
231
+ };
232
+ }
233
+
234
+ function sumQuantity(rows: readonly Record<string, unknown>[]): number {
235
+ let total = 0;
236
+ for (const row of rows) {
237
+ const quantity = row.quantity;
238
+ if (typeof quantity === "number" && Number.isSafeInteger(quantity) && quantity > 0) total += quantity;
239
+ }
240
+ return total;
241
+ }
242
+
243
+ function text(row: Record<string, unknown>, key: string): string {
244
+ const value = row[key];
245
+ return typeof value === "string" ? value : "";
246
+ }
247
+
248
+ function reserveShape(input: ReserveInput): ReserveResult | null {
249
+ if (!ID_RE.test(input.tenantId) || !ID_RE.test(input.orderId)) return { ok: false, finding: "malformed-request" };
250
+ if (input.stage !== "preview" && input.stage !== "publication") return { ok: false, finding: "malformed-request" };
251
+ if (!DIGEST_RE.test(input.cartDigest) || !/^[A-Z]{3}$/.test(input.currency)) return { ok: false, finding: "malformed-request" };
252
+ if (!ISSUER_RE.test(input.issuer) || !JTI_RE.test(input.jti)) return { ok: false, finding: "malformed-request" };
253
+ if (!Number.isSafeInteger(input.amountMinor) || input.amountMinor < 0) return { ok: false, finding: "malformed-request" };
254
+ if (input.holds.length < 1) return { ok: false, finding: "malformed-request" };
255
+ for (const hold of input.holds) {
256
+ if (!SKU_RE.test(hold.sku) || !LOCATION_RE.test(hold.locationId)) return { ok: false, finding: "malformed-request" };
257
+ if (!Number.isSafeInteger(hold.quantity) || hold.quantity < 1) return { ok: false, finding: "malformed-request" };
258
+ if (!Number.isSafeInteger(hold.onHand) || hold.onHand < 0) return { ok: false, finding: "malformed-request" };
259
+ }
260
+ return null;
261
+ }
262
+
263
+ function allocationInsert(input: ReserveInput, hold: ReserveHold, seenAt: string): SqlStatement {
264
+ return {
265
+ sql: `INSERT INTO scrappy_allocations (tenant_id, stage, order_id, sku, location_id, quantity, state)
266
+ SELECT ?, ?, ?, ?, ?, ?, 'reserved'
267
+ WHERE (
268
+ SELECT COALESCE(SUM(quantity), 0) FROM scrappy_allocations
269
+ WHERE tenant_id = ? AND stage = ? AND sku = ? AND location_id = ? AND state IN ('reserved', 'confirmed')
270
+ ) + ? <= ?
271
+ AND NOT EXISTS (
272
+ SELECT 1 FROM scrappy_allocations
273
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND sku = ? AND location_id = ?
274
+ AND state IN ('reserved', 'confirmed')
275
+ )
276
+ AND NOT EXISTS (
277
+ SELECT 1 FROM scrappy_final_cart_authorizations
278
+ WHERE tenant_id = ? AND stage = ? AND cart_digest = ? AND state IN ('active', 'consumed')
279
+ )
280
+ AND EXISTS (
281
+ SELECT 1 FROM scrappy_caller_replay
282
+ WHERE tenant_id = ? AND stage = ? AND issuer = ? AND jti = ? AND seen_at = ?
283
+ )
284
+ AND EXISTS (
285
+ SELECT 1 FROM scrappy_commerce_orders
286
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?
287
+ )
288
+ ON CONFLICT DO NOTHING
289
+ RETURNING order_id`,
290
+ params: [
291
+ input.tenantId,
292
+ input.stage,
293
+ input.orderId,
294
+ hold.sku,
295
+ hold.locationId,
296
+ hold.quantity,
297
+ input.tenantId,
298
+ input.stage,
299
+ hold.sku,
300
+ hold.locationId,
301
+ hold.quantity,
302
+ hold.onHand,
303
+ input.tenantId,
304
+ input.stage,
305
+ input.orderId,
306
+ hold.sku,
307
+ hold.locationId,
308
+ input.tenantId,
309
+ input.stage,
310
+ input.cartDigest,
311
+ input.tenantId,
312
+ input.stage,
313
+ input.issuer,
314
+ input.jti,
315
+ seenAt,
316
+ input.tenantId,
317
+ input.stage,
318
+ input.orderId,
319
+ ],
320
+ };
321
+ }
322
+
323
+ /** Admit one hold per sku. A second active digest returns the existing authorization. */
324
+ export async function reserveAllocation(runner: StatementRunner, input: ReserveInput): Promise<ReserveResult> {
325
+ const malformed = reserveShape(input);
326
+ if (malformed) return malformed;
327
+ const seen = await runner.all(
328
+ `SELECT jti FROM scrappy_caller_replay WHERE tenant_id = ? AND stage = ? AND issuer = ? AND jti = ?`,
329
+ [input.tenantId, input.stage, input.issuer, input.jti],
330
+ );
331
+ if (seen.length > 0) return { ok: false, finding: "caller-replay" };
332
+
333
+ const seenAt = `${input.now}|${crypto.randomUUID()}`;
334
+ const holds = [...input.holds].sort((left, right) => left.sku.localeCompare(right.sku) || left.locationId.localeCompare(right.locationId));
335
+ const statements: SqlStatement[] = [
336
+ {
337
+ sql: `INSERT INTO scrappy_caller_replay (tenant_id, stage, issuer, jti, seen_at)
338
+ VALUES (?, ?, ?, ?, ?)
339
+ ON CONFLICT DO NOTHING
340
+ RETURNING jti`,
341
+ params: [input.tenantId, input.stage, input.issuer, input.jti, seenAt],
342
+ },
343
+ {
344
+ sql: `INSERT INTO scrappy_commerce_orders (
345
+ tenant_id, stage, order_id, source, source_order_id, currency, placed_at,
346
+ payment_state, compliance_state, stock_state, fulfillment_state, label_state,
347
+ amount_minor, event_version
348
+ ) VALUES (?, ?, ?, 'scrappy', ?, ?, ?, 'pending', 'UNKNOWN', 'unallocated', 'held', 'none', ?, 1)
349
+ ON CONFLICT DO NOTHING
350
+ RETURNING order_id`,
351
+ params: [input.tenantId, input.stage, input.orderId, input.orderId, input.currency, input.now, input.amountMinor],
352
+ },
353
+ ...holds.map((hold) => allocationInsert(input, hold, seenAt)),
354
+ {
355
+ sql: `INSERT INTO scrappy_final_cart_authorizations (
356
+ tenant_id, stage, authorization_id, cart_digest, expires_at, state
357
+ )
358
+ SELECT ?, ?, ?, ?, ?, 'active'
359
+ WHERE (
360
+ SELECT COUNT(*) FROM scrappy_allocations
361
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND state = 'reserved'
362
+ ) = ?
363
+ AND NOT EXISTS (
364
+ SELECT 1 FROM scrappy_final_cart_authorizations
365
+ WHERE tenant_id = ? AND stage = ? AND cart_digest = ? AND state IN ('active', 'consumed')
366
+ )
367
+ AND EXISTS (
368
+ SELECT 1 FROM scrappy_caller_replay
369
+ WHERE tenant_id = ? AND stage = ? AND issuer = ? AND jti = ? AND seen_at = ?
370
+ )
371
+ ON CONFLICT DO NOTHING
372
+ RETURNING authorization_id`,
373
+ params: [
374
+ input.tenantId,
375
+ input.stage,
376
+ input.orderId,
377
+ input.cartDigest,
378
+ input.expiresAt,
379
+ input.tenantId,
380
+ input.stage,
381
+ input.orderId,
382
+ holds.length,
383
+ input.tenantId,
384
+ input.stage,
385
+ input.cartDigest,
386
+ input.tenantId,
387
+ input.stage,
388
+ input.issuer,
389
+ input.jti,
390
+ seenAt,
391
+ ],
392
+ },
393
+ {
394
+ sql: `UPDATE scrappy_allocations
395
+ SET state = 'released'
396
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND state = 'reserved'
397
+ AND NOT EXISTS (
398
+ SELECT 1 FROM scrappy_final_cart_authorizations
399
+ WHERE tenant_id = ? AND stage = ? AND authorization_id = ? AND state = 'active'
400
+ )
401
+ RETURNING quantity`,
402
+ params: [input.tenantId, input.stage, input.orderId, input.tenantId, input.stage, input.orderId],
403
+ },
404
+ {
405
+ sql: `UPDATE scrappy_commerce_orders
406
+ SET stock_state = 'reserved'
407
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?
408
+ AND EXISTS (
409
+ SELECT 1 FROM scrappy_final_cart_authorizations
410
+ WHERE tenant_id = ? AND stage = ? AND authorization_id = ? AND state = 'active'
411
+ )
412
+ RETURNING order_id`,
413
+ params: [input.tenantId, input.stage, input.orderId, input.tenantId, input.stage, input.orderId],
414
+ },
415
+ {
416
+ sql: `SELECT authorization_id, expires_at FROM scrappy_final_cart_authorizations
417
+ WHERE tenant_id = ? AND stage = ? AND cart_digest = ? AND state IN ('active', 'consumed')
418
+ ORDER BY CASE state WHEN 'active' THEN 0 ELSE 1 END
419
+ LIMIT 1`,
420
+ params: [input.tenantId, input.stage, input.cartDigest],
421
+ },
422
+ ];
423
+
424
+ const results = await runner.batch(statements);
425
+ const replayRows = results[0] ?? [];
426
+ const authRows = results[2 + holds.length] ?? [];
427
+ const selected = results[results.length - 1] ?? [];
428
+ if (authRows.length > 0) {
429
+ return { ok: true, authorizationId: input.orderId, repeated: false, expiresAt: input.expiresAt };
430
+ }
431
+ if (replayRows.length === 0) return { ok: false, finding: "caller-replay" };
432
+ const existing = selected[0];
433
+ if (existing) {
434
+ return {
435
+ ok: true,
436
+ authorizationId: text(existing, "authorization_id"),
437
+ repeated: true,
438
+ expiresAt: text(existing, "expires_at"),
439
+ };
440
+ }
441
+ return { ok: false, finding: "stock-unavailable" };
442
+ }
443
+
444
+ export async function releaseHoldOnce(
445
+ runner: StatementRunner,
446
+ input: {
447
+ tenantId: string;
448
+ stage: CommerceStage;
449
+ orderId: string;
450
+ allocationState: "released" | "expired";
451
+ authState: "denied" | "expired";
452
+ orderStock: StockState;
453
+ },
454
+ ): Promise<number> {
455
+ const scope = [input.tenantId, input.stage, input.orderId];
456
+ const results = await runner.batch([
457
+ {
458
+ sql: `UPDATE scrappy_final_cart_authorizations
459
+ SET state = ?
460
+ WHERE tenant_id = ? AND stage = ? AND authorization_id = ? AND state IN ('active', 'consumed')
461
+ RETURNING authorization_id`,
462
+ params: [input.authState, ...scope],
463
+ },
464
+ {
465
+ sql: `UPDATE scrappy_allocations
466
+ SET state = ?
467
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND state IN ('reserved', 'confirmed')
468
+ RETURNING quantity`,
469
+ params: [input.allocationState, ...scope],
470
+ },
471
+ {
472
+ sql: `UPDATE scrappy_commerce_orders
473
+ SET stock_state = ?
474
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND stock_state IN ('reserved', 'confirmed')
475
+ RETURNING order_id`,
476
+ params: [input.orderStock, ...scope],
477
+ },
478
+ ]);
479
+ return sumQuantity(results[1] ?? []);
480
+ }
481
+
482
+ export async function settleExpired(
483
+ runner: StatementRunner,
484
+ input: { tenantId: string; stage: CommerceStage; now: string; orderId?: string; force: boolean },
485
+ ): Promise<number> {
486
+ const orderId = input.orderId ?? "";
487
+ const force = input.force ? 1 : 0;
488
+ const due = [input.tenantId, input.stage, orderId, orderId, input.now, force];
489
+ const results = await runner.batch([
490
+ {
491
+ sql: `UPDATE scrappy_final_cart_authorizations
492
+ SET state = 'expired'
493
+ WHERE tenant_id = ? AND stage = ? AND state = 'active'
494
+ AND (? = '' OR authorization_id = ?)
495
+ AND (expires_at <= ? OR ? = 1)
496
+ RETURNING authorization_id`,
497
+ params: due,
498
+ },
499
+ {
500
+ sql: `UPDATE scrappy_allocations
501
+ SET state = 'expired'
502
+ WHERE tenant_id = ? AND stage = ? AND state = 'reserved'
503
+ AND order_id IN (
504
+ SELECT authorization_id FROM scrappy_final_cart_authorizations
505
+ WHERE tenant_id = ? AND stage = ? AND state = 'expired'
506
+ AND (? = '' OR authorization_id = ?)
507
+ AND (expires_at <= ? OR ? = 1)
508
+ )
509
+ RETURNING quantity`,
510
+ params: [input.tenantId, input.stage, ...due],
511
+ },
512
+ {
513
+ sql: `UPDATE scrappy_commerce_orders
514
+ SET stock_state = 'expired'
515
+ WHERE tenant_id = ? AND stage = ? AND stock_state IN ('reserved', 'confirmed')
516
+ AND order_id IN (
517
+ SELECT authorization_id FROM scrappy_final_cart_authorizations
518
+ WHERE tenant_id = ? AND stage = ? AND state = 'expired'
519
+ AND (? = '' OR authorization_id = ?)
520
+ AND (expires_at <= ? OR ? = 1)
521
+ )
522
+ RETURNING order_id`,
523
+ params: [input.tenantId, input.stage, ...due],
524
+ },
525
+ ]);
526
+ return sumQuantity(results[1] ?? []);
527
+ }
528
+
529
+ export async function releaseOrphanAllocations(
530
+ runner: StatementRunner,
531
+ input: { tenantId: string; stage: CommerceStage },
532
+ ): Promise<number> {
533
+ const results = await runner.batch([
534
+ {
535
+ sql: `UPDATE scrappy_allocations
536
+ SET state = 'released'
537
+ WHERE tenant_id = ? AND stage = ?
538
+ AND state IN ('reserved', 'confirmed')
539
+ AND NOT EXISTS (
540
+ SELECT 1 FROM scrappy_final_cart_authorizations AS auth
541
+ WHERE auth.tenant_id = scrappy_allocations.tenant_id
542
+ AND auth.stage = scrappy_allocations.stage
543
+ AND auth.authorization_id = scrappy_allocations.order_id
544
+ AND auth.state IN ('active', 'consumed')
545
+ )
546
+ RETURNING quantity`,
547
+ params: [input.tenantId, input.stage],
548
+ },
549
+ ]);
550
+ return sumQuantity(results[0] ?? []);
551
+ }
552
+
553
+ interface OrderRow {
554
+ payment: PaymentState;
555
+ stock: StockState;
556
+ fulfillment: FulfillmentState;
557
+ label: LabelState;
558
+ }
559
+
560
+ async function loadOrder(
561
+ runner: StatementRunner,
562
+ tenantId: string,
563
+ stage: CommerceStage,
564
+ orderId: string,
565
+ ): Promise<OrderRow | null> {
566
+ const rows = await runner.all(
567
+ `SELECT payment_state, stock_state, fulfillment_state, label_state
568
+ FROM scrappy_commerce_orders WHERE tenant_id = ? AND stage = ? AND order_id = ?`,
569
+ [tenantId, stage, orderId],
570
+ );
571
+ const row = rows[0];
572
+ if (!row) return null;
573
+ const allocs = await runner.all(
574
+ `SELECT state FROM scrappy_allocations WHERE tenant_id = ? AND stage = ? AND order_id = ?`,
575
+ [tenantId, stage, orderId],
576
+ );
577
+ const states = allocs.map((alloc) => text(alloc, "state"));
578
+ const stock = states.includes("reserved")
579
+ ? "reserved"
580
+ : states.includes("confirmed")
581
+ ? "confirmed"
582
+ : (text(row, "stock_state") as StockState);
583
+ return {
584
+ payment: text(row, "payment_state") as PaymentState,
585
+ stock,
586
+ fulfillment: text(row, "fulfillment_state") as FulfillmentState,
587
+ label: text(row, "label_state") as LabelState,
588
+ };
589
+ }
590
+
591
+ function commandFinding(reason: string): string {
592
+ switch (reason) {
593
+ case "stock":
594
+ return "stock-unknown";
595
+ case "fulfillment":
596
+ return "fulfillment-refused";
597
+ case "reconcile":
598
+ return "payment-reconcile";
599
+ case "payment":
600
+ return "payment-refused";
601
+ case "evidence":
602
+ return "evidence-refused";
603
+ default:
604
+ return "fulfillment-refused";
605
+ }
606
+ }
607
+
608
+ /** Move reserved units to confirmed once. A second call decrements nothing. */
609
+ export async function confirmHoldOnce(
610
+ runner: StatementRunner,
611
+ input: { tenantId: string; stage: CommerceStage; orderId: string },
612
+ ): Promise<{ decremented: number; states: string[] }> {
613
+ const scope = [input.tenantId, input.stage, input.orderId];
614
+ const results = await runner.batch([
615
+ {
616
+ sql: `UPDATE scrappy_allocations
617
+ SET state = 'confirmed'
618
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND state = 'reserved'
619
+ AND EXISTS (
620
+ SELECT 1 FROM scrappy_commerce_orders
621
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?
622
+ AND fulfillment_state IN ('held', 'ready')
623
+ AND payment_state IN ('pending', 'authorized', 'captured')
624
+ )
625
+ AND EXISTS (
626
+ SELECT 1 FROM scrappy_final_cart_authorizations
627
+ WHERE tenant_id = ? AND stage = ? AND authorization_id = ? AND state = 'active'
628
+ )
629
+ RETURNING quantity`,
630
+ params: [...scope, ...scope, ...scope],
631
+ },
632
+ {
633
+ sql: `UPDATE scrappy_final_cart_authorizations
634
+ SET state = 'consumed'
635
+ WHERE tenant_id = ? AND stage = ? AND authorization_id = ? AND state = 'active'
636
+ AND EXISTS (
637
+ SELECT 1 FROM scrappy_allocations
638
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND state = 'confirmed'
639
+ )
640
+ RETURNING authorization_id`,
641
+ params: [...scope, ...scope],
642
+ },
643
+ {
644
+ sql: `UPDATE scrappy_commerce_orders
645
+ SET payment_state = CASE
646
+ WHEN payment_state IN ('pending', 'authorized') THEN 'captured'
647
+ ELSE payment_state
648
+ END,
649
+ stock_state = 'confirmed'
650
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?
651
+ AND fulfillment_state IN ('held', 'ready')
652
+ AND payment_state IN ('pending', 'authorized', 'captured')
653
+ AND EXISTS (
654
+ SELECT 1 FROM scrappy_allocations
655
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND state = 'confirmed'
656
+ )
657
+ RETURNING order_id`,
658
+ params: [...scope, ...scope],
659
+ },
660
+ {
661
+ sql: `SELECT state FROM scrappy_allocations
662
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?`,
663
+ params: scope,
664
+ },
665
+ ]);
666
+ return {
667
+ decremented: sumQuantity(results[0] ?? []),
668
+ states: (results[3] ?? []).map((row) => text(row, "state")),
669
+ };
670
+ }
671
+
672
+ /** Apply the recorded release policy. `restock` stays false on every result. */
673
+ export async function applyAllocationPolicy(runner: StatementRunner, input: PolicyInput): Promise<PolicyResult> {
674
+ const scope = { tenantId: input.tenantId, stage: input.stage };
675
+ if (input.kind === "confirm") {
676
+ if (!input.orderId) return { released: 0, restock: false, decremented: 0, finding: "order-absent" };
677
+ const order = await loadOrder(runner, input.tenantId, input.stage, input.orderId);
678
+ if (!order) return { released: 0, restock: false, decremented: 0, finding: "order-absent" };
679
+ if (order.fulfillment === "handed_off") return { released: 0, restock: false, decremented: 0, finding: "shipped-out-of-scope" };
680
+ if (order.fulfillment === "cancelled" || order.fulfillment === "UNKNOWN") {
681
+ return { released: 0, restock: false, decremented: 0, finding: "fulfillment-refused" };
682
+ }
683
+ const confirmed = await confirmHoldOnce(runner, { tenantId: input.tenantId, stage: input.stage, orderId: input.orderId });
684
+ if (confirmed.decremented > 0) return { released: 0, restock: false, decremented: confirmed.decremented, finding: "confirmed" };
685
+ if (confirmed.states.includes("confirmed")) return { released: 0, restock: false, decremented: 0, finding: "already-confirmed" };
686
+ return { released: 0, restock: false, decremented: 0, finding: "already-released" };
687
+ }
688
+ if (input.kind === "restart") {
689
+ const released = await releaseOrphanAllocations(runner, scope);
690
+ return { released, restock: false, finding: "restart-reconciled" };
691
+ }
692
+ if (input.kind === "expiry" || input.kind === "timeout") {
693
+ const released = await settleExpired(runner, {
694
+ ...scope,
695
+ now: input.now ?? "",
696
+ orderId: input.orderId,
697
+ force: input.kind === "timeout",
698
+ });
699
+ return { released, restock: false, finding: released > 0 ? "expired" : "already-released" };
700
+ }
701
+ if (!input.orderId) return { released: 0, restock: false, finding: "order-absent" };
702
+ const order = await loadOrder(runner, input.tenantId, input.stage, input.orderId);
703
+ if (!order) return { released: 0, restock: false, finding: "order-absent" };
704
+
705
+ if (input.kind === "payment-failure") {
706
+ if (order.fulfillment === "handed_off") return { released: 0, restock: false, finding: "shipped-out-of-scope" };
707
+ if (order.stock === "confirmed" || order.payment === "captured") {
708
+ return { released: 0, restock: false, finding: "already-confirmed" };
709
+ }
710
+ const released = await releaseHoldOnce(runner, {
711
+ tenantId: input.tenantId,
712
+ stage: input.stage,
713
+ orderId: input.orderId,
714
+ allocationState: "released",
715
+ authState: "denied",
716
+ orderStock: "released",
717
+ });
718
+ return { released, restock: false, finding: released > 0 ? "released" : "already-released" };
719
+ }
720
+
721
+ const decision = decideCommerceCommand({
722
+ command: input.kind,
723
+ payment: order.payment,
724
+ stock: order.stock,
725
+ fulfillment: order.fulfillment,
726
+ label: order.label,
727
+ evidence: input.evidence ?? FRESH_EVIDENCE,
728
+ providerOutcome: input.providerOutcome ?? null,
729
+ });
730
+ if (input.kind === "refund") {
731
+ return {
732
+ released: 0,
733
+ restock: false,
734
+ finding: decision.admit ? "refund-no-restock" : commandFinding(decision.reason),
735
+ };
736
+ }
737
+ if (!decision.admit || !decision.releaseReservation) {
738
+ return { released: 0, restock: false, finding: decision.admit ? "cancel-no-release" : commandFinding(decision.reason) };
739
+ }
740
+ const released = await releaseHoldOnce(runner, {
741
+ tenantId: input.tenantId,
742
+ stage: input.stage,
743
+ orderId: input.orderId,
744
+ allocationState: "released",
745
+ authState: "denied",
746
+ orderStock: "released",
747
+ });
748
+ return { released, restock: false, finding: released > 0 ? "released" : "already-released" };
749
+ }
@@ -0,0 +1,366 @@
1
+ /**
2
+ * POST /api/commerce/final-cart-authorization — time-limited final-cart admission.
3
+ * Caller-owned asymmetric tokens only. Catalog availability, a browser confirmation,
4
+ * and missing verification evidence do not authorize payment.
5
+ */
6
+ import type { APIContext } from "astro";
7
+ import { createLocalJWKSet, decodeJwt, decodeProtectedHeader, jwtVerify, type JSONWebKeySet, type JWTPayload } from "jose";
8
+ import type { TenantConfig } from "@tot/public-runtime";
9
+ import {
10
+ type CallerToken,
11
+ type CommerceEvidence,
12
+ type CommerceStage,
13
+ type EvidenceState,
14
+ type FinalCartBinding,
15
+ type FulfillmentState,
16
+ type LabelState,
17
+ type PaymentState,
18
+ type StockState,
19
+ } from "@tot/public-runtime";
20
+ import { fromD1, listAllActiveProducts } from "@/lib/d1/catalog.js";
21
+ import { readD1, readEnv } from "@/lib/env";
22
+ import { evaluateFinalCart, type SeamView } from "@/lib/commerce/finalCartAuthorization.js";
23
+ import { d1CommerceRunner, mirrorOnHand, reserveAllocation } from "@/lib/inventory/allocations.js";
24
+ import { POST as compliancePreflight } from "../compliance/preflight.js";
25
+
26
+ export const prerender = false;
27
+
28
+ const EVIDENCE_STATES = new Set(["fresh", "UNKNOWN", "stale", "unreadable", "absent"]);
29
+ const STAGES = new Set(["preview", "publication"]);
30
+
31
+ function json(body: unknown, status = 200): Response {
32
+ return new Response(JSON.stringify(body), {
33
+ status,
34
+ headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" },
35
+ });
36
+ }
37
+
38
+ function deny(finding: string, status: number, owner?: string): Response {
39
+ return json(owner ? { ok: false, finding, owner } : { ok: false, finding }, status);
40
+ }
41
+
42
+ function statusFor(finding: string): number {
43
+ if (finding === "store-unconfigured" || finding === "caller-unconfigured") return 503;
44
+ if (
45
+ finding === "browser-not-authority" ||
46
+ finding === "tenant-unbound" ||
47
+ finding === "shared-secret" ||
48
+ finding === "signature" ||
49
+ finding === "kid" ||
50
+ finding === "issuer" ||
51
+ finding === "audience" ||
52
+ finding === "expired" ||
53
+ finding === "lifetime" ||
54
+ finding === "jti" ||
55
+ finding === "tenant" ||
56
+ finding === "stage"
57
+ ) {
58
+ return 401;
59
+ }
60
+ return 409;
61
+ }
62
+
63
+ function claimString(payload: JWTPayload, key: string): string {
64
+ const value = payload[key];
65
+ return typeof value === "string" ? value : "";
66
+ }
67
+
68
+ function parseList(value: string | undefined): string[] | null {
69
+ if (!value || value.trim() === "") return null;
70
+ const trimmed = value.trim();
71
+ if (trimmed.startsWith("[")) {
72
+ try {
73
+ const parsed = JSON.parse(trimmed) as unknown;
74
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string" || item.trim() === "")) return null;
75
+ return parsed as string[];
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+ const parts = trimmed.split(",").map((part) => part.trim()).filter(Boolean);
81
+ return parts.length > 0 ? parts : null;
82
+ }
83
+
84
+ function publicJwks(value: string | undefined): JSONWebKeySet | null {
85
+ if (!value) return null;
86
+ try {
87
+ const parsed = JSON.parse(value) as unknown;
88
+ if (!parsed || typeof parsed !== "object" || !Array.isArray((parsed as { keys?: unknown }).keys)) return null;
89
+ const keys = (parsed as { keys: unknown[] }).keys;
90
+ if (keys.length < 1) return null;
91
+ for (const key of keys) {
92
+ if (!key || typeof key !== "object") return null;
93
+ const record = key as Record<string, unknown>;
94
+ if (typeof record.d === "string" || record.kty === "oct" || typeof record.k === "string") return null;
95
+ }
96
+ return parsed as JSONWebKeySet;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ function readCart(value: unknown): FinalCartBinding | null {
103
+ if (!value || typeof value !== "object") return null;
104
+ const record = value as Record<string, unknown>;
105
+ if (typeof record.tenantId !== "string" || typeof record.currency !== "string" || typeof record.destinationDigest !== "string") {
106
+ return null;
107
+ }
108
+ if (typeof record.stage !== "string" || !STAGES.has(record.stage)) return null;
109
+ if (typeof record.verificationExpiresAt !== "string" || typeof record.taxExpiresAt !== "string") return null;
110
+ if (!Array.isArray(record.lines) || record.lines.length > 50) return null;
111
+ const lines = [];
112
+ for (const line of record.lines) {
113
+ if (!line || typeof line !== "object") return null;
114
+ const row = line as Record<string, unknown>;
115
+ if (typeof row.sku !== "string" || typeof row.quantity !== "number" || typeof row.unitMinor !== "number") return null;
116
+ lines.push({ sku: row.sku, quantity: row.quantity, unitMinor: row.unitMinor });
117
+ }
118
+ return {
119
+ tenantId: record.tenantId,
120
+ stage: record.stage as CommerceStage,
121
+ currency: record.currency,
122
+ lines,
123
+ destinationDigest: record.destinationDigest,
124
+ verificationExpiresAt: record.verificationExpiresAt,
125
+ taxExpiresAt: record.taxExpiresAt,
126
+ };
127
+ }
128
+
129
+ function readEvidence(value: unknown): CommerceEvidence | null {
130
+ if (!value || typeof value !== "object") return null;
131
+ const record = value as Record<string, unknown>;
132
+ const evidence = {} as CommerceEvidence;
133
+ for (const field of ["verification", "tax", "risk", "stock"] as const) {
134
+ const state = record[field];
135
+ if (typeof state !== "string" || !EVIDENCE_STATES.has(state)) return null;
136
+ evidence[field] = state as EvidenceState;
137
+ }
138
+ return evidence;
139
+ }
140
+
141
+ function readState<T extends string>(value: unknown, allowed: readonly T[]): T | undefined {
142
+ return typeof value === "string" && (allowed as readonly string[]).includes(value) ? (value as T) : undefined;
143
+ }
144
+
145
+ function destinationOf(value: unknown): { region: string; country: string } | null {
146
+ if (!value || typeof value !== "object") return null;
147
+ const record = value as Record<string, unknown>;
148
+ const region = typeof record.region === "string" ? record.region.trim() : "";
149
+ const country = typeof record.country === "string" ? record.country.trim() : "";
150
+ if (!/^[A-Za-z]{2}$/.test(region)) return null;
151
+ if (country !== "" && !/^[A-Za-z]{2}$/.test(country)) return null;
152
+ return { region: region.toUpperCase(), country: country === "" ? "US" : country.toUpperCase() };
153
+ }
154
+
155
+ async function catalogOffers(sku: string): Promise<boolean> {
156
+ const database = await readD1("CATALOG_DB");
157
+ if (!database) return false;
158
+ try {
159
+ const products = await listAllActiveProducts(fromD1(database));
160
+ for (const product of products) {
161
+ for (const variant of product.variants) {
162
+ if (variant.sku === sku && variant.inventory.available === true) return true;
163
+ }
164
+ }
165
+ } catch {
166
+ return false;
167
+ }
168
+ return false;
169
+ }
170
+
171
+ async function verificationSeam(
172
+ context: APIContext,
173
+ cart: FinalCartBinding,
174
+ destination: { region: string; country: string } | null,
175
+ ): Promise<SeamView> {
176
+ const tot = (context.locals as { tot?: { getProductByHandle: (handle: string) => Promise<unknown> } }).tot ?? {
177
+ getProductByHandle: async () => null,
178
+ };
179
+ const response = await compliancePreflight({
180
+ request: new Request("https://synthetic-tenant.invalid/api/compliance/preflight", {
181
+ method: "POST",
182
+ headers: { "content-type": "application/json" },
183
+ body: JSON.stringify({
184
+ lines: cart.lines.map((line) => ({ handle: line.sku, sku: line.sku, quantity: line.quantity })),
185
+ ...(destination ? { destination: { region: destination.region, country: destination.country } } : {}),
186
+ }),
187
+ }),
188
+ locals: { tenant: context.locals.tenant, tot },
189
+ } as APIContext);
190
+ let body: unknown;
191
+ try {
192
+ body = await response.json();
193
+ } catch {
194
+ return { status: "absent" };
195
+ }
196
+ const decision = body && typeof body === "object" ? (body as { decision?: { status?: unknown; failedClosed?: unknown; blocks?: unknown } }).decision : undefined;
197
+ if (!decision || decision.status === "clear") return { status: decision ? "clear" : "absent" };
198
+ const blocks = Array.isArray(decision.blocks) ? decision.blocks : [];
199
+ const codes = blocks
200
+ .map((block) => (block && typeof block === "object" ? (block as { code?: unknown }).code : undefined))
201
+ .filter((code): code is string => typeof code === "string");
202
+ return { status: "blocked", failedClosed: decision.failedClosed === true, codes };
203
+ }
204
+
205
+ export async function POST(context: APIContext): Promise<Response> {
206
+ const tenant = context.locals.tenant as TenantConfig | undefined;
207
+ const appDomain = tenant?.appDomain ?? "";
208
+ if (!tenant || appDomain === "") return deny("tenant-unbound", 401);
209
+
210
+ let body: Record<string, unknown>;
211
+ try {
212
+ const parsed = await context.request.json();
213
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return deny("malformed-request", 409);
214
+ body = parsed as Record<string, unknown>;
215
+ } catch {
216
+ return deny("malformed-request", 409);
217
+ }
218
+
219
+ if (body.scheme === "shared-secret") {
220
+ const decision = await evaluateFinalCart({
221
+ now: new Date().toISOString(),
222
+ routedTenantId: appDomain,
223
+ quoted: readCart(body.quoted) ?? emptyCart(appDomain),
224
+ presented: readCart(body.presented) ?? emptyCart(appDomain),
225
+ evidence: readEvidence(body.evidence),
226
+ seam: { status: "clear" },
227
+ catalogAvailable: false,
228
+ browserConfirmed: body.browserConfirmed === true,
229
+ token: { scheme: "shared-secret" },
230
+ trustedIssuers: [],
231
+ expectedAudience: "",
232
+ activeKids: [],
233
+ authorities: [],
234
+ });
235
+ return deny(decision.ok ? "shared-secret" : decision.finding, statusFor(decision.ok ? "shared-secret" : decision.finding), decision.ok ? undefined : decision.owner);
236
+ }
237
+
238
+ const quoted = readCart(body.quoted);
239
+ const presented = readCart(body.presented);
240
+ if (!quoted || !presented) return deny("malformed-request", 409);
241
+
242
+ const jwks = publicJwks(await readEnv("SCRAPPY_CALLER_JWKS"));
243
+ const issuers = parseList(await readEnv("SCRAPPY_CALLER_ISSUERS"));
244
+ const audience = await readEnv("SCRAPPY_CALLER_AUDIENCE");
245
+ const activeKids = parseList(await readEnv("SCRAPPY_CALLER_ACTIVE_KIDS"));
246
+ if (!jwks || !issuers || !audience || !activeKids) return deny("caller-unconfigured", 503);
247
+
248
+ const header = context.request.headers.get("authorization") ?? "";
249
+ const bearer = header.startsWith("Bearer ") ? header.slice("Bearer ".length).trim() : "";
250
+ let token: CallerToken | null = null;
251
+ if (bearer !== "") {
252
+ try {
253
+ const decoded = decodeJwt(bearer);
254
+ const protectedHeader = decodeProtectedHeader(bearer);
255
+ let signatureValid = false;
256
+ try {
257
+ await jwtVerify(bearer, createLocalJWKSet(jwks), { algorithms: ["EdDSA"] });
258
+ signatureValid = true;
259
+ } catch (error) {
260
+ const code = error && typeof error === "object" && "code" in error ? String((error as { code: unknown }).code) : "";
261
+ signatureValid = code === "ERR_JWT_EXPIRED" || code === "ERR_JWT_CLAIM_VALIDATION_FAILED";
262
+ }
263
+ const exp = typeof decoded.exp === "number" ? new Date(decoded.exp * 1000).toISOString() : "";
264
+ const audienceClaim = typeof decoded.aud === "string" ? decoded.aud : Array.isArray(decoded.aud) ? decoded.aud[0] ?? "" : "";
265
+ const stageClaim = claimString(decoded, "stage");
266
+ const stage: CommerceStage =
267
+ stageClaim === "preview" || stageClaim === "publication"
268
+ ? stageClaim
269
+ : presented.stage === "preview"
270
+ ? "publication"
271
+ : "preview";
272
+ token = {
273
+ scheme: "asymmetric-jwks",
274
+ signatureValid,
275
+ kid: typeof protectedHeader.kid === "string" ? protectedHeader.kid : "",
276
+ issuer: typeof decoded.iss === "string" ? decoded.iss : "",
277
+ audience: audienceClaim,
278
+ expiresAt: exp,
279
+ jti: claimString(decoded, "jti"),
280
+ tenantId: claimString(decoded, "tenantId"),
281
+ stage,
282
+ };
283
+ } catch {
284
+ token = null;
285
+ }
286
+ }
287
+
288
+ const provider = tenant.inventory?.provider ?? "mirror";
289
+ const authorities: unknown[] = [];
290
+ const now = new Date().toISOString();
291
+ if (provider !== "mirror") {
292
+ authorities.push({ provider, sku: presented.lines[0]?.sku });
293
+ } else {
294
+ const skus = [...new Set(presented.lines.map((line) => line.sku))];
295
+ for (const sku of skus) {
296
+ const onHand = mirrorOnHand(sku, "unspecified");
297
+ authorities.push(
298
+ onHand == null
299
+ ? null
300
+ : { provider: "mirror", readable: true, locationId: "unspecified", sku, onHand, asOf: now },
301
+ );
302
+ }
303
+ }
304
+ let catalogAvailable = false;
305
+ for (const line of presented.lines) {
306
+ if (await catalogOffers(line.sku)) catalogAvailable = true;
307
+ }
308
+
309
+ const decision = await evaluateFinalCart({
310
+ now,
311
+ routedTenantId: appDomain,
312
+ quoted,
313
+ presented,
314
+ evidence: readEvidence(body.evidence),
315
+ seam: await verificationSeam(context, presented, destinationOf(body.destination)),
316
+ catalogAvailable,
317
+ browserConfirmed: body.browserConfirmed === true,
318
+ token,
319
+ trustedIssuers: issuers,
320
+ expectedAudience: audience,
321
+ activeKids,
322
+ authorities,
323
+ payment: readState<PaymentState>(body.payment, ["UNKNOWN", "pending", "authorized", "captured", "capture_unknown", "voided", "refund_requested", "refunded", "refund_unknown"]),
324
+ stock: readState<StockState>(body.stock, ["UNKNOWN", "unallocated", "reserved", "confirmed", "released", "expired"]),
325
+ fulfillment: readState<FulfillmentState>(body.fulfillment, ["UNKNOWN", "held", "ready", "handed_off", "cancelled"]),
326
+ label: readState<LabelState>(body.label, ["UNKNOWN", "none", "intent", "purchased", "printed", "void_requested", "voided"]),
327
+ });
328
+ if (!decision.ok) return deny(decision.finding, statusFor(decision.finding), decision.owner);
329
+
330
+ const database = await readD1("STOREFRONT_APPS_DB");
331
+ if (!database || typeof database.prepare !== "function") return deny("store-unconfigured", 503);
332
+ const reserved = await reserveAllocation(d1CommerceRunner(database), {
333
+ tenantId: decision.tenantId,
334
+ stage: decision.stage,
335
+ orderId: `a${crypto.randomUUID()}`,
336
+ cartDigest: decision.digest,
337
+ expiresAt: decision.expiresAt,
338
+ now,
339
+ currency: presented.currency,
340
+ amountMinor: decision.amountMinor,
341
+ issuer: decision.issuer,
342
+ jti: decision.jti,
343
+ holds: decision.holds,
344
+ });
345
+ if (!reserved.ok) return deny(reserved.finding, statusFor(reserved.finding));
346
+ return json({
347
+ ok: true,
348
+ authorizationId: reserved.authorizationId,
349
+ cartDigest: decision.digest,
350
+ expiresAt: reserved.expiresAt,
351
+ repeated: reserved.repeated,
352
+ stage: decision.stage,
353
+ });
354
+ }
355
+
356
+ function emptyCart(tenantId: string): FinalCartBinding {
357
+ return {
358
+ tenantId,
359
+ stage: "preview",
360
+ currency: "USD",
361
+ lines: [{ sku: "sku-1", quantity: 1, unitMinor: 0 }],
362
+ destinationDigest: "dest-synthetic",
363
+ verificationExpiresAt: "1970-01-01T00:00:00.000Z",
364
+ taxExpiresAt: "1970-01-01T00:00:00.000Z",
365
+ };
366
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.4.50",
3
+ "version": "2.4.51",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",