@tokenoftrust/storefront-runner 2.0.16 → 2.0.17
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.
- package/apps/storefront/src/lib/apps/routeParams.ts +45 -0
- package/apps/storefront/src/lib/fixtures/regulated-commerce-truth/index.ts +540 -0
- package/apps/storefront/src/lib/fixtures/ws3-readonly-clone-preflight/index.ts +495 -0
- package/package.json +1 -1
- package/packages/public-runtime/tests/conformance/fake-destination.ts +402 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* operationParam — resolve the managed-analytics v1 `[operation]` path param
|
|
3
|
+
* robustly on BOTH admin surfaces.
|
|
4
|
+
*
|
|
5
|
+
* On the host-routed surface Astro populates `context.params.operation` normally.
|
|
6
|
+
* On the TENANT surface (`/<domain>/api/…`) the middleware selects the tenant by
|
|
7
|
+
* path prefix and rewrites via `next(renderPath)` — and across that rewrite this
|
|
8
|
+
* flat dynamic route comes back with an EMPTY `context.params` too (mirrors the
|
|
9
|
+
* identical rewrite defect `changeIdParam` solves for the change routes;
|
|
10
|
+
* `[operation].ts` is single-segment, so it has no nested-route case to worry
|
|
11
|
+
* about).
|
|
12
|
+
*
|
|
13
|
+
* Fallback: parse the operation from the pathname tail, which is stable on both
|
|
14
|
+
* surfaces and both sides of the rewrite (`request.url` is the original request,
|
|
15
|
+
* unaffected by the rewrite — see tenantPreview.ts's `stripBasePath`). No static
|
|
16
|
+
* sibling reaches this handler — Astro matches static routes first — so the tail
|
|
17
|
+
* capture is unambiguous wherever this helper runs.
|
|
18
|
+
*/
|
|
19
|
+
import type { APIContext } from "astro";
|
|
20
|
+
|
|
21
|
+
const TAIL = /\/api\/integrations\/v1\/([^/]+)\/?$/;
|
|
22
|
+
|
|
23
|
+
function fromPathname(pathname: string | undefined): string | undefined {
|
|
24
|
+
if (!pathname) return undefined;
|
|
25
|
+
const m = pathname.match(TAIL);
|
|
26
|
+
if (!m?.[1]) return undefined;
|
|
27
|
+
let id: string = m[1];
|
|
28
|
+
try {
|
|
29
|
+
id = decodeURIComponent(id);
|
|
30
|
+
} catch {
|
|
31
|
+
// keep the raw segment — the allowlist lookup downstream will refuse garbage
|
|
32
|
+
}
|
|
33
|
+
const trimmed = id.trim();
|
|
34
|
+
return trimmed || undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function operationParam(context: APIContext): string | undefined {
|
|
38
|
+
const fromParams = context.params.operation?.trim();
|
|
39
|
+
if (fromParams) return fromParams;
|
|
40
|
+
try {
|
|
41
|
+
return fromPathname(new URL(context.request.url).pathname);
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthetic regulated-commerce truth used by cross-workstream conformance tests.
|
|
3
|
+
*
|
|
4
|
+
* This module is deliberately fixture-only. It does not call an integration API,
|
|
5
|
+
* deliver an order, import a report, or mutate a tenant. Consumers receive a
|
|
6
|
+
* validated, detached snapshot whose identifiers can never name a live tenant or
|
|
7
|
+
* destination.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const REGULATED_COMMERCE_TRUTH_SCHEMA = "regulated-commerce-truth.v1" as const;
|
|
11
|
+
|
|
12
|
+
export type ExpectedInvariant =
|
|
13
|
+
| { disposition: "allow"; violations: [] }
|
|
14
|
+
| { disposition: "closed-failure"; violations: ["age-verification-required"] };
|
|
15
|
+
|
|
16
|
+
export interface RegulatedCommerceTruthFixture {
|
|
17
|
+
schemaVersion: typeof REGULATED_COMMERCE_TRUTH_SCHEMA;
|
|
18
|
+
fixtureId: string;
|
|
19
|
+
synthetic: true;
|
|
20
|
+
capturedAt: string;
|
|
21
|
+
tenant: {
|
|
22
|
+
tenantId: string;
|
|
23
|
+
/** Canonical public-runtime TenantTier vocabulary. */
|
|
24
|
+
tenantTier: "regulated-commerce";
|
|
25
|
+
jurisdiction: string;
|
|
26
|
+
minimumAge: number;
|
|
27
|
+
ageVerificationRequired: true;
|
|
28
|
+
};
|
|
29
|
+
integrations: Array<{
|
|
30
|
+
reference: string;
|
|
31
|
+
kind: "order-risk";
|
|
32
|
+
destination: string;
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
orders: Array<{
|
|
36
|
+
orderReference: string;
|
|
37
|
+
integrationReference: string;
|
|
38
|
+
occurredAt: string;
|
|
39
|
+
status: "paid" | "closed";
|
|
40
|
+
currency: "USD";
|
|
41
|
+
totalMinor: number;
|
|
42
|
+
destinationCountry: "US";
|
|
43
|
+
regulatedItems: Array<{
|
|
44
|
+
sku: string;
|
|
45
|
+
category: "age-restricted";
|
|
46
|
+
quantity: number;
|
|
47
|
+
unitPriceMinor: number;
|
|
48
|
+
}>;
|
|
49
|
+
compliance: {
|
|
50
|
+
ageVerification: "passed" | "missing";
|
|
51
|
+
consentRecorded: boolean;
|
|
52
|
+
taxCollectedMinor: number;
|
|
53
|
+
};
|
|
54
|
+
expectedInvariant: ExpectedInvariant;
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const FIXTURE_SOURCE = {
|
|
59
|
+
schemaVersion: REGULATED_COMMERCE_TRUTH_SCHEMA,
|
|
60
|
+
fixtureId: "synthetic-regulated-commerce-risk-v1",
|
|
61
|
+
synthetic: true,
|
|
62
|
+
capturedAt: "2026-08-29T12:00:00.000Z",
|
|
63
|
+
tenant: {
|
|
64
|
+
tenantId: "synthetic-regulated-merchant.test",
|
|
65
|
+
tenantTier: "regulated-commerce",
|
|
66
|
+
jurisdiction: "US-MN",
|
|
67
|
+
minimumAge: 21,
|
|
68
|
+
ageVerificationRequired: true,
|
|
69
|
+
},
|
|
70
|
+
integrations: [
|
|
71
|
+
{
|
|
72
|
+
reference: "integration:synthetic:order-risk-v1",
|
|
73
|
+
kind: "order-risk",
|
|
74
|
+
destination: "fake://synthetic-order-risk/deliveries",
|
|
75
|
+
enabled: true,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
orders: [
|
|
79
|
+
{
|
|
80
|
+
orderReference: "order:synthetic:verified-001",
|
|
81
|
+
integrationReference: "integration:synthetic:order-risk-v1",
|
|
82
|
+
occurredAt: "2026-08-29T12:01:00.000Z",
|
|
83
|
+
status: "paid",
|
|
84
|
+
currency: "USD",
|
|
85
|
+
totalMinor: 6499,
|
|
86
|
+
destinationCountry: "US",
|
|
87
|
+
regulatedItems: [
|
|
88
|
+
{
|
|
89
|
+
sku: "SYNTH-AGE-21-001",
|
|
90
|
+
category: "age-restricted",
|
|
91
|
+
quantity: 1,
|
|
92
|
+
unitPriceMinor: 5999,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
compliance: {
|
|
96
|
+
ageVerification: "passed",
|
|
97
|
+
consentRecorded: true,
|
|
98
|
+
taxCollectedMinor: 500,
|
|
99
|
+
},
|
|
100
|
+
expectedInvariant: { disposition: "allow", violations: [] },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
orderReference: "order:synthetic:closed-002",
|
|
104
|
+
integrationReference: "integration:synthetic:order-risk-v1",
|
|
105
|
+
occurredAt: "2026-08-29T12:02:00.000Z",
|
|
106
|
+
status: "closed",
|
|
107
|
+
currency: "USD",
|
|
108
|
+
totalMinor: 3299,
|
|
109
|
+
destinationCountry: "US",
|
|
110
|
+
regulatedItems: [
|
|
111
|
+
{
|
|
112
|
+
sku: "SYNTH-AGE-21-002",
|
|
113
|
+
category: "age-restricted",
|
|
114
|
+
quantity: 1,
|
|
115
|
+
unitPriceMinor: 2999,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
compliance: {
|
|
119
|
+
ageVerification: "missing",
|
|
120
|
+
consentRecorded: false,
|
|
121
|
+
taxCollectedMinor: 300,
|
|
122
|
+
},
|
|
123
|
+
expectedInvariant: {
|
|
124
|
+
disposition: "closed-failure",
|
|
125
|
+
violations: ["age-verification-required"],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
} satisfies RegulatedCommerceTruthFixture;
|
|
130
|
+
|
|
131
|
+
const UNSAFE_KEY = /(authorization|credential|secret|password|api[-_]?key|access[-_]?token|refresh[-_]?token|email|phone|address|(?:^|[-_])name(?:$|[-_]))/i;
|
|
132
|
+
const CREDENTIAL_VALUE = /(-----BEGIN [A-Z ]*PRIVATE KEY-----|\bBearer\s+[A-Za-z0-9._~-]+|\bAKIA[0-9A-Z]{16}\b)/;
|
|
133
|
+
const EMAIL_VALUE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
|
|
134
|
+
const PHONE_VALUE = /\b(?:\+?\d{1,3}[-. ]?)?(?:\(\d{3}\)|\d{3})[-. ]\d{3}[-. ]\d{4}\b/;
|
|
135
|
+
const PAYMENT_VALUE = /(?:^|[^0-9])(?:\d{4}[- ]?){3}\d{4}(?:$|[^0-9])|(?:^|[^0-9])\d{13,19}(?:$|[^0-9])/;
|
|
136
|
+
/** US SSN/EIN-like nine-digit identifiers, contiguous or in their common hyphenated forms. */
|
|
137
|
+
const GOVERNMENT_ID_VALUE = /(?:^|[^0-9])(?:\d{3}-\d{2}-\d{4}|\d{2}-\d{7}|\d{9})(?:$|[^0-9])/;
|
|
138
|
+
const ADDRESS_VALUE = /\b\d{1,6}(?:[- ]+[A-Za-z][A-Za-z0-9.']*){1,5}[- ]+(?:street|st|avenue|ave|road|rd|boulevard|blvd|lane|ln|drive|dr|court|ct|way)\b/i;
|
|
139
|
+
const PERSON_NAME_VALUE = /\b(?:[A-Z][a-z]{1,30}\s+[A-Z][a-z]{1,30}|(?:jane|john)[-_ ]doe)\b/i;
|
|
140
|
+
const LIVE_URL_VALUE = /\b(?:https?|wss?):\/\//i;
|
|
141
|
+
const CODE_VALUE = /(?:<\/?script\b|javascript:|\{\{|=>|\bfunction\s*\(|\b(?:window|document)\.|\bdataLayer\b|\bGTM-[A-Z0-9-]+\b)/i;
|
|
142
|
+
|
|
143
|
+
// This is one local golden fixture, not a public extensible identity contract.
|
|
144
|
+
const FIXTURE_ID = /^synthetic-regulated-commerce-risk-v1$/;
|
|
145
|
+
const TENANT_ID = /^synthetic-regulated-merchant\.test$/;
|
|
146
|
+
const JURISDICTION = /^US-MN$/;
|
|
147
|
+
const INTEGRATION_REFERENCE = /^integration:synthetic:order-risk-v1$/;
|
|
148
|
+
const FAKE_DESTINATION = /^fake:\/\/synthetic-order-risk\/deliveries$/;
|
|
149
|
+
const ORDER_REFERENCE = /^order:synthetic:(?:verified|closed)-\d{3}$/;
|
|
150
|
+
const SYNTHETIC_SKU = /^SYNTH-AGE-21-\d{3}$/;
|
|
151
|
+
const MAX_MINIMUM_AGE = 125;
|
|
152
|
+
const MAX_ITEM_QUANTITY = 10_000;
|
|
153
|
+
const MAX_MINOR_AMOUNT = 99_999_999;
|
|
154
|
+
|
|
155
|
+
class FixtureValidationError extends Error {}
|
|
156
|
+
const VALIDATION_ERRORS = new WeakSet<object>();
|
|
157
|
+
|
|
158
|
+
function fail(path: string, message: string): never {
|
|
159
|
+
const error = new FixtureValidationError(`Invalid regulated-commerce truth at ${path}: ${message}`);
|
|
160
|
+
VALIDATION_ERRORS.add(error);
|
|
161
|
+
throw Object.freeze(error);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function record(value: unknown, path: string): Record<string, unknown> {
|
|
165
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
166
|
+
fail(path, "expected a plain data object");
|
|
167
|
+
}
|
|
168
|
+
const prototype = Object.getPrototypeOf(value);
|
|
169
|
+
if (prototype !== Object.prototype && prototype !== null) fail(path, "expected a plain data object");
|
|
170
|
+
return value as Record<string, unknown>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function ownDataValue(value: Record<string, unknown>, key: string): unknown {
|
|
174
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
175
|
+
return descriptor && descriptor.enumerable && Object.hasOwn(descriptor, "value")
|
|
176
|
+
? descriptor.value
|
|
177
|
+
: undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function arrayDataValues(value: unknown, path: string): unknown[] {
|
|
181
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
182
|
+
fail(path, "expected an ordinary data array");
|
|
183
|
+
}
|
|
184
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
185
|
+
const length = lengthDescriptor && Object.hasOwn(lengthDescriptor, "value")
|
|
186
|
+
? lengthDescriptor.value
|
|
187
|
+
: -1;
|
|
188
|
+
if (!Number.isSafeInteger(length) || (length as number) < 0) fail(path, "array length is invalid");
|
|
189
|
+
const result: unknown[] = [];
|
|
190
|
+
for (let index = 0; index < (length as number); index += 1) {
|
|
191
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
192
|
+
if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
193
|
+
fail(`${path}[${index}]`, "expected an own enumerable data item");
|
|
194
|
+
}
|
|
195
|
+
result.push(descriptor.value);
|
|
196
|
+
}
|
|
197
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
198
|
+
if (key === "length") continue;
|
|
199
|
+
if (typeof key !== "string" || !/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= (length as number)) {
|
|
200
|
+
fail(path, "array contains unexpected metadata");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function string(value: unknown, path: string): string {
|
|
207
|
+
if (typeof value !== "string" || value.length === 0) fail(path, "expected a non-empty string");
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function boundedInteger(value: unknown, path: string, minimum: number, maximum: number): number {
|
|
212
|
+
if (!Number.isSafeInteger(value) || (value as number) < minimum || (value as number) > maximum) {
|
|
213
|
+
fail(path, `expected an integer from ${minimum} through ${maximum}`);
|
|
214
|
+
}
|
|
215
|
+
return value as number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function exactKeys(value: Record<string, unknown>, keys: string[], path: string): void {
|
|
219
|
+
const expected = new Set(keys);
|
|
220
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
221
|
+
if (typeof key !== "string" || !expected.has(key)) fail(path, "contains an unexpected field");
|
|
222
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
223
|
+
if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
224
|
+
fail(`${path}.${key}`, "must be an own enumerable data field");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const key of keys) {
|
|
228
|
+
if (ownDataValue(value, key) === undefined) fail(`${path}.${key}`, "required data field is missing");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function literal<T extends string | boolean>(value: unknown, expected: T, path: string): T {
|
|
233
|
+
if (value !== expected) fail(path, `expected ${JSON.stringify(expected)}`);
|
|
234
|
+
return expected;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function oneOf<T extends string>(value: unknown, allowed: readonly T[], path: string): T {
|
|
238
|
+
if (typeof value !== "string" || !allowed.includes(value as T)) {
|
|
239
|
+
fail(path, `expected one of ${allowed.join(", ")}`);
|
|
240
|
+
}
|
|
241
|
+
return value as T;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function isoTimestamp(value: unknown, path: string): string {
|
|
245
|
+
const parsed = string(value, path);
|
|
246
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(parsed) || Number.isNaN(Date.parse(parsed))) {
|
|
247
|
+
fail(path, "expected a canonical UTC timestamp");
|
|
248
|
+
}
|
|
249
|
+
return parsed;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function assertSanitized(value: unknown, path = "fixture"): void {
|
|
253
|
+
if (typeof value === "string") {
|
|
254
|
+
if (CREDENTIAL_VALUE.test(value)) fail(path, "credential-shaped value is forbidden");
|
|
255
|
+
if (
|
|
256
|
+
EMAIL_VALUE.test(value) ||
|
|
257
|
+
PHONE_VALUE.test(value) ||
|
|
258
|
+
PAYMENT_VALUE.test(value) ||
|
|
259
|
+
GOVERNMENT_ID_VALUE.test(value) ||
|
|
260
|
+
ADDRESS_VALUE.test(value) ||
|
|
261
|
+
PERSON_NAME_VALUE.test(value)
|
|
262
|
+
) {
|
|
263
|
+
fail(path, "PII-shaped value is forbidden");
|
|
264
|
+
}
|
|
265
|
+
if (LIVE_URL_VALUE.test(value)) fail(path, "live URL value is forbidden");
|
|
266
|
+
if (CODE_VALUE.test(value)) fail(path, "code-bearing value is forbidden");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (Array.isArray(value)) {
|
|
270
|
+
const entries = arrayDataValues(value, path);
|
|
271
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
272
|
+
assertSanitized(entries[index], `${path}[${index}]`);
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (value !== null && typeof value === "object") {
|
|
277
|
+
const object = record(value, path);
|
|
278
|
+
for (const key of Reflect.ownKeys(object)) {
|
|
279
|
+
if (typeof key !== "string") fail(path, "contains a non-JSON field");
|
|
280
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
|
281
|
+
if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
282
|
+
fail(path, "contains a non-data field");
|
|
283
|
+
}
|
|
284
|
+
if (UNSAFE_KEY.test(key)) fail(path, "contains a PII/secret-shaped field");
|
|
285
|
+
// Do not include caller-controlled property names in diagnostics.
|
|
286
|
+
assertSanitized(descriptor.value, path);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function parseTenant(value: unknown): RegulatedCommerceTruthFixture["tenant"] {
|
|
292
|
+
const input = record(value, "fixture.tenant");
|
|
293
|
+
exactKeys(
|
|
294
|
+
input,
|
|
295
|
+
["tenantId", "tenantTier", "jurisdiction", "minimumAge", "ageVerificationRequired"],
|
|
296
|
+
"fixture.tenant",
|
|
297
|
+
);
|
|
298
|
+
const tenantId = string(ownDataValue(input, "tenantId"), "fixture.tenant.tenantId");
|
|
299
|
+
if (!TENANT_ID.test(tenantId)) {
|
|
300
|
+
fail("fixture.tenant.tenantId", "must use the closed synthetic tenant-id format");
|
|
301
|
+
}
|
|
302
|
+
const jurisdiction = string(ownDataValue(input, "jurisdiction"), "fixture.tenant.jurisdiction");
|
|
303
|
+
if (!JURISDICTION.test(jurisdiction)) {
|
|
304
|
+
fail("fixture.tenant.jurisdiction", "must use the closed jurisdiction format");
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
tenantId,
|
|
308
|
+
tenantTier: literal(ownDataValue(input, "tenantTier"), "regulated-commerce", "fixture.tenant.tenantTier"),
|
|
309
|
+
jurisdiction,
|
|
310
|
+
minimumAge: boundedInteger(
|
|
311
|
+
ownDataValue(input, "minimumAge"),
|
|
312
|
+
"fixture.tenant.minimumAge",
|
|
313
|
+
18,
|
|
314
|
+
MAX_MINIMUM_AGE,
|
|
315
|
+
),
|
|
316
|
+
ageVerificationRequired: literal(
|
|
317
|
+
ownDataValue(input, "ageVerificationRequired"),
|
|
318
|
+
true,
|
|
319
|
+
"fixture.tenant.ageVerificationRequired",
|
|
320
|
+
),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function parseIntegrations(value: unknown): RegulatedCommerceTruthFixture["integrations"] {
|
|
325
|
+
const inputs = arrayDataValues(value, "fixture.integrations");
|
|
326
|
+
if (inputs.length === 0) fail("fixture.integrations", "at least one integration reference is required");
|
|
327
|
+
const integrations: RegulatedCommerceTruthFixture["integrations"] = [];
|
|
328
|
+
for (let index = 0; index < inputs.length; index += 1) {
|
|
329
|
+
const path = `fixture.integrations[${index}]`;
|
|
330
|
+
const input = record(inputs[index], path);
|
|
331
|
+
exactKeys(input, ["reference", "kind", "destination", "enabled"], path);
|
|
332
|
+
const reference = string(ownDataValue(input, "reference"), `${path}.reference`);
|
|
333
|
+
if (!INTEGRATION_REFERENCE.test(reference)) {
|
|
334
|
+
fail(`${path}.reference`, "must use the closed synthetic integration-reference format");
|
|
335
|
+
}
|
|
336
|
+
const destination = string(ownDataValue(input, "destination"), `${path}.destination`);
|
|
337
|
+
if (!FAKE_DESTINATION.test(destination)) {
|
|
338
|
+
fail(`${path}.destination`, "must use the closed fake destination format");
|
|
339
|
+
}
|
|
340
|
+
const enabled = ownDataValue(input, "enabled");
|
|
341
|
+
if (typeof enabled !== "boolean") fail(`${path}.enabled`, "expected a boolean");
|
|
342
|
+
integrations.push({
|
|
343
|
+
reference,
|
|
344
|
+
kind: literal(ownDataValue(input, "kind"), "order-risk", `${path}.kind`),
|
|
345
|
+
destination,
|
|
346
|
+
enabled,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return integrations;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function parseExpectedInvariant(value: unknown, path: string): ExpectedInvariant {
|
|
353
|
+
const input = record(value, path);
|
|
354
|
+
exactKeys(input, ["disposition", "violations"], path);
|
|
355
|
+
const disposition = oneOf(
|
|
356
|
+
ownDataValue(input, "disposition"),
|
|
357
|
+
["allow", "closed-failure"] as const,
|
|
358
|
+
`${path}.disposition`,
|
|
359
|
+
);
|
|
360
|
+
const violationInputs = arrayDataValues(ownDataValue(input, "violations"), `${path}.violations`);
|
|
361
|
+
const violations: Array<"age-verification-required"> = [];
|
|
362
|
+
for (let index = 0; index < violationInputs.length; index += 1) {
|
|
363
|
+
violations.push(
|
|
364
|
+
literal(violationInputs[index], "age-verification-required", `${path}.violations[${index}]`),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (disposition === "allow") {
|
|
368
|
+
if (violations.length !== 0) fail(`${path}.violations`, "allow must have no violations");
|
|
369
|
+
return { disposition, violations: [] };
|
|
370
|
+
}
|
|
371
|
+
if (violations.length !== 1) {
|
|
372
|
+
fail(`${path}.violations`, "closed-failure must contain the age-verification-required violation");
|
|
373
|
+
}
|
|
374
|
+
return { disposition, violations: ["age-verification-required"] };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function parseOrders(
|
|
378
|
+
value: unknown,
|
|
379
|
+
integrations: RegulatedCommerceTruthFixture["integrations"],
|
|
380
|
+
): RegulatedCommerceTruthFixture["orders"] {
|
|
381
|
+
const inputs = arrayDataValues(value, "fixture.orders");
|
|
382
|
+
if (inputs.length === 0) fail("fixture.orders", "at least one truth order is required");
|
|
383
|
+
const integrationReferences = new Set<string>();
|
|
384
|
+
for (const integration of integrations) integrationReferences.add(integration.reference);
|
|
385
|
+
const orders: RegulatedCommerceTruthFixture["orders"] = [];
|
|
386
|
+
for (let index = 0; index < inputs.length; index += 1) {
|
|
387
|
+
const path = `fixture.orders[${index}]`;
|
|
388
|
+
const input = record(inputs[index], path);
|
|
389
|
+
exactKeys(
|
|
390
|
+
input,
|
|
391
|
+
[
|
|
392
|
+
"orderReference",
|
|
393
|
+
"integrationReference",
|
|
394
|
+
"occurredAt",
|
|
395
|
+
"status",
|
|
396
|
+
"currency",
|
|
397
|
+
"totalMinor",
|
|
398
|
+
"destinationCountry",
|
|
399
|
+
"regulatedItems",
|
|
400
|
+
"compliance",
|
|
401
|
+
"expectedInvariant",
|
|
402
|
+
],
|
|
403
|
+
path,
|
|
404
|
+
);
|
|
405
|
+
const orderReference = string(ownDataValue(input, "orderReference"), `${path}.orderReference`);
|
|
406
|
+
if (!ORDER_REFERENCE.test(orderReference)) {
|
|
407
|
+
fail(`${path}.orderReference`, "must use the closed synthetic order-reference format");
|
|
408
|
+
}
|
|
409
|
+
const integrationReference = string(
|
|
410
|
+
ownDataValue(input, "integrationReference"),
|
|
411
|
+
`${path}.integrationReference`,
|
|
412
|
+
);
|
|
413
|
+
if (!integrationReferences.has(integrationReference)) {
|
|
414
|
+
fail(`${path}.integrationReference`, "does not reference a declared integration");
|
|
415
|
+
}
|
|
416
|
+
const itemInputs = arrayDataValues(ownDataValue(input, "regulatedItems"), `${path}.regulatedItems`);
|
|
417
|
+
if (itemInputs.length === 0) fail(`${path}.regulatedItems`, "at least one regulated item is required");
|
|
418
|
+
const regulatedItems: RegulatedCommerceTruthFixture["orders"][number]["regulatedItems"] = [];
|
|
419
|
+
for (let itemIndex = 0; itemIndex < itemInputs.length; itemIndex += 1) {
|
|
420
|
+
const itemPath = `${path}.regulatedItems[${itemIndex}]`;
|
|
421
|
+
const item = record(itemInputs[itemIndex], itemPath);
|
|
422
|
+
exactKeys(item, ["sku", "category", "quantity", "unitPriceMinor"], itemPath);
|
|
423
|
+
const sku = string(ownDataValue(item, "sku"), `${itemPath}.sku`);
|
|
424
|
+
if (!SYNTHETIC_SKU.test(sku)) fail(`${itemPath}.sku`, "must use the closed synthetic SKU format");
|
|
425
|
+
regulatedItems.push({
|
|
426
|
+
sku,
|
|
427
|
+
category: literal(ownDataValue(item, "category"), "age-restricted", `${itemPath}.category`),
|
|
428
|
+
quantity: boundedInteger(
|
|
429
|
+
ownDataValue(item, "quantity"),
|
|
430
|
+
`${itemPath}.quantity`,
|
|
431
|
+
1,
|
|
432
|
+
MAX_ITEM_QUANTITY,
|
|
433
|
+
),
|
|
434
|
+
unitPriceMinor: boundedInteger(
|
|
435
|
+
ownDataValue(item, "unitPriceMinor"),
|
|
436
|
+
`${itemPath}.unitPriceMinor`,
|
|
437
|
+
1,
|
|
438
|
+
MAX_MINOR_AMOUNT,
|
|
439
|
+
),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
const complianceInput = record(ownDataValue(input, "compliance"), `${path}.compliance`);
|
|
443
|
+
exactKeys(complianceInput, ["ageVerification", "consentRecorded", "taxCollectedMinor"], `${path}.compliance`);
|
|
444
|
+
const consentRecorded = ownDataValue(complianceInput, "consentRecorded");
|
|
445
|
+
if (typeof consentRecorded !== "boolean") {
|
|
446
|
+
fail(`${path}.compliance.consentRecorded`, "expected a boolean");
|
|
447
|
+
}
|
|
448
|
+
const compliance = {
|
|
449
|
+
ageVerification: oneOf(
|
|
450
|
+
ownDataValue(complianceInput, "ageVerification"),
|
|
451
|
+
["passed", "missing"] as const,
|
|
452
|
+
`${path}.compliance.ageVerification`,
|
|
453
|
+
),
|
|
454
|
+
consentRecorded,
|
|
455
|
+
taxCollectedMinor: boundedInteger(
|
|
456
|
+
ownDataValue(complianceInput, "taxCollectedMinor"),
|
|
457
|
+
`${path}.compliance.taxCollectedMinor`,
|
|
458
|
+
0,
|
|
459
|
+
MAX_MINOR_AMOUNT,
|
|
460
|
+
),
|
|
461
|
+
};
|
|
462
|
+
const status = oneOf(ownDataValue(input, "status"), ["paid", "closed"] as const, `${path}.status`);
|
|
463
|
+
const expectedInvariant = parseExpectedInvariant(
|
|
464
|
+
ownDataValue(input, "expectedInvariant"),
|
|
465
|
+
`${path}.expectedInvariant`,
|
|
466
|
+
);
|
|
467
|
+
const shouldFailClosed = status === "closed" || compliance.ageVerification !== "passed";
|
|
468
|
+
if (shouldFailClosed !== (expectedInvariant.disposition === "closed-failure")) {
|
|
469
|
+
fail(`${path}.expectedInvariant`, "does not match the fixture's regulated-commerce facts");
|
|
470
|
+
}
|
|
471
|
+
orders.push({
|
|
472
|
+
orderReference,
|
|
473
|
+
integrationReference,
|
|
474
|
+
occurredAt: isoTimestamp(ownDataValue(input, "occurredAt"), `${path}.occurredAt`),
|
|
475
|
+
status,
|
|
476
|
+
currency: literal(ownDataValue(input, "currency"), "USD", `${path}.currency`),
|
|
477
|
+
totalMinor: boundedInteger(
|
|
478
|
+
ownDataValue(input, "totalMinor"),
|
|
479
|
+
`${path}.totalMinor`,
|
|
480
|
+
1,
|
|
481
|
+
MAX_MINOR_AMOUNT,
|
|
482
|
+
),
|
|
483
|
+
destinationCountry: literal(
|
|
484
|
+
ownDataValue(input, "destinationCountry"),
|
|
485
|
+
"US",
|
|
486
|
+
`${path}.destinationCountry`,
|
|
487
|
+
),
|
|
488
|
+
regulatedItems,
|
|
489
|
+
compliance,
|
|
490
|
+
expectedInvariant,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return orders;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function deepFreeze<T>(value: T): T {
|
|
497
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
498
|
+
Object.freeze(value);
|
|
499
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
500
|
+
}
|
|
501
|
+
return value;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function parseRegulatedCommerceTruth(value: unknown): Readonly<RegulatedCommerceTruthFixture> {
|
|
505
|
+
assertSanitized(value);
|
|
506
|
+
const input = record(value, "fixture");
|
|
507
|
+
exactKeys(input, ["schemaVersion", "fixtureId", "synthetic", "capturedAt", "tenant", "integrations", "orders"], "fixture");
|
|
508
|
+
const fixtureId = string(ownDataValue(input, "fixtureId"), "fixture.fixtureId");
|
|
509
|
+
if (!FIXTURE_ID.test(fixtureId)) fail("fixture.fixtureId", "must use the closed synthetic fixture-id format");
|
|
510
|
+
const integrations = parseIntegrations(ownDataValue(input, "integrations"));
|
|
511
|
+
const parsed: RegulatedCommerceTruthFixture = {
|
|
512
|
+
schemaVersion: literal(
|
|
513
|
+
ownDataValue(input, "schemaVersion"),
|
|
514
|
+
REGULATED_COMMERCE_TRUTH_SCHEMA,
|
|
515
|
+
"fixture.schemaVersion",
|
|
516
|
+
),
|
|
517
|
+
fixtureId,
|
|
518
|
+
synthetic: literal(ownDataValue(input, "synthetic"), true, "fixture.synthetic"),
|
|
519
|
+
capturedAt: isoTimestamp(ownDataValue(input, "capturedAt"), "fixture.capturedAt"),
|
|
520
|
+
tenant: parseTenant(ownDataValue(input, "tenant")),
|
|
521
|
+
integrations,
|
|
522
|
+
orders: parseOrders(ownDataValue(input, "orders"), integrations),
|
|
523
|
+
};
|
|
524
|
+
return deepFreeze(parsed);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Validate unknown fixture input and return a detached, immutable truth snapshot. */
|
|
528
|
+
export function loadRegulatedCommerceTruth(value: unknown): Readonly<RegulatedCommerceTruthFixture> {
|
|
529
|
+
try {
|
|
530
|
+
return parseRegulatedCommerceTruth(value);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (error !== null && typeof error === "object" && VALIDATION_ERRORS.has(error)) throw error;
|
|
533
|
+
throw new FixtureValidationError("Invalid regulated-commerce truth: input could not be safely inspected");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Load the repository's canonical deterministic fixture as a fresh snapshot. */
|
|
538
|
+
export function loadSyntheticRegulatedCommerceTruth(): Readonly<RegulatedCommerceTruthFixture> {
|
|
539
|
+
return loadRegulatedCommerceTruth(FIXTURE_SOURCE);
|
|
540
|
+
}
|