@gradial/aci 0.1.51-ssr.5 → 0.1.52-rc.2
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/README.md +4 -4
- package/dist/astro/assets.d.ts +8 -8
- package/dist/astro/assets.js +8 -8
- package/dist/astro/index.js +2 -2
- package/dist/content/cache.d.ts +6 -1
- package/dist/content/cache.js +26 -0
- package/dist/dev/content-watch.js +3 -3
- package/dist/edge/personalization-core.d.ts +11 -0
- package/dist/edge/personalization-core.js +269 -0
- package/dist/internal/render-proof.d.ts +1 -0
- package/dist/internal/render-proof.js +197 -0
- package/dist/next/asset-route.d.ts +10 -0
- package/dist/next/asset-route.js +45 -0
- package/dist/next/collect-route.d.ts +19 -0
- package/dist/next/collect-route.js +459 -0
- package/dist/next/config.d.ts +9 -1
- package/dist/next/config.js +79 -4
- package/dist/next/content-watch.js +1 -1
- package/dist/next/edge-config.d.ts +1 -0
- package/dist/next/edge-config.js +92 -0
- package/dist/next/envelope-digest.d.ts +21 -0
- package/dist/next/envelope-digest.js +58 -0
- package/dist/next/error-pages.d.ts +3 -2
- package/dist/next/error-pages.js +9 -14
- package/dist/next/error-seat.d.ts +3 -2
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +2 -1
- package/dist/next/middleware.d.ts +40 -0
- package/dist/next/middleware.js +475 -0
- package/dist/next/page.d.ts +13 -3
- package/dist/next/page.js +61 -82
- package/dist/next/personalization-host.d.ts +134 -0
- package/dist/next/personalization-host.js +270 -0
- package/dist/next/server.d.ts +33 -13
- package/dist/next/server.js +374 -60
- package/dist/next/vercel-collect-sdk-route.d.ts +27 -0
- package/dist/next/vercel-collect-sdk-route.js +181 -0
- package/dist/next/vercel-edge-projection-v1.d.ts +24 -0
- package/dist/next/vercel-edge-projection-v1.js +880 -0
- package/dist/next/vercel-personalization.d.ts +4 -0
- package/dist/next/vercel-personalization.js +4 -0
- package/dist/next/vercel-s3-personalization.d.ts +79 -0
- package/dist/next/vercel-s3-personalization.js +570 -0
- package/dist/providers/object.d.ts +3 -0
- package/dist/providers/object.js +18 -1
- package/dist/providers/s3.d.ts +47 -0
- package/dist/providers/s3.js +440 -0
- package/dist/providers/vercel-object-reader.d.ts +1 -1
- package/dist/providers/vercel-object-reader.js +10 -0
- package/package.json +7 -7
- package/templates/common/.agents/skills/aci/SKILL.md +8 -7
- package/templates/common/.agents/skills/authoring/references/block-registry.md +16 -5
- package/templates/common/.agents/skills/authoring/references/contracts.md +4 -0
- package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +13 -7
- package/templates/common/.agents/skills/migration/references/canon-migration.md +7 -7
- package/templates/common/starter-content/.content/dam/blobs/sha256/58/09/5809142b357fca186bcb0fa41661ccaa142800570d2d4d2de2de77b6d8754773 +0 -0
- package/templates/common/starter-content/.content/dam/blobs/sha256/d7/df/d7dfcf7c59365632a408534cf56a9cc5ce848ccb5ce067fd655bbda7953ef5e8 +0 -0
- package/templates/common/starter-content/.content/dam/folders/seed/ast_site_about_hero/versions/v0001.json +3 -3
- package/templates/common/starter-content/.content/dam/folders/seed/ast_site_home_hero/versions/v0001.json +3 -3
- package/templates/common/starter-content/.content/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
- package/templates/common/starter-content/.content/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
- package/templates/common/starter-content/.content/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
- 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.
|
|
3
|
+
`@gradial/aci` `0.1.52-rc.2` 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.
|
|
11
|
+
npm install @gradial/aci@0.1.52-rc.2 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.
|
|
81
|
+
npx @gradial/aci@0.1.52-rc.2 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.
|
|
87
|
+
npx @gradial/aci@0.1.52-rc.2 init --list
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
## Personalization: The One-Tag Install
|
package/dist/astro/assets.d.ts
CHANGED
|
@@ -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
|
|
8
|
+
* content at `/dam/{path}` (already handled by withAci).
|
|
9
9
|
*
|
|
10
|
-
* - In production:
|
|
11
|
-
*
|
|
12
|
-
* (
|
|
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
|
-
*
|
|
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 = "
|
|
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
|
|
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. "
|
|
36
|
+
* @returns The full URL path (e.g. "/dam/images/hero.webp")
|
|
37
37
|
*
|
|
38
38
|
* @example
|
|
39
39
|
* ```astro
|
package/dist/astro/assets.js
CHANGED
|
@@ -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
|
|
8
|
+
* content at `/dam/{path}` (already handled by withAci).
|
|
9
9
|
*
|
|
10
|
-
* - In production:
|
|
11
|
-
*
|
|
12
|
-
* (
|
|
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
|
-
*
|
|
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 = '
|
|
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
|
|
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. "
|
|
36
|
+
* @returns The full URL path (e.g. "/dam/images/hero.webp")
|
|
37
37
|
*
|
|
38
38
|
* @example
|
|
39
39
|
* ```astro
|
package/dist/astro/index.js
CHANGED
|
@@ -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, '
|
|
97
|
+
const source = path.join(compiledRoot, 'dam');
|
|
98
98
|
if (!fs.existsSync(source))
|
|
99
99
|
return;
|
|
100
|
-
const target = path.join(dir.pathname, '
|
|
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
|
},
|
package/dist/content/cache.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/content/cache.js
CHANGED
|
@@ -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('
|
|
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, '
|
|
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('
|
|
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 {};
|