@gradial/aci 0.1.51-ssr.5 → 0.1.52-rc.1

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 (63) hide show
  1. package/README.md +4 -4
  2. package/dist/astro/assets.d.ts +8 -8
  3. package/dist/astro/assets.js +8 -8
  4. package/dist/astro/index.js +2 -2
  5. package/dist/content/cache.d.ts +6 -1
  6. package/dist/content/cache.js +26 -0
  7. package/dist/dev/content-watch.js +3 -3
  8. package/dist/edge/personalization-core.d.ts +11 -0
  9. package/dist/edge/personalization-core.js +269 -0
  10. package/dist/internal/render-proof.d.ts +1 -0
  11. package/dist/internal/render-proof.js +197 -0
  12. package/dist/next/asset-route.d.ts +10 -0
  13. package/dist/next/asset-route.js +45 -0
  14. package/dist/next/collect-route.d.ts +19 -0
  15. package/dist/next/collect-route.js +459 -0
  16. package/dist/next/config.d.ts +9 -1
  17. package/dist/next/config.js +79 -4
  18. package/dist/next/content-watch.js +1 -1
  19. package/dist/next/edge-config.d.ts +1 -0
  20. package/dist/next/edge-config.js +92 -0
  21. package/dist/next/envelope-digest.d.ts +21 -0
  22. package/dist/next/envelope-digest.js +58 -0
  23. package/dist/next/error-pages.d.ts +3 -2
  24. package/dist/next/error-pages.js +9 -14
  25. package/dist/next/error-seat.d.ts +3 -2
  26. package/dist/next/index.d.ts +2 -2
  27. package/dist/next/index.js +2 -1
  28. package/dist/next/middleware.d.ts +40 -0
  29. package/dist/next/middleware.js +475 -0
  30. package/dist/next/page.d.ts +13 -3
  31. package/dist/next/page.js +61 -82
  32. package/dist/next/personalization-host.d.ts +134 -0
  33. package/dist/next/personalization-host.js +270 -0
  34. package/dist/next/server.d.ts +33 -13
  35. package/dist/next/server.js +374 -60
  36. package/dist/next/vercel-collect-sdk-route.d.ts +27 -0
  37. package/dist/next/vercel-collect-sdk-route.js +181 -0
  38. package/dist/next/vercel-edge-projection-v1.d.ts +24 -0
  39. package/dist/next/vercel-edge-projection-v1.js +880 -0
  40. package/dist/next/vercel-personalization.d.ts +4 -0
  41. package/dist/next/vercel-personalization.js +4 -0
  42. package/dist/next/vercel-s3-personalization.d.ts +79 -0
  43. package/dist/next/vercel-s3-personalization.js +570 -0
  44. package/dist/providers/object.d.ts +3 -0
  45. package/dist/providers/object.js +18 -1
  46. package/dist/providers/s3.d.ts +47 -0
  47. package/dist/providers/s3.js +434 -0
  48. package/dist/providers/vercel-object-reader.d.ts +1 -1
  49. package/dist/providers/vercel-object-reader.js +10 -0
  50. package/package.json +7 -7
  51. package/templates/common/.agents/skills/aci/SKILL.md +8 -7
  52. package/templates/common/.agents/skills/authoring/references/block-registry.md +16 -5
  53. package/templates/common/.agents/skills/authoring/references/contracts.md +4 -0
  54. package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +13 -7
  55. package/templates/common/.agents/skills/migration/references/canon-migration.md +7 -7
  56. package/templates/common/starter-content/.content/dam/blobs/sha256/58/09/5809142b357fca186bcb0fa41661ccaa142800570d2d4d2de2de77b6d8754773 +0 -0
  57. package/templates/common/starter-content/.content/dam/blobs/sha256/d7/df/d7dfcf7c59365632a408534cf56a9cc5ce848ccb5ce067fd655bbda7953ef5e8 +0 -0
  58. package/templates/common/starter-content/.content/dam/folders/seed/ast_site_about_hero/versions/v0001.json +3 -3
  59. package/templates/common/starter-content/.content/dam/folders/seed/ast_site_home_hero/versions/v0001.json +3 -3
  60. package/templates/common/starter-content/.content/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
  61. package/templates/common/starter-content/.content/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
  62. package/templates/common/starter-content/.content/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
  63. package/templates/common/starter-content/.content/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agentic Content Infrastructure SDK
2
2
 
3
- `@gradial/aci` `0.1.51-ssr.5` is a pre-1.0 public-surface reset for the ACI
3
+ `@gradial/aci` `0.1.52-rc.1` is a pre-1.0 public-surface reset for the ACI
4
4
  client SDK. Breaking changes from the old `0.1.x` accidental surface are
5
5
  intentional: the root import is framework-neutral, framework runtime behavior is
6
6
  behind explicit subpaths, and the first required customer path is React/Next.
@@ -8,7 +8,7 @@ behind explicit subpaths, and the first required customer path is React/Next.
8
8
  ## Install
9
9
 
10
10
  ```bash
11
- npm install @gradial/aci@0.1.51-ssr.5 zod@^4
11
+ npm install @gradial/aci@0.1.52-rc.1 zod@^4
12
12
  ```
13
13
 
14
14
  ## Root Import
@@ -78,13 +78,13 @@ host bundles select and serve immutable release artifacts.
78
78
  The first RC scaffold path is Next:
79
79
 
80
80
  ```bash
81
- npx @gradial/aci@0.1.51-ssr.5 init my-site --template next-starter
81
+ npx @gradial/aci@0.1.52-rc.1 init my-site --template next-starter
82
82
  ```
83
83
 
84
84
  List available templates:
85
85
 
86
86
  ```bash
87
- npx @gradial/aci@0.1.51-ssr.5 init --list
87
+ npx @gradial/aci@0.1.52-rc.1 init --list
88
88
  ```
89
89
 
90
90
  ## Personalization: The One-Tag Install
@@ -5,16 +5,16 @@
5
5
  * at the URL level. Instead:
6
6
  *
7
7
  * - In dev: the Vite `damAssetPlugin` serves DAM assets from compiled
8
- * content at `/.gradial-dam/{path}` (already handled by withAci).
8
+ * content at `/dam/{path}` (already handled by withAci).
9
9
  *
10
- * - In production: the provider host maps `/.gradial-dam/*` to authoritative
11
- * S3/R2/CDN storage. There is no app-level asset route
12
- * (the retired Next `/gradial/assets` proxy no longer exists).
10
+ * - In production: CloudFront maps `/dam/*` to the S3 bucket
11
+ * containing the compiled assets. There is no app-level asset route
12
+ * (unlike Next.js which has `/gradial/assets/[releaseId]/[...path]`).
13
13
  * The assets are content-addressed (hashed filenames) so the CDN
14
14
  * can cache them with long TTLs.
15
15
  *
16
16
  * - At build time: `withAci`'s `astro:build:done` hook copies the
17
- * `.gradial-dam` directory from compiled content into the build output,
17
+ * `dam` directory from compiled content into the build output,
18
18
  * so `astro build` produces a self-contained artifact.
19
19
  *
20
20
  * This module provides a helper to construct asset URLs that work in both
@@ -25,15 +25,15 @@
25
25
  * Default asset URL prefix. Matches the Vite middleware path and the
26
26
  * provider gateway's asset path.
27
27
  */
28
- export declare const DAM_ASSET_PREFIX = "/.gradial-dam";
28
+ export declare const DAM_ASSET_PREFIX = "/dam";
29
29
  /**
30
30
  * Construct a DAM asset URL from a logical asset path.
31
31
  *
32
- * In both dev and production, DAM assets live under `/.gradial-dam/`.
32
+ * In both dev and production, DAM assets live under `/dam/`.
33
33
  * This function normalizes the path and prepends the prefix.
34
34
  *
35
35
  * @param logicalPath - The asset path as stored in content (e.g. "images/hero.webp")
36
- * @returns The full URL path (e.g. "/.gradial-dam/images/hero.webp")
36
+ * @returns The full URL path (e.g. "/dam/images/hero.webp")
37
37
  *
38
38
  * @example
39
39
  * ```astro
@@ -5,16 +5,16 @@
5
5
  * at the URL level. Instead:
6
6
  *
7
7
  * - In dev: the Vite `damAssetPlugin` serves DAM assets from compiled
8
- * content at `/.gradial-dam/{path}` (already handled by withAci).
8
+ * content at `/dam/{path}` (already handled by withAci).
9
9
  *
10
- * - In production: the provider host maps `/.gradial-dam/*` to authoritative
11
- * S3/R2/CDN storage. There is no app-level asset route
12
- * (the retired Next `/gradial/assets` proxy no longer exists).
10
+ * - In production: CloudFront maps `/dam/*` to the S3 bucket
11
+ * containing the compiled assets. There is no app-level asset route
12
+ * (unlike Next.js which has `/gradial/assets/[releaseId]/[...path]`).
13
13
  * The assets are content-addressed (hashed filenames) so the CDN
14
14
  * can cache them with long TTLs.
15
15
  *
16
16
  * - At build time: `withAci`'s `astro:build:done` hook copies the
17
- * `.gradial-dam` directory from compiled content into the build output,
17
+ * `dam` directory from compiled content into the build output,
18
18
  * so `astro build` produces a self-contained artifact.
19
19
  *
20
20
  * This module provides a helper to construct asset URLs that work in both
@@ -25,15 +25,15 @@
25
25
  * Default asset URL prefix. Matches the Vite middleware path and the
26
26
  * provider gateway's asset path.
27
27
  */
28
- export const DAM_ASSET_PREFIX = '/.gradial-dam';
28
+ export const DAM_ASSET_PREFIX = '/dam';
29
29
  /**
30
30
  * Construct a DAM asset URL from a logical asset path.
31
31
  *
32
- * In both dev and production, DAM assets live under `/.gradial-dam/`.
32
+ * In both dev and production, DAM assets live under `/dam/`.
33
33
  * This function normalizes the path and prepends the prefix.
34
34
  *
35
35
  * @param logicalPath - The asset path as stored in content (e.g. "images/hero.webp")
36
- * @returns The full URL path (e.g. "/.gradial-dam/images/hero.webp")
36
+ * @returns The full URL path (e.g. "/dam/images/hero.webp")
37
37
  *
38
38
  * @example
39
39
  * ```astro
@@ -94,10 +94,10 @@ export function withAci(options = {}) {
94
94
  'astro:build:done'({ dir }) {
95
95
  const siteDir = process.cwd();
96
96
  const compiledRoot = path.resolve(siteDir, options.compiledRoot || process.env.ACI_CONTENT_ROOT || '.aci/compiled');
97
- const source = path.join(compiledRoot, '.gradial-dam');
97
+ const source = path.join(compiledRoot, 'dam');
98
98
  if (!fs.existsSync(source))
99
99
  return;
100
- const target = path.join(dir.pathname, '.gradial-dam');
100
+ const target = path.join(dir.pathname, 'dam');
101
101
  fs.rmSync(target, { recursive: true, force: true });
102
102
  fs.cpSync(source, target, { recursive: true });
103
103
  },
@@ -1,11 +1,16 @@
1
- import type { ContentProvider } from './provider.js';
1
+ import type { CompiledManifest, ContentProvider } from './provider.js';
2
2
  export declare function getCacheStats(): {
3
+ manifest: {
4
+ hits: number;
5
+ misses: number;
6
+ };
3
7
  siteConfig: {
4
8
  hits: number;
5
9
  misses: number;
6
10
  };
7
11
  };
8
12
  export declare function resetCacheStats(): void;
13
+ export declare function getCachedManifest(provider: ContentProvider): Promise<CompiledManifest>;
9
14
  /**
10
15
  * Get site config with caching. Use this instead of calling provider.getSiteConfig() directly.
11
16
  */
@@ -5,19 +5,44 @@
5
5
  *
6
6
  * Keyed by provider instance for automatic invalidation when its input changes.
7
7
  */
8
+ const manifestCache = new WeakMap();
8
9
  const siteConfigCache = new WeakMap();
9
10
  // Cache hit/miss stats for observability
11
+ let manifestHits = 0;
12
+ let manifestMisses = 0;
10
13
  let siteConfigHits = 0;
11
14
  let siteConfigMisses = 0;
12
15
  export function getCacheStats() {
13
16
  return {
17
+ manifest: { hits: manifestHits, misses: manifestMisses },
14
18
  siteConfig: { hits: siteConfigHits, misses: siteConfigMisses },
15
19
  };
16
20
  }
17
21
  export function resetCacheStats() {
22
+ manifestHits = 0;
23
+ manifestMisses = 0;
18
24
  siteConfigHits = 0;
19
25
  siteConfigMisses = 0;
20
26
  }
27
+ // getCachedManifest is intentionally capability based: the general content
28
+ // interface does not require a manifest, while release-backed providers do.
29
+ export function getCachedManifest(provider) {
30
+ const cached = manifestCache.get(provider);
31
+ if (cached) {
32
+ manifestHits++;
33
+ return cached;
34
+ }
35
+ if (!('manifest' in provider) || typeof provider.manifest !== 'function') {
36
+ throw new Error('Provider does not support manifest()');
37
+ }
38
+ manifestMisses++;
39
+ const promise = provider.manifest().catch((error) => {
40
+ manifestCache.delete(provider);
41
+ throw error;
42
+ });
43
+ manifestCache.set(provider, promise);
44
+ return promise;
45
+ }
21
46
  /**
22
47
  * Get site config with caching. Use this instead of calling provider.getSiteConfig() directly.
23
48
  */
@@ -44,5 +69,6 @@ export function getCachedSiteConfig(provider) {
44
69
  * Useful for testing or manual cache invalidation.
45
70
  */
46
71
  export function clearProviderCache(provider) {
72
+ manifestCache.delete(provider);
47
73
  siteConfigCache.delete(provider);
48
74
  }
@@ -95,7 +95,7 @@ export function damAssetPlugin(options = {}) {
95
95
  configureServer(server) {
96
96
  if (!server.middlewares)
97
97
  return;
98
- server.middlewares.use('/.gradial-dam', async (req, res, next) => {
98
+ server.middlewares.use('/dam', async (req, res, next) => {
99
99
  const root = path.resolve(options.compiledRoot || compiledContentRoot());
100
100
  const requestPath = safeRequestPath(req.url || '');
101
101
  if (!requestPath) {
@@ -103,7 +103,7 @@ export function damAssetPlugin(options = {}) {
103
103
  res.end('asset not found');
104
104
  return;
105
105
  }
106
- const filePath = path.resolve(root, '.gradial-dam', requestPath.replace(/^\/+/, ''));
106
+ const filePath = path.resolve(root, 'dam', requestPath.replace(/^\/+/, ''));
107
107
  if (!isInside(root, filePath)) {
108
108
  res.statusCode = 404;
109
109
  res.end('asset not found');
@@ -127,7 +127,7 @@ function safeRequestPath(url) {
127
127
  const rawPath = url.split('?')[0] || '';
128
128
  try {
129
129
  const decoded = decodeURIComponent(rawPath);
130
- return decoded.startsWith('/.gradial-dam/') ? decoded.slice('/.gradial-dam/'.length) : decoded;
130
+ return decoded.startsWith('/dam/') ? decoded.slice('/dam/'.length) : decoded;
131
131
  }
132
132
  catch {
133
133
  return '';
@@ -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,269 @@
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, experiments);
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, experiments) {
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
+ const experimentID = String(values.experiment || '').trim();
41
+ if (experimentID && (!experiments || !experiments[experimentID]
42
+ || experiments[experimentID].status !== 'running'))
43
+ continue;
44
+ if (!requestDimensionsMatch(request, values))
45
+ continue;
46
+ const membershipMatch = (values.audience && claims && (claims.aud || []).includes(values.audience))
47
+ || (values.segment && claims && (claims.seg || []).includes(values.segment));
48
+ const ruleMatch = variant.rule ? evaluateRule(request, variant.rule) : false;
49
+ if (membershipMatch || ruleMatch)
50
+ matches.push(variant);
51
+ }
52
+ matches.sort((left, right) => (right.priority || 0) - (left.priority || 0)
53
+ || String(left.variantKey || '').localeCompare(String(right.variantKey || '')));
54
+ return matches[0] || null;
55
+ }
56
+ // CloudFront KVS values are capped at 1 KiB. The variant map key is the
57
+ // canonical encoding of its dimensions, so compact route records omit the
58
+ // duplicated variantValues object and recover it at the edge.
59
+ function variantValuesFromKey(key) {
60
+ const values = {};
61
+ const parts = String(key || '').replace(/&/g, ';').split(';');
62
+ for (let index = 0; index < parts.length; index++) {
63
+ const separator = parts[index].indexOf('=');
64
+ if (separator > 0)
65
+ values[parts[index].slice(0, separator)] = parts[index].slice(separator + 1);
66
+ }
67
+ return values;
68
+ }
69
+ function routeHasExperiment(route, experimentID) {
70
+ for (const key in route.variants || {}) {
71
+ const values = route.variants[key].variantValues || variantValuesFromKey(key);
72
+ if (values.experiment === experimentID)
73
+ return true;
74
+ }
75
+ return false;
76
+ }
77
+ function assignExperiment(experiment, identity) {
78
+ const version = String(experiment.allocationVersion || '').trim();
79
+ const salt = String(experiment.salt || '').trim();
80
+ const siteID = String(identity.siteId || '').trim();
81
+ const identityKey = `${String(identity.namespace || '').trim()}:${String(identity.identityHash || '').trim()}`;
82
+ if (!version || !salt || !siteID || identityKey === ':')
83
+ return null;
84
+ const input = [siteID, String(experiment.id || ''), version, identityKey, salt].join('\0');
85
+ const bucket = sha256Bucket(input);
86
+ let cumulative = 0;
87
+ const arms = experiment.arms || [];
88
+ for (let index = 0; index < arms.length; index++) {
89
+ const arm = arms[index];
90
+ if ((arm.allocationBasisPoints || 0) <= 0)
91
+ continue;
92
+ cumulative += arm.allocationBasisPoints;
93
+ if (bucket < cumulative) {
94
+ return {
95
+ experimentId: experiment.id,
96
+ armId: arm.id,
97
+ allocationVersion: version,
98
+ variantKey: arm.variantKey,
99
+ bucket,
100
+ };
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+ function betterVariant(candidatePriority, candidateKey, bestPriority, bestKey) {
106
+ if (candidatePriority !== bestPriority)
107
+ return candidatePriority > bestPriority;
108
+ return String(candidateKey || '').localeCompare(String(bestKey || '')) < 0;
109
+ }
110
+ function requestDimensionsMatch(request, values) {
111
+ for (const rawKey in values) {
112
+ const rawWant = values[rawKey];
113
+ const key = String(rawKey).trim();
114
+ const normalized = key.toLowerCase();
115
+ const want = String(rawWant).trim();
116
+ if (!key || !want || ['audience', 'segment', 'experiment', 'experimentarm'].includes(normalized))
117
+ continue;
118
+ 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] || '');
119
+ if (normalized === 'locale') {
120
+ const locales = String(request.headers['accept-language'] || '')
121
+ .split(',').map((part) => part.split(';')[0].trim()).filter(Boolean);
122
+ for (let localeIndex = 0; localeIndex < locales.length; localeIndex++)
123
+ candidates.push(locales[localeIndex]);
124
+ }
125
+ if (!candidates.some((candidate) => normalized === 'locale'
126
+ ? String(candidate).toLowerCase() === want.toLowerCase()
127
+ : String(candidate) === want))
128
+ return false;
129
+ }
130
+ return true;
131
+ }
132
+ function evaluateRule(request, rule) {
133
+ const all = rule.all || [];
134
+ const any = rule.any || [];
135
+ if (all.length + any.length === 0)
136
+ return false;
137
+ return all.every((condition) => evaluateCondition(request, condition))
138
+ && (any.length === 0 || any.some((condition) => evaluateCondition(request, condition)));
139
+ }
140
+ function evaluateCondition(request, condition) {
141
+ const values = signalValues(request, condition.field);
142
+ if (condition.operator === 'exists')
143
+ return values.length > 0;
144
+ if (values.length === 0 || !['string', 'number', 'boolean'].includes(typeof condition.value))
145
+ return false;
146
+ const expected = String(condition.value);
147
+ return values.some((value) => {
148
+ switch (condition.operator) {
149
+ case 'equals': return value === expected;
150
+ case 'contains': return value.includes(expected);
151
+ case 'startsWith': return value.startsWith(expected);
152
+ default: return false;
153
+ }
154
+ });
155
+ }
156
+ function signalValues(request, field) {
157
+ if (field.startsWith('query.'))
158
+ return request.query[field.slice(6)] || [];
159
+ if (field.startsWith('header.'))
160
+ return request.headers[field.slice(7).toLowerCase()] ? [request.headers[field.slice(7).toLowerCase()]] : [];
161
+ if (field.startsWith('cookie.'))
162
+ return request.cookies[field.slice(7)] ? [request.cookies[field.slice(7)]] : [];
163
+ if (field.startsWith('utm.'))
164
+ return request.query['utm_' + field.slice(4)] || [];
165
+ if (field.startsWith('geo.')) {
166
+ const geo = request.signals && request.signals.geo;
167
+ const value = geo && geo[field.slice(4).toLowerCase()];
168
+ return value ? [value] : [];
169
+ }
170
+ if (field === 'referrer') {
171
+ const value = request.headers.referer || request.headers.referrer;
172
+ return value ? [value] : [];
173
+ }
174
+ return [];
175
+ }
176
+ // SHA-256 mirrors experiment.assignment.go: the first eight digest bytes are
177
+ // interpreted big-endian and reduced modulo 10,000 basis points.
178
+ // CLOUDFRONT_STRIP_START — the build script removes this block for CloudFront
179
+ // Functions (experiment bucketing moves to the hosting Lambda to stay under
180
+ // the 10 KiB function size limit).
181
+ function sha256Bucket(value) {
182
+ const bytes = utf8(value);
183
+ const words = new Uint32Array(64);
184
+ const bitLength = bytes.length * 8;
185
+ const paddedLength = ((bytes.length + 9 + 63) >> 6) << 6;
186
+ const data = new Uint8Array(paddedLength);
187
+ data.set(bytes);
188
+ data[bytes.length] = 128;
189
+ // The message length is a 64-bit big-endian integer across the final
190
+ // eight bytes. The previous code wrote a single truncated byte into each
191
+ // word's most significant position, corrupting every digest (caught by
192
+ // the assignment parity vectors, spec experiment-variant-caching REQ-9).
193
+ const bitsHigh = Math.floor(bitLength / 0x100000000);
194
+ const bitsLow = bitLength >>> 0;
195
+ data[paddedLength - 8] = (bitsHigh >>> 24) & 255;
196
+ data[paddedLength - 7] = (bitsHigh >>> 16) & 255;
197
+ data[paddedLength - 6] = (bitsHigh >>> 8) & 255;
198
+ data[paddedLength - 5] = bitsHigh & 255;
199
+ data[paddedLength - 4] = (bitsLow >>> 24) & 255;
200
+ data[paddedLength - 3] = (bitsLow >>> 16) & 255;
201
+ data[paddedLength - 2] = (bitsLow >>> 8) & 255;
202
+ data[paddedLength - 1] = bitsLow & 255;
203
+ let a = 0x6a09e667, b = 0xbb67ae85, c = 0x3c6ef372, d = 0xa54ff53a;
204
+ let e = 0x510e527f, f = 0x9b05688c, g = 0x1f83d9ab, h = 0x5be0cd19;
205
+ 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];
206
+ for (let offset = 0; offset < paddedLength; offset += 64) {
207
+ for (let i = 0; i < 16; i++) {
208
+ const p = offset + i * 4;
209
+ words[i] = (data[p] << 24) | (data[p + 1] << 16) | (data[p + 2] << 8) | data[p + 3];
210
+ }
211
+ for (let i = 16; i < 64; i++) {
212
+ const x = words[i - 15], y = words[i - 2];
213
+ 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;
214
+ }
215
+ let aa = a, bb = b, cc = c, dd = d, ee = e, ff = f, gg = g, hh = h;
216
+ for (let i = 0; i < 64; i++) {
217
+ const s1 = rotr(ee, 6) ^ rotr(ee, 11) ^ rotr(ee, 25);
218
+ const ch = (ee & ff) ^ (~ee & gg);
219
+ const t1 = (hh + s1 + ch + k[i] + words[i]) >>> 0;
220
+ const s0 = rotr(aa, 2) ^ rotr(aa, 13) ^ rotr(aa, 22);
221
+ const maj = (aa & bb) ^ (aa & cc) ^ (bb & cc);
222
+ const t2 = (s0 + maj) >>> 0;
223
+ hh = gg;
224
+ gg = ff;
225
+ ff = ee;
226
+ ee = (dd + t1) >>> 0;
227
+ dd = cc;
228
+ cc = bb;
229
+ bb = aa;
230
+ aa = (t1 + t2) >>> 0;
231
+ }
232
+ a = (a + aa) >>> 0;
233
+ b = (b + bb) >>> 0;
234
+ c = (c + cc) >>> 0;
235
+ d = (d + dd) >>> 0;
236
+ e = (e + ee) >>> 0;
237
+ f = (f + ff) >>> 0;
238
+ g = (g + gg) >>> 0;
239
+ h = (h + hh) >>> 0;
240
+ }
241
+ // Reduce the 64-bit big-endian value modulo 10000 in 32-bit halves:
242
+ // (hi*2^32 + lo) % 10000 == ((hi%10000) * (2^32 % 10000) + lo%10000) % 10000,
243
+ // and 2^32 % 10000 = 7296. Multiplying hi by 2^32 directly exceeds
244
+ // Number.MAX_SAFE_INTEGER and rounds away the low bits — that float error
245
+ // made most JS buckets diverge from assignment.go (caught by the
246
+ // assignment parity vectors, spec experiment-variant-caching REQ-9).
247
+ return (((a >>> 0) % 10000) * 7296 + ((b >>> 0) % 10000)) % 10000;
248
+ }
249
+ function rotr(value, amount) { return (value >>> amount) | (value << (32 - amount)); }
250
+ function utf8(value) {
251
+ const out = [];
252
+ for (let i = 0; i < value.length; i++) {
253
+ let code = value.charCodeAt(i);
254
+ if (code >= 0xd800 && code <= 0xdbff && i + 1 < value.length) {
255
+ const next = value.charCodeAt(++i);
256
+ code = 0x10000 + ((code - 0xd800) << 10) + (next - 0xdc00);
257
+ }
258
+ if (code < 0x80)
259
+ out.push(code);
260
+ else if (code < 0x800)
261
+ out.push(0xc0 | (code >> 6), 0x80 | (code & 63));
262
+ else if (code < 0x10000)
263
+ out.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 63), 0x80 | (code & 63));
264
+ else
265
+ out.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 63), 0x80 | ((code >> 6) & 63), 0x80 | (code & 63));
266
+ }
267
+ return new Uint8Array(out);
268
+ }
269
+ // CLOUDFRONT_STRIP_END
@@ -0,0 +1 @@
1
+ export {};