@gradial/aci 0.1.45 → 0.1.46

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 (39) hide show
  1. package/dist/audience-data-models.d.ts +142 -2
  2. package/dist/audience-data-models.js +2 -0
  3. package/dist/cli/compile-registry.d.mts +1 -5
  4. package/dist/cli/compile-registry.mjs +10 -25
  5. package/dist/cli/validate-content.d.mts +1 -31
  6. package/dist/cli/validate-content.mjs +12 -50
  7. package/dist/cli/verify-next-cdn-routes.mjs +1 -0
  8. package/dist/content/provider.d.ts +1 -1
  9. package/dist/content/resolve-slots.js +0 -1
  10. package/dist/content/types.d.ts +0 -2
  11. package/dist/internal/render-proof.d.ts +1 -0
  12. package/dist/internal/render-proof.js +197 -0
  13. package/dist/next/middleware.js +9 -54
  14. package/dist/next/page.js +9 -4
  15. package/dist/next/personalization-host.d.ts +5 -0
  16. package/dist/next/server.js +88 -75
  17. package/dist/next/vercel-collect-sdk-route.js +6 -30
  18. package/dist/next/vercel-edge-projection-v1.d.ts +24 -0
  19. package/dist/next/vercel-edge-projection-v1.js +880 -0
  20. package/dist/next/vercel-s3-personalization.js +7 -11
  21. package/dist/providers/s3.d.ts +2 -4
  22. package/dist/providers/s3.js +173 -19
  23. package/dist/types/block-ref.d.ts +0 -2
  24. package/dist/types/block-ref.js +0 -1
  25. package/package.json +2 -2
  26. package/src/cli/compile-registry.mjs +10 -24
  27. package/src/cli/validate-content.mjs +13 -52
  28. package/src/cli/verify-next-cdn-routes.mjs +6 -0
  29. package/src/types/block-ref.ts +0 -2
  30. package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +6 -7
  31. package/templates/astro/starter-content/src/fixtures/composition.ts +2 -3
  32. package/templates/astro/starter-content/src/fixtures/sections.ts +1 -2
  33. package/templates/common/.agents/skills/authoring/SKILL.md +3 -11
  34. package/templates/common/.agents/skills/authoring/references/block-slots.md +0 -3
  35. package/templates/common/.content/pages/home/_index.json +0 -10
  36. package/templates/common/starter-content/.content/pages/about/_index.json +6 -6
  37. package/templates/common/starter-content/.content/pages/home/_index.json +38 -22
  38. package/src/types/block-ref.test.ts +0 -45
  39. package/templates/nextjs/template/src/app/collect/route.ts +0 -18
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server.js';
2
2
  import { getUncachedEdgeConfigValue } from './edge-config.js';
3
3
  import { PREVIEW_TOKEN_COOKIE, PREVIEW_RELEASE_COOKIE, extractPreviewToken, resolvePreviewContext, createPreviewCookies, previewSignKey, } from '../preview/core.js';
4
4
  import { CONTENT_PATH_HEADER, CONTENT_PATH_CACHE_PARAM, resolvePersonalizationRequest, } from './personalization-host.js';
5
+ import { activeReleaseV1Key, clampDeploymentCompatibleReleaseV1, decodeActiveReleaseV1, decodeDeploymentCompatibilityV1, deploymentCompatibilityKey, } from './vercel-edge-projection-v1.js';
5
6
  const RELEASE_SCOPED_PREFIX = '/__r';
6
7
  const FRAMEWORK_ORIGIN_AUTH_HEADER = 'x-gradial-framework-origin-auth';
7
8
  const FRAMEWORK_SITE_ID_HEADER = 'x-baremetal-site-id';
@@ -371,73 +372,27 @@ async function activeReleaseFromEdgeConfig(config) {
371
372
  if (!edgeConfig || !siteId)
372
373
  return '';
373
374
  const active = await activeReleasePointer(edgeConfig, siteId);
374
- if (!active.releaseId)
375
+ if (active == null)
375
376
  return '';
376
377
  const deploymentId = config.deploymentId || process.env.VERCEL_DEPLOYMENT_ID || '';
377
378
  if (!deploymentId)
378
379
  return active.releaseId;
379
- const compatibility = await getEdgeConfigJSON(edgeConfig, deploymentCompatibilityKey(siteId, deploymentId));
380
- if (!compatibility)
380
+ const compatibilityValue = await getEdgeConfigValue(edgeConfig, deploymentCompatibilityKey(siteId, deploymentId));
381
+ if (compatibilityValue == null)
381
382
  return active.releaseId;
382
- return clampRelease(active.releaseId, active.sequence || 0, compatibility);
383
+ return clampDeploymentCompatibleReleaseV1(active, decodeDeploymentCompatibilityV1(compatibilityValue));
383
384
  }
384
385
  function siteIdForConfig(config) {
385
386
  return config.siteId || process.env.ACI_SITE_ID || '';
386
387
  }
387
- async function getEdgeConfigJSON(edgeConfig, key) {
388
- const value = await getEdgeConfigValue(edgeConfig, key);
389
- if (!value)
390
- return null;
391
- if (typeof value === 'string') {
392
- try {
393
- return JSON.parse(value);
394
- }
395
- catch {
396
- return null;
397
- }
398
- }
399
- return value;
400
- }
401
388
  async function getEdgeConfigValue(edgeConfig, key) {
402
389
  return getUncachedEdgeConfigValue(edgeConfig, key);
403
390
  }
404
- function activeReleaseKey(siteId) {
405
- return `activeRelease_${edgeKeySegment(siteId)}`;
406
- }
407
- function deploymentCompatibilityKey(siteId, deploymentId) {
408
- return `deploymentCompatibility_${edgeKeySegment(siteId)}_${edgeKeySegment(deploymentId)}`;
409
- }
410
391
  async function activeReleasePointer(edgeConfig, siteId) {
411
- const value = await getEdgeConfigValue(edgeConfig, activeReleaseKey(siteId));
412
- if (!value)
413
- return { releaseId: '' };
414
- if (typeof value === 'string') {
415
- try {
416
- const parsed = JSON.parse(value);
417
- if (parsed.releaseId)
418
- return parsed;
419
- }
420
- catch {
421
- return { releaseId: value };
422
- }
423
- return { releaseId: value };
424
- }
425
- const pointer = value;
426
- return { releaseId: pointer.releaseId || '', sequence: pointer.sequence };
427
- }
428
- function clampRelease(activeReleaseId, activeSequence, compatibility) {
429
- if (!activeSequence)
430
- return activeReleaseId;
431
- if (compatibility.minSequence && activeSequence < compatibility.minSequence && compatibility.minReleaseId) {
432
- return compatibility.minReleaseId;
433
- }
434
- if (compatibility.maxSequence && activeSequence > compatibility.maxSequence && compatibility.maxReleaseId) {
435
- return compatibility.maxReleaseId;
436
- }
437
- return activeReleaseId;
438
- }
439
- function edgeKeySegment(value) {
440
- return value.replace(/[^A-Za-z0-9_-]/g, '_');
392
+ const value = await getEdgeConfigValue(edgeConfig, activeReleaseV1Key(siteId));
393
+ if (value == null)
394
+ return null;
395
+ return decodeActiveReleaseV1(value);
441
396
  }
442
397
  function isPassthroughRoute(pathname) {
443
398
  return PASSTHROUGH_ROUTES.some((route) => routeMatches(pathname, route));
package/dist/next/page.js CHANGED
@@ -6,6 +6,7 @@ import { routeMetadataForContent, resolveSlots, } from '../content/index.js';
6
6
  import { renderBlocks } from './render.js';
7
7
  import { AssignmentsScript } from './assignments-script.js';
8
8
  import { CollectScript } from './collect-script.js';
9
+ import { renderProofContentFor } from '../internal/render-proof.js';
9
10
  /**
10
11
  * Creates the catch-all route handler for a Next.js ACI site.
11
12
  *
@@ -64,12 +65,14 @@ export function createPage(registry, options) {
64
65
  }
65
66
  async function CatchAllPage({ params }) {
66
67
  const { route, releaseId } = await resolveRouteAndRelease(params);
68
+ const fetchConfig = { releaseId };
67
69
  try {
68
- const input = await getRenderInput(route, { releaseId });
70
+ const input = await getRenderInput(route, fetchConfig);
69
71
  if (!input.page)
70
72
  notFound();
71
- const layout = await getLayout(input.page.layout);
72
- const resolvedSlots = await resolveSlots(layout, input.page.regions || {}, (id) => getFragment(id));
73
+ const renderProofContent = renderProofContentFor(input);
74
+ const layout = await getLayout(input.page.layout, fetchConfig);
75
+ const resolvedSlots = await resolveSlots(layout, input.page.regions || {}, (id) => getFragment(id, fetchConfig));
73
76
  const renderedSlots = await Promise.all(layout.slots.map(async (slot) => {
74
77
  const blocks = resolvedSlots[slot.name];
75
78
  if (!blocks?.length)
@@ -81,7 +84,9 @@ export function createPage(registry, options) {
81
84
  }
82
85
  return (_jsx(Fragment, { children: renderedBlocks }, slot.name));
83
86
  }));
84
- return (_jsxs(_Fragment, { children: [renderedSlots.filter(Boolean), _jsx(AssignmentsScript, { page: input.page }), _jsx(CollectScript, { siteConfig: input.siteConfig })] }));
87
+ return (_jsxs(_Fragment, { children: [renderProofContent
88
+ ? _jsx("meta", { name: "aci-render-proof", content: renderProofContent })
89
+ : null, renderedSlots.filter(Boolean), _jsx(AssignmentsScript, { page: input.page }), _jsx(CollectScript, { siteConfig: input.siteConfig })] }));
85
90
  }
86
91
  catch (error) {
87
92
  if (error instanceof PageNotFoundError)
@@ -29,12 +29,17 @@ export interface EdgeRouteEntry {
29
29
  dispatch: EdgeDispatch;
30
30
  variants?: Record<string, EdgeRouteVariant>;
31
31
  }
32
+ export interface CollectSDKEntry {
33
+ hash: string;
34
+ path: string;
35
+ }
32
36
  export interface EdgeRouteManifest {
33
37
  manifestVersion: string;
34
38
  releaseId: string;
35
39
  siteId: string;
36
40
  routes: Record<string, EdgeRouteEntry>;
37
41
  experiments?: Record<string, PersonalizationExperiment>;
42
+ collectSdk?: CollectSDKEntry;
38
43
  notFoundPage?: string;
39
44
  errorPage?: string;
40
45
  }
@@ -3,17 +3,19 @@ import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError, } from '
3
3
  import { getCachedManifest, getCachedSiteConfig } from '../content/cache.js';
4
4
  import { S3ContentProvider } from '../providers/s3.js';
5
5
  import { GRADIAL_RENDER_INPUT_HEADER, getPendingRenderInput } from '../runtime/page.js';
6
+ import { attachRenderProofReceipt, selectVerifiedPage, } from '../internal/render-proof.js';
6
7
  import { getUncachedEdgeConfigValue } from './edge-config.js';
8
+ import { activeReleaseV1Key, clampDeploymentCompatibleReleaseV1, decodeActiveReleaseV1, decodeDeploymentCompatibilityV1, deploymentCompatibilityKey, } from './vercel-edge-projection-v1.js';
7
9
  export { FragmentNotFoundError, PageNotFoundError, LayoutNotFoundError };
10
+ const MAX_S3_PROVIDER_CACHE_ENTRIES = 8;
11
+ const s3ProviderCache = [];
12
+ const s3ProviderReleaseIds = new WeakMap();
8
13
  export async function getPage(route = '', config = {}) {
9
14
  const runtimeInput = await getPageRuntimeRenderInput();
10
15
  if (runtimeInput)
11
16
  return runtimeInput.page;
12
17
  const provider = await resolveProviderWithFallback(config);
13
- const variantPage = await pageFromContentPath(provider);
14
- if (variantPage !== null)
15
- return variantPage;
16
- return await provider.getPage(route);
18
+ return (await selectRenderPage(provider, route)).page;
17
19
  }
18
20
  export async function getSiteConfig(config = {}) {
19
21
  const runtimeInput = await getPageRuntimeRenderInput();
@@ -55,17 +57,21 @@ export async function getRenderInput(route = '/', config = {}) {
55
57
  const provider = await resolveProviderWithFallback(config);
56
58
  // Cache shared resources (manifest, siteConfig) but not per-page content
57
59
  // Page-level caching is handled by CDN
58
- const [page, siteConfig] = await Promise.all([
59
- pageFromContentPath(provider).then((variantPage) => variantPage ?? provider.getPage(route)),
60
+ const [selection, siteConfig] = await Promise.all([
61
+ selectRenderPage(provider, route),
60
62
  getCachedSiteConfig(provider),
61
63
  ]);
62
- return {
64
+ const input = {
63
65
  route,
64
66
  domain: siteConfig.domain || 'www.aci.local',
65
67
  locale: siteConfig.defaultLocale || 'en-us',
66
68
  siteConfig,
67
- page,
69
+ page: selection.page,
68
70
  };
71
+ if (selection.receipt) {
72
+ attachRenderProofReceipt(input, selection.receipt);
73
+ }
74
+ return input;
69
75
  }
70
76
  export async function getPageRuntimeRenderInput() {
71
77
  try {
@@ -83,20 +89,39 @@ export async function getPageRuntimeRenderInput() {
83
89
  // is cosmetic. The S3 provider only serves refs the release manifest
84
90
  // declares.
85
91
  const ACI_CONTENT_PATH_HEADER = 'x-aci-content-path';
86
- async function pageFromContentPath(provider) {
87
- if (!(provider instanceof S3ContentProvider))
88
- return null;
89
- let ref = '';
92
+ const ACI_VARIANT_KEY_HEADER = 'x-aci-variant-key';
93
+ async function selectRenderPage(provider, route) {
94
+ if (!(provider instanceof S3ContentProvider)) {
95
+ return { page: await provider.getPage(route) };
96
+ }
97
+ const releaseId = s3ProviderReleaseIds.get(provider);
98
+ if (!releaseId) {
99
+ throw new Error('ACI S3 page integrity error: provider release is unavailable');
100
+ }
101
+ let contentRef = '';
102
+ let contentRefPresent = false;
103
+ let variantKey = '';
104
+ let variantKeyPresent = false;
90
105
  try {
91
106
  const current = await headers();
92
- ref = (current.get(ACI_CONTENT_PATH_HEADER) || '').trim();
107
+ const selectedContentRef = current.get(ACI_CONTENT_PATH_HEADER);
108
+ contentRefPresent = selectedContentRef !== null;
109
+ contentRef = selectedContentRef ?? '';
110
+ const selectedVariantKey = current.get(ACI_VARIANT_KEY_HEADER);
111
+ variantKeyPresent = selectedVariantKey !== null;
112
+ variantKey = selectedVariantKey ?? '';
93
113
  }
94
114
  catch {
95
- return null;
115
+ // `headers()` has no request context in static generation and unit tests.
96
116
  }
97
- if (!ref)
98
- return null;
99
- return await provider.getPageByRef(ref);
117
+ return await provider[selectVerifiedPage]({
118
+ releaseId,
119
+ route,
120
+ contentRef,
121
+ contentRefPresent,
122
+ variantKey,
123
+ variantKeyPresent,
124
+ });
100
125
  }
101
126
  export { isPreviewMode } from './preview-mode.js';
102
127
  export async function routeFromNextParams(params) {
@@ -230,7 +255,11 @@ export async function resolveReleaseId(config = {}) {
230
255
  }
231
256
  async function resolveS3Provider(config) {
232
257
  const releaseId = await resolveReleaseId(config);
233
- const provider = new S3ContentProvider({
258
+ if (releaseId.length > 128
259
+ || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(releaseId)) {
260
+ throw new Error(`ACI release ID is invalid: ${releaseId}`);
261
+ }
262
+ const candidate = new S3ContentProvider({
234
263
  bucket: config.bucket,
235
264
  keyPrefix: releaseKey(config, releaseId),
236
265
  region: config.region,
@@ -240,9 +269,29 @@ async function resolveS3Provider(config) {
240
269
  roleArn: config.roleArn,
241
270
  endpoint: config.endpoint,
242
271
  });
243
- // Pre-warm the manifest with cached version
244
- await getCachedManifest(provider);
245
- return provider;
272
+ s3ProviderReleaseIds.set(candidate, releaseId);
273
+ const cachedIndex = s3ProviderCache.findIndex((entry) => sameS3ProviderConfig(entry.config, candidate.config));
274
+ if (cachedIndex >= 0) {
275
+ const [cached] = s3ProviderCache.splice(cachedIndex, 1);
276
+ s3ProviderCache.push(cached);
277
+ return await cached.provider;
278
+ }
279
+ const provider = getCachedManifest(candidate).then(() => candidate);
280
+ const entry = { config: candidate.config, provider };
281
+ s3ProviderCache.push(entry);
282
+ if (s3ProviderCache.length > MAX_S3_PROVIDER_CACHE_ENTRIES) {
283
+ s3ProviderCache.shift();
284
+ }
285
+ try {
286
+ return await provider;
287
+ }
288
+ catch (error) {
289
+ const rejectedIndex = s3ProviderCache.findIndex((cached) => cached.provider === provider);
290
+ if (rejectedIndex >= 0) {
291
+ s3ProviderCache.splice(rejectedIndex, 1);
292
+ }
293
+ throw error;
294
+ }
246
295
  }
247
296
  async function resolveProviderWithFallback(config) {
248
297
  if (process.env.ACI_CONTENT_PROVIDER === 'file') {
@@ -251,35 +300,31 @@ async function resolveProviderWithFallback(config) {
251
300
  }
252
301
  return await resolveS3Provider(config);
253
302
  }
303
+ function sameS3ProviderConfig(left, right) {
304
+ return left.bucket === right.bucket
305
+ && left.keyPrefix === right.keyPrefix
306
+ && left.region === right.region
307
+ && left.accessKeyId === right.accessKeyId
308
+ && left.secretAccessKey === right.secretAccessKey
309
+ && left.sessionToken === right.sessionToken
310
+ && left.roleArn === right.roleArn
311
+ && left.endpoint === right.endpoint;
312
+ }
254
313
  async function activeReleaseFromEdgeConfig(config) {
255
314
  const edgeConfig = config.edgeConfig || process.env.EDGE_CONFIG || '';
256
315
  const siteId = config.siteId || process.env.ACI_SITE_ID || '';
257
316
  if (!edgeConfig || !siteId)
258
317
  return '';
259
318
  const active = await activeReleasePointer(edgeConfig, siteId);
260
- if (!active.releaseId)
319
+ if (active == null)
261
320
  return '';
262
321
  const deploymentId = config.deploymentId || process.env.VERCEL_DEPLOYMENT_ID || '';
263
322
  if (!deploymentId)
264
323
  return active.releaseId;
265
- const compatibility = await getEdgeConfigJSON(edgeConfig, deploymentCompatibilityKey(siteId, deploymentId));
266
- if (!compatibility)
324
+ const compatibilityValue = await getEdgeConfigValue(edgeConfig, deploymentCompatibilityKey(siteId, deploymentId));
325
+ if (compatibilityValue == null)
267
326
  return active.releaseId;
268
- return clampRelease(active.releaseId, active.sequence || 0, compatibility);
269
- }
270
- async function getEdgeConfigJSON(edgeConfig, key) {
271
- const value = await getEdgeConfigValue(edgeConfig, key);
272
- if (!value)
273
- return null;
274
- if (typeof value === 'string') {
275
- try {
276
- return JSON.parse(value);
277
- }
278
- catch {
279
- return null;
280
- }
281
- }
282
- return value;
327
+ return clampDeploymentCompatibleReleaseV1(active, decodeDeploymentCompatibilityV1(compatibilityValue));
283
328
  }
284
329
  async function getEdgeConfigValue(edgeConfig, key) {
285
330
  return getUncachedEdgeConfigValue(edgeConfig, key);
@@ -287,43 +332,11 @@ async function getEdgeConfigValue(edgeConfig, key) {
287
332
  function releaseKey(config, releaseId) {
288
333
  return joinKey(config.keyPrefix || process.env.ACI_S3_KEY_PREFIX || '', 'releases', releaseId);
289
334
  }
290
- function activeReleaseKey(siteId) {
291
- return `activeRelease_${edgeKeySegment(siteId)}`;
292
- }
293
- function deploymentCompatibilityKey(siteId, deploymentId) {
294
- return `deploymentCompatibility_${edgeKeySegment(siteId)}_${edgeKeySegment(deploymentId)}`;
295
- }
296
335
  async function activeReleasePointer(edgeConfig, siteId) {
297
- const value = await getEdgeConfigValue(edgeConfig, activeReleaseKey(siteId));
298
- if (!value)
299
- return { releaseId: '' };
300
- if (typeof value === 'string') {
301
- try {
302
- const parsed = JSON.parse(value);
303
- if (parsed.releaseId)
304
- return parsed;
305
- }
306
- catch {
307
- return { releaseId: value };
308
- }
309
- return { releaseId: value };
310
- }
311
- const pointer = value;
312
- return { releaseId: pointer.releaseId || '', sequence: pointer.sequence };
313
- }
314
- function clampRelease(activeReleaseId, activeSequence, compatibility) {
315
- if (!activeSequence)
316
- return activeReleaseId;
317
- if (compatibility.minSequence && activeSequence < compatibility.minSequence && compatibility.minReleaseId) {
318
- return compatibility.minReleaseId;
319
- }
320
- if (compatibility.maxSequence && activeSequence > compatibility.maxSequence && compatibility.maxReleaseId) {
321
- return compatibility.maxReleaseId;
322
- }
323
- return activeReleaseId;
324
- }
325
- function edgeKeySegment(value) {
326
- return value.replace(/[^A-Za-z0-9_-]/g, '_');
336
+ const value = await getEdgeConfigValue(edgeConfig, activeReleaseV1Key(siteId));
337
+ if (value == null)
338
+ return null;
339
+ return decodeActiveReleaseV1(value);
327
340
  }
328
341
  function joinKey(...parts) {
329
342
  return parts
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
3
3
  import { awsCredentialsProvider } from '@vercel/oidc-aws-credentials-provider';
4
4
  import { getUncachedEdgeConfigValue } from './edge-config.js';
5
+ import { activeReleaseV1Key, decodeActiveReleaseV1, decodeEdgeRouteProjectionV1, edgeRoutesV1Key, } from './vercel-edge-projection-v1.js';
5
6
  const SHA256_HEX = /^[0-9a-f]{64}$/;
6
7
  /**
7
8
  * Creates the Node-only stable collect-SDK route used by ACI's injected
@@ -34,12 +35,11 @@ export function createVercelCollectSDKRoute(input, dependencies = {}) {
34
35
  });
35
36
  }
36
37
  try {
37
- const active = parseActiveReleasePointer(await readEdgeConfig(config.edgeConfig, activeReleaseKey(config.siteId)));
38
- const projection = parseProjection(await readEdgeConfig(config.edgeConfig, edgeRoutesKey(config.siteId, active.releaseId)));
39
- if (projection.siteId !== config.siteId || projection.releaseId !== active.releaseId) {
40
- throw new Error(`collect SDK projection identity mismatch: got ${projection.siteId}/${projection.releaseId}, `
41
- + `want ${config.siteId}/${active.releaseId}`);
42
- }
38
+ const active = decodeActiveReleaseV1(await readEdgeConfig(config.edgeConfig, activeReleaseV1Key(config.siteId)));
39
+ const projection = decodeEdgeRouteProjectionV1(await readEdgeConfig(config.edgeConfig, edgeRoutesV1Key(config.siteId, active.releaseId)), {
40
+ siteId: config.siteId,
41
+ releaseId: active.releaseId,
42
+ });
43
43
  if (!Object.prototype.hasOwnProperty.call(projection, 'collectSdk')) {
44
44
  return new Response(method === 'HEAD' ? null : 'collect SDK is not enabled for this release', {
45
45
  status: 404,
@@ -115,21 +115,6 @@ function validateConfig(input) {
115
115
  hostingKeyPrefix: cleanKey(input.hostingKeyPrefix || ''),
116
116
  };
117
117
  }
118
- function parseActiveReleasePointer(value) {
119
- const parsed = parseRecord(value, 'active release pointer');
120
- return { releaseId: requiredPathSegment(parsed.releaseId || '', 'active release ID') };
121
- }
122
- function parseProjection(value) {
123
- const parsed = parseRecord(value, 'collect SDK route projection');
124
- const projection = {
125
- releaseId: requiredPathSegment(parsed.releaseId || '', 'projection release ID'),
126
- siteId: requiredPathSegment(parsed.siteId || '', 'projection site ID'),
127
- };
128
- if (Object.prototype.hasOwnProperty.call(parsed, 'collectSdk')) {
129
- projection.collectSdk = parsed.collectSdk;
130
- }
131
- return projection;
132
- }
133
118
  function parseRecord(value, label) {
134
119
  const parsed = typeof value === 'string' ? JSON.parse(value) : value;
135
120
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
@@ -172,15 +157,6 @@ function ifNoneMatchSatisfied(value, etag) {
172
157
  return normalized === etag;
173
158
  });
174
159
  }
175
- function activeReleaseKey(siteId) {
176
- return `activeRelease_${edgeKeySegment(siteId)}`;
177
- }
178
- function edgeRoutesKey(siteId, releaseId) {
179
- return `edgeRoutes_${edgeKeySegment(siteId)}_${edgeKeySegment(releaseId)}`;
180
- }
181
- function edgeKeySegment(value) {
182
- return value.replace(/[^A-Za-z0-9_-]/g, '_');
183
- }
184
160
  function requiredPathSegment(value, label) {
185
161
  const clean = value.trim();
186
162
  if (!clean || clean === '.' || clean === '..' || clean.includes('/') || clean.includes('\\')) {
@@ -0,0 +1,24 @@
1
+ import type { EdgeRouteManifest } from './personalization-host.js';
2
+ export declare const VERCEL_EDGE_PROJECTION_SCHEMA_V1 = "gradial.vercel.edgeRoutes.v1";
3
+ export declare const VERCEL_EDGE_PROJECTION_ENCODING_V1 = "gzip+base64";
4
+ export declare const MAX_VERCEL_EDGE_MANIFEST_BYTES_V1: number;
5
+ export interface ActiveReleasePointerV1 {
6
+ releaseId: string;
7
+ sequence: bigint;
8
+ }
9
+ export interface DeploymentCompatibilityV1 {
10
+ minReleaseId?: string;
11
+ minSequence?: bigint;
12
+ maxReleaseId?: string;
13
+ maxSequence?: bigint;
14
+ }
15
+ export declare function activeReleaseV1Key(siteId: string): string;
16
+ export declare function edgeRoutesV1Key(siteId: string, releaseId: string): string;
17
+ export declare function deploymentCompatibilityKey(siteId: string, deploymentId: string): string;
18
+ export declare function decodeActiveReleaseV1(value: unknown): ActiveReleasePointerV1;
19
+ export declare function decodeDeploymentCompatibilityV1(value: unknown): DeploymentCompatibilityV1;
20
+ export declare function clampDeploymentCompatibleReleaseV1(active: ActiveReleasePointerV1, compatibility: DeploymentCompatibilityV1): string;
21
+ export declare function decodeEdgeRouteProjectionV1(value: unknown, expected: {
22
+ siteId: string;
23
+ releaseId: string;
24
+ }): EdgeRouteManifest;