@tokenoftrust/storefront-runner 2.0.16 → 2.0.18

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.
Files changed (90) hide show
  1. package/apps/storefront/env.d.ts +18 -0
  2. package/apps/storefront/migrations-apps/0012_test_destinations.sql +29 -0
  3. package/apps/storefront/src/components/blog/ReactionBar.astro +1 -1
  4. package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +365 -49
  5. package/apps/storefront/src/components/islands/checkoutVerificationFlow.ts +251 -0
  6. package/apps/storefront/src/config/compliance/rulesets.ts +18 -7
  7. package/apps/storefront/src/config/devTenantSeed.ts +24 -1
  8. package/apps/storefront/src/config/evidenceTenantRoster.ts +25 -0
  9. package/apps/storefront/src/config/publicationSpaceConfig.ts +25 -0
  10. package/apps/storefront/src/config/resolver.ts +5 -0
  11. package/apps/storefront/src/config/tenantAnalyticsFiles.ts +43 -0
  12. package/apps/storefront/src/config/tenants.ts +11 -0
  13. package/apps/storefront/src/layouts/Layout.astro +5 -1
  14. package/apps/storefront/src/lib/activity/killSwitch.ts +1 -1
  15. package/apps/storefront/src/lib/admin/consentSettingsClient.ts +113 -0
  16. package/apps/storefront/src/lib/apps/consentConfigRead.ts +24 -0
  17. package/apps/storefront/src/lib/apps/gtmMigrationDraft.ts +103 -0
  18. package/apps/storefront/src/lib/apps/routeParams.ts +45 -0
  19. package/apps/storefront/src/lib/auth/loginGate.ts +25 -0
  20. package/apps/storefront/src/lib/checkoutCommerce.ts +1 -1
  21. package/apps/storefront/src/lib/compliance/enforcement.ts +25 -3
  22. package/apps/storefront/src/lib/d1/schema-apps.ts +44 -0
  23. package/apps/storefront/src/lib/env.ts +13 -111
  24. package/apps/storefront/src/lib/fixtures/regulated-commerce-truth/index.ts +540 -0
  25. package/apps/storefront/src/lib/fixtures/ws3-readonly-clone-preflight/index.ts +495 -0
  26. package/apps/storefront/src/lib/integration/tenantAnalyticsDeclarations.ts +198 -0
  27. package/apps/storefront/src/lib/monitoring/manifest.ts +1 -1
  28. package/apps/storefront/src/lib/rawMarketingHtml.ts +9 -1
  29. package/apps/storefront/src/lib/runtimeEnv.ts +124 -0
  30. package/apps/storefront/src/lib/verification/totVerificationClient.ts +485 -0
  31. package/apps/storefront/src/middleware/candidatePin.ts +35 -0
  32. package/apps/storefront/src/middleware/controlPlane.ts +270 -0
  33. package/apps/storefront/src/middleware/index.ts +100 -796
  34. package/apps/storefront/src/middleware/publicRender.ts +43 -0
  35. package/apps/storefront/src/middleware/renderChain.ts +685 -0
  36. package/apps/storefront/src/middleware/routeBranch.ts +160 -0
  37. package/apps/storefront/src/middleware/staticBundle.ts +109 -0
  38. package/apps/storefront/src/pages/[...slug].astro +1 -0
  39. package/apps/storefront/src/pages/api/compliance/preflight.ts +179 -2
  40. package/apps/storefront/src/pages/index.astro +1 -0
  41. package/apps/storefront/vitest.config.ts +5 -0
  42. package/package.json +1 -1
  43. package/packages/public-runtime/package.json +1 -0
  44. package/packages/public-runtime/schemas/managed-analytics/v1/deliver-test-event-request.schema.json +122 -0
  45. package/packages/public-runtime/schemas/managed-analytics/v1/get-test-destination-delivery-request.schema.json +79 -0
  46. package/packages/public-runtime/schemas/managed-analytics/v1/register-test-destination-request.schema.json +93 -0
  47. package/packages/public-runtime/schemas/managed-analytics/v1/test-destination-delivery.schema.json +106 -0
  48. package/packages/public-runtime/schemas/managed-analytics/v1/test-destination.schema.json +77 -0
  49. package/packages/public-runtime/schemas/publication-space/v1/manifest.schema.json +195 -0
  50. package/packages/public-runtime/schemas/publication-space/v1/normalized-mount.schema.json +80 -0
  51. package/packages/public-runtime/schemas/publication-space/v1/ref.schema.json +20 -0
  52. package/packages/public-runtime/schemas/publication-version/v1/pointer.schema.json +28 -0
  53. package/packages/public-runtime/schemas/publication-version/v1/transition-request.schema.json +44 -0
  54. package/packages/public-runtime/schemas/publication-version/v1/transition.schema.json +47 -0
  55. package/packages/public-runtime/schemas/publication-version/v1/version.schema.json +58 -0
  56. package/packages/public-runtime/src/activity/catalog.ts +1 -1
  57. package/packages/public-runtime/src/candidate-index.ts +6 -0
  58. package/packages/public-runtime/src/customization-reconcile.ts +8 -0
  59. package/packages/public-runtime/src/html-cache-policy.ts +57 -0
  60. package/packages/public-runtime/src/index.ts +7 -0
  61. package/packages/public-runtime/src/integration/consent-broker.ts +115 -0
  62. package/packages/public-runtime/src/integration/interim-bridge.ts +111 -27
  63. package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +65 -0
  64. package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +81 -0
  65. package/packages/public-runtime/src/integration/render-phases.ts +8 -3
  66. package/packages/public-runtime/src/publication-artifact.ts +203 -0
  67. package/packages/public-runtime/src/publication-space.ts +853 -0
  68. package/packages/public-runtime/src/publication-version.ts +587 -0
  69. package/packages/public-runtime/src/route-tier.ts +257 -0
  70. package/packages/public-runtime/src/static-bundle.ts +18 -5
  71. package/packages/public-runtime/src/tenant-assets.ts +85 -17
  72. package/packages/public-runtime/src/tenant.ts +70 -0
  73. package/packages/public-runtime/tests/conformance/fake-destination.ts +402 -0
  74. package/packages/public-runtime/tests/fixtures/publication-spaces/internal.json +32 -0
  75. package/packages/public-runtime/tests/fixtures/publication-spaces/mounted-public.json +38 -0
  76. package/packages/public-runtime/tests/fixtures/publication-spaces/normalized-mounts.json +39 -0
  77. package/packages/public-runtime/tests/fixtures/publication-spaces/ref.json +5 -0
  78. package/packages/public-runtime/tests/fixtures/publication-spaces/root-public.json +31 -0
  79. package/packages/public-runtime/tests/fixtures/publication-versions/articles-v1.json +20 -0
  80. package/packages/public-runtime/tests/fixtures/publication-versions/articles-v2.json +20 -0
  81. package/packages/public-runtime/tests/fixtures/publication-versions/internal-v1.json +20 -0
  82. package/packages/public-runtime/tests/fixtures/publication-versions/live-pointer.json +11 -0
  83. package/packages/public-runtime/tests/fixtures/publication-versions/live-transition-request.json +14 -0
  84. package/packages/public-runtime/tests/fixtures/publication-versions/live-transition.json +17 -0
  85. package/packages/public-runtime/tests/fixtures/publication-versions/root-v1.json +20 -0
  86. package/packages/public-runtime/tests/fixtures/publication-versions/root-v2.json +20 -0
  87. package/scripts/dev/ai-edit.mjs +16 -3
  88. package/scripts/dev/port-check.mjs +18 -3
  89. package/scripts/dev/publish.mjs +26 -3
  90. package/scripts/tenant/validate.mjs +14 -1
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Test-only destination for integration conformance suites.
3
+ *
4
+ * This is deliberately not a `fetch` wrapper and accepts no URL, credential,
5
+ * tenant, or environment configuration. A caller hands an already-built
6
+ * delivery attempt to `deliver`; the harness records a redacted snapshot and
7
+ * returns the next planned result without opening a socket or reproducing a
8
+ * delivery runtime.
9
+ */
10
+
11
+ export const FAKE_DESTINATION_ID = "fixture:integration-conformance" as const;
12
+ export const FAKE_REDACTION_MARKER = "«redacted»" as const;
13
+
14
+ type JsonPrimitive = string | number | boolean | null;
15
+ export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
16
+
17
+ export type FakeDestinationOutcome =
18
+ | { kind: "success"; status: 200 | 201 | 202 | 204 }
19
+ | { kind: "retryable-failure"; status: 408 | 425 | 429 | 500 | 502 | 503 | 504; retryAfterMs?: number }
20
+ | { kind: "terminal-failure"; status: 400 | 401 | 403 | 404 | 409 | 410 | 422 };
21
+
22
+ export interface FakeDeliveryAttempt {
23
+ deliveryId: string;
24
+ eventType: string;
25
+ payload: unknown;
26
+ headers?: Readonly<Record<string, string>>;
27
+ }
28
+
29
+ export interface FakeDeliveryReceipt {
30
+ receiptId: string;
31
+ attemptNumber: number;
32
+ destination: typeof FAKE_DESTINATION_ID;
33
+ deliveryId: string;
34
+ eventType: string;
35
+ request: {
36
+ payload: JsonValue;
37
+ headers: Readonly<Record<string, string>>;
38
+ };
39
+ outcome: FakeDestinationOutcome;
40
+ }
41
+
42
+ export interface FakeDestination {
43
+ readonly id: typeof FAKE_DESTINATION_ID;
44
+ deliver(attempt: FakeDeliveryAttempt): Promise<FakeDeliveryReceipt>;
45
+ receipts(): readonly FakeDeliveryReceipt[];
46
+ assertReceipts(expected: readonly FakeDeliveryReceipt[]): void;
47
+ }
48
+
49
+ const UNSAFE_KEY = /(authorization|cookie|credential|password|private.?key|secret|signature|token|api.?key|email|phone|mobile|telephone|address|street|city|postal|zip|first.?name|last.?name|full.?name|customer.?name|recipient.?name|date.?of.?birth|birth.?date|dob|social.?security|ssn|card|cvv|custom.?html|javascript|script|code|template|loader|data.?layer|gtm|url|uri|endpoint|webhook)/i;
50
+ const AUTH_VALUE = /\b(?:basic|bearer)\s+[^\s,;]+/i;
51
+ const EMAIL_VALUE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
52
+ const PHONE_VALUE = /(?:^|\D)(?:\+?1[\s().-]*)?(?:\d[\s().-]*){7,15}(?:\D|$)/;
53
+ const SSN_VALUE = /(?:^|\D)\d{3}[ -]?\d{2}[ -]?\d{4}(?:\D|$)/;
54
+ const PAYMENT_NUMBER_VALUE = /(?:^|\D)(?:\d[ -]?){13,19}(?:\D|$)/;
55
+ const GOVERNMENT_ID_VALUE = /(?:^|\D)(?:\d{2}-\d{7}|[A-Z]\d{7,8})(?:\D|$)/i;
56
+ const ADDRESS_VALUE = /\b\d{1,6}\s+[A-Z0-9][A-Z0-9 .'-]{1,80}\s(?:street|st|road|rd|avenue|ave|boulevard|blvd|lane|ln|drive|dr|court|ct|way|parkway|pkwy)\b/i;
57
+ const PERSON_NAME_VALUE = /^(?:[A-Z][A-Z'-]{1,30})\s+(?:[A-Z][A-Z'-]{1,30})(?:\s+[A-Z][A-Z'-]{1,30}){0,2}$/i;
58
+ const TECHNICAL_IDENTIFIER_PREFIXES = new Set([
59
+ "delivery", "offline", "unexpected", "storefront", "order", "integration",
60
+ "event", "attempt", "receipt", "fixture", "synthetic", "fake", "test",
61
+ ]);
62
+ const IDENTIFIER_WORD = /^[A-Z]{2,30}$/i;
63
+ const HYPHENATED_ADDRESS_VALUE = /(?:^|-)\d{1,6}(?:-[A-Z0-9]+){1,5}-(?:street|st|road|rd|avenue|ave|boulevard|blvd|lane|ln|drive|dr|court|ct|way|parkway|pkwy)(?:-|$)/i;
64
+ const LIVE_URL_VALUE = /\b(?:https?|wss?):\/\//i;
65
+ const CODE_VALUE = /(?:<\/?[a-z][^>]*>|javascript\s*:|data\s*:\s*(?:text\/html|application\/javascript)|\b(?:eval|Function|import|require|alert|fetch|XMLHttpRequest)\s*\(|\b(?:document|window|globalThis|console|dataLayer)\s*\.|\bGTM-[A-Z0-9-]+\b|=>|\{\{[^}]+\}\})/i;
66
+ const CREDENTIAL_VALUE = /(-----BEGIN [A-Z ]*PRIVATE KEY-----|\bAKIA[0-9A-Z]{16}\b)/i;
67
+ const SAFE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
68
+ const SAFE_HEADERS = new Set(["content-type", "idempotency-key", "x-delivery-attempt", "x-event-type"]);
69
+ const ALLOWED_CONFIG_KEYS = new Set(["outcomes"]);
70
+ const ALLOWED_ATTEMPT_KEYS = new Set(["deliveryId", "eventType", "payload", "headers"]);
71
+ const MAX_SAFE_FIXTURE_NUMBER = 999_999;
72
+ const MAX_RETRY_AFTER_MS = 86_400_000;
73
+ const INTERNAL_REFUSALS = new WeakSet<object>();
74
+
75
+ function fail(message: string): never {
76
+ const error = new Error(`Fake destination configuration refused: ${message}`);
77
+ INTERNAL_REFUSALS.add(error);
78
+ throw error;
79
+ }
80
+
81
+ function inspect<T>(label: string, operation: () => T): T {
82
+ try {
83
+ return operation();
84
+ } catch (error) {
85
+ if (error !== null && typeof error === "object" && INTERNAL_REFUSALS.has(error)) throw error;
86
+ fail(`${label} could not be safely inspected`);
87
+ }
88
+ }
89
+
90
+ function isArray(value: unknown, label: string): value is unknown[] {
91
+ return inspect(label, () => Array.isArray(value));
92
+ }
93
+
94
+ interface DataField {
95
+ key: string;
96
+ value: unknown;
97
+ }
98
+
99
+ function readDataRecord(value: unknown, label: string): readonly DataField[] {
100
+ return inspect(label, () => {
101
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
102
+ fail(`${label} must be a plain object`);
103
+ }
104
+ const prototype = Object.getPrototypeOf(value);
105
+ if (prototype !== Object.prototype && prototype !== null) {
106
+ fail(`${label} must have a plain data prototype`);
107
+ }
108
+ const ownKeys = Reflect.ownKeys(value);
109
+ const keys: string[] = [];
110
+ for (let index = 0; index < ownKeys.length; index += 1) {
111
+ const key = ownKeys[index];
112
+ if (typeof key !== "string") fail(`${label} contains a non-string field`);
113
+ keys.push(key);
114
+ }
115
+ for (let index = 1; index < keys.length; index += 1) {
116
+ const key = keys[index]!;
117
+ let position = index - 1;
118
+ while (position >= 0 && keys[position]! > key) {
119
+ keys[position + 1] = keys[position]!;
120
+ position -= 1;
121
+ }
122
+ keys[position + 1] = key;
123
+ }
124
+ const fields: DataField[] = [];
125
+ for (let fieldIndex = 0; fieldIndex < keys.length; fieldIndex += 1) {
126
+ const key = keys[fieldIndex]!;
127
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
128
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
129
+ fail(`${label} field at index ${fieldIndex} must be enumerable own data`);
130
+ }
131
+ fields.push({ key, value: descriptor.value });
132
+ }
133
+ return fields;
134
+ });
135
+ }
136
+
137
+ function valuesByKey(fields: readonly DataField[]): ReadonlyMap<string, unknown> {
138
+ const values = new Map<string, unknown>();
139
+ for (const { key, value } of fields) values.set(key, value);
140
+ return values;
141
+ }
142
+
143
+ function readDataArray(value: unknown, label: string): readonly unknown[] {
144
+ return inspect(label, () => {
145
+ if (!Array.isArray(value)) fail(`${label} must be an array`);
146
+ if (Object.getPrototypeOf(value) !== Array.prototype) fail(`${label} must use the intrinsic array prototype`);
147
+
148
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
149
+ const length = lengthDescriptor && "value" in lengthDescriptor ? lengthDescriptor.value : -1;
150
+ if (!Number.isSafeInteger(length) || length < 0) fail(`${label} has an invalid length`);
151
+
152
+ const ownKeys = Reflect.ownKeys(value);
153
+ for (let fieldIndex = 0; fieldIndex < ownKeys.length; fieldIndex += 1) {
154
+ const key = ownKeys[fieldIndex];
155
+ if (key === "length") continue;
156
+ if (typeof key !== "string" || !/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= length) {
157
+ fail(`${label} contains a rejected array field`);
158
+ }
159
+ }
160
+
161
+ const items: unknown[] = [];
162
+ for (let index = 0; index < length; index += 1) {
163
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
164
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
165
+ fail(`${label} item at index ${index} must be dense enumerable own data`);
166
+ }
167
+ items.push(descriptor.value);
168
+ }
169
+ return items;
170
+ });
171
+ }
172
+
173
+ function isHyphenatedNameLike(value: string): boolean {
174
+ const words = value.split("-");
175
+ if (words.length < 2) return false;
176
+ for (let index = 0; index < words.length; index += 1) {
177
+ if (!IDENTIFIER_WORD.test(words[index]!)) return false;
178
+ }
179
+ const candidateWords = TECHNICAL_IDENTIFIER_PREFIXES.has(words[0]!.toLowerCase()) ? words.slice(1) : words;
180
+ return candidateWords.length >= 2;
181
+ }
182
+
183
+ function sanitizeString(value: string): string {
184
+ return AUTH_VALUE.test(value)
185
+ || EMAIL_VALUE.test(value)
186
+ || PHONE_VALUE.test(value)
187
+ || SSN_VALUE.test(value)
188
+ || PAYMENT_NUMBER_VALUE.test(value)
189
+ || GOVERNMENT_ID_VALUE.test(value)
190
+ || ADDRESS_VALUE.test(value)
191
+ || PERSON_NAME_VALUE.test(value)
192
+ || HYPHENATED_ADDRESS_VALUE.test(value)
193
+ || isHyphenatedNameLike(value)
194
+ || LIVE_URL_VALUE.test(value)
195
+ || CODE_VALUE.test(value)
196
+ || CREDENTIAL_VALUE.test(value)
197
+ ? FAKE_REDACTION_MARKER
198
+ : value;
199
+ }
200
+
201
+ function sanitizeIdentifier(value: string): string {
202
+ const sanitized = sanitizeString(value);
203
+ return sanitized === value && SAFE_IDENTIFIER.test(value) ? value : FAKE_REDACTION_MARKER;
204
+ }
205
+
206
+ function sanitizeJson(value: unknown, path = "payload", seen = new WeakSet<object>()): JsonValue {
207
+ if (value === null || typeof value === "boolean") return value;
208
+ if (typeof value === "number") {
209
+ if (!Number.isFinite(value)) fail(`${path} must contain finite JSON numbers`);
210
+ return Math.abs(value) <= MAX_SAFE_FIXTURE_NUMBER ? value : FAKE_REDACTION_MARKER;
211
+ }
212
+ if (typeof value === "string") return sanitizeString(value);
213
+ if (typeof value !== "object") fail(`${path} must be JSON-compatible`);
214
+ if (seen.has(value)) fail(`${path} must not be cyclic`);
215
+ seen.add(value);
216
+
217
+ if (isArray(value, path)) {
218
+ const entries = readDataArray(value, path);
219
+ const result: JsonValue[] = [];
220
+ for (let index = 0; index < entries.length; index += 1) {
221
+ result.push(sanitizeJson(entries[index], `${path}[${index}]`, seen));
222
+ }
223
+ seen.delete(value);
224
+ return result;
225
+ }
226
+
227
+ const fields = readDataRecord(value, path);
228
+ const result: Record<string, JsonValue> = {};
229
+ for (let fieldIndex = 0; fieldIndex < fields.length; fieldIndex += 1) {
230
+ const { key, value: entry } = fields[fieldIndex]!;
231
+ const safeKey = UNSAFE_KEY.test(key) || !SAFE_IDENTIFIER.test(key) ? FAKE_REDACTION_MARKER : key;
232
+ result[safeKey] = safeKey === FAKE_REDACTION_MARKER
233
+ ? FAKE_REDACTION_MARKER
234
+ : sanitizeJson(entry, `${path}.$field[${fieldIndex}]`, seen);
235
+ }
236
+ seen.delete(value);
237
+ return result;
238
+ }
239
+
240
+ function copyJsonData(value: unknown, path: string, seen = new WeakSet<object>()): JsonValue {
241
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
242
+ if (typeof value === "number") {
243
+ if (!Number.isFinite(value)) fail(`${path} must contain finite JSON numbers`);
244
+ return value;
245
+ }
246
+ if (typeof value !== "object") fail(`${path} must be JSON-compatible`);
247
+ if (seen.has(value)) fail(`${path} must not be cyclic`);
248
+ seen.add(value);
249
+
250
+ if (isArray(value, path)) {
251
+ const entries = readDataArray(value, path);
252
+ const result: JsonValue[] = [];
253
+ for (let index = 0; index < entries.length; index += 1) {
254
+ result.push(copyJsonData(entries[index], `${path}[${index}]`, seen));
255
+ }
256
+ seen.delete(value);
257
+ return result;
258
+ }
259
+
260
+ const fields = readDataRecord(value, path);
261
+ const result: Record<string, JsonValue> = {};
262
+ for (let fieldIndex = 0; fieldIndex < fields.length; fieldIndex += 1) {
263
+ const { key, value: entry } = fields[fieldIndex]!;
264
+ result[key] = copyJsonData(entry, `${path}.$field[${fieldIndex}]`, seen);
265
+ }
266
+ seen.delete(value);
267
+ return result;
268
+ }
269
+
270
+ function sanitizeHeaders(headers: Readonly<Record<string, string>> | undefined): Readonly<Record<string, string>> {
271
+ if (headers === undefined) return Object.freeze({});
272
+ const fields = readDataRecord(headers, "headers");
273
+ const sanitized: Record<string, string> = {};
274
+ for (let index = 0; index < fields.length; index += 1) {
275
+ const { key, value } = fields[index]!;
276
+ const name = key.toLowerCase();
277
+ if (typeof value !== "string") fail(`header at index ${index} must be a string`);
278
+ const safeName = UNSAFE_KEY.test(name) || !SAFE_HEADERS.has(name) ? FAKE_REDACTION_MARKER : name;
279
+ sanitized[safeName] = safeName === FAKE_REDACTION_MARKER
280
+ ? FAKE_REDACTION_MARKER
281
+ : sanitizeString(value);
282
+ }
283
+ return Object.freeze(sanitized);
284
+ }
285
+
286
+ function parseOutcome(outcome: unknown, index: number): FakeDestinationOutcome {
287
+ const fields = readDataRecord(outcome, `outcomes[${index}]`);
288
+ for (let fieldIndex = 0; fieldIndex < fields.length; fieldIndex += 1) {
289
+ const { key } = fields[fieldIndex]!;
290
+ if (key !== "kind" && key !== "status" && key !== "retryAfterMs") {
291
+ fail(`outcomes[${index}] field at index ${fieldIndex} is unsupported`);
292
+ }
293
+ }
294
+ const values = valuesByKey(fields);
295
+ const kind = values.get("kind");
296
+ const status = values.get("status");
297
+ const success = new Set([200, 201, 202, 204]);
298
+ const retryable = new Set([408, 425, 429, 500, 502, 503, 504]);
299
+ const terminal = new Set([400, 401, 403, 404, 409, 410, 422]);
300
+ const statusAllowed =
301
+ (kind === "success" && success.has(status as number)) ||
302
+ (kind === "retryable-failure" && retryable.has(status as number)) ||
303
+ (kind === "terminal-failure" && terminal.has(status as number));
304
+ if (!statusAllowed) fail(`outcomes[${index}] has an invalid kind/status pairing`);
305
+ if (values.has("retryAfterMs")) {
306
+ const retryAfterMs = values.get("retryAfterMs");
307
+ if (
308
+ kind !== "retryable-failure"
309
+ || !Number.isSafeInteger(retryAfterMs)
310
+ || (retryAfterMs as number) < 0
311
+ || (retryAfterMs as number) > MAX_RETRY_AFTER_MS
312
+ ) {
313
+ fail(`outcomes[${index}].retryAfterMs must be an integer from 0 through 86400000 on a retryable failure`);
314
+ }
315
+ return { kind, status: status as 408 | 425 | 429 | 500 | 502 | 503 | 504, retryAfterMs: retryAfterMs as number };
316
+ }
317
+ return { kind, status } as FakeDestinationOutcome;
318
+ }
319
+
320
+ function clone<T>(value: T): T {
321
+ return structuredClone(value);
322
+ }
323
+
324
+ function canonical(value: unknown): string {
325
+ return JSON.stringify(value);
326
+ }
327
+
328
+ /**
329
+ * Build an isolated deterministic destination. The outcome plan is finite:
330
+ * exhausting it is an assertion failure, so a conformance test cannot hide an
331
+ * unexpected delivery or retry.
332
+ */
333
+ export function createFakeDestination(config: { outcomes: readonly FakeDestinationOutcome[] }): FakeDestination {
334
+ const configFields = readDataRecord(config, "config");
335
+ for (let fieldIndex = 0; fieldIndex < configFields.length; fieldIndex += 1) {
336
+ const { key } = configFields[fieldIndex]!;
337
+ if (!ALLOWED_CONFIG_KEYS.has(key)) {
338
+ fail(`config field at index ${fieldIndex} is unsupported`);
339
+ }
340
+ }
341
+ const configValues = valuesByKey(configFields);
342
+ if (!configValues.has("outcomes")) {
343
+ fail("outcomes must contain at least one planned result");
344
+ }
345
+ const outcomeInputs = readDataArray(configValues.get("outcomes"), "outcomes");
346
+ if (outcomeInputs.length === 0) fail("outcomes must contain at least one planned result");
347
+ const outcomes: FakeDestinationOutcome[] = [];
348
+ for (let index = 0; index < outcomeInputs.length; index += 1) {
349
+ outcomes.push(parseOutcome(outcomeInputs[index], index));
350
+ }
351
+ const captured: FakeDeliveryReceipt[] = [];
352
+
353
+ return Object.freeze({
354
+ id: FAKE_DESTINATION_ID,
355
+ async deliver(attempt: FakeDeliveryAttempt): Promise<FakeDeliveryReceipt> {
356
+ const attemptFields = readDataRecord(attempt, "attempt");
357
+ for (let fieldIndex = 0; fieldIndex < attemptFields.length; fieldIndex += 1) {
358
+ const { key } = attemptFields[fieldIndex]!;
359
+ if (!ALLOWED_ATTEMPT_KEYS.has(key)) fail(`attempt field at index ${fieldIndex} is unsupported`);
360
+ }
361
+ const attemptValues = valuesByKey(attemptFields);
362
+ const deliveryId = attemptValues.get("deliveryId");
363
+ const eventType = attemptValues.get("eventType");
364
+ if (typeof deliveryId !== "string" || deliveryId.length === 0) fail("deliveryId is required");
365
+ if (typeof eventType !== "string" || eventType.length === 0) fail("eventType is required");
366
+
367
+ const outcome = outcomes[captured.length];
368
+ if (!outcome) {
369
+ throw new Error(`Fake destination outcome plan exhausted after ${captured.length} attempt(s)`);
370
+ }
371
+ const attemptNumber = captured.length + 1;
372
+ const receipt: FakeDeliveryReceipt = {
373
+ receiptId: `fake-receipt-${String(attemptNumber).padStart(4, "0")}`,
374
+ attemptNumber,
375
+ destination: FAKE_DESTINATION_ID,
376
+ deliveryId: sanitizeIdentifier(deliveryId),
377
+ eventType: sanitizeIdentifier(eventType),
378
+ request: {
379
+ payload: sanitizeJson(attemptValues.get("payload")),
380
+ headers: sanitizeHeaders(attemptValues.get("headers") as Readonly<Record<string, string>> | undefined),
381
+ },
382
+ outcome: clone(outcome),
383
+ };
384
+ captured.push(receipt);
385
+ return clone(receipt);
386
+ },
387
+ receipts(): readonly FakeDeliveryReceipt[] {
388
+ return clone(captured);
389
+ },
390
+ assertReceipts(expected: readonly FakeDeliveryReceipt[]): void {
391
+ const expectedSnapshot = copyJsonData(expected, "expected receipts");
392
+ if (!Array.isArray(expectedSnapshot)) fail("expected receipts must be an array");
393
+ const actualJson = canonical(copyJsonData(captured, "captured receipts"));
394
+ const expectedJson = canonical(expectedSnapshot);
395
+ if (actualJson !== expectedJson) {
396
+ throw new Error(
397
+ `Fake destination receipts did not match (${expectedSnapshot.length} expected, ${captured.length} captured)`,
398
+ );
399
+ }
400
+ },
401
+ });
402
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "version": 1,
3
+ "id": "knowledge",
4
+ "tenant": "fixture.example",
5
+ "source": {
6
+ "forge": "fixture",
7
+ "repo": "fixture-private-publications",
8
+ "defaultBranch": "main"
9
+ },
10
+ "routes": {
11
+ "canonicalMount": "/knowledge"
12
+ },
13
+ "visibility": "internal",
14
+ "authorization": {
15
+ "policy": "capability",
16
+ "capability": "internal-content-reader"
17
+ },
18
+ "discovery": {
19
+ "sitemap": false,
20
+ "feeds": false,
21
+ "publicSearch": false,
22
+ "llms": false
23
+ },
24
+ "assets": {
25
+ "namespace": "knowledge",
26
+ "access": "authorized"
27
+ },
28
+ "renderer": {
29
+ "contractVersion": "storefront-v1",
30
+ "themeContractVersion": "theme-v1"
31
+ }
32
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "version": 1,
3
+ "id": "articles",
4
+ "tenant": "fixture.example",
5
+ "source": {
6
+ "forge": "fixture",
7
+ "repo": "fixture-publications",
8
+ "defaultBranch": "main"
9
+ },
10
+ "routes": {
11
+ "canonicalMount": "/blog",
12
+ "aliases": [
13
+ {
14
+ "from": "/the-build",
15
+ "behavior": "redirect-308",
16
+ "to": "/blog"
17
+ }
18
+ ]
19
+ },
20
+ "visibility": "public",
21
+ "authorization": {
22
+ "policy": "public"
23
+ },
24
+ "discovery": {
25
+ "sitemap": true,
26
+ "feeds": true,
27
+ "publicSearch": true,
28
+ "llms": true
29
+ },
30
+ "assets": {
31
+ "namespace": "articles",
32
+ "access": "public"
33
+ },
34
+ "renderer": {
35
+ "contractVersion": "storefront-v1",
36
+ "themeContractVersion": "theme-v1"
37
+ }
38
+ }
@@ -0,0 +1,39 @@
1
+ [
2
+ {
3
+ "version": 1,
4
+ "tenant": "fixture.example",
5
+ "spaceId": "knowledge",
6
+ "kind": "canonical",
7
+ "mount": "/knowledge",
8
+ "canonicalMount": "/knowledge",
9
+ "behavior": "render"
10
+ },
11
+ {
12
+ "version": 1,
13
+ "tenant": "fixture.example",
14
+ "spaceId": "articles",
15
+ "kind": "alias",
16
+ "mount": "/the-build",
17
+ "canonicalMount": "/blog",
18
+ "behavior": "redirect-308",
19
+ "redirectTo": "/blog"
20
+ },
21
+ {
22
+ "version": 1,
23
+ "tenant": "fixture.example",
24
+ "spaceId": "articles",
25
+ "kind": "canonical",
26
+ "mount": "/blog",
27
+ "canonicalMount": "/blog",
28
+ "behavior": "render"
29
+ },
30
+ {
31
+ "version": 1,
32
+ "tenant": "fixture.example",
33
+ "spaceId": "root",
34
+ "kind": "canonical",
35
+ "mount": "/",
36
+ "canonicalMount": "/",
37
+ "behavior": "render"
38
+ }
39
+ ]
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": 1,
3
+ "tenant": "fixture.example",
4
+ "spaceId": "articles"
5
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "version": 1,
3
+ "id": "root",
4
+ "tenant": "fixture.example",
5
+ "source": {
6
+ "forge": "fixture",
7
+ "repo": "fixture.example",
8
+ "defaultBranch": "main"
9
+ },
10
+ "routes": {
11
+ "canonicalMount": "/"
12
+ },
13
+ "visibility": "public",
14
+ "authorization": {
15
+ "policy": "public"
16
+ },
17
+ "discovery": {
18
+ "sitemap": true,
19
+ "feeds": true,
20
+ "publicSearch": true,
21
+ "llms": true
22
+ },
23
+ "assets": {
24
+ "namespace": "root",
25
+ "access": "public"
26
+ },
27
+ "renderer": {
28
+ "contractVersion": "storefront-v1",
29
+ "themeContractVersion": "theme-v1"
30
+ }
31
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "versionId": "pv_articles-version-0001",
4
+ "tenant": "fixture.example",
5
+ "spaceId": "articles",
6
+ "source": {
7
+ "forge": "fixture",
8
+ "repo": "fixture-publications",
9
+ "commitSha": "3333333333333333333333333333333333333333"
10
+ },
11
+ "renderer": {
12
+ "contractVersion": "storefront-v1",
13
+ "themeContractVersion": "theme-v1"
14
+ },
15
+ "artifactDigest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
16
+ "createdAt": "2026-09-01T12:02:00.000Z",
17
+ "metadata": {
18
+ "buildId": "build-articles-0001"
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "versionId": "pv_articles-version-0002",
4
+ "tenant": "fixture.example",
5
+ "spaceId": "articles",
6
+ "source": {
7
+ "forge": "fixture",
8
+ "repo": "fixture-publications",
9
+ "commitSha": "4444444444444444444444444444444444444444"
10
+ },
11
+ "renderer": {
12
+ "contractVersion": "storefront-v1",
13
+ "themeContractVersion": "theme-v1"
14
+ },
15
+ "artifactDigest": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
16
+ "createdAt": "2026-09-01T12:03:00.000Z",
17
+ "metadata": {
18
+ "buildId": "build-articles-0002"
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "versionId": "pv_knowledge-version-0001",
4
+ "tenant": "fixture.example",
5
+ "spaceId": "knowledge",
6
+ "source": {
7
+ "forge": "fixture",
8
+ "repo": "fixture-private-publications",
9
+ "commitSha": "5555555555555555555555555555555555555555"
10
+ },
11
+ "renderer": {
12
+ "contractVersion": "storefront-v1",
13
+ "themeContractVersion": "theme-v1"
14
+ },
15
+ "artifactDigest": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
16
+ "createdAt": "2026-09-01T12:04:00.000Z",
17
+ "metadata": {
18
+ "buildId": "build-knowledge-0001"
19
+ }
20
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "tenant": "fixture.example",
4
+ "spaceId": "articles",
5
+ "channel": "live",
6
+ "revision": 1,
7
+ "versionId": "pv_articles-version-0001",
8
+ "sourceSha": "3333333333333333333333333333333333333333",
9
+ "artifactDigest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
10
+ "updatedAt": "2026-09-01T12:05:00.000Z"
11
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "transitionId": "activate-articles-0001",
4
+ "operation": "activate",
5
+ "tenant": "fixture.example",
6
+ "spaceId": "articles",
7
+ "channel": "live",
8
+ "targetVersionId": "pv_articles-version-0001",
9
+ "expectedSourceSha": "3333333333333333333333333333333333333333",
10
+ "expectedArtifactDigest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
11
+ "expectedRevision": 0,
12
+ "principal": "publisher.fixture",
13
+ "occurredAt": "2026-09-01T12:05:00.000Z"
14
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "transitionId": "activate-articles-0001",
4
+ "operation": "activate",
5
+ "tenant": "fixture.example",
6
+ "spaceId": "articles",
7
+ "channel": "live",
8
+ "previous": null,
9
+ "next": {
10
+ "revision": 1,
11
+ "versionId": "pv_articles-version-0001",
12
+ "sourceSha": "3333333333333333333333333333333333333333",
13
+ "artifactDigest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
14
+ },
15
+ "principal": "publisher.fixture",
16
+ "occurredAt": "2026-09-01T12:05:00.000Z"
17
+ }