@tokenoftrust/storefront-runner 2.4.59 → 2.4.61

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
  }
@@ -21,8 +21,7 @@ import ReviewSummary from "@/components/commerce/ReviewSummary.astro";
21
21
  import ReviewList from "@/components/commerce/ReviewList.astro";
22
22
  import { isOnSale, isNew, isAvailable, fromPrice } from "@/lib/tot/query";
23
23
  import {
24
- buildVariantCartPayload,
25
- priceForPurchaseOption,
24
+ buildMemberSignedCartPayload,
26
25
  workerCartApiPath,
27
26
  normalizePurchaseOptions,
28
27
  visiblePurchaseOptions,
@@ -243,18 +242,13 @@ if (commerce) {
243
242
  async (v) => {
244
243
  const payloads = await Promise.all(
245
244
  purchaseOptions.map(async (purchaseOption) => {
246
- const effective = priceForPurchaseOption(
247
- v.price,
248
- purchaseOption,
249
- pricingCtx,
250
- );
251
245
  return [
252
246
  purchaseOption.id,
253
- await buildVariantCartPayload(product, v, {
247
+ await buildMemberSignedCartPayload(product, v, {
254
248
  mode: commerce.mode,
255
249
  secret: signing.secret,
256
250
  purchaseOption,
257
- signedPrice: effective.price,
251
+ pricing: pricingCtx,
258
252
  autopilotEnabled,
259
253
  tenantId: tenant.tenant_id,
260
254
  }),
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.61",
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",
@@ -276,6 +276,8 @@ export interface MemberSignedCartPayloadOptions {
276
276
  * is off. Callers should still pre-filter with {@link visiblePurchaseOptions}.
277
277
  */
278
278
  autopilotEnabled: boolean;
279
+ /** Tenant id written onto the signed `tenant` field when present. */
280
+ tenantId?: string;
279
281
  }
280
282
 
281
283
  /**
@@ -309,6 +311,7 @@ export async function buildMemberSignedCartPayload(
309
311
  purchaseOption: opts.purchaseOption,
310
312
  signedPrice: effective.price,
311
313
  autopilotEnabled: opts.autopilotEnabled,
314
+ tenantId: opts.tenantId,
312
315
  });
313
316
  }
314
317