@gradial/aci 0.1.52-rc.3 → 0.1.52-rc.34-e2e.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.
- package/CHANGELOG.md +80 -0
- package/README.md +86 -5
- package/dist/astro/error-page.js +1 -0
- package/dist/astro/index.js +4 -0
- package/dist/astro/page.d.ts +5 -2
- package/dist/astro/page.js +35 -23
- package/dist/astro/preview.js +2 -17
- package/dist/audience-data-models.d.ts +308 -194
- package/dist/audience-data-models.js +314 -51
- package/dist/cli/compile-registry.d.mts +5 -1
- package/dist/cli/compile-registry.mjs +31 -46
- package/dist/cli/mintlify-render.mjs +8 -0
- package/dist/cli/validate-content.d.mts +31 -1
- package/dist/cli/validate-content.mjs +50 -12
- package/dist/collect/aci.js +1 -1
- package/dist/collect/aci.js.sha256 +1 -1
- package/dist/collect/api.d.ts +6 -7
- package/dist/collect/api.js +21 -8
- package/dist/collect/assignments.d.ts +7 -9
- package/dist/collect/assignments.js +23 -32
- package/dist/collect/batcher.d.ts +9 -7
- package/dist/collect/batcher.js +6 -5
- package/dist/collect/consent/envelope.d.ts +7 -1
- package/dist/collect/consent/envelope.js +3 -1
- package/dist/collect/consent/mapping.d.ts +2 -0
- package/dist/collect/consent/mapping.js +1 -0
- package/dist/collect/consenthold.d.ts +38 -0
- package/dist/collect/consenthold.js +82 -0
- package/dist/collect/declarative.js +7 -5
- package/dist/collect/gapbuffer.d.ts +13 -4
- package/dist/collect/gapbuffer.js +21 -10
- package/dist/collect/index.js +67 -20
- package/dist/collect/pageviews.d.ts +11 -11
- package/dist/collect/pageviews.js +27 -22
- package/dist/consent-banner/index.d.ts +17 -0
- package/dist/consent-banner/index.js +23 -0
- package/dist/content/index.d.ts +2 -1
- package/dist/content/index.js +4 -1
- package/dist/content/provider.d.ts +16 -2
- package/dist/content/provider.js +5 -1
- package/dist/content/resolve-slots.d.ts +33 -4
- package/dist/content/resolve-slots.js +127 -19
- package/dist/content/types.d.ts +7 -1
- package/dist/edge/expression-capabilities.v1.d.ts +106 -0
- package/dist/edge/expression-capabilities.v1.js +165 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -1
- package/dist/internal/domain/edge/compose.d.ts +24 -0
- package/dist/internal/domain/edge/compose.js +89 -0
- package/dist/internal/domain/edge/identity.d.ts +5 -0
- package/dist/internal/domain/edge/identity.js +50 -14
- package/dist/internal/domain/edge/index.d.ts +2 -0
- package/dist/internal/domain/edge/index.js +2 -0
- package/dist/internal/domain/edge/mimic_token.d.ts +14 -0
- package/dist/internal/domain/edge/mimic_token.js +125 -0
- package/dist/internal/domain/edge/personalization-core.d.ts +61 -22
- package/dist/internal/domain/edge/personalization-core.js +789 -198
- package/dist/internal/domain/edge/personalization_index.d.ts +17 -0
- package/dist/internal/domain/edge/personalization_index.js +56 -0
- package/dist/internal/domain/edge/record/codec.d.ts +6 -32
- package/dist/internal/domain/edge/record/codec.js +86 -168
- package/dist/internal/domain/edge/resolve.js +76 -39
- package/dist/internal/domain/edge/testdata/flat_only_validator_v1.d.ts +10 -0
- package/dist/internal/domain/edge/testdata/flat_only_validator_v1.js +253 -0
- package/dist/internal/domain/edge/types.d.ts +93 -41
- package/dist/internal/domain/edge/types.js +3 -3
- package/dist/internal/domain/edge/variant_key.d.ts +3 -0
- package/dist/internal/domain/edge/variant_key.js +44 -0
- package/dist/internal/platform/collectionstore/errors.d.ts +24 -0
- package/dist/internal/platform/collectionstore/errors.js +48 -0
- package/dist/mimic/core.d.ts +2 -0
- package/dist/mimic/core.js +31 -0
- package/dist/mintlify/blocks.d.ts +6 -0
- package/dist/mintlify/blocks.js +201 -0
- package/dist/mintlify/config.d.ts +3 -0
- package/dist/mintlify/config.js +77 -0
- package/dist/mintlify/index.d.ts +3 -0
- package/dist/mintlify/index.js +2 -0
- package/dist/mintlify/json.d.ts +8 -0
- package/dist/mintlify/json.js +37 -0
- package/dist/mintlify/materialize.d.ts +14 -0
- package/dist/mintlify/materialize.js +254 -0
- package/dist/mintlify/types.d.ts +74 -0
- package/dist/mintlify/types.js +1 -0
- package/dist/mintlify/validate.d.ts +7 -0
- package/dist/mintlify/validate.js +256 -0
- package/dist/next/assignments-navigation.d.ts +4 -0
- package/dist/next/assignments-navigation.js +18 -0
- package/dist/next/assignments-script.d.ts +3 -2
- package/dist/next/assignments-script.js +5 -5
- package/dist/next/error-pages.d.ts +4 -0
- package/dist/next/error-pages.js +4 -0
- package/dist/next/error-seat-payload.d.ts +2 -1
- package/dist/next/error-seat-payload.js +14 -24
- package/dist/next/error-seat.js +7 -2
- package/dist/next/hosted-page-target.d.ts +7 -0
- package/dist/next/hosted-page-target.js +39 -0
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +3 -1
- package/dist/next/page.d.ts +29 -6
- package/dist/next/page.js +47 -25
- package/dist/next/release-context.d.ts +29 -0
- package/dist/next/release-context.js +55 -0
- package/dist/next/render.d.ts +1 -1
- package/dist/next/render.js +15 -5
- package/dist/next/server.d.ts +8 -0
- package/dist/next/server.js +44 -3
- package/dist/next/vercel-edge-projection-v1.d.ts +0 -9
- package/dist/next/vercel-edge-projection-v1.js +9 -539
- package/dist/preview/core.d.ts +1 -0
- package/dist/preview/core.js +27 -0
- package/dist/react/VideoPlayer.d.ts +3 -3
- package/dist/react/VideoPlayer.js +2 -2
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +2 -3
- package/dist/registry.d.ts +4 -2
- package/dist/testing/conformance/checks.d.ts +2 -2
- package/dist/testing/conformance/checks.js +2 -0
- package/dist/testing/conformance/index.d.ts +1 -0
- package/dist/testing/conformance/index.js +1 -0
- package/dist/testing/conformance/next-stubs.d.ts +6 -0
- package/dist/testing/conformance/next-stubs.js +9 -0
- package/dist/testing/conformance/preset.d.ts +3 -2
- package/dist/testing/conformance/preset.js +15 -8
- package/dist/testing/conformance/root-identity.d.ts +31 -0
- package/dist/testing/conformance/root-identity.js +361 -0
- package/dist/testing/conformance/stubs/next-image.d.ts +7 -0
- package/dist/testing/conformance/stubs/next-image.js +5 -0
- package/dist/testing/conformance/stubs/next-link.d.ts +7 -0
- package/dist/testing/conformance/stubs/next-link.js +4 -0
- package/dist/testing/conformance/types.d.ts +5 -2
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +4 -3
- package/dist/types/block-ref.d.ts +2 -0
- package/dist/types/block-ref.js +3 -2
- package/dist/types/component.d.ts +9 -0
- package/dist/types/config.d.ts +0 -1
- package/dist/types/image.d.ts +54 -17
- package/dist/types/image.js +102 -18
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/media.d.ts +14 -6
- package/dist/types/page.d.ts +0 -1
- package/dist/types/video.d.ts +7 -3
- package/migrations/conformance-registry.md +22 -0
- package/package.json +21 -7
- package/src/cli/compile-registry.mjs +30 -42
- package/src/cli/mintlify-render.mjs +9 -0
- package/src/cli/validate-content.mjs +52 -13
- package/src/types/block-ref.test.ts +60 -0
- package/src/types/block-ref.ts +4 -2
- package/src/types/component.ts +10 -0
- package/src/types/config.ts +0 -1
- package/src/types/image.ts +133 -29
- package/src/types/index.ts +0 -1
- package/src/types/page.ts +0 -1
- package/src/types/video.ts +1 -1
- package/templates/astro/starter-content/src/components/chrome/Footer.astro +5 -3
- package/templates/astro/starter-content/src/components/chrome/Header.astro +5 -3
- package/templates/astro/starter-content/src/components/elements/Button.astro +5 -2
- package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +5 -3
- package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/HomeHero.astro +5 -3
- package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -3
- package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
- package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
- package/templates/astro/starter-content/src/fixtures/sections.ts +3 -3
- package/templates/astro/starter-content/src/stories/CompositionStoryFrame.astro +7 -2
- package/templates/astro/template/.aci.yaml.tmpl +0 -1
- package/templates/astro/template/package.json.tmpl +5 -5
- package/templates/astro/template/src/layouts/SiteLayout.astro +2 -2
- package/templates/common/.agents/skills/aci/SKILL.md +58 -1
- package/templates/common/.agents/skills/authoring/SKILL.md +203 -43
- package/templates/common/.agents/skills/authoring/references/block-registry.md +19 -3
- package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
- package/templates/common/.agents/skills/authoring/references/contracts.md +9 -30
- package/templates/common/.agents/skills/authoring/references/repo-structure.md +5 -4
- package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +3 -3
- package/templates/common/.agents/skills/dam-delivery/SKILL.md +217 -0
- package/templates/common/.agents/skills/dam-delivery/references/permalink-grammar.md +167 -0
- package/templates/common/.agents/skills/migration/references/canon-migration.md +18 -67
- package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
- package/templates/common/.content/fragments/navbar/_index.json +24 -0
- package/templates/common/.content/fragments/site-footer/_index.json +61 -0
- package/templates/common/.content/pages/404/_index.json +4 -2
- package/templates/common/.content/pages/{home/_index.json → _index.json} +10 -1
- package/templates/common/.content/pages/error/_index.json +4 -2
- package/templates/common/.content/schemas/site.schema.json +11 -1
- package/templates/common/.content/templates/error.json +24 -0
- package/templates/common/.content/templates/home.json +28 -0
- package/templates/common/.content/templates/site.json +8 -0
- package/templates/common/.content/templates/standard.json +24 -0
- package/templates/common/.nvmrc +1 -1
- package/templates/common/starter-content/.content/.rules-baseline.json +8 -0
- package/templates/common/starter-content/.content/config/site.json +1 -1
- package/templates/common/starter-content/.content/content-rules/no-public-resource-paths.json +6 -0
- package/templates/common/starter-content/.content/dam/README.md +74 -0
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/asset.json +1 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +2 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/asset.json +1 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +2 -1
- package/templates/common/starter-content/.content/fragments/navbar/_index.json +23 -20
- package/templates/common/starter-content/.content/fragments/site-footer/_index.json +59 -56
- package/templates/common/starter-content/.content/pages/404/_index.json +2 -2
- package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +23 -3
- package/templates/common/starter-content/.content/pages/about/_index.json +5 -2
- package/templates/common/starter-content/.content/pages/error/_index.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/button.json +55 -0
- package/templates/common/starter-content/.content/patterns/components/container.json +67 -0
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +30 -0
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +59 -0
- package/templates/common/starter-content/.content/patterns/components/home_hero.json +151 -0
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +42 -0
- package/templates/common/starter-content/.content/patterns/components/site-footer.json +203 -0
- package/templates/common/starter-content/.content/patterns/components/site-navigation.json +115 -0
- package/templates/common/starter-content/.content/patterns/components/stats_bar.json +45 -0
- package/templates/common/starter-content/.content/schemas/site.schema.json +11 -1
- package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/elements/Button.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +4 -3
- package/templates/nextjs/template/.aci.yaml +9 -0
- package/templates/nextjs/template/.aci.yaml.tmpl +0 -1
- package/templates/nextjs/template/package.json.tmpl +4 -3
- package/templates/nextjs/template/src/app/_components/c15t-bridge.ts +30 -0
- package/templates/nextjs/template/src/app/_components/c15t-consent-ui.tsx +37 -0
- package/templates/nextjs/template/src/app/_components/consent-manager.css +63 -0
- package/templates/nextjs/template/src/app/_components/consent-manager.tsx +76 -0
- package/templates/nextjs/template/src/app/fonts/inter/OFL.txt +93 -0
- package/templates/nextjs/template/src/app/fonts/inter/inter-latin.woff2 +0 -0
- package/templates/nextjs/template/src/app/fonts/space-grotesk/OFL.txt +93 -0
- package/templates/nextjs/template/src/app/fonts/space-grotesk/space-grotesk-latin.woff2 +0 -0
- package/templates/nextjs/template/src/app/layout.tsx +12 -7
- package/templates/nextjs/template/tests/conformance.test.ts.tmpl +1 -0
- package/dist/define-layout.d.ts +0 -6
- package/dist/define-layout.js +0 -14
- package/dist/edge/personalization-core.d.ts +0 -11
- package/dist/edge/personalization-core.js +0 -269
- package/dist/next/collect-route.d.ts +0 -19
- package/dist/next/collect-route.js +0 -459
- package/dist/next/envelope-digest.d.ts +0 -21
- package/dist/next/envelope-digest.js +0 -58
- package/dist/next/middleware.d.ts +0 -40
- package/dist/next/middleware.js +0 -475
- package/dist/next/personalization-host.d.ts +0 -134
- package/dist/next/personalization-host.js +0 -270
- package/dist/next/root-layout.d.ts +0 -4
- package/dist/next/root-layout.js +0 -5
- package/dist/next/vercel-collect-sdk-route.d.ts +0 -27
- package/dist/next/vercel-collect-sdk-route.js +0 -181
- package/dist/next/vercel-personalization.d.ts +0 -4
- package/dist/next/vercel-personalization.js +0 -4
- package/dist/next/vercel-s3-personalization.d.ts +0 -79
- package/dist/next/vercel-s3-personalization.js +0 -570
- package/dist/types/layout.d.ts +0 -24
- package/src/types/layout.ts +0 -29
- package/templates/astro/starter-content/src/render/RenderPage.astro +0 -67
- package/templates/astro/template/src/cms/contracts/layouts/index.ts +0 -18
- package/templates/nextjs/starter-content/src/components/elements/AtomicBlock.tsx +0 -13
- package/templates/nextjs/template/src/cms/contracts/layouts/index.ts +0 -16
- /package/dist/{types/layout.js → cli/mintlify-render.d.mts} +0 -0
- /package/templates/common/.content/overlays/audience/preference-blue/pages/{home/_index.json → _index.json} +0 -0
- /package/templates/common/.content/overlays/audience/preference-red/pages/{home/_index.json → _index.json} +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PersonalizationExperiment, PersonalizationRoute, PersonalizationVariant } from './types.js';
|
|
2
|
+
export interface AuthoredCandidate {
|
|
3
|
+
readonly key: string;
|
|
4
|
+
readonly variant: PersonalizationVariant;
|
|
5
|
+
readonly values: Record<string, string>;
|
|
6
|
+
readonly experiments: readonly PersonalizationExperiment[];
|
|
7
|
+
}
|
|
8
|
+
export interface PersonalizationSelection {
|
|
9
|
+
readonly sourceExperiments: Record<string, PersonalizationExperiment> | undefined;
|
|
10
|
+
readonly authored: readonly AuthoredCandidate[];
|
|
11
|
+
readonly experiments: readonly PersonalizationExperiment[];
|
|
12
|
+
readonly arms: Readonly<Record<string, Readonly<Record<string, PersonalizationVariant>>>>;
|
|
13
|
+
readonly tuples: Readonly<Record<string, PersonalizationVariant>>;
|
|
14
|
+
readonly legacyExperimentExclusions: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function preparePersonalizationRoute(route: PersonalizationRoute, experiments?: Record<string, PersonalizationExperiment>): PersonalizationRoute;
|
|
17
|
+
export declare function personalizationSelection(route: PersonalizationRoute, experiments?: Record<string, PersonalizationExperiment>): PersonalizationSelection;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { variantValuesFromKey } from './record/codec.js';
|
|
2
|
+
import { compareUTF8 } from './variant_key.js';
|
|
3
|
+
const preparedSelection = Symbol('prepared personalization selection');
|
|
4
|
+
// Call once when an immutable partition is decoded. The derived index is a
|
|
5
|
+
// non-enumerable symbol property, so it cannot alter any serialized record.
|
|
6
|
+
export function preparePersonalizationRoute(route, experiments) {
|
|
7
|
+
const existing = route[preparedSelection];
|
|
8
|
+
if (existing && existing.sourceExperiments === experiments)
|
|
9
|
+
return route;
|
|
10
|
+
return Object.freeze(Object.defineProperty({ ...route }, preparedSelection, { value: deriveSelection(route, experiments) }));
|
|
11
|
+
}
|
|
12
|
+
export function personalizationSelection(route, experiments) {
|
|
13
|
+
return preparePersonalizationRoute(route, experiments)[preparedSelection];
|
|
14
|
+
}
|
|
15
|
+
function deriveSelection(route, experiments) {
|
|
16
|
+
const authored = [];
|
|
17
|
+
const arms = Object.create(null);
|
|
18
|
+
const legacy = new Set();
|
|
19
|
+
const qualified = new Set();
|
|
20
|
+
for (const [key, variant] of Object.entries(route.variants || {})) {
|
|
21
|
+
const values = Object.freeze({ ...(variant.variantValues || variantValuesFromKey(key)) });
|
|
22
|
+
// DEBT(edge-functions): ADR-0045 bounded pair reader and counter. Remove
|
|
23
|
+
// only when every retained release in every tenant compiles under the
|
|
24
|
+
// post-removal compiler; pair entries then become malformed.
|
|
25
|
+
if (isPairVariant(key, values)) {
|
|
26
|
+
const id = values.experiment || key.split(';').find((part) => part.startsWith('experiment='))?.slice('experiment='.length);
|
|
27
|
+
if (id && experiments?.[id])
|
|
28
|
+
legacy.add(id);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const names = Object.keys(values);
|
|
32
|
+
const ids = names.filter((name) => name.startsWith('experiment.')).map((name) => name.slice('experiment.'.length));
|
|
33
|
+
for (const id of ids)
|
|
34
|
+
qualified.add(id);
|
|
35
|
+
if (ids.length === 1 && names.length === 1) {
|
|
36
|
+
const id = ids[0];
|
|
37
|
+
(arms[id] ||= Object.create(null))[values[`experiment.${id}`]] = variant;
|
|
38
|
+
}
|
|
39
|
+
const factors = ids.map((id) => experiments?.[id]);
|
|
40
|
+
if (factors.some((item) => !item || item.kind !== 'xt' || item.status !== 'running'))
|
|
41
|
+
continue;
|
|
42
|
+
authored.push(Object.freeze({ key, variant, values, experiments: Object.freeze(factors) }));
|
|
43
|
+
}
|
|
44
|
+
for (const id of qualified)
|
|
45
|
+
legacy.delete(id);
|
|
46
|
+
authored.sort((left, right) => (right.variant.priority || 0) - (left.variant.priority || 0) || compareUTF8(left.key, right.key));
|
|
47
|
+
const scoped = Object.keys(experiments || {}).sort(compareUTF8).map((id) => experiments[id])
|
|
48
|
+
.filter((item) => (item.kind === 'ab' || item.kind === 'abn') && item.status === 'running' && !legacy.has(item.id));
|
|
49
|
+
for (const entries of Object.values(arms))
|
|
50
|
+
Object.freeze(entries);
|
|
51
|
+
return Object.freeze({ sourceExperiments: experiments, authored: Object.freeze(authored), experiments: Object.freeze(scoped), arms: Object.freeze(arms), tuples: route.variants || {}, legacyExperimentExclusions: legacy.size });
|
|
52
|
+
}
|
|
53
|
+
function isPairVariant(key, values) {
|
|
54
|
+
const names = [...key.split(';').map((part) => part.split('=')[0]), ...Object.keys(values)];
|
|
55
|
+
return names.some((name) => name.toLowerCase() === 'experiment' || name.toLowerCase() === 'experimentarm');
|
|
56
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EdgeRouteEntry
|
|
1
|
+
import type { EdgeRouteEntry } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Unique sentinel object returned by expand functions on failure.
|
|
4
4
|
* Callers check `result === INVALID`. Never throws.
|
|
@@ -10,14 +10,17 @@ interface CompactDispatch {
|
|
|
10
10
|
ap: string;
|
|
11
11
|
}
|
|
12
12
|
interface CompactCondition {
|
|
13
|
-
f
|
|
14
|
-
o
|
|
13
|
+
f?: string;
|
|
14
|
+
o?: string;
|
|
15
15
|
w?: unknown;
|
|
16
|
+
a?: CompactCondition[];
|
|
17
|
+
n?: CompactCondition[];
|
|
16
18
|
}
|
|
17
19
|
interface CompactRule {
|
|
18
20
|
a?: CompactCondition[];
|
|
19
21
|
n?: CompactCondition[];
|
|
20
22
|
}
|
|
23
|
+
export declare const COMPACT_MAX_DEPTH = 32;
|
|
21
24
|
interface CompactVariant {
|
|
22
25
|
pr?: number;
|
|
23
26
|
r?: CompactRule;
|
|
@@ -34,35 +37,6 @@ export interface CompactRouteRecord {
|
|
|
34
37
|
}
|
|
35
38
|
export declare function projectRouteRecord(entry: EdgeRouteEntry): CompactRouteRecord;
|
|
36
39
|
export declare function expandRouteRecord(raw: unknown): EdgeRouteEntry | Invalid;
|
|
37
|
-
export interface CompactClassificationData {
|
|
38
|
-
au?: Record<string, {
|
|
39
|
-
r?: CompactRule;
|
|
40
|
-
}>;
|
|
41
|
-
sg?: Record<string, {
|
|
42
|
-
r?: CompactRule;
|
|
43
|
-
}>;
|
|
44
|
-
x?: Record<string, CompactExperiment>;
|
|
45
|
-
}
|
|
46
|
-
interface CompactExperimentArm {
|
|
47
|
-
id: string;
|
|
48
|
-
ab?: number;
|
|
49
|
-
vk?: string;
|
|
50
|
-
ro?: string;
|
|
51
|
-
}
|
|
52
|
-
interface CompactExperiment {
|
|
53
|
-
id: string;
|
|
54
|
-
st?: string;
|
|
55
|
-
k?: string;
|
|
56
|
-
av?: string;
|
|
57
|
-
sl?: string;
|
|
58
|
-
pr?: number;
|
|
59
|
-
ar?: CompactExperimentArm[];
|
|
60
|
-
dn?: string;
|
|
61
|
-
pg?: string;
|
|
62
|
-
gl?: string[];
|
|
63
|
-
}
|
|
64
|
-
export declare function projectClassificationData(data: ClassificationData): CompactClassificationData;
|
|
65
|
-
export declare function expandClassificationData(raw: unknown): ClassificationData | Invalid;
|
|
66
40
|
export declare const MARKER = "{\"s\":1,\"e\":1}";
|
|
67
41
|
export declare function isMarker(raw: string): boolean;
|
|
68
42
|
export declare function canonicalSerialize(value: unknown): string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Compact JSON codec for edge-decisioning route records
|
|
1
|
+
// Compact JSON codec for edge-decisioning route records.
|
|
2
2
|
//
|
|
3
3
|
// project(): canonical → compact (short field names via alias table, omit defaults)
|
|
4
4
|
// expand(): compact → canonical (reverse, never throws — returns INVALID sentinel on failure)
|
|
@@ -41,21 +41,13 @@ const DISPATCH_KIND_FROM_COMPACT = {
|
|
|
41
41
|
};
|
|
42
42
|
const OPERATOR_TO_COMPACT = {
|
|
43
43
|
equals: 'eq', contains: 'ct', startsWith: 'sw', exists: 'ex',
|
|
44
|
+
greaterThan: 'gt', greaterThanOrEqual: 'ge', lessThan: 'lt', lessThanOrEqual: 'le',
|
|
45
|
+
before: 'bf', after: 'af', between: 'bt', in: 'in',
|
|
44
46
|
};
|
|
45
47
|
const OPERATOR_FROM_COMPACT = {
|
|
46
48
|
eq: 'equals', ct: 'contains', sw: 'startsWith', ex: 'exists',
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
running: 'r', paused: 'p', completed: 'd',
|
|
50
|
-
};
|
|
51
|
-
const EXPERIMENT_STATUS_FROM_COMPACT = {
|
|
52
|
-
r: 'running', p: 'paused', d: 'completed',
|
|
53
|
-
};
|
|
54
|
-
const EXPERIMENT_KIND_TO_COMPACT = {
|
|
55
|
-
ab: 'a', abn: 'n',
|
|
56
|
-
};
|
|
57
|
-
const EXPERIMENT_KIND_FROM_COMPACT = {
|
|
58
|
-
a: 'ab', n: 'abn',
|
|
49
|
+
gt: 'greaterThan', ge: 'greaterThanOrEqual', lt: 'lessThan', le: 'lessThanOrEqual',
|
|
50
|
+
bf: 'before', af: 'after', bt: 'between', in: 'in',
|
|
59
51
|
};
|
|
60
52
|
// Compact type guard — reduces minified bytes vs repeated typeof+null+isArray checks
|
|
61
53
|
function isObj(v) {
|
|
@@ -78,20 +70,89 @@ function expandDispatch(raw) {
|
|
|
78
70
|
return INVALID;
|
|
79
71
|
return { kind, artifactPath };
|
|
80
72
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
73
|
+
// Structural recursion guard, deliberately looser than the authored MaxDepth
|
|
74
|
+
// (5) the validators enforce. The codec's job is to bound adversarial
|
|
75
|
+
// recursion in a byte-constrained runtime; the authored contract is
|
|
76
|
+
// validation's. Keeping it looser is what lets an over-depth expression
|
|
77
|
+
// round-trip and be rejected by validation identically in every runtime,
|
|
78
|
+
// rather than being erased by the codec first (ADR 0040).
|
|
79
|
+
export const COMPACT_MAX_DEPTH = 32;
|
|
80
|
+
// A member carrying all/any is an envelope, not a condition — the same
|
|
81
|
+
// presence rule the evaluators apply, so the codec cannot disagree with them
|
|
82
|
+
// about what a member is.
|
|
83
|
+
function isGroupMember(cond) {
|
|
84
|
+
return !!cond && (cond.all !== undefined || cond.any !== undefined);
|
|
85
|
+
}
|
|
86
|
+
function projectCondition(cond, depth) {
|
|
87
|
+
// Projection runs at publish time in a trusted context behind write-time
|
|
88
|
+
// validation: a malformed condition or an operator with no compact token is
|
|
89
|
+
// vocabulary/codec drift, and silently dropping the condition would ship a
|
|
90
|
+
// rule missing a leaf — MORE permissive than authored (ADR 0038). The same
|
|
91
|
+
// reasoning covers a group node: dropping one publishes a rule the author
|
|
92
|
+
// did not write. Expansion stays fail-closed for already-deployed bundles.
|
|
93
|
+
if (!cond) {
|
|
94
|
+
throw new Error('edge rule condition must name a field and operator');
|
|
95
|
+
}
|
|
96
|
+
if (isGroupMember(cond)) {
|
|
97
|
+
if (depth > COMPACT_MAX_DEPTH) {
|
|
98
|
+
throw new Error('edge rule nests deeper than the codec can project');
|
|
99
|
+
}
|
|
100
|
+
if (cond.field !== undefined || cond.operator !== undefined || cond.value !== undefined) {
|
|
101
|
+
throw new Error('edge rule member mixes group and leaf keys');
|
|
102
|
+
}
|
|
103
|
+
const group = {};
|
|
104
|
+
if (cond.all && cond.all.length > 0)
|
|
105
|
+
group.a = cond.all.map((member) => projectCondition(member, depth + 1));
|
|
106
|
+
if (cond.any && cond.any.length > 0)
|
|
107
|
+
group.n = cond.any.map((member) => projectCondition(member, depth + 1));
|
|
108
|
+
if (!group.a && !group.n) {
|
|
109
|
+
throw new Error('edge rule group must include at least one member');
|
|
110
|
+
}
|
|
111
|
+
return group;
|
|
112
|
+
}
|
|
113
|
+
if (!cond.field || !cond.operator) {
|
|
114
|
+
throw new Error('edge rule condition must name a field and operator');
|
|
115
|
+
}
|
|
84
116
|
const compactOp = OPERATOR_TO_COMPACT[cond.operator];
|
|
85
|
-
if (!compactOp)
|
|
86
|
-
|
|
117
|
+
if (!compactOp) {
|
|
118
|
+
throw new Error(`edge rule operator has no compact token: ${cond.operator}`);
|
|
119
|
+
}
|
|
87
120
|
const result = { f: cond.field, o: compactOp };
|
|
88
|
-
if (cond.
|
|
121
|
+
if (cond.value !== undefined)
|
|
89
122
|
result.w = cond.value;
|
|
90
123
|
return result;
|
|
91
124
|
}
|
|
92
|
-
function expandCondition(raw) {
|
|
125
|
+
function expandCondition(raw, depth) {
|
|
93
126
|
if (!isObj(raw))
|
|
94
127
|
return INVALID;
|
|
128
|
+
if (raw.a !== undefined || raw.n !== undefined) {
|
|
129
|
+
if (depth > COMPACT_MAX_DEPTH)
|
|
130
|
+
return INVALID;
|
|
131
|
+
// A member cannot be both: expanding a mixed record would hand the
|
|
132
|
+
// evaluator something neither a group nor a condition.
|
|
133
|
+
if (raw.f !== undefined || raw.o !== undefined || raw.w !== undefined)
|
|
134
|
+
return INVALID;
|
|
135
|
+
const group = {};
|
|
136
|
+
for (const [compact, key] of [['a', 'all'], ['n', 'any']]) {
|
|
137
|
+
if (raw[compact] === undefined)
|
|
138
|
+
continue;
|
|
139
|
+
if (!Array.isArray(raw[compact]))
|
|
140
|
+
return INVALID;
|
|
141
|
+
const members = [];
|
|
142
|
+
for (const member of raw[compact]) {
|
|
143
|
+
const expanded = expandCondition(member, depth + 1);
|
|
144
|
+
if (expanded === INVALID)
|
|
145
|
+
return INVALID;
|
|
146
|
+
members.push(expanded);
|
|
147
|
+
}
|
|
148
|
+
group[key] = members;
|
|
149
|
+
}
|
|
150
|
+
// Length, not truthiness: [] is truthy, so a group whose only list is
|
|
151
|
+
// empty would otherwise expand into a member with no predicate at all.
|
|
152
|
+
if ((group.all?.length || 0) + (group.any?.length || 0) === 0)
|
|
153
|
+
return INVALID;
|
|
154
|
+
return group;
|
|
155
|
+
}
|
|
95
156
|
const f = raw.f;
|
|
96
157
|
const co = raw.o;
|
|
97
158
|
if (typeof f !== 'string')
|
|
@@ -100,7 +161,7 @@ function expandCondition(raw) {
|
|
|
100
161
|
if (!op)
|
|
101
162
|
return INVALID;
|
|
102
163
|
const c = { field: f, operator: op };
|
|
103
|
-
if (
|
|
164
|
+
if (raw.w !== undefined)
|
|
104
165
|
c.value = raw.w;
|
|
105
166
|
return c;
|
|
106
167
|
}
|
|
@@ -109,9 +170,9 @@ function projectRule(rule) {
|
|
|
109
170
|
return undefined;
|
|
110
171
|
const result = {};
|
|
111
172
|
if (rule.all && rule.all.length > 0)
|
|
112
|
-
result.a = rule.all.map(
|
|
173
|
+
result.a = rule.all.map((cond) => projectCondition(cond, 1));
|
|
113
174
|
if (rule.any && rule.any.length > 0)
|
|
114
|
-
result.n = rule.any.map(
|
|
175
|
+
result.n = rule.any.map((cond) => projectCondition(cond, 1));
|
|
115
176
|
if (!result.a && !result.n)
|
|
116
177
|
return undefined;
|
|
117
178
|
return result;
|
|
@@ -124,7 +185,7 @@ function expandRule(raw) {
|
|
|
124
185
|
const rule = {};
|
|
125
186
|
if (Array.isArray(raw.a))
|
|
126
187
|
for (const c of raw.a) {
|
|
127
|
-
const r = expandCondition(c);
|
|
188
|
+
const r = expandCondition(c, 1);
|
|
128
189
|
if (r === INVALID)
|
|
129
190
|
return INVALID;
|
|
130
191
|
if (r)
|
|
@@ -132,7 +193,7 @@ function expandRule(raw) {
|
|
|
132
193
|
}
|
|
133
194
|
if (Array.isArray(raw.n))
|
|
134
195
|
for (const c of raw.n) {
|
|
135
|
-
const r = expandCondition(c);
|
|
196
|
+
const r = expandCondition(c, 1);
|
|
136
197
|
if (r === INVALID)
|
|
137
198
|
return INVALID;
|
|
138
199
|
if (r)
|
|
@@ -218,149 +279,6 @@ export function expandRouteRecord(raw) {
|
|
|
218
279
|
}
|
|
219
280
|
return entry;
|
|
220
281
|
}
|
|
221
|
-
export function projectClassificationData(data) {
|
|
222
|
-
const result = {};
|
|
223
|
-
if (data.audiences && Object.keys(data.audiences).length > 0) {
|
|
224
|
-
result.au = {};
|
|
225
|
-
for (const [key, def] of Object.entries(data.audiences)) {
|
|
226
|
-
const entry = {};
|
|
227
|
-
const rule = projectRule(def.rule);
|
|
228
|
-
if (rule)
|
|
229
|
-
entry.r = rule;
|
|
230
|
-
result.au[key] = entry;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
if (data.segments && Object.keys(data.segments).length > 0) {
|
|
234
|
-
result.sg = {};
|
|
235
|
-
for (const [key, def] of Object.entries(data.segments)) {
|
|
236
|
-
const entry = {};
|
|
237
|
-
const rule = projectRule(def.rule);
|
|
238
|
-
if (rule)
|
|
239
|
-
entry.r = rule;
|
|
240
|
-
result.sg[key] = entry;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (data.experiments && Object.keys(data.experiments).length > 0) {
|
|
244
|
-
result.x = {};
|
|
245
|
-
for (const [key, exp] of Object.entries(data.experiments))
|
|
246
|
-
result.x[key] = projectExperiment(exp);
|
|
247
|
-
}
|
|
248
|
-
return result;
|
|
249
|
-
}
|
|
250
|
-
function projectExperiment(exp) {
|
|
251
|
-
const result = { id: exp.id };
|
|
252
|
-
if (exp.status) {
|
|
253
|
-
const c = EXPERIMENT_STATUS_TO_COMPACT[exp.status];
|
|
254
|
-
if (c)
|
|
255
|
-
result.st = c;
|
|
256
|
-
}
|
|
257
|
-
if (exp.kind) {
|
|
258
|
-
const c = EXPERIMENT_KIND_TO_COMPACT[exp.kind];
|
|
259
|
-
if (c)
|
|
260
|
-
result.k = c;
|
|
261
|
-
}
|
|
262
|
-
if (exp.allocationVersion)
|
|
263
|
-
result.av = exp.allocationVersion;
|
|
264
|
-
if (exp.salt)
|
|
265
|
-
result.sl = exp.salt;
|
|
266
|
-
if (exp.priority)
|
|
267
|
-
result.pr = exp.priority;
|
|
268
|
-
if (exp.arms && exp.arms.length > 0) {
|
|
269
|
-
result.ar = exp.arms.map((arm) => {
|
|
270
|
-
const ca = { id: arm.id };
|
|
271
|
-
if (arm.allocationBasisPoints)
|
|
272
|
-
ca.ab = arm.allocationBasisPoints;
|
|
273
|
-
if (arm.variantKey)
|
|
274
|
-
ca.vk = arm.variantKey;
|
|
275
|
-
return ca;
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
return result;
|
|
279
|
-
}
|
|
280
|
-
export function expandClassificationData(raw) {
|
|
281
|
-
if (!isObj(raw))
|
|
282
|
-
return INVALID;
|
|
283
|
-
const obj = raw;
|
|
284
|
-
const data = { audiences: {}, segments: {}, experiments: {} };
|
|
285
|
-
if (obj.au !== undefined) {
|
|
286
|
-
if (!isObj(obj.au))
|
|
287
|
-
return INVALID;
|
|
288
|
-
for (const [key, defRaw] of Object.entries(obj.au)) {
|
|
289
|
-
if (!isObj(defRaw))
|
|
290
|
-
return INVALID;
|
|
291
|
-
const ruleResult = expandRule(defRaw.r);
|
|
292
|
-
if (ruleResult === INVALID)
|
|
293
|
-
return INVALID;
|
|
294
|
-
data.audiences[key] = { rule: ruleResult || { all: [] } };
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
if (obj.sg !== undefined) {
|
|
298
|
-
if (!isObj(obj.sg))
|
|
299
|
-
return INVALID;
|
|
300
|
-
for (const [key, defRaw] of Object.entries(obj.sg)) {
|
|
301
|
-
if (!isObj(defRaw))
|
|
302
|
-
return INVALID;
|
|
303
|
-
const ruleResult = expandRule(defRaw.r);
|
|
304
|
-
if (ruleResult === INVALID)
|
|
305
|
-
return INVALID;
|
|
306
|
-
data.segments[key] = { rule: ruleResult || { all: [] } };
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
if (obj.x !== undefined) {
|
|
310
|
-
if (!isObj(obj.x))
|
|
311
|
-
return INVALID;
|
|
312
|
-
for (const [key, expRaw] of Object.entries(obj.x)) {
|
|
313
|
-
const expResult = expandExperiment(expRaw);
|
|
314
|
-
if (expResult === INVALID)
|
|
315
|
-
return INVALID;
|
|
316
|
-
data.experiments[key] = expResult;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
return data;
|
|
320
|
-
}
|
|
321
|
-
function expandExperiment(raw) {
|
|
322
|
-
if (!isObj(raw))
|
|
323
|
-
return INVALID;
|
|
324
|
-
const obj = raw;
|
|
325
|
-
const id = obj.id;
|
|
326
|
-
if (typeof id !== 'string')
|
|
327
|
-
return INVALID;
|
|
328
|
-
const exp = { id, status: 'running', kind: 'ab', arms: [] };
|
|
329
|
-
if (typeof obj.st === 'string') {
|
|
330
|
-
const expanded = EXPERIMENT_STATUS_FROM_COMPACT[obj.st];
|
|
331
|
-
if (!expanded)
|
|
332
|
-
return INVALID;
|
|
333
|
-
exp.status = expanded;
|
|
334
|
-
}
|
|
335
|
-
if (typeof obj.k === 'string') {
|
|
336
|
-
const expanded = EXPERIMENT_KIND_FROM_COMPACT[obj.k];
|
|
337
|
-
if (!expanded)
|
|
338
|
-
return INVALID;
|
|
339
|
-
exp.kind = expanded;
|
|
340
|
-
}
|
|
341
|
-
if (typeof obj.av === 'string')
|
|
342
|
-
exp.allocationVersion = obj.av;
|
|
343
|
-
if (typeof obj.sl === 'string')
|
|
344
|
-
exp.salt = obj.sl;
|
|
345
|
-
if (typeof obj.pr === 'number')
|
|
346
|
-
exp.priority = obj.pr;
|
|
347
|
-
if (Array.isArray(obj.ar)) {
|
|
348
|
-
exp.arms = [];
|
|
349
|
-
for (const armRaw of obj.ar) {
|
|
350
|
-
if (!isObj(armRaw))
|
|
351
|
-
return INVALID;
|
|
352
|
-
const armObj = armRaw;
|
|
353
|
-
if (typeof armObj.id !== 'string')
|
|
354
|
-
return INVALID;
|
|
355
|
-
exp.arms.push({
|
|
356
|
-
id: armObj.id,
|
|
357
|
-
allocationBasisPoints: typeof armObj.ab === 'number' ? armObj.ab : 0,
|
|
358
|
-
variantKey: typeof armObj.vk === 'string' ? armObj.vk : '',
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return exp;
|
|
363
|
-
}
|
|
364
282
|
// ── Marker (tri-state: record | marker | absent) ─────────────────────
|
|
365
283
|
export const MARKER = '{"s":1,"e":1}';
|
|
366
284
|
export function isMarker(raw) {
|
|
@@ -1,58 +1,91 @@
|
|
|
1
1
|
// resolvePersonalizationRequest — the edge decisioning entry point.
|
|
2
2
|
//
|
|
3
|
-
// Reads cookies for classification but never writes them
|
|
4
|
-
// identity
|
|
5
|
-
//
|
|
6
|
-
// the selected immutable artifact by release and
|
|
3
|
+
// Reads cookies for classification but never writes them. It may issue a
|
|
4
|
+
// first-party identity in memory when the host permits (ADR 0048); persisting
|
|
5
|
+
// that identity is the host's job. Public personalized responses may be
|
|
6
|
+
// private while providers cache the selected immutable artifact by release and
|
|
7
|
+
// exact artifact path.
|
|
7
8
|
import { resolvePersonalizationDecision, } from './personalization-core.js';
|
|
8
|
-
import { consentSignals, verifyIdentityCookie, identityHashHex, verifyClaims, parseCookieHeader, } from './identity.js';
|
|
9
|
+
import { consentSignals, verifyIdentityCookie, identityHashHex, randomIdentity, verifyClaims, parseCookieHeader, } from './identity.js';
|
|
9
10
|
export async function resolvePersonalizationRequest(request, config) {
|
|
10
11
|
const cookies = parseCookieHeader(request.headers.get('cookie'));
|
|
11
12
|
const consent = consentSignals(request, cookies);
|
|
12
13
|
const now = (config.now ?? (() => new Date()))();
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
14
|
+
// Identity model (ADR 0016 as amended by ADR 0048): experiment bucketing is
|
|
15
|
+
// measurement, keyed by the first-party aci_id. A verified cookie (or the
|
|
16
|
+
// provider bootstrap's identity) is used as presented. With none usable, and
|
|
17
|
+
// only when the host's issuance rule permits it for this request, resolve
|
|
18
|
+
// issues a fresh identity so the request that issues it is also bucketed —
|
|
19
|
+
// an unverifiable cookie is replaced the same way. Persisting the issued
|
|
20
|
+
// identity is the host's decision (page responses only, never shared-cached).
|
|
21
|
+
// Claims and memberships stay gated on the personalization grant.
|
|
18
22
|
//
|
|
19
|
-
// GPC/DNT deny fully: no read, no identity, no
|
|
20
|
-
|
|
23
|
+
// GPC/DNT deny fully: no read, no identity, no issuance, no bucketing.
|
|
24
|
+
//
|
|
25
|
+
// Mimic (specs/features/audience-mimic): a validated mimicClaims input
|
|
26
|
+
// bypasses identity resolution entirely — the session reads no aci_id and
|
|
27
|
+
// no aci_claims (D-2/D-7), so identityHash stays empty and the experiment
|
|
28
|
+
// tier is structurally skipped (D-5). An ARMS-bearing token is the D-17
|
|
29
|
+
// exception: the experiment tier runs in forced mode — the token's named
|
|
30
|
+
// arm replaces the dice for each experiment the request selects through
|
|
31
|
+
// the ordinary gate — and identity still never exists (I-17).
|
|
32
|
+
const mimic = config.mimicClaims;
|
|
33
|
+
const verifiedVisitor = mimic || consent.privacyDenied ? undefined : config.verifiedVisitor;
|
|
34
|
+
const presentedIdentity = mimic || consent.privacyDenied
|
|
21
35
|
? null
|
|
22
|
-
:
|
|
36
|
+
: verifiedVisitor
|
|
37
|
+
? verifiedVisitor.rawIdentity
|
|
38
|
+
: await verifyIdentityCookie(cookies.get('aci_id') || '', config.identityKeys);
|
|
39
|
+
const issuedIdentity = !presentedIdentity && !mimic && !consent.privacyDenied && config.issueIdentity
|
|
40
|
+
? randomIdentity(config.randomBytes)
|
|
41
|
+
: undefined;
|
|
42
|
+
const rawIdentity = presentedIdentity || issuedIdentity || null;
|
|
23
43
|
// The identity hash feeds experiment bucketing for identified visitors.
|
|
24
|
-
// Claims are a signed cookie written by /collect
|
|
25
|
-
// never reads the customer object store to
|
|
26
|
-
|
|
44
|
+
// Claims are a signed cookie written by /collect or the provider identity
|
|
45
|
+
// bootstrap; the EdgeRouter never reads the customer object store to
|
|
46
|
+
// refresh or revoke them.
|
|
27
47
|
let identityHash = '';
|
|
28
48
|
if (rawIdentity) {
|
|
29
49
|
identityHash = await identityHashHex('aci_id', rawIdentity);
|
|
30
50
|
}
|
|
31
|
-
if (rawIdentity && consent.allowsIdentityTier && signedClaims) {
|
|
32
|
-
if (!await verifyClaims(signedClaims, config.claimsKeys || {}, now)) {
|
|
33
|
-
signedClaims = '';
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
51
|
const matchedRoute = config.route;
|
|
37
52
|
let variantKey = '';
|
|
38
53
|
if (matchedRoute) {
|
|
39
|
-
const claims =
|
|
40
|
-
?
|
|
41
|
-
:
|
|
42
|
-
|
|
54
|
+
const claims = mimic
|
|
55
|
+
? { aud: mimic.aud, seg: mimic.seg, iat: 0, exp: 0 }
|
|
56
|
+
: consent.allowsIdentityTier
|
|
57
|
+
? verifiedVisitor
|
|
58
|
+
? verifiedVisitor.claims
|
|
59
|
+
: await verifyClaims(cookies.get('aci_claims') || '', config.claimsKeys || {}, now)
|
|
60
|
+
: null;
|
|
61
|
+
const decision = resolvePersonalizationDecision(toPersonalizationRequestContext(request, now, config.requestGeo, config.onRuleRejected), matchedRoute, claims, config.experiments, identityHash
|
|
43
62
|
? { usable: true, siteId: config.siteId, namespace: 'aci_id', identityHash }
|
|
44
|
-
: undefined);
|
|
63
|
+
: undefined, mimic?.arms);
|
|
45
64
|
if (decision.variant) {
|
|
46
65
|
variantKey = decision.variant.variantKey;
|
|
47
66
|
}
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
:
|
|
51
|
-
|
|
67
|
+
const experiments = decision.experiments.map((item) => ({
|
|
68
|
+
id: item.experimentId,
|
|
69
|
+
arm: item.armId,
|
|
70
|
+
allocationVersion: item.allocationVersion,
|
|
71
|
+
...(item.forced ? { forced: true } : {}),
|
|
72
|
+
}));
|
|
73
|
+
return {
|
|
74
|
+
variantKey,
|
|
75
|
+
experiments,
|
|
76
|
+
...(decision.legacyExperimentExclusions ? { legacyExperimentExclusions: decision.legacyExperimentExclusions } : {}),
|
|
77
|
+
...(issuedIdentity ? { issuedIdentity } : {}),
|
|
78
|
+
};
|
|
52
79
|
}
|
|
53
|
-
return { variantKey };
|
|
80
|
+
return { variantKey, experiments: [], ...(issuedIdentity ? { issuedIdentity } : {}) };
|
|
54
81
|
}
|
|
55
|
-
|
|
82
|
+
// The return type is annotated rather than inferred so the contract is stated
|
|
83
|
+
// at the construction site: a misspelled property would then silently never
|
|
84
|
+
// fire at the sink, which is the exact failure mode this context exists to
|
|
85
|
+
// make visible. No tsconfig covers internal/domain/** today (esbuild/tsx strip
|
|
86
|
+
// types without checking), so the annotation is documentation now and becomes
|
|
87
|
+
// real protection under any future typecheck of these trees.
|
|
88
|
+
function toPersonalizationRequestContext(request, now, requestGeo, onRuleRejected) {
|
|
56
89
|
const url = new URL(request.url);
|
|
57
90
|
const cookies = parseCookieHeader(request.headers.get('cookie'));
|
|
58
91
|
const query = {};
|
|
@@ -60,17 +93,21 @@ function toPersonalizationRequestContext(request) {
|
|
|
60
93
|
(query[key] ||= []).push(value);
|
|
61
94
|
});
|
|
62
95
|
const headers = Object.fromEntries(request.headers.entries());
|
|
96
|
+
// geo.* facts come only from the host's trusted geo. Reading provider geo
|
|
97
|
+
// headers here trusted whatever the viewer sent on every lane whose
|
|
98
|
+
// provider does not stamp that exact header before the decision runs.
|
|
99
|
+
const geo = Object.fromEntries(Object.entries(requestGeo ?? {})
|
|
100
|
+
.filter((entry) => typeof entry[1] === 'string' && entry[1] !== ''));
|
|
63
101
|
return {
|
|
64
102
|
path: url.pathname,
|
|
65
103
|
query,
|
|
66
104
|
headers,
|
|
67
105
|
cookies: Object.fromEntries(cookies.entries()),
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
},
|
|
106
|
+
// The host's pinned per-request clock resolves the time.now fact
|
|
107
|
+
// (ADR 0038), so claims verification and window evaluation read one
|
|
108
|
+
// instant — and config.now-injected tests are deterministic.
|
|
109
|
+
now: now.getTime(),
|
|
110
|
+
...(Object.keys(geo).length > 0 ? { signals: { geo } } : {}),
|
|
111
|
+
...(onRuleRejected ? { onRuleRejected } : {}),
|
|
75
112
|
};
|
|
76
113
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface FrozenExpressionDiagnostic {
|
|
2
|
+
code: string;
|
|
3
|
+
path: string;
|
|
4
|
+
scope: 'request';
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function frozenValidateRequestExpression(rule: {
|
|
8
|
+
all?: unknown[];
|
|
9
|
+
any?: unknown[];
|
|
10
|
+
}): FrozenExpressionDiagnostic[];
|