@tokenoftrust/storefront-runner 6.5.1 → 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.
@@ -0,0 +1,70 @@
1
+ -- Owner consent for a Private App install to read order recipients for one purpose. Opaque ids and
2
+ -- times only; granted_by is sha256 of the owner actor id. Revoke sets revoked_at once; rows are never
3
+ -- deleted, and re-granting inserts a new row.
4
+ CREATE TABLE `app_contact_consents` (
5
+ `consent_id` text PRIMARY KEY NOT NULL,
6
+ `install_id` text NOT NULL,
7
+ `tenant_id` text NOT NULL,
8
+ `stage` text NOT NULL CHECK (`stage` IN ('preview', 'publication')),
9
+ `purpose` text NOT NULL CHECK (`purpose` = 'fulfillment-shipping'),
10
+ `granted_by` text NOT NULL CHECK (length(`granted_by`) = 64 AND `granted_by` NOT GLOB '*[^0-9a-f]*'),
11
+ `granted_at` text NOT NULL,
12
+ `expires_at` text NOT NULL,
13
+ `revoked_at` text,
14
+ CHECK (`expires_at` > `granted_at`),
15
+ CHECK (julianday(`expires_at`) IS NOT NULL AND julianday(`granted_at`) IS NOT NULL
16
+ AND julianday(`expires_at`) - julianday(`granted_at`) <= 365)
17
+ );
18
+ --> statement-breakpoint
19
+ CREATE UNIQUE INDEX `app_contact_consents_live`
20
+ ON `app_contact_consents` (`install_id`, `tenant_id`, `stage`, `purpose`)
21
+ WHERE `revoked_at` IS NULL;
22
+ --> statement-breakpoint
23
+ CREATE TRIGGER `app_contact_consents_no_delete`
24
+ BEFORE DELETE ON `app_contact_consents`
25
+ BEGIN
26
+ SELECT RAISE(ABORT, 'append-only');
27
+ END;
28
+ --> statement-breakpoint
29
+ CREATE TRIGGER `app_contact_consents_revoke_only`
30
+ BEFORE UPDATE ON `app_contact_consents`
31
+ WHEN OLD.`revoked_at` IS NOT NULL
32
+ OR NEW.`revoked_at` IS NULL
33
+ OR NEW.`consent_id` IS NOT OLD.`consent_id`
34
+ OR NEW.`install_id` IS NOT OLD.`install_id`
35
+ OR NEW.`tenant_id` IS NOT OLD.`tenant_id`
36
+ OR NEW.`stage` IS NOT OLD.`stage`
37
+ OR NEW.`purpose` IS NOT OLD.`purpose`
38
+ OR NEW.`granted_by` IS NOT OLD.`granted_by`
39
+ OR NEW.`granted_at` IS NOT OLD.`granted_at`
40
+ OR NEW.`expires_at` IS NOT OLD.`expires_at`
41
+ BEGIN
42
+ SELECT RAISE(ABORT, 'revoke-only');
43
+ END;
44
+ --> statement-breakpoint
45
+ -- Every admitted app read of an order recipient, written before anything is decrypted. Opaque ids
46
+ -- only; append-only.
47
+ CREATE TABLE `app_contact_access_audit` (
48
+ `audit_id` text PRIMARY KEY NOT NULL,
49
+ `install_id` text NOT NULL,
50
+ `tenant_id` text NOT NULL,
51
+ `stage` text NOT NULL CHECK (`stage` IN ('preview', 'publication')),
52
+ `order_id` text NOT NULL,
53
+ `purpose` text NOT NULL CHECK (`purpose` = 'fulfillment-shipping'),
54
+ `at` text NOT NULL,
55
+ `outcome` text NOT NULL CHECK (`outcome` = 'admitted')
56
+ );
57
+ --> statement-breakpoint
58
+ CREATE INDEX `idx_app_contact_access_audit_quota` ON `app_contact_access_audit` (`install_id`, `tenant_id`, `at`);
59
+ --> statement-breakpoint
60
+ CREATE TRIGGER `app_contact_access_audit_no_update`
61
+ BEFORE UPDATE ON `app_contact_access_audit`
62
+ BEGIN
63
+ SELECT RAISE(ABORT, 'append-only');
64
+ END;
65
+ --> statement-breakpoint
66
+ CREATE TRIGGER `app_contact_access_audit_no_delete`
67
+ BEFORE DELETE ON `app_contact_access_audit`
68
+ BEGIN
69
+ SELECT RAISE(ABORT, 'append-only');
70
+ END;
@@ -18,7 +18,8 @@ export function auditRecordIds(ids: readonly string[]): string | null {
18
18
  if (ids.length > AUDIT_RECORD_IDS_MAX) throw new Error("contact audit: too many record ids");
19
19
  return ids.length === 0 ? null : ids.join(",");
20
20
  }
21
- const HOUR_MS = 3_600_000;
21
+ /** The quota window. A row counts while its `at` is strictly after `now - HOUR_MS`: one exactly an hour old no longer counts. */
22
+ export const HOUR_MS = 3_600_000;
22
23
 
23
24
  export interface ContactActor {
24
25
  actorId: string;
@@ -88,6 +89,60 @@ export async function admitAudit(db: AuditDb, statement: AuditStatement): Promis
88
89
  }
89
90
  }
90
91
 
92
+ /** Admitted app recipient reads per install and tenant per hour. */
93
+ export const APP_CONTACT_READS_PER_HOUR = 300;
94
+ export const APP_CONTACT_PURPOSE = "fulfillment-shipping";
95
+
96
+ /**
97
+ * The app-access audit insert (`app_contact_access_audit`): the recipient read's admission and its
98
+ * only write. It inserts one row only when, at that instant, the install is active on the tenant
99
+ * holding `scope`, the assignment is the install's active assignment of the order, the order still
100
+ * stands at `eventVersion` (the version its releasability was computed at), a live consent covers
101
+ * (install, tenant, stage, purpose), and the install has fewer than
102
+ * {@link APP_CONTACT_READS_PER_HOUR} admitted rows on the tenant in the last hour.
103
+ */
104
+ export function appAuditStatement(input: {
105
+ auditId: string;
106
+ installId: string;
107
+ tenantId: string;
108
+ stage: VaultStage;
109
+ orderId: string;
110
+ assignmentId: string;
111
+ eventVersion: number;
112
+ scope: string;
113
+ now: Date;
114
+ }): AuditStatement {
115
+ const { auditId, installId, tenantId, stage, orderId, assignmentId, eventVersion, scope, now } = input;
116
+ const at = now.toISOString();
117
+ const hourAgo = new Date(now.getTime() - HOUR_MS).toISOString();
118
+ return {
119
+ sql: `INSERT INTO app_contact_access_audit (audit_id, install_id, tenant_id, stage, order_id, purpose, at, outcome)
120
+ SELECT ?, ?, ?, ?, ?, '${APP_CONTACT_PURPOSE}', ?, 'admitted'
121
+ WHERE EXISTS (SELECT 1 FROM app_installs AS ai
122
+ WHERE ai.install_id = ? AND ai.tenant_id = ? AND ai.status = 'active'
123
+ AND EXISTS (SELECT 1 FROM json_each(ai.scopes) WHERE json_each.value = ?))
124
+ AND EXISTS (SELECT 1 FROM fulfillment_assignments AS fa
125
+ WHERE fa.assignment_id = ? AND fa.tenant_id = ? AND fa.stage = ? AND fa.order_id = ?
126
+ AND fa.install_id = ? AND fa.state = 'active')
127
+ AND EXISTS (SELECT 1 FROM scrappy_commerce_orders AS o
128
+ WHERE o.tenant_id = ? AND o.stage = ? AND o.order_id = ? AND o.event_version = ?)
129
+ AND EXISTS (SELECT 1 FROM app_contact_consents AS c
130
+ WHERE c.install_id = ? AND c.tenant_id = ? AND c.stage = ? AND c.purpose = '${APP_CONTACT_PURPOSE}'
131
+ AND c.revoked_at IS NULL AND c.expires_at > ?)
132
+ AND (SELECT COUNT(*) FROM app_contact_access_audit AS q
133
+ WHERE q.install_id = ? AND q.tenant_id = ? AND q.at > ?) < ?
134
+ RETURNING audit_id`,
135
+ params: [
136
+ auditId, installId, tenantId, stage, orderId, at,
137
+ installId, tenantId, scope,
138
+ assignmentId, tenantId, stage, orderId, installId,
139
+ tenantId, stage, orderId, eventVersion,
140
+ installId, tenantId, stage, at,
141
+ installId, tenantId, hourAgo, APP_CONTACT_READS_PER_HOUR,
142
+ ],
143
+ };
144
+ }
145
+
91
146
  /** The same insert without RETURNING, for use inside a batch whose later statements check the row. */
92
147
  export function batchAuditStatement(statement: AuditStatement): AuditStatement {
93
148
  return { sql: statement.sql.replace(/\s*RETURNING audit_id$/, ""), params: statement.params };
@@ -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
+ }