@gradial/aci 0.1.52-rc.3 → 0.1.52-rc.34-e2e.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +86 -5
  3. package/dist/astro/error-page.js +1 -0
  4. package/dist/astro/index.js +4 -0
  5. package/dist/astro/page.d.ts +5 -2
  6. package/dist/astro/page.js +35 -23
  7. package/dist/astro/preview.js +2 -17
  8. package/dist/audience-data-models.d.ts +308 -194
  9. package/dist/audience-data-models.js +314 -51
  10. package/dist/cli/compile-registry.d.mts +5 -1
  11. package/dist/cli/compile-registry.mjs +31 -46
  12. package/dist/cli/mintlify-render.mjs +8 -0
  13. package/dist/cli/validate-content.d.mts +31 -1
  14. package/dist/cli/validate-content.mjs +50 -12
  15. package/dist/collect/aci.js +1 -1
  16. package/dist/collect/aci.js.sha256 +1 -1
  17. package/dist/collect/api.d.ts +6 -7
  18. package/dist/collect/api.js +21 -8
  19. package/dist/collect/assignments.d.ts +7 -9
  20. package/dist/collect/assignments.js +23 -32
  21. package/dist/collect/batcher.d.ts +9 -7
  22. package/dist/collect/batcher.js +6 -5
  23. package/dist/collect/consent/envelope.d.ts +7 -1
  24. package/dist/collect/consent/envelope.js +3 -1
  25. package/dist/collect/consent/mapping.d.ts +2 -0
  26. package/dist/collect/consent/mapping.js +1 -0
  27. package/dist/collect/consenthold.d.ts +38 -0
  28. package/dist/collect/consenthold.js +82 -0
  29. package/dist/collect/declarative.js +7 -5
  30. package/dist/collect/gapbuffer.d.ts +13 -4
  31. package/dist/collect/gapbuffer.js +21 -10
  32. package/dist/collect/index.js +67 -20
  33. package/dist/collect/pageviews.d.ts +11 -11
  34. package/dist/collect/pageviews.js +27 -22
  35. package/dist/consent-banner/index.d.ts +17 -0
  36. package/dist/consent-banner/index.js +23 -0
  37. package/dist/content/index.d.ts +2 -1
  38. package/dist/content/index.js +4 -1
  39. package/dist/content/provider.d.ts +16 -2
  40. package/dist/content/provider.js +5 -1
  41. package/dist/content/resolve-slots.d.ts +33 -4
  42. package/dist/content/resolve-slots.js +127 -19
  43. package/dist/content/types.d.ts +7 -1
  44. package/dist/edge/expression-capabilities.v1.d.ts +106 -0
  45. package/dist/edge/expression-capabilities.v1.js +165 -0
  46. package/dist/index.d.ts +1 -2
  47. package/dist/index.js +0 -1
  48. package/dist/internal/domain/edge/compose.d.ts +24 -0
  49. package/dist/internal/domain/edge/compose.js +89 -0
  50. package/dist/internal/domain/edge/identity.d.ts +5 -0
  51. package/dist/internal/domain/edge/identity.js +50 -14
  52. package/dist/internal/domain/edge/index.d.ts +2 -0
  53. package/dist/internal/domain/edge/index.js +2 -0
  54. package/dist/internal/domain/edge/mimic_token.d.ts +14 -0
  55. package/dist/internal/domain/edge/mimic_token.js +125 -0
  56. package/dist/internal/domain/edge/personalization-core.d.ts +61 -22
  57. package/dist/internal/domain/edge/personalization-core.js +789 -198
  58. package/dist/internal/domain/edge/personalization_index.d.ts +17 -0
  59. package/dist/internal/domain/edge/personalization_index.js +56 -0
  60. package/dist/internal/domain/edge/record/codec.d.ts +6 -32
  61. package/dist/internal/domain/edge/record/codec.js +86 -168
  62. package/dist/internal/domain/edge/resolve.js +76 -39
  63. package/dist/internal/domain/edge/testdata/flat_only_validator_v1.d.ts +10 -0
  64. package/dist/internal/domain/edge/testdata/flat_only_validator_v1.js +253 -0
  65. package/dist/internal/domain/edge/types.d.ts +93 -41
  66. package/dist/internal/domain/edge/types.js +3 -3
  67. package/dist/internal/domain/edge/variant_key.d.ts +3 -0
  68. package/dist/internal/domain/edge/variant_key.js +44 -0
  69. package/dist/internal/platform/collectionstore/errors.d.ts +24 -0
  70. package/dist/internal/platform/collectionstore/errors.js +48 -0
  71. package/dist/mimic/core.d.ts +2 -0
  72. package/dist/mimic/core.js +31 -0
  73. package/dist/mintlify/blocks.d.ts +6 -0
  74. package/dist/mintlify/blocks.js +201 -0
  75. package/dist/mintlify/config.d.ts +3 -0
  76. package/dist/mintlify/config.js +77 -0
  77. package/dist/mintlify/index.d.ts +3 -0
  78. package/dist/mintlify/index.js +2 -0
  79. package/dist/mintlify/json.d.ts +8 -0
  80. package/dist/mintlify/json.js +37 -0
  81. package/dist/mintlify/materialize.d.ts +14 -0
  82. package/dist/mintlify/materialize.js +254 -0
  83. package/dist/mintlify/types.d.ts +74 -0
  84. package/dist/mintlify/types.js +1 -0
  85. package/dist/mintlify/validate.d.ts +7 -0
  86. package/dist/mintlify/validate.js +256 -0
  87. package/dist/next/assignments-navigation.d.ts +4 -0
  88. package/dist/next/assignments-navigation.js +18 -0
  89. package/dist/next/assignments-script.d.ts +3 -2
  90. package/dist/next/assignments-script.js +5 -5
  91. package/dist/next/error-pages.d.ts +4 -0
  92. package/dist/next/error-pages.js +4 -0
  93. package/dist/next/error-seat-payload.d.ts +2 -1
  94. package/dist/next/error-seat-payload.js +14 -24
  95. package/dist/next/error-seat.js +7 -2
  96. package/dist/next/hosted-page-target.d.ts +7 -0
  97. package/dist/next/hosted-page-target.js +39 -0
  98. package/dist/next/index.d.ts +1 -1
  99. package/dist/next/index.js +3 -1
  100. package/dist/next/page.d.ts +29 -6
  101. package/dist/next/page.js +47 -25
  102. package/dist/next/release-context.d.ts +29 -0
  103. package/dist/next/release-context.js +55 -0
  104. package/dist/next/render.d.ts +1 -1
  105. package/dist/next/render.js +15 -5
  106. package/dist/next/server.d.ts +8 -0
  107. package/dist/next/server.js +44 -3
  108. package/dist/next/vercel-edge-projection-v1.d.ts +0 -9
  109. package/dist/next/vercel-edge-projection-v1.js +9 -539
  110. package/dist/preview/core.d.ts +1 -0
  111. package/dist/preview/core.js +27 -0
  112. package/dist/react/VideoPlayer.d.ts +3 -3
  113. package/dist/react/VideoPlayer.js +2 -2
  114. package/dist/react/index.d.ts +4 -4
  115. package/dist/react/index.js +2 -3
  116. package/dist/registry.d.ts +4 -2
  117. package/dist/testing/conformance/checks.d.ts +2 -2
  118. package/dist/testing/conformance/checks.js +2 -0
  119. package/dist/testing/conformance/index.d.ts +1 -0
  120. package/dist/testing/conformance/index.js +1 -0
  121. package/dist/testing/conformance/next-stubs.d.ts +6 -0
  122. package/dist/testing/conformance/next-stubs.js +9 -0
  123. package/dist/testing/conformance/preset.d.ts +3 -2
  124. package/dist/testing/conformance/preset.js +15 -8
  125. package/dist/testing/conformance/root-identity.d.ts +31 -0
  126. package/dist/testing/conformance/root-identity.js +361 -0
  127. package/dist/testing/conformance/stubs/next-image.d.ts +7 -0
  128. package/dist/testing/conformance/stubs/next-image.js +5 -0
  129. package/dist/testing/conformance/stubs/next-link.d.ts +7 -0
  130. package/dist/testing/conformance/stubs/next-link.js +4 -0
  131. package/dist/testing/conformance/types.d.ts +5 -2
  132. package/dist/testing/index.d.ts +1 -1
  133. package/dist/testing/index.js +4 -3
  134. package/dist/types/block-ref.d.ts +2 -0
  135. package/dist/types/block-ref.js +3 -2
  136. package/dist/types/component.d.ts +9 -0
  137. package/dist/types/config.d.ts +0 -1
  138. package/dist/types/image.d.ts +54 -17
  139. package/dist/types/image.js +102 -18
  140. package/dist/types/index.d.ts +0 -1
  141. package/dist/types/index.js +0 -1
  142. package/dist/types/media.d.ts +14 -6
  143. package/dist/types/page.d.ts +0 -1
  144. package/dist/types/video.d.ts +7 -3
  145. package/migrations/conformance-registry.md +22 -0
  146. package/package.json +21 -7
  147. package/src/cli/compile-registry.mjs +30 -42
  148. package/src/cli/mintlify-render.mjs +9 -0
  149. package/src/cli/validate-content.mjs +52 -13
  150. package/src/types/block-ref.test.ts +60 -0
  151. package/src/types/block-ref.ts +4 -2
  152. package/src/types/component.ts +10 -0
  153. package/src/types/config.ts +0 -1
  154. package/src/types/image.ts +133 -29
  155. package/src/types/index.ts +0 -1
  156. package/src/types/page.ts +0 -1
  157. package/src/types/video.ts +1 -1
  158. package/templates/astro/starter-content/src/components/chrome/Footer.astro +5 -3
  159. package/templates/astro/starter-content/src/components/chrome/Header.astro +5 -3
  160. package/templates/astro/starter-content/src/components/elements/Button.astro +5 -2
  161. package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +5 -3
  162. package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -3
  163. package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -3
  164. package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -3
  165. package/templates/astro/starter-content/src/components/sections/HomeHero.astro +5 -3
  166. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -3
  167. package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -3
  168. package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
  169. package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
  170. package/templates/astro/starter-content/src/fixtures/sections.ts +3 -3
  171. package/templates/astro/starter-content/src/stories/CompositionStoryFrame.astro +7 -2
  172. package/templates/astro/template/.aci.yaml.tmpl +0 -1
  173. package/templates/astro/template/package.json.tmpl +5 -5
  174. package/templates/astro/template/src/layouts/SiteLayout.astro +2 -2
  175. package/templates/common/.agents/skills/aci/SKILL.md +58 -1
  176. package/templates/common/.agents/skills/authoring/SKILL.md +203 -43
  177. package/templates/common/.agents/skills/authoring/references/block-registry.md +19 -3
  178. package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
  179. package/templates/common/.agents/skills/authoring/references/contracts.md +9 -30
  180. package/templates/common/.agents/skills/authoring/references/repo-structure.md +5 -4
  181. package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +3 -3
  182. package/templates/common/.agents/skills/dam-delivery/SKILL.md +217 -0
  183. package/templates/common/.agents/skills/dam-delivery/references/permalink-grammar.md +167 -0
  184. package/templates/common/.agents/skills/migration/references/canon-migration.md +18 -67
  185. package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
  186. package/templates/common/.content/fragments/navbar/_index.json +24 -0
  187. package/templates/common/.content/fragments/site-footer/_index.json +61 -0
  188. package/templates/common/.content/pages/404/_index.json +4 -2
  189. package/templates/common/.content/pages/{home/_index.json → _index.json} +10 -1
  190. package/templates/common/.content/pages/error/_index.json +4 -2
  191. package/templates/common/.content/schemas/site.schema.json +11 -1
  192. package/templates/common/.content/templates/error.json +24 -0
  193. package/templates/common/.content/templates/home.json +28 -0
  194. package/templates/common/.content/templates/site.json +8 -0
  195. package/templates/common/.content/templates/standard.json +24 -0
  196. package/templates/common/.nvmrc +1 -1
  197. package/templates/common/starter-content/.content/.rules-baseline.json +8 -0
  198. package/templates/common/starter-content/.content/config/site.json +1 -1
  199. package/templates/common/starter-content/.content/content-rules/no-public-resource-paths.json +6 -0
  200. package/templates/common/starter-content/.content/dam/README.md +74 -0
  201. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/asset.json +1 -1
  202. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +2 -1
  203. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/asset.json +1 -1
  204. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +2 -1
  205. package/templates/common/starter-content/.content/fragments/navbar/_index.json +23 -20
  206. package/templates/common/starter-content/.content/fragments/site-footer/_index.json +59 -56
  207. package/templates/common/starter-content/.content/pages/404/_index.json +2 -2
  208. package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +23 -3
  209. package/templates/common/starter-content/.content/pages/about/_index.json +5 -2
  210. package/templates/common/starter-content/.content/pages/error/_index.json +2 -2
  211. package/templates/common/starter-content/.content/patterns/components/button.json +55 -0
  212. package/templates/common/starter-content/.content/patterns/components/container.json +67 -0
  213. package/templates/common/starter-content/.content/patterns/components/feature_card.json +30 -0
  214. package/templates/common/starter-content/.content/patterns/components/feature_grid.json +59 -0
  215. package/templates/common/starter-content/.content/patterns/components/home_hero.json +151 -0
  216. package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +42 -0
  217. package/templates/common/starter-content/.content/patterns/components/site-footer.json +203 -0
  218. package/templates/common/starter-content/.content/patterns/components/site-navigation.json +115 -0
  219. package/templates/common/starter-content/.content/patterns/components/stats_bar.json +45 -0
  220. package/templates/common/starter-content/.content/schemas/site.schema.json +11 -1
  221. package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +4 -3
  222. package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +4 -3
  223. package/templates/nextjs/starter-content/src/components/elements/Button.tsx +4 -3
  224. package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +4 -3
  225. package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +4 -2
  226. package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +4 -3
  227. package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +4 -2
  228. package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +4 -3
  229. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +4 -2
  230. package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +4 -3
  231. package/templates/nextjs/template/.aci.yaml +9 -0
  232. package/templates/nextjs/template/.aci.yaml.tmpl +0 -1
  233. package/templates/nextjs/template/package.json.tmpl +4 -3
  234. package/templates/nextjs/template/src/app/_components/c15t-bridge.ts +30 -0
  235. package/templates/nextjs/template/src/app/_components/c15t-consent-ui.tsx +37 -0
  236. package/templates/nextjs/template/src/app/_components/consent-manager.css +63 -0
  237. package/templates/nextjs/template/src/app/_components/consent-manager.tsx +76 -0
  238. package/templates/nextjs/template/src/app/fonts/inter/OFL.txt +93 -0
  239. package/templates/nextjs/template/src/app/fonts/inter/inter-latin.woff2 +0 -0
  240. package/templates/nextjs/template/src/app/fonts/space-grotesk/OFL.txt +93 -0
  241. package/templates/nextjs/template/src/app/fonts/space-grotesk/space-grotesk-latin.woff2 +0 -0
  242. package/templates/nextjs/template/src/app/layout.tsx +12 -7
  243. package/templates/nextjs/template/tests/conformance.test.ts.tmpl +1 -0
  244. package/dist/define-layout.d.ts +0 -6
  245. package/dist/define-layout.js +0 -14
  246. package/dist/edge/personalization-core.d.ts +0 -11
  247. package/dist/edge/personalization-core.js +0 -269
  248. package/dist/next/collect-route.d.ts +0 -19
  249. package/dist/next/collect-route.js +0 -459
  250. package/dist/next/envelope-digest.d.ts +0 -21
  251. package/dist/next/envelope-digest.js +0 -58
  252. package/dist/next/middleware.d.ts +0 -40
  253. package/dist/next/middleware.js +0 -475
  254. package/dist/next/personalization-host.d.ts +0 -134
  255. package/dist/next/personalization-host.js +0 -270
  256. package/dist/next/root-layout.d.ts +0 -4
  257. package/dist/next/root-layout.js +0 -5
  258. package/dist/next/vercel-collect-sdk-route.d.ts +0 -27
  259. package/dist/next/vercel-collect-sdk-route.js +0 -181
  260. package/dist/next/vercel-personalization.d.ts +0 -4
  261. package/dist/next/vercel-personalization.js +0 -4
  262. package/dist/next/vercel-s3-personalization.d.ts +0 -79
  263. package/dist/next/vercel-s3-personalization.js +0 -570
  264. package/dist/types/layout.d.ts +0 -24
  265. package/src/types/layout.ts +0 -29
  266. package/templates/astro/starter-content/src/render/RenderPage.astro +0 -67
  267. package/templates/astro/template/src/cms/contracts/layouts/index.ts +0 -18
  268. package/templates/nextjs/starter-content/src/components/elements/AtomicBlock.tsx +0 -13
  269. package/templates/nextjs/template/src/cms/contracts/layouts/index.ts +0 -16
  270. /package/dist/{types/layout.js → cli/mintlify-render.d.mts} +0 -0
  271. /package/templates/common/.content/overlays/audience/preference-blue/pages/{home/_index.json → _index.json} +0 -0
  272. /package/templates/common/.content/overlays/audience/preference-red/pages/{home/_index.json → _index.json} +0 -0
  273. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
  274. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
  275. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
  276. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
@@ -8,7 +8,7 @@
8
8
  // - Cookie mutation helpers
9
9
  //
10
10
  // Shared by every static edge adapter; this logic has no SDK host wrapper.
11
- // internal/domain/audience/runtime/identity.ts imports from here as well.
11
+ // internal/domain/audience/runtime/collect.ts imports from here as well.
12
12
  const encoder = new TextEncoder();
13
13
  const decoder = new TextDecoder();
14
14
  // ── Constants ──
@@ -59,6 +59,20 @@ export function consentSignals(request, cookies) {
59
59
  privacyDenied,
60
60
  };
61
61
  }
62
+ // identityIssuancePermitted is the one rule for issuing a first-party identity
63
+ // on a request that carries none (ADR 0048). Privacy signals deny outright. A
64
+ // recorded consent choice — any envelope, even a malformed one — decides by
65
+ // itself, so an explicit denial holds in an opt-out country. Only a visitor
66
+ // with no recorded choice falls back to the release's opt-out countries, and
67
+ // an unknown country is never one of them. Personalization uses of the
68
+ // identity (claims, memberships, provider bootstrap) stay gated on the grant.
69
+ export function identityIssuancePermitted(consent, country, optOutCountries) {
70
+ if (consent.privacyDenied)
71
+ return false;
72
+ if (consent.hasConsent || consent.consentInvalid)
73
+ return consent.allowsIdentityTier;
74
+ return country !== '' && optOutCountries.includes(country);
75
+ }
62
76
  export function parseConsent(raw, decodeCookieValue = true) {
63
77
  let decoded = trimConsentWhitespace(raw);
64
78
  if (!decoded)
@@ -127,11 +141,17 @@ export function randomIdentity(random) {
127
141
  }
128
142
  export async function identityHashHex(namespace, value) {
129
143
  const digest = await crypto.subtle.digest('SHA-256', encoder.encode(`${namespace.trim().toLowerCase()}\x00${value.trim()}`));
130
- return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
144
+ return bytesToHex(new Uint8Array(digest));
131
145
  }
132
146
  export async function sha256Hex(value) {
133
147
  const digest = await crypto.subtle.digest('SHA-256', encoder.encode(value));
134
- return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
148
+ return bytesToHex(new Uint8Array(digest));
149
+ }
150
+ function bytesToHex(bytes) {
151
+ let value = '';
152
+ for (const byte of bytes)
153
+ value += byte.toString(16).padStart(2, '0');
154
+ return value;
135
155
  }
136
156
  // ── Claims cookie (aci_claims) ──
137
157
  export async function verifyClaims(value, keys, now) {
@@ -139,7 +159,9 @@ export async function verifyClaims(value, keys, now) {
139
159
  if (parts.length !== 4 || parts[0] !== 'v1')
140
160
  return null;
141
161
  const [, kid, body, signature] = parts;
142
- const secret = keys[kid];
162
+ // The kid is viewer-controlled: select only an owned key, never an inherited
163
+ // Object.prototype member such as `__proto__` or `constructor`.
164
+ const secret = Object.hasOwn(keys, kid) ? keys[kid] : undefined;
143
165
  if (!secret || !await verifyHMAC(`v1.${kid}.${body}`, signature, secret))
144
166
  return null;
145
167
  try {
@@ -164,26 +186,40 @@ export function expireCookie(name, secure) {
164
186
  return { name, value: '', options: { path: '/', httpOnly: true, secure, sameSite: 'lax', maxAge: 0, expires: new Date(0) } };
165
187
  }
166
188
  // ── Internal crypto helpers ──
167
- async function verifyHMAC(value, signature, secret) {
168
- try {
169
- const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
170
- return await crypto.subtle.verify('HMAC', key, base64URLDecode(signature), encoder.encode(value));
171
- }
172
- catch {
189
+ // verifyHMAC returns a verdict about the signature, which is the only visitor
190
+ // input here: a malformed or wrong signature is `false`. A fault importing the
191
+ // key or computing the MAC — an empty or misconfigured secret, a WebCrypto
192
+ // failure — is not a verdict and propagates to the caller's documented failure
193
+ // boundary (hosting D-14's logged private 503, the collect handler's observed
194
+ // 500). Folding it into `false` would silently read every visitor as
195
+ // unidentified.
196
+ export async function verifyHMAC(value, signature, secret) {
197
+ const signatureBytes = decodeSignature(signature);
198
+ if (!signatureBytes)
173
199
  return false;
174
- }
200
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
201
+ return await crypto.subtle.verify('HMAC', key, signatureBytes, encoder.encode(value));
202
+ }
203
+ // Signatures are unpadded base64url, the only encoding the Go signers emit. A
204
+ // value outside that grammar (or of a length no base64 string can have) is
205
+ // malformed input; within it, decoding cannot fail.
206
+ const BASE64URL_SIGNATURE = /^[A-Za-z0-9_-]+$/;
207
+ function decodeSignature(signature) {
208
+ if (!BASE64URL_SIGNATURE.test(signature) || signature.length % 4 === 1)
209
+ return null;
210
+ return base64URLDecode(signature);
175
211
  }
176
- async function hmacSHA256(value, secret) {
212
+ export async function hmacSHA256(value, secret) {
177
213
  const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
178
214
  return base64URLEncode(new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(value))));
179
215
  }
180
- function base64URLEncode(bytes) {
216
+ export function base64URLEncode(bytes) {
181
217
  let binary = '';
182
218
  for (const byte of bytes)
183
219
  binary += String.fromCharCode(byte);
184
220
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
185
221
  }
186
- function base64URLDecode(value) {
222
+ export function base64URLDecode(value) {
187
223
  const padded = value.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - value.length % 4) % 4);
188
224
  const binary = atob(padded);
189
225
  const bytes = new Uint8Array(binary.length);
@@ -1,4 +1,6 @@
1
1
  export * from './types.js';
2
2
  export * from './personalization-core.js';
3
3
  export * from './identity.js';
4
+ export * from './mimic_token.js';
5
+ export * from './compose.js';
4
6
  export { resolvePersonalizationRequest, } from './resolve.js';
@@ -3,4 +3,6 @@
3
3
  export * from './types.js';
4
4
  export * from './personalization-core.js';
5
5
  export * from './identity.js';
6
+ export * from './mimic_token.js';
7
+ export * from './compose.js';
6
8
  export { resolvePersonalizationRequest, } from './resolve.js';
@@ -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, PersonalizationVariant, PersonalizationRoute, PersonalizationExperiment, PersonalizationIdentity, PersonalizationDecision, ClassificationData, Classification, EdgeRouteEntry } from './types.js';
2
- export declare function selectPersonalizationVariant(request: PersonalizationRequestContext, route: PersonalizationRoute | null, claims: PersonalizationClaims | null, experiments?: Record<string, PersonalizationExperiment>, identity?: PersonalizationIdentity): PersonalizationVariant | null;
3
- export declare function resolvePersonalizationDecision(request: PersonalizationRequestContext, route: PersonalizationRoute | null, claims: PersonalizationClaims | null, experiments?: Record<string, PersonalizationExperiment>, identity?: PersonalizationIdentity): PersonalizationDecision;
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
- * Phase 1: Classify the visitor against all release-level rules.
6
- *
7
- * @param ctx The request context (cookies, headers, geo, query)
8
- * @param data Release-level classification data (audiences, segments, experiments)
9
- * @param identity Visitor identity (for experiment bucketing). Pass undefined
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 classify(ctx: PersonalizationRequestContext, data: ClassificationData, identity?: PersonalizationIdentity): Classification;
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;