@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,253 @@
|
|
|
1
|
+
// FROZEN FIXTURE — the request-expression validation path as it stood before
|
|
2
|
+
// ADR 0040 added nested boolean expression trees, vendored from
|
|
3
|
+
// `internal/domain/edge/personalization-core.ts` at commit 1b785e4ac^ (the
|
|
4
|
+
// parent of the ADR 0040 landing commit). Do NOT update this file to track the
|
|
5
|
+
// live validator: its whole value is that it is stale.
|
|
6
|
+
//
|
|
7
|
+
// It exists so one test can reproduce the incident ADR 0040 predicts and says
|
|
8
|
+
// it could not previously debug — a lane running a flat-only evaluator meeting
|
|
9
|
+
// a record carrying a valid nested rule. That evaluator has no concept of a
|
|
10
|
+
// group node, so it classifies `{all:[...]}` as a leaf condition and reports
|
|
11
|
+
// the leaf vocabulary as unsupported. The resulting diagnostics are what the
|
|
12
|
+
// host recorder actually writes during real version skew, and they are NOT the
|
|
13
|
+
// same as those produced by a current, nesting-aware validator refusing a
|
|
14
|
+
// malformed rule.
|
|
15
|
+
//
|
|
16
|
+
// What is vendored: the COMPLETE validation path — `validateRequestExpression`
|
|
17
|
+
// and `validateRequestCondition` with every branch (per-field operator
|
|
18
|
+
// restrictions for device.class / time.now / audience.memberships, and every
|
|
19
|
+
// value check), plus each helper those functions reach
|
|
20
|
+
// (`expressionNumberInDomain`, `parseExpressionTimestamp`,
|
|
21
|
+
// `expressionDaysInMonth`, `canonicalScalar`, `validExpressionRange`,
|
|
22
|
+
// `homogeneousExpressionMembers`, and the DEVICE_CLASSES / MAX_IN_VALUES /
|
|
23
|
+
// grammar constants). Evaluation, matching, and the fact vocabulary are
|
|
24
|
+
// irrelevant to the record's shape and are deliberately absent.
|
|
25
|
+
//
|
|
26
|
+
// Adaptations from the source commit (everything not listed here is verbatim):
|
|
27
|
+
// 1. Entry-point signature. The original `validateRequestExpression` was
|
|
28
|
+
// 3-argument — its callers pre-extracted `all`/`any` with `rule.all || []`.
|
|
29
|
+
// The frozen export is 1-argument and derives them itself with
|
|
30
|
+
// `Array.isArray` guards (strictly stronger than the original `|| []`) so
|
|
31
|
+
// the fixture is safe to call directly on untrusted record data.
|
|
32
|
+
// 2. The diagnostic type is renamed `FrozenExpressionDiagnostic` (from
|
|
33
|
+
// `ExpressionDiagnostic`) so a frozen diagnostic can never be confused
|
|
34
|
+
// with a live one at a call site.
|
|
35
|
+
// 3. Export keywords were removed from the vendored internals; only the
|
|
36
|
+
// entry point and its diagnostic type are exported.
|
|
37
|
+
const MAX_EXPRESSION_CONDITIONS = 64;
|
|
38
|
+
const EXPRESSION_OPERATORS = [
|
|
39
|
+
'equals', 'contains', 'startsWith', 'exists',
|
|
40
|
+
'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual',
|
|
41
|
+
'before', 'after', 'between', 'in',
|
|
42
|
+
];
|
|
43
|
+
const MAX_IN_VALUES = 32;
|
|
44
|
+
// Observed values are gated on the published grammars before any host parser
|
|
45
|
+
// runs (ADR 0038): Number() and Date.parse accept supersets (hex, empty
|
|
46
|
+
// string, vendor date formats) that would let the same fact match here and
|
|
47
|
+
// not in Go. Keep these literals identical to the capability artifact — the
|
|
48
|
+
// drift test compares them byte-for-byte.
|
|
49
|
+
const EXPRESSION_NUMBER_GRAMMAR = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?$/;
|
|
50
|
+
const EXPRESSION_TIMESTAMP_GRAMMAR = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;
|
|
51
|
+
const DEVICE_CLASSES = ['desktop', 'mobile', 'tablet', 'unknown'];
|
|
52
|
+
// Adaptation 1 (see header): the original was 3-argument with caller-extracted
|
|
53
|
+
// `all`/`any`; this 1-argument form derives them with Array.isArray guards.
|
|
54
|
+
// The body below this line is verbatim from the source commit.
|
|
55
|
+
export function frozenValidateRequestExpression(rule) {
|
|
56
|
+
const all = (Array.isArray(rule.all) ? rule.all : []);
|
|
57
|
+
const any = (Array.isArray(rule.any) ? rule.any : []);
|
|
58
|
+
const diagnostics = [];
|
|
59
|
+
if (all.length + any.length === 0)
|
|
60
|
+
diagnostics.push({ code: 'expression.empty', path: 'expression', scope: 'request', message: 'expression must include at least one all or any condition' });
|
|
61
|
+
if (all.length + any.length > MAX_EXPRESSION_CONDITIONS)
|
|
62
|
+
diagnostics.push({ code: 'expression.conditions.too_many', path: 'expression', scope: 'request', message: `expression must contain at most ${MAX_EXPRESSION_CONDITIONS} conditions` });
|
|
63
|
+
Object.keys(rule).filter((group) => group !== 'all' && group !== 'any').sort().forEach((group) => {
|
|
64
|
+
diagnostics.push({ code: 'expression.group.unsupported', path: `expression.${group}`, scope: 'request', message: `expression.${group} is not a supported expression group` });
|
|
65
|
+
});
|
|
66
|
+
for (const [group, conditions] of [['all', all], ['any', any]]) {
|
|
67
|
+
conditions.forEach((condition, index) => frozenValidateRequestCondition(condition, `expression.${group}[${index}]`, diagnostics));
|
|
68
|
+
}
|
|
69
|
+
return diagnostics;
|
|
70
|
+
}
|
|
71
|
+
// Verbatim from the source commit: the full flat-era condition validator,
|
|
72
|
+
// including the per-field operator restrictions and every value check.
|
|
73
|
+
function frozenValidateRequestCondition(condition, path, diagnostics) {
|
|
74
|
+
Object.keys(condition).filter((property) => !['field', 'operator', 'value'].includes(property)).sort().forEach((property) => {
|
|
75
|
+
diagnostics.push({ code: 'expression.property.unsupported', path: `${path}.${property}`, scope: 'request', message: `${path}.${property} is not a supported condition property` });
|
|
76
|
+
});
|
|
77
|
+
const field = String(condition.field || '');
|
|
78
|
+
if (field !== 'referrer' && field !== 'device.class' && field !== 'time.now' && field !== 'audience.memberships' && !['query.', 'header.', 'cookie.', 'geo.', 'utm.'].some((prefix) => field.startsWith(prefix) && field.length > prefix.length)) {
|
|
79
|
+
diagnostics.push({ code: 'expression.field.unsupported', path: `${path}.field`, scope: 'request', message: `${path}.field ${JSON.stringify(field)} is not supported` });
|
|
80
|
+
}
|
|
81
|
+
const operator = String(condition.operator || '');
|
|
82
|
+
if (!EXPRESSION_OPERATORS.includes(operator)) {
|
|
83
|
+
diagnostics.push({ code: 'expression.operator.unsupported', path: `${path}.operator`, scope: 'request', message: `${path}.operator ${JSON.stringify(operator)} is not supported` });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// device.class compares against a closed four-value set, so substring
|
|
87
|
+
// operators express nothing the set cannot — and admitting them would
|
|
88
|
+
// re-create the user-agent-substring habit inside the safe field (ADR 0037).
|
|
89
|
+
if (field === 'device.class' && !['equals', 'in', 'exists'].includes(operator)) {
|
|
90
|
+
diagnostics.push({ code: 'expression.operator.unsupported', path: `${path}.operator`, scope: 'request', message: `${path}.operator ${JSON.stringify(operator)} is not supported for field ${JSON.stringify(field)}, which compares against a closed value set` });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// time.now is a sub-second-churning clock: string operators are silently
|
|
94
|
+
// inert on it and the numeric family can never match an RFC 3339 string
|
|
95
|
+
// (ADR 0038).
|
|
96
|
+
if (field === 'time.now' && !['before', 'after', 'between', 'exists'].includes(operator)) {
|
|
97
|
+
diagnostics.push({ code: 'expression.operator.unsupported', path: `${path}.operator`, scope: 'request', message: `${path}.operator ${JSON.stringify(operator)} is not supported for field ${JSON.stringify(field)}, which supports only before, after, between, exists` });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// audience.memberships is an opaque-ID list: equals is already
|
|
101
|
+
// any-member-equals under the multi-valued match loop, so the substring
|
|
102
|
+
// family would only add silent overmatching of prefixed IDs, and the
|
|
103
|
+
// numeric/instant families can never match an ID (ADR 0039).
|
|
104
|
+
if (field === 'audience.memberships' && !['equals', 'in', 'exists'].includes(operator)) {
|
|
105
|
+
diagnostics.push({ code: 'expression.operator.unsupported', path: `${path}.operator`, scope: 'request', message: `${path}.operator ${JSON.stringify(operator)} is not supported for field ${JSON.stringify(field)}, which supports only equals, in, exists` });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const value = condition.value;
|
|
109
|
+
if (operator === 'exists') {
|
|
110
|
+
if (value !== undefined && value !== null && typeof value !== 'boolean') {
|
|
111
|
+
diagnostics.push({ code: 'expression.value.invalid', path: `${path}.value`, scope: 'request', message: `${path}.value must be a boolean for operator exists` });
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (value === undefined || value === null) {
|
|
116
|
+
diagnostics.push({ code: 'expression.value.required', path: `${path}.value`, scope: 'request', message: `${path}.value is required for operator ${operator}` });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (['equals', 'contains', 'startsWith'].includes(operator)) {
|
|
120
|
+
if (!['string', 'number', 'boolean'].includes(typeof value)
|
|
121
|
+
|| (typeof value === 'number' && !expressionNumberInDomain(value))) {
|
|
122
|
+
diagnostics.push({ code: 'expression.value.invalid', path: `${path}.value`, scope: 'request', message: `${path}.value must be a string, boolean, or finite number using only safe integers` });
|
|
123
|
+
}
|
|
124
|
+
else if (field === 'device.class' && DEVICE_CLASSES.indexOf(canonicalScalar(value)) < 0) {
|
|
125
|
+
diagnostics.push({ code: 'expression.value.invalid', path: `${path}.value`, scope: 'request', message: `${path}.value must be one of ${DEVICE_CLASSES.join(', ')} for field ${JSON.stringify(field)}` });
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (['greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual'].includes(operator)) {
|
|
130
|
+
if (typeof value !== 'number' || !expressionNumberInDomain(value)) {
|
|
131
|
+
diagnostics.push({ code: 'expression.value.invalid', path: `${path}.value`, scope: 'request', message: `${path}.value must be a finite JSON number using only safe integers for operator ${operator}` });
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (operator === 'before' || operator === 'after') {
|
|
136
|
+
if (typeof value !== 'string' || parseExpressionTimestamp(value) === null) {
|
|
137
|
+
diagnostics.push({ code: 'expression.value.invalid', path: `${path}.value`, scope: 'request', message: `${path}.value must be an RFC 3339 timestamp with an explicit UTC offset for operator ${operator}` });
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (operator === 'between') {
|
|
142
|
+
if (!validExpressionRange(value)) {
|
|
143
|
+
diagnostics.push({ code: 'expression.value.invalid', path: `${path}.value`, scope: 'request', message: `${path}.value must be a min/max object whose bounds are both numbers or both timestamps with min not above max` });
|
|
144
|
+
}
|
|
145
|
+
else if (field === 'time.now' && typeof value.min === 'number') {
|
|
146
|
+
// An instant field's observed values are instants: a numeric range on
|
|
147
|
+
// it would validate and then sit silently inert at execution.
|
|
148
|
+
diagnostics.push({ code: 'expression.value.invalid', path: `${path}.value`, scope: 'request', message: `${path}.value bounds must be timestamps for field ${JSON.stringify(field)}` });
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// in: bounded, non-empty, every member one canonicalizable scalar, all
|
|
153
|
+
// members one JSON type — a mixed list is almost always an authoring bug.
|
|
154
|
+
const members = homogeneousExpressionMembers(value);
|
|
155
|
+
if (members === null) {
|
|
156
|
+
diagnostics.push({ code: 'expression.value.invalid', path: `${path}.value`, scope: 'request', message: `${path}.value must be an array of 1 to ${MAX_IN_VALUES} scalars sharing one type` });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (field === 'device.class') {
|
|
160
|
+
for (const member of members) {
|
|
161
|
+
if (DEVICE_CLASSES.indexOf(member) < 0) {
|
|
162
|
+
diagnostics.push({ code: 'expression.value.invalid', path: `${path}.value`, scope: 'request', message: `${path}.value must be one of ${DEVICE_CLASSES.join(', ')} for field ${JSON.stringify(field)}` });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// ── Validation helpers, verbatim from the source commit ──────────────────
|
|
169
|
+
function expressionNumberInDomain(value) {
|
|
170
|
+
if (!Number.isFinite(value))
|
|
171
|
+
return false;
|
|
172
|
+
if (Number.isInteger(value) && !Number.isSafeInteger(value))
|
|
173
|
+
return false;
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
// Explicit calendar validation runs before Date arithmetic because Date.UTC
|
|
177
|
+
// (like Go's time.Date) silently normalizes impossible dates. Instants
|
|
178
|
+
// compare as epoch milliseconds; fractional seconds truncate to milliseconds.
|
|
179
|
+
function parseExpressionTimestamp(value) {
|
|
180
|
+
const match = EXPRESSION_TIMESTAMP_GRAMMAR.exec(value);
|
|
181
|
+
if (!match)
|
|
182
|
+
return null;
|
|
183
|
+
const year = Number(match[1]), month = Number(match[2]), day = Number(match[3]);
|
|
184
|
+
const hour = Number(match[4]), minute = Number(match[5]), second = Number(match[6]);
|
|
185
|
+
if (month < 1 || month > 12 || day < 1 || day > expressionDaysInMonth(year, month))
|
|
186
|
+
return null;
|
|
187
|
+
if (hour > 23 || minute > 59 || second > 59)
|
|
188
|
+
return null;
|
|
189
|
+
let offsetMinutes = 0;
|
|
190
|
+
if (match[8] !== 'Z') {
|
|
191
|
+
const offsetHour = Number(match[8].slice(1, 3)), offsetMinute = Number(match[8].slice(4, 6));
|
|
192
|
+
if (offsetHour > 23 || offsetMinute > 59)
|
|
193
|
+
return null;
|
|
194
|
+
offsetMinutes = offsetHour * 60 + offsetMinute;
|
|
195
|
+
if (match[8][0] === '-')
|
|
196
|
+
offsetMinutes = -offsetMinutes;
|
|
197
|
+
}
|
|
198
|
+
const fractionMillis = match[7] ? Number((match[7].slice(1) + '00').slice(0, 3)) : 0;
|
|
199
|
+
// setUTCFullYear, not Date.UTC: Date.UTC maps two-digit years to 1900+.
|
|
200
|
+
const date = new Date(0);
|
|
201
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
202
|
+
date.setUTCHours(hour, minute, second, 0);
|
|
203
|
+
return date.getTime() - offsetMinutes * 60000 + fractionMillis;
|
|
204
|
+
}
|
|
205
|
+
function expressionDaysInMonth(year, month) {
|
|
206
|
+
if (month === 4 || month === 6 || month === 9 || month === 11)
|
|
207
|
+
return 30;
|
|
208
|
+
if (month === 2)
|
|
209
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
|
|
210
|
+
return 31;
|
|
211
|
+
}
|
|
212
|
+
function canonicalScalar(value) {
|
|
213
|
+
if (typeof value === 'number')
|
|
214
|
+
return Object.is(value, -0) ? '0' : JSON.stringify(value);
|
|
215
|
+
return String(value);
|
|
216
|
+
}
|
|
217
|
+
function validExpressionRange(value) {
|
|
218
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
219
|
+
return false;
|
|
220
|
+
const record = value;
|
|
221
|
+
if (Object.keys(record).some((key) => key !== 'min' && key !== 'max'))
|
|
222
|
+
return false;
|
|
223
|
+
const min = record.min, max = record.max;
|
|
224
|
+
if (min === undefined || min === null || max === undefined || max === null)
|
|
225
|
+
return false;
|
|
226
|
+
if (typeof min === 'number' && expressionNumberInDomain(min)) {
|
|
227
|
+
return typeof max === 'number' && expressionNumberInDomain(max) && min <= max;
|
|
228
|
+
}
|
|
229
|
+
const minMillis = typeof min === 'string' ? parseExpressionTimestamp(min) : null;
|
|
230
|
+
if (minMillis === null)
|
|
231
|
+
return false;
|
|
232
|
+
const maxMillis = typeof max === 'string' ? parseExpressionTimestamp(max) : null;
|
|
233
|
+
return maxMillis !== null && minMillis <= maxMillis;
|
|
234
|
+
}
|
|
235
|
+
function homogeneousExpressionMembers(value) {
|
|
236
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_IN_VALUES)
|
|
237
|
+
return null;
|
|
238
|
+
let memberKind = '';
|
|
239
|
+
const members = [];
|
|
240
|
+
for (const member of value) {
|
|
241
|
+
const kind = typeof member;
|
|
242
|
+
if (kind !== 'string' && kind !== 'number' && kind !== 'boolean')
|
|
243
|
+
return null;
|
|
244
|
+
if (kind === 'number' && !expressionNumberInDomain(member))
|
|
245
|
+
return null;
|
|
246
|
+
if (!memberKind)
|
|
247
|
+
memberKind = kind;
|
|
248
|
+
else if (memberKind !== kind)
|
|
249
|
+
return null;
|
|
250
|
+
members.push(canonicalScalar(member));
|
|
251
|
+
}
|
|
252
|
+
return members;
|
|
253
|
+
}
|
|
@@ -4,10 +4,17 @@ export interface EdgeDispatch {
|
|
|
4
4
|
/** Release-relative immutable HTML artifact selected for this route. */
|
|
5
5
|
artifactPath: string;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* One member of an expression group: a leaf condition, or a nested envelope
|
|
9
|
+
* when it carries `all`/`any` (ADR 0040). Presence of a group key decides
|
|
10
|
+
* which it is, so flat records stay a byte-identical subset.
|
|
11
|
+
*/
|
|
7
12
|
export interface EdgeRuleCondition {
|
|
8
|
-
field
|
|
9
|
-
operator
|
|
13
|
+
field?: string;
|
|
14
|
+
operator?: 'exists' | 'equals' | 'contains' | 'startsWith' | 'greaterThan' | 'greaterThanOrEqual' | 'lessThan' | 'lessThanOrEqual' | 'before' | 'after' | 'between' | 'in' | string;
|
|
10
15
|
value?: unknown;
|
|
16
|
+
all?: EdgeRuleCondition[];
|
|
17
|
+
any?: EdgeRuleCondition[];
|
|
11
18
|
}
|
|
12
19
|
export interface EdgeRuleExpression {
|
|
13
20
|
all?: EdgeRuleCondition[];
|
|
@@ -48,9 +55,30 @@ export interface PersonalizationRequestContext {
|
|
|
48
55
|
signals?: {
|
|
49
56
|
geo?: Record<string, string>;
|
|
50
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* Epoch milliseconds resolving the time.now clock fact (ADR 0038).
|
|
60
|
+
* Defaults to Date.now(); hosts may pin it once per request so every
|
|
61
|
+
* expression in one evaluation reads the same instant.
|
|
62
|
+
*/
|
|
63
|
+
now?: number;
|
|
51
64
|
identityHash?: string;
|
|
52
65
|
identityNamespace?: string;
|
|
53
66
|
siteId?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Called when a variant's rule fails validation during serving (ADR 0040).
|
|
69
|
+
* Records pass the write-time gate before shipping, so this only fires on
|
|
70
|
+
* version skew or a projection bug — a precise signal that must not vanish.
|
|
71
|
+
* Receives routing facts and diagnostic codes only, never request data.
|
|
72
|
+
*/
|
|
73
|
+
onRuleRejected?: (event: RuleRejectionEvent) => void;
|
|
74
|
+
}
|
|
75
|
+
export interface RuleRejectionEvent {
|
|
76
|
+
experimentId?: string;
|
|
77
|
+
armId?: string;
|
|
78
|
+
variantKey: string;
|
|
79
|
+
diagnosticCode: string;
|
|
80
|
+
diagnosticPath: string;
|
|
81
|
+
diagnosticCount: number;
|
|
54
82
|
}
|
|
55
83
|
export interface PersonalizationClaims {
|
|
56
84
|
aud?: string[];
|
|
@@ -58,13 +86,7 @@ export interface PersonalizationClaims {
|
|
|
58
86
|
iat: number;
|
|
59
87
|
exp: number;
|
|
60
88
|
}
|
|
61
|
-
export
|
|
62
|
-
variantKey: string;
|
|
63
|
-
variantValues?: Record<string, string>;
|
|
64
|
-
priority?: number;
|
|
65
|
-
rule?: EdgeRuleExpression;
|
|
66
|
-
dispatch: EdgeDispatch;
|
|
67
|
-
}
|
|
89
|
+
export type PersonalizationVariant = EdgeRouteVariant;
|
|
68
90
|
export interface PersonalizationRoute {
|
|
69
91
|
variants?: Record<string, PersonalizationVariant>;
|
|
70
92
|
}
|
|
@@ -75,10 +97,19 @@ export interface PersonalizationExperiment {
|
|
|
75
97
|
priority?: number;
|
|
76
98
|
allocationVersion?: string;
|
|
77
99
|
salt?: string;
|
|
100
|
+
eligibilityAudienceId?: string;
|
|
101
|
+
/**
|
|
102
|
+
* ADR 0050 (experiment I-18): the projection carries this only when the
|
|
103
|
+
* experiment declares `eligibility.mode: "selected"`; absent means the
|
|
104
|
+
* claims gate (I-6) applies. The selected-leg predicate itself is slice
|
|
105
|
+
* A5 (edge-functions I-16) — A3 is projection plumbing only.
|
|
106
|
+
*/
|
|
107
|
+
eligibilityMode?: 'selected';
|
|
78
108
|
arms: Array<{
|
|
79
109
|
id: string;
|
|
80
110
|
allocationBasisPoints?: number;
|
|
81
|
-
|
|
111
|
+
/** DEBT(edge-functions): decode old arm keys until every retained tenant release passes the ADR-0045 removal inventory. */
|
|
112
|
+
variantKey?: string;
|
|
82
113
|
}>;
|
|
83
114
|
}
|
|
84
115
|
export interface PersonalizationIdentity {
|
|
@@ -93,24 +124,14 @@ export interface PersonalizationExperimentDecision {
|
|
|
93
124
|
allocationVersion: string;
|
|
94
125
|
variantKey: string;
|
|
95
126
|
bucket: number;
|
|
127
|
+
/** audience-mimic design/09: the token's assertion replaced the dice. */
|
|
128
|
+
forced?: boolean;
|
|
96
129
|
}
|
|
97
130
|
export interface PersonalizationDecision {
|
|
131
|
+
/** ADR-0045: number of route experiments excluded as legacy pair-only entries. */
|
|
132
|
+
legacyExperimentExclusions?: number;
|
|
98
133
|
variant: PersonalizationVariant | null;
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
export interface ClassificationData {
|
|
102
|
-
audiences: Record<string, {
|
|
103
|
-
rule: EdgeRuleExpression;
|
|
104
|
-
}>;
|
|
105
|
-
segments: Record<string, {
|
|
106
|
-
rule: EdgeRuleExpression;
|
|
107
|
-
}>;
|
|
108
|
-
experiments: Record<string, PersonalizationExperiment>;
|
|
109
|
-
}
|
|
110
|
-
export interface Classification {
|
|
111
|
-
matchedAudiences: string[];
|
|
112
|
-
matchedSegments: string[];
|
|
113
|
-
experimentArms: Record<string, PersonalizationExperimentDecision>;
|
|
134
|
+
experiments: PersonalizationExperimentDecision[];
|
|
114
135
|
}
|
|
115
136
|
export interface SigningKey {
|
|
116
137
|
kid: string;
|
|
@@ -145,20 +166,6 @@ export interface ConsentSignals {
|
|
|
145
166
|
allowsIdentityTier: boolean;
|
|
146
167
|
privacyDenied: boolean;
|
|
147
168
|
}
|
|
148
|
-
export interface PersonalizationEnvelopeWrite {
|
|
149
|
-
key: string;
|
|
150
|
-
envelopeId: string;
|
|
151
|
-
siteId: string;
|
|
152
|
-
identityNamespace: string;
|
|
153
|
-
identityHash: string;
|
|
154
|
-
records: readonly unknown[];
|
|
155
|
-
digest: string;
|
|
156
|
-
}
|
|
157
|
-
export interface PersonalizationCollectionStore {
|
|
158
|
-
readClaims(siteId: string, namespace: string, identityHash: string, now?: Date): Promise<string | null>;
|
|
159
|
-
appendEnvelope(write: PersonalizationEnvelopeWrite): Promise<void>;
|
|
160
|
-
putSuppression(key: string, record: unknown): Promise<void>;
|
|
161
|
-
}
|
|
162
169
|
export interface PersonalizationHostConfig {
|
|
163
170
|
siteId: string;
|
|
164
171
|
route: EdgeRouteEntry | null;
|
|
@@ -169,13 +176,58 @@ export interface PersonalizationHostConfig {
|
|
|
169
176
|
secureCookies?: boolean;
|
|
170
177
|
now?: () => Date;
|
|
171
178
|
randomBytes?: (length: number) => Uint8Array;
|
|
179
|
+
verifiedVisitor?: {
|
|
180
|
+
rawIdentity: string | null;
|
|
181
|
+
claims: ClaimsPayload | null;
|
|
182
|
+
};
|
|
183
|
+
requestGeo?: {
|
|
184
|
+
country?: string;
|
|
185
|
+
region?: string;
|
|
186
|
+
city?: string;
|
|
187
|
+
};
|
|
188
|
+
issueIdentity?: boolean;
|
|
189
|
+
mimicClaims?: {
|
|
190
|
+
aud?: string[];
|
|
191
|
+
seg?: string[];
|
|
192
|
+
arms?: Record<string, string>;
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Passed straight through to the request context's rejection sink so a host
|
|
196
|
+
* can record a variant skipped for a rule it could not validate (ADR 0040).
|
|
197
|
+
* Unset means the skip stays silent, which is what made the skew incident
|
|
198
|
+
* the ADR predicts undebuggable.
|
|
199
|
+
*/
|
|
200
|
+
onRuleRejected?: (event: RuleRejectionEvent) => void;
|
|
201
|
+
}
|
|
202
|
+
export interface ClaimsPayload {
|
|
203
|
+
aud?: string[];
|
|
204
|
+
seg?: string[];
|
|
205
|
+
iat: number;
|
|
206
|
+
exp: number;
|
|
172
207
|
}
|
|
173
208
|
export interface PersonalizationResolution {
|
|
209
|
+
/** ADR-0045: number of route experiments excluded as legacy pair-only entries. */
|
|
210
|
+
legacyExperimentExclusions?: number;
|
|
211
|
+
/** ADR 0048: the raw identity issued for this request, unsigned and not yet persisted. */
|
|
212
|
+
issuedIdentity?: string;
|
|
174
213
|
variantKey: string;
|
|
175
|
-
|
|
214
|
+
experiments: Array<{
|
|
176
215
|
id: string;
|
|
177
216
|
arm: string;
|
|
178
217
|
allocationVersion: string;
|
|
179
|
-
|
|
218
|
+
/** audience-mimic design/09: the assignment came from the token, not the dice. */
|
|
219
|
+
forced?: boolean;
|
|
220
|
+
}>;
|
|
221
|
+
}
|
|
222
|
+
export interface CompositionDescriptor {
|
|
223
|
+
layoutArtifact: string;
|
|
224
|
+
fragments?: Record<string, string>;
|
|
225
|
+
}
|
|
226
|
+
export type SelectedTargetKind = 'direct' | 'compose' | 'render';
|
|
227
|
+
export interface SelectedTarget {
|
|
228
|
+
kind: SelectedTargetKind;
|
|
229
|
+
path: string;
|
|
230
|
+
digest: string;
|
|
231
|
+
composition?: CompositionDescriptor;
|
|
180
232
|
}
|
|
181
233
|
export { IdentityErasureFencedError } from '../../platform/collectionstore/errors.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Shared edge-decisioning types.
|
|
2
2
|
//
|
|
3
3
|
// These types are the canonical shape for the edge decisioning algorithm.
|
|
4
|
-
// All host adapters (AWS, Cloudflare, Vercel, emulator) import from here
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// All host adapters (AWS, Cloudflare, Vercel, emulator) import from here,
|
|
5
|
+
// and the SDK build compiles this module into its dist/internal tree — there
|
|
6
|
+
// is no SDK-side mirror (ADR 0041 retired the hand-ported copy).
|
|
7
7
|
export { IdentityErasureFencedError } from '../../platform/collectionstore/errors.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Canonical factor keys mirror audience.VariantKey/ParseVariantKey. Their
|
|
2
|
+
// shared vector lives in audience/testdata/variant-key.v1.json.
|
|
3
|
+
export function compareUTF8(left, right) {
|
|
4
|
+
const a = Array.from(left);
|
|
5
|
+
const b = Array.from(right);
|
|
6
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
7
|
+
const difference = a[i].codePointAt(0) - b[i].codePointAt(0);
|
|
8
|
+
if (difference)
|
|
9
|
+
return difference;
|
|
10
|
+
}
|
|
11
|
+
return a.length - b.length;
|
|
12
|
+
}
|
|
13
|
+
export function canonicalVariantKey(values) {
|
|
14
|
+
const fixed = ['locale', 'audience', 'segment'].filter((name) => values[name]?.trim());
|
|
15
|
+
const rest = Object.keys(values).filter((name) => values[name]?.trim() && !fixed.includes(name));
|
|
16
|
+
const generic = rest.filter((name) => !name.startsWith('experiment.')).sort(compareUTF8);
|
|
17
|
+
const experiments = rest.filter((name) => name.startsWith('experiment.')).sort(compareUTF8);
|
|
18
|
+
return [...fixed, ...generic, ...experiments].map((name) => `${name}=${values[name].trim()}`).join(';') || 'base';
|
|
19
|
+
}
|
|
20
|
+
export function parseVariantKey(key) {
|
|
21
|
+
const values = {};
|
|
22
|
+
if (key === 'base')
|
|
23
|
+
return values;
|
|
24
|
+
for (const part of key.split(';')) {
|
|
25
|
+
const separator = part.indexOf('=');
|
|
26
|
+
const name = part.slice(0, separator);
|
|
27
|
+
const value = part.slice(separator + 1);
|
|
28
|
+
if (separator < 1 || !validToken(name) || !validToken(value))
|
|
29
|
+
throw new Error('variant key has malformed factors');
|
|
30
|
+
if (name === 'experiment' || name === 'experimentArm')
|
|
31
|
+
throw new Error('pair-form experiment key is not canonical');
|
|
32
|
+
if (name.startsWith('experiment.') && (!/^[a-zA-Z0-9_-]+$/.test(name.slice('experiment.'.length)) || !/^[a-zA-Z0-9_-]+$/.test(value)))
|
|
33
|
+
throw new Error('variant key has malformed experiment factor');
|
|
34
|
+
if (Object.prototype.hasOwnProperty.call(values, name))
|
|
35
|
+
throw new Error('variant key repeats a dimension');
|
|
36
|
+
Object.defineProperty(values, name, { value, enumerable: true, writable: true, configurable: true });
|
|
37
|
+
}
|
|
38
|
+
if (canonicalVariantKey(values) !== key)
|
|
39
|
+
throw new Error('variant key is not canonically ordered');
|
|
40
|
+
return values;
|
|
41
|
+
}
|
|
42
|
+
function validToken(value) {
|
|
43
|
+
return value !== '' && value !== '.' && value !== '..' && !/[\p{White_Space};=/]/u.test(value);
|
|
44
|
+
}
|
|
@@ -3,3 +3,27 @@ export declare class IdentityErasureFencedError extends Error {
|
|
|
3
3
|
readonly code = "IDENTITY_ERASURE_FENCED";
|
|
4
4
|
constructor();
|
|
5
5
|
}
|
|
6
|
+
export declare class CollectionGateBusyError extends Error {
|
|
7
|
+
readonly name = "CollectionGateBusyError";
|
|
8
|
+
readonly code = "COLLECTION_GATE_BUSY";
|
|
9
|
+
constructor(detail?: string);
|
|
10
|
+
}
|
|
11
|
+
export declare class EnvelopeIdCollisionError extends Error {
|
|
12
|
+
readonly name = "EnvelopeIdCollisionError";
|
|
13
|
+
readonly code = "ENVELOPE_ID_COLLISION";
|
|
14
|
+
readonly envelopeId: string;
|
|
15
|
+
readonly existingDigest: string;
|
|
16
|
+
readonly incomingDigest: string;
|
|
17
|
+
constructor(envelopeId: string, existingDigest: string, incomingDigest: string);
|
|
18
|
+
}
|
|
19
|
+
export declare class AdmissionConflictError extends Error {
|
|
20
|
+
readonly kind: 'envelope' | 'event';
|
|
21
|
+
readonly name = "AdmissionConflictError";
|
|
22
|
+
readonly code = "COLLECT_ADMISSION_CONFLICT";
|
|
23
|
+
constructor(kind: 'envelope' | 'event');
|
|
24
|
+
}
|
|
25
|
+
export declare class CollectionStoreError extends Error {
|
|
26
|
+
readonly code: string;
|
|
27
|
+
readonly name = "CollectionStoreError";
|
|
28
|
+
constructor(code: string, message: string, options?: ErrorOptions);
|
|
29
|
+
}
|
|
@@ -5,3 +5,51 @@ export class IdentityErasureFencedError extends Error {
|
|
|
5
5
|
super('identity collection is fenced by erasure');
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
|
+
// Go parity: audience.ErrCollectionGateBusy. A live or unproven collecting
|
|
9
|
+
// generation owns the identity; the caller must fail loudly and retry later,
|
|
10
|
+
// never treat the append as stored or dropped.
|
|
11
|
+
export class CollectionGateBusyError extends Error {
|
|
12
|
+
name = 'CollectionGateBusyError';
|
|
13
|
+
code = 'COLLECTION_GATE_BUSY';
|
|
14
|
+
constructor(detail) {
|
|
15
|
+
super(detail
|
|
16
|
+
? `audience: identity collection gate is busy: ${detail}`
|
|
17
|
+
: 'audience: identity collection gate is busy');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// Go parity: audience.EnvelopeIDCollisionError. The envelope ID already names
|
|
21
|
+
// an object whose stable-input digest differs (or is absent), so this request
|
|
22
|
+
// is not an identical retry and must not be acknowledged as stored.
|
|
23
|
+
export class EnvelopeIdCollisionError extends Error {
|
|
24
|
+
name = 'EnvelopeIdCollisionError';
|
|
25
|
+
code = 'ENVELOPE_ID_COLLISION';
|
|
26
|
+
envelopeId;
|
|
27
|
+
existingDigest;
|
|
28
|
+
incomingDigest;
|
|
29
|
+
constructor(envelopeId, existingDigest, incomingDigest) {
|
|
30
|
+
super(`audience: envelope ID collision: ${envelopeId}`);
|
|
31
|
+
this.envelopeId = envelopeId;
|
|
32
|
+
this.existingDigest = existingDigest;
|
|
33
|
+
this.incomingDigest = incomingDigest;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Different site-scoped acceptance owner; no visitor identifiers in the error.
|
|
37
|
+
export class AdmissionConflictError extends Error {
|
|
38
|
+
kind;
|
|
39
|
+
name = 'AdmissionConflictError';
|
|
40
|
+
code = 'COLLECT_ADMISSION_CONFLICT';
|
|
41
|
+
constructor(kind) {
|
|
42
|
+
super(`collect ${kind} ID already belongs to another acceptance; retry only the original sealed envelope with its original identity and consent`);
|
|
43
|
+
this.kind = kind;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Internal store failure with a stable bounded code. Collector diagnostics log
|
|
47
|
+
// the code and never the message, which may interpolate object keys.
|
|
48
|
+
export class CollectionStoreError extends Error {
|
|
49
|
+
code;
|
|
50
|
+
name = 'CollectionStoreError';
|
|
51
|
+
constructor(code, message, options) {
|
|
52
|
+
super(message, options);
|
|
53
|
+
this.code = code;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// View-as-audience mimic banner contract (specs/features/audience-mimic).
|
|
2
|
+
// The platform edge router (internal/domain/hosting/runtime/mimic.ts) owns
|
|
3
|
+
// the mimic session — token validation, cookies, redirects, and banner
|
|
4
|
+
// creation. On body-less SSR rewrites it forwards the rendered banner to the
|
|
5
|
+
// framework host via MIMIC_BANNER_HEADER; this module is the framework-side
|
|
6
|
+
// leg that decodes it (the MimicBanner component in next/page.tsx). The
|
|
7
|
+
// middleware strips any inbound copy of the header and sets it only for a
|
|
8
|
+
// validated session, so the decoded HTML is trusted as authored.
|
|
9
|
+
//
|
|
10
|
+
// The SDK-side token validator that used to live here served the retired
|
|
11
|
+
// Next personalization-host lane (ADR 0041); the Go/TS validators in
|
|
12
|
+
// internal/domain/edge remain pinned by the shared vector file.
|
|
13
|
+
export const MIMIC_BANNER_HEADER = 'x-aci-mimic-banner';
|
|
14
|
+
export function decodeMimicBanner(encoded) {
|
|
15
|
+
try {
|
|
16
|
+
return new TextDecoder().decode(base64URLToBytes(encoded));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function base64URLToBytes(value) {
|
|
23
|
+
const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
24
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
|
|
25
|
+
const binary = atob(padded);
|
|
26
|
+
const bytes = new Uint8Array(binary.length);
|
|
27
|
+
for (let i = 0; i < binary.length; i++) {
|
|
28
|
+
bytes[i] = binary.charCodeAt(i);
|
|
29
|
+
}
|
|
30
|
+
return bytes;
|
|
31
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MintlifyBlock, MintlifyDocument, MintlifySnippet } from './types.js';
|
|
2
|
+
export declare function renderDocument(document: MintlifyDocument): string;
|
|
3
|
+
export declare function renderSnippet(snippet: MintlifySnippet): string;
|
|
4
|
+
export declare function validateBlock(value: unknown, label: string, ids: Set<string>): MintlifyBlock;
|
|
5
|
+
export declare function visitBlocks(blocks: MintlifyBlock[], visit: (block: MintlifyBlock) => void): void;
|
|
6
|
+
export declare function validateSnippetReferences(documents: MintlifyDocument[], snippets: MintlifySnippet[]): void;
|