@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.
- package/CHANGELOG.md +15 -11
- package/README.md +4 -4
- package/dist/astro/index.d.ts +1 -1
- package/dist/astro/index.js +1 -1
- package/dist/astro/page.d.ts +6 -0
- package/dist/astro/page.js +3 -2
- package/dist/audience-data-models.d.ts +286 -0
- package/dist/audience-data-models.js +46 -0
- package/dist/cli/compile-registry.d.mts +6 -0
- package/dist/cli/compile-registry.mjs +774 -0
- package/dist/cli/materialize-template.d.mts +19 -0
- package/dist/cli/materialize-template.mjs +128 -0
- package/dist/cli/validate-content.d.mts +32 -0
- package/dist/cli/validate-content.mjs +464 -0
- package/dist/cli/verify-next-cdn-routes.d.mts +2 -0
- package/dist/cli/verify-next-cdn-routes.mjs +47 -0
- package/dist/collect/aci.js +1 -0
- package/dist/collect/aci.js.sha256 +1 -0
- package/dist/collect/assignments.d.ts +65 -0
- package/dist/collect/assignments.js +138 -0
- package/dist/collect/batcher.d.ts +23 -0
- package/dist/collect/batcher.js +28 -6
- package/dist/collect/consent/envelope.d.ts +9 -3
- package/dist/collect/consent/envelope.js +58 -40
- package/dist/collect/declarative.d.ts +1 -1
- package/dist/collect/declarative.js +7 -1
- package/dist/collect/index.js +39 -6
- package/dist/collect/linkclicks.js +4 -2
- package/dist/collect/pageviews.d.ts +15 -3
- package/dist/collect/pageviews.js +28 -13
- package/dist/collect/transport.d.ts +4 -0
- package/dist/collect/transport.js +37 -2
- package/dist/consent/portable-envelope.d.ts +9 -0
- package/dist/consent/portable-envelope.js +110 -0
- package/dist/consent-banner/index.d.ts +33 -0
- package/dist/consent-banner/index.js +100 -0
- package/dist/content/assets.d.ts +1 -0
- package/dist/content/assets.js +1 -0
- package/dist/content/personalization.d.ts +35 -0
- package/dist/content/personalization.js +57 -0
- package/dist/content/resolve-slots.js +1 -0
- package/dist/content/types.d.ts +9 -1
- package/dist/edge/personalization-core.d.ts +11 -0
- package/dist/edge/personalization-core.js +265 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/next/assignments-script.d.ts +14 -0
- package/dist/next/assignments-script.js +17 -0
- package/dist/next/collect-route.d.ts +19 -0
- package/dist/next/collect-route.js +422 -0
- package/dist/next/envelope-digest.d.ts +21 -0
- package/dist/next/envelope-digest.js +58 -0
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +1 -0
- package/dist/next/middleware.d.ts +31 -3
- package/dist/next/middleware.js +254 -30
- package/dist/next/page.js +2 -1
- package/dist/next/personalization-host.d.ts +127 -0
- package/dist/next/personalization-host.js +257 -0
- package/dist/next/server.d.ts +1 -0
- package/dist/next/server.js +9 -6
- package/dist/next/vercel-collect-sdk-route.d.ts +27 -0
- package/dist/next/vercel-collect-sdk-route.js +205 -0
- package/dist/next/vercel-personalization.d.ts +4 -0
- package/dist/next/vercel-personalization.js +4 -0
- package/dist/next/vercel-s3-personalization.d.ts +79 -0
- package/dist/next/vercel-s3-personalization.js +574 -0
- package/dist/providers/s3.d.ts +20 -5
- package/dist/providers/s3.js +93 -23
- package/dist/react/VideoPlayer.d.ts +6 -6
- package/dist/react/index.d.ts +6 -6
- package/dist/testing/conformance/checks.js +75 -4
- package/dist/types/block-ref.d.ts +2 -0
- package/dist/types/block-ref.js +1 -0
- package/dist/types/page.d.ts +1 -1
- package/package.json +44 -15
- package/src/astro/ConsentBanner.astro +62 -0
- package/src/cli/compile-registry.mjs +24 -10
- package/src/cli/validate-content.mjs +52 -13
- package/src/cli/verify-next-cdn-routes.mjs +5 -4
- package/src/consent-banner/styles.css +110 -0
- package/src/types/block-ref.test.ts +45 -0
- package/src/types/block-ref.ts +2 -0
- package/src/types/page.ts +1 -1
- package/templates/astro/starter-content/src/cms/contracts/components/index.ts +2 -0
- package/templates/astro/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
- package/templates/astro/starter-content/src/cms/registry.ts +3 -0
- package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +41 -0
- package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
- package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
- package/templates/astro/starter-content/src/fixtures/sections.ts +2 -1
- package/templates/astro/starter-content/src/render/RenderPage.astro +3 -2
- package/templates/astro/template/src/layouts/SiteLayout.astro +3 -1
- package/templates/astro/template/src/pages/[...slug].astro +1 -0
- package/templates/common/.agents/skills/authoring/SKILL.md +11 -3
- package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
- package/templates/common/.content/audiences/preference-blue/_index.json +11 -0
- package/templates/common/.content/audiences/preference-red/_index.json +11 -0
- package/templates/common/.content/overlays/audience/preference-blue/pages/home/_index.json +13 -0
- package/templates/common/.content/overlays/audience/preference-red/pages/home/_index.json +13 -0
- package/templates/common/.content/pages/home/_index.json +124 -4
- package/templates/common/.content/segments/preference-blue/_index.json +20 -0
- package/templates/common/.content/segments/preference-red/_index.json +20 -0
- package/templates/common/contracts/components/index.ts +21 -1
- package/templates/common/contracts/components/personalizationChoice.contract.ts +17 -0
- package/templates/common/starter-content/.content/pages/about/_index.json +3 -0
- package/templates/common/starter-content/.content/pages/home/_index.json +9 -0
- package/templates/nextjs/starter-content/src/cms/contracts/components/index.ts +3 -1
- package/templates/nextjs/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
- package/templates/nextjs/starter-content/src/cms/registry.ts +3 -0
- package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +53 -0
- package/templates/nextjs/starter-content/src/middleware.ts +41 -0
- package/templates/nextjs/template/src/app/collect/route.ts +18 -0
- package/templates/nextjs/template/src/middleware.ts +36 -2
- package/templates/nextjs/template/src/middleware.ts.tmpl +31 -1
- package/templates/astro/template/.aci.yaml +0 -28
- package/templates/common/.content/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
- package/templates/common/.content/config/robots.txt +0 -2
- package/templates/common/.content/config/site.json +0 -197
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/asset.json +0 -9
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/editorial.json +0 -14
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/governance.json +0 -23
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/versions/v0001.json +0 -37
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/asset.json +0 -9
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/editorial.json +0 -14
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/governance.json +0 -23
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/versions/v0001.json +0 -37
- package/templates/common/.content/fragments/navbar/_index.json +0 -25
- package/templates/common/.content/fragments/site-footer/_index.json +0 -62
- package/templates/common/.content/pages/about/_index.json +0 -51
- package/templates/nextjs/template/.aci.yaml +0 -26
package/dist/next/middleware.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
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
|
|
26
|
-
}
|
|
27
|
-
if (CMS_DOCUMENT_PATHS.has(url.pathname)) {
|
|
28
|
-
return NextResponse.next();
|
|
47
|
+
return passThroughResponse(request, frameworkDispatch);
|
|
29
48
|
}
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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/{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(?:/|$)|
|
|
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 {};
|