@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
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// - Cookie mutation helpers
|
|
9
9
|
//
|
|
10
10
|
// Shared by every static edge adapter; this logic has no SDK host wrapper.
|
|
11
|
-
// internal/domain/audience/runtime/
|
|
11
|
+
// internal/domain/audience/runtime/collect.ts imports from here as well.
|
|
12
12
|
const encoder = new TextEncoder();
|
|
13
13
|
const decoder = new TextDecoder();
|
|
14
14
|
// ── Constants ──
|
|
@@ -59,6 +59,20 @@ export function consentSignals(request, cookies) {
|
|
|
59
59
|
privacyDenied,
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
// identityIssuancePermitted is the one rule for issuing a first-party identity
|
|
63
|
+
// on a request that carries none (ADR 0048). Privacy signals deny outright. A
|
|
64
|
+
// recorded consent choice — any envelope, even a malformed one — decides by
|
|
65
|
+
// itself, so an explicit denial holds in an opt-out country. Only a visitor
|
|
66
|
+
// with no recorded choice falls back to the release's opt-out countries, and
|
|
67
|
+
// an unknown country is never one of them. Personalization uses of the
|
|
68
|
+
// identity (claims, memberships, provider bootstrap) stay gated on the grant.
|
|
69
|
+
export function identityIssuancePermitted(consent, country, optOutCountries) {
|
|
70
|
+
if (consent.privacyDenied)
|
|
71
|
+
return false;
|
|
72
|
+
if (consent.hasConsent || consent.consentInvalid)
|
|
73
|
+
return consent.allowsIdentityTier;
|
|
74
|
+
return country !== '' && optOutCountries.includes(country);
|
|
75
|
+
}
|
|
62
76
|
export function parseConsent(raw, decodeCookieValue = true) {
|
|
63
77
|
let decoded = trimConsentWhitespace(raw);
|
|
64
78
|
if (!decoded)
|
|
@@ -127,11 +141,17 @@ export function randomIdentity(random) {
|
|
|
127
141
|
}
|
|
128
142
|
export async function identityHashHex(namespace, value) {
|
|
129
143
|
const digest = await crypto.subtle.digest('SHA-256', encoder.encode(`${namespace.trim().toLowerCase()}\x00${value.trim()}`));
|
|
130
|
-
return
|
|
144
|
+
return bytesToHex(new Uint8Array(digest));
|
|
131
145
|
}
|
|
132
146
|
export async function sha256Hex(value) {
|
|
133
147
|
const digest = await crypto.subtle.digest('SHA-256', encoder.encode(value));
|
|
134
|
-
return
|
|
148
|
+
return bytesToHex(new Uint8Array(digest));
|
|
149
|
+
}
|
|
150
|
+
function bytesToHex(bytes) {
|
|
151
|
+
let value = '';
|
|
152
|
+
for (const byte of bytes)
|
|
153
|
+
value += byte.toString(16).padStart(2, '0');
|
|
154
|
+
return value;
|
|
135
155
|
}
|
|
136
156
|
// ── Claims cookie (aci_claims) ──
|
|
137
157
|
export async function verifyClaims(value, keys, now) {
|
|
@@ -139,7 +159,9 @@ export async function verifyClaims(value, keys, now) {
|
|
|
139
159
|
if (parts.length !== 4 || parts[0] !== 'v1')
|
|
140
160
|
return null;
|
|
141
161
|
const [, kid, body, signature] = parts;
|
|
142
|
-
|
|
162
|
+
// The kid is viewer-controlled: select only an owned key, never an inherited
|
|
163
|
+
// Object.prototype member such as `__proto__` or `constructor`.
|
|
164
|
+
const secret = Object.hasOwn(keys, kid) ? keys[kid] : undefined;
|
|
143
165
|
if (!secret || !await verifyHMAC(`v1.${kid}.${body}`, signature, secret))
|
|
144
166
|
return null;
|
|
145
167
|
try {
|
|
@@ -164,26 +186,40 @@ export function expireCookie(name, secure) {
|
|
|
164
186
|
return { name, value: '', options: { path: '/', httpOnly: true, secure, sameSite: 'lax', maxAge: 0, expires: new Date(0) } };
|
|
165
187
|
}
|
|
166
188
|
// ── Internal crypto helpers ──
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
189
|
+
// verifyHMAC returns a verdict about the signature, which is the only visitor
|
|
190
|
+
// input here: a malformed or wrong signature is `false`. A fault importing the
|
|
191
|
+
// key or computing the MAC — an empty or misconfigured secret, a WebCrypto
|
|
192
|
+
// failure — is not a verdict and propagates to the caller's documented failure
|
|
193
|
+
// boundary (hosting D-14's logged private 503, the collect handler's observed
|
|
194
|
+
// 500). Folding it into `false` would silently read every visitor as
|
|
195
|
+
// unidentified.
|
|
196
|
+
export async function verifyHMAC(value, signature, secret) {
|
|
197
|
+
const signatureBytes = decodeSignature(signature);
|
|
198
|
+
if (!signatureBytes)
|
|
173
199
|
return false;
|
|
174
|
-
}
|
|
200
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
201
|
+
return await crypto.subtle.verify('HMAC', key, signatureBytes, encoder.encode(value));
|
|
202
|
+
}
|
|
203
|
+
// Signatures are unpadded base64url, the only encoding the Go signers emit. A
|
|
204
|
+
// value outside that grammar (or of a length no base64 string can have) is
|
|
205
|
+
// malformed input; within it, decoding cannot fail.
|
|
206
|
+
const BASE64URL_SIGNATURE = /^[A-Za-z0-9_-]+$/;
|
|
207
|
+
function decodeSignature(signature) {
|
|
208
|
+
if (!BASE64URL_SIGNATURE.test(signature) || signature.length % 4 === 1)
|
|
209
|
+
return null;
|
|
210
|
+
return base64URLDecode(signature);
|
|
175
211
|
}
|
|
176
|
-
async function hmacSHA256(value, secret) {
|
|
212
|
+
export async function hmacSHA256(value, secret) {
|
|
177
213
|
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
178
214
|
return base64URLEncode(new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(value))));
|
|
179
215
|
}
|
|
180
|
-
function base64URLEncode(bytes) {
|
|
216
|
+
export function base64URLEncode(bytes) {
|
|
181
217
|
let binary = '';
|
|
182
218
|
for (const byte of bytes)
|
|
183
219
|
binary += String.fromCharCode(byte);
|
|
184
220
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
185
221
|
}
|
|
186
|
-
function base64URLDecode(value) {
|
|
222
|
+
export function base64URLDecode(value) {
|
|
187
223
|
const padded = value.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - value.length % 4) % 4);
|
|
188
224
|
const binary = atob(padded);
|
|
189
225
|
const bytes = new Uint8Array(binary.length);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface MimicClaims {
|
|
2
|
+
scope: string;
|
|
3
|
+
siteId: string;
|
|
4
|
+
aud?: string[];
|
|
5
|
+
seg?: string[];
|
|
6
|
+
arms?: Record<string, string>;
|
|
7
|
+
sub?: string;
|
|
8
|
+
jti?: string;
|
|
9
|
+
iat: string;
|
|
10
|
+
expiresAt: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const MIMIC_MAX_MEMBERSHIPS = 32;
|
|
13
|
+
export declare const MIMIC_MAX_TOKEN_BYTES = 2048;
|
|
14
|
+
export declare function validateMimicToken(token: string, signingKey: string, siteId: string): Promise<MimicClaims | null>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Mimic token validation for the edge runtime (specs/features/audience-mimic).
|
|
2
|
+
//
|
|
3
|
+
// Same HS256 envelope and signing key as preview; the scope claim makes the
|
|
4
|
+
// tokens non-interchangeable (D-6). This validator is the twin of
|
|
5
|
+
// internal/domain/edge/routing/mimic.go, pinned by the shared vector file
|
|
6
|
+
// (internal/domain/edge/routing/testdata/mimic_token_vectors.json).
|
|
7
|
+
export const MIMIC_MAX_MEMBERSHIPS = 32;
|
|
8
|
+
export const MIMIC_MAX_TOKEN_BYTES = 2048;
|
|
9
|
+
export async function validateMimicToken(token, signingKey, siteId) {
|
|
10
|
+
if (!token || token.length > MIMIC_MAX_TOKEN_BYTES || !signingKey || !siteId)
|
|
11
|
+
return null;
|
|
12
|
+
const parts = token.split('.');
|
|
13
|
+
if (parts.length !== 3)
|
|
14
|
+
return null;
|
|
15
|
+
const signingInput = parts[0] + '.' + parts[1];
|
|
16
|
+
let signatureBytes;
|
|
17
|
+
try {
|
|
18
|
+
signatureBytes = base64URLDecode(parts[2]);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(signingKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
24
|
+
const valid = await crypto.subtle.verify('HMAC', key, signatureBytes, new TextEncoder().encode(signingInput));
|
|
25
|
+
if (!valid)
|
|
26
|
+
return null;
|
|
27
|
+
let claims;
|
|
28
|
+
try {
|
|
29
|
+
claims = JSON.parse(new TextDecoder().decode(base64URLDecode(parts[1])));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
// Explicit shape validation (edge-functions I-17): the Go twin rejects a
|
|
35
|
+
// wrong-typed present field at json.Unmarshal, so this validator must
|
|
36
|
+
// reject the same tokens rather than tolerating-and-defaulting them.
|
|
37
|
+
// Absent optional fields stay absent — addition stays compatible,
|
|
38
|
+
// corruption does not (design/09 claims delta).
|
|
39
|
+
if (typeof claims !== 'object' || claims === null || Array.isArray(claims))
|
|
40
|
+
return null;
|
|
41
|
+
const raw = claims;
|
|
42
|
+
if (raw.scope !== 'mimic')
|
|
43
|
+
return null;
|
|
44
|
+
if (typeof raw.siteId !== 'string' || raw.siteId !== siteId)
|
|
45
|
+
return null;
|
|
46
|
+
const aud = shapeStringList(raw.aud);
|
|
47
|
+
const seg = shapeStringList(raw.seg);
|
|
48
|
+
const arms = shapeStringMap(raw.arms);
|
|
49
|
+
if (aud === null || seg === null || arms === null)
|
|
50
|
+
return null;
|
|
51
|
+
if (raw.sub !== undefined && typeof raw.sub !== 'string')
|
|
52
|
+
return null;
|
|
53
|
+
if (raw.jti !== undefined && typeof raw.jti !== 'string')
|
|
54
|
+
return null;
|
|
55
|
+
if (typeof raw.iat !== 'string' || Number.isNaN(new Date(raw.iat).getTime()))
|
|
56
|
+
return null;
|
|
57
|
+
if (typeof raw.expiresAt !== 'string')
|
|
58
|
+
return null;
|
|
59
|
+
// Amended validity rule (03, design/09): at least one entry across aud,
|
|
60
|
+
// seg, and arms — an arms-only token is valid; empty memberships are a
|
|
61
|
+
// real visitor state.
|
|
62
|
+
if (aud.length + seg.length + Object.keys(arms).length === 0)
|
|
63
|
+
return null;
|
|
64
|
+
if (aud.length > MIMIC_MAX_MEMBERSHIPS || seg.length > MIMIC_MAX_MEMBERSHIPS)
|
|
65
|
+
return null;
|
|
66
|
+
const expiresAt = new Date(raw.expiresAt);
|
|
67
|
+
if (Number.isNaN(expiresAt.getTime()) || expiresAt.getTime() <= Date.now())
|
|
68
|
+
return null;
|
|
69
|
+
const validated = {
|
|
70
|
+
scope: 'mimic',
|
|
71
|
+
siteId: raw.siteId,
|
|
72
|
+
iat: raw.iat,
|
|
73
|
+
expiresAt: raw.expiresAt,
|
|
74
|
+
};
|
|
75
|
+
if (aud.length > 0)
|
|
76
|
+
validated.aud = aud;
|
|
77
|
+
if (seg.length > 0)
|
|
78
|
+
validated.seg = seg;
|
|
79
|
+
if (Object.keys(arms).length > 0)
|
|
80
|
+
validated.arms = arms;
|
|
81
|
+
if (raw.sub !== undefined)
|
|
82
|
+
validated.sub = raw.sub;
|
|
83
|
+
if (raw.jti !== undefined)
|
|
84
|
+
validated.jti = raw.jti;
|
|
85
|
+
return validated;
|
|
86
|
+
}
|
|
87
|
+
// shapeStringList admits an absent field or a list of strings; anything else
|
|
88
|
+
// (a scalar, a list with non-string members) is the malformed shape Go's
|
|
89
|
+
// json.Unmarshal rejects.
|
|
90
|
+
function shapeStringList(value) {
|
|
91
|
+
if (value === undefined)
|
|
92
|
+
return [];
|
|
93
|
+
if (!Array.isArray(value))
|
|
94
|
+
return null;
|
|
95
|
+
for (const member of value) {
|
|
96
|
+
if (typeof member !== 'string')
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
// shapeStringMap is the arms-field twin of shapeStringList: an absent field
|
|
102
|
+
// or a string→string map; a scalar, array, or non-string value is malformed.
|
|
103
|
+
function shapeStringMap(value) {
|
|
104
|
+
if (value === undefined)
|
|
105
|
+
return {};
|
|
106
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
107
|
+
return null;
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const [key, member] of Object.entries(value)) {
|
|
110
|
+
if (typeof member !== 'string')
|
|
111
|
+
return null;
|
|
112
|
+
out[key] = member;
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
function base64URLDecode(value) {
|
|
117
|
+
const padded = value.replace(/-/g, '+').replace(/_/g, '/')
|
|
118
|
+
+ '='.repeat((4 - value.length % 4) % 4);
|
|
119
|
+
const binary = atob(padded);
|
|
120
|
+
const bytes = new Uint8Array(binary.length);
|
|
121
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
122
|
+
bytes[index] = binary.charCodeAt(index);
|
|
123
|
+
}
|
|
124
|
+
return bytes;
|
|
125
|
+
}
|
|
@@ -1,24 +1,63 @@
|
|
|
1
|
-
import type { PersonalizationRequestContext, PersonalizationClaims,
|
|
2
|
-
export
|
|
3
|
-
export declare
|
|
1
|
+
import type { PersonalizationRequestContext, PersonalizationClaims, PersonalizationRoute, PersonalizationExperiment, PersonalizationIdentity, PersonalizationDecision } from './types.js';
|
|
2
|
+
export { preparePersonalizationRoute } from './personalization_index.js';
|
|
3
|
+
export declare class PersonalizationResolutionError extends Error {
|
|
4
|
+
readonly variantKey: string;
|
|
5
|
+
readonly code = "personalization.tuple_missing";
|
|
6
|
+
constructor(variantKey: string);
|
|
7
|
+
}
|
|
8
|
+
export declare function resolvePersonalizationDecision(request: PersonalizationRequestContext, route: PersonalizationRoute | null, claims: PersonalizationClaims | null, experiments?: Record<string, PersonalizationExperiment>, identity?: PersonalizationIdentity, forcedArms?: Record<string, string>): PersonalizationDecision;
|
|
9
|
+
export type ExpressionFactStatus = 'present' | 'absent' | 'unavailable';
|
|
10
|
+
export interface ExpressionFact {
|
|
11
|
+
status: ExpressionFactStatus;
|
|
12
|
+
values?: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface ExpressionDiagnostic {
|
|
15
|
+
code: string;
|
|
16
|
+
path: string;
|
|
17
|
+
scope: 'request';
|
|
18
|
+
message: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ExpressionLeafTrace {
|
|
21
|
+
path: string;
|
|
22
|
+
group: 'all' | 'any';
|
|
23
|
+
index: number;
|
|
24
|
+
field: string;
|
|
25
|
+
operator: string;
|
|
26
|
+
status: ExpressionFactStatus;
|
|
27
|
+
matched: boolean;
|
|
28
|
+
reason: 'matched' | 'not_matched' | 'absent' | 'unavailable';
|
|
29
|
+
}
|
|
30
|
+
export interface ExpressionGroupTrace {
|
|
31
|
+
path: string;
|
|
32
|
+
group: 'all' | 'any';
|
|
33
|
+
total: number;
|
|
34
|
+
matched: number;
|
|
35
|
+
passed: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface RequestExpressionResult {
|
|
38
|
+
qualified: boolean;
|
|
39
|
+
diagnostics: ExpressionDiagnostic[];
|
|
40
|
+
trace: {
|
|
41
|
+
scope: 'request';
|
|
42
|
+
empty: boolean;
|
|
43
|
+
groups: ExpressionGroupTrace[];
|
|
44
|
+
leaves: ExpressionLeafTrace[];
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Evaluate one request expression and produce the same redacted trace used for qualification. */
|
|
48
|
+
export declare function evaluateRequestExpression(rule: {
|
|
49
|
+
all?: unknown[];
|
|
50
|
+
any?: unknown[];
|
|
51
|
+
}, resolve: (field: string) => ExpressionFact): RequestExpressionResult;
|
|
52
|
+
export declare const EXPRESSION_OPERATORS: string[];
|
|
53
|
+
export declare const EXPRESSION_NUMBER_GRAMMAR: RegExp;
|
|
54
|
+
export declare const EXPRESSION_TIMESTAMP_GRAMMAR: RegExp;
|
|
55
|
+
export declare const DEVICE_CLASSES: string[];
|
|
4
56
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* if no identity or privacy denied.
|
|
11
|
-
* @returns Classification — matched audiences, segments, and experiment arm assignments
|
|
57
|
+
* Classifies the request's device class from its own headers. Returns '' when
|
|
58
|
+
* the request carried neither User-Agent nor Sec-CH-UA-Mobile, which the fact
|
|
59
|
+
* resolver reports as ABSENT — the same status the missing header itself
|
|
60
|
+
* resolves to, and deliberately distinct from the 'unknown' class, which means
|
|
61
|
+
* classified and unrecognized.
|
|
12
62
|
*/
|
|
13
|
-
export declare function
|
|
14
|
-
/**
|
|
15
|
-
* Phase 2: Decide the winning variant for a specific route.
|
|
16
|
-
*
|
|
17
|
-
* Uses the classification from Phase 1 for cheap set-membership lookups.
|
|
18
|
-
* No rule evaluation — just set membership and priority comparison.
|
|
19
|
-
*
|
|
20
|
-
* @param record The route entry (path, dispatch, variants)
|
|
21
|
-
* @param classification The visitor's classification from Phase 1
|
|
22
|
-
* @returns The decision — winning variant + experiment assignment, or nulls
|
|
23
|
-
*/
|
|
24
|
-
export declare function decide(record: EdgeRouteEntry, classification: Classification, ctx: PersonalizationRequestContext): PersonalizationDecision;
|
|
63
|
+
export declare function deviceClassFromHeaders(headers: Record<string, string>): string;
|