@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.
Files changed (143) hide show
  1. package/README.md +79 -5
  2. package/dist/astro/index.js +4 -0
  3. package/dist/audience-data-models.d.ts +12 -0
  4. package/dist/audience-data-models.js +3 -0
  5. package/dist/cli/mintlify-render.d.mts +1 -0
  6. package/dist/cli/mintlify-render.mjs +8 -0
  7. package/dist/collect/aci.js +1 -1
  8. package/dist/collect/aci.js.sha256 +1 -1
  9. package/dist/collect/api.d.ts +3 -0
  10. package/dist/collect/api.js +19 -0
  11. package/dist/collect/assignments.d.ts +7 -9
  12. package/dist/collect/assignments.js +23 -32
  13. package/dist/collect/batcher.d.ts +3 -2
  14. package/dist/collect/consent/envelope.d.ts +7 -1
  15. package/dist/collect/consent/envelope.js +3 -1
  16. package/dist/collect/consent/mapping.d.ts +2 -0
  17. package/dist/collect/consent/mapping.js +1 -0
  18. package/dist/collect/consenthold.d.ts +38 -0
  19. package/dist/collect/consenthold.js +82 -0
  20. package/dist/collect/declarative.js +7 -5
  21. package/dist/collect/gapbuffer.d.ts +13 -4
  22. package/dist/collect/gapbuffer.js +10 -4
  23. package/dist/collect/index.js +66 -19
  24. package/dist/collect/pageviews.d.ts +11 -11
  25. package/dist/collect/pageviews.js +27 -22
  26. package/dist/consent-banner/index.d.ts +17 -0
  27. package/dist/consent-banner/index.js +23 -0
  28. package/dist/content/provider.d.ts +8 -0
  29. package/dist/index.d.ts +1 -1
  30. package/dist/internal/domain/edge/identity.d.ts +5 -0
  31. package/dist/internal/domain/edge/identity.js +49 -13
  32. package/dist/internal/domain/edge/mimic_token.d.ts +1 -0
  33. package/dist/internal/domain/edge/mimic_token.js +74 -7
  34. package/dist/internal/domain/edge/personalization-core.d.ts +8 -33
  35. package/dist/internal/domain/edge/personalization-core.js +151 -195
  36. package/dist/internal/domain/edge/personalization_index.d.ts +17 -0
  37. package/dist/internal/domain/edge/personalization_index.js +56 -0
  38. package/dist/internal/domain/edge/record/codec.d.ts +1 -30
  39. package/dist/internal/domain/edge/record/codec.js +1 -156
  40. package/dist/internal/domain/edge/resolve.js +55 -38
  41. package/dist/internal/domain/edge/types.d.ts +41 -32
  42. package/dist/internal/domain/edge/variant_key.d.ts +3 -0
  43. package/dist/internal/domain/edge/variant_key.js +44 -0
  44. package/dist/internal/platform/collectionstore/errors.d.ts +13 -0
  45. package/dist/internal/platform/collectionstore/errors.js +28 -0
  46. package/dist/mintlify/blocks.d.ts +6 -0
  47. package/dist/mintlify/blocks.js +198 -0
  48. package/dist/mintlify/config.d.ts +3 -0
  49. package/dist/mintlify/config.js +77 -0
  50. package/dist/mintlify/index.d.ts +3 -0
  51. package/dist/mintlify/index.js +2 -0
  52. package/dist/mintlify/json.d.ts +8 -0
  53. package/dist/mintlify/json.js +37 -0
  54. package/dist/mintlify/materialize.d.ts +12 -0
  55. package/dist/mintlify/materialize.js +159 -0
  56. package/dist/mintlify/types.d.ts +69 -0
  57. package/dist/mintlify/types.js +1 -0
  58. package/dist/mintlify/validate.d.ts +7 -0
  59. package/dist/mintlify/validate.js +252 -0
  60. package/dist/next/assignments-navigation.d.ts +4 -0
  61. package/dist/next/assignments-navigation.js +18 -0
  62. package/dist/next/assignments-script.d.ts +3 -2
  63. package/dist/next/assignments-script.js +5 -5
  64. package/dist/next/error-seat.js +3 -0
  65. package/dist/next/index.d.ts +1 -0
  66. package/dist/next/index.js +3 -0
  67. package/dist/next/render.js +8 -1
  68. package/dist/react/VideoPlayer.d.ts +1 -1
  69. package/dist/react/index.d.ts +1 -1
  70. package/dist/registry.d.ts +4 -2
  71. package/dist/testing/conformance/checks.d.ts +2 -2
  72. package/dist/testing/conformance/checks.js +2 -0
  73. package/dist/testing/conformance/index.d.ts +1 -0
  74. package/dist/testing/conformance/index.js +1 -0
  75. package/dist/testing/conformance/next-stubs.d.ts +6 -0
  76. package/dist/testing/conformance/next-stubs.js +9 -0
  77. package/dist/testing/conformance/preset.d.ts +3 -2
  78. package/dist/testing/conformance/preset.js +12 -8
  79. package/dist/testing/conformance/root-identity.d.ts +31 -0
  80. package/dist/testing/conformance/root-identity.js +380 -0
  81. package/dist/testing/conformance/stubs/next-image.d.ts +7 -0
  82. package/dist/testing/conformance/stubs/next-image.js +5 -0
  83. package/dist/testing/conformance/stubs/next-link.d.ts +7 -0
  84. package/dist/testing/conformance/stubs/next-link.js +4 -0
  85. package/dist/testing/conformance/types.d.ts +2 -2
  86. package/dist/testing/index.js +2 -2
  87. package/dist/types/component.d.ts +9 -0
  88. package/package.json +11 -7
  89. package/src/cli/mintlify-render.mjs +9 -0
  90. package/src/types/component.ts +10 -0
  91. package/templates/astro/starter-content/src/components/chrome/Footer.astro +5 -3
  92. package/templates/astro/starter-content/src/components/chrome/Header.astro +5 -3
  93. package/templates/astro/starter-content/src/components/elements/Button.astro +5 -2
  94. package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +5 -3
  95. package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -3
  96. package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -3
  97. package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -3
  98. package/templates/astro/starter-content/src/components/sections/HomeHero.astro +5 -3
  99. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -3
  100. package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -3
  101. package/templates/astro/starter-content/src/fixtures/sections.ts +1 -1
  102. package/templates/astro/starter-content/src/stories/CompositionStoryFrame.astro +7 -2
  103. package/templates/astro/template/package.json.tmpl +4 -4
  104. package/templates/common/.agents/skills/aci/SKILL.md +50 -0
  105. package/templates/common/.agents/skills/authoring/SKILL.md +4 -3
  106. package/templates/common/.agents/skills/authoring/references/block-registry.md +15 -0
  107. package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
  108. package/templates/common/.content/schemas/site.schema.json +11 -1
  109. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/asset.json +1 -1
  110. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/asset.json +1 -1
  111. package/templates/common/starter-content/.content/pages/404/_index.json +1 -1
  112. package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +2 -2
  113. package/templates/common/starter-content/.content/pages/about/_index.json +1 -1
  114. package/templates/common/starter-content/.content/pages/error/_index.json +1 -1
  115. package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
  116. package/templates/common/starter-content/.content/patterns/components/container.json +2 -2
  117. package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
  118. package/templates/common/starter-content/.content/patterns/components/feature_grid.json +2 -2
  119. package/templates/common/starter-content/.content/patterns/components/home_hero.json +2 -2
  120. package/templates/common/starter-content/.content/patterns/components/site-footer.json +2 -2
  121. package/templates/common/starter-content/.content/patterns/components/site-navigation.json +2 -2
  122. package/templates/common/starter-content/.content/patterns/components/stats_bar.json +2 -2
  123. package/templates/common/starter-content/.content/patterns/layouts/default.json +2 -2
  124. package/templates/common/starter-content/.content/schemas/site.schema.json +11 -1
  125. package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +4 -3
  126. package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +4 -3
  127. package/templates/nextjs/starter-content/src/components/elements/Button.tsx +4 -3
  128. package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +4 -3
  129. package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +4 -2
  130. package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +4 -3
  131. package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +4 -2
  132. package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +4 -3
  133. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +4 -2
  134. package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +4 -3
  135. package/templates/astro/starter-content/src/render/RenderPage.astro +0 -67
  136. package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +0 -42
  137. package/templates/nextjs/starter-content/src/components/elements/AtomicBlock.tsx +0 -13
  138. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
  139. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +0 -0
  140. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
  141. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
  142. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +0 -0
  143. /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 and classification data.
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 (R25: consent-gated
4
- // identity model). Identity minting happens in the collect/consent endpoint,
5
- // not here. Public personalized responses may be private while providers cache
6
- // the selected immutable artifact by release and exact artifact path.
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
- // Consent-gated identity model (R25): artifact selection NEVER mints
14
- // identity. It reads cookies for classification but never writes them.
15
- // Identity (aci_id) is minted only after explicit consent — by the
16
- // collect/consent endpoint, and by the hosting edge router / framework
17
- // host on consented 200 HTML page responses (which then bypass shared
18
- // caches). Unconsented content responses stay cookie-free and cacheable
19
- // (no non-essential cookie before consent).
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 experiment bucketing.
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 rawIdentity = mimic || consent.privacyDenied
33
+ const verifiedVisitor = mimic || consent.privacyDenied ? undefined : config.verifiedVisitor;
34
+ const presentedIdentity = mimic || consent.privacyDenied
29
35
  ? null
30
- : await verifyIdentityCookie(cookies.get('aci_id') || '', config.identityKeys);
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; the EdgeRouter
33
- // never reads the customer object store to refresh or revoke them.
34
- let signedClaims = mimic ? '' : cookies.get('aci_claims') || '';
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
- ? await verifyClaims(signedClaims, config.claimsKeys || {}, now)
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 experiment = decision.experiment
59
- ? { id: decision.experiment.experimentId, arm: decision.experiment.armId, allocationVersion: decision.experiment.allocationVersion }
60
- : undefined;
61
- return { variantKey, experiment };
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
- const geo = Object.fromEntries(Object.entries({
80
- country: headers['x-vercel-ip-country'] || headers['cloudfront-viewer-country'] || '',
81
- region: headers['x-vercel-ip-country-region'] || headers['cloudfront-viewer-country-region'] || '',
82
- city: headers['x-vercel-ip-city'] || headers['cloudfront-viewer-city'] || '',
83
- }).filter(([, value]) => value !== ''));
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
- variantKey: string;
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
- experiment: PersonalizationExperimentDecision | null;
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
- experiment?: {
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,3 @@
1
+ export declare function compareUTF8(left: string, right: string): number;
2
+ export declare function canonicalVariantKey(values: Record<string, string>): string;
3
+ export declare function parseVariantKey(key: string): Record<string, 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;