@tokenoftrust/storefront-runner 3.10.2 → 3.10.3

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.
@@ -2,6 +2,8 @@
2
2
  * Final-cart authorization decision. Payment is admitted only when the caller
3
3
  * token, the bound cart, fresh evidence, the verification seam, and mirror stock
4
4
  * all agree. Catalog availability and a browser confirmation are not authority.
5
+ * A tenant that declares no inventory provider does not track stock: its stock
6
+ * evidence is `not-required` and its holds carry no balance.
5
7
  */
6
8
  import {
7
9
  admitCallerToken,
@@ -43,6 +45,11 @@ export interface FinalCartRequest {
43
45
  trustedIssuers: readonly string[];
44
46
  expectedAudience: string;
45
47
  activeKids: readonly string[];
48
+ /**
49
+ * False only when the tenant declares no inventory provider; `authorities` is read otherwise.
50
+ * Anything but an explicit `false` counts as tracked, so a caller that omits it fails closed.
51
+ */
52
+ stockTracked: boolean;
46
53
  authorities: readonly unknown[];
47
54
  payment?: PaymentState;
48
55
  stock?: StockState;
@@ -90,11 +97,11 @@ function cartFinding(quoted: FinalCartBinding, presented: FinalCartBinding, rout
90
97
  return null;
91
98
  }
92
99
 
93
- function evidenceFinding(evidence: CommerceEvidence | null): string | null {
100
+ function evidenceFinding(evidence: CommerceEvidence | null, stockTracked: boolean): string | null {
94
101
  if (!evidence) return "missing-evidence";
95
102
  for (const field of EVIDENCE_FIELDS) {
96
103
  const state: EvidenceState | string = evidence[field];
97
- if (state === "fresh" || (field !== "stock" && state === "not-required")) continue;
104
+ if (state === "fresh" || (state === "not-required" && (field !== "stock" || !stockTracked))) continue;
98
105
  if (state === "UNKNOWN") return `evidence-${field}-unknown`;
99
106
  if (state === "stale" || state === "unreadable" || state === "absent") return `evidence-${field}-${state}`;
100
107
  return `evidence-${field}-unreadable`;
@@ -157,7 +164,8 @@ export async function evaluateFinalCart(input: FinalCartRequest): Promise<FinalC
157
164
 
158
165
  if (input.presented.lines.length < 1) return { ok: false, finding: "malformed-request" };
159
166
 
160
- const evidence = evidenceFinding(input.evidence);
167
+ const stockTracked = input.stockTracked !== false;
168
+ const evidence = evidenceFinding(input.evidence, stockTracked);
161
169
  if (evidence) return { ok: false, finding: evidence };
162
170
 
163
171
  const expiresAt = earlierExpiry(input.presented.verificationExpiresAt, input.presented.taxExpiresAt);
@@ -205,6 +213,10 @@ export async function evaluateFinalCart(input: FinalCartRequest): Promise<FinalC
205
213
  const holds: ReserveHold[] = [];
206
214
  for (const sku of [...quantities.keys()].sort()) {
207
215
  const quantity = quantities.get(sku)!;
216
+ if (!stockTracked) {
217
+ holds.push({ sku, locationId: "unspecified", quantity, onHand: 0 });
218
+ continue;
219
+ }
208
220
  const named =
209
221
  input.authorities.find((value) => {
210
222
  if (!value || typeof value !== "object") return false;
@@ -88,6 +88,11 @@ export interface ReserveInput {
88
88
  admissionStatement?: SqlStatement;
89
89
  /** Immutable provider identity, claimed atomically with its receipt and holds. */
90
90
  providerCart?: { provider: "foxycart"; id: string; digest: string; expectedAuthorizationId?: string };
91
+ /**
92
+ * The tenant declares no inventory provider: each hold records the order's units so payment
93
+ * confirmation and release work unchanged, but no balance bounds it.
94
+ */
95
+ stockUntracked?: boolean;
91
96
  }
92
97
 
93
98
  export type ReserveResult =
@@ -255,17 +260,28 @@ function reserveShape(input: ReserveInput): ReserveResult | null {
255
260
  }
256
261
 
257
262
  function allocationInsert(input: ReserveInput, hold: ReserveHold, seenAt: string): SqlStatement {
258
- return {
259
- sql: `INSERT INTO scrappy_allocations (tenant_id, stage, order_id, sku, location_id, quantity, state)
260
- SELECT ?, ?, ?, ?, ?, ?, 'reserved'
261
- WHERE (
263
+ const bound = input.stockUntracked ? "" : `(
262
264
  SELECT COALESCE(SUM(quantity), 0) FROM scrappy_allocations
263
265
  WHERE tenant_id = ? AND stage = ? AND sku = ? AND location_id = ? AND state IN ('reserved', 'confirmed')
264
266
  ) + ? <= ${input.admissionStatement
265
267
  ? `(SELECT COALESCE(SUM(quantity_delta), 0) FROM commerce_inventory_movements
266
268
  WHERE tenant_id = ? AND stage = ? AND sku = ? AND location_id = ?)`
267
269
  : "?"}
268
- AND NOT EXISTS (
270
+ AND `;
271
+ const boundParams = input.stockUntracked ? [] : [
272
+ input.tenantId,
273
+ input.stage,
274
+ hold.sku,
275
+ hold.locationId,
276
+ hold.quantity,
277
+ ...(input.admissionStatement
278
+ ? [input.tenantId, input.stage, hold.sku, hold.locationId]
279
+ : [hold.onHand]),
280
+ ];
281
+ return {
282
+ sql: `INSERT INTO scrappy_allocations (tenant_id, stage, order_id, sku, location_id, quantity, state)
283
+ SELECT ?, ?, ?, ?, ?, ?, 'reserved'
284
+ WHERE ${bound}NOT EXISTS (
269
285
  SELECT 1 FROM scrappy_allocations
270
286
  WHERE tenant_id = ? AND stage = ? AND order_id = ? AND sku = ? AND location_id = ?
271
287
  AND state IN ('reserved', 'confirmed')
@@ -292,14 +308,7 @@ function allocationInsert(input: ReserveInput, hold: ReserveHold, seenAt: string
292
308
  hold.sku,
293
309
  hold.locationId,
294
310
  hold.quantity,
295
- input.tenantId,
296
- input.stage,
297
- hold.sku,
298
- hold.locationId,
299
- hold.quantity,
300
- ...(input.admissionStatement
301
- ? [input.tenantId, input.stage, hold.sku, hold.locationId]
302
- : [hold.onHand]),
311
+ ...boundParams,
303
312
  input.tenantId,
304
313
  input.stage,
305
314
  input.orderId,
@@ -233,12 +233,17 @@ async function authorize(context: APIContext): Promise<Response> {
233
233
  const requirements = await readCheckoutRequirements(tenant.tenant_id, contentEnv);
234
234
  if (!requirements) return deny("checkout-requirements-unreadable", 503);
235
235
  const policy = testStoreEvidencePolicy(requirements);
236
+ // Stock is tracked only for a tenant that declares the `mirror` provider. An undeclared tenant
237
+ // has no balance to read, so its stock evidence is honestly `not-required`, not a refusal.
238
+ const stockTracked = tenant.inventory?.provider === "mirror";
236
239
  const authorities = [];
237
- for (const sku of new Set(snapshot.lines.map(line => line.sku))) {
238
- authorities.push(await readMirrorStock(queryable, { tenantId: snapshot.tenantId, stage: snapshot.stage, sku, locationId: "unspecified", now, maxAgeMs: 15 * 60_000 }));
240
+ if (stockTracked) {
241
+ for (const sku of new Set(snapshot.lines.map(line => line.sku))) {
242
+ authorities.push(await readMirrorStock(queryable, { tenantId: snapshot.tenantId, stage: snapshot.stage, sku, locationId: "unspecified", now, maxAgeMs: 15 * 60_000 }));
243
+ }
239
244
  }
240
245
  const stockExpiry = Math.min(...authorities.flatMap(authority => authority ? [Date.parse(authority.asOf) + 15 * 60_000] : []));
241
- const evidence: CommerceEvidence = { verification: "absent", tax: "absent", risk: "absent", stock: "fresh" };
246
+ const evidence: CommerceEvidence = { verification: "absent", tax: "absent", risk: "absent", stock: stockTracked ? "fresh" : "not-required" };
242
247
  const expiresAt = existing?.expiresAt ?? new Date(Math.min(Date.now() + 5 * 60_000, stockExpiry)).toISOString();
243
248
  const provenance = {} as CommerceEvidenceProvenance;
244
249
  for (const field of ["verification", "tax", "risk"] as const) {
@@ -247,7 +252,9 @@ async function authorize(context: APIContext): Promise<Response> {
247
252
  provenance[field] = { requirement: "not-required", state: "not-required", source: SHARED_STORE_POLICY_SOURCE, policyRevision: SHARED_STORE_POLICY_REVISION, expiresAt };
248
253
  }
249
254
  }
250
- provenance.stock = { requirement: "required", state: "satisfied", source: "tenant-inventory-movements", policyRevision: "mirror-v1", expiresAt };
255
+ provenance.stock = stockTracked
256
+ ? { requirement: "required", state: "satisfied", source: "tenant-inventory-movements", policyRevision: "mirror-v1", expiresAt }
257
+ : { requirement: "not-required", state: "not-required", source: "tenant-inventory-undeclared", policyRevision: "untracked-v1", expiresAt };
251
258
  const presented: FinalCartBinding = { tenantId: snapshot.tenantId, stage: snapshot.stage, currency: snapshot.currency, lines: snapshot.lines,
252
259
  destinationDigest: snapshot.destinationDigest, verificationExpiresAt: expiresAt, taxExpiresAt: expiresAt };
253
260
  const seamView = await seam(authorityContext, snapshot, selected, policy);
@@ -257,7 +264,7 @@ async function authorize(context: APIContext): Promise<Response> {
257
264
  }
258
265
  const decision = await evaluateFinalCart({ now, routedTenantId: commerceTenantId(tenant), quoted: presented, presented, evidence,
259
266
  seam: seamView, catalogAvailable: true, browserConfirmed: false, token,
260
- trustedIssuers: issuers, expectedAudience: audience, activeKids, authorities });
267
+ trustedIssuers: issuers, expectedAudience: audience, activeKids, stockTracked, authorities });
261
268
  if (!decision.ok) {
262
269
  if (decision.finding === "verification-refused:flavored_prohibited" && seamView.flavors) {
263
270
  return json({ ok: false, finding: decision.finding, flavors: seamView.flavors }, 409);
@@ -269,7 +276,7 @@ async function authorize(context: APIContext): Promise<Response> {
269
276
  const cartDigest = existing?.cartDigest ?? await digest(`commerce-provider-cart-v1\n${snapshot.providerCartId}\n${snapshot.providerCartDigest}`);
270
277
  const reserved = await reserveAllocation(d1CommerceRunner(database), { tenantId: decision.tenantId, stage: decision.stage, orderId: authorizationId,
271
278
  cartDigest, expiresAt: decision.expiresAt, now, currency: snapshot.currency, amountMinor: snapshot.totals.totalMinor,
272
- issuer: decision.issuer, jti: decision.jti, holds: decision.holds,
279
+ issuer: decision.issuer, jti: decision.jti, holds: decision.holds, stockUntracked: !stockTracked,
273
280
  providerCart: { provider: snapshot.provider, id: snapshot.providerCartId, digest: snapshot.providerCartDigest, expectedAuthorizationId: snapshot.expectedAuthorizationId },
274
281
  admissionStatement: admissionSnapshotStatement({ ...snapshot, authorizationId, cartDigest, evidence, provenance, expiresAt: decision.expiresAt, now }),
275
282
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "3.10.2",
3
+ "version": "3.10.3",
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",
@@ -401,7 +401,7 @@ export type CallerTokenRefusal =
401
401
  | "stage";
402
402
 
403
403
  export function evidencePermitsRelease(evidence: CommerceEvidence): boolean {
404
- return evidence.stock === "fresh" && (["verification", "tax", "risk"] as const)
404
+ return (["verification", "tax", "risk", "stock"] as const)
405
405
  .every(field => evidence[field] === "fresh" || evidence[field] === "not-required");
406
406
  }
407
407