@gradial/aci 0.1.41 → 0.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +15 -11
  2. package/README.md +4 -4
  3. package/dist/astro/index.d.ts +1 -1
  4. package/dist/astro/index.js +1 -1
  5. package/dist/astro/page.d.ts +6 -0
  6. package/dist/astro/page.js +3 -2
  7. package/dist/audience-data-models.d.ts +286 -0
  8. package/dist/audience-data-models.js +46 -0
  9. package/dist/cli/compile-registry.d.mts +6 -0
  10. package/dist/cli/compile-registry.mjs +774 -0
  11. package/dist/cli/materialize-template.d.mts +19 -0
  12. package/dist/cli/materialize-template.mjs +128 -0
  13. package/dist/cli/validate-content.d.mts +32 -0
  14. package/dist/cli/validate-content.mjs +464 -0
  15. package/dist/cli/verify-next-cdn-routes.d.mts +2 -0
  16. package/dist/cli/verify-next-cdn-routes.mjs +47 -0
  17. package/dist/collect/aci.js +1 -0
  18. package/dist/collect/aci.js.sha256 +1 -0
  19. package/dist/collect/assignments.d.ts +65 -0
  20. package/dist/collect/assignments.js +138 -0
  21. package/dist/collect/batcher.d.ts +23 -0
  22. package/dist/collect/batcher.js +28 -6
  23. package/dist/collect/consent/envelope.d.ts +9 -3
  24. package/dist/collect/consent/envelope.js +58 -40
  25. package/dist/collect/declarative.d.ts +1 -1
  26. package/dist/collect/declarative.js +7 -1
  27. package/dist/collect/index.js +39 -6
  28. package/dist/collect/linkclicks.js +4 -2
  29. package/dist/collect/pageviews.d.ts +15 -3
  30. package/dist/collect/pageviews.js +28 -13
  31. package/dist/collect/transport.d.ts +4 -0
  32. package/dist/collect/transport.js +37 -2
  33. package/dist/consent/portable-envelope.d.ts +9 -0
  34. package/dist/consent/portable-envelope.js +110 -0
  35. package/dist/consent-banner/index.d.ts +33 -0
  36. package/dist/consent-banner/index.js +100 -0
  37. package/dist/content/assets.d.ts +1 -0
  38. package/dist/content/assets.js +1 -0
  39. package/dist/content/personalization.d.ts +35 -0
  40. package/dist/content/personalization.js +57 -0
  41. package/dist/content/resolve-slots.js +1 -0
  42. package/dist/content/types.d.ts +9 -1
  43. package/dist/edge/personalization-core.d.ts +11 -0
  44. package/dist/edge/personalization-core.js +265 -0
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +1 -0
  47. package/dist/next/assignments-script.d.ts +14 -0
  48. package/dist/next/assignments-script.js +17 -0
  49. package/dist/next/collect-route.d.ts +19 -0
  50. package/dist/next/collect-route.js +422 -0
  51. package/dist/next/envelope-digest.d.ts +21 -0
  52. package/dist/next/envelope-digest.js +58 -0
  53. package/dist/next/index.d.ts +1 -0
  54. package/dist/next/index.js +1 -0
  55. package/dist/next/middleware.d.ts +31 -3
  56. package/dist/next/middleware.js +254 -30
  57. package/dist/next/page.js +2 -1
  58. package/dist/next/personalization-host.d.ts +127 -0
  59. package/dist/next/personalization-host.js +257 -0
  60. package/dist/next/server.d.ts +1 -0
  61. package/dist/next/server.js +9 -6
  62. package/dist/next/vercel-collect-sdk-route.d.ts +27 -0
  63. package/dist/next/vercel-collect-sdk-route.js +205 -0
  64. package/dist/next/vercel-personalization.d.ts +4 -0
  65. package/dist/next/vercel-personalization.js +4 -0
  66. package/dist/next/vercel-s3-personalization.d.ts +79 -0
  67. package/dist/next/vercel-s3-personalization.js +574 -0
  68. package/dist/providers/s3.d.ts +20 -5
  69. package/dist/providers/s3.js +93 -23
  70. package/dist/react/VideoPlayer.d.ts +6 -6
  71. package/dist/react/index.d.ts +6 -6
  72. package/dist/testing/conformance/checks.js +75 -4
  73. package/dist/types/block-ref.d.ts +2 -0
  74. package/dist/types/block-ref.js +1 -0
  75. package/dist/types/page.d.ts +1 -1
  76. package/package.json +44 -15
  77. package/src/astro/ConsentBanner.astro +62 -0
  78. package/src/cli/compile-registry.mjs +24 -10
  79. package/src/cli/validate-content.mjs +52 -13
  80. package/src/cli/verify-next-cdn-routes.mjs +5 -4
  81. package/src/consent-banner/styles.css +110 -0
  82. package/src/types/block-ref.test.ts +45 -0
  83. package/src/types/block-ref.ts +2 -0
  84. package/src/types/page.ts +1 -1
  85. package/templates/astro/starter-content/src/cms/contracts/components/index.ts +2 -0
  86. package/templates/astro/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
  87. package/templates/astro/starter-content/src/cms/registry.ts +3 -0
  88. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +41 -0
  89. package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
  90. package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
  91. package/templates/astro/starter-content/src/fixtures/sections.ts +2 -1
  92. package/templates/astro/starter-content/src/render/RenderPage.astro +3 -2
  93. package/templates/astro/template/src/layouts/SiteLayout.astro +3 -1
  94. package/templates/astro/template/src/pages/[...slug].astro +1 -0
  95. package/templates/common/.agents/skills/authoring/SKILL.md +11 -3
  96. package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
  97. package/templates/common/.content/audiences/preference-blue/_index.json +11 -0
  98. package/templates/common/.content/audiences/preference-red/_index.json +11 -0
  99. package/templates/common/.content/overlays/audience/preference-blue/pages/home/_index.json +13 -0
  100. package/templates/common/.content/overlays/audience/preference-red/pages/home/_index.json +13 -0
  101. package/templates/common/.content/pages/home/_index.json +124 -4
  102. package/templates/common/.content/segments/preference-blue/_index.json +20 -0
  103. package/templates/common/.content/segments/preference-red/_index.json +20 -0
  104. package/templates/common/contracts/components/index.ts +21 -1
  105. package/templates/common/contracts/components/personalizationChoice.contract.ts +17 -0
  106. package/templates/common/starter-content/.content/pages/about/_index.json +3 -0
  107. package/templates/common/starter-content/.content/pages/home/_index.json +9 -0
  108. package/templates/nextjs/starter-content/src/cms/contracts/components/index.ts +3 -1
  109. package/templates/nextjs/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
  110. package/templates/nextjs/starter-content/src/cms/registry.ts +3 -0
  111. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +53 -0
  112. package/templates/nextjs/starter-content/src/middleware.ts +41 -0
  113. package/templates/nextjs/template/src/app/collect/route.ts +18 -0
  114. package/templates/nextjs/template/src/middleware.ts +36 -2
  115. package/templates/nextjs/template/src/middleware.ts.tmpl +31 -1
  116. package/templates/astro/template/.aci.yaml +0 -28
  117. package/templates/common/.content/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
  118. package/templates/common/.content/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
  119. package/templates/common/.content/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
  120. package/templates/common/.content/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
  121. package/templates/common/.content/config/robots.txt +0 -2
  122. package/templates/common/.content/config/site.json +0 -197
  123. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/asset.json +0 -9
  124. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/editorial.json +0 -14
  125. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/governance.json +0 -23
  126. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/versions/v0001.json +0 -37
  127. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/asset.json +0 -9
  128. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/editorial.json +0 -14
  129. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/governance.json +0 -23
  130. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/versions/v0001.json +0 -37
  131. package/templates/common/.content/fragments/navbar/_index.json +0 -25
  132. package/templates/common/.content/fragments/site-footer/_index.json +0 -62
  133. package/templates/common/.content/pages/about/_index.json +0 -51
  134. package/templates/nextjs/template/.aci.yaml +0 -26
@@ -1,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;
@@ -42,11 +42,13 @@ export interface KernelPageSeo {
42
42
  canonical?: string;
43
43
  }
44
44
  export interface KernelPageBlock {
45
+ $type: 'component';
45
46
  id: string;
46
47
  component: string;
47
48
  props: Record<string, unknown>;
48
49
  }
49
50
  export interface KernelAtomicBlock {
51
+ $type: 'component';
50
52
  id: string;
51
53
  component: string;
52
54
  props: Record<string, unknown>;
@@ -58,6 +60,12 @@ export interface KernelPage {
58
60
  layout: string;
59
61
  metadata: KernelPageSeo;
60
62
  regions: Record<string, KernelPageBlock[]>;
63
+ /**
64
+ * Build-time experiment assignment context (personalization-reporting
65
+ * design/04); present only on experiment variant payloads. The starters
66
+ * render it as the data-aci-assignments tag.
67
+ */
68
+ experiments?: ExperimentAssignmentContext[];
61
69
  }
62
70
  export interface KernelSiteConfig {
63
71
  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 PersonalizationCollectionStore } from './personalization-host.js';
2
+ export interface PersonalizationCollectRouteConfig {
3
+ siteId: string;
4
+ store: PersonalizationCollectionStore;
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>;