@gradial/aci 0.1.41 → 0.1.42

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 (102) hide show
  1. package/CHANGELOG.md +9 -13
  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/cli/compile-registry.d.mts +2 -0
  8. package/dist/cli/compile-registry.mjs +759 -0
  9. package/dist/cli/materialize-template.d.mts +19 -0
  10. package/dist/cli/materialize-template.mjs +128 -0
  11. package/dist/cli/validate-content.d.mts +2 -0
  12. package/dist/cli/validate-content.mjs +426 -0
  13. package/dist/cli/verify-next-cdn-routes.d.mts +2 -0
  14. package/dist/cli/verify-next-cdn-routes.mjs +47 -0
  15. package/dist/collect/assignments.d.ts +65 -0
  16. package/dist/collect/assignments.js +138 -0
  17. package/dist/collect/batcher.d.ts +21 -0
  18. package/dist/collect/batcher.js +10 -0
  19. package/dist/collect/declarative.d.ts +1 -1
  20. package/dist/collect/declarative.js +7 -1
  21. package/dist/collect/index.js +30 -4
  22. package/dist/collect/pageviews.d.ts +15 -3
  23. package/dist/collect/pageviews.js +28 -13
  24. package/dist/collect/transport.d.ts +2 -0
  25. package/dist/collect/transport.js +32 -0
  26. package/dist/consent-banner/index.d.ts +33 -0
  27. package/dist/consent-banner/index.js +99 -0
  28. package/dist/content/personalization.d.ts +35 -0
  29. package/dist/content/personalization.js +57 -0
  30. package/dist/content/types.d.ts +7 -1
  31. package/dist/edge/personalization-core.d.ts +11 -0
  32. package/dist/edge/personalization-core.js +265 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +1 -0
  35. package/dist/next/assignments-script.d.ts +14 -0
  36. package/dist/next/assignments-script.js +17 -0
  37. package/dist/next/collect-route.d.ts +19 -0
  38. package/dist/next/collect-route.js +351 -0
  39. package/dist/next/index.d.ts +1 -0
  40. package/dist/next/index.js +1 -0
  41. package/dist/next/middleware.d.ts +15 -1
  42. package/dist/next/middleware.js +71 -27
  43. package/dist/next/page.js +2 -1
  44. package/dist/next/personalization-host.d.ts +118 -0
  45. package/dist/next/personalization-host.js +276 -0
  46. package/dist/next/server.js +6 -6
  47. package/dist/next/vercel-personalization.d.ts +3 -0
  48. package/dist/next/vercel-personalization.js +3 -0
  49. package/dist/next/vercel-s3-personalization.d.ts +58 -0
  50. package/dist/next/vercel-s3-personalization.js +220 -0
  51. package/dist/providers/s3.d.ts +1 -1
  52. package/dist/providers/s3.js +1 -1
  53. package/dist/react/VideoPlayer.d.ts +6 -6
  54. package/dist/react/index.d.ts +6 -6
  55. package/dist/types/page.d.ts +1 -1
  56. package/package.json +19 -1
  57. package/src/astro/ConsentBanner.astro +62 -0
  58. package/src/cli/verify-next-cdn-routes.mjs +5 -4
  59. package/src/consent-banner/styles.css +110 -0
  60. package/src/types/page.ts +1 -1
  61. package/templates/astro/starter-content/src/cms/contracts/components/index.ts +2 -0
  62. package/templates/astro/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
  63. package/templates/astro/starter-content/src/cms/registry.ts +3 -0
  64. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +41 -0
  65. package/templates/astro/starter-content/src/render/RenderPage.astro +3 -2
  66. package/templates/astro/template/src/layouts/SiteLayout.astro +3 -1
  67. package/templates/astro/template/src/pages/[...slug].astro +1 -0
  68. package/templates/common/.content/audiences/preference-blue/_index.json +11 -0
  69. package/templates/common/.content/audiences/preference-red/_index.json +11 -0
  70. package/templates/common/.content/overlays/audience/preference-blue/pages/home/_index.json +13 -0
  71. package/templates/common/.content/overlays/audience/preference-red/pages/home/_index.json +13 -0
  72. package/templates/common/.content/pages/home/_index.json +114 -4
  73. package/templates/common/.content/segments/preference-blue/_index.json +20 -0
  74. package/templates/common/.content/segments/preference-red/_index.json +20 -0
  75. package/templates/common/contracts/components/index.ts +21 -1
  76. package/templates/common/contracts/components/personalizationChoice.contract.ts +17 -0
  77. package/templates/nextjs/starter-content/src/cms/contracts/components/index.ts +3 -1
  78. package/templates/nextjs/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
  79. package/templates/nextjs/starter-content/src/cms/registry.ts +3 -0
  80. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +53 -0
  81. package/templates/nextjs/starter-content/src/middleware.ts +11 -0
  82. package/templates/nextjs/template/src/app/collect/route.ts +18 -0
  83. package/templates/nextjs/template/src/middleware.ts +6 -2
  84. package/templates/astro/template/.aci.yaml +0 -28
  85. package/templates/common/.content/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
  86. package/templates/common/.content/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
  87. package/templates/common/.content/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
  88. package/templates/common/.content/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
  89. package/templates/common/.content/config/robots.txt +0 -2
  90. package/templates/common/.content/config/site.json +0 -197
  91. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/asset.json +0 -9
  92. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/editorial.json +0 -14
  93. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/governance.json +0 -23
  94. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/versions/v0001.json +0 -37
  95. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/asset.json +0 -9
  96. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/editorial.json +0 -14
  97. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/governance.json +0 -23
  98. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/versions/v0001.json +0 -37
  99. package/templates/common/.content/fragments/navbar/_index.json +0 -25
  100. package/templates/common/.content/fragments/site-footer/_index.json +0 -62
  101. package/templates/common/.content/pages/about/_index.json +0 -51
  102. package/templates/nextjs/template/.aci.yaml +0 -26
@@ -55,3 +55,38 @@ export declare function collectTagAttributes(siteConfig: PersonalizationSiteConf
55
55
  * when personalization is disabled.
56
56
  */
57
57
  export declare function collectScriptTagHTML(siteConfig: PersonalizationSiteConfig | null | undefined): string;
58
+ /** Version of the data-aci-assignments tag body. */
59
+ export declare const ASSIGNMENTS_TAG_VERSION = 1;
60
+ /**
61
+ * One experiment assignment carried by a variant payload's `experiments`
62
+ * block (wire field names match the SDK event contract).
63
+ */
64
+ export interface ExperimentAssignmentContext {
65
+ experimentId: string;
66
+ armId: string;
67
+ allocationVersion: string;
68
+ }
69
+ /**
70
+ * The minimal page-payload surface assignments tag emission reads: the
71
+ * top-level `experiments` block stamped at build time onto experiment
72
+ * variant payloads. `KernelPage` satisfies this structurally.
73
+ */
74
+ export interface AssignmentsPayloadSource {
75
+ experiments?: ExperimentAssignmentContext[] | null;
76
+ }
77
+ /**
78
+ * assignmentsScriptJSON derives the HTML-safe JSON body of the
79
+ * data-aci-assignments tag ({"v":1,"experiments":[…]}) from the page
80
+ * payload, or "" when the payload carries no experiment assignments.
81
+ * `<` / `>` / `&` are \u-escaped so the JSON can never break out of the
82
+ * script element (standard JSON-in-script escaping; application/json tags
83
+ * are data, never executed).
84
+ */
85
+ export declare function assignmentsScriptJSON(page: AssignmentsPayloadSource | null | undefined): string;
86
+ /**
87
+ * assignmentsScriptTagHTML renders the contract tag
88
+ * (`<script type="application/json" data-aci-assignments>…</script>`,
89
+ * design/04) as an HTML snippet for emitters without a React element seam
90
+ * (the Astro slug page). Returns "" when the payload carries no assignments.
91
+ */
92
+ export declare function assignmentsScriptTagHTML(page: AssignmentsPayloadSource | null | undefined): string;
@@ -52,3 +52,60 @@ function escapeAttribute(value) {
52
52
  .replace(/</g, "&lt;")
53
53
  .replace(/>/g, "&gt;");
54
54
  }
55
+ // ---------------------------------------------------------------------------
56
+ // Assignments tag (personalization-reporting design/04, D-PR-6): experiment
57
+ // variant payloads carry a build-time `experiments` block; the starters
58
+ // render it as ONE page-level JSON script tag the collect SDK reads for
59
+ // exposure emission and context stamping. Base payloads carry no block and
60
+ // emit no tag. Like the collect tag, both framework emitters derive the tag
61
+ // through this module so there is exactly one derivation code path.
62
+ // ---------------------------------------------------------------------------
63
+ /** Version of the data-aci-assignments tag body. */
64
+ export const ASSIGNMENTS_TAG_VERSION = 1;
65
+ function isAssignment(value) {
66
+ if (typeof value !== "object" || value === null)
67
+ return false;
68
+ const record = value;
69
+ return (typeof record.experimentId === "string" &&
70
+ record.experimentId !== "" &&
71
+ typeof record.armId === "string" &&
72
+ record.armId !== "" &&
73
+ typeof record.allocationVersion === "string");
74
+ }
75
+ /**
76
+ * assignmentsScriptJSON derives the HTML-safe JSON body of the
77
+ * data-aci-assignments tag ({"v":1,"experiments":[…]}) from the page
78
+ * payload, or "" when the payload carries no experiment assignments.
79
+ * `<` / `>` / `&` are \u-escaped so the JSON can never break out of the
80
+ * script element (standard JSON-in-script escaping; application/json tags
81
+ * are data, never executed).
82
+ */
83
+ export function assignmentsScriptJSON(page) {
84
+ const experiments = (page?.experiments ?? []).filter(isAssignment);
85
+ if (experiments.length === 0)
86
+ return "";
87
+ const body = JSON.stringify({
88
+ v: ASSIGNMENTS_TAG_VERSION,
89
+ experiments: experiments.map(({ experimentId, armId, allocationVersion }) => ({
90
+ experimentId,
91
+ armId,
92
+ allocationVersion,
93
+ })),
94
+ });
95
+ return body
96
+ .replace(/</g, "\\u003c")
97
+ .replace(/>/g, "\\u003e")
98
+ .replace(/&/g, "\\u0026");
99
+ }
100
+ /**
101
+ * assignmentsScriptTagHTML renders the contract tag
102
+ * (`<script type="application/json" data-aci-assignments>…</script>`,
103
+ * design/04) as an HTML snippet for emitters without a React element seam
104
+ * (the Astro slug page). Returns "" when the payload carries no assignments.
105
+ */
106
+ export function assignmentsScriptTagHTML(page) {
107
+ const json = assignmentsScriptJSON(page);
108
+ if (!json)
109
+ return "";
110
+ return `<script type="application/json" data-aci-assignments>${json}</script>`;
111
+ }
@@ -1,4 +1,4 @@
1
- import type { SitePersonalizationConfig } from './personalization.js';
1
+ import type { ExperimentAssignmentContext, SitePersonalizationConfig } from './personalization.js';
2
2
  export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'outline' | 'ghost';
3
3
  export interface KernelImage {
4
4
  src: string;
@@ -58,6 +58,12 @@ export interface KernelPage {
58
58
  layout: string;
59
59
  metadata: KernelPageSeo;
60
60
  regions: Record<string, KernelPageBlock[]>;
61
+ /**
62
+ * Build-time experiment assignment context (personalization-reporting
63
+ * design/04); present only on experiment variant payloads. The starters
64
+ * render it as the data-aci-assignments tag.
65
+ */
66
+ experiments?: ExperimentAssignmentContext[];
61
67
  }
62
68
  export interface KernelSiteConfig {
63
69
  id: string;
@@ -0,0 +1,11 @@
1
+ export function selectPersonalizationVariant(request: any, route: any, claims: any, experiments: any, identity: any): any;
2
+ export function resolvePersonalizationDecision(request: any, route: any, claims: any, experiments: any, identity: any): {
3
+ variant: any;
4
+ experiment: {
5
+ experimentId: any;
6
+ armId: any;
7
+ allocationVersion: string;
8
+ variantKey: any;
9
+ bucket: number;
10
+ } | null;
11
+ };
@@ -0,0 +1,265 @@
1
+ // Runtime-neutral personalization selection. Vercel imports this module and
2
+ // the CloudFront Terraform wrapper inlines the same source into its function.
3
+ // Keep this file free of platform APIs and dependencies.
4
+ export function selectPersonalizationVariant(request, route, claims, experiments, identity) {
5
+ return resolvePersonalizationDecision(request, route, claims, experiments, identity).variant;
6
+ }
7
+ export function resolvePersonalizationDecision(request, route, claims, experiments, identity) {
8
+ if (!route)
9
+ return { variant: null, experiment: null };
10
+ let variant = selectRuleVariant(request, route, claims);
11
+ let experiment = null;
12
+ if (identity && identity.usable && route.variants && experiments) {
13
+ const candidates = Object.values(experiments)
14
+ .filter((item) => (item.kind === 'ab' || item.kind === 'abn') && item.status === 'running')
15
+ .filter((item) => routeHasExperiment(route, item.id))
16
+ .sort((left, right) => (right.priority || 0) - (left.priority || 0)
17
+ || String(left.id || '').localeCompare(String(right.id || '')));
18
+ for (let index = 0; index < candidates.length; index++) {
19
+ const item = candidates[index];
20
+ const assignment = assignExperiment(item, identity);
21
+ const candidate = assignment && route.variants[assignment.variantKey];
22
+ if (!candidate)
23
+ continue;
24
+ if (!variant || betterVariant(candidate.priority || 0, candidate.variantKey, variant.priority || 0, variant.variantKey)) {
25
+ variant = candidate;
26
+ experiment = assignment;
27
+ }
28
+ break;
29
+ }
30
+ }
31
+ return { variant: variant || null, experiment };
32
+ }
33
+ function selectRuleVariant(request, route, claims) {
34
+ if (!route.variants)
35
+ return null;
36
+ const matches = [];
37
+ for (const key in route.variants) {
38
+ const variant = route.variants[key];
39
+ const values = variant.variantValues || variantValuesFromKey(key);
40
+ if (!requestDimensionsMatch(request, values))
41
+ continue;
42
+ const membershipMatch = (values.audience && claims && (claims.aud || []).includes(values.audience))
43
+ || (values.segment && claims && (claims.seg || []).includes(values.segment));
44
+ const ruleMatch = variant.rule ? evaluateRule(request, variant.rule) : false;
45
+ if (membershipMatch || ruleMatch)
46
+ matches.push(variant);
47
+ }
48
+ matches.sort((left, right) => (right.priority || 0) - (left.priority || 0)
49
+ || String(left.variantKey || '').localeCompare(String(right.variantKey || '')));
50
+ return matches[0] || null;
51
+ }
52
+ // CloudFront KVS values are capped at 1 KiB. The variant map key is the
53
+ // canonical encoding of its dimensions, so compact route records omit the
54
+ // duplicated variantValues object and recover it at the edge.
55
+ function variantValuesFromKey(key) {
56
+ const values = {};
57
+ const parts = String(key || '').replace(/&/g, ';').split(';');
58
+ for (let index = 0; index < parts.length; index++) {
59
+ const separator = parts[index].indexOf('=');
60
+ if (separator > 0)
61
+ values[parts[index].slice(0, separator)] = parts[index].slice(separator + 1);
62
+ }
63
+ return values;
64
+ }
65
+ function routeHasExperiment(route, experimentID) {
66
+ for (const key in route.variants || {}) {
67
+ const values = route.variants[key].variantValues || variantValuesFromKey(key);
68
+ if (values.experiment === experimentID)
69
+ return true;
70
+ }
71
+ return false;
72
+ }
73
+ function assignExperiment(experiment, identity) {
74
+ const version = String(experiment.allocationVersion || '').trim();
75
+ const salt = String(experiment.salt || '').trim();
76
+ const siteID = String(identity.siteId || '').trim();
77
+ const identityKey = `${String(identity.namespace || '').trim()}:${String(identity.identityHash || '').trim()}`;
78
+ if (!version || !salt || !siteID || identityKey === ':')
79
+ return null;
80
+ const input = [siteID, String(experiment.id || ''), version, identityKey, salt].join('\0');
81
+ const bucket = sha256Bucket(input);
82
+ let cumulative = 0;
83
+ const arms = experiment.arms || [];
84
+ for (let index = 0; index < arms.length; index++) {
85
+ const arm = arms[index];
86
+ if ((arm.allocationBasisPoints || 0) <= 0)
87
+ continue;
88
+ cumulative += arm.allocationBasisPoints;
89
+ if (bucket < cumulative) {
90
+ return {
91
+ experimentId: experiment.id,
92
+ armId: arm.id,
93
+ allocationVersion: version,
94
+ variantKey: arm.variantKey,
95
+ bucket,
96
+ };
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+ function betterVariant(candidatePriority, candidateKey, bestPriority, bestKey) {
102
+ if (candidatePriority !== bestPriority)
103
+ return candidatePriority > bestPriority;
104
+ return String(candidateKey || '').localeCompare(String(bestKey || '')) < 0;
105
+ }
106
+ function requestDimensionsMatch(request, values) {
107
+ for (const rawKey in values) {
108
+ const rawWant = values[rawKey];
109
+ const key = String(rawKey).trim();
110
+ const normalized = key.toLowerCase();
111
+ const want = String(rawWant).trim();
112
+ if (!key || !want || ['audience', 'segment', 'experiment', 'experimentarm'].includes(normalized))
113
+ continue;
114
+ const candidates = (request.query[key] || []).concat(request.query['aci_' + key] || [], request.headers['x-aci-' + normalized] || '', request.headers['x-gradial-' + normalized] || '', request.cookies[key] || '', request.cookies['aci_' + key] || '');
115
+ if (normalized === 'locale') {
116
+ const locales = String(request.headers['accept-language'] || '')
117
+ .split(',').map((part) => part.split(';')[0].trim()).filter(Boolean);
118
+ for (let localeIndex = 0; localeIndex < locales.length; localeIndex++)
119
+ candidates.push(locales[localeIndex]);
120
+ }
121
+ if (!candidates.some((candidate) => normalized === 'locale'
122
+ ? String(candidate).toLowerCase() === want.toLowerCase()
123
+ : String(candidate) === want))
124
+ return false;
125
+ }
126
+ return true;
127
+ }
128
+ function evaluateRule(request, rule) {
129
+ const all = rule.all || [];
130
+ const any = rule.any || [];
131
+ if (all.length + any.length === 0)
132
+ return false;
133
+ return all.every((condition) => evaluateCondition(request, condition))
134
+ && (any.length === 0 || any.some((condition) => evaluateCondition(request, condition)));
135
+ }
136
+ function evaluateCondition(request, condition) {
137
+ const values = signalValues(request, condition.field);
138
+ if (condition.operator === 'exists')
139
+ return values.length > 0;
140
+ if (values.length === 0 || !['string', 'number', 'boolean'].includes(typeof condition.value))
141
+ return false;
142
+ const expected = String(condition.value);
143
+ return values.some((value) => {
144
+ switch (condition.operator) {
145
+ case 'equals': return value === expected;
146
+ case 'contains': return value.includes(expected);
147
+ case 'startsWith': return value.startsWith(expected);
148
+ default: return false;
149
+ }
150
+ });
151
+ }
152
+ function signalValues(request, field) {
153
+ if (field.startsWith('query.'))
154
+ return request.query[field.slice(6)] || [];
155
+ if (field.startsWith('header.'))
156
+ return request.headers[field.slice(7).toLowerCase()] ? [request.headers[field.slice(7).toLowerCase()]] : [];
157
+ if (field.startsWith('cookie.'))
158
+ return request.cookies[field.slice(7)] ? [request.cookies[field.slice(7)]] : [];
159
+ if (field.startsWith('utm.'))
160
+ return request.query['utm_' + field.slice(4)] || [];
161
+ if (field.startsWith('geo.')) {
162
+ const geo = request.signals && request.signals.geo;
163
+ const value = geo && geo[field.slice(4).toLowerCase()];
164
+ return value ? [value] : [];
165
+ }
166
+ if (field === 'referrer') {
167
+ const value = request.headers.referer || request.headers.referrer;
168
+ return value ? [value] : [];
169
+ }
170
+ return [];
171
+ }
172
+ // SHA-256 mirrors experiment.assignment.go: the first eight digest bytes are
173
+ // interpreted big-endian and reduced modulo 10,000 basis points.
174
+ // CLOUDFRONT_STRIP_START — the build script removes this block for CloudFront
175
+ // Functions (experiment bucketing moves to the hosting Lambda to stay under
176
+ // the 10 KiB function size limit).
177
+ function sha256Bucket(value) {
178
+ const bytes = utf8(value);
179
+ const words = new Uint32Array(64);
180
+ const bitLength = bytes.length * 8;
181
+ const paddedLength = ((bytes.length + 9 + 63) >> 6) << 6;
182
+ const data = new Uint8Array(paddedLength);
183
+ data.set(bytes);
184
+ data[bytes.length] = 128;
185
+ // The message length is a 64-bit big-endian integer across the final
186
+ // eight bytes. The previous code wrote a single truncated byte into each
187
+ // word's most significant position, corrupting every digest (caught by
188
+ // the assignment parity vectors, spec experiment-variant-caching REQ-9).
189
+ const bitsHigh = Math.floor(bitLength / 0x100000000);
190
+ const bitsLow = bitLength >>> 0;
191
+ data[paddedLength - 8] = (bitsHigh >>> 24) & 255;
192
+ data[paddedLength - 7] = (bitsHigh >>> 16) & 255;
193
+ data[paddedLength - 6] = (bitsHigh >>> 8) & 255;
194
+ data[paddedLength - 5] = bitsHigh & 255;
195
+ data[paddedLength - 4] = (bitsLow >>> 24) & 255;
196
+ data[paddedLength - 3] = (bitsLow >>> 16) & 255;
197
+ data[paddedLength - 2] = (bitsLow >>> 8) & 255;
198
+ data[paddedLength - 1] = bitsLow & 255;
199
+ let a = 0x6a09e667, b = 0xbb67ae85, c = 0x3c6ef372, d = 0xa54ff53a;
200
+ let e = 0x510e527f, f = 0x9b05688c, g = 0x1f83d9ab, h = 0x5be0cd19;
201
+ const k = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2];
202
+ for (let offset = 0; offset < paddedLength; offset += 64) {
203
+ for (let i = 0; i < 16; i++) {
204
+ const p = offset + i * 4;
205
+ words[i] = (data[p] << 24) | (data[p + 1] << 16) | (data[p + 2] << 8) | data[p + 3];
206
+ }
207
+ for (let i = 16; i < 64; i++) {
208
+ const x = words[i - 15], y = words[i - 2];
209
+ words[i] = (words[i - 16] + (rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3)) + words[i - 7] + (rotr(y, 17) ^ rotr(y, 19) ^ (y >>> 10))) >>> 0;
210
+ }
211
+ let aa = a, bb = b, cc = c, dd = d, ee = e, ff = f, gg = g, hh = h;
212
+ for (let i = 0; i < 64; i++) {
213
+ const s1 = rotr(ee, 6) ^ rotr(ee, 11) ^ rotr(ee, 25);
214
+ const ch = (ee & ff) ^ (~ee & gg);
215
+ const t1 = (hh + s1 + ch + k[i] + words[i]) >>> 0;
216
+ const s0 = rotr(aa, 2) ^ rotr(aa, 13) ^ rotr(aa, 22);
217
+ const maj = (aa & bb) ^ (aa & cc) ^ (bb & cc);
218
+ const t2 = (s0 + maj) >>> 0;
219
+ hh = gg;
220
+ gg = ff;
221
+ ff = ee;
222
+ ee = (dd + t1) >>> 0;
223
+ dd = cc;
224
+ cc = bb;
225
+ bb = aa;
226
+ aa = (t1 + t2) >>> 0;
227
+ }
228
+ a = (a + aa) >>> 0;
229
+ b = (b + bb) >>> 0;
230
+ c = (c + cc) >>> 0;
231
+ d = (d + dd) >>> 0;
232
+ e = (e + ee) >>> 0;
233
+ f = (f + ff) >>> 0;
234
+ g = (g + gg) >>> 0;
235
+ h = (h + hh) >>> 0;
236
+ }
237
+ // Reduce the 64-bit big-endian value modulo 10000 in 32-bit halves:
238
+ // (hi*2^32 + lo) % 10000 == ((hi%10000) * (2^32 % 10000) + lo%10000) % 10000,
239
+ // and 2^32 % 10000 = 7296. Multiplying hi by 2^32 directly exceeds
240
+ // Number.MAX_SAFE_INTEGER and rounds away the low bits — that float error
241
+ // made most JS buckets diverge from assignment.go (caught by the
242
+ // assignment parity vectors, spec experiment-variant-caching REQ-9).
243
+ return (((a >>> 0) % 10000) * 7296 + ((b >>> 0) % 10000)) % 10000;
244
+ }
245
+ function rotr(value, amount) { return (value >>> amount) | (value << (32 - amount)); }
246
+ function utf8(value) {
247
+ const out = [];
248
+ for (let i = 0; i < value.length; i++) {
249
+ let code = value.charCodeAt(i);
250
+ if (code >= 0xd800 && code <= 0xdbff && i + 1 < value.length) {
251
+ const next = value.charCodeAt(++i);
252
+ code = 0x10000 + ((code - 0xd800) << 10) + (next - 0xdc00);
253
+ }
254
+ if (code < 0x80)
255
+ out.push(code);
256
+ else if (code < 0x800)
257
+ out.push(0xc0 | (code >> 6), 0x80 | (code & 63));
258
+ else if (code < 0x10000)
259
+ out.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 63), 0x80 | (code & 63));
260
+ else
261
+ out.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 63), 0x80 | ((code >> 6) & 63), 0x80 | (code & 63));
262
+ }
263
+ return new Uint8Array(out);
264
+ }
265
+ // CLOUDFRONT_STRIP_END
package/dist/index.d.ts CHANGED
@@ -15,3 +15,4 @@ export { AssetSchema, isImage, isAsset, } from './types/media.js';
15
15
  export * from './audience-data-models.js';
16
16
  export * from './define-data-model.js';
17
17
  export * from './types/index.js';
18
+ export { mountConsentBanner, hasConsentCookie, type ConsentBannerOptions, } from './consent-banner/index.js';
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export { AssetSchema, isImage, isAsset, } from './types/media.js';
9
9
  export * from './audience-data-models.js';
10
10
  export * from './define-data-model.js';
11
11
  export * from './types/index.js';
12
+ export { mountConsentBanner, hasConsentCookie, } from './consent-banner/index.js';
12
13
  // Note: content/ is NOT re-exported here because it imports node:path,
13
14
  // making it incompatible with client-side bundlers. Import server-only
14
15
  // content utilities from '@gradial/aci/content' explicitly.
@@ -0,0 +1,14 @@
1
+ import { type AssignmentsPayloadSource } from '../content/personalization.js';
2
+ export interface AssignmentsScriptProps {
3
+ page: AssignmentsPayloadSource | null | undefined;
4
+ }
5
+ /**
6
+ * AssignmentsScript renders the page-level experiment assignment tag
7
+ * (`<script type="application/json" data-aci-assignments>` with a
8
+ * {"v":1,"experiments":[…]} body, personalization-reporting design/04) when
9
+ * the page payload carries a build-time `experiments` block, and nothing
10
+ * otherwise. createPage appends it to the page tree exactly once per page;
11
+ * sites composing their own pages can render it directly. The JSON body is
12
+ * HTML-safe (assignmentsScriptJSON \u-escapes angle brackets).
13
+ */
14
+ export declare function AssignmentsScript({ page }: AssignmentsScriptProps): import("react").JSX.Element | null;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { assignmentsScriptJSON, } from '../content/personalization.js';
3
+ /**
4
+ * AssignmentsScript renders the page-level experiment assignment tag
5
+ * (`<script type="application/json" data-aci-assignments>` with a
6
+ * {"v":1,"experiments":[…]} body, personalization-reporting design/04) when
7
+ * the page payload carries a build-time `experiments` block, and nothing
8
+ * otherwise. createPage appends it to the page tree exactly once per page;
9
+ * sites composing their own pages can render it directly. The JSON body is
10
+ * HTML-safe (assignmentsScriptJSON \u-escapes angle brackets).
11
+ */
12
+ export function AssignmentsScript({ page }) {
13
+ const json = assignmentsScriptJSON(page);
14
+ if (!json)
15
+ return null;
16
+ return (_jsx("script", { type: "application/json", "data-aci-assignments": "", dangerouslySetInnerHTML: { __html: json } }));
17
+ }
@@ -0,0 +1,19 @@
1
+ import { type IdentitySigningKeys, type PersonalizationBlobStore } from './personalization-host.js';
2
+ export interface PersonalizationCollectRouteConfig {
3
+ siteId: string;
4
+ store: PersonalizationBlobStore;
5
+ identityKeys: IdentitySigningKeys;
6
+ /**
7
+ * Claims verify keys (kid -> secret) for withdrawal EVIDENCE
8
+ * (anonymous-experiments REQ-6): suppression fires only when the request
9
+ * presents a signature-verified aci_claims cookie — presence alone is
10
+ * spoofable, and under universal minting an unverified trigger would
11
+ * suppress every reject-all visitor. Empty = evidence never verifies =
12
+ * suppression disabled (fails safe).
13
+ */
14
+ claimsKeys?: Record<string, string>;
15
+ retentionSeconds?: number;
16
+ secureCookies?: boolean;
17
+ now?: () => Date;
18
+ }
19
+ export declare function createPersonalizationCollectRoute(config: PersonalizationCollectRouteConfig): (request: Request) => Promise<Response>;