@gradial/aci 0.1.52-rc.21 → 0.1.52-rc.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -5
- package/dist/astro/index.js +4 -0
- package/dist/audience-data-models.d.ts +12 -0
- package/dist/audience-data-models.js +3 -0
- package/dist/cli/mintlify-render.d.mts +1 -0
- 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 +3 -0
- package/dist/collect/api.js +19 -0
- package/dist/collect/assignments.d.ts +7 -9
- package/dist/collect/assignments.js +23 -32
- package/dist/collect/batcher.d.ts +3 -2
- 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 +10 -4
- package/dist/collect/index.js +66 -19
- 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/provider.d.ts +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/internal/domain/edge/identity.d.ts +5 -0
- package/dist/internal/domain/edge/identity.js +49 -13
- package/dist/internal/domain/edge/mimic_token.d.ts +1 -0
- package/dist/internal/domain/edge/mimic_token.js +74 -7
- package/dist/internal/domain/edge/personalization-core.d.ts +8 -33
- package/dist/internal/domain/edge/personalization-core.js +151 -195
- 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 +1 -30
- package/dist/internal/domain/edge/record/codec.js +1 -156
- package/dist/internal/domain/edge/resolve.js +55 -38
- package/dist/internal/domain/edge/types.d.ts +41 -32
- 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 +13 -0
- package/dist/internal/platform/collectionstore/errors.js +28 -0
- package/dist/mintlify/blocks.d.ts +6 -0
- package/dist/mintlify/blocks.js +198 -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 +12 -0
- package/dist/mintlify/materialize.js +159 -0
- package/dist/mintlify/types.d.ts +69 -0
- package/dist/mintlify/types.js +1 -0
- package/dist/mintlify/validate.d.ts +7 -0
- package/dist/mintlify/validate.js +252 -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-seat.js +3 -0
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +3 -0
- package/dist/next/render.js +8 -1
- package/dist/react/VideoPlayer.d.ts +1 -1
- package/dist/react/index.d.ts +1 -1
- 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 +12 -8
- package/dist/testing/conformance/root-identity.d.ts +31 -0
- package/dist/testing/conformance/root-identity.js +380 -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 +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/types/component.d.ts +9 -0
- package/package.json +11 -7
- package/src/cli/mintlify-render.mjs +9 -0
- package/src/types/component.ts +10 -0
- 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/package.json.tmpl +4 -4
- package/templates/common/.agents/skills/aci/SKILL.md +50 -0
- package/templates/common/.agents/skills/authoring/SKILL.md +4 -3
- package/templates/common/.agents/skills/authoring/references/block-registry.md +15 -0
- package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
- package/templates/common/.content/schemas/site.schema.json +11 -1
- 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 → 000000000000000000000000102}/asset.json +1 -1
- package/templates/common/starter-content/.content/pages/404/_index.json +1 -1
- package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +2 -2
- package/templates/common/starter-content/.content/pages/about/_index.json +1 -1
- package/templates/common/starter-content/.content/pages/error/_index.json +1 -1
- package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/container.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/home_hero.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/patterns/layouts/default.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/astro/starter-content/src/render/RenderPage.astro +0 -67
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +0 -42
- package/templates/nextjs/starter-content/src/components/elements/AtomicBlock.tsx +0 -13
- /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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Compact JSON codec for edge-decisioning route records
|
|
1
|
+
// Compact JSON codec for edge-decisioning route records.
|
|
2
2
|
//
|
|
3
3
|
// project(): canonical → compact (short field names via alias table, omit defaults)
|
|
4
4
|
// expand(): compact → canonical (reverse, never throws — returns INVALID sentinel on failure)
|
|
@@ -49,18 +49,6 @@ const OPERATOR_FROM_COMPACT = {
|
|
|
49
49
|
gt: 'greaterThan', ge: 'greaterThanOrEqual', lt: 'lessThan', le: 'lessThanOrEqual',
|
|
50
50
|
bf: 'before', af: 'after', bt: 'between', in: 'in',
|
|
51
51
|
};
|
|
52
|
-
const EXPERIMENT_STATUS_TO_COMPACT = {
|
|
53
|
-
running: 'r', paused: 'p', completed: 'd',
|
|
54
|
-
};
|
|
55
|
-
const EXPERIMENT_STATUS_FROM_COMPACT = {
|
|
56
|
-
r: 'running', p: 'paused', d: 'completed',
|
|
57
|
-
};
|
|
58
|
-
const EXPERIMENT_KIND_TO_COMPACT = {
|
|
59
|
-
ab: 'a', abn: 'n',
|
|
60
|
-
};
|
|
61
|
-
const EXPERIMENT_KIND_FROM_COMPACT = {
|
|
62
|
-
a: 'ab', n: 'abn',
|
|
63
|
-
};
|
|
64
52
|
// Compact type guard — reduces minified bytes vs repeated typeof+null+isArray checks
|
|
65
53
|
function isObj(v) {
|
|
66
54
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
@@ -291,149 +279,6 @@ export function expandRouteRecord(raw) {
|
|
|
291
279
|
}
|
|
292
280
|
return entry;
|
|
293
281
|
}
|
|
294
|
-
export function projectClassificationData(data) {
|
|
295
|
-
const result = {};
|
|
296
|
-
if (data.audiences && Object.keys(data.audiences).length > 0) {
|
|
297
|
-
result.au = {};
|
|
298
|
-
for (const [key, def] of Object.entries(data.audiences)) {
|
|
299
|
-
const entry = {};
|
|
300
|
-
const rule = projectRule(def.rule);
|
|
301
|
-
if (rule)
|
|
302
|
-
entry.r = rule;
|
|
303
|
-
result.au[key] = entry;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
if (data.segments && Object.keys(data.segments).length > 0) {
|
|
307
|
-
result.sg = {};
|
|
308
|
-
for (const [key, def] of Object.entries(data.segments)) {
|
|
309
|
-
const entry = {};
|
|
310
|
-
const rule = projectRule(def.rule);
|
|
311
|
-
if (rule)
|
|
312
|
-
entry.r = rule;
|
|
313
|
-
result.sg[key] = entry;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
if (data.experiments && Object.keys(data.experiments).length > 0) {
|
|
317
|
-
result.x = {};
|
|
318
|
-
for (const [key, exp] of Object.entries(data.experiments))
|
|
319
|
-
result.x[key] = projectExperiment(exp);
|
|
320
|
-
}
|
|
321
|
-
return result;
|
|
322
|
-
}
|
|
323
|
-
function projectExperiment(exp) {
|
|
324
|
-
const result = { id: exp.id };
|
|
325
|
-
if (exp.status) {
|
|
326
|
-
const c = EXPERIMENT_STATUS_TO_COMPACT[exp.status];
|
|
327
|
-
if (c)
|
|
328
|
-
result.st = c;
|
|
329
|
-
}
|
|
330
|
-
if (exp.kind) {
|
|
331
|
-
const c = EXPERIMENT_KIND_TO_COMPACT[exp.kind];
|
|
332
|
-
if (c)
|
|
333
|
-
result.k = c;
|
|
334
|
-
}
|
|
335
|
-
if (exp.allocationVersion)
|
|
336
|
-
result.av = exp.allocationVersion;
|
|
337
|
-
if (exp.salt)
|
|
338
|
-
result.sl = exp.salt;
|
|
339
|
-
if (exp.priority)
|
|
340
|
-
result.pr = exp.priority;
|
|
341
|
-
if (exp.arms && exp.arms.length > 0) {
|
|
342
|
-
result.ar = exp.arms.map((arm) => {
|
|
343
|
-
const ca = { id: arm.id };
|
|
344
|
-
if (arm.allocationBasisPoints)
|
|
345
|
-
ca.ab = arm.allocationBasisPoints;
|
|
346
|
-
if (arm.variantKey)
|
|
347
|
-
ca.vk = arm.variantKey;
|
|
348
|
-
return ca;
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
return result;
|
|
352
|
-
}
|
|
353
|
-
export function expandClassificationData(raw) {
|
|
354
|
-
if (!isObj(raw))
|
|
355
|
-
return INVALID;
|
|
356
|
-
const obj = raw;
|
|
357
|
-
const data = { audiences: {}, segments: {}, experiments: {} };
|
|
358
|
-
if (obj.au !== undefined) {
|
|
359
|
-
if (!isObj(obj.au))
|
|
360
|
-
return INVALID;
|
|
361
|
-
for (const [key, defRaw] of Object.entries(obj.au)) {
|
|
362
|
-
if (!isObj(defRaw))
|
|
363
|
-
return INVALID;
|
|
364
|
-
const ruleResult = expandRule(defRaw.r);
|
|
365
|
-
if (ruleResult === INVALID)
|
|
366
|
-
return INVALID;
|
|
367
|
-
data.audiences[key] = { rule: ruleResult || { all: [] } };
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
if (obj.sg !== undefined) {
|
|
371
|
-
if (!isObj(obj.sg))
|
|
372
|
-
return INVALID;
|
|
373
|
-
for (const [key, defRaw] of Object.entries(obj.sg)) {
|
|
374
|
-
if (!isObj(defRaw))
|
|
375
|
-
return INVALID;
|
|
376
|
-
const ruleResult = expandRule(defRaw.r);
|
|
377
|
-
if (ruleResult === INVALID)
|
|
378
|
-
return INVALID;
|
|
379
|
-
data.segments[key] = { rule: ruleResult || { all: [] } };
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (obj.x !== undefined) {
|
|
383
|
-
if (!isObj(obj.x))
|
|
384
|
-
return INVALID;
|
|
385
|
-
for (const [key, expRaw] of Object.entries(obj.x)) {
|
|
386
|
-
const expResult = expandExperiment(expRaw);
|
|
387
|
-
if (expResult === INVALID)
|
|
388
|
-
return INVALID;
|
|
389
|
-
data.experiments[key] = expResult;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return data;
|
|
393
|
-
}
|
|
394
|
-
function expandExperiment(raw) {
|
|
395
|
-
if (!isObj(raw))
|
|
396
|
-
return INVALID;
|
|
397
|
-
const obj = raw;
|
|
398
|
-
const id = obj.id;
|
|
399
|
-
if (typeof id !== 'string')
|
|
400
|
-
return INVALID;
|
|
401
|
-
const exp = { id, status: 'running', kind: 'ab', arms: [] };
|
|
402
|
-
if (typeof obj.st === 'string') {
|
|
403
|
-
const expanded = EXPERIMENT_STATUS_FROM_COMPACT[obj.st];
|
|
404
|
-
if (!expanded)
|
|
405
|
-
return INVALID;
|
|
406
|
-
exp.status = expanded;
|
|
407
|
-
}
|
|
408
|
-
if (typeof obj.k === 'string') {
|
|
409
|
-
const expanded = EXPERIMENT_KIND_FROM_COMPACT[obj.k];
|
|
410
|
-
if (!expanded)
|
|
411
|
-
return INVALID;
|
|
412
|
-
exp.kind = expanded;
|
|
413
|
-
}
|
|
414
|
-
if (typeof obj.av === 'string')
|
|
415
|
-
exp.allocationVersion = obj.av;
|
|
416
|
-
if (typeof obj.sl === 'string')
|
|
417
|
-
exp.salt = obj.sl;
|
|
418
|
-
if (typeof obj.pr === 'number')
|
|
419
|
-
exp.priority = obj.pr;
|
|
420
|
-
if (Array.isArray(obj.ar)) {
|
|
421
|
-
exp.arms = [];
|
|
422
|
-
for (const armRaw of obj.ar) {
|
|
423
|
-
if (!isObj(armRaw))
|
|
424
|
-
return INVALID;
|
|
425
|
-
const armObj = armRaw;
|
|
426
|
-
if (typeof armObj.id !== 'string')
|
|
427
|
-
return INVALID;
|
|
428
|
-
exp.arms.push({
|
|
429
|
-
id: armObj.id,
|
|
430
|
-
allocationBasisPoints: typeof armObj.ab === 'number' ? armObj.ab : 0,
|
|
431
|
-
variantKey: typeof armObj.vk === 'string' ? armObj.vk : '',
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
return exp;
|
|
436
|
-
}
|
|
437
282
|
// ── Marker (tri-state: record | marker | absent) ─────────────────────
|
|
438
283
|
export const MARKER = '{"s":1,"e":1}';
|
|
439
284
|
export function isMarker(raw) {
|
|
@@ -1,66 +1,83 @@
|
|
|
1
1
|
// resolvePersonalizationRequest — the edge decisioning entry point.
|
|
2
2
|
//
|
|
3
|
-
// Reads cookies for classification but never writes them
|
|
4
|
-
// identity
|
|
5
|
-
//
|
|
6
|
-
// the selected immutable artifact by release and
|
|
3
|
+
// Reads cookies for classification but never writes them. It may issue a
|
|
4
|
+
// first-party identity in memory when the host permits (ADR 0048); persisting
|
|
5
|
+
// that identity is the host's job. Public personalized responses may be
|
|
6
|
+
// private while providers cache the selected immutable artifact by release and
|
|
7
|
+
// exact artifact path.
|
|
7
8
|
import { resolvePersonalizationDecision, } from './personalization-core.js';
|
|
8
|
-
import { consentSignals, verifyIdentityCookie, identityHashHex, verifyClaims, parseCookieHeader, } from './identity.js';
|
|
9
|
+
import { consentSignals, verifyIdentityCookie, identityHashHex, randomIdentity, verifyClaims, parseCookieHeader, } from './identity.js';
|
|
9
10
|
export async function resolvePersonalizationRequest(request, config) {
|
|
10
11
|
const cookies = parseCookieHeader(request.headers.get('cookie'));
|
|
11
12
|
const consent = consentSignals(request, cookies);
|
|
12
13
|
const now = (config.now ?? (() => new Date()))();
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// (
|
|
14
|
+
// Identity model (ADR 0016 as amended by ADR 0048): experiment bucketing is
|
|
15
|
+
// measurement, keyed by the first-party aci_id. A verified cookie (or the
|
|
16
|
+
// provider bootstrap's identity) is used as presented. With none usable, and
|
|
17
|
+
// only when the host's issuance rule permits it for this request, resolve
|
|
18
|
+
// issues a fresh identity so the request that issues it is also bucketed —
|
|
19
|
+
// an unverifiable cookie is replaced the same way. Persisting the issued
|
|
20
|
+
// identity is the host's decision (page responses only, never shared-cached).
|
|
21
|
+
// Claims and memberships stay gated on the personalization grant.
|
|
20
22
|
//
|
|
21
|
-
// GPC/DNT deny fully: no read, no identity, no
|
|
23
|
+
// GPC/DNT deny fully: no read, no identity, no issuance, no bucketing.
|
|
22
24
|
//
|
|
23
25
|
// Mimic (specs/features/audience-mimic): a validated mimicClaims input
|
|
24
26
|
// bypasses identity resolution entirely — the session reads no aci_id and
|
|
25
27
|
// no aci_claims (D-2/D-7), so identityHash stays empty and the experiment
|
|
26
|
-
// tier is structurally skipped (D-5).
|
|
28
|
+
// tier is structurally skipped (D-5). An ARMS-bearing token is the D-17
|
|
29
|
+
// exception: the experiment tier runs in forced mode — the token's named
|
|
30
|
+
// arm replaces the dice for each experiment the request selects through
|
|
31
|
+
// the ordinary gate — and identity still never exists (I-17).
|
|
27
32
|
const mimic = config.mimicClaims;
|
|
28
|
-
const
|
|
33
|
+
const verifiedVisitor = mimic || consent.privacyDenied ? undefined : config.verifiedVisitor;
|
|
34
|
+
const presentedIdentity = mimic || consent.privacyDenied
|
|
29
35
|
? null
|
|
30
|
-
:
|
|
36
|
+
: verifiedVisitor
|
|
37
|
+
? verifiedVisitor.rawIdentity
|
|
38
|
+
: await verifyIdentityCookie(cookies.get('aci_id') || '', config.identityKeys);
|
|
39
|
+
const issuedIdentity = !presentedIdentity && !mimic && !consent.privacyDenied && config.issueIdentity
|
|
40
|
+
? randomIdentity(config.randomBytes)
|
|
41
|
+
: undefined;
|
|
42
|
+
const rawIdentity = presentedIdentity || issuedIdentity || null;
|
|
31
43
|
// The identity hash feeds experiment bucketing for identified visitors.
|
|
32
|
-
// Claims are a signed cookie written by /collect
|
|
33
|
-
// never reads the customer object store to
|
|
34
|
-
|
|
44
|
+
// Claims are a signed cookie written by /collect or the provider identity
|
|
45
|
+
// bootstrap; the EdgeRouter never reads the customer object store to
|
|
46
|
+
// refresh or revoke them.
|
|
35
47
|
let identityHash = '';
|
|
36
48
|
if (rawIdentity) {
|
|
37
49
|
identityHash = await identityHashHex('aci_id', rawIdentity);
|
|
38
50
|
}
|
|
39
|
-
if (rawIdentity && consent.allowsIdentityTier && signedClaims) {
|
|
40
|
-
if (!await verifyClaims(signedClaims, config.claimsKeys || {}, now)) {
|
|
41
|
-
signedClaims = '';
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
51
|
const matchedRoute = config.route;
|
|
45
52
|
let variantKey = '';
|
|
46
53
|
if (matchedRoute) {
|
|
47
54
|
const claims = mimic
|
|
48
55
|
? { aud: mimic.aud, seg: mimic.seg, iat: 0, exp: 0 }
|
|
49
56
|
: consent.allowsIdentityTier
|
|
50
|
-
?
|
|
57
|
+
? verifiedVisitor
|
|
58
|
+
? verifiedVisitor.claims
|
|
59
|
+
: await verifyClaims(cookies.get('aci_claims') || '', config.claimsKeys || {}, now)
|
|
51
60
|
: null;
|
|
52
|
-
const decision = resolvePersonalizationDecision(toPersonalizationRequestContext(request, now, config.onRuleRejected), matchedRoute, claims, config.experiments, identityHash
|
|
61
|
+
const decision = resolvePersonalizationDecision(toPersonalizationRequestContext(request, now, config.requestGeo, config.onRuleRejected), matchedRoute, claims, config.experiments, identityHash
|
|
53
62
|
? { usable: true, siteId: config.siteId, namespace: 'aci_id', identityHash }
|
|
54
|
-
: undefined);
|
|
63
|
+
: undefined, mimic?.arms);
|
|
55
64
|
if (decision.variant) {
|
|
56
65
|
variantKey = decision.variant.variantKey;
|
|
57
66
|
}
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
:
|
|
61
|
-
|
|
67
|
+
const experiments = decision.experiments.map((item) => ({
|
|
68
|
+
id: item.experimentId,
|
|
69
|
+
arm: item.armId,
|
|
70
|
+
allocationVersion: item.allocationVersion,
|
|
71
|
+
...(item.forced ? { forced: true } : {}),
|
|
72
|
+
}));
|
|
73
|
+
return {
|
|
74
|
+
variantKey,
|
|
75
|
+
experiments,
|
|
76
|
+
...(decision.legacyExperimentExclusions ? { legacyExperimentExclusions: decision.legacyExperimentExclusions } : {}),
|
|
77
|
+
...(issuedIdentity ? { issuedIdentity } : {}),
|
|
78
|
+
};
|
|
62
79
|
}
|
|
63
|
-
return { variantKey };
|
|
80
|
+
return { variantKey, experiments: [], ...(issuedIdentity ? { issuedIdentity } : {}) };
|
|
64
81
|
}
|
|
65
82
|
// The return type is annotated rather than inferred so the contract is stated
|
|
66
83
|
// at the construction site: a misspelled property would then silently never
|
|
@@ -68,7 +85,7 @@ export async function resolvePersonalizationRequest(request, config) {
|
|
|
68
85
|
// make visible. No tsconfig covers internal/domain/** today (esbuild/tsx strip
|
|
69
86
|
// types without checking), so the annotation is documentation now and becomes
|
|
70
87
|
// real protection under any future typecheck of these trees.
|
|
71
|
-
function toPersonalizationRequestContext(request, now, onRuleRejected) {
|
|
88
|
+
function toPersonalizationRequestContext(request, now, requestGeo, onRuleRejected) {
|
|
72
89
|
const url = new URL(request.url);
|
|
73
90
|
const cookies = parseCookieHeader(request.headers.get('cookie'));
|
|
74
91
|
const query = {};
|
|
@@ -76,11 +93,11 @@ function toPersonalizationRequestContext(request, now, onRuleRejected) {
|
|
|
76
93
|
(query[key] ||= []).push(value);
|
|
77
94
|
});
|
|
78
95
|
const headers = Object.fromEntries(request.headers.entries());
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
// geo.* facts come only from the host's trusted geo. Reading provider geo
|
|
97
|
+
// headers here trusted whatever the viewer sent on every lane whose
|
|
98
|
+
// provider does not stamp that exact header before the decision runs.
|
|
99
|
+
const geo = Object.fromEntries(Object.entries(requestGeo ?? {})
|
|
100
|
+
.filter((entry) => typeof entry[1] === 'string' && entry[1] !== ''));
|
|
84
101
|
return {
|
|
85
102
|
path: url.pathname,
|
|
86
103
|
query,
|
|
@@ -73,6 +73,8 @@ export interface PersonalizationRequestContext {
|
|
|
73
73
|
onRuleRejected?: (event: RuleRejectionEvent) => void;
|
|
74
74
|
}
|
|
75
75
|
export interface RuleRejectionEvent {
|
|
76
|
+
experimentId?: string;
|
|
77
|
+
armId?: string;
|
|
76
78
|
variantKey: string;
|
|
77
79
|
diagnosticCode: string;
|
|
78
80
|
diagnosticPath: string;
|
|
@@ -96,10 +98,18 @@ export interface PersonalizationExperiment {
|
|
|
96
98
|
allocationVersion?: string;
|
|
97
99
|
salt?: string;
|
|
98
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';
|
|
99
108
|
arms: Array<{
|
|
100
109
|
id: string;
|
|
101
110
|
allocationBasisPoints?: number;
|
|
102
|
-
|
|
111
|
+
/** DEBT(edge-functions): decode old arm keys until every retained tenant release passes the ADR-0045 removal inventory. */
|
|
112
|
+
variantKey?: string;
|
|
103
113
|
}>;
|
|
104
114
|
}
|
|
105
115
|
export interface PersonalizationIdentity {
|
|
@@ -114,24 +124,14 @@ export interface PersonalizationExperimentDecision {
|
|
|
114
124
|
allocationVersion: string;
|
|
115
125
|
variantKey: string;
|
|
116
126
|
bucket: number;
|
|
127
|
+
/** audience-mimic design/09: the token's assertion replaced the dice. */
|
|
128
|
+
forced?: boolean;
|
|
117
129
|
}
|
|
118
130
|
export interface PersonalizationDecision {
|
|
131
|
+
/** ADR-0045: number of route experiments excluded as legacy pair-only entries. */
|
|
132
|
+
legacyExperimentExclusions?: number;
|
|
119
133
|
variant: PersonalizationVariant | null;
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
export interface ClassificationData {
|
|
123
|
-
audiences: Record<string, {
|
|
124
|
-
rule: EdgeRuleExpression;
|
|
125
|
-
}>;
|
|
126
|
-
segments: Record<string, {
|
|
127
|
-
rule: EdgeRuleExpression;
|
|
128
|
-
}>;
|
|
129
|
-
experiments: Record<string, PersonalizationExperiment>;
|
|
130
|
-
}
|
|
131
|
-
export interface Classification {
|
|
132
|
-
matchedAudiences: string[];
|
|
133
|
-
matchedSegments: string[];
|
|
134
|
-
experimentArms: Record<string, PersonalizationExperimentDecision>;
|
|
134
|
+
experiments: PersonalizationExperimentDecision[];
|
|
135
135
|
}
|
|
136
136
|
export interface SigningKey {
|
|
137
137
|
kid: string;
|
|
@@ -166,20 +166,6 @@ export interface ConsentSignals {
|
|
|
166
166
|
allowsIdentityTier: boolean;
|
|
167
167
|
privacyDenied: boolean;
|
|
168
168
|
}
|
|
169
|
-
export interface PersonalizationEnvelopeWrite {
|
|
170
|
-
key: string;
|
|
171
|
-
envelopeId: string;
|
|
172
|
-
siteId: string;
|
|
173
|
-
identityNamespace: string;
|
|
174
|
-
identityHash: string;
|
|
175
|
-
records: readonly unknown[];
|
|
176
|
-
digest: string;
|
|
177
|
-
}
|
|
178
|
-
export interface PersonalizationCollectionStore {
|
|
179
|
-
readClaims(siteId: string, namespace: string, identityHash: string, now?: Date): Promise<string | null>;
|
|
180
|
-
appendEnvelope(write: PersonalizationEnvelopeWrite): Promise<void>;
|
|
181
|
-
putSuppression(key: string, record: unknown): Promise<void>;
|
|
182
|
-
}
|
|
183
169
|
export interface PersonalizationHostConfig {
|
|
184
170
|
siteId: string;
|
|
185
171
|
route: EdgeRouteEntry | null;
|
|
@@ -190,9 +176,20 @@ export interface PersonalizationHostConfig {
|
|
|
190
176
|
secureCookies?: boolean;
|
|
191
177
|
now?: () => Date;
|
|
192
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;
|
|
193
189
|
mimicClaims?: {
|
|
194
190
|
aud?: string[];
|
|
195
191
|
seg?: string[];
|
|
192
|
+
arms?: Record<string, string>;
|
|
196
193
|
};
|
|
197
194
|
/**
|
|
198
195
|
* Passed straight through to the request context's rejection sink so a host
|
|
@@ -202,13 +199,25 @@ export interface PersonalizationHostConfig {
|
|
|
202
199
|
*/
|
|
203
200
|
onRuleRejected?: (event: RuleRejectionEvent) => void;
|
|
204
201
|
}
|
|
202
|
+
export interface ClaimsPayload {
|
|
203
|
+
aud?: string[];
|
|
204
|
+
seg?: string[];
|
|
205
|
+
iat: number;
|
|
206
|
+
exp: number;
|
|
207
|
+
}
|
|
205
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;
|
|
206
213
|
variantKey: string;
|
|
207
|
-
|
|
214
|
+
experiments: Array<{
|
|
208
215
|
id: string;
|
|
209
216
|
arm: string;
|
|
210
217
|
allocationVersion: string;
|
|
211
|
-
|
|
218
|
+
/** audience-mimic design/09: the assignment came from the token, not the dice. */
|
|
219
|
+
forced?: boolean;
|
|
220
|
+
}>;
|
|
212
221
|
}
|
|
213
222
|
export interface CompositionDescriptor {
|
|
214
223
|
layoutArtifact: string;
|
|
@@ -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,16 @@ 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
|
+
}
|
|
@@ -5,3 +5,31 @@ 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
|
+
}
|
|
@@ -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;
|