@tokenoftrust/storefront-runner 3.10.3 → 3.10.4

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,23 @@
1
+ -- Source authority is independent of business event arrival and fulfillment progress.
2
+ CREATE TABLE commerce_source_authority (
3
+ tenant_id TEXT NOT NULL, stage TEXT NOT NULL CHECK(stage IN ('preview','publication')),
4
+ source TEXT NOT NULL, provider_store_id TEXT NOT NULL, source_order_id TEXT NOT NULL,
5
+ order_id TEXT, source_state_version INTEGER NOT NULL, check_revision INTEGER NOT NULL,
6
+ active_check_id TEXT, pending_check INTEGER NOT NULL DEFAULT 1,
7
+ accepted_ledger_generation TEXT, accepted_business_event_set_digest TEXT,
8
+ source_proof_expires_at TEXT, source_sync_pending INTEGER NOT NULL DEFAULT 1,
9
+ unresolved_count INTEGER NOT NULL DEFAULT 0, authority_budget INTEGER NOT NULL DEFAULT 0,
10
+ binding_digest TEXT NOT NULL, state_json TEXT NOT NULL, mutation_id TEXT NOT NULL,
11
+ PRIMARY KEY(tenant_id,stage,source,provider_store_id,source_order_id),
12
+ UNIQUE(tenant_id,stage,source,source_order_id), UNIQUE(tenant_id,stage,order_id)
13
+ );
14
+ -- Immutable issued checks, results and hold openings survive retries and process restarts.
15
+ CREATE TABLE commerce_source_observations (
16
+ tenant_id TEXT NOT NULL, stage TEXT NOT NULL, source TEXT NOT NULL,
17
+ provider_store_id TEXT NOT NULL, source_order_id TEXT NOT NULL,
18
+ record_kind TEXT NOT NULL CHECK(record_kind IN ('check','result','hold','manifest')),
19
+ record_id TEXT NOT NULL, payload_digest TEXT NOT NULL, payload_json TEXT NOT NULL,
20
+ resolved_version INTEGER, resolved_generation TEXT,
21
+ PRIMARY KEY(tenant_id,stage,source,provider_store_id,source_order_id,record_kind,record_id)
22
+ );
23
+ CREATE INDEX commerce_source_observations_scope ON commerce_source_observations(tenant_id,stage,source,source_order_id,record_kind);
@@ -11,6 +11,7 @@ export async function recoverTenantCommerce(
11
11
  now: string,
12
12
  deployedSha: string,
13
13
  ): Promise<number | null> {
14
+ let incomplete=false;
14
15
  for (const stage of ["preview", "publication"] as const) {
15
16
  const runId = crypto.randomUUID();
16
17
  await db.run(
@@ -37,16 +38,17 @@ export async function recoverTenantCommerce(
37
38
  });
38
39
  },
39
40
  });
40
- if (!result.ok) throw new Error("scheduler-unavailable");
41
+ if(!result.ok)incomplete=true;
41
42
  await db.run(
42
43
  `UPDATE commerce_recovery_runs SET completed_at=?,delivered=?,retried=?,dead=? WHERE run_id=?`,
43
- now,
44
- result.delivered,
45
- result.retried,
46
- result.dead,
44
+ result.ok?now:null,
45
+ result.delivered ?? 0,
46
+ result.retried ?? 0,
47
+ result.dead ?? 0,
47
48
  runId,
48
49
  );
49
50
  }
51
+ if(incomplete)throw new Error("commerce-recovery-incomplete");
50
52
  const due = await db.first<{ available_at: string }>(
51
53
  `SELECT MIN(available_at) available_at FROM scrappy_commerce_outbox WHERE tenant_id=? AND state='pending'`,
52
54
  tenantId,
@@ -6,6 +6,7 @@
6
6
  * read the admin fulfillment mock. Label purchase stays an intent row for a
7
7
  * separate adapter. UI fields are not source truth.
8
8
  */
9
+ import { authorityHolds, authorityFence, type AuthorityRow, type SourceReadiness } from "../orders/data/sourceAuthorityStore.js";
9
10
  import type { APIContext } from "astro";
10
11
  import {
11
12
  COMPLIANCE_STATES,
@@ -149,6 +150,8 @@ export type FulfillmentResult<T> =
149
150
  | { ok: false; code: string; holds?: readonly ReleaseHold[] };
150
151
 
151
152
  interface LoadedOrder {
153
+ sourceAuthority: AuthorityRow|null;
154
+ sourceHolds: SourceReadiness[];
152
155
  order: {
153
156
  tenantId: string;
154
157
  stage: CommerceStage;
@@ -185,7 +188,7 @@ interface LoadedOrder {
185
188
  type CommitPlan =
186
189
  | { type: "done"; view: FulfillmentView }
187
190
  | { type: "refuse"; code: string; holds?: readonly ReleaseHold[] }
188
- | { type: "write"; statements: SqlStatement[]; minimumChanges: number };
191
+ | { type: "write"; statements: SqlStatement[]; minimumChanges: number; sourceAdvance?:boolean };
189
192
 
190
193
  const FAIL_CLOSED_EVIDENCE: CommerceEvidence = {
191
194
  verification: "unreadable",
@@ -655,8 +658,10 @@ export class FulfillmentService {
655
658
  return this.batch(actor, [...batches][0] ?? "");
656
659
  }
657
660
  if (batches.size > 0) return refuse("conflict");
661
+ const sourceBlocked=states.find(s=>s.sourceHolds.length);
662
+ if(sourceBlocked)return refuse(sourceBlocked.sourceHolds[0]!,sourceBlocked.sourceHolds);
658
663
  const batchId = `batch_${crypto.randomUUID()}`;
659
- const statements: SqlStatement[] = [claimUpdate(states)];
664
+ const statements: SqlStatement[] = [claimUpdate(states,this.now())];
660
665
  states.forEach((state, index) => {
661
666
  statements.push(
662
667
  auditStatement(
@@ -670,7 +675,7 @@ export class FulfillmentService {
670
675
  ),
671
676
  );
672
677
  });
673
- const changes = await this.write(statements);
678
+ const changes = await this.write(statements,states.length);
674
679
  if (!changes) return refuse("unavailable");
675
680
  if ((changes[0] ?? 0) === states.length) return this.batch(actor, batchId);
676
681
  if (expectedVersions) return refuse("conflict");
@@ -823,10 +828,12 @@ export class FulfillmentService {
823
828
  if (state.shipment.labelPurchaseState === mapped.shipment && state.labelOutcome?.adultSignature === input.adultSignature) {
824
829
  return { type: "done", view: state.view };
825
830
  }
831
+ if(input.outcome==="purchased"&&state.sourceHolds.length)return {type:"refuse",code:state.sourceHolds[0]!,holds:state.sourceHolds};
826
832
  const version = state.order.eventVersion + 1;
827
833
  return {
828
834
  type: "write",
829
835
  minimumChanges: 1,
836
+ sourceAdvance:input.outcome==="purchased",
830
837
  statements: [
831
838
  {
832
839
  sql: `UPDATE scrappy_commerce_orders
@@ -906,6 +913,7 @@ export class FulfillmentService {
906
913
  return {
907
914
  type: "write",
908
915
  minimumChanges: 1,
916
+ sourceAdvance:true,
909
917
  statements: [
910
918
  {
911
919
  sql: `UPDATE scrappy_commerce_orders
@@ -967,6 +975,7 @@ export class FulfillmentService {
967
975
 
968
976
  private readiness(state: LoadedOrder): ReadinessInput {
969
977
  return {
978
+ sourceHolds: state.sourceHolds,
970
979
  payment: state.order.payment,
971
980
  stock: state.order.stock,
972
981
  fulfillment: state.order.fulfillment,
@@ -1032,7 +1041,7 @@ export class FulfillmentService {
1032
1041
  });
1033
1042
  statements.push(...intent);
1034
1043
  }
1035
- return { type: "write", statements, minimumChanges: 1 };
1044
+ return { type: "write", statements, minimumChanges: 1, sourceAdvance:true };
1036
1045
  });
1037
1046
  }
1038
1047
 
@@ -1099,7 +1108,11 @@ export class FulfillmentService {
1099
1108
  if (plan.type === "done") return ok(plan.view);
1100
1109
  if (plan.type === "refuse") return refuse(plan.code, plan.holds);
1101
1110
  if (expectedVersion !== undefined && state.order.eventVersion !== expectedVersion) return refuse("conflict");
1102
- const changes = await this.write(plan.statements);
1111
+ const fence=authorityFence(state.sourceAuthority,this.now(),plan.sourceAdvance===true);
1112
+ const first=plan.statements[0];
1113
+ if(!first)return refuse("unavailable");
1114
+ const guarded=[{sql:`${first.sql} AND (${fence.sql})`,params:[...first.params,...fence.params]},...plan.statements.slice(1)];
1115
+ const changes = await this.write(guarded);
1103
1116
  if (!changes) return refuse("unavailable");
1104
1117
  if ((changes[0] ?? 0) >= plan.minimumChanges) {
1105
1118
  const next = await this.load(scope.tenantId, scope.stage, orderId);
@@ -1110,10 +1123,17 @@ export class FulfillmentService {
1110
1123
  return refuse("conflict");
1111
1124
  }
1112
1125
 
1113
- private async write(statements: readonly SqlStatement[]): Promise<number[] | null> {
1126
+ private async write(statements: readonly SqlStatement[],minimumChanges=1): Promise<number[] | null> {
1114
1127
  try {
1115
- return await this.db.batch(statements);
1116
- } catch {
1128
+ if(!statements.length)return [];
1129
+ // A competing transaction can occupy the expected next event version. Abort this
1130
+ // entire batch unless its first CAS actually wrote; version-only side effects are unsafe.
1131
+ const gated=[statements[0]!,{sql:`SELECT json(CASE WHEN changes() >= ? THEN 'true' ELSE 'cas-not-applied' END)`,params:[minimumChanges]},...statements.slice(1)];
1132
+ const changes=await this.db.batch(gated);
1133
+ return [changes[0] ?? 0,...changes.slice(2)];
1134
+ } catch(error) {
1135
+ let cause:unknown=error;
1136
+ for(let depth=0;depth<4&&cause instanceof Error;depth++,cause=cause.cause){if(cause.message.includes("malformed JSON"))return statements.map(()=>0);}
1117
1137
  return null;
1118
1138
  }
1119
1139
  }
@@ -1222,7 +1242,7 @@ export class FulfillmentService {
1222
1242
  );
1223
1243
  if (snapshot.sourceVersion !== undefined) statements.push(projectionCheckpoint(snapshot,version,this.now()));
1224
1244
  const changes = await this.write(statements);
1225
- if (!changes) return refuse("unavailable");
1245
+ if (!changes) {const existing=await this.load(snapshot.tenantId,snapshot.stage,snapshot.orderId);return existing?"exists":refuse("unavailable");}
1226
1246
  if ((changes[0] ?? 0) !== 1) return "exists";
1227
1247
  const loaded = await this.load(snapshot.tenantId, snapshot.stage, snapshot.orderId);
1228
1248
  return loaded ? ok(loaded.view) : refuse("unavailable");
@@ -1437,7 +1457,12 @@ export class FulfillmentService {
1437
1457
  amountMinor: Number(row.amount_minor),
1438
1458
  eventVersion: Number(row.event_version),
1439
1459
  };
1460
+ let sourceAuthority:AuthorityRow|null=null;let sourceHolds:SourceReadiness[];
1461
+ try { sourceAuthority=await this.db.get<AuthorityRow & Record<string,unknown>>(`SELECT * FROM commerce_source_authority WHERE tenant_id=? AND stage=? AND source=? AND source_order_id=?`,[tenantId,stage,order.source,order.sourceOrderId]) ?? null;
1462
+ sourceHolds=sourceAuthority?.order_id!==order.orderId?["source-authority-unavailable"]:authorityHolds(sourceAuthority,this.now());
1463
+ }catch{sourceHolds=["source-authority-unreadable"];}
1440
1464
  const loaded: LoadedOrder = {
1465
+ sourceAuthority,sourceHolds,
1441
1466
  order,
1442
1467
  evidence,
1443
1468
  lines,
@@ -1477,6 +1502,7 @@ export class FulfillmentService {
1477
1502
  const readiness = this.readiness(loaded);
1478
1503
  const holds = releaseHolds(readiness);
1479
1504
  loaded.view.holds = holds;
1505
+ if(sourceHolds.length&&order.fulfillment==="ready")loaded.view.fulfillment="held";
1480
1506
  loaded.view.canRelease = canRelease(readiness) && !loaded.shipment;
1481
1507
  loaded.view.canHandOff = handoffRefusal(readiness) === null && loaded.shipment?.handoffState !== "handed_off";
1482
1508
  loaded.view.shipment = loaded.shipment
@@ -1517,11 +1543,12 @@ function touchOrder(state: LoadedOrder, wantIntent: boolean): SqlStatement {
1517
1543
  };
1518
1544
  }
1519
1545
 
1520
- function claimUpdate(states: readonly LoadedOrder[]): SqlStatement {
1546
+ function claimUpdate(states: readonly LoadedOrder[],now:string): SqlStatement {
1521
1547
  const first = states[0];
1522
1548
  if (!first) return { sql: "SELECT 0", params: [] };
1523
- const match = states.map(() => "(order_id = ? AND event_version = ?)").join(" OR ");
1524
- const versionParams = states.flatMap((state) => [state.order.orderId, state.order.eventVersion]);
1549
+ const fences=states.map(state=>authorityFence(state.sourceAuthority,now,true));
1550
+ const match = states.map((_,i) => `(order_id = ? AND event_version = ? AND (${fences[i]!.sql}))`).join(" OR ");
1551
+ const versionParams = states.flatMap((state,i) => [state.order.orderId, state.order.eventVersion,...fences[i]!.params]);
1525
1552
  return {
1526
1553
  sql: `UPDATE scrappy_commerce_orders
1527
1554
  SET event_version = event_version + 1
@@ -17,9 +17,15 @@ import {
17
17
  type ProviderOutcome,
18
18
  type StockState,
19
19
  } from "@tot/public-runtime";
20
+ import type { SourceReadiness } from "../orders/data/sourceAuthorityStore.js";
20
21
  import { COMPLIANCE_RULE_PROFILES } from "@/config/compliance/rulesets";
21
22
 
22
23
  export const RELEASE_HOLDS = [
24
+ "source-check-pending",
25
+ "source-ledger-conflict",
26
+ "source-authority-unreadable",
27
+ "source-authority-unavailable",
28
+ "source-sync-pending",
23
29
  "evidence-unknown",
24
30
  "evidence-stale",
25
31
  "evidence-unreadable",
@@ -48,6 +54,7 @@ export interface CarrierPolicy {
48
54
  }
49
55
 
50
56
  export interface ReadinessInput {
57
+ sourceHolds?: readonly SourceReadiness[];
51
58
  payment: PaymentState;
52
59
  stock: StockState;
53
60
  fulfillment: FulfillmentState;
@@ -75,7 +82,7 @@ function add(holds: ReleaseHold[], hold: ReleaseHold | null): void {
75
82
 
76
83
  /** Holds that forbid creating a shipment intent. Compliance is not an input. */
77
84
  export function releaseHolds(input: ReadinessInput): ReleaseHold[] {
78
- const holds: ReleaseHold[] = [];
85
+ const holds: ReleaseHold[] = [...(input.sourceHolds ?? [])];
79
86
  for (const field of ["verification", "tax", "risk", "stock"] as const) {
80
87
  const value = input.evidence[field];
81
88
  add(holds, value in EVIDENCE_HOLDS ? EVIDENCE_HOLDS[value as EvidenceState] : "evidence-unreadable");
@@ -326,7 +326,7 @@ function publicCommercePayload(payload: string): Record<string, unknown> | null
326
326
 
327
327
  export type CommerceSweepResult =
328
328
  | { ok: true; delivered: number; retried: number; dead: number }
329
- | { ok: false; reason: "scheduler" };
329
+ | { ok: false; reason: "scheduler" | "storage"; delivered?:number; retried?:number; dead?:number; incomplete?:number };
330
330
 
331
331
  export type CommerceDeliver = (event: Record<string, unknown>, row: CommerceOutboxDue) => Promise<void>;
332
332
 
@@ -344,10 +344,13 @@ export async function sweepDurableCommerce(input: {
344
344
  }): Promise<CommerceSweepResult> {
345
345
  if (input.scheduler !== COMMERCE_SCHEDULER) return { ok: false, reason: "scheduler" };
346
346
  const port = new ScrappyCommerceOutbox(input.db);
347
- const due = await port.due(input.now,input.tenantId,input.stage);
347
+ let due:CommerceOutboxDue[];
348
+ try{due=await port.due(input.now,input.tenantId,input.stage);}catch{return {ok:false,reason:"storage",delivered:0,retried:0,dead:0,incomplete:1};}
348
349
  let delivered = 0;
349
350
  let retried = 0;
350
351
  let dead = 0;
352
+ let incomplete=0;
353
+ const failedTenants=new Set<string>();
351
354
  const tenants: { tenantId: string; stage: string }[] = [];
352
355
  const seenTenant = (tenantId: string, stage: string) => {
353
356
  if (!tenants.some((item) => item.tenantId === tenantId && item.stage === stage)) {
@@ -356,39 +359,34 @@ export async function sweepDurableCommerce(input: {
356
359
  };
357
360
  for (const row of due) {
358
361
  seenTenant(row.tenant_id, row.stage);
359
- const outcome = await settleCommerceRow(port, row, input.now, input.deliver);
362
+ let outcome:"delivered"|"retried"|"dead";
363
+ try{outcome=await settleCommerceRow(input.db,row,input.now,input.deliver);}catch{incomplete++;failedTenants.add(JSON.stringify([row.tenant_id,row.stage]));continue;}
360
364
  if (outcome === "delivered") delivered += 1;
361
365
  else if (outcome === "retried") retried += 1;
362
366
  else dead += 1;
363
367
  }
364
- for (const tenant of tenants) await port.noteCursor(tenant.tenantId, tenant.stage, input.now);
368
+ for(const tenant of tenants){if(failedTenants.has(JSON.stringify([tenant.tenantId,tenant.stage])))continue;try{await port.noteCursor(tenant.tenantId,tenant.stage,input.now);}catch{incomplete++;}}
369
+ if(incomplete)return {ok:false,reason:"storage",delivered,retried,dead,incomplete};
365
370
  return { ok: true, delivered, retried, dead };
366
371
  }
367
372
 
373
+ async function persistCommerceOutcome(db:Queryable,row:CommerceOutboxDue,state:"delivered"|"pending"|"dead",attemptCount=row.attempt_count,availableAt=row.available_at):Promise<void> {
374
+ const committed=await db.first<{outbox_id:string}>(`UPDATE scrappy_commerce_outbox SET state=?,attempt_count=?,available_at=? WHERE outbox_id=? AND tenant_id=? AND stage=? AND order_id=? AND state='pending' AND attempt_count=? RETURNING outbox_id`,state,attemptCount,availableAt,row.outbox_id,row.tenant_id,row.stage,row.order_id,row.attempt_count);
375
+ if(committed?.outbox_id!==row.outbox_id)throw new Error("commerce-outcome-incomplete");
376
+ }
368
377
  async function settleCommerceRow(
369
- port: ScrappyCommerceOutbox,
378
+ db:Queryable,
370
379
  row: CommerceOutboxDue,
371
380
  now: string,
372
381
  deliver: CommerceDeliver | undefined,
373
382
  ): Promise<"delivered" | "retried" | "dead"> {
374
- const event = publicCommercePayload(row.payload);
375
- if (!event) {
376
- await port.markDead(row.outbox_id);
377
- return "dead";
378
- }
379
- try {
380
- if (!deliver) throw new Error("delivery-unconfigured");
381
- await deliver(event,row);
382
- await port.markDelivered(row.outbox_id);
383
- return "delivered";
384
- } catch {
385
- const next = row.attempt_count + 1;
386
- if (next >= MAX_DELIVERY_ATTEMPTS) {
387
- await port.markDead(row.outbox_id, next);
388
- return "dead";
389
- }
390
- const availableAt = new Date(Date.parse(now) + computeBackoffDelaySeconds(next) * 1000).toISOString();
391
- await port.markRetry(row.outbox_id, next, availableAt);
392
- return "retried";
393
- }
383
+ const event=publicCommercePayload(row.payload);
384
+ if(!event){await persistCommerceOutcome(db,row,"dead");return "dead";}
385
+ let succeeded=false;
386
+ try{if(deliver){await deliver(event,row);succeeded=true;}}catch{/* Retry the failed order independently; diagnostic payloads stay private. */}
387
+ if(succeeded){await persistCommerceOutcome(db,row,"delivered");return "delivered";}
388
+ const next=row.attempt_count+1;
389
+ if(next>=MAX_DELIVERY_ATTEMPTS){await persistCommerceOutcome(db,row,"dead",next);return "dead";}
390
+ const availableAt=new Date(Date.parse(now)+computeBackoffDelaySeconds(next)*1000).toISOString();
391
+ await persistCommerceOutcome(db,row,"pending",next,availableAt);return "retried";
394
392
  }
@@ -32,6 +32,8 @@
32
32
  * compute one FOR either. `rawCustomerId` is accepted and simply unused;
33
33
  * see `docs/private-apps/order-forward-contract.md` for the full rationale.
34
34
  */
35
+ import { SourceAuthorityStore } from "./sourceAuthorityStore.js";
36
+ import { SourceAuthorityError, parseSourceWire, exactKeys, digestJson, type BeginAck, type ResultAck, type AuthorityCode, isRecord, isSourceId } from "./sourceAuthorityContract.js";
35
37
  import { OrdersStore, ScrappyOrderLedger, type OrderLineItem, type OrderStatus, type Order } from "./ordersStore.js";
36
38
  import { emitWebhookEvent } from "../../integrations/webhooks/emit.js";
37
39
  import type { WebhookTopic } from "../../integrations/webhooks/deliveryStore.js";
@@ -149,6 +151,7 @@ export async function receiveForwardedOrder(db: Queryable, input: OrderForwardIn
149
151
  }
150
152
 
151
153
  export interface CommerceCallerConfig {
154
+ expectedStage: CommerceStage;
152
155
  jwks: JSONWebKeySet;
153
156
  trustedIssuers: readonly string[];
154
157
  expectedAudience: string;
@@ -159,6 +162,7 @@ export interface CommerceCallerConfig {
159
162
  export type SignedCommerceRefusal = CallerTokenRefusal | "version" | "malformed" | "totals" | "private" | "replay";
160
163
 
161
164
  export type SignedCommerceResult =
165
+ | { ok:true; authorityAck:BeginAck|ResultAck; tenantId:string; stage:CommerceStage }
162
166
  | {
163
167
  ok: true;
164
168
  duplicate: boolean;
@@ -170,7 +174,7 @@ export type SignedCommerceResult =
170
174
  currency: string;
171
175
  tenantId: string;
172
176
  }
173
- | { ok: false; status: number; reason: SignedCommerceRefusal | "order commit failed" };
177
+ | { ok: false; status: number; reason: SignedCommerceRefusal | AuthorityCode | "order commit failed" };
174
178
 
175
179
  function isStage(value: unknown): value is CommerceStage {
176
180
  return value === "preview" || value === "publication";
@@ -198,6 +202,7 @@ export async function verifySignedCommerceToken(
198
202
  config: CommerceCallerConfig,
199
203
  binding: { tenantId: string; stage: CommerceStage },
200
204
  ): Promise<{ ok: true; issuer: string; jti: string; digest: string } | { ok: false; reason: CallerTokenRefusal | "version" }> {
205
+ if(!isStage(config.expectedStage)||binding.stage!==config.expectedStage)return {ok:false,reason:"stage"};
201
206
  if (token.split(".").length !== 3) {
202
207
  const admitted = admitCallerToken({
203
208
  token: { scheme: "shared-secret" },
@@ -265,6 +270,7 @@ export async function verifySignedCommerceToken(
265
270
  });
266
271
  if (!isStage(claims.stage)) return { ok: false, reason: admitted.ok ? "stage" : admitted.reason === "signature" ? "signature" : "stage" };
267
272
  if (!admitted.ok) return admitted;
273
+ if(typeof claims.exp!=="number"||claims.exp-Math.floor(Date.parse(config.now)/1000)>60)return {ok:false,reason:"lifetime"};
268
274
  if (claims.ver !== 1) return { ok: false, reason: "version" };
269
275
  if (typeof claims.digest !== "string" || claims.digest === "") return { ok: false, reason: "signature" };
270
276
  return { ok: true, issuer: typeof claims.iss === "string" ? claims.iss : "", jti: typeof claims.jti === "string" ? claims.jti : "", digest: claims.digest };
@@ -277,6 +283,7 @@ export async function receiveSignedCommerceOrder(
277
283
  token: string,
278
284
  config: CommerceCallerConfig,
279
285
  ): Promise<SignedCommerceResult> {
286
+ if(!isStage(config.expectedStage))return {ok:false,status:503,reason:"source-authority-unavailable"};
280
287
  if (body === null || typeof body !== "object" || Array.isArray(body)) {
281
288
  return { ok: false, status: 400, reason: "malformed" };
282
289
  }
@@ -285,7 +292,7 @@ export async function receiveSignedCommerceOrder(
285
292
  return { ok: false, status: 401, reason: "tenant" };
286
293
  }
287
294
  if (!isStage(record.stage)) return { ok: false, status: 401, reason: "stage" };
288
- if (!("event" in record)) return { ok: false, status: 400, reason: "malformed" };
295
+ if (!exactKeys(record,["tenantId","stage","event"])) return { ok: false, status: 400, reason: "malformed" };
289
296
 
290
297
  const verified = await verifySignedCommerceToken(token, config, {
291
298
  tenantId: record.tenantId,
@@ -298,18 +305,40 @@ export async function receiveSignedCommerceOrder(
298
305
  const digest = await sha256Hex(canonicalJson(envelope));
299
306
  if (digest !== verified.digest) return { ok: false, status: 401, reason: "signature" };
300
307
 
301
- const event = normalizeCommerceEvent(record.event);
302
- if (event === "malformed" || event === "totals") return { ok: false, status: 400, reason: event };
303
-
308
+ const authority=new SourceAuthorityStore(db);
304
309
  try {
310
+ const replay={issuer:verified.issuer,jti:verified.jti};
311
+ const seen=await db.first(`SELECT jti FROM scrappy_caller_replay WHERE tenant_id=? AND stage=? AND issuer=? AND jti=?`,record.tenantId,record.stage,replay.issuer,replay.jti);
312
+ if(seen)return {ok:false,status:401,reason:"replay"};
313
+ const wire=parseSourceWire(record.event);
314
+ if(!wire){
315
+ const e=record.event;
316
+ if(isRecord(e)&&e.source==="foxycart"&&isSourceId(e.providerStoreId)&&isSourceId(e.sourceOrderId)&&["validatedManifestCores","businessEventDigestPairs","resolvesHoldIds","completeValidatedManifestSetOrNull","conflictingValidatedGenerationsOrNull"].some(k=>Array.isArray(e[k])&&(e[k] as unknown[]).length>64)){await authority.budget({tenantId:record.tenantId,stage:record.stage,source:"foxycart",providerStoreId:e.providerStoreId,sourceOrderId:e.sourceOrderId},config.now);return {ok:false,status:409,reason:"authority-budget"};}
317
+ return {ok:false,status:400,reason:"malformed"};
318
+ }
319
+ if(wire.kind!=="source-order-event") {
320
+ const key={tenantId:record.tenantId,stage:record.stage,source:wire.source,providerStoreId:wire.providerStoreId,sourceOrderId:wire.sourceOrderId};
321
+ const authorityAck=wire.kind==="source-check-begin"?await authority.begin(key,wire,config.now,replay):await authority.result(key,wire,config.now,replay);
322
+ return {ok:true,authorityAck,tenantId:record.tenantId,stage:record.stage};
323
+ }
324
+ const event=normalizeCommerceEvent(wire.event);
325
+ if(event==="malformed"||event==="totals")return {ok:false,status:400,reason:event};
326
+ // Strict original bytes: defaults, aliases, omitted keys and additional keys cannot silently change the digest.
327
+ if(canonicalJson(wire.event)!==canonicalJson(event))return {ok:false,status:400,reason:"malformed"};
328
+ if(await digestJson(wire.event)!==wire.provenance.exactOriginalEventDigest)return {ok:false,status:401,reason:"signature"};
329
+ const sourceAuthority=await authority.authorizeEvent(record.tenantId,record.stage,event,wire.provenance);
305
330
  const committed = await new ScrappyOrderLedger(db).commit({
306
331
  tenantId: record.tenantId,
307
332
  stage: record.stage,
308
333
  event,
309
334
  now: config.now,
310
335
  replay: { issuer: verified.issuer, jti: verified.jti },
336
+ sourceAuthority,
311
337
  });
312
- if (!committed.ok) return { ok: false, status: 401, reason: "replay" };
338
+ if(!committed.ok){
339
+ if(committed.reason==="conflict")await authority.budget({tenantId:record.tenantId,stage:record.stage,source:"foxycart",providerStoreId:sourceAuthority.provider_store_id,sourceOrderId:sourceAuthority.source_order_id},config.now);
340
+ return {ok:false,status:committed.reason==="conflict"?409:401,reason:committed.reason==="conflict"?"control-identity-conflict":"replay"};
341
+ }
313
342
  return {
314
343
  ok: true,
315
344
  duplicate: committed.duplicate,
@@ -321,7 +350,8 @@ export async function receiveSignedCommerceOrder(
321
350
  currency: committed.snapshot.currency,
322
351
  tenantId: record.tenantId,
323
352
  };
324
- } catch {
353
+ } catch(error) {
354
+ if(error instanceof SourceAuthorityError)return {ok:false,status:error.code==="malformed"?400:error.code==="signature"?401:409,reason:error.code};
325
355
  return { ok: false, status: 500, reason: "order commit failed" };
326
356
  }
327
357
  }
@@ -10,6 +10,7 @@
10
10
  * `AppRegistryService` uses, so this runs unchanged against real D1 in the
11
11
  * Worker and an in-memory `node:sqlite` in tests.
12
12
  */
13
+ import { SourceAuthorityStore, type AuthorityRow } from "./sourceAuthorityStore.js";
13
14
  import type { Queryable, QueryStatement } from "../../catalog/d1/catalog.js";
14
15
  import {
15
16
  canonicalJson,
@@ -233,6 +234,7 @@ export interface CommitCommerceInput {
233
234
  event: CommerceSourceEvent;
234
235
  now: string;
235
236
  replay?: { issuer: string; jti: string };
237
+ sourceAuthority?: AuthorityRow;
236
238
  }
237
239
 
238
240
  export type CommitCommerceResult =
@@ -288,7 +290,7 @@ export class ScrappyOrderLedger {
288
290
  );
289
291
  if (existing) {
290
292
  const storedDigest = await this.storedPayloadDigest(tenantId, stage, event.source, event.sourceEventId);
291
- if (storedDigest !== null && storedDigest !== digest) return { ok: false, reason: "conflict" };
293
+ if (storedDigest !== digest) return { ok: false, reason: "conflict" };
292
294
  if (input.replay) {
293
295
  try {
294
296
  await this.insertReplay(input);
@@ -317,6 +319,10 @@ export class ScrappyOrderLedger {
317
319
  const key = idempotencyKey(tenantId, stage, event.source, event.sourceEventId);
318
320
  const outboxId = (await sha256Hex(key)).slice(0, 32);
319
321
  const statements: QueryStatement[] = [];
322
+ if (input.sourceAuthority) {
323
+ const a=input.sourceAuthority;
324
+ statements.push({sql:`SELECT json(CASE WHEN EXISTS(SELECT 1 FROM commerce_source_authority WHERE tenant_id=? AND stage=? AND source=? AND source_order_id=? AND source_state_version=? AND accepted_ledger_generation=? AND accepted_business_event_set_digest=?) THEN 'true' ELSE 'source-state-changed' END)`,params:[tenantId,stage,event.source,event.sourceOrderId,a.source_state_version,a.accepted_ledger_generation,a.accepted_business_event_set_digest]});
325
+ }
320
326
  if (input.replay) {
321
327
  statements.push({
322
328
  sql: `INSERT INTO scrappy_caller_replay (tenant_id, stage, issuer, jti, seen_at) VALUES (?,?,?,?,?)`,
@@ -393,6 +399,7 @@ export class ScrappyOrderLedger {
393
399
  },
394
400
  );
395
401
 
402
+ if(input.sourceAuthority)statements.push(...await new SourceAuthorityStore(this.db).businessStatements(input.sourceAuthority,event.orderId));
396
403
  try {
397
404
  await atomic(this.db, statements);
398
405
  } catch (error) {
@@ -409,7 +416,7 @@ export class ScrappyOrderLedger {
409
416
  );
410
417
  if (raced) {
411
418
  const storedDigest = await this.storedPayloadDigest(tenantId, stage, event.source, event.sourceEventId);
412
- if (storedDigest !== null && storedDigest !== digest) return { ok: false, reason: "conflict" };
419
+ if (storedDigest !== digest) return { ok: false, reason: "conflict" };
413
420
  const snapshotNow = await this.readSnapshot(tenantId, stage, event.orderId, event);
414
421
  return { ok: true, duplicate: true, snapshot: snapshotNow, outboxId: raced.outbox_id };
415
422
  }