@gradial/aci 0.1.52-rc.1 → 0.1.52-rc.13
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 +7 -0
- package/README.md +4 -4
- package/dist/audience-data-models.d.ts +96 -0
- package/dist/audience-data-models.js +52 -0
- package/dist/cli/compile-registry.d.mts +5 -1
- package/dist/cli/compile-registry.mjs +25 -10
- package/dist/cli/validate-content.d.mts +31 -1
- package/dist/cli/validate-content.mjs +50 -12
- package/dist/collect/aci.js +1 -1
- package/dist/collect/aci.js.sha256 +1 -1
- package/dist/collect/api.d.ts +3 -7
- package/dist/collect/api.js +2 -8
- package/dist/collect/index.js +1 -1
- package/dist/content/provider.d.ts +1 -0
- package/dist/content/resolve-slots.d.ts +9 -2
- package/dist/content/resolve-slots.js +5 -3
- package/dist/content/types.d.ts +2 -0
- package/dist/edge/personalization-core.js +13 -1
- package/dist/internal/domain/edge/compose.d.ts +24 -0
- package/dist/internal/domain/edge/compose.js +89 -0
- package/dist/internal/domain/edge/index.d.ts +2 -0
- package/dist/internal/domain/edge/index.js +2 -0
- package/dist/internal/domain/edge/mimic_token.d.ts +13 -0
- package/dist/internal/domain/edge/mimic_token.js +59 -0
- package/dist/internal/domain/edge/personalization-core.d.ts +4 -1
- package/dist/internal/domain/edge/personalization-core.js +25 -8
- package/dist/internal/domain/edge/resolve.js +19 -9
- package/dist/internal/domain/edge/types.d.ts +16 -0
- package/dist/mimic/core.d.ts +34 -0
- package/dist/mimic/core.js +127 -0
- package/dist/next/collect-route.d.ts +6 -0
- package/dist/next/collect-route.js +15 -0
- package/dist/next/error-seat-payload.js +1 -1
- package/dist/next/hosted-page-target.d.ts +7 -0
- package/dist/next/hosted-page-target.js +39 -0
- package/dist/next/index.d.ts +0 -1
- package/dist/next/index.js +0 -1
- package/dist/next/page.d.ts +29 -6
- package/dist/next/page.js +47 -25
- package/dist/next/personalization-host.d.ts +4 -0
- package/dist/next/personalization-host.js +18 -7
- package/dist/next/server.js +28 -1
- package/dist/next/vercel-edge-projection-v1.js +7 -1
- package/dist/next/vercel-s3-personalization.d.ts +1 -0
- package/dist/next/vercel-s3-personalization.js +1 -0
- package/dist/providers/s3.js +11 -4
- package/dist/types/block-ref.d.ts +2 -0
- package/dist/types/block-ref.js +1 -0
- package/package.json +7 -7
- package/src/cli/compile-registry.mjs +24 -10
- package/src/cli/validate-content.mjs +52 -13
- package/src/types/block-ref.test.ts +45 -0
- package/src/types/block-ref.ts +2 -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/template/package.json.tmpl +1 -1
- package/templates/astro/template/src/layouts/SiteLayout.astro +2 -2
- 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/pages/{home/_index.json → _index.json} +10 -0
- package/templates/common/.content/schemas/site.schema.json +1 -1
- package/templates/common/.nvmrc +1 -1
- package/templates/common/starter-content/.content/.rules-baseline.json +8 -0
- package/templates/common/starter-content/.content/config/site.json +1 -1
- package/templates/common/starter-content/.content/content-rules/no-public-resource-paths.json +6 -0
- package/templates/common/starter-content/.content/fragments/navbar/_index.json +23 -20
- package/templates/common/starter-content/.content/fragments/site-footer/_index.json +59 -56
- package/templates/common/starter-content/.content/pages/404/_index.json +1 -0
- package/templates/common/starter-content/.content/pages/about/_index.json +3 -0
- package/templates/common/starter-content/.content/pages/error/_index.json +1 -0
- package/templates/common/starter-content/.content/pages/home/_index.json +9 -0
- package/templates/common/starter-content/.content/patterns/components/button.json +55 -0
- package/templates/common/starter-content/.content/patterns/components/container.json +62 -0
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +30 -0
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +57 -0
- package/templates/common/starter-content/.content/patterns/components/home_hero.json +142 -0
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +42 -0
- package/templates/common/starter-content/.content/patterns/components/site-footer.json +203 -0
- package/templates/common/starter-content/.content/patterns/components/site-navigation.json +115 -0
- package/templates/common/starter-content/.content/patterns/components/stats_bar.json +45 -0
- package/templates/common/starter-content/.content/patterns/layouts/default.json +14 -0
- package/templates/common/starter-content/.content/schemas/site.schema.json +1 -1
- package/templates/nextjs/template/.aci.yaml +10 -0
- package/templates/nextjs/template/package.json.tmpl +2 -1
- package/templates/nextjs/template/src/app/_components/c15t-bridge.ts +30 -0
- package/templates/nextjs/template/src/app/_components/c15t-consent-ui.tsx +37 -0
- package/templates/nextjs/template/src/app/_components/consent-manager.css +63 -0
- package/templates/nextjs/template/src/app/_components/consent-manager.tsx +76 -0
- package/templates/nextjs/template/src/app/layout.tsx +2 -0
- package/dist/next/middleware.d.ts +0 -40
- package/dist/next/middleware.js +0 -475
- /package/templates/common/.content/overlays/audience/preference-blue/pages/{home/_index.json → _index.json} +0 -0
- /package/templates/common/.content/overlays/audience/preference-red/pages/{home/_index.json → _index.json} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MAX_BATCH_EVENTS } from '../collect/batcher.js';
|
|
2
2
|
import { computeEnvelopeDigest } from './envelope-digest.js';
|
|
3
|
+
import { validateMimicToken } from '../mimic/core.js';
|
|
3
4
|
import { consentSignals, IdentityErasureFencedError, identityHashHex, parseCookieHeader, verifyClaims, verifyIdentityCookie, } from './personalization-host.js';
|
|
4
5
|
const MAX_COLLECT_BODY_BYTES = 64 * 1024;
|
|
5
6
|
const DEFAULT_RETENTION_SECONDS = 13 * 30 * 24 * 60 * 60;
|
|
@@ -78,6 +79,20 @@ export function createPersonalizationCollectRoute(config) {
|
|
|
78
79
|
return validationResponse(error);
|
|
79
80
|
}
|
|
80
81
|
const cookies = parseCookieHeader(request.headers.get('cookie'));
|
|
82
|
+
// Mimic suppression (audience-mimic REQ-5): a validly signed, unexpired
|
|
83
|
+
// aci_mimic cookie marks a marketer's view-as session — accept uniformly,
|
|
84
|
+
// store nothing, and never rotate the visitor's real aci_claims. Placed
|
|
85
|
+
// after schema validation so the 400/202 split stays independent of
|
|
86
|
+
// session type; an invalid cookie is ignored (events process normally).
|
|
87
|
+
if (config.mimicSignKey) {
|
|
88
|
+
const mimicCookie = cookies.get('aci_mimic') || '';
|
|
89
|
+
if (mimicCookie) {
|
|
90
|
+
const mimic = await validateMimicToken(mimicCookie, config.mimicSignKey, config.siteId);
|
|
91
|
+
if (mimic) {
|
|
92
|
+
return accepted(new Headers({ 'cache-control': 'no-store', 'content-type': 'application/json' }));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
81
96
|
const signals = consentSignals(request, cookies);
|
|
82
97
|
const rawIdentity = await verifyIdentityCookie(cookies.get('aci_id') || '', config.identityKeys);
|
|
83
98
|
const now = (config.now ?? (() => new Date()))();
|
|
@@ -104,7 +104,7 @@ export function buildErrorSeatPayload(contentRoot, posture) {
|
|
|
104
104
|
if (!fragmentRef)
|
|
105
105
|
continue;
|
|
106
106
|
const fragment = readJSON(root, fragmentRef);
|
|
107
|
-
blocks.push({ id: fragment.id, component: fragment.component, props: fragment.props || {} });
|
|
107
|
+
blocks.push({ $type: 'component', id: fragment.id, component: fragment.component, props: fragment.props || {} });
|
|
108
108
|
}
|
|
109
109
|
if (blocks.length)
|
|
110
110
|
slots[slot.name] = blocks;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const VARIANT_NAMESPACE = '__variant';
|
|
2
|
+
export function parseHostedPageTarget(rawSegments) {
|
|
3
|
+
const segments = decodeSegments(rawSegments);
|
|
4
|
+
if (!segments)
|
|
5
|
+
return null;
|
|
6
|
+
const variantAt = segments.indexOf(VARIANT_NAMESPACE);
|
|
7
|
+
if (variantAt < 0)
|
|
8
|
+
return target('base', segments, segments);
|
|
9
|
+
if (variantAt !== segments.lastIndexOf(VARIANT_NAMESPACE) || variantAt !== segments.length - 2) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
...target('variant', segments.slice(0, variantAt), segments),
|
|
14
|
+
variantKey: segments[variantAt + 1],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function target(kind, routeSegments, targetSegments) {
|
|
18
|
+
return {
|
|
19
|
+
kind,
|
|
20
|
+
route: routeSegments.length === 0 ? '/' : `/${routeSegments.join('/')}/`,
|
|
21
|
+
pageRef: ['routes', ...targetSegments, 'index.json'].join('/'),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function decodeSegments(rawSegments) {
|
|
25
|
+
const segments = [];
|
|
26
|
+
for (const raw of rawSegments) {
|
|
27
|
+
let segment;
|
|
28
|
+
try {
|
|
29
|
+
segment = decodeURIComponent(raw);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!segment || segment === '.' || segment === '..' || segment.includes('/'))
|
|
35
|
+
return null;
|
|
36
|
+
segments.push(segment);
|
|
37
|
+
}
|
|
38
|
+
return segments;
|
|
39
|
+
}
|
package/dist/next/index.d.ts
CHANGED
|
@@ -5,7 +5,6 @@ export { createPage, type CreatePageOptions, type SlotPropsFn } from './page.js'
|
|
|
5
5
|
export { getSiteConfig, getFragment, getLayout, getPage, getRenderInput, routeFromNextParams } from './server.js';
|
|
6
6
|
export { PageNotFoundError, FragmentNotFoundError, LayoutNotFoundError } from './server.js';
|
|
7
7
|
export { CollectScript, type CollectScriptProps } from './collect-script.js';
|
|
8
|
-
export { aciMiddlewareMatcher } from './middleware.js';
|
|
9
8
|
export { AssignmentsScript, type AssignmentsScriptProps } from './assignments-script.js';
|
|
10
9
|
export { createNotFound, renderPageForRoute, resolveErrorPage, type NextResolvedErrorPage, } from './error-pages.js';
|
|
11
10
|
export { ErrorPageInstrumentation } from './error-page-instrumentation.js';
|
package/dist/next/index.js
CHANGED
|
@@ -5,7 +5,6 @@ export { createPage } from './page.js';
|
|
|
5
5
|
export { getSiteConfig, getFragment, getLayout, getPage, getRenderInput, routeFromNextParams } from './server.js';
|
|
6
6
|
export { PageNotFoundError, FragmentNotFoundError, LayoutNotFoundError } from './server.js';
|
|
7
7
|
export { CollectScript } from './collect-script.js';
|
|
8
|
-
export { aciMiddlewareMatcher } from './middleware.js';
|
|
9
8
|
export { AssignmentsScript } from './assignments-script.js';
|
|
10
9
|
export { createNotFound, renderPageForRoute, resolveErrorPage, } from './error-pages.js';
|
|
11
10
|
export { ErrorPageInstrumentation } from './error-page-instrumentation.js';
|
package/dist/next/page.d.ts
CHANGED
|
@@ -31,12 +31,10 @@ export interface CreatePageOptions {
|
|
|
31
31
|
* the collect script tag exactly once per page (design/04 contract tag).
|
|
32
32
|
*
|
|
33
33
|
* Release-scoped routes (production): the middleware rewrites a public URL
|
|
34
|
-
* to `/__r/{releaseId}/
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* The `routes` namespace segment leaves room for future siblings under
|
|
39
|
-
* the release prefix (e.g. `/__r/{releaseId}/assets/*`).
|
|
34
|
+
* to `/__r/{releaseId}/{targetRef}`. The target ref selects the exact immutable
|
|
35
|
+
* base or variant page while its prefix identifies the logical public route.
|
|
36
|
+
* createPage passes both identities to the content loader, giving each target
|
|
37
|
+
* a distinct CDN cache key without sending selection headers into Next.
|
|
40
38
|
*
|
|
41
39
|
* Dev pass-through (ACI_CONTENT_PROVIDER=file): the middleware passes
|
|
42
40
|
* requests through unrewritten, so `/` arrives with an empty/undefined
|
|
@@ -54,4 +52,29 @@ export declare function createPage(registry: Registry, options?: CreatePageOptio
|
|
|
54
52
|
generateMetadata: ({ params }: NextPageProps) => Promise<Metadata>;
|
|
55
53
|
default: ({ params }: NextPageProps) => Promise<import("react").JSX.Element>;
|
|
56
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* Resolves the logical route and releaseId from Next.js catch-all params.
|
|
57
|
+
*
|
|
58
|
+
* Two shapes are supported:
|
|
59
|
+
*
|
|
60
|
+
* 1. **Rewritten (production):** middleware rewrites a public URL to the exact
|
|
61
|
+
* selected target under `/__r/{releaseId}`. A root variant therefore has
|
|
62
|
+
* `['__r', releaseId, '__variant', variantKey]`. The full suffix selects the
|
|
63
|
+
* immutable page object; the segments before the reserved `__variant`
|
|
64
|
+
* namespace form the logical public route.
|
|
65
|
+
*
|
|
66
|
+
* 2. **Dev pass-through (ACI_CONTENT_PROVIDER=file):** middleware passes
|
|
67
|
+
* requests through unrewritten, so the slug is the raw public path
|
|
68
|
+
* segments (possibly empty/undefined for `/`). No releaseId is present;
|
|
69
|
+
* the file provider serves content from disk.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveRouteAndRelease(params: Promise<{
|
|
72
|
+
slug?: string[];
|
|
73
|
+
}> | {
|
|
74
|
+
slug?: string[];
|
|
75
|
+
}): Promise<{
|
|
76
|
+
route: string;
|
|
77
|
+
releaseId?: string;
|
|
78
|
+
pageRef?: string;
|
|
79
|
+
}>;
|
|
57
80
|
export {};
|
package/dist/next/page.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { notFound } from 'next/navigation.js';
|
|
3
|
+
import { headers } from 'next/headers.js';
|
|
3
4
|
import { Fragment } from 'react';
|
|
4
5
|
import { PageNotFoundError, getRenderInput, getLayout, getFragment, } from './server.js';
|
|
6
|
+
import { MIMIC_BANNER_HEADER, decodeMimicBanner } from '../mimic/core.js';
|
|
5
7
|
import { routeMetadataForContent, resolveSlots, } from '../content/index.js';
|
|
6
8
|
import { renderBlocks } from './render.js';
|
|
7
9
|
import { AssignmentsScript } from './assignments-script.js';
|
|
8
10
|
import { CollectScript } from './collect-script.js';
|
|
9
11
|
import { renderProofContentFor } from '../internal/render-proof.js';
|
|
12
|
+
import { parseHostedPageTarget } from './hosted-page-target.js';
|
|
10
13
|
/**
|
|
11
14
|
* Creates the catch-all route handler for a Next.js ACI site.
|
|
12
15
|
*
|
|
@@ -28,12 +31,10 @@ import { renderProofContentFor } from '../internal/render-proof.js';
|
|
|
28
31
|
* the collect script tag exactly once per page (design/04 contract tag).
|
|
29
32
|
*
|
|
30
33
|
* Release-scoped routes (production): the middleware rewrites a public URL
|
|
31
|
-
* to `/__r/{releaseId}/
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* The `routes` namespace segment leaves room for future siblings under
|
|
36
|
-
* the release prefix (e.g. `/__r/{releaseId}/assets/*`).
|
|
34
|
+
* to `/__r/{releaseId}/{targetRef}`. The target ref selects the exact immutable
|
|
35
|
+
* base or variant page while its prefix identifies the logical public route.
|
|
36
|
+
* createPage passes both identities to the content loader, giving each target
|
|
37
|
+
* a distinct CDN cache key without sending selection headers into Next.
|
|
37
38
|
*
|
|
38
39
|
* Dev pass-through (ACI_CONTENT_PROVIDER=file): the middleware passes
|
|
39
40
|
* requests through unrewritten, so `/` arrives with an empty/undefined
|
|
@@ -49,8 +50,9 @@ import { renderProofContentFor } from '../internal/render-proof.js';
|
|
|
49
50
|
export function createPage(registry, options) {
|
|
50
51
|
const dynamic = 'force-dynamic';
|
|
51
52
|
async function generateMetadata({ params }) {
|
|
52
|
-
const { route, releaseId } = await resolveRouteAndRelease(params);
|
|
53
|
-
const
|
|
53
|
+
const { route, releaseId, pageRef } = await resolveRouteAndRelease(params);
|
|
54
|
+
const fetchConfig = { releaseId, pageRef };
|
|
55
|
+
const input = await getRenderInput(route, fetchConfig);
|
|
54
56
|
const meta = routeMetadataForContent(input.siteConfig, input.page);
|
|
55
57
|
return {
|
|
56
58
|
title: meta.title,
|
|
@@ -64,8 +66,8 @@ export function createPage(registry, options) {
|
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
async function CatchAllPage({ params }) {
|
|
67
|
-
const { route, releaseId } = await resolveRouteAndRelease(params);
|
|
68
|
-
const fetchConfig = { releaseId };
|
|
69
|
+
const { route, releaseId, pageRef } = await resolveRouteAndRelease(params);
|
|
70
|
+
const fetchConfig = { releaseId, pageRef };
|
|
69
71
|
try {
|
|
70
72
|
const input = await getRenderInput(route, fetchConfig);
|
|
71
73
|
if (!input.page)
|
|
@@ -84,9 +86,9 @@ export function createPage(registry, options) {
|
|
|
84
86
|
}
|
|
85
87
|
return (_jsx(Fragment, { children: renderedBlocks }, slot.name));
|
|
86
88
|
}));
|
|
87
|
-
return (_jsxs(_Fragment, { children: [renderProofContent
|
|
89
|
+
return (_jsxs(_Fragment, { children: [renderedSlots.filter(Boolean), _jsx(AssignmentsScript, { page: input.page }), _jsx(CollectScript, { siteConfig: input.siteConfig }), _jsx(MimicBanner, { html: await readMimicBannerHTML() }), renderProofContent
|
|
88
90
|
? _jsx("meta", { name: "aci-render-proof", content: renderProofContent })
|
|
89
|
-
: null
|
|
91
|
+
: null] }));
|
|
90
92
|
}
|
|
91
93
|
catch (error) {
|
|
92
94
|
if (error instanceof PageNotFoundError)
|
|
@@ -105,19 +107,18 @@ export function createPage(registry, options) {
|
|
|
105
107
|
*
|
|
106
108
|
* Two shapes are supported:
|
|
107
109
|
*
|
|
108
|
-
* 1. **Rewritten (production):** middleware rewrites a public URL to
|
|
109
|
-
* `/__r/{releaseId}
|
|
110
|
-
* `['__r', releaseId, '
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* under the same release prefix).
|
|
110
|
+
* 1. **Rewritten (production):** middleware rewrites a public URL to the exact
|
|
111
|
+
* selected target under `/__r/{releaseId}`. A root variant therefore has
|
|
112
|
+
* `['__r', releaseId, '__variant', variantKey]`. The full suffix selects the
|
|
113
|
+
* immutable page object; the segments before the reserved `__variant`
|
|
114
|
+
* namespace form the logical public route.
|
|
114
115
|
*
|
|
115
116
|
* 2. **Dev pass-through (ACI_CONTENT_PROVIDER=file):** middleware passes
|
|
116
117
|
* requests through unrewritten, so the slug is the raw public path
|
|
117
118
|
* segments (possibly empty/undefined for `/`). No releaseId is present;
|
|
118
119
|
* the file provider serves content from disk.
|
|
119
120
|
*/
|
|
120
|
-
async function resolveRouteAndRelease(params) {
|
|
121
|
+
export async function resolveRouteAndRelease(params) {
|
|
121
122
|
const resolved = await params;
|
|
122
123
|
const slug = resolved.slug ?? [];
|
|
123
124
|
if (slug.length >= 2 && slug[0] === '__r') {
|
|
@@ -125,13 +126,14 @@ async function resolveRouteAndRelease(params) {
|
|
|
125
126
|
if (!releaseId || releaseId.includes('/') || releaseId === '.' || releaseId === '..') {
|
|
126
127
|
return { route: '/' };
|
|
127
128
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (rest.length === 0)
|
|
131
|
-
return { route: '/', releaseId };
|
|
132
|
-
if (rest.some((s) => s === '' || s === '.' || s === '..'))
|
|
129
|
+
const target = parseHostedPageTarget(slug.slice(2));
|
|
130
|
+
if (!target)
|
|
133
131
|
return { route: '/' };
|
|
134
|
-
return {
|
|
132
|
+
return {
|
|
133
|
+
route: target.route,
|
|
134
|
+
releaseId,
|
|
135
|
+
pageRef: target.pageRef,
|
|
136
|
+
};
|
|
135
137
|
}
|
|
136
138
|
const route = routeFromSlug(slug);
|
|
137
139
|
return { route };
|
|
@@ -141,3 +143,23 @@ function routeFromSlug(slug) {
|
|
|
141
143
|
return '/';
|
|
142
144
|
return `/${slug.join('/')}/`;
|
|
143
145
|
}
|
|
146
|
+
// The middleware emits the banner HTML in a reserved request header only
|
|
147
|
+
// after validating a signed mimic token; viewer-forged copies are stripped at
|
|
148
|
+
// the dispatch gate (specs/features/audience-mimic REQ-4). The header read
|
|
149
|
+
// happens here (the page already awaits request-scoped data) so the banner
|
|
150
|
+
// itself stays a synchronous leaf.
|
|
151
|
+
async function readMimicBannerHTML() {
|
|
152
|
+
try {
|
|
153
|
+
const current = await headers();
|
|
154
|
+
return decodeMimicBanner(current.get(MIMIC_BANNER_HEADER) || '');
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// headers() has no request context in static generation and unit tests.
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function MimicBanner({ html }) {
|
|
162
|
+
if (!html)
|
|
163
|
+
return null;
|
|
164
|
+
return _jsx("div", { dangerouslySetInnerHTML: { __html: html } });
|
|
165
|
+
}
|
|
@@ -88,6 +88,10 @@ export interface PersonalizationHostConfig {
|
|
|
88
88
|
secureCookies?: boolean;
|
|
89
89
|
now?: () => Date;
|
|
90
90
|
randomBytes?: (length: number) => Uint8Array;
|
|
91
|
+
mimicClaims?: {
|
|
92
|
+
aud?: string[];
|
|
93
|
+
seg?: string[];
|
|
94
|
+
};
|
|
91
95
|
}
|
|
92
96
|
export interface CookieMutation {
|
|
93
97
|
name: string;
|
|
@@ -23,6 +23,9 @@ export class IdentityErasureFencedError extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
export async function resolvePersonalizationRequest(request, releaseId, config) {
|
|
25
25
|
const requestHeaders = new Headers(request.headers);
|
|
26
|
+
// The mimic banner header is middleware-emitted only; a viewer-presented
|
|
27
|
+
// copy never reaches the render.
|
|
28
|
+
requestHeaders.delete('x-aci-mimic-banner');
|
|
26
29
|
const cookies = parseCookieHeader(request.headers.get('cookie'));
|
|
27
30
|
const consent = consentSignals(request, cookies);
|
|
28
31
|
const mutations = [];
|
|
@@ -35,10 +38,14 @@ export async function resolvePersonalizationRequest(request, releaseId, config)
|
|
|
35
38
|
// re-minting (REQ-4). aci_m is issue-only (D-11): a resolve-path refresh
|
|
36
39
|
// would put a Set-Cookie on every returning visitor's response, defeating
|
|
37
40
|
// the any-Set-Cookie => private, no-store cache rule.
|
|
38
|
-
|
|
41
|
+
//
|
|
42
|
+
// Mimic (audience-mimic): a validated mimicClaims input skips identity
|
|
43
|
+
// entirely — no read, no mint, no claims-cookie touch (REQ-5).
|
|
44
|
+
const mimic = config.mimicClaims;
|
|
45
|
+
let rawIdentity = mimic || consent.privacyDenied
|
|
39
46
|
? null
|
|
40
47
|
: await verifyIdentityCookie(cookies.get('aci_id') || '', config.identityKeys);
|
|
41
|
-
if (!consent.privacyDenied && !rawIdentity) {
|
|
48
|
+
if (!mimic && !consent.privacyDenied && !rawIdentity) {
|
|
42
49
|
rawIdentity = randomIdentity(config.randomBytes);
|
|
43
50
|
const signed = await signIdentity(rawIdentity, config.identityKeys.current);
|
|
44
51
|
const maxAge = config.identityCookieTTLSeconds ?? DEFAULT_IDENTITY_TTL_SECONDS;
|
|
@@ -48,7 +55,7 @@ export async function resolvePersonalizationRequest(request, releaseId, config)
|
|
|
48
55
|
// The identity hash feeds experiment bucketing for EVERY identified
|
|
49
56
|
// visitor. Claims are a signed cookie written by /collect; the EdgeRouter
|
|
50
57
|
// never reads the customer object store to refresh or revoke them.
|
|
51
|
-
let signedClaims = cookies.get('aci_claims') || '';
|
|
58
|
+
let signedClaims = mimic ? '' : cookies.get('aci_claims') || '';
|
|
52
59
|
let identityHash = '';
|
|
53
60
|
if (rawIdentity) {
|
|
54
61
|
identityHash = await identityHashHex('aci_id', rawIdentity);
|
|
@@ -58,7 +65,9 @@ export async function resolvePersonalizationRequest(request, releaseId, config)
|
|
|
58
65
|
signedClaims = '';
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
|
-
|
|
68
|
+
// Under mimic the visitor's real aci_claims is never touched (REQ-5) — the
|
|
69
|
+
// exit path returns them to exactly their pre-mimic state.
|
|
70
|
+
if (!mimic && (!consent.allowsIdentityTier || !rawIdentity || !signedClaims) && cookies.has('aci_claims')) {
|
|
62
71
|
mutations.push(expireCookie('aci_claims', secure));
|
|
63
72
|
}
|
|
64
73
|
const manifest = await config.store.readEdgeManifest(releaseId);
|
|
@@ -66,9 +75,11 @@ export async function resolvePersonalizationRequest(request, releaseId, config)
|
|
|
66
75
|
let contentPath = route?.dispatch.contentPath || '';
|
|
67
76
|
let variantKey = '';
|
|
68
77
|
if (route) {
|
|
69
|
-
const claims =
|
|
70
|
-
?
|
|
71
|
-
:
|
|
78
|
+
const claims = mimic
|
|
79
|
+
? { aud: mimic.aud, seg: mimic.seg, iat: 0, exp: 0 }
|
|
80
|
+
: consent.allowsIdentityTier
|
|
81
|
+
? await verifyClaims(signedClaims, config.claimsKeys || {}, now)
|
|
82
|
+
: null;
|
|
72
83
|
const decision = resolvePersonalizationDecision(toPersonalizationRequestContext(request), route, claims, manifest?.experiments, identityHash
|
|
73
84
|
? { usable: true, siteId: config.siteId, namespace: 'aci_id', identityHash }
|
|
74
85
|
: undefined);
|
package/dist/next/server.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { headers } from 'next/headers.js';
|
|
2
2
|
import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError, } from '../content/provider.js';
|
|
3
|
+
import { ObjectContentProvider } from '../providers/object.js';
|
|
3
4
|
import { normalizeRoute } from '../content/routes.js';
|
|
4
5
|
import { getCachedManifest, getCachedSiteConfig } from '../content/cache.js';
|
|
5
6
|
import { S3ContentProvider } from '../providers/s3.js';
|
|
7
|
+
import { VercelReleaseObjectReader } from '../providers/vercel-object-reader.js';
|
|
6
8
|
import { GRADIAL_RENDER_INPUT_HEADER, getPendingRenderInput } from '../runtime/page.js';
|
|
7
9
|
import { attachRenderProofReceipt, selectVerifiedPage, } from '../internal/render-proof.js';
|
|
8
10
|
import { getUncachedEdgeConfigValue } from './edge-config.js';
|
|
9
11
|
import { activeReleaseV1Key, clampDeploymentCompatibleReleaseV1, decodeActiveReleaseV1, decodeDeploymentCompatibilityV1, deploymentCompatibilityKey, } from './vercel-edge-projection-v1.js';
|
|
10
12
|
export { FragmentNotFoundError, PageNotFoundError, LayoutNotFoundError };
|
|
11
|
-
const releaseObjectReaderSymbol = Symbol.for('aci.
|
|
13
|
+
const releaseObjectReaderSymbol = Symbol.for('@gradial/aci.release-object-reader');
|
|
14
|
+
const objectProviders = new WeakMap();
|
|
15
|
+
let vercelReleaseObjectReader;
|
|
12
16
|
export function setReleaseObjectReader(reader) {
|
|
13
17
|
const scope = globalThis;
|
|
14
18
|
if (reader)
|
|
@@ -339,6 +343,29 @@ async function resolveS3Provider(config) {
|
|
|
339
343
|
}
|
|
340
344
|
async function resolveProviderWithFallback(config) {
|
|
341
345
|
const injected = getInjectedReader();
|
|
346
|
+
const pageRef = config.pageRef;
|
|
347
|
+
const hostedReaderConfigured = injected || config.roleArn || process.env.AWS_ROLE_ARN;
|
|
348
|
+
if (pageRef && hostedReaderConfigured) {
|
|
349
|
+
const releaseId = config.releaseId || process.env.ACI_RELEASE_ID || 'test';
|
|
350
|
+
let provider = objectProviders.get(config);
|
|
351
|
+
if (!provider) {
|
|
352
|
+
if (!injected && !vercelReleaseObjectReader) {
|
|
353
|
+
vercelReleaseObjectReader = new VercelReleaseObjectReader({
|
|
354
|
+
bucket: config.bucket,
|
|
355
|
+
keyPrefix: config.keyPrefix,
|
|
356
|
+
region: config.region,
|
|
357
|
+
roleArn: config.roleArn,
|
|
358
|
+
endpoint: config.endpoint,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const reader = injected || vercelReleaseObjectReader;
|
|
362
|
+
if (!reader)
|
|
363
|
+
throw new Error('ACI hosted page reader is unavailable');
|
|
364
|
+
provider = new ObjectContentProvider({ releaseId, pageRef, reader });
|
|
365
|
+
objectProviders.set(config, provider);
|
|
366
|
+
}
|
|
367
|
+
return provider;
|
|
368
|
+
}
|
|
342
369
|
if (injected) {
|
|
343
370
|
const releaseId = config.releaseId || process.env.ACI_RELEASE_ID || 'test';
|
|
344
371
|
return new InjectedReaderProvider(releaseId, injected);
|
|
@@ -411,7 +411,7 @@ function validateExperiment(value) {
|
|
|
411
411
|
'allocationVersion',
|
|
412
412
|
'salt',
|
|
413
413
|
'arms',
|
|
414
|
-
], [], 'v1 edge experiment');
|
|
414
|
+
], ['eligibilityAudienceId'], 'v1 edge experiment');
|
|
415
415
|
if (!Array.isArray(record.arms)) {
|
|
416
416
|
throw new Error('v1 edge experiment arms must be an array');
|
|
417
417
|
}
|
|
@@ -441,6 +441,11 @@ function validateExperiment(value) {
|
|
|
441
441
|
priority: requiredSafeInteger(record.priority, 'v1 edge experiment priority'),
|
|
442
442
|
allocationVersion: optionalString(record.allocationVersion, 'v1 edge experiment allocation version'),
|
|
443
443
|
salt: optionalString(record.salt, 'v1 edge experiment assignment salt'),
|
|
444
|
+
...(Object.prototype.hasOwnProperty.call(record, 'eligibilityAudienceId')
|
|
445
|
+
? {
|
|
446
|
+
eligibilityAudienceId: optionalString(record.eligibilityAudienceId, 'v1 edge experiment eligibility audience ID'),
|
|
447
|
+
}
|
|
448
|
+
: {}),
|
|
444
449
|
arms,
|
|
445
450
|
};
|
|
446
451
|
}
|
|
@@ -529,6 +534,7 @@ function canonicalExperimentNode(value) {
|
|
|
529
534
|
['priority', canonicalUnknownNode(record.priority)],
|
|
530
535
|
['allocationVersion', canonicalUnknownNode(record.allocationVersion)],
|
|
531
536
|
['salt', canonicalUnknownNode(record.salt)],
|
|
537
|
+
...optionalCanonicalEntry(record, 'eligibilityAudienceId', canonicalUnknownNode),
|
|
532
538
|
[
|
|
533
539
|
'arms',
|
|
534
540
|
record.arms.map((armValue) => {
|
|
@@ -493,6 +493,7 @@ export function createVercelCollectionFromEnv(env = process.env) {
|
|
|
493
493
|
...(previousKID ? { [previousKID]: previousSecret } : {}),
|
|
494
494
|
},
|
|
495
495
|
secureCookies: env.AUDIENCE_IDENTITY_COOKIE_SECURE !== 'false',
|
|
496
|
+
mimicSignKey: env.GRADIAL_PREVIEW_SIGN_KEY || env.ACI_PREVIEW_SIGN_KEY || undefined,
|
|
496
497
|
};
|
|
497
498
|
}
|
|
498
499
|
function requiredSegment(value, label) {
|
package/dist/providers/s3.js
CHANGED
|
@@ -78,12 +78,13 @@ export class S3ContentProvider {
|
|
|
78
78
|
}
|
|
79
79
|
async getLayout(name) {
|
|
80
80
|
const manifest = await this.manifest();
|
|
81
|
-
const
|
|
82
|
-
if (!
|
|
81
|
+
const entry = manifest.layouts?.[name];
|
|
82
|
+
if (!entry) {
|
|
83
83
|
throw new LayoutNotFoundError(name);
|
|
84
84
|
}
|
|
85
|
+
const ref = entry.payloadRef;
|
|
85
86
|
try {
|
|
86
|
-
return await this.getJSON(
|
|
87
|
+
return await this.getJSON(ref);
|
|
87
88
|
}
|
|
88
89
|
catch (error) {
|
|
89
90
|
throw new LayoutNotFoundError(name, error);
|
|
@@ -199,7 +200,13 @@ export class S3ContentProvider {
|
|
|
199
200
|
}
|
|
200
201
|
let page;
|
|
201
202
|
try {
|
|
202
|
-
|
|
203
|
+
const parsed = JSON.parse(source);
|
|
204
|
+
if (parsed?.schemaVersion === 'gradial.ssr-page.v1' && parsed.page !== undefined) {
|
|
205
|
+
page = parsed.page;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
page = parsed;
|
|
209
|
+
}
|
|
203
210
|
}
|
|
204
211
|
catch (cause) {
|
|
205
212
|
throw pageIntegrityError(`invalid JSON for ${tuple.contentRef}`, cause);
|
|
@@ -5,6 +5,7 @@ export interface BlockRefMeta {
|
|
|
5
5
|
multiple: boolean;
|
|
6
6
|
}
|
|
7
7
|
declare const blockRefBase: z.ZodObject<{
|
|
8
|
+
$type: z.ZodLiteral<"component">;
|
|
8
9
|
id: z.ZodString;
|
|
9
10
|
component: z.ZodString;
|
|
10
11
|
props: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
@@ -12,6 +13,7 @@ declare const blockRefBase: z.ZodObject<{
|
|
|
12
13
|
type InferContractProps<T> = T extends ComponentContract<string, infer Schema> ? Schema extends z.ZodType<infer P> ? P : Record<string, unknown> : Record<string, unknown>;
|
|
13
14
|
type InferContractId<T> = T extends ComponentContract<infer Id, unknown> ? Id : never;
|
|
14
15
|
export type BlockRefEntry<T extends ComponentContract> = {
|
|
16
|
+
$type: 'component';
|
|
15
17
|
id: string;
|
|
16
18
|
component: InferContractId<T>;
|
|
17
19
|
props: InferContractProps<T>;
|
package/dist/types/block-ref.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradial/aci",
|
|
3
|
-
"version": "0.1.52-rc.
|
|
3
|
+
"version": "0.1.52-rc.13",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/gradial/bare-metal-go.git",
|
|
@@ -146,11 +146,11 @@
|
|
|
146
146
|
"zod": "^4.0.0"
|
|
147
147
|
},
|
|
148
148
|
"optionalDependencies": {
|
|
149
|
-
"@gradial/aci-cli-darwin-arm64": "0.1.52-rc.
|
|
150
|
-
"@gradial/aci-cli-darwin-x64": "0.1.52-rc.
|
|
151
|
-
"@gradial/aci-cli-linux-arm64": "0.1.52-rc.
|
|
152
|
-
"@gradial/aci-cli-linux-x64": "0.1.52-rc.
|
|
153
|
-
"@gradial/aci-cli-win32-arm64": "0.1.52-rc.
|
|
154
|
-
"@gradial/aci-cli-win32-x64": "0.1.52-rc.
|
|
149
|
+
"@gradial/aci-cli-darwin-arm64": "0.1.52-rc.13",
|
|
150
|
+
"@gradial/aci-cli-darwin-x64": "0.1.52-rc.13",
|
|
151
|
+
"@gradial/aci-cli-linux-arm64": "0.1.52-rc.13",
|
|
152
|
+
"@gradial/aci-cli-linux-x64": "0.1.52-rc.13",
|
|
153
|
+
"@gradial/aci-cli-win32-arm64": "0.1.52-rc.13",
|
|
154
|
+
"@gradial/aci-cli-win32-x64": "0.1.52-rc.13"
|
|
155
155
|
}
|
|
156
156
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
2
3
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
4
|
-
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
6
|
import { z } from 'zod';
|
|
6
7
|
import { ImageSchema } from '../types/image.ts';
|
|
7
8
|
import { VideoSchema } from '../types/video.ts';
|
|
@@ -19,14 +20,15 @@ try {
|
|
|
19
20
|
if (!globalThis.React) globalThis.React = React;
|
|
20
21
|
} catch { /* react not available — component files without JSX will still work */ }
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} catch (error) {
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
if (isMainModule()) {
|
|
24
|
+
const args = parseArgs(process.argv.slice(2));
|
|
25
|
+
try {
|
|
26
|
+
const result = await compileRegistry(args);
|
|
27
|
+
console.log(JSON.stringify({ success: true, ...result, errors: [] }, null, 2));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(JSON.stringify({ success: false, errors: [String(error?.message ?? error)] }, null, 2));
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
}
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
async function compileRegistry(options) {
|
|
@@ -430,9 +432,10 @@ async function generatePageSchema(schemaDir, componentEntries, outDir) {
|
|
|
430
432
|
const defs = {
|
|
431
433
|
baseBlock: {
|
|
432
434
|
type: 'object',
|
|
433
|
-
required: ['id', 'component', 'props'],
|
|
435
|
+
required: ['$type', 'id', 'component', 'props'],
|
|
434
436
|
additionalProperties: true,
|
|
435
437
|
properties: {
|
|
438
|
+
$type: { const: 'component' },
|
|
436
439
|
id: { type: 'string', minLength: 1 },
|
|
437
440
|
component: { type: 'string' },
|
|
438
441
|
props: { type: 'object' },
|
|
@@ -741,3 +744,14 @@ function parseArgs(argv) {
|
|
|
741
744
|
}
|
|
742
745
|
return out;
|
|
743
746
|
}
|
|
747
|
+
|
|
748
|
+
function isMainModule() {
|
|
749
|
+
if (!process.argv[1]) return false;
|
|
750
|
+
try {
|
|
751
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(path.resolve(process.argv[1]));
|
|
752
|
+
} catch {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export { compileRegistry, generatePageSchema };
|