@tokenoftrust/storefront-runner 2.4.59 → 2.4.60

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.
@@ -111,7 +111,7 @@ export async function toContractOrder(order: FoxyOrder, ctx: ToContractOrderCont
111
111
  export const COMMERCE_COMMANDS = ["observe", "capture", "refund", "cancel"] as const;
112
112
  export type CommerceCommandName = (typeof COMMERCE_COMMANDS)[number];
113
113
 
114
- const PRIVATE_COMMERCE_KEYS = new Set([
114
+ export const PRIVATE_COMMERCE_KEYS = new Set([
115
115
  "customer",
116
116
  "customerEmail",
117
117
  "customerName",
@@ -237,7 +237,7 @@ export interface CommitCommerceInput {
237
237
 
238
238
  export type CommitCommerceResult =
239
239
  | { ok: true; duplicate: boolean; snapshot: CommerceSnapshot; outboxId: string }
240
- | { ok: false; reason: "replay" };
240
+ | { ok: false; reason: "replay" | "conflict" };
241
241
 
242
242
  async function atomic(db: Queryable, statements: readonly QueryStatement[]): Promise<void> {
243
243
  if (typeof db.batch === "function") {
@@ -277,8 +277,9 @@ export class ScrappyOrderLedger {
277
277
  if (seen) return { ok: false, reason: "replay" };
278
278
  }
279
279
 
280
- const existing = await this.db.first<{ outbox_id: string; payload: string }>(
281
- `SELECT outbox_id, payload FROM scrappy_commerce_outbox
280
+ const digest = await sha256Hex(canonicalJson(event));
281
+ const existing = await this.db.first<{ outbox_id: string }>(
282
+ `SELECT outbox_id FROM scrappy_commerce_outbox
282
283
  WHERE tenant_id = ? AND stage = ? AND source = ? AND source_event_id = ?`,
283
284
  tenantId,
284
285
  stage,
@@ -286,6 +287,8 @@ export class ScrappyOrderLedger {
286
287
  event.sourceEventId,
287
288
  );
288
289
  if (existing) {
290
+ const storedDigest = await this.storedPayloadDigest(tenantId, stage, event.source, event.sourceEventId);
291
+ if (storedDigest !== null && storedDigest !== digest) return { ok: false, reason: "conflict" };
289
292
  if (input.replay) {
290
293
  try {
291
294
  await this.insertReplay(input);
@@ -313,7 +316,6 @@ export class ScrappyOrderLedger {
313
316
  const packed = packCommercePayload(event, snapshot);
314
317
  const key = idempotencyKey(tenantId, stage, event.source, event.sourceEventId);
315
318
  const outboxId = (await sha256Hex(key)).slice(0, 32);
316
- const digest = await sha256Hex(canonicalJson(event));
317
319
  const statements: QueryStatement[] = [];
318
320
  if (input.replay) {
319
321
  statements.push({
@@ -398,6 +400,8 @@ export class ScrappyOrderLedger {
398
400
  event.sourceEventId,
399
401
  );
400
402
  if (raced) {
403
+ const storedDigest = await this.storedPayloadDigest(tenantId, stage, event.source, event.sourceEventId);
404
+ if (storedDigest !== null && storedDigest !== digest) return { ok: false, reason: "conflict" };
401
405
  const snapshotNow = await this.readSnapshot(tenantId, stage, event.orderId, event);
402
406
  return { ok: true, duplicate: true, snapshot: snapshotNow, outboxId: raced.outbox_id };
403
407
  }
@@ -479,6 +483,23 @@ export class ScrappyOrderLedger {
479
483
  );
480
484
  }
481
485
 
486
+ private async storedPayloadDigest(
487
+ tenantId: string,
488
+ stage: CommerceStage,
489
+ source: string,
490
+ sourceEventId: string,
491
+ ): Promise<string | null> {
492
+ const row = await this.db.first<{ payload_digest: string }>(
493
+ `SELECT payload_digest FROM scrappy_commerce_inbox
494
+ WHERE tenant_id = ? AND stage = ? AND source = ? AND source_event_id = ?`,
495
+ tenantId,
496
+ stage,
497
+ source,
498
+ sourceEventId,
499
+ );
500
+ return row?.payload_digest ?? null;
501
+ }
502
+
482
503
  private async insertReplay(input: CommitCommerceInput): Promise<void> {
483
504
  if (!input.replay) return;
484
505
  await atomic(this.db, [
@@ -29,6 +29,7 @@ import { computeBackoffDelaySeconds, MAX_DELIVERY_ATTEMPTS, type AttemptOutcome
29
29
  import { validateWebhookEndpoint } from "./endpointGuard.js";
30
30
  import { COMMERCE_SCHEDULER } from "@tot/public-runtime";
31
31
  import type { Queryable } from "../d1/catalog.js";
32
+ import { PRIVATE_COMMERCE_KEYS } from "../apps/orders/orderMapper.js";
32
33
 
33
34
  /** Bounded per-attempt timeout — a hung receiver must not hold a dispatcher slot open indefinitely. */
34
35
  const DEFAULT_TIMEOUT_MS = 10_000;
@@ -280,28 +281,11 @@ async function postWithTimeout(
280
281
  }
281
282
  }
282
283
 
283
- const COMMERCE_PRIVATE_KEYS = new Set([
284
- "customer",
285
- "customerEmail",
286
- "customerName",
287
- "email",
288
- "phone",
289
- "line1",
290
- "line2",
291
- "postalCode",
292
- "shippingAddress",
293
- "billingAddress",
294
- "address",
295
- "firstName",
296
- "lastName",
297
- "name",
298
- ]);
299
-
300
284
  function commercePayloadHasPrivateIdentity(value: unknown): boolean {
301
285
  if (value === null || typeof value !== "object") return false;
302
286
  if (Array.isArray(value)) return value.some((item) => commercePayloadHasPrivateIdentity(item));
303
287
  return Object.entries(value as Record<string, unknown>).some(
304
- ([key, child]) => COMMERCE_PRIVATE_KEYS.has(key) || commercePayloadHasPrivateIdentity(child),
288
+ ([key, child]) => PRIVATE_COMMERCE_KEYS.has(key) || commercePayloadHasPrivateIdentity(child),
305
289
  );
306
290
  }
307
291
 
@@ -60,8 +60,6 @@ export class CommerceAlarmDispatcher {
60
60
  private readonly scheduler: string,
61
61
  ) {}
62
62
 
63
- async enqueue(): Promise<void> {}
64
-
65
63
  sweep(now: string, deliver?: CommerceDeliver): Promise<CommerceSweepResult> {
66
64
  return sweepDurableCommerce({ db: this.db, scheduler: this.scheduler, now, deliver });
67
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.4.59",
3
+ "version": "2.4.60",
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",