@tokenoftrust/storefront-runner 3.10.2 → 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,830 @@
1
+ /** Durable authority transitions and immutable control receipts, fenced by D1 CAS. */
2
+ import type { Queryable, QueryStatement } from "../../catalog/d1/catalog.js";
3
+ import type { CommerceStage } from "@tot/public-runtime";
4
+ import {
5
+ SOURCE_LEASE_MS,
6
+ isHash,
7
+ canonicalInstant,
8
+ isSourceId,
9
+ exactKeys,
10
+ SOURCE_LIMIT,
11
+ SourceAuthorityError,
12
+ digestJson,
13
+ observationId,
14
+ conditionDigest,
15
+ faultProof,
16
+ validateManifestSet,
17
+ uniqueLineage,
18
+ type SourceKey,
19
+ type SourceBegin,
20
+ type SourceResult,
21
+ type SourceHold,
22
+ type ManifestCore,
23
+ type BusinessPair,
24
+ type BeginAck,
25
+ type ResultAck,
26
+ type Provenance,
27
+ } from "./sourceAuthorityContract.js";
28
+ interface State {
29
+ version: number;
30
+ revision: number;
31
+ active: string | null;
32
+ pending: boolean;
33
+ accepted: string | null;
34
+ businessDigest: string | null;
35
+ pairs: BusinessPair[];
36
+ expires: string | null;
37
+ sync: boolean;
38
+ holds: SourceHold[];
39
+ budget: boolean;
40
+ orderId: string | null;
41
+ }
42
+ export interface AuthorityRow {
43
+ tenant_id: string;
44
+ stage: CommerceStage;
45
+ source: string;
46
+ provider_store_id: string;
47
+ source_order_id: string;
48
+ order_id: string | null;
49
+ source_state_version: number;
50
+ check_revision: number;
51
+ active_check_id: string | null;
52
+ pending_check: number;
53
+ accepted_ledger_generation: string | null;
54
+ accepted_business_event_set_digest: string | null;
55
+ source_proof_expires_at: string | null;
56
+ source_sync_pending: number;
57
+ unresolved_count: number;
58
+ authority_budget: number;
59
+ binding_digest: string;
60
+ state_json: string;
61
+ mutation_id: string;
62
+ }
63
+ interface Replay {
64
+ issuer: string;
65
+ jti: string;
66
+ }
67
+ const WHERE =
68
+ "tenant_id=? AND stage=? AND source=? AND provider_store_id=? AND source_order_id=?";
69
+ const keyParams = (k: SourceKey): unknown[] => [
70
+ k.tenantId,
71
+ k.stage,
72
+ k.source,
73
+ k.providerStoreId,
74
+ k.sourceOrderId,
75
+ ];
76
+ const initial = (): State => ({
77
+ version: 0,
78
+ revision: 0,
79
+ active: null,
80
+ pending: true,
81
+ accepted: null,
82
+ businessDigest: null,
83
+ pairs: [],
84
+ expires: null,
85
+ sync: true,
86
+ holds: [],
87
+ budget: false,
88
+ orderId: null,
89
+ });
90
+ export type SourceReadiness =
91
+ | "source-check-pending"
92
+ | "source-ledger-conflict"
93
+ | "source-authority-unreadable"
94
+ | "source-authority-unavailable"
95
+ | "source-sync-pending";
96
+ export function authorityHolds(
97
+ row: AuthorityRow | null,
98
+ now: string,
99
+ ): SourceReadiness[] {
100
+ if (!row) return ["source-authority-unavailable"];
101
+ try {
102
+ const s = JSON.parse(row.state_json) as State;
103
+ if (
104
+ !Number.isSafeInteger(row.source_state_version) ||
105
+ row.source_state_version < 0 ||
106
+ !Number.isSafeInteger(row.check_revision) ||
107
+ row.check_revision < 0 ||
108
+ ![0, 1].includes(row.pending_check) ||
109
+ ![0, 1].includes(row.source_sync_pending) ||
110
+ ![0, 1].includes(row.authority_budget) ||
111
+ !isSourceId(row.provider_store_id) ||
112
+ !isHash(row.binding_digest) ||
113
+ (row.accepted_ledger_generation !== null &&
114
+ !isHash(row.accepted_ledger_generation)) ||
115
+ (row.accepted_business_event_set_digest !== null &&
116
+ !isHash(row.accepted_business_event_set_digest)) ||
117
+ (row.source_proof_expires_at !== null &&
118
+ !canonicalInstant(row.source_proof_expires_at))
119
+ )
120
+ return ["source-authority-unreadable"];
121
+ if (
122
+ Array.isArray(s.pairs) &&
123
+ s.pairs.some(
124
+ (p, i) =>
125
+ !exactKeys(p, ["sourceEventId", "originalEventDigest"]) ||
126
+ typeof p.sourceEventId !== "string" ||
127
+ !/^[-A-Za-z0-9._:]{1,128}$/.test(p.sourceEventId) ||
128
+ !isHash(p.originalEventDigest) ||
129
+ (i > 0 && s.pairs[i - 1]!.sourceEventId >= p.sourceEventId),
130
+ )
131
+ )
132
+ return ["source-authority-unreadable"];
133
+ if (
134
+ !Number.isSafeInteger(s.version) ||
135
+ !Number.isSafeInteger(s.revision) ||
136
+ !Array.isArray(s.holds) ||
137
+ s.holds.length > SOURCE_LIMIT ||
138
+ !Array.isArray(s.pairs) ||
139
+ s.pairs.length > SOURCE_LIMIT ||
140
+ s.version !== row.source_state_version ||
141
+ s.revision !== row.check_revision ||
142
+ s.active !== row.active_check_id ||
143
+ Number(s.pending) !== row.pending_check ||
144
+ s.accepted !== row.accepted_ledger_generation ||
145
+ s.businessDigest !== row.accepted_business_event_set_digest ||
146
+ s.expires !== row.source_proof_expires_at ||
147
+ Number(s.sync) !== row.source_sync_pending ||
148
+ s.holds.length !== row.unresolved_count ||
149
+ Number(s.budget) !== row.authority_budget ||
150
+ s.orderId !== row.order_id
151
+ )
152
+ return ["source-authority-unreadable"];
153
+ } catch {
154
+ return ["source-authority-unreadable"];
155
+ }
156
+ const holds: SourceReadiness[] = [];
157
+ if (row.pending_check) holds.push("source-check-pending");
158
+ if (row.unresolved_count || row.authority_budget)
159
+ holds.push("source-ledger-conflict");
160
+ if (
161
+ !row.accepted_ledger_generation ||
162
+ !row.accepted_business_event_set_digest ||
163
+ !row.source_proof_expires_at ||
164
+ !Number.isFinite(Date.parse(row.source_proof_expires_at)) ||
165
+ Date.parse(row.source_proof_expires_at) <= Date.parse(now)
166
+ )
167
+ holds.push("source-authority-unavailable");
168
+ if (row.source_sync_pending) holds.push("source-sync-pending");
169
+ return holds;
170
+ }
171
+ /** SQL checks both the loaded receiver revision and its live lease at the write boundary. */
172
+ export function authorityFence(
173
+ row: AuthorityRow | null,
174
+ now: string,
175
+ requireHealthy: boolean,
176
+ ): { sql: string; params: unknown[] } {
177
+ if (!row)
178
+ return {
179
+ sql: requireHealthy
180
+ ? "0"
181
+ : "NOT EXISTS(SELECT 1 FROM commerce_source_authority sa WHERE sa.tenant_id=scrappy_commerce_orders.tenant_id AND sa.stage=scrappy_commerce_orders.stage AND sa.source=scrappy_commerce_orders.source AND sa.source_order_id=scrappy_commerce_orders.source_order_id)",
182
+ params: [],
183
+ };
184
+ return {
185
+ sql: `EXISTS(SELECT 1 FROM commerce_source_authority sa WHERE sa.tenant_id=scrappy_commerce_orders.tenant_id AND sa.stage=scrappy_commerce_orders.stage AND sa.source=scrappy_commerce_orders.source AND sa.source_order_id=scrappy_commerce_orders.source_order_id AND sa.order_id=scrappy_commerce_orders.order_id AND sa.provider_store_id=? AND sa.source_state_version=? AND sa.check_revision=? AND sa.binding_digest=? AND json_valid(sa.state_json)=1 ${requireHealthy ? "AND sa.pending_check=0 AND sa.unresolved_count=0 AND sa.authority_budget=0 AND sa.source_sync_pending=0 AND sa.accepted_ledger_generation IS NOT NULL AND sa.source_proof_expires_at>?" : ""})`,
186
+ params: [
187
+ row.provider_store_id,
188
+ row.source_state_version,
189
+ row.check_revision,
190
+ row.binding_digest,
191
+ ...(requireHealthy ? [now] : []),
192
+ ],
193
+ };
194
+ }
195
+ export class SourceAuthorityStore {
196
+ constructor(private readonly db: Queryable) {}
197
+ async read(key: SourceKey): Promise<AuthorityRow | null> {
198
+ return this.db.first<AuthorityRow>(
199
+ `SELECT * FROM commerce_source_authority WHERE ${WHERE}`,
200
+ ...keyParams(key),
201
+ );
202
+ }
203
+ async forOrder(
204
+ tenant: string,
205
+ stage: CommerceStage,
206
+ source: string,
207
+ sourceOrderId: string,
208
+ ): Promise<AuthorityRow | null> {
209
+ return this.db.first<AuthorityRow>(
210
+ `SELECT * FROM commerce_source_authority WHERE tenant_id=? AND stage=? AND source=? AND source_order_id=?`,
211
+ tenant,
212
+ stage,
213
+ source,
214
+ sourceOrderId,
215
+ );
216
+ }
217
+ private async consumeReplay(
218
+ key: SourceKey,
219
+ now: string,
220
+ replay?: Replay,
221
+ ): Promise<void> {
222
+ if (replay)
223
+ await this.db.run(
224
+ `INSERT INTO scrappy_caller_replay(tenant_id,stage,issuer,jti,seen_at) VALUES(?,?,?,?,?)`,
225
+ key.tenantId,
226
+ key.stage,
227
+ replay.issuer,
228
+ replay.jti,
229
+ now,
230
+ );
231
+ }
232
+ private async record<T>(
233
+ key: SourceKey,
234
+ kind: string,
235
+ id: string,
236
+ ): Promise<{ payload_digest: string; payload: T } | null> {
237
+ const r = await this.db.first<{
238
+ payload_digest: string;
239
+ payload_json: string;
240
+ }>(
241
+ `SELECT payload_digest,payload_json FROM commerce_source_observations WHERE ${WHERE} AND record_kind=? AND record_id=?`,
242
+ ...keyParams(key),
243
+ kind,
244
+ id,
245
+ );
246
+ return r
247
+ ? {
248
+ payload_digest: r.payload_digest,
249
+ payload: JSON.parse(r.payload_json) as T,
250
+ }
251
+ : null;
252
+ }
253
+ private async transition(
254
+ key: SourceKey,
255
+ old: AuthorityRow | null,
256
+ state: State,
257
+ now: string,
258
+ records: { kind: string; id: string; digest: string; payload: unknown }[],
259
+ replay?: Replay,
260
+ resolve: string[] = [],
261
+ ): Promise<boolean> {
262
+ if (!this.db.batchResults)
263
+ throw new SourceAuthorityError("source-authority-unavailable");
264
+ const mutation = crypto.randomUUID();
265
+ const binding = await digestJson({ ...key, orderId: state.orderId });
266
+ const values = [
267
+ state.orderId,
268
+ state.version,
269
+ state.revision,
270
+ state.active,
271
+ state.pending ? 1 : 0,
272
+ state.accepted,
273
+ state.businessDigest,
274
+ state.expires,
275
+ state.sync ? 1 : 0,
276
+ state.holds.length,
277
+ state.budget ? 1 : 0,
278
+ binding,
279
+ JSON.stringify(state),
280
+ mutation,
281
+ ];
282
+ const statements: QueryStatement[] = [
283
+ old
284
+ ? {
285
+ sql: `UPDATE commerce_source_authority SET order_id=?,source_state_version=?,check_revision=?,active_check_id=?,pending_check=?,accepted_ledger_generation=?,accepted_business_event_set_digest=?,source_proof_expires_at=?,source_sync_pending=?,unresolved_count=?,authority_budget=?,binding_digest=?,state_json=?,mutation_id=? WHERE ${WHERE} AND source_state_version=? AND mutation_id=? RETURNING source_state_version`,
286
+ params: [
287
+ ...values,
288
+ ...keyParams(key),
289
+ old.source_state_version,
290
+ old.mutation_id,
291
+ ],
292
+ }
293
+ : {
294
+ sql: `INSERT INTO commerce_source_authority(tenant_id,stage,source,provider_store_id,source_order_id,order_id,source_state_version,check_revision,active_check_id,pending_check,accepted_ledger_generation,accepted_business_event_set_digest,source_proof_expires_at,source_sync_pending,unresolved_count,authority_budget,binding_digest,state_json,mutation_id) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT DO NOTHING RETURNING source_state_version`,
295
+ params: [...keyParams(key), ...values],
296
+ },
297
+ ];
298
+ // One bounded insert persists every immutable record inside the authority CAS.
299
+ statements.push({
300
+ sql: `INSERT INTO commerce_source_observations(tenant_id,stage,source,provider_store_id,source_order_id,record_kind,record_id,payload_digest,payload_json)
301
+ SELECT sa.tenant_id,sa.stage,sa.source,sa.provider_store_id,sa.source_order_id,json_extract(r.value,'$.kind'),json_extract(r.value,'$.id'),json_extract(r.value,'$.digest'),json_extract(r.value,'$.payloadJson')
302
+ FROM commerce_source_authority sa,json_each(?) r
303
+ WHERE sa.tenant_id=? AND sa.stage=? AND sa.source=? AND sa.provider_store_id=? AND sa.source_order_id=? AND sa.mutation_id=?`,
304
+ params: [
305
+ JSON.stringify(
306
+ records.map((r) => ({
307
+ kind: r.kind,
308
+ id: r.id,
309
+ digest: r.digest,
310
+ payloadJson: JSON.stringify(r.payload),
311
+ })),
312
+ ),
313
+ ...keyParams(key),
314
+ mutation,
315
+ ],
316
+ });
317
+ for (const r of records.filter(
318
+ (r) => r.kind === "check" || r.kind === "result",
319
+ )) {
320
+ const controlId = `source-control:${await digestJson({ key, kind: r.kind, id: r.id })}`;
321
+ statements.push({
322
+ sql: `INSERT INTO scrappy_commerce_inbox(tenant_id,stage,source,source_event_id,event_version,idempotency_key,payload_digest,received_at) SELECT tenant_id,stage,source,?,1,?,?,? FROM commerce_source_authority WHERE ${WHERE} AND mutation_id=?`,
323
+ params: [
324
+ controlId,
325
+ controlId,
326
+ r.digest,
327
+ now,
328
+ ...keyParams(key),
329
+ mutation,
330
+ ],
331
+ });
332
+ }
333
+ if (replay)
334
+ statements.push({
335
+ sql: `INSERT INTO scrappy_caller_replay(tenant_id,stage,issuer,jti,seen_at) SELECT tenant_id,stage,?,?,? FROM commerce_source_authority WHERE ${WHERE} AND mutation_id=?`,
336
+ params: [replay.issuer, replay.jti, now, ...keyParams(key), mutation],
337
+ });
338
+ statements.push({
339
+ sql: `UPDATE commerce_source_observations SET resolved_version=?,resolved_generation=? WHERE ${WHERE} AND record_kind='hold' AND record_id IN(SELECT value FROM json_each(?)) AND resolved_version IS NULL AND EXISTS(SELECT 1 FROM commerce_source_authority WHERE ${WHERE} AND mutation_id=?)`,
340
+ params: [
341
+ state.version,
342
+ state.accepted,
343
+ ...keyParams(key),
344
+ JSON.stringify(resolve),
345
+ ...keyParams(key),
346
+ mutation,
347
+ ],
348
+ });
349
+ // Every authority change invalidates a previously loaded fulfillment CAS. Terminal progress is retained.
350
+ statements.push({
351
+ sql: `UPDATE scrappy_commerce_orders SET event_version=event_version+1 WHERE tenant_id=? AND stage=? AND source=? AND source_order_id=? AND EXISTS(SELECT 1 FROM commerce_source_authority WHERE ${WHERE} AND mutation_id=?)`,
352
+ params: [
353
+ key.tenantId,
354
+ key.stage,
355
+ key.source,
356
+ key.sourceOrderId,
357
+ ...keyParams(key),
358
+ mutation,
359
+ ],
360
+ });
361
+ statements.push({
362
+ sql: `INSERT INTO scrappy_shipment_audit(audit_id,tenant_id,stage,order_id,at,actor_id,kind,from_state,to_state)
363
+ SELECT ?,tenant_id,stage,order_id,?,'commerce-source-authority','source-authority','source',?
364
+ FROM scrappy_commerce_orders WHERE tenant_id=? AND stage=? AND source=? AND source_order_id=?
365
+ AND EXISTS(SELECT 1 FROM commerce_source_authority WHERE ${WHERE} AND mutation_id=?)`,
366
+ params: [
367
+ `audit_${crypto.randomUUID()}`,
368
+ `${now}#source-${state.version}`,
369
+ state.pending || state.sync || state.holds.length || state.budget
370
+ ? "held"
371
+ : "verified",
372
+ key.tenantId,
373
+ key.stage,
374
+ key.source,
375
+ key.sourceOrderId,
376
+ ...keyParams(key),
377
+ mutation,
378
+ ],
379
+ });
380
+ const result = await this.db.batchResults(statements);
381
+ return Boolean(result[0]?.results.length);
382
+ }
383
+ async begin(
384
+ key: SourceKey,
385
+ event: SourceBegin,
386
+ now: string,
387
+ replay?: Replay,
388
+ ): Promise<BeginAck> {
389
+ for (let attempt = 0; attempt < 8; attempt++) {
390
+ const old = await this.read(key);
391
+ const priorBegin = await this.db.first<{ payload_json: string }>(
392
+ `SELECT payload_json FROM commerce_source_observations WHERE ${WHERE} AND record_kind='check' AND json_extract(payload_json,'$.requestId')=?`,
393
+ ...keyParams(key),
394
+ event.requestId,
395
+ );
396
+ if (priorBegin) {
397
+ await this.consumeReplay(key, now, replay);
398
+ return (JSON.parse(priorBegin.payload_json) as { beginAck: BeginAck })
399
+ .beginAck;
400
+ }
401
+ const state: State = old ? JSON.parse(old.state_json) : initial();
402
+ if (state.active && state.pending) {
403
+ const check = await this.record<{
404
+ issuedAt: string;
405
+ checkRevision: number;
406
+ }>(key, "check", state.active);
407
+ if (
408
+ !check ||
409
+ !canonicalInstant(check.payload.issuedAt) ||
410
+ check.payload.checkRevision !== state.revision ||
411
+ Date.parse(check.payload.issuedAt) > Date.parse(now)
412
+ )
413
+ throw new SourceAuthorityError("source-authority-unavailable");
414
+ if (
415
+ Date.parse(check.payload.issuedAt) + SOURCE_LEASE_MS >
416
+ Date.parse(now)
417
+ ) {
418
+ await this.consumeReplay(key, now, replay);
419
+ return {
420
+ ok: true,
421
+ duplicate: true,
422
+ checkId: state.active,
423
+ checkRevision: state.revision,
424
+ sourceStateVersion: state.version,
425
+ acceptedLedgerGeneration: state.accepted,
426
+ acceptedBusinessEventSetDigest: state.businessDigest,
427
+ unresolvedHolds: state.holds,
428
+ };
429
+ }
430
+ }
431
+ state.version++;
432
+ state.revision++;
433
+ state.active = `check_${crypto.randomUUID()}`;
434
+ state.pending = true;
435
+ const beginAck: BeginAck = {
436
+ ok: true,
437
+ duplicate: false,
438
+ checkId: state.active,
439
+ checkRevision: state.revision,
440
+ sourceStateVersion: state.version,
441
+ acceptedLedgerGeneration: state.accepted,
442
+ acceptedBusinessEventSetDigest: state.businessDigest,
443
+ unresolvedHolds: state.holds,
444
+ };
445
+ const check = {
446
+ issuedAt: now,
447
+ checkRevision: state.revision,
448
+ sourceStateVersion: state.version,
449
+ requestId: event.requestId,
450
+ beginAck,
451
+ };
452
+ if (
453
+ await this.transition(
454
+ key,
455
+ old,
456
+ state,
457
+ now,
458
+ [
459
+ {
460
+ kind: "check",
461
+ id: state.active,
462
+ digest: await digestJson(check),
463
+ payload: check,
464
+ },
465
+ ],
466
+ replay,
467
+ )
468
+ )
469
+ return {
470
+ ok: true,
471
+ duplicate: false,
472
+ checkId: state.active,
473
+ checkRevision: state.revision,
474
+ sourceStateVersion: state.version,
475
+ acceptedLedgerGeneration: state.accepted,
476
+ acceptedBusinessEventSetDigest: state.businessDigest,
477
+ unresolvedHolds: state.holds,
478
+ };
479
+ }
480
+ throw new SourceAuthorityError("source-state-changed");
481
+ }
482
+ async result(
483
+ key: SourceKey,
484
+ event: SourceResult,
485
+ now: string,
486
+ replay?: Replay,
487
+ ): Promise<ResultAck> {
488
+ const digest = await digestJson(event);
489
+ for (let attempt = 0; attempt < 8; attempt++) {
490
+ const old = await this.read(key);
491
+ if (!old) throw new SourceAuthorityError("source-authority-unavailable");
492
+ const state: State = JSON.parse(old.state_json);
493
+ const prior = await this.record<{ ack: ResultAck; event: SourceResult }>(
494
+ key,
495
+ "result",
496
+ event.observationId,
497
+ );
498
+ if (prior) {
499
+ if (prior.payload_digest === digest) {
500
+ await this.consumeReplay(key, now, replay);
501
+ return prior.payload.ack;
502
+ }
503
+ await this.conflict(key, old, state, now);
504
+ throw new SourceAuthorityError("control-identity-conflict");
505
+ }
506
+ if ((await observationId(key, event)) !== event.observationId) {
507
+ await this.conflict(key, old, state, now);
508
+ throw new SourceAuthorityError("control-identity-conflict");
509
+ }
510
+ const check = await this.record<{
511
+ issuedAt: string;
512
+ checkRevision: number;
513
+ sourceStateVersion: number;
514
+ }>(key, "check", event.checkId);
515
+ if (
516
+ !check ||
517
+ check.payload.checkRevision !== event.expectedCheckRevision ||
518
+ event.expectedSourceStateVersion < check.payload.sourceStateVersion ||
519
+ event.expectedSourceStateVersion > state.version
520
+ )
521
+ throw new SourceAuthorityError("source-state-changed");
522
+ const records: {
523
+ kind: string;
524
+ id: string;
525
+ digest: string;
526
+ payload: unknown;
527
+ }[] = [];
528
+ let resolves: string[] = [];
529
+ if (event.outcome === "fault") {
530
+ const proof = faultProof(event);
531
+ const { conditionDigest: _, ...facts } = proof;
532
+ if ((await conditionDigest(key, facts)) !== event.conditionDigest)
533
+ throw new SourceAuthorityError("malformed");
534
+ const manifests = event.completeValidatedManifestSetOrNull
535
+ ? await validateManifestSet(
536
+ key,
537
+ event.completeValidatedManifestSetOrNull,
538
+ )
539
+ : new Map<string, ManifestCore>();
540
+ if (
541
+ event.faultKind === "fork" &&
542
+ (!event.completeValidatedManifestSetOrNull ||
543
+ !event.conflictingValidatedGenerationsOrNull ||
544
+ event.conflictingValidatedGenerationsOrNull.some(
545
+ (id) =>
546
+ !manifests.has(id) ||
547
+ manifests.get(id)!.parent !== event.divergenceParentOrNull,
548
+ ))
549
+ )
550
+ throw new SourceAuthorityError("malformed");
551
+ for (const [id, c] of manifests)
552
+ if (!(await this.record(key, "manifest", id)))
553
+ records.push({ kind: "manifest", id, digest: id, payload: c });
554
+ let changed = false;
555
+ if (
556
+ !state.holds.some((h) => h.conditionDigest === event.conditionDigest)
557
+ ) {
558
+ changed = true;
559
+ if (state.holds.length >= SOURCE_LIMIT) state.budget = true;
560
+ else {
561
+ const hold: SourceHold = {
562
+ holdId: `hold_${crypto.randomUUID()}`,
563
+ conditionDigest: event.conditionDigest,
564
+ faultKind: event.faultKind,
565
+ openingObservationId: event.observationId,
566
+ openingProof: proof,
567
+ };
568
+ state.holds.push(hold);
569
+ records.push({
570
+ kind: "hold",
571
+ id: hold.holdId,
572
+ digest: await digestJson(hold),
573
+ payload: hold,
574
+ });
575
+ }
576
+ }
577
+ if (changed) state.version++;
578
+ } else {
579
+ if (
580
+ state.active !== event.checkId ||
581
+ !state.pending ||
582
+ state.version !== event.expectedSourceStateVersion ||
583
+ state.revision !== event.expectedCheckRevision
584
+ )
585
+ throw new SourceAuthorityError("source-state-changed");
586
+ const issued = Date.parse(check.payload.issuedAt);
587
+ if (
588
+ Date.parse(event.actualObservedAt) < issued ||
589
+ Date.parse(event.actualObservedAt) > Date.parse(now) ||
590
+ issued + SOURCE_LEASE_MS <= Date.parse(now)
591
+ )
592
+ throw new SourceAuthorityError("source-authority-unavailable");
593
+ const map = await validateManifestSet(
594
+ key,
595
+ event.validatedManifestCores,
596
+ );
597
+ if (
598
+ !uniqueLineage(map, event.tipGeneration) ||
599
+ (state.accepted && !map.has(state.accepted))
600
+ )
601
+ throw new SourceAuthorityError("source-state-changed");
602
+ const retained = await this.db.all<{ record_id: string }>(
603
+ `SELECT record_id FROM commerce_source_observations WHERE ${WHERE} AND record_kind='manifest'`,
604
+ ...keyParams(key),
605
+ );
606
+ if (retained.some((r) => !map.has(r.record_id)))
607
+ throw new SourceAuthorityError("source-state-changed");
608
+ if (
609
+ state.budget ||
610
+ state.holds.some(
611
+ (h) =>
612
+ ![
613
+ "source-unavailable",
614
+ "census-incomplete",
615
+ "census-changing",
616
+ "committed-payload-missing",
617
+ ].includes(h.faultKind) ||
618
+ (h.faultKind === "committed-payload-missing" &&
619
+ h.openingProof.completeValidatedManifestSetOrNull === null),
620
+ )
621
+ )
622
+ throw new SourceAuthorityError("source-state-changed");
623
+ const expected = state.holds.map((h) => h.holdId).sort();
624
+ if (
625
+ JSON.stringify(expected) !==
626
+ JSON.stringify([...event.resolvesHoldIds].sort())
627
+ )
628
+ throw new SourceAuthorityError("source-state-changed");
629
+ if (
630
+ (await digestJson(event.businessEventDigestPairs)) !==
631
+ event.businessEventSetDigest
632
+ )
633
+ throw new SourceAuthorityError("malformed");
634
+ // A new accepted source set must preserve all immutable business identities already admitted.
635
+ if (
636
+ state.pairs.some(
637
+ (p) =>
638
+ !event.businessEventDigestPairs.some(
639
+ (q) =>
640
+ q.sourceEventId === p.sourceEventId &&
641
+ q.originalEventDigest === p.originalEventDigest,
642
+ ),
643
+ )
644
+ )
645
+ throw new SourceAuthorityError("source-state-changed");
646
+ const persisted = await this.persistedPairs(key);
647
+ if (
648
+ persisted.some(
649
+ (p) =>
650
+ !event.businessEventDigestPairs.some(
651
+ (q) =>
652
+ q.sourceEventId === p.sourceEventId &&
653
+ q.originalEventDigest === p.originalEventDigest,
654
+ ),
655
+ )
656
+ )
657
+ throw new SourceAuthorityError("source-state-changed");
658
+ for (const [id, c] of map)
659
+ if (!(await this.record(key, "manifest", id)))
660
+ records.push({ kind: "manifest", id, digest: id, payload: c });
661
+ resolves = expected;
662
+ state.holds = [];
663
+ state.version++;
664
+ state.pending = false;
665
+ state.accepted = event.tipGeneration;
666
+ state.businessDigest = event.businessEventSetDigest;
667
+ state.pairs = event.businessEventDigestPairs;
668
+ state.expires = new Date(issued + SOURCE_LEASE_MS).toISOString();
669
+ state.sync = (await digestJson(persisted)) !== state.businessDigest;
670
+ }
671
+ const ack: ResultAck = {
672
+ ok: true,
673
+ duplicate: false,
674
+ checkId: event.checkId,
675
+ observationId: event.observationId,
676
+ sourceStateVersion: state.version,
677
+ acceptedLedgerGeneration: state.accepted,
678
+ acceptedBusinessEventSetDigest: state.businessDigest,
679
+ sourceProofExpiresAt: state.expires,
680
+ pendingCheck: state.pending,
681
+ unresolvedHoldIdsDigest: await digestJson(
682
+ state.holds.map((h) => h.holdId).sort(),
683
+ ),
684
+ sourceSyncPending: state.sync,
685
+ };
686
+ records.push({
687
+ kind: "result",
688
+ id: event.observationId,
689
+ digest,
690
+ payload: { event, ack },
691
+ });
692
+ // A result receipt shares the exact authority transaction; ACK loss is safely retryable.
693
+ try {
694
+ if (
695
+ await this.transition(key, old, state, now, records, replay, resolves)
696
+ )
697
+ return ack;
698
+ } catch (error) {
699
+ const receipt = await this.record<{
700
+ ack: ResultAck;
701
+ event: SourceResult;
702
+ }>(key, "result", event.observationId);
703
+ if (receipt?.payload_digest === digest) return receipt.payload.ack;
704
+ throw error;
705
+ }
706
+ }
707
+ throw new SourceAuthorityError("source-state-changed");
708
+ }
709
+ async budget(key: SourceKey, now: string): Promise<void> {
710
+ for (let n = 0; n < 8; n++) {
711
+ const old = await this.read(key);
712
+ const s: State = old ? JSON.parse(old.state_json) : initial();
713
+ s.budget = true;
714
+ s.version++;
715
+ if (await this.transition(key, old, s, now, [])) return;
716
+ }
717
+ throw new SourceAuthorityError("source-state-changed");
718
+ }
719
+ private async conflict(
720
+ key: SourceKey,
721
+ old: AuthorityRow,
722
+ state: State,
723
+ now: string,
724
+ ): Promise<void> {
725
+ state.version++;
726
+ state.budget = true;
727
+ if (!(await this.transition(key, old, state, now, []))) {
728
+ const current = await this.read(key);
729
+ if (!current)
730
+ throw new SourceAuthorityError("source-authority-unavailable");
731
+ return this.conflict(key, current, JSON.parse(current.state_json), now);
732
+ }
733
+ }
734
+ async persistedPairs(key: SourceKey): Promise<BusinessPair[]> {
735
+ const rows = await this.db.all<{
736
+ source_event_id: string;
737
+ payload_digest: string;
738
+ }>(
739
+ `SELECT i.source_event_id,i.payload_digest FROM scrappy_commerce_inbox i JOIN scrappy_commerce_outbox o ON o.tenant_id=i.tenant_id AND o.stage=i.stage AND o.source=i.source AND o.source_event_id=i.source_event_id WHERE o.tenant_id=? AND o.stage=? AND o.source=? AND json_extract(o.payload,'$.source.sourceOrderId')=? ORDER BY i.source_event_id`,
740
+ key.tenantId,
741
+ key.stage,
742
+ key.source,
743
+ key.sourceOrderId,
744
+ );
745
+ return rows.map((r) => ({
746
+ sourceEventId: r.source_event_id,
747
+ originalEventDigest: r.payload_digest,
748
+ }));
749
+ }
750
+ async authorizeEvent(
751
+ tenantId: string,
752
+ stage: CommerceStage,
753
+ event: {
754
+ source: string;
755
+ sourceOrderId: string;
756
+ orderId: string;
757
+ sourceEventId: string;
758
+ },
759
+ provenance: Provenance,
760
+ ): Promise<AuthorityRow> {
761
+ const row = await this.forOrder(
762
+ tenantId,
763
+ stage,
764
+ event.source,
765
+ event.sourceOrderId,
766
+ );
767
+ if (!row) throw new SourceAuthorityError("source-authority-unavailable");
768
+ const state: State = JSON.parse(row.state_json);
769
+ const expected = state.pairs.find(
770
+ (p) => p.sourceEventId === event.sourceEventId,
771
+ );
772
+ if (
773
+ expected &&
774
+ expected.originalEventDigest !== provenance.exactOriginalEventDigest
775
+ ) {
776
+ await this.budget(
777
+ {
778
+ tenantId,
779
+ stage,
780
+ source: "foxycart",
781
+ providerStoreId: row.provider_store_id,
782
+ sourceOrderId: row.source_order_id,
783
+ },
784
+ new Date().toISOString(),
785
+ );
786
+ throw new SourceAuthorityError("control-identity-conflict");
787
+ }
788
+ if (
789
+ (row.order_id !== null && row.order_id !== event.orderId) ||
790
+ event.orderId !== event.sourceOrderId ||
791
+ state.accepted !== provenance.acceptedLedgerGeneration ||
792
+ state.businessDigest !== provenance.businessEventSetDigest ||
793
+ !state.pairs.some(
794
+ (p) =>
795
+ p.sourceEventId === event.sourceEventId &&
796
+ p.originalEventDigest === provenance.exactOriginalEventDigest,
797
+ )
798
+ )
799
+ throw new SourceAuthorityError("source-state-changed");
800
+ return row;
801
+ }
802
+ /** Part of the business commit transaction: sync reflects the exact committed immutable set. */
803
+ async businessStatements(
804
+ row: AuthorityRow,
805
+ orderId: string,
806
+ ): Promise<QueryStatement[]> {
807
+ const key: SourceKey = {
808
+ tenantId: row.tenant_id,
809
+ stage: row.stage,
810
+ source: "foxycart",
811
+ providerStoreId: row.provider_store_id,
812
+ sourceOrderId: row.source_order_id,
813
+ };
814
+ return [
815
+ {
816
+ sql: `UPDATE commerce_source_authority SET source_state_version=source_state_version+1,mutation_id=?,order_id=?,binding_digest=?,source_sync_pending=CASE WHEN (SELECT COUNT(*) FROM json_each(state_json,'$.pairs'))=(SELECT COUNT(*) FROM scrappy_commerce_outbox o WHERE o.tenant_id=commerce_source_authority.tenant_id AND o.stage=commerce_source_authority.stage AND o.source=commerce_source_authority.source AND json_extract(o.payload,'$.source.sourceOrderId')=commerce_source_authority.source_order_id) AND NOT EXISTS(SELECT 1 FROM json_each(state_json,'$.pairs') p WHERE NOT EXISTS(SELECT 1 FROM scrappy_commerce_inbox i WHERE i.tenant_id=commerce_source_authority.tenant_id AND i.stage=commerce_source_authority.stage AND i.source=commerce_source_authority.source AND i.source_event_id=json_extract(p.value,'$.sourceEventId') AND i.payload_digest=json_extract(p.value,'$.originalEventDigest'))) THEN 0 ELSE 1 END WHERE ${WHERE}`,
817
+ params: [
818
+ crypto.randomUUID(),
819
+ orderId,
820
+ await digestJson({ ...key, orderId }),
821
+ ...keyParams(key),
822
+ ],
823
+ },
824
+ {
825
+ sql: `UPDATE commerce_source_authority SET state_json=json_set(state_json,'$.version',source_state_version,'$.orderId',order_id,'$.sync',json(CASE WHEN source_sync_pending=0 THEN 'false' ELSE 'true' END)) WHERE ${WHERE}`,
826
+ params: keyParams(key),
827
+ },
828
+ ];
829
+ }
830
+ }