@gradial/aci 0.1.52-rc.8 → 0.1.52
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 +126 -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 -197
- package/dist/audience-data-models.js +313 -51
- package/dist/cli/compile-registry.mjs +6 -34
- package/dist/cli/mintlify-render.mjs +8 -0
- 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 +78 -21
- 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 +24 -2
- package/dist/content/resolve-slots.js +127 -21
- package/dist/content/types.d.ts +5 -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/identity.d.ts +47 -4
- package/dist/internal/domain/edge/identity.js +130 -26
- package/dist/internal/domain/edge/index.d.ts +1 -1
- package/dist/internal/domain/edge/index.js +1 -1
- 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 +790 -200
- package/dist/internal/domain/edge/personalization_index.d.ts +17 -0
- package/dist/internal/domain/edge/personalization_index.js +55 -0
- 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 +82 -78
- package/dist/internal/domain/edge/types.js +3 -3
- package/dist/internal/domain/edge/variant_key.d.ts +4 -0
- package/dist/internal/domain/edge/variant_key.js +56 -0
- package/dist/internal/platform/collectionstore/errors.d.ts +24 -0
- package/dist/internal/platform/collectionstore/errors.js +48 -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/index.d.ts +1 -1
- package/dist/next/index.js +3 -1
- package/dist/next/page.d.ts +2 -1
- package/dist/next/page.js +15 -4
- package/dist/next/release-context.d.ts +30 -0
- package/dist/next/release-context.js +56 -0
- package/dist/next/render.d.ts +1 -1
- package/dist/next/render.js +15 -5
- package/dist/next/server.d.ts +9 -3
- package/dist/next/server.js +17 -32
- 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/index.d.ts +3 -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.js +2 -2
- package/dist/types/component.d.ts +9 -0
- package/dist/types/config.d.ts +0 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/page.d.ts +0 -1
- package/migrations/conformance-registry.md +22 -0
- package/migrations/experiment-goal-ids.md +43 -0
- package/package.json +21 -7
- package/src/cli/compile-registry.mjs +6 -30
- package/src/cli/mintlify-render.mjs +9 -0
- package/src/types/block-ref.test.ts +16 -1
- package/src/types/block-ref.ts +2 -2
- package/src/types/component.ts +10 -0
- package/src/types/config.ts +0 -1
- package/src/types/index.ts +0 -1
- package/src/types/page.ts +0 -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/sections.ts +1 -1
- 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 +194 -42
- package/templates/common/.agents/skills/authoring/references/block-registry.md +19 -3
- 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 +216 -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/_index.json +0 -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/config/site.json +1 -1
- package/templates/common/starter-content/.content/dam/README.md +74 -0
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/asset.json +1 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/asset.json +1 -1
- package/templates/common/starter-content/.content/pages/404/_index.json +1 -2
- package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +14 -3
- package/templates/common/starter-content/.content/pages/about/_index.json +2 -2
- package/templates/common/starter-content/.content/pages/error/_index.json +1 -2
- package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/container.json +8 -3
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +5 -3
- package/templates/common/starter-content/.content/patterns/components/home_hero.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/site-footer.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/site-navigation.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/stats_bar.json +2 -2
- 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 +0 -1
- 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/internal/domain/edge/compose.d.ts +0 -24
- package/dist/internal/domain/edge/compose.js +0 -89
- package/dist/internal/domain/edge/record/codec.d.ts +0 -71
- package/dist/internal/domain/edge/record/codec.js +0 -401
- package/dist/next/collect-route.d.ts +0 -19
- package/dist/next/collect-route.js +0 -459
- package/dist/next/edge-config.d.ts +0 -1
- package/dist/next/edge-config.js +0 -92
- 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-edge-projection-v1.d.ts +0 -24
- package/dist/next/vercel-edge-projection-v1.js +0 -880
- 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/common/starter-content/.content/patterns/layouts/default.json +0 -14
- 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/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_about_hero → 000000000000000000000000101}/governance.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_home_hero → 000000000000000000000000102}/governance.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
// - Consent signal extraction (GPC/DNT + portable consent envelope)
|
|
6
6
|
// - Identity cookie verification and minting (aci_id)
|
|
7
7
|
// - Claims cookie verification (aci_claims)
|
|
8
|
-
// - Cookie mutation helpers
|
|
9
8
|
//
|
|
10
9
|
// Shared by every static edge adapter; this logic has no SDK host wrapper.
|
|
11
|
-
// internal/domain/audience/runtime/
|
|
10
|
+
// internal/domain/audience/runtime/collect.ts imports from here as well.
|
|
12
11
|
const encoder = new TextEncoder();
|
|
13
12
|
const decoder = new TextDecoder();
|
|
14
13
|
// ── Constants ──
|
|
@@ -59,6 +58,20 @@ export function consentSignals(request, cookies) {
|
|
|
59
58
|
privacyDenied,
|
|
60
59
|
};
|
|
61
60
|
}
|
|
61
|
+
// identityIssuancePermitted is the one rule for issuing a first-party identity
|
|
62
|
+
// on a request that carries none (ADR 0048). Privacy signals deny outright. A
|
|
63
|
+
// recorded consent choice — any envelope, even a malformed one — decides by
|
|
64
|
+
// itself, so an explicit denial holds in an opt-out country. Only a visitor
|
|
65
|
+
// with no recorded choice falls back to the release's opt-out countries, and
|
|
66
|
+
// an unknown country is never one of them. Personalization uses of the
|
|
67
|
+
// identity (claims, memberships, provider bootstrap) stay gated on the grant.
|
|
68
|
+
export function identityIssuancePermitted(consent, country, optOutCountries) {
|
|
69
|
+
if (consent.privacyDenied)
|
|
70
|
+
return false;
|
|
71
|
+
if (consent.hasConsent || consent.consentInvalid)
|
|
72
|
+
return consent.allowsIdentityTier;
|
|
73
|
+
return country !== '' && optOutCountries.includes(country);
|
|
74
|
+
}
|
|
62
75
|
export function parseConsent(raw, decodeCookieValue = true) {
|
|
63
76
|
let decoded = trimConsentWhitespace(raw);
|
|
64
77
|
if (!decoded)
|
|
@@ -127,19 +140,46 @@ export function randomIdentity(random) {
|
|
|
127
140
|
}
|
|
128
141
|
export async function identityHashHex(namespace, value) {
|
|
129
142
|
const digest = await crypto.subtle.digest('SHA-256', encoder.encode(`${namespace.trim().toLowerCase()}\x00${value.trim()}`));
|
|
130
|
-
return
|
|
143
|
+
return bytesToHex(new Uint8Array(digest));
|
|
131
144
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
function bytesToHex(bytes) {
|
|
146
|
+
let value = '';
|
|
147
|
+
for (const byte of bytes)
|
|
148
|
+
value += byte.toString(16).padStart(2, '0');
|
|
149
|
+
return value;
|
|
135
150
|
}
|
|
136
151
|
// ── Claims cookie (aci_claims) ──
|
|
152
|
+
// verifyClaims is the PERSONALIZATION grade: signature, expiry, and the
|
|
153
|
+
// ADR 0059 §6 issuer-marker gate — a claim without a fresh marker is never
|
|
154
|
+
// trusted for membership selection. Existing signed cookies expire by their
|
|
155
|
+
// own TTL; their old contents are not read during the transition.
|
|
137
156
|
export async function verifyClaims(value, keys, now) {
|
|
157
|
+
const payload = await verifyClaimsAuthenticity(value, keys, now);
|
|
158
|
+
if (payload === null || claimsIssuer(payload) === undefined)
|
|
159
|
+
return null;
|
|
160
|
+
return payload;
|
|
161
|
+
}
|
|
162
|
+
// verifyClaimsEvidence is the EVIDENCE grade (anonymous-experiments REQ-6):
|
|
163
|
+
// signature and expiry WITHOUT the issuer gate. The collector's
|
|
164
|
+
// withdrawal/suppression trigger only needs proof that identity-tier state
|
|
165
|
+
// existed — an authentically signed, unexpired pre-cutover (unmarked) cookie
|
|
166
|
+
// is exactly that proof, and the Go collector accepts it the same way (Go
|
|
167
|
+
// ClaimsVerifier.VerifyClaims has no marker gate). Evidence never feeds
|
|
168
|
+
// membership selection, so the §6 gate does not apply here.
|
|
169
|
+
export async function verifyClaimsEvidence(value, keys, now) {
|
|
170
|
+
return verifyClaimsAuthenticity(value, keys, now);
|
|
171
|
+
}
|
|
172
|
+
// verifyClaimsAuthenticity is the shared core: envelope, keyed signature,
|
|
173
|
+
// and expiry — no policy gate. Every structural failure degrades to null
|
|
174
|
+
// (never an error), matching Go's no-oracle posture.
|
|
175
|
+
async function verifyClaimsAuthenticity(value, keys, now) {
|
|
138
176
|
const parts = value.trim().split('.');
|
|
139
177
|
if (parts.length !== 4 || parts[0] !== 'v1')
|
|
140
178
|
return null;
|
|
141
179
|
const [, kid, body, signature] = parts;
|
|
142
|
-
|
|
180
|
+
// The kid is viewer-controlled: select only an owned key, never an inherited
|
|
181
|
+
// Object.prototype member such as `__proto__` or `constructor`.
|
|
182
|
+
const secret = Object.hasOwn(keys, kid) ? keys[kid] : undefined;
|
|
143
183
|
if (!secret || !await verifyHMAC(`v1.${kid}.${body}`, signature, secret))
|
|
144
184
|
return null;
|
|
145
185
|
try {
|
|
@@ -152,38 +192,102 @@ export async function verifyClaims(value, keys, now) {
|
|
|
152
192
|
return null;
|
|
153
193
|
}
|
|
154
194
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Exposes the payload's `iss` marker. Malformed/unknown markers — and
|
|
197
|
+
* pre-0059 unmarked payloads — yield undefined; since the §6 cutover,
|
|
198
|
+
* verifyClaims rejects those payloads outright, so a verified payload always
|
|
199
|
+
* carries a marker. The marker identifies the minting path; it does not
|
|
200
|
+
* grant membership by itself.
|
|
201
|
+
*/
|
|
202
|
+
export function claimsIssuer(payload) {
|
|
203
|
+
const iss = payload?.iss;
|
|
204
|
+
return iss === 'aci' || iss === 'edge' ? iss : undefined;
|
|
162
205
|
}
|
|
163
|
-
|
|
164
|
-
|
|
206
|
+
/** audience.MaxClaimsCookieBytes — the hard 1 KiB aci_claims budget. */
|
|
207
|
+
export const MAX_CLAIMS_COOKIE_BYTES = 1024;
|
|
208
|
+
/** audience.DefaultClaimsTTL — the 24-hour signed-lifetime ceiling. */
|
|
209
|
+
export const DEFAULT_CLAIMS_TTL_SECONDS = 24 * 60 * 60;
|
|
210
|
+
/**
|
|
211
|
+
* Mint rejection: oversize, a malformed key, or a missing issuer marker.
|
|
212
|
+
* Callers fail closed on it — the edge never truncates or subsets
|
|
213
|
+
* memberships to fit (ADR 0059 §2/§3).
|
|
214
|
+
*/
|
|
215
|
+
export class ClaimsMintError extends Error {
|
|
216
|
+
name = 'ClaimsMintError';
|
|
165
217
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Mints one signed aci_claims value: v1.{kid}.{base64url(payload)}.{hmac},
|
|
220
|
+
* byte-identical to Go's ClaimsIssuer.IssueClaims — payload field order is
|
|
221
|
+
* aud (omitted when empty), seg (omitted when empty), iat, exp, iss, and the
|
|
222
|
+
* TTL clamps to the 24-hour ceiling. The exact bytes are pinned against the
|
|
223
|
+
* Go golden in claims_contract.test.ts. Issuance requires an explicit issuer
|
|
224
|
+
* marker; the marker identifies the minting path, it does not grant
|
|
225
|
+
* membership by itself.
|
|
226
|
+
*/
|
|
227
|
+
export async function issueClaims(memberships, issuer, key, now, ttlSeconds) {
|
|
228
|
+
const kid = key.kid.trim();
|
|
229
|
+
if (!key.secret)
|
|
230
|
+
throw new ClaimsMintError('claims issuer requires a secret');
|
|
231
|
+
if (!kid)
|
|
232
|
+
throw new ClaimsMintError('claims issuer requires a kid');
|
|
233
|
+
if (kid.includes('.'))
|
|
234
|
+
throw new ClaimsMintError("claims kid must not contain '.'");
|
|
235
|
+
if (issuer !== 'aci' && issuer !== 'edge') {
|
|
236
|
+
throw new ClaimsMintError('claims issuer requires an explicit issuer marker');
|
|
171
237
|
}
|
|
172
|
-
|
|
173
|
-
|
|
238
|
+
const ttl = ttlSeconds === undefined || ttlSeconds <= 0 || ttlSeconds > DEFAULT_CLAIMS_TTL_SECONDS
|
|
239
|
+
? DEFAULT_CLAIMS_TTL_SECONDS
|
|
240
|
+
: Math.floor(ttlSeconds);
|
|
241
|
+
const iat = Math.floor(now.getTime() / 1000);
|
|
242
|
+
const body = JSON.stringify({
|
|
243
|
+
...(memberships.aud?.length ? { aud: memberships.aud } : {}),
|
|
244
|
+
...(memberships.seg?.length ? { seg: memberships.seg } : {}),
|
|
245
|
+
iat,
|
|
246
|
+
exp: iat + ttl,
|
|
247
|
+
iss: issuer,
|
|
248
|
+
});
|
|
249
|
+
const encoded = base64URLEncode(encoder.encode(body));
|
|
250
|
+
const value = `v1.${kid}.${encoded}.${await hmacSHA256(`v1.${kid}.${encoded}`, key.secret)}`;
|
|
251
|
+
if (encoder.encode(value).byteLength > MAX_CLAIMS_COOKIE_BYTES) {
|
|
252
|
+
throw new ClaimsMintError(`claims cookie exceeds the ${MAX_CLAIMS_COOKIE_BYTES}-byte budget (aud=${memberships.aud?.length ?? 0} seg=${memberships.seg?.length ?? 0})`);
|
|
174
253
|
}
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
// ── Internal crypto helpers ──
|
|
257
|
+
// verifyHMAC returns a verdict about the signature, which is the only visitor
|
|
258
|
+
// input here: a malformed or wrong signature is `false`. A fault importing the
|
|
259
|
+
// key or computing the MAC — an empty or misconfigured secret, a WebCrypto
|
|
260
|
+
// failure — is not a verdict and propagates to the caller's documented failure
|
|
261
|
+
// boundary (hosting D-14's logged private 503, the collect handler's observed
|
|
262
|
+
// 500). Folding it into `false` would silently read every visitor as
|
|
263
|
+
// unidentified.
|
|
264
|
+
export async function verifyHMAC(value, signature, secret) {
|
|
265
|
+
const signatureBytes = decodeSignature(signature);
|
|
266
|
+
if (!signatureBytes)
|
|
267
|
+
return false;
|
|
268
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
269
|
+
return await crypto.subtle.verify('HMAC', key, signatureBytes, encoder.encode(value));
|
|
270
|
+
}
|
|
271
|
+
// Signatures are unpadded base64url, the only encoding the Go signers emit. A
|
|
272
|
+
// value outside that grammar (or of a length no base64 string can have) is
|
|
273
|
+
// malformed input; within it, decoding cannot fail.
|
|
274
|
+
const BASE64URL_SIGNATURE = /^[A-Za-z0-9_-]+$/;
|
|
275
|
+
function decodeSignature(signature) {
|
|
276
|
+
if (!BASE64URL_SIGNATURE.test(signature) || signature.length % 4 === 1)
|
|
277
|
+
return null;
|
|
278
|
+
return base64URLDecode(signature);
|
|
175
279
|
}
|
|
176
|
-
async function hmacSHA256(value, secret) {
|
|
280
|
+
export async function hmacSHA256(value, secret) {
|
|
177
281
|
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
178
282
|
return base64URLEncode(new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(value))));
|
|
179
283
|
}
|
|
180
|
-
function base64URLEncode(bytes) {
|
|
284
|
+
export function base64URLEncode(bytes) {
|
|
181
285
|
let binary = '';
|
|
182
286
|
for (const byte of bytes)
|
|
183
287
|
binary += String.fromCharCode(byte);
|
|
184
288
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
185
289
|
}
|
|
186
|
-
function base64URLDecode(value) {
|
|
290
|
+
export function base64URLDecode(value) {
|
|
187
291
|
const padded = value.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - value.length % 4) % 4);
|
|
188
292
|
const binary = atob(padded);
|
|
189
293
|
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;
|