@gradial/aci 0.1.41 → 0.1.43

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 (134) hide show
  1. package/CHANGELOG.md +15 -11
  2. package/README.md +4 -4
  3. package/dist/astro/index.d.ts +1 -1
  4. package/dist/astro/index.js +1 -1
  5. package/dist/astro/page.d.ts +6 -0
  6. package/dist/astro/page.js +3 -2
  7. package/dist/audience-data-models.d.ts +286 -0
  8. package/dist/audience-data-models.js +46 -0
  9. package/dist/cli/compile-registry.d.mts +6 -0
  10. package/dist/cli/compile-registry.mjs +774 -0
  11. package/dist/cli/materialize-template.d.mts +19 -0
  12. package/dist/cli/materialize-template.mjs +128 -0
  13. package/dist/cli/validate-content.d.mts +32 -0
  14. package/dist/cli/validate-content.mjs +464 -0
  15. package/dist/cli/verify-next-cdn-routes.d.mts +2 -0
  16. package/dist/cli/verify-next-cdn-routes.mjs +47 -0
  17. package/dist/collect/aci.js +1 -0
  18. package/dist/collect/aci.js.sha256 +1 -0
  19. package/dist/collect/assignments.d.ts +65 -0
  20. package/dist/collect/assignments.js +138 -0
  21. package/dist/collect/batcher.d.ts +23 -0
  22. package/dist/collect/batcher.js +28 -6
  23. package/dist/collect/consent/envelope.d.ts +9 -3
  24. package/dist/collect/consent/envelope.js +58 -40
  25. package/dist/collect/declarative.d.ts +1 -1
  26. package/dist/collect/declarative.js +7 -1
  27. package/dist/collect/index.js +39 -6
  28. package/dist/collect/linkclicks.js +4 -2
  29. package/dist/collect/pageviews.d.ts +15 -3
  30. package/dist/collect/pageviews.js +28 -13
  31. package/dist/collect/transport.d.ts +4 -0
  32. package/dist/collect/transport.js +37 -2
  33. package/dist/consent/portable-envelope.d.ts +9 -0
  34. package/dist/consent/portable-envelope.js +110 -0
  35. package/dist/consent-banner/index.d.ts +33 -0
  36. package/dist/consent-banner/index.js +100 -0
  37. package/dist/content/assets.d.ts +1 -0
  38. package/dist/content/assets.js +1 -0
  39. package/dist/content/personalization.d.ts +35 -0
  40. package/dist/content/personalization.js +57 -0
  41. package/dist/content/resolve-slots.js +1 -0
  42. package/dist/content/types.d.ts +9 -1
  43. package/dist/edge/personalization-core.d.ts +11 -0
  44. package/dist/edge/personalization-core.js +265 -0
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +1 -0
  47. package/dist/next/assignments-script.d.ts +14 -0
  48. package/dist/next/assignments-script.js +17 -0
  49. package/dist/next/collect-route.d.ts +19 -0
  50. package/dist/next/collect-route.js +422 -0
  51. package/dist/next/envelope-digest.d.ts +21 -0
  52. package/dist/next/envelope-digest.js +58 -0
  53. package/dist/next/index.d.ts +1 -0
  54. package/dist/next/index.js +1 -0
  55. package/dist/next/middleware.d.ts +31 -3
  56. package/dist/next/middleware.js +254 -30
  57. package/dist/next/page.js +2 -1
  58. package/dist/next/personalization-host.d.ts +127 -0
  59. package/dist/next/personalization-host.js +257 -0
  60. package/dist/next/server.d.ts +1 -0
  61. package/dist/next/server.js +9 -6
  62. package/dist/next/vercel-collect-sdk-route.d.ts +27 -0
  63. package/dist/next/vercel-collect-sdk-route.js +205 -0
  64. package/dist/next/vercel-personalization.d.ts +4 -0
  65. package/dist/next/vercel-personalization.js +4 -0
  66. package/dist/next/vercel-s3-personalization.d.ts +79 -0
  67. package/dist/next/vercel-s3-personalization.js +574 -0
  68. package/dist/providers/s3.d.ts +20 -5
  69. package/dist/providers/s3.js +93 -23
  70. package/dist/react/VideoPlayer.d.ts +6 -6
  71. package/dist/react/index.d.ts +6 -6
  72. package/dist/testing/conformance/checks.js +75 -4
  73. package/dist/types/block-ref.d.ts +2 -0
  74. package/dist/types/block-ref.js +1 -0
  75. package/dist/types/page.d.ts +1 -1
  76. package/package.json +44 -15
  77. package/src/astro/ConsentBanner.astro +62 -0
  78. package/src/cli/compile-registry.mjs +24 -10
  79. package/src/cli/validate-content.mjs +52 -13
  80. package/src/cli/verify-next-cdn-routes.mjs +5 -4
  81. package/src/consent-banner/styles.css +110 -0
  82. package/src/types/block-ref.test.ts +45 -0
  83. package/src/types/block-ref.ts +2 -0
  84. package/src/types/page.ts +1 -1
  85. package/templates/astro/starter-content/src/cms/contracts/components/index.ts +2 -0
  86. package/templates/astro/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
  87. package/templates/astro/starter-content/src/cms/registry.ts +3 -0
  88. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +41 -0
  89. package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
  90. package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
  91. package/templates/astro/starter-content/src/fixtures/sections.ts +2 -1
  92. package/templates/astro/starter-content/src/render/RenderPage.astro +3 -2
  93. package/templates/astro/template/src/layouts/SiteLayout.astro +3 -1
  94. package/templates/astro/template/src/pages/[...slug].astro +1 -0
  95. package/templates/common/.agents/skills/authoring/SKILL.md +11 -3
  96. package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
  97. package/templates/common/.content/audiences/preference-blue/_index.json +11 -0
  98. package/templates/common/.content/audiences/preference-red/_index.json +11 -0
  99. package/templates/common/.content/overlays/audience/preference-blue/pages/home/_index.json +13 -0
  100. package/templates/common/.content/overlays/audience/preference-red/pages/home/_index.json +13 -0
  101. package/templates/common/.content/pages/home/_index.json +124 -4
  102. package/templates/common/.content/segments/preference-blue/_index.json +20 -0
  103. package/templates/common/.content/segments/preference-red/_index.json +20 -0
  104. package/templates/common/contracts/components/index.ts +21 -1
  105. package/templates/common/contracts/components/personalizationChoice.contract.ts +17 -0
  106. package/templates/common/starter-content/.content/pages/about/_index.json +3 -0
  107. package/templates/common/starter-content/.content/pages/home/_index.json +9 -0
  108. package/templates/nextjs/starter-content/src/cms/contracts/components/index.ts +3 -1
  109. package/templates/nextjs/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
  110. package/templates/nextjs/starter-content/src/cms/registry.ts +3 -0
  111. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +53 -0
  112. package/templates/nextjs/starter-content/src/middleware.ts +41 -0
  113. package/templates/nextjs/template/src/app/collect/route.ts +18 -0
  114. package/templates/nextjs/template/src/middleware.ts +36 -2
  115. package/templates/nextjs/template/src/middleware.ts.tmpl +31 -1
  116. package/templates/astro/template/.aci.yaml +0 -28
  117. package/templates/common/.content/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
  118. package/templates/common/.content/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
  119. package/templates/common/.content/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
  120. package/templates/common/.content/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
  121. package/templates/common/.content/config/robots.txt +0 -2
  122. package/templates/common/.content/config/site.json +0 -197
  123. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/asset.json +0 -9
  124. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/editorial.json +0 -14
  125. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/governance.json +0 -23
  126. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/versions/v0001.json +0 -37
  127. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/asset.json +0 -9
  128. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/editorial.json +0 -14
  129. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/governance.json +0 -23
  130. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/versions/v0001.json +0 -37
  131. package/templates/common/.content/fragments/navbar/_index.json +0 -25
  132. package/templates/common/.content/fragments/site-footer/_index.json +0 -62
  133. package/templates/common/.content/pages/about/_index.json +0 -51
  134. package/templates/nextjs/template/.aci.yaml +0 -26
@@ -1,18 +1,40 @@
1
1
  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
+ import { CONTENT_PATH_HEADER, CONTENT_PATH_CACHE_PARAM, resolvePersonalizationRequest, } from './personalization-host.js';
4
5
  const RELEASE_SCOPED_PREFIX = '/__r';
5
- // CMS document paths (robots.txt, sitemap.xml) pass through the middleware
6
- // and are served by Next.js rewrites configured in withAci().
7
- const CMS_DOCUMENT_PATHS = new Set(['/robots.txt', '/sitemap.xml']);
6
+ const FRAMEWORK_ORIGIN_AUTH_HEADER = 'x-gradial-framework-origin-auth';
7
+ const FRAMEWORK_SITE_ID_HEADER = 'x-baremetal-site-id';
8
+ const FRAMEWORK_RELEASE_ID_HEADER = 'x-baremetal-release-id';
9
+ const FRAMEWORK_CONTENT_REF_HEADER = 'x-aci-content-ref';
10
+ const FRAMEWORK_CONTENT_BASE_URL_HEADER = 'x-baremetal-content-base-url';
11
+ const textEncoder = new TextEncoder();
12
+ const CMS_ASSET_PATHS = new Set(['/robots.txt', '/sitemap.xml']);
13
+ const FRAMEWORK_RUNTIME_PATHS = new Set(['/favicon.ico', '/_health']);
14
+ const FRAMEWORK_RUNTIME_PREFIXES = [
15
+ '/_next',
16
+ '/api',
17
+ '/collect',
18
+ '/gradial/assets',
19
+ '/.gradial-dam',
20
+ ];
8
21
  const PUBLIC_PATHS = new Set(typeof __ACI_PUBLIC_PATHS__ !== 'undefined' ? __ACI_PUBLIC_PATHS__ : []);
9
22
  const PASSTHROUGH_ROUTES = (typeof __ACI_PASSTHROUGH_ROUTES__ !== 'undefined' ? __ACI_PASSTHROUGH_ROUTES__ : []);
10
- const ASSET_PREFIX = typeof __ACI_ASSET_PREFIX__ !== 'undefined'
11
- ? __ACI_ASSET_PREFIX__
12
- : '/.gradial-dam';
13
23
  export function createGradialMiddleware(config = {}) {
14
24
  return async function middleware(request) {
15
25
  const url = request.nextUrl;
26
+ const frameworkDispatch = await resolveFrameworkDispatch(request, config);
27
+ if (frameworkDispatch.kind === 'rejected') {
28
+ return frameworkDispatch.response;
29
+ }
30
+ // The outer framework proxy authenticates every request with reserved
31
+ // dispatch headers, including Next/API/static runtime paths. Match those
32
+ // paths so the handoff is validated and reserved headers are stripped.
33
+ // Document rewrites retain only the validated, canonical content-path
34
+ // query because Vercel uses it as the selected payload's cache identity.
35
+ if (isFrameworkRuntimePath(url.pathname)) {
36
+ return passThroughResponse(request, frameworkDispatch);
37
+ }
16
38
  // ?leave_preview=1 → clear cookies, redirect to clean URL
17
39
  if (url.searchParams.get('leave_preview') === '1') {
18
40
  const cleanUrl = new URL(url.pathname, url.origin);
@@ -22,18 +44,13 @@ export function createGradialMiddleware(config = {}) {
22
44
  return response;
23
45
  }
24
46
  if (PUBLIC_PATHS.has(url.pathname)) {
25
- return NextResponse.next();
26
- }
27
- if (CMS_DOCUMENT_PATHS.has(url.pathname)) {
28
- return NextResponse.next();
47
+ return passThroughResponse(request, frameworkDispatch);
29
48
  }
30
- // DAM asset requests are NOT release-scoped; they rewrite to S3 via
31
- // withAci() rewrites. Pass them through so the rewrite fires.
32
- if (isAssetPath(url.pathname)) {
33
- return NextResponse.next();
49
+ if (CMS_ASSET_PATHS.has(url.pathname)) {
50
+ return passThroughResponse(request, frameworkDispatch);
34
51
  }
35
52
  if (isPassthroughRoute(url.pathname)) {
36
- return NextResponse.next();
53
+ return passThroughResponse(request, frameworkDispatch);
37
54
  }
38
55
  // Block direct access to /__r/... — only middleware rewrites may use it.
39
56
  if (isReleaseScopedPath(url.pathname, RELEASE_SCOPED_PREFIX)) {
@@ -42,12 +59,20 @@ export function createGradialMiddleware(config = {}) {
42
59
  headers: { 'Cache-Control': 'private, no-store' },
43
60
  });
44
61
  }
62
+ if (frameworkDispatch.kind === 'trusted') {
63
+ const response = rewriteToReleaseScopedPath(request, frameworkDispatch.releaseId, false, {
64
+ contentPath: frameworkDispatch.contentPath,
65
+ requestHeaders: frameworkRequestHeaders(request, frameworkDispatch.contentPath),
66
+ });
67
+ enforceCookieCacheRule(response);
68
+ return response;
69
+ }
45
70
  // Try preview token first; if valid, use that release.
46
71
  // If no token or invalid, fall through to active release from edge config.
47
72
  const preview = await resolvePreviewFromRequest(request, config);
48
73
  // Local file-backed development has no release-indexed artifact tree. A
49
74
  // valid preview token is still useful there—it proves signing, expiry, and
50
- // cookie propagation—but rewriting to /__r/{releaseId}/{path} would create
75
+ // cookie propagation—but rewriting to /__r/{release}/routes would create
51
76
  // a path the file provider cannot serve. Keep the public route and mark
52
77
  // the response private, matching preview's cache semantics.
53
78
  if (process.env.ACI_CONTENT_PROVIDER === 'file') {
@@ -59,6 +84,7 @@ export function createGradialMiddleware(config = {}) {
59
84
  response.cookies.set(cookie.name, cookie.value, cookie.options);
60
85
  }
61
86
  }
87
+ enforceCookieCacheRule(response);
62
88
  return response;
63
89
  }
64
90
  const releaseId = preview?.releaseId || await activeReleaseFromEdgeConfig(config);
@@ -85,25 +111,88 @@ export function createGradialMiddleware(config = {}) {
85
111
  response.cookies.set(cookie.name, cookie.value, cookie.options);
86
112
  }
87
113
  }
114
+ enforceCookieCacheRule(response);
88
115
  return response;
89
116
  }
90
117
  // Rewrite public URL to release-scoped internal path for CDN cache isolation.
91
- // Each release gets a unique URL (/__r/{releaseId}/...) so the CDN
118
+ // Each release gets a unique URL (/__r/{releaseId}/routes/...) so the CDN
92
119
  // cache key is per-release. New release = new URL = cache miss = fresh content.
93
- const response = rewriteToReleaseScopedPath(request, releaseId, Boolean(preview?.isPreview));
120
+ let personalization;
121
+ if (config.personalization) {
122
+ const siteId = config.personalization.siteId || siteIdForConfig(config);
123
+ if (!siteId) {
124
+ return new Response('ACI site id is not configured', {
125
+ status: 500,
126
+ headers: { 'Cache-Control': 'private, no-store' },
127
+ });
128
+ }
129
+ try {
130
+ personalization = await resolvePersonalizationRequest(request, releaseId, {
131
+ ...config.personalization,
132
+ siteId,
133
+ });
134
+ }
135
+ catch {
136
+ return new Response('personalization artifacts unavailable', {
137
+ status: 503,
138
+ headers: { 'Cache-Control': 'private, no-store' },
139
+ });
140
+ }
141
+ }
142
+ const response = rewriteToReleaseScopedPath(request, releaseId, Boolean(preview?.isPreview), personalization
143
+ ? {
144
+ contentPath: personalization.contentPath,
145
+ requestHeaders: personalization.requestHeaders,
146
+ }
147
+ : undefined);
94
148
  // Persist preview token as cookie so navigation stays in preview
95
149
  if (preview?.token && preview.isPreview) {
96
150
  for (const cookie of createPreviewCookies(preview.token, releaseId)) {
97
151
  response.cookies.set(cookie.name, cookie.value, cookie.options);
98
152
  }
99
153
  }
154
+ if (personalization) {
155
+ for (const cookie of personalization.cookies) {
156
+ response.cookies.set(cookie.name, cookie.value, cookie.options);
157
+ }
158
+ if (personalization.variantKey) {
159
+ response.headers.set('X-ACI-Variant-Key', personalization.variantKey);
160
+ }
161
+ if (personalization.experiment) {
162
+ response.headers.set('X-ACI-Experiment-ID', personalization.experiment.id);
163
+ response.headers.set('X-ACI-Experiment-Arm', personalization.experiment.arm);
164
+ response.headers.set('X-ACI-Experiment-Allocation-Version', personalization.experiment.allocationVersion);
165
+ }
166
+ }
167
+ enforceCookieCacheRule(response);
100
168
  return response;
101
169
  };
102
170
  }
103
- function rewriteToReleaseScopedPath(request, releaseId, isPreview) {
171
+ /**
172
+ * Any response that writes a cookie is per-visitor and must never enter a
173
+ * shared cache (anonymous-experiments REQ-3(a)): a CDN-cached Set-Cookie
174
+ * replays one visitor's identity/claims to everyone under that cache key.
175
+ * Per-variant CACHING itself stays legitimate — the variant travels in the
176
+ * cache-keyed query parameter, not in cookies.
177
+ */
178
+ export function enforceCookieCacheRule(response) {
179
+ if (response.cookies.getAll().length === 0) {
180
+ return;
181
+ }
182
+ response.headers.set('Cache-Control', 'private, no-store');
183
+ response.headers.delete('CDN-Cache-Control');
184
+ response.headers.delete('Vercel-CDN-Cache-Control');
185
+ }
186
+ function rewriteToReleaseScopedPath(request, releaseId, isPreview, options) {
104
187
  const rewriteURL = new URL(request.url);
105
188
  rewriteURL.pathname = releaseScopedPath(RELEASE_SCOPED_PREFIX, releaseId, request.nextUrl.pathname);
106
- const response = NextResponse.rewrite(rewriteURL);
189
+ rewriteURL.searchParams.delete(CONTENT_PATH_CACHE_PARAM);
190
+ if (options?.contentPath) {
191
+ rewriteURL.searchParams.set(CONTENT_PATH_CACHE_PARAM, options.contentPath);
192
+ }
193
+ const response = options?.requestHeaders
194
+ ? NextResponse.rewrite(rewriteURL, { request: { headers: options.requestHeaders } })
195
+ : NextResponse.rewrite(rewriteURL);
107
196
  if (isPreview) {
108
197
  response.headers.set('Cache-Control', 'private, no-store');
109
198
  }
@@ -114,6 +203,113 @@ function rewriteToReleaseScopedPath(request, releaseId, isPreview) {
114
203
  }
115
204
  return response;
116
205
  }
206
+ async function resolveFrameworkDispatch(request, config) {
207
+ // The outer edge has already minted identity and selected a release/payload.
208
+ // A trusted handoff must use that exact decision; falling through would mint
209
+ // a second identity and may select a different experiment arm.
210
+ const originAuth = request.headers.get(FRAMEWORK_ORIGIN_AUTH_HEADER);
211
+ const forwardedSiteId = request.headers.get(FRAMEWORK_SITE_ID_HEADER);
212
+ const forwardedReleaseId = request.headers.get(FRAMEWORK_RELEASE_ID_HEADER);
213
+ const forwardedContentRef = request.headers.get(FRAMEWORK_CONTENT_REF_HEADER);
214
+ const contentPathValues = request.nextUrl.searchParams.getAll(CONTENT_PATH_CACHE_PARAM);
215
+ const hasReservedDispatch = originAuth !== null
216
+ || forwardedSiteId !== null
217
+ || forwardedReleaseId !== null
218
+ || forwardedContentRef !== null
219
+ || request.headers.has(FRAMEWORK_CONTENT_BASE_URL_HEADER)
220
+ || request.headers.has(CONTENT_PATH_HEADER)
221
+ || contentPathValues.length > 0;
222
+ if (!hasReservedDispatch) {
223
+ return { kind: 'direct' };
224
+ }
225
+ const expectedOriginAuth = config.frameworkOriginAuthToken
226
+ || process.env.ACI_FRAMEWORK_ORIGIN_AUTH_TOKEN
227
+ || '';
228
+ if (!expectedOriginAuth
229
+ || originAuth === null
230
+ || !(await secureStringEqual(expectedOriginAuth, originAuth))) {
231
+ return rejectedFrameworkDispatch(403, 'framework dispatch forbidden');
232
+ }
233
+ if (!forwardedSiteId || !forwardedReleaseId) {
234
+ return rejectedFrameworkDispatch(400, 'invalid framework dispatch');
235
+ }
236
+ const expectedSiteId = siteIdForConfig(config);
237
+ if (!expectedSiteId || forwardedSiteId !== expectedSiteId) {
238
+ return rejectedFrameworkDispatch(403, 'framework dispatch forbidden');
239
+ }
240
+ if (forwardedContentRef === null && contentPathValues.length === 0) {
241
+ return {
242
+ kind: 'trusted',
243
+ releaseId: forwardedReleaseId,
244
+ contentPath: '',
245
+ };
246
+ }
247
+ if (!forwardedContentRef
248
+ || contentPathValues.length !== 1
249
+ || contentPathValues[0] !== forwardedContentRef) {
250
+ return rejectedFrameworkDispatch(400, 'invalid framework dispatch');
251
+ }
252
+ return {
253
+ kind: 'trusted',
254
+ releaseId: forwardedReleaseId,
255
+ contentPath: forwardedContentRef,
256
+ };
257
+ }
258
+ function passThroughResponse(request, frameworkDispatch) {
259
+ if (frameworkDispatch.kind !== 'trusted') {
260
+ return NextResponse.next();
261
+ }
262
+ if (frameworkDispatch.contentPath) {
263
+ return new Response('invalid framework dispatch', {
264
+ status: 400,
265
+ headers: { 'Cache-Control': 'private, no-store' },
266
+ });
267
+ }
268
+ return NextResponse.next({
269
+ request: {
270
+ headers: sanitizedFrameworkRequestHeaders(request),
271
+ },
272
+ });
273
+ }
274
+ function frameworkRequestHeaders(request, contentPath) {
275
+ const requestHeaders = sanitizedFrameworkRequestHeaders(request);
276
+ if (contentPath) {
277
+ requestHeaders.set(CONTENT_PATH_HEADER, contentPath);
278
+ }
279
+ return requestHeaders;
280
+ }
281
+ function sanitizedFrameworkRequestHeaders(request) {
282
+ const requestHeaders = new Headers(request.headers);
283
+ requestHeaders.delete(FRAMEWORK_ORIGIN_AUTH_HEADER);
284
+ requestHeaders.delete(FRAMEWORK_SITE_ID_HEADER);
285
+ requestHeaders.delete(FRAMEWORK_RELEASE_ID_HEADER);
286
+ requestHeaders.delete(FRAMEWORK_CONTENT_REF_HEADER);
287
+ requestHeaders.delete(FRAMEWORK_CONTENT_BASE_URL_HEADER);
288
+ requestHeaders.delete(CONTENT_PATH_HEADER);
289
+ return requestHeaders;
290
+ }
291
+ function rejectedFrameworkDispatch(status, message) {
292
+ return {
293
+ kind: 'rejected',
294
+ response: new Response(message, {
295
+ status,
296
+ headers: { 'Cache-Control': 'private, no-store' },
297
+ }),
298
+ };
299
+ }
300
+ async function secureStringEqual(expected, actual) {
301
+ const [expectedDigest, actualDigest] = await Promise.all([
302
+ crypto.subtle.digest('SHA-256', textEncoder.encode(expected)),
303
+ crypto.subtle.digest('SHA-256', textEncoder.encode(actual)),
304
+ ]);
305
+ const expectedBytes = new Uint8Array(expectedDigest);
306
+ const actualBytes = new Uint8Array(actualDigest);
307
+ let different = 0;
308
+ for (let index = 0; index < expectedBytes.length; index += 1) {
309
+ different |= expectedBytes[index] ^ actualBytes[index];
310
+ }
311
+ return different === 0;
312
+ }
117
313
  function isReleaseScopedPath(pathname, prefix) {
118
314
  return pathname === prefix || pathname.startsWith(`${prefix}/`);
119
315
  }
@@ -185,8 +381,8 @@ async function activeReleaseFromEdgeConfig(config) {
185
381
  return active.releaseId;
186
382
  return clampRelease(active.releaseId, active.sequence || 0, compatibility);
187
383
  }
188
- function siteIdForConfig(_config) {
189
- return process.env.ACI_SITE_ID || '';
384
+ function siteIdForConfig(config) {
385
+ return config.siteId || process.env.ACI_SITE_ID || '';
190
386
  }
191
387
  async function getEdgeConfigJSON(edgeConfig, key) {
192
388
  const value = await getEdgeConfigValue(edgeConfig, key);
@@ -243,16 +439,15 @@ function clampRelease(activeReleaseId, activeSequence, compatibility) {
243
439
  function edgeKeySegment(value) {
244
440
  return value.replace(/[^A-Za-z0-9_-]/g, '_');
245
441
  }
246
- function isAssetPath(pathname) {
247
- if (pathname === ASSET_PREFIX || pathname.startsWith(`${ASSET_PREFIX}/`))
248
- return true;
249
- if (pathname.startsWith('/gradial/assets/'))
250
- return true;
251
- return false;
252
- }
253
442
  function isPassthroughRoute(pathname) {
254
443
  return PASSTHROUGH_ROUTES.some((route) => routeMatches(pathname, route));
255
444
  }
445
+ function isFrameworkRuntimePath(pathname) {
446
+ if (FRAMEWORK_RUNTIME_PATHS.has(pathname)) {
447
+ return true;
448
+ }
449
+ return FRAMEWORK_RUNTIME_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
450
+ }
256
451
  function routeMatches(pathname, route) {
257
452
  const cleanPath = normalizeRoutePath(pathname);
258
453
  const cleanRoute = normalizeRoutePath(route);
@@ -276,8 +471,37 @@ function normalizeRoutePath(value) {
276
471
  return pathname.replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
277
472
  }
278
473
  export const aciMiddlewareMatcher = [
279
- '/((?!_next(?:/|$)|api(?:/|$)|favicon\\.ico$|\\.gradial-dam(?:/|$)).*)',
474
+ '/((?!_next(?:/|$)|api(?:/|$)|collect(?:/|$)|gradial/assets(?:/|$)|favicon\\.ico$).*)',
475
+ {
476
+ source: '/:path*',
477
+ has: [{ type: 'header', key: 'x-gradial-framework-origin-auth' }],
478
+ },
479
+ {
480
+ source: '/:path*',
481
+ has: [{ type: 'header', key: 'x-baremetal-site-id' }],
482
+ },
483
+ {
484
+ source: '/:path*',
485
+ has: [{ type: 'header', key: 'x-baremetal-release-id' }],
486
+ },
487
+ {
488
+ source: '/:path*',
489
+ has: [{ type: 'header', key: 'x-aci-content-ref' }],
490
+ },
491
+ {
492
+ source: '/:path*',
493
+ has: [{ type: 'header', key: 'x-baremetal-content-base-url' }],
494
+ },
495
+ {
496
+ source: '/:path*',
497
+ has: [{ type: 'header', key: 'x-aci-content-path' }],
498
+ },
499
+ {
500
+ source: '/:path*',
501
+ has: [{ type: 'query', key: '__aci_content_path' }],
502
+ },
280
503
  ];
281
504
  export const config = {
505
+ runtime: 'nodejs',
282
506
  matcher: aciMiddlewareMatcher,
283
507
  };
package/dist/next/page.js CHANGED
@@ -4,6 +4,7 @@ import { Fragment } from 'react';
4
4
  import { PageNotFoundError, getRenderInput, getLayout, getFragment, } from './server.js';
5
5
  import { routeMetadataForContent, resolveSlots, } from '../content/index.js';
6
6
  import { renderBlocks } from './render.js';
7
+ import { AssignmentsScript } from './assignments-script.js';
7
8
  import { CollectScript } from './collect-script.js';
8
9
  /**
9
10
  * Creates the catch-all route handler for a Next.js ACI site.
@@ -80,7 +81,7 @@ export function createPage(registry, options) {
80
81
  }
81
82
  return (_jsx(Fragment, { children: renderedBlocks }, slot.name));
82
83
  }));
83
- return (_jsxs(_Fragment, { children: [renderedSlots.filter(Boolean), _jsx(CollectScript, { siteConfig: input.siteConfig })] }));
84
+ return (_jsxs(_Fragment, { children: [renderedSlots.filter(Boolean), _jsx(AssignmentsScript, { page: input.page }), _jsx(CollectScript, { siteConfig: input.siteConfig })] }));
84
85
  }
85
86
  catch (error) {
86
87
  if (error instanceof PageNotFoundError)
@@ -0,0 +1,127 @@
1
+ import { type PersonalizationExperiment } from '../edge/personalization-core.js';
2
+ import { type PortableConsentSnapshot } from '../consent/portable-envelope.js';
3
+ export declare const CONTENT_PATH_HEADER = "x-aci-content-path";
4
+ export declare const CONTENT_PATH_CACHE_PARAM = "__aci_content_path";
5
+ export interface EdgeDispatch {
6
+ kind: 'static-direct' | 'compose-origin' | 'framework-origin' | string;
7
+ artifactPath?: string;
8
+ contentPath?: string;
9
+ }
10
+ export interface EdgeRuleCondition {
11
+ field: string;
12
+ operator: 'exists' | 'equals' | 'contains' | 'startsWith' | string;
13
+ value?: unknown;
14
+ }
15
+ export interface EdgeRuleExpression {
16
+ all?: EdgeRuleCondition[];
17
+ any?: EdgeRuleCondition[];
18
+ }
19
+ export interface EdgeRouteVariant {
20
+ variantKey: string;
21
+ variantValues?: Record<string, string>;
22
+ priority?: number;
23
+ rule?: EdgeRuleExpression;
24
+ dispatch: EdgeDispatch;
25
+ }
26
+ export interface EdgeRouteEntry {
27
+ pattern: string;
28
+ priority: number;
29
+ dispatch: EdgeDispatch;
30
+ variants?: Record<string, EdgeRouteVariant>;
31
+ }
32
+ export interface EdgeRouteManifest {
33
+ manifestVersion: string;
34
+ releaseId: string;
35
+ siteId: string;
36
+ routes: Record<string, EdgeRouteEntry>;
37
+ experiments?: Record<string, PersonalizationExperiment>;
38
+ notFoundPage?: string;
39
+ errorPage?: string;
40
+ }
41
+ export interface PersonalizationEnvelopeWrite {
42
+ key: string;
43
+ envelopeId: string;
44
+ siteId: string;
45
+ identityNamespace: string;
46
+ identityHash: string;
47
+ records: readonly unknown[];
48
+ digest: string;
49
+ }
50
+ export declare class IdentityErasureFencedError extends Error {
51
+ readonly name = "IdentityErasureFencedError";
52
+ constructor();
53
+ }
54
+ /** Immutable release routing state read by an EdgeRouter. */
55
+ export interface PersonalizationRouteStore {
56
+ readEdgeManifest(releaseId: string): Promise<EdgeRouteManifest | null>;
57
+ }
58
+ /** Customer-owned collection state. This is deliberately not an EdgeRouter dependency. */
59
+ export interface PersonalizationCollectionStore {
60
+ readClaims(siteId: string, namespace: string, identityHash: string): Promise<string | null>;
61
+ appendEnvelope(write: PersonalizationEnvelopeWrite): Promise<void>;
62
+ putSuppression(key: string, record: unknown): Promise<void>;
63
+ }
64
+ /** @deprecated Use the narrower router or collection store at each boundary. */
65
+ export interface PersonalizationBlobStore extends PersonalizationRouteStore, PersonalizationCollectionStore {
66
+ }
67
+ export interface SigningKey {
68
+ kid: string;
69
+ secret: string;
70
+ }
71
+ export interface IdentitySigningKeys {
72
+ current: SigningKey;
73
+ previous?: SigningKey;
74
+ }
75
+ export interface PersonalizationHostConfig {
76
+ siteId: string;
77
+ store: PersonalizationRouteStore;
78
+ identityKeys: IdentitySigningKeys;
79
+ claimsKeys?: Record<string, string>;
80
+ identityCookieTTLSeconds?: number;
81
+ secureCookies?: boolean;
82
+ now?: () => Date;
83
+ randomBytes?: (length: number) => Uint8Array;
84
+ }
85
+ export interface CookieMutation {
86
+ name: string;
87
+ value: string;
88
+ options: {
89
+ path: '/';
90
+ httpOnly?: boolean;
91
+ secure?: boolean;
92
+ sameSite: 'lax';
93
+ maxAge: number;
94
+ expires?: Date;
95
+ };
96
+ }
97
+ export interface PersonalizationResolution {
98
+ requestHeaders: Headers;
99
+ contentPath: string;
100
+ variantKey: string;
101
+ experiment?: {
102
+ id: string;
103
+ arm: string;
104
+ allocationVersion: string;
105
+ };
106
+ cookies: CookieMutation[];
107
+ }
108
+ export type ConsentSnapshot = PortableConsentSnapshot;
109
+ interface ClaimsPayload {
110
+ aud?: string[];
111
+ seg?: string[];
112
+ iat: number;
113
+ exp: number;
114
+ }
115
+ export declare function resolvePersonalizationRequest(request: Request, releaseId: string, config: PersonalizationHostConfig): Promise<PersonalizationResolution>;
116
+ export declare function consentSignals(request: Request, cookies: Map<string, string>): {
117
+ hasConsent: boolean;
118
+ snapshot: ConsentSnapshot | null;
119
+ allowsIdentityTier: boolean;
120
+ privacyDenied: boolean;
121
+ };
122
+ export declare function parseCookieHeader(value: string | null): Map<string, string>;
123
+ export declare function verifyIdentityCookie(value: string, keys: IdentitySigningKeys): Promise<string | null>;
124
+ export declare function sha256Hex(value: string): Promise<string>;
125
+ export declare function identityHashHex(namespace: string, value: string): Promise<string>;
126
+ export declare function verifyClaims(value: string, keys: Record<string, string>, now: Date): Promise<ClaimsPayload | null>;
127
+ export {};