@tokenoftrust/storefront-runner 6.5.2 → 6.5.3

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.
@@ -1,6 +1,7 @@
1
1
  /** Operational browser client. All authority and state changes remain in the authenticated APIs. */
2
2
  import type { FulfillmentView } from "../fulfillmentService.js";
3
3
  import { parseCommerceDeskApiRoot } from "./commerceDeskRouting.js";
4
+ import { trackingUrlAllowed } from "../carrierEvidence.js";
4
5
  export function mountCommerceDesk(root: HTMLElement): void {
5
6
  const status = root.querySelector<HTMLElement>("[data-desk-status]")!;
6
7
  const rows = root.querySelector<HTMLElement>("[data-desk-orders]")!;
@@ -31,6 +32,30 @@ export function mountCommerceDesk(root: HTMLElement): void {
31
32
  parent.append(node);
32
33
  return node;
33
34
  }
35
+ /** Label and handoff state, carrier and tracking. The link is set only for an allowlisted https tracking URL. */
36
+ function renderShipment(shipment: NonNullable<FulfillmentView["shipment"]>, card: HTMLElement): void {
37
+ const block = document.createElement("div");
38
+ block.dataset.shipment = "";
39
+ card.append(block);
40
+ const state = text("p", `Label ${shipment.labelPurchaseState} · Handoff ${shipment.handoffState}`, block);
41
+ state.dataset.labelState = shipment.labelPurchaseState;
42
+ state.dataset.handoffState = shipment.handoffState;
43
+ if (shipment.carrier) {
44
+ const carrier = text("p", [shipment.carrier, shipment.service].filter(Boolean).join(" · "), block);
45
+ carrier.dataset.carrier = shipment.carrier;
46
+ }
47
+ if (shipment.trackingNumber) {
48
+ const tracking = text("a", `Tracking ${shipment.trackingNumber}`, block) as HTMLAnchorElement;
49
+ tracking.dataset.trackingNumber = shipment.trackingNumber;
50
+ tracking.rel = "noopener noreferrer";
51
+ tracking.referrerPolicy = "no-referrer";
52
+ if (trackingUrlAllowed(shipment.carrier, shipment.trackingUrl)) {
53
+ tracking.href = shipment.trackingUrl;
54
+ tracking.target = "_blank";
55
+ tracking.dataset.trackingUrl = shipment.trackingUrl;
56
+ }
57
+ }
58
+ }
34
59
  function render(order: FulfillmentView): void {
35
60
  const card = document.createElement("section");
36
61
  card.dataset.orderId = order.orderId;
@@ -43,6 +68,7 @@ export function mountCommerceDesk(root: HTMLElement): void {
43
68
  const owner = text("p", `Assigned to processor ${order.assignment.installId} · ${order.assignment.state}`, card);
44
69
  owner.dataset.assignmentState = order.assignment.state;
45
70
  }
71
+ if (order.shipment) renderShipment(order.shipment, card);
46
72
  const actions = order.batch
47
73
  ? [
48
74
  ["pick", "Confirm pick"],
@@ -21,11 +21,11 @@ import {
21
21
  type PublicCommerceEvent,
22
22
  type StockState,
23
23
  } from "@tot/public-runtime";
24
+ import { trackingHostsFor } from "../../carrierEvidence.js";
24
25
 
25
26
  export const SCRAPPY_FULFILLMENT_PATH = "/scrappy-fulfillment";
26
27
  export const WORKSPACE_PAGE_LIMIT = 4;
27
28
  export const TEST_ADAPTER_QUOTE_MINOR = 640;
28
- export const TRACKING_HOSTS = ["tracking.example"] as const;
29
29
 
30
30
  export const WORKSPACE_QUEUES = ["ready", "held", "in_progress", "shipped"] as const;
31
31
  export type WorkspaceQueue = (typeof WORKSPACE_QUEUES)[number];
@@ -98,6 +98,7 @@ interface WorkspaceOrder {
98
98
  purchaseKey: string | null;
99
99
  purchaseId: string | null;
100
100
  labelCostMinor: number | null;
101
+ carrier: string | null;
101
102
  trackingNumber: string | null;
102
103
  trackingUrl: string | null;
103
104
  ownerDestination: string;
@@ -353,6 +354,7 @@ export function initialWorkspaceState(tenantId: string, stage: CommerceStage = "
353
354
  purchaseKey: extra.purchaseKey ?? null,
354
355
  purchaseId: extra.purchaseId ?? null,
355
356
  labelCostMinor: extra.labelCostMinor ?? null,
357
+ carrier: extra.carrier ?? null,
356
358
  trackingNumber: extra.trackingNumber ?? null,
357
359
  trackingUrl: extra.trackingUrl ?? null,
358
360
  ownerDestination: `dest-${id}`,
@@ -445,8 +447,9 @@ export function initialWorkspaceState(tenantId: string, stage: CommerceStage = "
445
447
  purchaseKey: "key-shipped",
446
448
  purchaseId: "test-key-shipped",
447
449
  labelCostMinor: TEST_ADAPTER_QUOTE_MINOR,
450
+ carrier: "UPS",
448
451
  trackingNumber: "TRK-ord-shipped",
449
- trackingUrl: "https://tracking.example/label/ord-shipped",
452
+ trackingUrl: "https://www.ups.com/track?tracknum=TRKORDSHIPPED",
450
453
  }),
451
454
  ],
452
455
  events: [],
@@ -471,7 +474,7 @@ function projectOrder(state: WorkspaceState, order: WorkspaceOrder, actor: Comme
471
474
  actor,
472
475
  resource: scope,
473
476
  trackingUrl: order.trackingUrl,
474
- allowedHosts: TRACKING_HOSTS,
477
+ allowedHosts: trackingHostsFor(order.carrier),
475
478
  });
476
479
  if (admitted.ok) trackingUrl = order.trackingUrl;
477
480
  }
@@ -740,8 +743,9 @@ function applyToOrder(state: WorkspaceState, order: WorkspaceOrder, command: Wor
740
743
  });
741
744
  if (!decision.admit) return refuse(decision.reason);
742
745
  order.fulfillment = "handed_off";
746
+ order.carrier = "UPS";
743
747
  order.trackingNumber = `TRK-${order.id}`;
744
- order.trackingUrl = `https://tracking.example/label/${encodeURIComponent(order.id)}`;
748
+ order.trackingUrl = `https://www.ups.com/track?tracknum=${encodeURIComponent(order.id)}`;
745
749
  return finish({ ...blankResult(command.command, order.id, "admitted", "handed-off"), purchaseId: order.purchaseId });
746
750
  }
747
751
 
@@ -0,0 +1,245 @@
1
+ /**
2
+ * The processor's shipment-evidence contract: the closed request schema, the minimized response,
3
+ * the active-install and active-assignment predicate every app write carries in its first
4
+ * statement, and the idempotency record a replay returns.
5
+ */
6
+ import type { LabelState } from "@tot/public-runtime";
7
+ import { PROCESSOR_SCOPE } from "./assignmentStore.js";
8
+ import type { SqlStatement } from "./store.js";
9
+ import { CARRIER_SERVICE, TRACKING_NUMBER, parseCarrierCode, trackingUrlAllowed, type CarrierCode } from "./carrierEvidence.js";
10
+
11
+ /** The calling install and the assignment it writes under. Every app write requires both. */
12
+ export interface AppAssignmentBinding {
13
+ readonly installId: string;
14
+ readonly assignmentId: string;
15
+ }
16
+
17
+ export type AdultSignatureResult = "accepted" | "refused" | "absent";
18
+
19
+ export interface LabelOutcomeEvidence {
20
+ kind: "label-outcome";
21
+ assignmentId: string;
22
+ purchaseId: string;
23
+ outcome: "purchased" | "unknown" | "voided";
24
+ carrier?: CarrierCode;
25
+ service?: string;
26
+ trackingNumber?: string;
27
+ trackingUrl?: string;
28
+ adultSignature: AdultSignatureResult;
29
+ expectedVersion?: number;
30
+ }
31
+
32
+ export interface HandoffEvidence {
33
+ kind: "handoff";
34
+ assignmentId: string;
35
+ purchaseId: string;
36
+ carrier: CarrierCode;
37
+ expectedVersion?: number;
38
+ }
39
+
40
+ export type ShipmentEvidence = LabelOutcomeEvidence | HandoffEvidence;
41
+
42
+ export interface AppShipmentView {
43
+ orderId: string;
44
+ version: number;
45
+ label: LabelState;
46
+ handoff: "none" | "handed_off" | "unknown";
47
+ purchaseId: string;
48
+ carrier?: CarrierCode;
49
+ tracking?: { number: string; url: string };
50
+ }
51
+
52
+ export interface AppRefusalBody {
53
+ code: string;
54
+ error: string;
55
+ holds?: readonly string[];
56
+ }
57
+
58
+ export interface AppEvidenceResponse {
59
+ status: number;
60
+ body: AppShipmentView | AppRefusalBody;
61
+ }
62
+
63
+ /** The facts a label-outcome audit row stores, read back by the desk and the app view. */
64
+ export interface StoredLabelFacts {
65
+ outcome: LabelOutcomeEvidence["outcome"] | null;
66
+ adultSignature: AdultSignatureResult;
67
+ carrier: CarrierCode | null;
68
+ service: string | null;
69
+ trackingNumber: string | null;
70
+ trackingUrl: string | null;
71
+ }
72
+
73
+ const OPAQUE = /^[A-Za-z0-9._:-]{1,128}$/;
74
+ export const IDEMPOTENCY_KEY = /^[\x21-\x7e]{8,128}$/;
75
+ const LABEL_KEYS = new Set([
76
+ "kind",
77
+ "assignmentId",
78
+ "purchaseId",
79
+ "outcome",
80
+ "carrier",
81
+ "service",
82
+ "trackingNumber",
83
+ "trackingUrl",
84
+ "adultSignature",
85
+ "expectedVersion",
86
+ ]);
87
+ const HANDOFF_KEYS = new Set(["kind", "assignmentId", "purchaseId", "carrier", "expectedVersion"]);
88
+ const PURCHASED_REQUIRES = ["carrier", "service", "trackingNumber", "trackingUrl", "adultSignature"] as const;
89
+
90
+ function opaque(value: unknown): value is string {
91
+ return typeof value === "string" && OPAQUE.test(value) && !value.includes("@");
92
+ }
93
+
94
+ function isRecord(value: unknown): value is Record<string, unknown> {
95
+ return typeof value === "object" && value !== null && !Array.isArray(value);
96
+ }
97
+
98
+ /** The body in the closed schema, or null (400 malformed): unknown keys, a null value, unknown carrier, unsafe URL. */
99
+ export function parseShipmentEvidence(body: unknown): ShipmentEvidence | null {
100
+ if (!isRecord(body)) return null;
101
+ const keys = body.kind === "label-outcome" ? LABEL_KEYS : body.kind === "handoff" ? HANDOFF_KEYS : null;
102
+ if (!keys || Object.keys(body).some((key) => !keys.has(key))) return null;
103
+ if (Object.values(body).some((value) => value === null)) return null;
104
+ if (!opaque(body.assignmentId) || !opaque(body.purchaseId)) return null;
105
+ if (body.expectedVersion !== undefined && (!Number.isSafeInteger(body.expectedVersion) || Number(body.expectedVersion) < 0)) return null;
106
+ const expected = body.expectedVersion === undefined ? {} : { expectedVersion: Number(body.expectedVersion) };
107
+ if (body.kind === "handoff") {
108
+ const carrier = parseCarrierCode(body.carrier);
109
+ if (!carrier) return null;
110
+ return { kind: "handoff", assignmentId: body.assignmentId, purchaseId: body.purchaseId, carrier, ...expected };
111
+ }
112
+ if (body.outcome !== "purchased" && body.outcome !== "unknown" && body.outcome !== "voided") return null;
113
+ if (body.outcome === "purchased" && PURCHASED_REQUIRES.some((key) => body[key] === undefined)) return null;
114
+ const carrier = body.carrier === undefined ? undefined : parseCarrierCode(body.carrier);
115
+ if (carrier === null) return null;
116
+ if (body.service !== undefined && (typeof body.service !== "string" || !CARRIER_SERVICE.test(body.service))) return null;
117
+ if (body.trackingNumber !== undefined && (typeof body.trackingNumber !== "string" || !TRACKING_NUMBER.test(body.trackingNumber))) return null;
118
+ if (body.trackingUrl !== undefined && !trackingUrlAllowed(carrier, body.trackingUrl)) return null;
119
+ const adult = body.adultSignature ?? "absent";
120
+ if (adult !== "accepted" && adult !== "refused" && adult !== "absent") return null;
121
+ return {
122
+ kind: "label-outcome",
123
+ assignmentId: body.assignmentId,
124
+ purchaseId: body.purchaseId,
125
+ outcome: body.outcome,
126
+ adultSignature: adult,
127
+ ...(carrier ? { carrier } : {}),
128
+ ...(typeof body.service === "string" ? { service: body.service } : {}),
129
+ ...(typeof body.trackingNumber === "string" ? { trackingNumber: body.trackingNumber } : {}),
130
+ ...(typeof body.trackingUrl === "string" ? { trackingUrl: body.trackingUrl } : {}),
131
+ ...expected,
132
+ };
133
+ }
134
+
135
+ /** The label-outcome audit `to_state`: closed keys, absent facts omitted. */
136
+ export function labelOutcomeRecord(evidence: LabelOutcomeEvidence): Record<string, string> {
137
+ return {
138
+ outcome: evidence.outcome,
139
+ adultSignature: evidence.adultSignature,
140
+ ...(evidence.carrier ? { carrier: evidence.carrier } : {}),
141
+ ...(evidence.service ? { service: evidence.service } : {}),
142
+ ...(evidence.trackingNumber ? { trackingNumber: evidence.trackingNumber } : {}),
143
+ ...(evidence.trackingUrl ? { trackingUrl: evidence.trackingUrl } : {}),
144
+ };
145
+ }
146
+
147
+ /** A stored label-outcome audit `to_state`, re-validated on read. Facts that fail their shape read as absent. */
148
+ export function parseStoredLabelFacts(value: unknown): StoredLabelFacts | null {
149
+ if (!isRecord(value)) return null;
150
+ if (value.adultSignature !== "accepted" && value.adultSignature !== "refused" && value.adultSignature !== "absent") return null;
151
+ const carrier = parseCarrierCode(value.carrier);
152
+ const outcome = value.outcome === "purchased" || value.outcome === "unknown" || value.outcome === "voided" ? value.outcome : null;
153
+ return {
154
+ outcome,
155
+ adultSignature: value.adultSignature,
156
+ carrier,
157
+ service: typeof value.service === "string" && CARRIER_SERVICE.test(value.service) ? value.service : null,
158
+ trackingNumber: typeof value.trackingNumber === "string" && TRACKING_NUMBER.test(value.trackingNumber) ? value.trackingNumber : null,
159
+ trackingUrl: trackingUrlAllowed(carrier, value.trackingUrl) ? value.trackingUrl : null,
160
+ };
161
+ }
162
+
163
+ /** True when a recorded purchase carries exactly these facts. */
164
+ export function sameLabelFacts(stored: StoredLabelFacts | null, evidence: LabelOutcomeEvidence): boolean {
165
+ return (
166
+ stored !== null &&
167
+ stored.adultSignature === evidence.adultSignature &&
168
+ stored.carrier === (evidence.carrier ?? null) &&
169
+ stored.service === (evidence.service ?? null) &&
170
+ stored.trackingNumber === (evidence.trackingNumber ?? null) &&
171
+ stored.trackingUrl === (evidence.trackingUrl ?? null)
172
+ );
173
+ }
174
+
175
+ /**
176
+ * SQL condition (no leading AND) every app write carries in statements[0]: the calling install is
177
+ * active on the host tenant with the processor scope, and the bound assignment of this order names
178
+ * it and is active.
179
+ */
180
+ export function appWritePredicate(
181
+ binding: AppAssignmentBinding,
182
+ scope: { tenantId: string; stage: string; orderId: string },
183
+ ): SqlStatement {
184
+ return {
185
+ sql: `EXISTS (
186
+ SELECT 1 FROM app_installs AS ai
187
+ WHERE ai.install_id = ? AND ai.tenant_id = ? AND ai.status = 'active'
188
+ AND EXISTS (SELECT 1 FROM json_each(ai.scopes) WHERE json_each.value = '${PROCESSOR_SCOPE}')
189
+ ) AND EXISTS (
190
+ SELECT 1 FROM fulfillment_assignments AS fa
191
+ WHERE fa.assignment_id = ? AND fa.tenant_id = ? AND fa.stage = ? AND fa.order_id = ?
192
+ AND fa.install_id = ? AND fa.state = 'active'
193
+ )`,
194
+ params: [
195
+ binding.installId,
196
+ scope.tenantId,
197
+ binding.assignmentId,
198
+ scope.tenantId,
199
+ scope.stage,
200
+ scope.orderId,
201
+ binding.installId,
202
+ ],
203
+ };
204
+ }
205
+
206
+ const STATUS: Record<string, number> = {
207
+ malformed: 400,
208
+ invalid_token: 401,
209
+ scope_denied: 403,
210
+ denied: 403,
211
+ "not-assigned": 404,
212
+ unavailable: 503,
213
+ };
214
+
215
+ /** A refusal response: `{ code, error }`, plus `holds` on a handoff refusal. */
216
+ export function appRefusal(code: string, holds?: readonly string[]): AppEvidenceResponse {
217
+ if (code === "invalid_token") return { status: 401, body: { error: "Unauthorized", code } };
218
+ const body: AppRefusalBody = holds && holds.length > 0 ? { code, error: code, holds: [...holds] } : { code, error: code };
219
+ return { status: STATUS[code] ?? 409, body };
220
+ }
221
+
222
+ /** The idempotency record: the response status and body a same-key, same-body retry returns. */
223
+ export function idempotencyRecord(response: AppEvidenceResponse): string {
224
+ return JSON.stringify({ status: response.status, body: response.body });
225
+ }
226
+
227
+ /** A recorded response, or null when the record is not one this route wrote. */
228
+ export function replayedResponse(recorded: unknown): AppEvidenceResponse | null {
229
+ if (!isRecord(recorded) || !Number.isSafeInteger(recorded.status) || !isRecord(recorded.body)) return null;
230
+ return { status: Number(recorded.status), body: recorded.body as unknown as AppShipmentView | AppRefusalBody };
231
+ }
232
+
233
+ /** The batch key insert: a plain INSERT, so a same-key race rolls the whole batch back. */
234
+ export function idempotencyInsertStatement(input: {
235
+ installId: string;
236
+ key: string;
237
+ bodyHash: string;
238
+ response: AppEvidenceResponse;
239
+ now: string;
240
+ }): SqlStatement {
241
+ return {
242
+ sql: `INSERT INTO idempotency_keys (install_id, idempotency_key, body_hash, response_body, created_at) VALUES (?, ?, ?, ?, ?)`,
243
+ params: [input.installId, input.key, input.bodyHash, idempotencyRecord(input.response), input.now],
244
+ };
245
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Carrier evidence a fulfillment processor reports: a closed set of canonical carrier codes and,
3
+ * per carrier, the tracking hosts a tracking URL may name. The same allowlist validates an app's
4
+ * write and gates the merchant desk's tracking link. Tracking URLs are never fetched.
5
+ */
6
+ export const CARRIER_CODES = ["UPS", "FEDEX", "USPS", "DHL"] as const;
7
+ export type CarrierCode = (typeof CARRIER_CODES)[number];
8
+
9
+ export const CARRIER_TRACKING_HOSTS: Readonly<Record<CarrierCode, readonly string[]>> = {
10
+ UPS: ["www.ups.com"],
11
+ FEDEX: ["www.fedex.com"],
12
+ USPS: ["tools.usps.com"],
13
+ DHL: ["www.dhl.com"],
14
+ };
15
+
16
+ export const TRACKING_NUMBER = /^[A-Z0-9]{6,40}$/;
17
+ export const CARRIER_SERVICE = /^[A-Z0-9_]{1,40}$/;
18
+ export const TRACKING_URL_MAX = 2048;
19
+ export const TRACKING_PART_MAX = 512;
20
+
21
+ /** The canonical form every carrier comparison uses: trimmed and uppercased. */
22
+ export function canonicalCarrier(value: string): string {
23
+ return value.trim().toUpperCase();
24
+ }
25
+
26
+ /** A supplied carrier as its canonical code, or null when it is not in the closed set. */
27
+ export function parseCarrierCode(value: unknown): CarrierCode | null {
28
+ if (typeof value !== "string") return null;
29
+ const canonical = canonicalCarrier(value);
30
+ return (CARRIER_CODES as readonly string[]).includes(canonical) ? (canonical as CarrierCode) : null;
31
+ }
32
+
33
+ /** The tracking hosts allowed for `carrier`; none for a value outside the closed set. */
34
+ export function trackingHostsFor(carrier: unknown): readonly string[] {
35
+ const code = parseCarrierCode(carrier);
36
+ return code ? CARRIER_TRACKING_HOSTS[code] : [];
37
+ }
38
+
39
+ /**
40
+ * True when `url` is an https URL on one of `carrier`'s tracking hosts: at most 2048 characters,
41
+ * printable ASCII only, the authority exactly an allowed host (so no userinfo, port or IP literal),
42
+ * no fragment, and a path and query of at most 512 characters each.
43
+ */
44
+ export function trackingUrlAllowed(carrier: unknown, url: unknown): url is string {
45
+ if (typeof url !== "string" || url.length > TRACKING_URL_MAX) return false;
46
+ if (!/^[\x21-\x7e]+$/.test(url) || url.includes("#") || url.includes("\\")) return false;
47
+ if (!url.startsWith("https://")) return false;
48
+ const authority = url.slice("https://".length).split(/[/?]/, 1)[0] ?? "";
49
+ if (!trackingHostsFor(carrier).includes(authority)) return false;
50
+ let parsed: URL;
51
+ try {
52
+ parsed = new URL(url);
53
+ } catch {
54
+ return false;
55
+ }
56
+ if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "" || parsed.port !== "") return false;
57
+ if (parsed.host !== authority || parsed.hash !== "") return false;
58
+ return parsed.pathname.length <= TRACKING_PART_MAX && parsed.search.replace(/^\?/, "").length <= TRACKING_PART_MAX;
59
+ }
60
+
61
+ /** True when `carrier` is in either prohibited set, both compared in canonical form. */
62
+ export function carrierProhibited(carrier: string, ...prohibited: ReadonlyArray<readonly string[]>): boolean {
63
+ const canonical = canonicalCarrier(carrier);
64
+ return prohibited.some((set) => set.some((item) => canonicalCarrier(item) === canonical));
65
+ }
@@ -38,6 +38,7 @@ import { readViewerSession } from "@/lib/access/auth/route";
38
38
  import type { SessionRecord } from "@/lib/access/auth/session";
39
39
  import {
40
40
  canRelease,
41
+ carrierEvidenceRefusal,
41
42
  carrierPolicyForTenant,
42
43
  carrierSelectionRefusal,
43
44
  handoffRefusal,
@@ -52,6 +53,7 @@ import {
52
53
  type SqlStatement,
53
54
  } from "./store.js";
54
55
  import {
56
+ PROCESSOR_SCOPE,
55
57
  assignmentAtGate,
56
58
  insertAssignmentStatement,
57
59
  moveAssignmentStatement,
@@ -70,6 +72,24 @@ import {
70
72
  import { dispatchCommerceDeliveries } from "./commerceRuntime.js";
71
73
  import type { CommerceTopic } from "../integrations/webhooks/commercePayloads.js";
72
74
  import { claimedByEventGate, handoffClaimStatement } from "../integrations/webhooks/orderFulfilledClaim.js";
75
+ import { checkIdempotency, computeBodyHash } from "../integrations/apps/orders/idempotency.js";
76
+ import type { Queryable } from "../catalog/d1/catalog.js";
77
+ import {
78
+ IDEMPOTENCY_KEY,
79
+ appRefusal,
80
+ appWritePredicate,
81
+ idempotencyInsertStatement,
82
+ labelOutcomeRecord,
83
+ parseStoredLabelFacts,
84
+ replayedResponse,
85
+ sameLabelFacts,
86
+ type AppAssignmentBinding,
87
+ type AppEvidenceResponse,
88
+ type AppShipmentView,
89
+ type HandoffEvidence,
90
+ type LabelOutcomeEvidence,
91
+ type StoredLabelFacts,
92
+ } from "./appShipmentEvidence.js";
73
93
 
74
94
  export const OPAQUE_ID = /^[A-Za-z0-9._:-]{1,128}$/;
75
95
 
@@ -121,6 +141,11 @@ export interface ShipmentIntentView {
121
141
  carrierAccountId: string | null;
122
142
  adultSignatureRequired: boolean;
123
143
  prohibitedCarriers: string[];
144
+ /** The recorded label's carrier, service and tracking, as the processor reported them. */
145
+ carrier: string | null;
146
+ service: string | null;
147
+ trackingNumber: string | null;
148
+ trackingUrl: string | null;
124
149
  }
125
150
 
126
151
  export interface FulfillmentView {
@@ -201,7 +226,7 @@ interface LoadedOrder {
201
226
  packingSlip: PackingSlip | null;
202
227
  shipment: { labelPurchaseState: string; handoffState: string } | null;
203
228
  provider: { purchaseId: string; outcome: ProviderOutcome } | null;
204
- labelOutcome: { adultSignature: "accepted" | "refused" | "absent" } | null;
229
+ labelOutcome: StoredLabelFacts | null;
205
230
  carrierAccountId: string | null;
206
231
  prohibitedCarriers: string[];
207
232
  adultSignatureRequired: boolean;
@@ -230,6 +255,27 @@ type CommitPlan =
230
255
  assignment?: boolean;
231
256
  };
232
257
 
258
+ /** One app write: the install actor (built only by the route), the order, the key, the raw and parsed body. */
259
+ export interface AppEvidenceRequest<T> {
260
+ actor: FulfillmentActor;
261
+ orderId: string;
262
+ idempotencyKey: string;
263
+ body: unknown;
264
+ evidence: T;
265
+ }
266
+
267
+ type AppPlan =
268
+ | { type: "done"; view: AppShipmentView }
269
+ | { type: "refuse"; code: string; holds?: readonly string[] }
270
+ | {
271
+ type: "write";
272
+ statements: SqlStatement[];
273
+ sourceAdvance: boolean;
274
+ deliveries: readonly PlannedDelivery[];
275
+ /** The planned post-state: the response and the idempotency record. */
276
+ view: AppShipmentView;
277
+ };
278
+
233
279
  /** Statement and delivery accumulator for a plan that may carry commerce events. */
234
280
  interface PlanStatements {
235
281
  statements: SqlStatement[];
@@ -955,66 +1001,306 @@ export class FulfillmentService {
955
1001
  return { type: "refuse", code: "label" };
956
1002
  }
957
1003
  const version = state.order.eventVersion + 1;
1004
+ const plan: PlanStatements = { deliveries: [], statements: this.handoffStatements(state, actor.actorId, version) };
1005
+ await this.appendOrderFulfilled(plan, state, version);
1006
+ return { type: "write", minimumChanges: 1, sourceAdvance: true, statements: plan.statements, deliveries: plan.deliveries };
1007
+ });
1008
+ }
1009
+
1010
+ /**
1011
+ * The processor's label outcome under its assignment, from `app:<installId>` only. The first
1012
+ * outcome opens the whole-order shipment and its label operation under the platform purchase id.
1013
+ */
1014
+ async appRecordLabelOutcome(request: AppEvidenceRequest<LabelOutcomeEvidence>): Promise<AppEvidenceResponse> {
1015
+ const evidence = request.evidence;
1016
+ return this.appCommit(request, async (state, binding, purchaseId) => {
1017
+ if (evidence.purchaseId !== purchaseId || (state.provider && state.provider.purchaseId !== purchaseId)) {
1018
+ return { type: "refuse", code: "conflict" };
1019
+ }
1020
+ const current = state.shipment?.labelPurchaseState ?? "none";
1021
+ const unchanged = { type: "done", view: appShipmentView(state, purchaseId) } as const;
1022
+ if (current === "purchased") {
1023
+ if (evidence.outcome !== "purchased") return { type: "refuse", code: "label-regress" };
1024
+ return sameLabelFacts(state.labelOutcome, evidence) ? unchanged : { type: "refuse", code: "conflict" };
1025
+ }
1026
+ if (current === "voided") return evidence.outcome === "voided" ? unchanged : { type: "refuse", code: "label-regress" };
1027
+ if (current === "unknown" && evidence.outcome === "unknown") return unchanged;
1028
+ if (state.order.fulfillment === "handed_off") return { type: "refuse", code: "fulfillment" };
1029
+ if (evidence.carrier) {
1030
+ const carrier = carrierEvidenceRefusal({ carrier: evidence.carrier, policy: this.policy, snapshotProhibited: state.prohibitedCarriers });
1031
+ if (carrier) return { type: "refuse", code: carrier };
1032
+ }
1033
+ const mapped = labelOutcomeColumns(evidence.outcome);
1034
+ const version = state.order.eventVersion + 1;
1035
+ const { order } = state;
1036
+ const actorId = request.actor.actorId;
1037
+ let n = state.auditCount;
1038
+ const nextStamp = () => stamp(this.now(), ++n);
1039
+ const predicate = appWritePredicate(binding, order);
1040
+ const statements: SqlStatement[] = [
1041
+ {
1042
+ sql: `UPDATE scrappy_commerce_orders
1043
+ SET label_state = ?, event_version = event_version + 1
1044
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
1045
+ AND fulfillment_state != 'handed_off' AND label_state = ?
1046
+ AND ${predicate.sql}`,
1047
+ params: [mapped.order, order.tenantId, order.stage, order.orderId, order.eventVersion, order.label, ...predicate.params],
1048
+ },
1049
+ ];
1050
+ if (!state.shipment) statements.push(...(await this.intentStatements(state, actorId, version, nextStamp, { assigned: true })));
1051
+ statements.push(
1052
+ {
1053
+ sql: `UPDATE scrappy_shipments
1054
+ SET label_purchase_state = ?
1055
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND handoff_state = 'none'
1056
+ AND EXISTS (
1057
+ SELECT 1 FROM scrappy_commerce_orders
1058
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
1059
+ )`,
1060
+ params: [mapped.shipment, order.tenantId, order.stage, order.orderId, order.tenantId, order.stage, order.orderId, version],
1061
+ },
1062
+ {
1063
+ sql: `UPDATE scrappy_provider_operations
1064
+ SET outcome = ?
1065
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND kind = 'label_purchase'
1066
+ AND provider = 'unselected' AND purchase_id = ?
1067
+ AND EXISTS (
1068
+ SELECT 1 FROM scrappy_commerce_orders
1069
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
1070
+ )`,
1071
+ params: [mapped.provider, order.tenantId, order.stage, order.orderId, purchaseId, order.tenantId, order.stage, order.orderId, version],
1072
+ },
1073
+ auditStatement(state, actorId, "label-outcome", order.label, jsonText(labelOutcomeRecord(evidence)), version, nextStamp()),
1074
+ );
1075
+ return {
1076
+ type: "write",
1077
+ statements,
1078
+ sourceAdvance: evidence.outcome === "purchased",
1079
+ deliveries: [],
1080
+ view: appShipmentView(state, purchaseId, { version, label: mapped.order, facts: parseStoredLabelFacts(labelOutcomeRecord(evidence)) }),
1081
+ };
1082
+ });
1083
+ }
1084
+
1085
+ /** The processor's confirmed handoff under its assignment: every desk handoff gate, the label carrier, one order.fulfilled. */
1086
+ async appConfirmHandoff(request: AppEvidenceRequest<HandoffEvidence>): Promise<AppEvidenceResponse> {
1087
+ const evidence = request.evidence;
1088
+ return this.appCommit(request, async (state, binding, purchaseId) => {
1089
+ if (evidence.purchaseId !== purchaseId || (state.provider && state.provider.purchaseId !== purchaseId)) {
1090
+ return { type: "refuse", code: "conflict" };
1091
+ }
1092
+ const labelCarrier = state.labelOutcome?.carrier ?? null;
1093
+ if (state.shipment?.handoffState === "handed_off" || state.order.fulfillment === "handed_off") {
1094
+ return labelCarrier === evidence.carrier
1095
+ ? { type: "done", view: appShipmentView(state, purchaseId) }
1096
+ : { type: "refuse", code: "carrier-mismatch" };
1097
+ }
1098
+ const prohibited = carrierEvidenceRefusal({ carrier: evidence.carrier, policy: this.policy, snapshotProhibited: state.prohibitedCarriers });
1099
+ if (prohibited) return { type: "refuse", code: prohibited };
1100
+ const readiness = this.readiness(state);
1101
+ const code = handoffRefusal(readiness);
1102
+ if (code) return { type: "refuse", code, holds: releaseHolds(readiness) };
1103
+ if (state.shipment?.labelPurchaseState !== "purchased" || state.provider?.outcome !== "accepted") return { type: "refuse", code: "label" };
1104
+ const mismatch = carrierEvidenceRefusal({
1105
+ carrier: evidence.carrier,
1106
+ policy: this.policy,
1107
+ snapshotProhibited: state.prohibitedCarriers,
1108
+ labelCarrier,
1109
+ });
1110
+ if (mismatch) return { type: "refuse", code: mismatch };
1111
+ const version = state.order.eventVersion + 1;
1112
+ const [first, ...rest] = this.handoffStatements(state, request.actor.actorId, version);
1113
+ const predicate = appWritePredicate(binding, state.order);
958
1114
  const plan: PlanStatements = {
959
1115
  deliveries: [],
960
- statements: [
961
- {
962
- sql: `UPDATE scrappy_commerce_orders
963
- SET fulfillment_state = 'handed_off', event_version = event_version + 1
964
- WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
965
- AND payment_state = 'captured' AND stock_state = 'confirmed'
966
- AND fulfillment_state = 'ready' AND label_state IN ('purchased', 'printed')
967
- AND EXISTS (
968
- SELECT 1 FROM scrappy_shipments
969
- WHERE tenant_id = ? AND stage = ? AND order_id = ?
970
- AND scope = 'whole-order' AND label_purchase_state = 'purchased' AND handoff_state = 'none'
971
- )`,
972
- params: [
973
- state.order.tenantId,
974
- state.order.stage,
975
- state.order.orderId,
976
- state.order.eventVersion,
977
- state.order.tenantId,
978
- state.order.stage,
979
- state.order.orderId,
980
- ],
981
- },
982
- {
983
- sql: `UPDATE scrappy_shipments
984
- SET handoff_state = 'handed_off'
985
- WHERE tenant_id = ? AND stage = ? AND order_id = ?
986
- AND scope = 'whole-order' AND label_purchase_state = 'purchased' AND handoff_state = 'none'
987
- AND EXISTS (
988
- SELECT 1 FROM scrappy_commerce_orders
989
- WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
990
- )`,
991
- params: [
992
- state.order.tenantId,
993
- state.order.stage,
994
- state.order.orderId,
995
- state.order.tenantId,
996
- state.order.stage,
997
- state.order.orderId,
998
- version,
999
- ],
1000
- },
1001
- auditStatement(state, actor.actorId, "transition", state.order.fulfillment, "handed_off", version, stamp(this.now(), state.auditCount + 1)),
1002
- terminalTransitionExpiryStatement(state.order, version, new Date(this.now())),
1003
- ],
1116
+ statements: [{ sql: `${first!.sql} AND ${predicate.sql}`, params: [...first!.params, ...predicate.params] }, ...rest],
1004
1117
  };
1005
1118
  await this.appendOrderFulfilled(plan, state, version);
1006
- return { type: "write", minimumChanges: 1, sourceAdvance: true, statements: plan.statements, deliveries: plan.deliveries };
1119
+ return {
1120
+ type: "write",
1121
+ statements: plan.statements,
1122
+ sourceAdvance: true,
1123
+ deliveries: plan.deliveries,
1124
+ view: appShipmentView(state, purchaseId, { version, handoff: "handed_off" }),
1125
+ };
1007
1126
  });
1008
1127
  }
1009
1128
 
1129
+ /**
1130
+ * The app write path: the key ledger first, the assignment binding, the plan, then one batch whose
1131
+ * first statement carries the install and assignment predicate and whose last records the key.
1132
+ * A plan-time answer records the key alone. A batch that does not commit is re-classified.
1133
+ */
1134
+ private async appCommit(
1135
+ request: AppEvidenceRequest<LabelOutcomeEvidence | HandoffEvidence>,
1136
+ build: (state: LoadedOrder, binding: AppAssignmentBinding, purchaseId: string) => AppPlan | Promise<AppPlan>,
1137
+ ): Promise<AppEvidenceResponse> {
1138
+ const binding = appBinding(request.actor, request.evidence.assignmentId);
1139
+ if (!binding) return appRefusal("denied");
1140
+ if (!IDEMPOTENCY_KEY.test(request.idempotencyKey)) return appRefusal("malformed");
1141
+ const { tenantId, stage } = request.actor;
1142
+ const { orderId, idempotencyKey: key } = request;
1143
+ if (!isOpaqueId(orderId)) return appRefusal("not-assigned");
1144
+ const bodyHash = await computeBodyHash({ body: request.body, assignmentId: binding.assignmentId, orderId });
1145
+ const prior = await this.priorResponse(binding.installId, key, bodyHash);
1146
+ if (prior) return prior;
1147
+ const state = await this.load(tenantId, stage, orderId);
1148
+ const assigned = await this.assignmentRefusal(binding, tenantId, stage, orderId, state?.assignment ?? null);
1149
+ if (assigned || !state) return appRefusal(assigned ?? "not-assigned");
1150
+ const expected = request.evidence.expectedVersion;
1151
+ if (expected !== undefined && state.order.eventVersion !== expected) return appRefusal("conflict");
1152
+ const purchaseId = await purchaseIdFor(tenantId, stage, orderId);
1153
+ let plan: AppPlan;
1154
+ try {
1155
+ plan = await build(state, binding, purchaseId);
1156
+ } catch (error) {
1157
+ if (error instanceof CommercePayloadRefused) return appRefusal("malformed");
1158
+ throw error;
1159
+ }
1160
+ if (plan.type === "done") return this.recordAppAnswer(binding.installId, key, bodyHash, { status: 200, body: plan.view });
1161
+ if (plan.type === "refuse") return this.recordAppAnswer(binding.installId, key, bodyHash, appRefusal(plan.code, plan.holds));
1162
+ const response: AppEvidenceResponse = { status: 200, body: plan.view };
1163
+ const fence = authorityFence(state.sourceAuthority, this.now(), plan.sourceAdvance);
1164
+ const [first, ...rest] = plan.statements;
1165
+ if (!first) return appRefusal("unavailable");
1166
+ const changes = await this.write([
1167
+ { sql: `${first.sql} AND (${fence.sql})`, params: [...first.params, ...fence.params] },
1168
+ ...rest,
1169
+ idempotencyInsertStatement({ installId: binding.installId, key, bodyHash, response, now: this.now() }),
1170
+ ]);
1171
+ if (changes && (changes[0] ?? 0) >= 1) {
1172
+ await this.notifyCommitted(tenantId, plan.deliveries, changes);
1173
+ return response;
1174
+ }
1175
+ return this.classifyUncommitted(binding, state, plan.sourceAdvance, key, bodyHash, changes === null);
1176
+ }
1177
+
1178
+ /** Why an app batch did not commit, read once after it: install, scope, assignment, authority, key, version. */
1179
+ private async classifyUncommitted(
1180
+ binding: AppAssignmentBinding,
1181
+ original: LoadedOrder,
1182
+ sourceAdvance: boolean,
1183
+ key: string,
1184
+ bodyHash: string,
1185
+ failed: boolean,
1186
+ ): Promise<AppEvidenceResponse> {
1187
+ const { tenantId, stage, orderId } = original.order;
1188
+ const install = await this.db.get<{ status: string; scopes: string }>(
1189
+ `SELECT status, scopes FROM app_installs WHERE install_id = ? AND tenant_id = ?`,
1190
+ [binding.installId, tenantId],
1191
+ );
1192
+ if (!install || install.status !== "active") return appRefusal("invalid_token");
1193
+ if (!scopeListIncludes(install.scopes, PROCESSOR_SCOPE)) return appRefusal("scope_denied");
1194
+ const current = await this.load(tenantId, stage, orderId);
1195
+ const assigned = await this.assignmentRefusal(binding, tenantId, stage, orderId, current?.assignment ?? null);
1196
+ if (assigned || !current) return appRefusal(assigned ?? "not-assigned");
1197
+ const fence = authorityFence(original.sourceAuthority, this.now(), sourceAdvance);
1198
+ const fenced = await this.db.get<{ ok: number }>(
1199
+ `SELECT 1 AS ok FROM scrappy_commerce_orders WHERE tenant_id = ? AND stage = ? AND order_id = ? AND (${fence.sql})`,
1200
+ [tenantId, stage, orderId, ...fence.params],
1201
+ );
1202
+ if (!fenced) return appRefusal(current.sourceHolds[0] ?? "source-check-pending");
1203
+ const prior = await this.priorResponse(binding.installId, key, bodyHash);
1204
+ if (prior) return prior;
1205
+ return appRefusal(failed ? "unavailable" : "conflict");
1206
+ }
1207
+
1208
+ /** A same-key request already answered: its recorded response, or idempotency-conflict for another body. */
1209
+ private async priorResponse(installId: string, key: string, bodyHash: string): Promise<AppEvidenceResponse | null> {
1210
+ const check = await checkIdempotency(this.ledger(), installId, key, bodyHash);
1211
+ if (check.status === "proceed") return null;
1212
+ if (check.status === "conflict") return appRefusal("idempotency-conflict");
1213
+ return replayedResponse(check.responseBody) ?? appRefusal("idempotency-conflict");
1214
+ }
1215
+
1216
+ /** Records a plan-time answer under its key; a same-key race answers with whatever won. */
1217
+ private async recordAppAnswer(installId: string, key: string, bodyHash: string, response: AppEvidenceResponse): Promise<AppEvidenceResponse> {
1218
+ let changes: number[];
1219
+ try {
1220
+ changes = await this.db.batch([idempotencyInsertStatement({ installId, key, bodyHash, response, now: this.now() })]);
1221
+ } catch {
1222
+ return appRefusal("unavailable");
1223
+ }
1224
+ if ((changes[0] ?? 0) >= 1) return response;
1225
+ return (await this.priorResponse(installId, key, bodyHash)) ?? appRefusal("unavailable");
1226
+ }
1227
+
1228
+ /** The order's assignment as the calling install sees it: null when its bound assignment is active. */
1229
+ private async assignmentRefusal(
1230
+ binding: AppAssignmentBinding,
1231
+ tenantId: string,
1232
+ stage: CommerceStage,
1233
+ orderId: string,
1234
+ open: OpenAssignment | null,
1235
+ ): Promise<"not-assigned" | "assignment-ended" | "assignment-held" | null> {
1236
+ if (open && open.installId === binding.installId) {
1237
+ if (open.assignmentId !== binding.assignmentId) return "assignment-ended";
1238
+ return open.state === "held" ? "assignment-held" : null;
1239
+ }
1240
+ const named = await this.db.get<{ n: number }>(
1241
+ `SELECT 1 AS n FROM fulfillment_assignments WHERE tenant_id = ? AND stage = ? AND order_id = ? AND install_id = ? LIMIT 1`,
1242
+ [tenantId, stage, orderId, binding.installId],
1243
+ );
1244
+ return named ? "assignment-ended" : "not-assigned";
1245
+ }
1246
+
1247
+ /** The idempotency ledger over this store. */
1248
+ private ledger(): Queryable {
1249
+ const db = this.db;
1250
+ return {
1251
+ all: async <T,>(sql: string, ...params: unknown[]) => (await db.all(sql, params)) as T[],
1252
+ first: async <T,>(sql: string, ...params: unknown[]) => ((await db.get(sql, params)) ?? null) as T | null,
1253
+ run: async (sql: string, ...params: unknown[]) => {
1254
+ await db.batch([{ sql, params }]);
1255
+ },
1256
+ exec: async () => {
1257
+ throw new Error("storage");
1258
+ },
1259
+ };
1260
+ }
1261
+
1262
+ /** The whole-order handoff: order CAS, shipment move, transition audit, and the contact expiry. */
1263
+ private handoffStatements(state: LoadedOrder, actorId: string, version: number): SqlStatement[] {
1264
+ const { order } = state;
1265
+ return [
1266
+ {
1267
+ sql: `UPDATE scrappy_commerce_orders
1268
+ SET fulfillment_state = 'handed_off', event_version = event_version + 1
1269
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
1270
+ AND payment_state = 'captured' AND stock_state = 'confirmed'
1271
+ AND fulfillment_state = 'ready' AND label_state IN ('purchased', 'printed')
1272
+ AND EXISTS (
1273
+ SELECT 1 FROM scrappy_shipments
1274
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?
1275
+ AND scope = 'whole-order' AND label_purchase_state = 'purchased' AND handoff_state = 'none'
1276
+ )`,
1277
+ params: [order.tenantId, order.stage, order.orderId, order.eventVersion, order.tenantId, order.stage, order.orderId],
1278
+ },
1279
+ {
1280
+ sql: `UPDATE scrappy_shipments
1281
+ SET handoff_state = 'handed_off'
1282
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?
1283
+ AND scope = 'whole-order' AND label_purchase_state = 'purchased' AND handoff_state = 'none'
1284
+ AND EXISTS (
1285
+ SELECT 1 FROM scrappy_commerce_orders
1286
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
1287
+ )`,
1288
+ params: [order.tenantId, order.stage, order.orderId, order.tenantId, order.stage, order.orderId, version],
1289
+ },
1290
+ auditStatement(state, actorId, "transition", order.fulfillment, "handed_off", version, stamp(this.now(), state.auditCount + 1)),
1291
+ terminalTransitionExpiryStatement(order, version, new Date(this.now())),
1292
+ ];
1293
+ }
1294
+
1010
1295
  /** Returns are not automated. This records nothing. */
1011
1296
  createReturn(actor: FulfillmentActor, _orderId: string): FulfillmentResult<never> {
1012
1297
  if (!this.commandScope(actor)) return refuse("denied");
1013
1298
  return refuse("returns-not-automated");
1014
1299
  }
1015
1300
 
1301
+ /** Merchant and desk commands. An install actor (`app:`) commands only through the app methods. */
1016
1302
  private commandScope(actor: FulfillmentActor): { tenantId: string; stage: CommerceStage } | null {
1017
- if (!isOpaqueId(actor.actorId)) return null;
1303
+ if (!isOpaqueId(actor.actorId) || actor.actorId.startsWith(APP_ACTOR_PREFIX)) return null;
1018
1304
  const scope = { tenantId: actor.tenantId, stage: actor.stage };
1019
1305
  return actorMayCommand(actor, scope) ? scope : null;
1020
1306
  }
@@ -1122,6 +1408,8 @@ export class FulfillmentService {
1122
1408
  actorId: string,
1123
1409
  version: number,
1124
1410
  nextStamp: () => string,
1411
+ /** The assigned processor's first label outcome opens the shipment under its own assignment. */
1412
+ options: { assigned?: boolean } = {},
1125
1413
  ): Promise<SqlStatement[]> {
1126
1414
  const purchaseId = state.provider?.purchaseId ?? (await purchaseIdFor(state.order.tenantId, state.order.stage, state.order.orderId));
1127
1415
  const intent = {
@@ -1144,12 +1432,12 @@ export class FulfillmentService {
1144
1432
  SELECT 1 FROM scrappy_shipments AS s
1145
1433
  WHERE s.tenant_id = o.tenant_id AND s.stage = o.stage AND s.order_id = o.order_id
1146
1434
  )
1147
- AND NOT EXISTS (
1435
+ AND (? = 1 OR NOT EXISTS (
1148
1436
  SELECT 1 FROM fulfillment_assignments AS fa
1149
1437
  WHERE fa.tenant_id = o.tenant_id AND fa.stage = o.stage AND fa.order_id = o.order_id
1150
1438
  AND fa.state IN ('active', 'held')
1151
- )`,
1152
- params: [order.tenantId, order.stage, order.orderId, version],
1439
+ ))`,
1440
+ params: [order.tenantId, order.stage, order.orderId, version, options.assigned ? 1 : 0],
1153
1441
  },
1154
1442
  {
1155
1443
  sql: `INSERT INTO scrappy_provider_operations (
@@ -1659,7 +1947,7 @@ export class FulfillmentService {
1659
1947
  else if (audit.kind === "operator-hold") operatorHold = audit.to_state === "open";
1660
1948
  else if (audit.kind === "pending-cancel") cancelPending = audit.to_state === "open";
1661
1949
  else if (audit.kind === "packing-slip") packingSlip = parseSlip(parseJson(audit.to_state));
1662
- else if (audit.kind === "label-outcome") labelOutcome = parseLabelOutcome(parseJson(audit.to_state));
1950
+ else if (audit.kind === "label-outcome") labelOutcome = parseStoredLabelFacts(parseJson(audit.to_state));
1663
1951
  else if (audit.kind === "label-intent") {
1664
1952
  const intent = parseJson(audit.to_state);
1665
1953
  if (isRecord(intent)) {
@@ -1759,6 +2047,10 @@ export class FulfillmentService {
1759
2047
  carrierAccountId: loaded.carrierAccountId,
1760
2048
  adultSignatureRequired: loaded.adultSignatureRequired,
1761
2049
  prohibitedCarriers: loaded.prohibitedCarriers,
2050
+ carrier: labelOutcome?.carrier ?? null,
2051
+ service: labelOutcome?.service ?? null,
2052
+ trackingNumber: labelOutcome?.trackingNumber ?? null,
2053
+ trackingUrl: labelOutcome?.trackingUrl ?? null,
1762
2054
  }
1763
2055
  : null;
1764
2056
  return loaded;
@@ -1990,10 +2282,45 @@ function parseSlip(value: unknown): PackingSlip | null {
1990
2282
  };
1991
2283
  }
1992
2284
 
1993
- function parseLabelOutcome(value: unknown): { adultSignature: "accepted" | "refused" | "absent" } | null {
1994
- if (!isRecord(value)) return null;
1995
- if (value.adultSignature !== "accepted" && value.adultSignature !== "refused" && value.adultSignature !== "absent") return null;
1996
- return { adultSignature: value.adultSignature };
2285
+ const APP_ACTOR_PREFIX = "app:";
2286
+
2287
+ /** The binding an install actor writes under; null for any other actor or an unusable id. */
2288
+ function appBinding(actor: FulfillmentActor, assignmentId: string): AppAssignmentBinding | null {
2289
+ if (actor.role !== "caller" || !actor.actorId.startsWith(APP_ACTOR_PREFIX)) return null;
2290
+ const installId = actor.actorId.slice(APP_ACTOR_PREFIX.length);
2291
+ if (!isOpaqueId(installId) || !isOpaqueId(assignmentId)) return null;
2292
+ if (!actorMayCommand(actor, { tenantId: actor.tenantId, stage: actor.stage })) return null;
2293
+ return { installId, assignmentId };
2294
+ }
2295
+
2296
+ function scopeListIncludes(scopes: unknown, scope: string): boolean {
2297
+ const parsed = typeof scopes === "string" ? parseJson(scopes) : null;
2298
+ return Array.isArray(parsed) && parsed.includes(scope);
2299
+ }
2300
+
2301
+ /** The minimized app view of `state`, or of its planned successor when `next` is given. */
2302
+ function appShipmentView(
2303
+ state: LoadedOrder,
2304
+ purchaseId: string,
2305
+ next: { version?: number; label?: LabelState; handoff?: AppShipmentView["handoff"]; facts?: StoredLabelFacts | null } = {},
2306
+ ): AppShipmentView {
2307
+ const handoff =
2308
+ next.handoff ??
2309
+ (state.shipment?.handoffState === "handed_off" || state.order.fulfillment === "handed_off"
2310
+ ? "handed_off"
2311
+ : state.order.fulfillment === "UNKNOWN"
2312
+ ? "unknown"
2313
+ : "none");
2314
+ const facts = next.facts === undefined ? state.labelOutcome : next.facts;
2315
+ return {
2316
+ orderId: state.order.orderId,
2317
+ version: next.version ?? state.order.eventVersion,
2318
+ label: next.label ?? state.order.label,
2319
+ handoff,
2320
+ purchaseId,
2321
+ ...(facts?.carrier ? { carrier: facts.carrier } : {}),
2322
+ ...(facts?.trackingNumber && facts.trackingUrl ? { tracking: { number: facts.trackingNumber, url: facts.trackingUrl } } : {}),
2323
+ };
1997
2324
  }
1998
2325
 
1999
2326
  /** A stored audit state that is not JSON parses to null, which every caller fails closed on. */
@@ -20,6 +20,7 @@ import {
20
20
  } from "@tot/public-runtime";
21
21
  import type { SourceReadiness } from "../orders/data/sourceAuthorityStore.js";
22
22
  import { COMPLIANCE_RULE_PROFILES } from "@/config/compliance/rulesets";
23
+ import { canonicalCarrier, carrierProhibited, type CarrierCode } from "./carrierEvidence.js";
23
24
 
24
25
  export const RELEASE_HOLDS = [
25
26
  "source-check-pending",
@@ -167,3 +168,20 @@ export function carrierSelectionRefusal(
167
168
  if (policy.prohibitedCarriers.some((item) => item.toUpperCase() === normalized)) return "carrier-prohibited";
168
169
  return "vendor-not-selected";
169
170
  }
171
+
172
+ /**
173
+ * A processor's reported carrier. Prohibited when it is in the current policy's set or the order's
174
+ * label-intent snapshot; a handoff carrier must equal the carrier its label was bought with.
175
+ */
176
+ export function carrierEvidenceRefusal(input: {
177
+ carrier: CarrierCode;
178
+ policy: CarrierPolicy;
179
+ snapshotProhibited: readonly string[];
180
+ labelCarrier?: string | null;
181
+ }): "carrier-prohibited" | "carrier-mismatch" | null {
182
+ if (carrierProhibited(input.carrier, input.policy.prohibitedCarriers, input.snapshotProhibited)) return "carrier-prohibited";
183
+ if (input.labelCarrier !== undefined && (input.labelCarrier === null || canonicalCarrier(input.labelCarrier) !== input.carrier)) {
184
+ return "carrier-mismatch";
185
+ }
186
+ return null;
187
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "6.5.2",
3
+ "version": "6.5.3",
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",