@gradial/aci 0.1.45 → 0.1.47

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/README.md +4 -4
  2. package/dist/astro/error-page.d.ts +56 -0
  3. package/dist/astro/error-page.js +79 -0
  4. package/dist/astro/index.d.ts +4 -0
  5. package/dist/astro/index.js +9 -0
  6. package/dist/audience-data-models.d.ts +142 -2
  7. package/dist/audience-data-models.js +2 -0
  8. package/dist/cli/compile-registry.d.mts +1 -5
  9. package/dist/cli/compile-registry.mjs +10 -25
  10. package/dist/cli/emit-error-pages.d.mts +39 -0
  11. package/dist/cli/emit-error-pages.mjs +402 -0
  12. package/dist/cli/validate-content.d.mts +1 -31
  13. package/dist/cli/validate-content.mjs +12 -50
  14. package/dist/cli/verify-next-cdn-routes.mjs +1 -0
  15. package/dist/content/default-error-page-html.d.ts +2 -0
  16. package/dist/content/default-error-page-html.js +6 -0
  17. package/dist/content/default-error-page.d.ts +23 -0
  18. package/dist/content/default-error-page.js +34 -0
  19. package/dist/content/error-page-kind.d.ts +57 -0
  20. package/dist/content/error-page-kind.js +106 -0
  21. package/dist/content/error-pages.d.ts +88 -0
  22. package/dist/content/error-pages.js +120 -0
  23. package/dist/content/index.d.ts +2 -0
  24. package/dist/content/index.js +2 -0
  25. package/dist/content/provider.d.ts +1 -1
  26. package/dist/content/resolve-slots.js +0 -1
  27. package/dist/content/types.d.ts +12 -2
  28. package/dist/internal/render-proof.d.ts +1 -0
  29. package/dist/internal/render-proof.js +197 -0
  30. package/dist/next/config.js +17 -0
  31. package/dist/next/error-page-instrumentation.d.ts +23 -0
  32. package/dist/next/error-page-instrumentation.js +36 -0
  33. package/dist/next/error-page-marker.d.ts +11 -0
  34. package/dist/next/error-page-marker.js +20 -0
  35. package/dist/next/error-pages.d.ts +52 -0
  36. package/dist/next/error-pages.js +94 -0
  37. package/dist/next/error-seat-payload.d.ts +33 -0
  38. package/dist/next/error-seat-payload.js +143 -0
  39. package/dist/next/error-seat.d.ts +57 -0
  40. package/dist/next/error-seat.js +83 -0
  41. package/dist/next/index.d.ts +4 -0
  42. package/dist/next/index.js +8 -0
  43. package/dist/next/middleware.js +9 -54
  44. package/dist/next/page.js +9 -4
  45. package/dist/next/personalization-host.d.ts +5 -0
  46. package/dist/next/server.js +88 -75
  47. package/dist/next/vercel-collect-sdk-route.js +6 -30
  48. package/dist/next/vercel-edge-projection-v1.d.ts +24 -0
  49. package/dist/next/vercel-edge-projection-v1.js +880 -0
  50. package/dist/next/vercel-s3-personalization.js +7 -11
  51. package/dist/providers/s3.d.ts +2 -4
  52. package/dist/providers/s3.js +173 -19
  53. package/dist/types/block-ref.d.ts +0 -2
  54. package/dist/types/block-ref.js +0 -1
  55. package/package.json +15 -9
  56. package/src/cli/compile-registry.mjs +10 -24
  57. package/src/cli/emit-error-pages.mjs +424 -0
  58. package/src/cli/validate-content.mjs +13 -52
  59. package/src/cli/verify-next-cdn-routes.mjs +6 -0
  60. package/src/types/block-ref.ts +0 -2
  61. package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +6 -7
  62. package/templates/astro/starter-content/src/fixtures/composition.ts +2 -3
  63. package/templates/astro/starter-content/src/fixtures/sections.ts +1 -2
  64. package/templates/astro/template/package.json.tmpl +1 -1
  65. package/templates/astro/template/src/aci/error-page-instrumentation.mjs +33 -0
  66. package/templates/astro/template/src/pages/404.astro +54 -0
  67. package/templates/astro/template/src/pages/500.astro +53 -0
  68. package/templates/astro/template/src/pages/[...slug].astro +15 -2
  69. package/templates/common/.agents/skills/authoring/SKILL.md +74 -11
  70. package/templates/common/.agents/skills/authoring/references/block-slots.md +0 -3
  71. package/templates/common/.content/config/robots.txt +4 -0
  72. package/templates/common/.content/config/site.json.tmpl +4 -0
  73. package/templates/common/.content/pages/404/_index.json +31 -0
  74. package/templates/common/.content/pages/error/_index.json +31 -0
  75. package/templates/common/.content/pages/home/_index.json +0 -10
  76. package/templates/common/.content/schemas/site.schema.json +9 -0
  77. package/templates/common/starter-content/.content/config/robots.txt +2 -0
  78. package/templates/common/starter-content/.content/config/site.json +4 -0
  79. package/templates/common/starter-content/.content/pages/404/_index.json +31 -0
  80. package/templates/common/starter-content/.content/pages/about/_index.json +0 -3
  81. package/templates/common/starter-content/.content/pages/error/_index.json +31 -0
  82. package/templates/common/starter-content/.content/pages/home/_index.json +21 -14
  83. package/templates/common/starter-content/.content/schemas/site.schema.json +9 -0
  84. package/templates/nextjs/template/package.json.tmpl +1 -1
  85. package/templates/nextjs/template/src/aci/error-page-instrumentation.d.mts +4 -0
  86. package/templates/nextjs/template/src/aci/error-page-instrumentation.mjs +38 -0
  87. package/templates/nextjs/template/src/app/error.tsx +18 -0
  88. package/templates/nextjs/template/src/app/not-found.tsx +13 -0
  89. package/src/types/block-ref.test.ts +0 -45
  90. package/templates/nextjs/template/src/app/collect/route.ts +0 -18
@@ -0,0 +1,880 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { inflateRawSync } from 'node:zlib';
3
+ export const VERCEL_EDGE_PROJECTION_SCHEMA_V1 = 'gradial.vercel.edgeRoutes.v1';
4
+ export const VERCEL_EDGE_PROJECTION_ENCODING_V1 = 'gzip+base64';
5
+ export const MAX_VERCEL_EDGE_MANIFEST_BYTES_V1 = 1 << 20;
6
+ const ACTIVE_RELEASE_V1_PREFIX = 'activeReleaseV1_';
7
+ const EDGE_ROUTES_V1_PREFIX = 'edgeRoutesV1_';
8
+ const MAX_EDGE_CONFIG_VALUE_BYTES = 65_536;
9
+ const MAX_JSON_DEPTH = 64;
10
+ const SHA256_HEX = /^[0-9a-f]{64}$/;
11
+ const POSITIVE_DECIMAL = /^[1-9][0-9]*$/;
12
+ const MAX_INT64 = 9223372036854775807n;
13
+ const CANONICAL_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
14
+ const NORMALIZED_GZIP_HEADER = Buffer.from([
15
+ 0x1f, 0x8b, 0x08, 0x00, 0x00,
16
+ 0x00, 0x00, 0x00, 0x02, 0xff,
17
+ ]);
18
+ const textDecoder = new TextDecoder('utf-8', { fatal: true });
19
+ export function activeReleaseV1Key(siteId) {
20
+ return `${ACTIVE_RELEASE_V1_PREFIX}${edgeKeySegment(siteId)}`;
21
+ }
22
+ export function edgeRoutesV1Key(siteId, releaseId) {
23
+ return `${EDGE_ROUTES_V1_PREFIX}${edgeKeySegment(siteId)}_${edgeKeySegment(releaseId)}`;
24
+ }
25
+ export function deploymentCompatibilityKey(siteId, deploymentId) {
26
+ return `deploymentCompatibility_${edgeKeySegment(siteId)}_${edgeKeySegment(deploymentId)}`;
27
+ }
28
+ export function decodeActiveReleaseV1(value) {
29
+ const pointer = parseCanonicalJSONStringRecord(value, 'v1 active release pointer');
30
+ const orderedKeys = ['releaseId', 'sequence'];
31
+ assertExactKeys(pointer, orderedKeys, [], 'v1 active release pointer');
32
+ if (Object.keys(pointer).some((key, index) => key !== orderedKeys[index])) {
33
+ throw new Error('v1 active release pointer fields are not canonical');
34
+ }
35
+ return {
36
+ releaseId: requiredIdentity(pointer.releaseId, 'v1 active release ID'),
37
+ sequence: requiredPositiveDecimalBigInt(pointer.sequence, 'v1 active release sequence'),
38
+ };
39
+ }
40
+ export function decodeDeploymentCompatibilityV1(value) {
41
+ const compatibility = parseCanonicalJSONStringRecord(value, 'v1 deployment compatibility');
42
+ const orderedKeys = [
43
+ 'minReleaseId',
44
+ 'minSequence',
45
+ 'maxReleaseId',
46
+ 'maxSequence',
47
+ ];
48
+ assertExactKeys(compatibility, [], orderedKeys, 'v1 deployment compatibility');
49
+ const actualKeys = Object.keys(compatibility);
50
+ const expectedKeys = orderedKeys.filter((key) => (Object.prototype.hasOwnProperty.call(compatibility, key)));
51
+ if (actualKeys.some((key, index) => key !== expectedKeys[index])) {
52
+ throw new Error('v1 deployment compatibility fields are not canonical');
53
+ }
54
+ const hasMinRelease = Object.prototype.hasOwnProperty.call(compatibility, 'minReleaseId');
55
+ const hasMinSequence = Object.prototype.hasOwnProperty.call(compatibility, 'minSequence');
56
+ const hasMaxRelease = Object.prototype.hasOwnProperty.call(compatibility, 'maxReleaseId');
57
+ const hasMaxSequence = Object.prototype.hasOwnProperty.call(compatibility, 'maxSequence');
58
+ if (hasMinRelease !== hasMinSequence || hasMaxRelease !== hasMaxSequence) {
59
+ throw new Error('v1 deployment compatibility release IDs and sequences must be paired');
60
+ }
61
+ return {
62
+ ...(hasMinRelease
63
+ ? {
64
+ minReleaseId: requiredIdentity(compatibility.minReleaseId, 'v1 deployment compatibility minimum release ID'),
65
+ minSequence: requiredPositiveDecimalBigInt(compatibility.minSequence, 'v1 deployment compatibility minimum sequence'),
66
+ }
67
+ : {}),
68
+ ...(hasMaxRelease
69
+ ? {
70
+ maxReleaseId: requiredIdentity(compatibility.maxReleaseId, 'v1 deployment compatibility maximum release ID'),
71
+ maxSequence: requiredPositiveDecimalBigInt(compatibility.maxSequence, 'v1 deployment compatibility maximum sequence'),
72
+ }
73
+ : {}),
74
+ };
75
+ }
76
+ export function clampDeploymentCompatibleReleaseV1(active, compatibility) {
77
+ if (compatibility.minSequence !== undefined
78
+ && active.sequence < compatibility.minSequence
79
+ && compatibility.minReleaseId) {
80
+ return compatibility.minReleaseId;
81
+ }
82
+ if (compatibility.maxSequence !== undefined
83
+ && active.sequence > compatibility.maxSequence
84
+ && compatibility.maxReleaseId) {
85
+ return compatibility.maxReleaseId;
86
+ }
87
+ return active.releaseId;
88
+ }
89
+ export function decodeEdgeRouteProjectionV1(value, expected) {
90
+ const envelope = parseEnvelope(value);
91
+ const expectedSiteId = requiredIdentity(expected.siteId, 'expected site ID');
92
+ const expectedReleaseId = requiredIdentity(expected.releaseId, 'expected release ID');
93
+ if (envelope.siteId !== expectedSiteId
94
+ || envelope.releaseId !== expectedReleaseId) {
95
+ throw new Error(`v1 edge projection identity mismatch: got `
96
+ + `${envelope.siteId}/${envelope.releaseId}, `
97
+ + `want ${expectedSiteId}/${expectedReleaseId}`);
98
+ }
99
+ const compressed = decodeCanonicalBase64(envelope.payload);
100
+ const runtimeBytes = inflateOneNormalizedGzipMember(compressed, envelope.uncompressedLength);
101
+ const runtimeDigest = sha256(runtimeBytes);
102
+ if (runtimeDigest !== envelope.runtimeSha256) {
103
+ throw new Error(`v1 edge projection runtime digest mismatch: got ${runtimeDigest}, `
104
+ + `want ${envelope.runtimeSha256}`);
105
+ }
106
+ let runtimeJSON;
107
+ try {
108
+ runtimeJSON = textDecoder.decode(runtimeBytes);
109
+ }
110
+ catch (error) {
111
+ throw new Error('v1 edge projection runtime payload is not valid UTF-8', {
112
+ cause: error,
113
+ });
114
+ }
115
+ assertCanonicalJSONSyntax(runtimeJSON, 'v1 edge projection runtime payload');
116
+ let parsed;
117
+ try {
118
+ parsed = JSON.parse(runtimeJSON);
119
+ }
120
+ catch (error) {
121
+ throw new Error('v1 edge projection runtime payload is malformed JSON', {
122
+ cause: error,
123
+ });
124
+ }
125
+ const decoded = validateRuntimeProjection(parsed, {
126
+ siteId: expectedSiteId,
127
+ releaseId: expectedReleaseId,
128
+ });
129
+ if (canonicalRuntimeProjectionJSON(parsed) !== runtimeJSON) {
130
+ throw new Error('v1 edge projection runtime payload is not the canonical Go encoding');
131
+ }
132
+ return decoded;
133
+ }
134
+ function parseEnvelope(value) {
135
+ const record = parseCanonicalJSONStringRecord(value, 'v1 edge projection envelope');
136
+ const orderedKeys = [
137
+ 'schemaVersion',
138
+ 'encoding',
139
+ 'siteId',
140
+ 'releaseId',
141
+ 'sourceSha256',
142
+ 'uncompressedLength',
143
+ 'runtimeSha256',
144
+ 'payload',
145
+ ];
146
+ assertExactKeys(record, orderedKeys, [], 'v1 edge projection envelope');
147
+ if (Object.keys(record).some((key, index) => key !== orderedKeys[index])) {
148
+ throw new Error('v1 edge projection envelope fields are not canonical');
149
+ }
150
+ const envelope = {
151
+ schemaVersion: requiredString(record.schemaVersion, 'v1 edge projection schema version'),
152
+ encoding: requiredString(record.encoding, 'v1 edge projection encoding'),
153
+ siteId: requiredIdentity(record.siteId, 'v1 edge projection site ID'),
154
+ releaseId: requiredIdentity(record.releaseId, 'v1 edge projection release ID'),
155
+ sourceSha256: requiredSHA256(record.sourceSha256, 'v1 edge projection source digest'),
156
+ uncompressedLength: requiredPositiveSafeInteger(record.uncompressedLength, 'v1 edge projection uncompressed length'),
157
+ runtimeSha256: requiredSHA256(record.runtimeSha256, 'v1 edge projection runtime digest'),
158
+ payload: requiredString(record.payload, 'v1 edge projection payload'),
159
+ };
160
+ if (envelope.schemaVersion !== VERCEL_EDGE_PROJECTION_SCHEMA_V1) {
161
+ throw new Error(`unsupported v1 edge projection schema ${JSON.stringify(envelope.schemaVersion)}`);
162
+ }
163
+ if (envelope.encoding !== VERCEL_EDGE_PROJECTION_ENCODING_V1) {
164
+ throw new Error(`unsupported v1 edge projection encoding ${JSON.stringify(envelope.encoding)}`);
165
+ }
166
+ if (envelope.uncompressedLength > MAX_VERCEL_EDGE_MANIFEST_BYTES_V1) {
167
+ throw new Error(`v1 edge projection declares ${envelope.uncompressedLength} bytes, `
168
+ + `maximum ${MAX_VERCEL_EDGE_MANIFEST_BYTES_V1}`);
169
+ }
170
+ return envelope;
171
+ }
172
+ function parseCanonicalJSONStringRecord(value, label) {
173
+ if (typeof value !== 'string') {
174
+ throw new Error(`${label} must be a canonical JSON string`);
175
+ }
176
+ if (Buffer.byteLength(value, 'utf8') > MAX_EDGE_CONFIG_VALUE_BYTES) {
177
+ throw new Error(`${label} exceeds the Edge Config value bound`);
178
+ }
179
+ assertCanonicalJSONSyntax(value, label);
180
+ let parsed;
181
+ try {
182
+ parsed = JSON.parse(value);
183
+ }
184
+ catch (error) {
185
+ throw new Error(`${label} is malformed JSON`, { cause: error });
186
+ }
187
+ const record = requiredRecord(parsed, label);
188
+ if (canonicalRecordJSONInInputOrder(record) !== value) {
189
+ throw new Error(`${label} is not canonical JSON`);
190
+ }
191
+ return record;
192
+ }
193
+ function decodeCanonicalBase64(value) {
194
+ if (value.length === 0
195
+ || value.length % 4 !== 0
196
+ || !CANONICAL_BASE64.test(value)) {
197
+ throw new Error('v1 edge projection payload is not canonical padded base64');
198
+ }
199
+ const decoded = Buffer.from(value, 'base64');
200
+ if (decoded.toString('base64') !== value) {
201
+ throw new Error('v1 edge projection payload is not canonical padded base64');
202
+ }
203
+ return decoded;
204
+ }
205
+ function inflateOneNormalizedGzipMember(compressed, declaredLength) {
206
+ if (compressed.length < NORMALIZED_GZIP_HEADER.length + 8
207
+ || !compressed.subarray(0, NORMALIZED_GZIP_HEADER.length)
208
+ .equals(NORMALIZED_GZIP_HEADER)) {
209
+ throw new Error('v1 edge projection gzip header is not normalized');
210
+ }
211
+ let result;
212
+ try {
213
+ result = inflateRawSync(compressed.subarray(NORMALIZED_GZIP_HEADER.length), {
214
+ info: true,
215
+ maxOutputLength: declaredLength,
216
+ });
217
+ }
218
+ catch (error) {
219
+ throw new Error('v1 edge projection gzip payload is corrupt or oversized', {
220
+ cause: error,
221
+ });
222
+ }
223
+ const deflateBytes = result.engine.bytesWritten;
224
+ const trailerOffset = NORMALIZED_GZIP_HEADER.length + deflateBytes;
225
+ if (!Number.isSafeInteger(deflateBytes)
226
+ || deflateBytes <= 0
227
+ || trailerOffset + 8 !== compressed.length) {
228
+ throw new Error('v1 edge projection must contain exactly one gzip member with no trailing bytes');
229
+ }
230
+ if (result.buffer.byteLength !== declaredLength) {
231
+ throw new Error(`v1 edge projection decompressed length is ${result.buffer.byteLength}, `
232
+ + `want ${declaredLength}`);
233
+ }
234
+ const expectedCRC = compressed.readUInt32LE(trailerOffset);
235
+ const actualCRC = crc32(result.buffer);
236
+ if (actualCRC !== expectedCRC) {
237
+ throw new Error(`v1 edge projection gzip CRC32 is ${hex32(actualCRC)}, `
238
+ + `want ${hex32(expectedCRC)}`);
239
+ }
240
+ const expectedSize = compressed.readUInt32LE(trailerOffset + 4);
241
+ if (expectedSize !== result.buffer.byteLength) {
242
+ throw new Error(`v1 edge projection gzip size is ${expectedSize}, `
243
+ + `want ${result.buffer.byteLength}`);
244
+ }
245
+ return result.buffer;
246
+ }
247
+ function validateRuntimeProjection(value, expected) {
248
+ const record = requiredRecord(value, 'v1 edge runtime projection');
249
+ assertExactKeys(record, ['manifestVersion', 'releaseId', 'siteId', 'routes'], ['experiments', 'collectSdk'], 'v1 edge runtime projection');
250
+ const manifestVersion = requiredString(record.manifestVersion, 'v1 edge runtime manifest version');
251
+ if (manifestVersion !== 'gradial.edge.v1') {
252
+ throw new Error(`unsupported v1 edge runtime manifest version ${JSON.stringify(manifestVersion)}`);
253
+ }
254
+ const releaseId = requiredIdentity(record.releaseId, 'v1 edge runtime release ID');
255
+ const siteId = requiredIdentity(record.siteId, 'v1 edge runtime site ID');
256
+ if (siteId !== expected.siteId || releaseId !== expected.releaseId) {
257
+ throw new Error(`v1 edge runtime identity mismatch: got ${siteId}/${releaseId}, `
258
+ + `want ${expected.siteId}/${expected.releaseId}`);
259
+ }
260
+ const routesRecord = requiredRecord(record.routes, 'v1 edge runtime routes');
261
+ const routes = {};
262
+ for (const [routeKey, routeValue] of Object.entries(routesRecord)) {
263
+ defineOwnEnumerable(routes, routeKey, validateRoute(routeKey, routeValue));
264
+ }
265
+ let experiments;
266
+ if (Object.prototype.hasOwnProperty.call(record, 'experiments')) {
267
+ const experimentsRecord = requiredRecord(record.experiments, 'v1 edge runtime experiments');
268
+ experiments = {};
269
+ for (const [experimentKey, experimentValue] of Object.entries(experimentsRecord)) {
270
+ const experiment = validateExperiment(experimentValue);
271
+ if (experiment.id !== experimentKey) {
272
+ throw new Error(`v1 edge runtime experiment ${JSON.stringify(experimentKey)} `
273
+ + `has identity ${JSON.stringify(experiment.id)}`);
274
+ }
275
+ defineOwnEnumerable(experiments, experimentKey, experiment);
276
+ }
277
+ if (Object.keys(experiments).length === 0) {
278
+ throw new Error('v1 edge runtime experiments must be omitted when empty');
279
+ }
280
+ }
281
+ let collectSdk;
282
+ if (Object.prototype.hasOwnProperty.call(record, 'collectSdk')) {
283
+ collectSdk = validateCollectSDK(record.collectSdk);
284
+ }
285
+ return {
286
+ manifestVersion,
287
+ releaseId,
288
+ siteId,
289
+ routes,
290
+ ...(experiments ? { experiments } : {}),
291
+ ...(collectSdk ? { collectSdk } : {}),
292
+ };
293
+ }
294
+ function validateRoute(routeKey, value) {
295
+ const record = requiredRecord(value, `v1 edge route ${JSON.stringify(routeKey)}`);
296
+ assertExactKeys(record, ['pattern', 'priority', 'dispatch'], ['variants'], `v1 edge route ${JSON.stringify(routeKey)}`);
297
+ const pattern = requiredString(record.pattern, 'v1 edge route pattern');
298
+ if (pattern !== routeKey) {
299
+ throw new Error(`v1 edge route ${JSON.stringify(routeKey)} has pattern `
300
+ + `${JSON.stringify(pattern)}`);
301
+ }
302
+ let variants;
303
+ if (Object.prototype.hasOwnProperty.call(record, 'variants')) {
304
+ const variantsRecord = requiredRecord(record.variants, `v1 edge route ${JSON.stringify(routeKey)} variants`);
305
+ variants = {};
306
+ for (const [variantKey, variantValue] of Object.entries(variantsRecord)) {
307
+ defineOwnEnumerable(variants, variantKey, validateVariant(variantKey, variantValue));
308
+ }
309
+ if (Object.keys(variants).length === 0) {
310
+ throw new Error(`v1 edge route ${JSON.stringify(routeKey)} variants must be omitted when empty`);
311
+ }
312
+ }
313
+ return {
314
+ pattern,
315
+ priority: requiredSafeInteger(record.priority, 'v1 edge route priority'),
316
+ dispatch: validateDispatch(record.dispatch),
317
+ ...(variants ? { variants } : {}),
318
+ };
319
+ }
320
+ function validateVariant(mapKey, value) {
321
+ const record = requiredRecord(value, `v1 edge variant ${JSON.stringify(mapKey)}`);
322
+ assertExactKeys(record, ['variantKey', 'priority', 'dispatch'], ['variantValues', 'rule'], `v1 edge variant ${JSON.stringify(mapKey)}`);
323
+ const variantKey = requiredString(record.variantKey, 'v1 edge variant key');
324
+ if (variantKey !== mapKey) {
325
+ throw new Error(`v1 edge variant ${JSON.stringify(mapKey)} has identity `
326
+ + `${JSON.stringify(variantKey)}`);
327
+ }
328
+ let variantValues;
329
+ if (Object.prototype.hasOwnProperty.call(record, 'variantValues')) {
330
+ const values = requiredRecord(record.variantValues, `v1 edge variant ${JSON.stringify(mapKey)} values`);
331
+ variantValues = {};
332
+ for (const [key, item] of Object.entries(values)) {
333
+ defineOwnEnumerable(variantValues, key, requiredString(item, `v1 edge variant value ${JSON.stringify(key)}`));
334
+ }
335
+ if (Object.keys(variantValues).length === 0) {
336
+ throw new Error(`v1 edge variant ${JSON.stringify(mapKey)} values must be omitted when empty`);
337
+ }
338
+ }
339
+ return {
340
+ variantKey,
341
+ ...(variantValues ? { variantValues } : {}),
342
+ priority: requiredSafeInteger(record.priority, 'v1 edge variant priority'),
343
+ ...(Object.prototype.hasOwnProperty.call(record, 'rule')
344
+ ? { rule: validateRule(record.rule) }
345
+ : {}),
346
+ dispatch: validateDispatch(record.dispatch),
347
+ };
348
+ }
349
+ function validateDispatch(value) {
350
+ const record = requiredRecord(value, 'v1 edge dispatch');
351
+ assertExactKeys(record, ['kind'], ['artifactPath', 'contentPath'], 'v1 edge dispatch');
352
+ return {
353
+ kind: requiredString(record.kind, 'v1 edge dispatch kind'),
354
+ ...(Object.prototype.hasOwnProperty.call(record, 'artifactPath')
355
+ ? {
356
+ artifactPath: requiredString(record.artifactPath, 'v1 edge dispatch artifact path'),
357
+ }
358
+ : {}),
359
+ ...(Object.prototype.hasOwnProperty.call(record, 'contentPath')
360
+ ? {
361
+ contentPath: requiredString(record.contentPath, 'v1 edge dispatch content path'),
362
+ }
363
+ : {}),
364
+ };
365
+ }
366
+ function validateRule(value) {
367
+ const record = requiredRecord(value, 'v1 edge variant rule');
368
+ assertExactKeys(record, [], ['all', 'any'], 'v1 edge variant rule');
369
+ const rule = {};
370
+ if (Object.prototype.hasOwnProperty.call(record, 'all')) {
371
+ const all = validateConditions(record.all, 'all');
372
+ if (all.length === 0) {
373
+ throw new Error('v1 edge rule all must be omitted when empty');
374
+ }
375
+ rule.all = all;
376
+ }
377
+ if (Object.prototype.hasOwnProperty.call(record, 'any')) {
378
+ const any = validateConditions(record.any, 'any');
379
+ if (any.length === 0) {
380
+ throw new Error('v1 edge rule any must be omitted when empty');
381
+ }
382
+ rule.any = any;
383
+ }
384
+ return rule;
385
+ }
386
+ function validateConditions(value, group) {
387
+ if (!Array.isArray(value)) {
388
+ throw new Error(`v1 edge rule ${group} must be an array`);
389
+ }
390
+ return value.map((item, index) => {
391
+ const record = requiredRecord(item, `v1 edge rule ${group}[${index}]`);
392
+ assertExactKeys(record, ['field', 'operator'], ['value'], `v1 edge rule ${group}[${index}]`);
393
+ return {
394
+ field: requiredString(record.field, 'v1 edge rule field'),
395
+ operator: requiredString(record.operator, 'v1 edge rule operator'),
396
+ ...(Object.prototype.hasOwnProperty.call(record, 'value')
397
+ ? {
398
+ value: requiredRuntimeRuleScalar(record.value, 'v1 edge rule value'),
399
+ }
400
+ : {}),
401
+ };
402
+ });
403
+ }
404
+ function validateExperiment(value) {
405
+ const record = requiredRecord(value, 'v1 edge experiment');
406
+ assertExactKeys(record, [
407
+ 'id',
408
+ 'status',
409
+ 'kind',
410
+ 'priority',
411
+ 'allocationVersion',
412
+ 'salt',
413
+ 'arms',
414
+ ], [], 'v1 edge experiment');
415
+ if (!Array.isArray(record.arms)) {
416
+ throw new Error('v1 edge experiment arms must be an array');
417
+ }
418
+ const armIDs = new Set();
419
+ const arms = record.arms.map((value, index) => {
420
+ const arm = requiredRecord(value, `v1 edge experiment arm ${index}`);
421
+ assertExactKeys(arm, ['id', 'role', 'allocationBasisPoints'], ['variantKey'], `v1 edge experiment arm ${index}`);
422
+ const id = requiredString(arm.id, 'v1 edge experiment arm ID');
423
+ if (armIDs.has(id)) {
424
+ throw new Error(`duplicate v1 edge experiment arm ID ${JSON.stringify(id)}`);
425
+ }
426
+ armIDs.add(id);
427
+ const role = requiredString(arm.role, 'v1 edge experiment arm role');
428
+ return {
429
+ id,
430
+ role,
431
+ allocationBasisPoints: requiredBoundedInteger(arm.allocationBasisPoints, 0, 10_000, 'v1 edge experiment arm allocation'),
432
+ variantKey: Object.prototype.hasOwnProperty.call(arm, 'variantKey')
433
+ ? requiredString(arm.variantKey, 'v1 edge experiment arm variant key')
434
+ : '',
435
+ };
436
+ });
437
+ return {
438
+ id: requiredString(record.id, 'v1 edge experiment ID'),
439
+ status: requiredString(record.status, 'v1 edge experiment status'),
440
+ kind: requiredString(record.kind, 'v1 edge experiment kind'),
441
+ priority: requiredSafeInteger(record.priority, 'v1 edge experiment priority'),
442
+ allocationVersion: optionalString(record.allocationVersion, 'v1 edge experiment allocation version'),
443
+ salt: optionalString(record.salt, 'v1 edge experiment assignment salt'),
444
+ arms,
445
+ };
446
+ }
447
+ function validateCollectSDK(value) {
448
+ const record = requiredRecord(value, 'v1 edge collect SDK');
449
+ assertExactKeys(record, ['hash', 'path'], [], 'v1 edge collect SDK');
450
+ const hash = requiredSHA256(record.hash, 'v1 edge collect SDK hash');
451
+ const path = requiredString(record.path, 'v1 edge collect SDK path');
452
+ const expectedPath = `_aci/aci.${hash}.js`;
453
+ if (path !== expectedPath) {
454
+ throw new Error(`v1 edge collect SDK path ${JSON.stringify(path)} must equal `
455
+ + `${JSON.stringify(expectedPath)}`);
456
+ }
457
+ return { hash, path };
458
+ }
459
+ function canonicalRuntimeProjectionJSON(value) {
460
+ const record = requiredRecord(value, 'v1 edge runtime projection');
461
+ return encodeCanonicalJSONNode(canonicalObject([
462
+ ['manifestVersion', canonicalUnknownNode(record.manifestVersion)],
463
+ ['releaseId', canonicalUnknownNode(record.releaseId)],
464
+ ['siteId', canonicalUnknownNode(record.siteId)],
465
+ [
466
+ 'routes',
467
+ canonicalSortedMap(requiredRecord(record.routes, 'v1 edge runtime routes'), canonicalRouteNode),
468
+ ],
469
+ ...optionalCanonicalEntry(record, 'experiments', (experiments) => (canonicalSortedMap(requiredRecord(experiments, 'v1 edge runtime experiments'), canonicalExperimentNode))),
470
+ ...optionalCanonicalEntry(record, 'collectSdk', canonicalCollectSDKNode),
471
+ ]));
472
+ }
473
+ function canonicalRouteNode(value) {
474
+ const record = requiredRecord(value, 'v1 edge route');
475
+ return canonicalObject([
476
+ ['pattern', canonicalUnknownNode(record.pattern)],
477
+ ['priority', canonicalUnknownNode(record.priority)],
478
+ ['dispatch', canonicalDispatchNode(record.dispatch)],
479
+ ...optionalCanonicalEntry(record, 'variants', (variants) => (canonicalSortedMap(requiredRecord(variants, 'v1 edge route variants'), canonicalVariantNode))),
480
+ ]);
481
+ }
482
+ function canonicalVariantNode(value) {
483
+ const record = requiredRecord(value, 'v1 edge variant');
484
+ return canonicalObject([
485
+ ['variantKey', canonicalUnknownNode(record.variantKey)],
486
+ ...optionalCanonicalEntry(record, 'variantValues', (values) => (canonicalSortedMap(requiredRecord(values, 'v1 edge variant values'), canonicalUnknownNode))),
487
+ ['priority', canonicalUnknownNode(record.priority)],
488
+ ...optionalCanonicalEntry(record, 'rule', canonicalRuleNode),
489
+ ['dispatch', canonicalDispatchNode(record.dispatch)],
490
+ ]);
491
+ }
492
+ function canonicalDispatchNode(value) {
493
+ const record = requiredRecord(value, 'v1 edge dispatch');
494
+ return canonicalObject([
495
+ ['kind', canonicalUnknownNode(record.kind)],
496
+ ...optionalCanonicalEntry(record, 'artifactPath', canonicalUnknownNode),
497
+ ...optionalCanonicalEntry(record, 'contentPath', canonicalUnknownNode),
498
+ ]);
499
+ }
500
+ function canonicalRuleNode(value) {
501
+ const record = requiredRecord(value, 'v1 edge rule');
502
+ return canonicalObject([
503
+ ...optionalCanonicalEntry(record, 'all', canonicalConditionArrayNode),
504
+ ...optionalCanonicalEntry(record, 'any', canonicalConditionArrayNode),
505
+ ]);
506
+ }
507
+ function canonicalConditionArrayNode(value) {
508
+ if (!Array.isArray(value)) {
509
+ throw new Error('v1 edge rule conditions must be an array');
510
+ }
511
+ return value.map((condition) => {
512
+ const record = requiredRecord(condition, 'v1 edge rule condition');
513
+ return canonicalObject([
514
+ ['field', canonicalUnknownNode(record.field)],
515
+ ['operator', canonicalUnknownNode(record.operator)],
516
+ ...optionalCanonicalEntry(record, 'value', canonicalUnknownNode),
517
+ ]);
518
+ });
519
+ }
520
+ function canonicalExperimentNode(value) {
521
+ const record = requiredRecord(value, 'v1 edge experiment');
522
+ if (!Array.isArray(record.arms)) {
523
+ throw new Error('v1 edge experiment arms must be an array');
524
+ }
525
+ return canonicalObject([
526
+ ['id', canonicalUnknownNode(record.id)],
527
+ ['status', canonicalUnknownNode(record.status)],
528
+ ['kind', canonicalUnknownNode(record.kind)],
529
+ ['priority', canonicalUnknownNode(record.priority)],
530
+ ['allocationVersion', canonicalUnknownNode(record.allocationVersion)],
531
+ ['salt', canonicalUnknownNode(record.salt)],
532
+ [
533
+ 'arms',
534
+ record.arms.map((armValue) => {
535
+ const arm = requiredRecord(armValue, 'v1 edge experiment arm');
536
+ return canonicalObject([
537
+ ['id', canonicalUnknownNode(arm.id)],
538
+ ['role', canonicalUnknownNode(arm.role)],
539
+ [
540
+ 'allocationBasisPoints',
541
+ canonicalUnknownNode(arm.allocationBasisPoints),
542
+ ],
543
+ ...optionalCanonicalEntry(arm, 'variantKey', canonicalUnknownNode),
544
+ ]);
545
+ }),
546
+ ],
547
+ ]);
548
+ }
549
+ function canonicalCollectSDKNode(value) {
550
+ const record = requiredRecord(value, 'v1 edge collect SDK');
551
+ return canonicalObject([
552
+ ['hash', canonicalUnknownNode(record.hash)],
553
+ ['path', canonicalUnknownNode(record.path)],
554
+ ]);
555
+ }
556
+ function optionalCanonicalEntry(record, key, encode) {
557
+ return Object.prototype.hasOwnProperty.call(record, key)
558
+ ? [[key, encode(record[key])]]
559
+ : [];
560
+ }
561
+ function canonicalSortedMap(record, encode) {
562
+ const entries = Object.entries(record)
563
+ .sort(([left], [right]) => Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8')))
564
+ .map(([key, item]) => [key, encode(item)]);
565
+ return canonicalObject(entries);
566
+ }
567
+ function canonicalUnknownNode(value) {
568
+ if (value === null
569
+ || typeof value === 'boolean'
570
+ || typeof value === 'number'
571
+ || typeof value === 'string') {
572
+ return value;
573
+ }
574
+ if (Array.isArray(value)) {
575
+ return value.map(canonicalUnknownNode);
576
+ }
577
+ if (value && typeof value === 'object') {
578
+ return canonicalSortedMap(value, canonicalUnknownNode);
579
+ }
580
+ throw new Error('v1 edge runtime payload contains an unsupported JSON value');
581
+ }
582
+ function canonicalObject(entries) {
583
+ return { entries };
584
+ }
585
+ function canonicalRecordJSONInInputOrder(record) {
586
+ const entries = Object.entries(record).map(([key, item]) => [
587
+ key,
588
+ canonicalUnknownNode(item),
589
+ ]);
590
+ return encodeCanonicalJSONNode(canonicalObject(entries));
591
+ }
592
+ function encodeCanonicalJSONNode(value) {
593
+ if (value === null)
594
+ return 'null';
595
+ if (typeof value === 'string')
596
+ return encodeGoJSONString(value);
597
+ if (typeof value === 'number' || typeof value === 'boolean') {
598
+ return JSON.stringify(value);
599
+ }
600
+ if (Array.isArray(value)) {
601
+ return `[${value.map(encodeCanonicalJSONNode).join(',')}]`;
602
+ }
603
+ return `{${value.entries.map(([key, item]) => (`${encodeGoJSONString(key)}:${encodeCanonicalJSONNode(item)}`)).join(',')}}`;
604
+ }
605
+ function encodeGoJSONString(value) {
606
+ const normalized = Array.from(value, (character) => {
607
+ const code = character.charCodeAt(0);
608
+ return character.length === 1 && code >= 0xd800 && code <= 0xdfff
609
+ ? '\ufffd'
610
+ : character;
611
+ }).join('');
612
+ return JSON.stringify(normalized).replace(/[<>&\u2028\u2029]/gu, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`);
613
+ }
614
+ function assertExactKeys(record, required, optional, label) {
615
+ const allowed = new Set([...required, ...optional]);
616
+ for (const key of Object.keys(record)) {
617
+ if (!allowed.has(key)) {
618
+ throw new Error(`${label} contains unsupported field ${JSON.stringify(key)}`);
619
+ }
620
+ }
621
+ for (const key of required) {
622
+ if (!Object.prototype.hasOwnProperty.call(record, key)) {
623
+ throw new Error(`${label} is missing field ${JSON.stringify(key)}`);
624
+ }
625
+ }
626
+ }
627
+ function requiredRecord(value, label) {
628
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
629
+ throw new Error(`${label} must be an object`);
630
+ }
631
+ return value;
632
+ }
633
+ function defineOwnEnumerable(record, key, value) {
634
+ Object.defineProperty(record, key, {
635
+ value,
636
+ enumerable: true,
637
+ configurable: true,
638
+ writable: true,
639
+ });
640
+ }
641
+ function requiredString(value, label) {
642
+ if (typeof value !== 'string' || value.length === 0) {
643
+ throw new Error(`${label} must be a non-empty string`);
644
+ }
645
+ return value;
646
+ }
647
+ function optionalString(value, label) {
648
+ if (typeof value !== 'string') {
649
+ throw new Error(`${label} must be a string`);
650
+ }
651
+ return value;
652
+ }
653
+ function requiredIdentity(value, label) {
654
+ const result = requiredString(value, label);
655
+ if (result === '.'
656
+ || result === '..'
657
+ || result.includes('/')
658
+ || result.includes('\\')) {
659
+ throw new Error(`${label} must be one path segment`);
660
+ }
661
+ return result;
662
+ }
663
+ function requiredSHA256(value, label) {
664
+ const digest = requiredString(value, label);
665
+ if (!SHA256_HEX.test(digest)) {
666
+ throw new Error(`${label} must be lowercase SHA-256`);
667
+ }
668
+ return digest;
669
+ }
670
+ function requiredRuntimeRuleScalar(value, label) {
671
+ if (value === null
672
+ || typeof value === 'string'
673
+ || typeof value === 'boolean'
674
+ || (typeof value === 'number' && Number.isFinite(value))) {
675
+ return value;
676
+ }
677
+ throw new Error(`${label} must be a JSON string, number, boolean, or null`);
678
+ }
679
+ function requiredSafeInteger(value, label) {
680
+ if (typeof value !== 'number' || !Number.isSafeInteger(value)) {
681
+ throw new Error(`${label} must be a safe integer`);
682
+ }
683
+ return value;
684
+ }
685
+ function requiredPositiveSafeInteger(value, label) {
686
+ const result = requiredSafeInteger(value, label);
687
+ if (result <= 0)
688
+ throw new Error(`${label} must be positive`);
689
+ return result;
690
+ }
691
+ function requiredPositiveDecimalBigInt(value, label) {
692
+ if (typeof value !== 'string' || !POSITIVE_DECIMAL.test(value)) {
693
+ throw new Error(`${label} must be a canonical positive decimal string`);
694
+ }
695
+ const result = BigInt(value);
696
+ if (result > MAX_INT64) {
697
+ throw new Error(`${label} exceeds the signed 64-bit sequence bound`);
698
+ }
699
+ return result;
700
+ }
701
+ function requiredBoundedInteger(value, minimum, maximum, label) {
702
+ const result = requiredSafeInteger(value, label);
703
+ if (result < minimum || result > maximum) {
704
+ throw new Error(`${label} must be between ${minimum} and ${maximum}`);
705
+ }
706
+ return result;
707
+ }
708
+ function sha256(value) {
709
+ return createHash('sha256').update(value).digest('hex');
710
+ }
711
+ function edgeKeySegment(value) {
712
+ return value.replace(/[^A-Za-z0-9_-]/gu, '_');
713
+ }
714
+ function hex32(value) {
715
+ return value.toString(16).padStart(8, '0');
716
+ }
717
+ const CRC32_TABLE = new Uint32Array(256);
718
+ for (let index = 0; index < CRC32_TABLE.length; index += 1) {
719
+ let value = index;
720
+ for (let bit = 0; bit < 8; bit += 1) {
721
+ value = (value & 1) !== 0
722
+ ? (value >>> 1) ^ 0xedb88320
723
+ : value >>> 1;
724
+ }
725
+ CRC32_TABLE[index] = value >>> 0;
726
+ }
727
+ function crc32(value) {
728
+ let result = 0xffffffff;
729
+ for (const byte of value) {
730
+ result = CRC32_TABLE[(result ^ byte) & 0xff] ^ (result >>> 8);
731
+ }
732
+ return (result ^ 0xffffffff) >>> 0;
733
+ }
734
+ function assertCanonicalJSONSyntax(value, label) {
735
+ let index = 0;
736
+ function parseValue(depth) {
737
+ if (depth > MAX_JSON_DEPTH) {
738
+ throw new Error(`${label} exceeds the JSON nesting bound`);
739
+ }
740
+ const token = value[index];
741
+ if (token === '{') {
742
+ if (depth >= MAX_JSON_DEPTH) {
743
+ throw new Error(`${label} exceeds the JSON nesting bound`);
744
+ }
745
+ parseObject(depth + 1);
746
+ return;
747
+ }
748
+ if (token === '[') {
749
+ if (depth >= MAX_JSON_DEPTH) {
750
+ throw new Error(`${label} exceeds the JSON nesting bound`);
751
+ }
752
+ parseArray(depth + 1);
753
+ return;
754
+ }
755
+ if (token === '"') {
756
+ parseString();
757
+ return;
758
+ }
759
+ if (token === 't') {
760
+ parseLiteral('true');
761
+ return;
762
+ }
763
+ if (token === 'f') {
764
+ parseLiteral('false');
765
+ return;
766
+ }
767
+ if (token === 'n') {
768
+ parseLiteral('null');
769
+ return;
770
+ }
771
+ const match = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/
772
+ .exec(value.slice(index));
773
+ if (!match)
774
+ throw new Error(`${label} contains invalid JSON at byte ${index}`);
775
+ index += match[0].length;
776
+ }
777
+ function parseObject(depth) {
778
+ index += 1;
779
+ const keys = new Set();
780
+ if (value[index] === '}') {
781
+ index += 1;
782
+ return;
783
+ }
784
+ for (;;) {
785
+ if (value[index] !== '"') {
786
+ throw new Error(`${label} contains invalid object key at byte ${index}`);
787
+ }
788
+ const key = parseString();
789
+ if (keys.has(key)) {
790
+ throw new Error(`${label} contains duplicate object key ${JSON.stringify(key)}`);
791
+ }
792
+ keys.add(key);
793
+ if (value[index] !== ':') {
794
+ throw new Error(`${label} contains invalid object separator at byte ${index}`);
795
+ }
796
+ index += 1;
797
+ parseValue(depth);
798
+ if (value[index] === '}') {
799
+ index += 1;
800
+ return;
801
+ }
802
+ if (value[index] !== ',') {
803
+ throw new Error(`${label} contains invalid object delimiter at byte ${index}`);
804
+ }
805
+ index += 1;
806
+ }
807
+ }
808
+ function parseArray(depth) {
809
+ index += 1;
810
+ if (value[index] === ']') {
811
+ index += 1;
812
+ return;
813
+ }
814
+ for (;;) {
815
+ parseValue(depth);
816
+ if (value[index] === ']') {
817
+ index += 1;
818
+ return;
819
+ }
820
+ if (value[index] !== ',') {
821
+ throw new Error(`${label} contains invalid array delimiter at byte ${index}`);
822
+ }
823
+ index += 1;
824
+ }
825
+ }
826
+ function parseString() {
827
+ const start = index;
828
+ index += 1;
829
+ while (index < value.length) {
830
+ const code = value.charCodeAt(index);
831
+ if (code === 0x22) {
832
+ index += 1;
833
+ try {
834
+ return JSON.parse(value.slice(start, index));
835
+ }
836
+ catch (error) {
837
+ throw new Error(`${label} contains an invalid JSON string`, {
838
+ cause: error,
839
+ });
840
+ }
841
+ }
842
+ if (code < 0x20) {
843
+ throw new Error(`${label} contains an unescaped control character`);
844
+ }
845
+ if (code === 0x5c) {
846
+ index += 1;
847
+ const escaped = value[index];
848
+ if (escaped === 'u') {
849
+ const digits = value.slice(index + 1, index + 5);
850
+ if (!/^[0-9a-fA-F]{4}$/.test(digits)) {
851
+ throw new Error(`${label} contains an invalid Unicode escape`);
852
+ }
853
+ index += 5;
854
+ continue;
855
+ }
856
+ if (!escaped || !'"\\/bfnrt'.includes(escaped)) {
857
+ throw new Error(`${label} contains an invalid string escape`);
858
+ }
859
+ }
860
+ index += 1;
861
+ }
862
+ throw new Error(`${label} contains an unterminated JSON string`);
863
+ }
864
+ function parseLiteral(literal) {
865
+ if (!value.startsWith(literal, index)) {
866
+ throw new Error(`${label} contains invalid JSON at byte ${index}`);
867
+ }
868
+ index += literal.length;
869
+ }
870
+ if (value.length === 0)
871
+ throw new Error(`${label} is empty`);
872
+ parseValue(0);
873
+ if (index !== value.length) {
874
+ const remaining = value.slice(index);
875
+ if (/^\s/u.test(remaining)) {
876
+ throw new Error(`${label} contains noncanonical whitespace`);
877
+ }
878
+ throw new Error(`${label} contains trailing JSON at byte ${index}`);
879
+ }
880
+ }