@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
|
|
281
|
-
|
|
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]) =>
|
|
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.
|
|
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",
|