@gradial/aci 0.1.52-rc.9 → 0.1.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/CHANGELOG.md +126 -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 -197
  9. package/dist/audience-data-models.js +313 -51
  10. package/dist/cli/compile-registry.mjs +6 -34
  11. package/dist/cli/mintlify-render.mjs +8 -0
  12. package/dist/collect/aci.js +1 -1
  13. package/dist/collect/aci.js.sha256 +1 -1
  14. package/dist/collect/api.d.ts +6 -7
  15. package/dist/collect/api.js +21 -8
  16. package/dist/collect/assignments.d.ts +7 -9
  17. package/dist/collect/assignments.js +23 -32
  18. package/dist/collect/batcher.d.ts +9 -7
  19. package/dist/collect/batcher.js +6 -5
  20. package/dist/collect/consent/envelope.d.ts +7 -1
  21. package/dist/collect/consent/envelope.js +3 -1
  22. package/dist/collect/consent/mapping.d.ts +2 -0
  23. package/dist/collect/consent/mapping.js +1 -0
  24. package/dist/collect/consenthold.d.ts +38 -0
  25. package/dist/collect/consenthold.js +82 -0
  26. package/dist/collect/declarative.js +7 -5
  27. package/dist/collect/gapbuffer.d.ts +13 -4
  28. package/dist/collect/gapbuffer.js +21 -10
  29. package/dist/collect/index.js +78 -21
  30. package/dist/collect/pageviews.d.ts +11 -11
  31. package/dist/collect/pageviews.js +27 -22
  32. package/dist/consent-banner/index.d.ts +17 -0
  33. package/dist/consent-banner/index.js +23 -0
  34. package/dist/content/index.d.ts +2 -1
  35. package/dist/content/index.js +4 -1
  36. package/dist/content/provider.d.ts +16 -2
  37. package/dist/content/provider.js +5 -1
  38. package/dist/content/resolve-slots.d.ts +24 -2
  39. package/dist/content/resolve-slots.js +127 -21
  40. package/dist/content/types.d.ts +5 -1
  41. package/dist/edge/expression-capabilities.v1.d.ts +106 -0
  42. package/dist/edge/expression-capabilities.v1.js +165 -0
  43. package/dist/index.d.ts +1 -2
  44. package/dist/index.js +0 -1
  45. package/dist/internal/domain/edge/identity.d.ts +47 -4
  46. package/dist/internal/domain/edge/identity.js +130 -26
  47. package/dist/internal/domain/edge/index.d.ts +1 -1
  48. package/dist/internal/domain/edge/index.js +1 -1
  49. package/dist/internal/domain/edge/mimic_token.d.ts +14 -0
  50. package/dist/internal/domain/edge/mimic_token.js +125 -0
  51. package/dist/internal/domain/edge/personalization-core.d.ts +61 -22
  52. package/dist/internal/domain/edge/personalization-core.js +790 -200
  53. package/dist/internal/domain/edge/personalization_index.d.ts +17 -0
  54. package/dist/internal/domain/edge/personalization_index.js +55 -0
  55. package/dist/internal/domain/edge/resolve.js +76 -39
  56. package/dist/internal/domain/edge/testdata/flat_only_validator_v1.d.ts +10 -0
  57. package/dist/internal/domain/edge/testdata/flat_only_validator_v1.js +253 -0
  58. package/dist/internal/domain/edge/types.d.ts +82 -78
  59. package/dist/internal/domain/edge/types.js +3 -3
  60. package/dist/internal/domain/edge/variant_key.d.ts +4 -0
  61. package/dist/internal/domain/edge/variant_key.js +56 -0
  62. package/dist/internal/platform/collectionstore/errors.d.ts +24 -0
  63. package/dist/internal/platform/collectionstore/errors.js +48 -0
  64. package/dist/mintlify/blocks.d.ts +6 -0
  65. package/dist/mintlify/blocks.js +201 -0
  66. package/dist/mintlify/config.d.ts +3 -0
  67. package/dist/mintlify/config.js +77 -0
  68. package/dist/mintlify/index.d.ts +3 -0
  69. package/dist/mintlify/index.js +2 -0
  70. package/dist/mintlify/json.d.ts +8 -0
  71. package/dist/mintlify/json.js +37 -0
  72. package/dist/mintlify/materialize.d.ts +14 -0
  73. package/dist/mintlify/materialize.js +254 -0
  74. package/dist/mintlify/types.d.ts +74 -0
  75. package/dist/mintlify/types.js +1 -0
  76. package/dist/mintlify/validate.d.ts +7 -0
  77. package/dist/mintlify/validate.js +256 -0
  78. package/dist/next/assignments-navigation.d.ts +4 -0
  79. package/dist/next/assignments-navigation.js +18 -0
  80. package/dist/next/assignments-script.d.ts +3 -2
  81. package/dist/next/assignments-script.js +5 -5
  82. package/dist/next/error-pages.d.ts +4 -0
  83. package/dist/next/error-pages.js +4 -0
  84. package/dist/next/error-seat-payload.d.ts +2 -1
  85. package/dist/next/error-seat-payload.js +14 -24
  86. package/dist/next/error-seat.js +7 -2
  87. package/dist/next/index.d.ts +1 -1
  88. package/dist/next/index.js +3 -1
  89. package/dist/next/page.d.ts +2 -1
  90. package/dist/next/page.js +15 -4
  91. package/dist/next/release-context.d.ts +30 -0
  92. package/dist/next/release-context.js +56 -0
  93. package/dist/next/render.d.ts +1 -1
  94. package/dist/next/render.js +15 -5
  95. package/dist/next/server.d.ts +9 -3
  96. package/dist/next/server.js +17 -32
  97. package/dist/preview/core.d.ts +1 -0
  98. package/dist/preview/core.js +27 -0
  99. package/dist/react/VideoPlayer.d.ts +3 -3
  100. package/dist/react/index.d.ts +3 -3
  101. package/dist/registry.d.ts +4 -2
  102. package/dist/testing/conformance/checks.d.ts +2 -2
  103. package/dist/testing/conformance/checks.js +2 -0
  104. package/dist/testing/conformance/index.d.ts +1 -0
  105. package/dist/testing/conformance/index.js +1 -0
  106. package/dist/testing/conformance/next-stubs.d.ts +6 -0
  107. package/dist/testing/conformance/next-stubs.js +9 -0
  108. package/dist/testing/conformance/preset.d.ts +3 -2
  109. package/dist/testing/conformance/preset.js +15 -8
  110. package/dist/testing/conformance/root-identity.d.ts +31 -0
  111. package/dist/testing/conformance/root-identity.js +361 -0
  112. package/dist/testing/conformance/stubs/next-image.d.ts +7 -0
  113. package/dist/testing/conformance/stubs/next-image.js +5 -0
  114. package/dist/testing/conformance/stubs/next-link.d.ts +7 -0
  115. package/dist/testing/conformance/stubs/next-link.js +4 -0
  116. package/dist/testing/conformance/types.d.ts +5 -2
  117. package/dist/testing/index.d.ts +1 -1
  118. package/dist/testing/index.js +4 -3
  119. package/dist/types/block-ref.js +2 -2
  120. package/dist/types/component.d.ts +9 -0
  121. package/dist/types/config.d.ts +0 -1
  122. package/dist/types/index.d.ts +0 -1
  123. package/dist/types/index.js +0 -1
  124. package/dist/types/page.d.ts +0 -1
  125. package/migrations/conformance-registry.md +22 -0
  126. package/migrations/experiment-goal-ids.md +43 -0
  127. package/package.json +21 -7
  128. package/src/cli/compile-registry.mjs +6 -30
  129. package/src/cli/mintlify-render.mjs +9 -0
  130. package/src/types/block-ref.test.ts +16 -1
  131. package/src/types/block-ref.ts +2 -2
  132. package/src/types/component.ts +10 -0
  133. package/src/types/config.ts +0 -1
  134. package/src/types/index.ts +0 -1
  135. package/src/types/page.ts +0 -1
  136. package/templates/astro/starter-content/src/components/chrome/Footer.astro +5 -3
  137. package/templates/astro/starter-content/src/components/chrome/Header.astro +5 -3
  138. package/templates/astro/starter-content/src/components/elements/Button.astro +5 -2
  139. package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +5 -3
  140. package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -3
  141. package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -3
  142. package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -3
  143. package/templates/astro/starter-content/src/components/sections/HomeHero.astro +5 -3
  144. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -3
  145. package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -3
  146. package/templates/astro/starter-content/src/fixtures/sections.ts +1 -1
  147. package/templates/astro/starter-content/src/stories/CompositionStoryFrame.astro +7 -2
  148. package/templates/astro/template/.aci.yaml.tmpl +0 -1
  149. package/templates/astro/template/package.json.tmpl +5 -5
  150. package/templates/astro/template/src/layouts/SiteLayout.astro +2 -2
  151. package/templates/common/.agents/skills/aci/SKILL.md +58 -1
  152. package/templates/common/.agents/skills/authoring/SKILL.md +194 -42
  153. package/templates/common/.agents/skills/authoring/references/block-registry.md +19 -3
  154. package/templates/common/.agents/skills/authoring/references/contracts.md +9 -30
  155. package/templates/common/.agents/skills/authoring/references/repo-structure.md +5 -4
  156. package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +3 -3
  157. package/templates/common/.agents/skills/dam-delivery/SKILL.md +216 -0
  158. package/templates/common/.agents/skills/dam-delivery/references/permalink-grammar.md +167 -0
  159. package/templates/common/.agents/skills/migration/references/canon-migration.md +18 -67
  160. package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
  161. package/templates/common/.content/fragments/navbar/_index.json +24 -0
  162. package/templates/common/.content/fragments/site-footer/_index.json +61 -0
  163. package/templates/common/.content/pages/404/_index.json +4 -2
  164. package/templates/common/.content/pages/_index.json +0 -1
  165. package/templates/common/.content/pages/error/_index.json +4 -2
  166. package/templates/common/.content/schemas/site.schema.json +11 -1
  167. package/templates/common/.content/templates/error.json +24 -0
  168. package/templates/common/.content/templates/home.json +28 -0
  169. package/templates/common/.content/templates/site.json +8 -0
  170. package/templates/common/.content/templates/standard.json +24 -0
  171. package/templates/common/.nvmrc +1 -1
  172. package/templates/common/starter-content/.content/config/site.json +1 -1
  173. package/templates/common/starter-content/.content/dam/README.md +74 -0
  174. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/asset.json +1 -1
  175. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/asset.json +1 -1
  176. package/templates/common/starter-content/.content/pages/404/_index.json +1 -2
  177. package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +14 -3
  178. package/templates/common/starter-content/.content/pages/about/_index.json +2 -2
  179. package/templates/common/starter-content/.content/pages/error/_index.json +1 -2
  180. package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
  181. package/templates/common/starter-content/.content/patterns/components/container.json +8 -3
  182. package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
  183. package/templates/common/starter-content/.content/patterns/components/feature_grid.json +5 -3
  184. package/templates/common/starter-content/.content/patterns/components/home_hero.json +2 -2
  185. package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +2 -2
  186. package/templates/common/starter-content/.content/patterns/components/site-footer.json +2 -2
  187. package/templates/common/starter-content/.content/patterns/components/site-navigation.json +2 -2
  188. package/templates/common/starter-content/.content/patterns/components/stats_bar.json +2 -2
  189. package/templates/common/starter-content/.content/schemas/site.schema.json +11 -1
  190. package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +4 -3
  191. package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +4 -3
  192. package/templates/nextjs/starter-content/src/components/elements/Button.tsx +4 -3
  193. package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +4 -3
  194. package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +4 -2
  195. package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +4 -3
  196. package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +4 -2
  197. package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +4 -3
  198. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +4 -2
  199. package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +4 -3
  200. package/templates/nextjs/template/.aci.yaml +0 -1
  201. package/templates/nextjs/template/.aci.yaml.tmpl +0 -1
  202. package/templates/nextjs/template/package.json.tmpl +4 -3
  203. package/templates/nextjs/template/src/app/_components/c15t-bridge.ts +30 -0
  204. package/templates/nextjs/template/src/app/_components/c15t-consent-ui.tsx +37 -0
  205. package/templates/nextjs/template/src/app/_components/consent-manager.css +63 -0
  206. package/templates/nextjs/template/src/app/_components/consent-manager.tsx +76 -0
  207. package/templates/nextjs/template/src/app/fonts/inter/OFL.txt +93 -0
  208. package/templates/nextjs/template/src/app/fonts/inter/inter-latin.woff2 +0 -0
  209. package/templates/nextjs/template/src/app/fonts/space-grotesk/OFL.txt +93 -0
  210. package/templates/nextjs/template/src/app/fonts/space-grotesk/space-grotesk-latin.woff2 +0 -0
  211. package/templates/nextjs/template/src/app/layout.tsx +12 -7
  212. package/templates/nextjs/template/tests/conformance.test.ts.tmpl +1 -0
  213. package/dist/define-layout.d.ts +0 -6
  214. package/dist/define-layout.js +0 -14
  215. package/dist/edge/personalization-core.d.ts +0 -11
  216. package/dist/edge/personalization-core.js +0 -269
  217. package/dist/internal/domain/edge/compose.d.ts +0 -24
  218. package/dist/internal/domain/edge/compose.js +0 -89
  219. package/dist/internal/domain/edge/record/codec.d.ts +0 -71
  220. package/dist/internal/domain/edge/record/codec.js +0 -401
  221. package/dist/next/collect-route.d.ts +0 -19
  222. package/dist/next/collect-route.js +0 -459
  223. package/dist/next/edge-config.d.ts +0 -1
  224. package/dist/next/edge-config.js +0 -92
  225. package/dist/next/envelope-digest.d.ts +0 -21
  226. package/dist/next/envelope-digest.js +0 -58
  227. package/dist/next/middleware.d.ts +0 -40
  228. package/dist/next/middleware.js +0 -475
  229. package/dist/next/personalization-host.d.ts +0 -134
  230. package/dist/next/personalization-host.js +0 -270
  231. package/dist/next/root-layout.d.ts +0 -4
  232. package/dist/next/root-layout.js +0 -5
  233. package/dist/next/vercel-collect-sdk-route.d.ts +0 -27
  234. package/dist/next/vercel-collect-sdk-route.js +0 -181
  235. package/dist/next/vercel-edge-projection-v1.d.ts +0 -24
  236. package/dist/next/vercel-edge-projection-v1.js +0 -880
  237. package/dist/next/vercel-personalization.d.ts +0 -4
  238. package/dist/next/vercel-personalization.js +0 -4
  239. package/dist/next/vercel-s3-personalization.d.ts +0 -79
  240. package/dist/next/vercel-s3-personalization.js +0 -570
  241. package/dist/types/layout.d.ts +0 -24
  242. package/src/types/layout.ts +0 -29
  243. package/templates/astro/starter-content/src/render/RenderPage.astro +0 -67
  244. package/templates/astro/template/src/cms/contracts/layouts/index.ts +0 -18
  245. package/templates/common/starter-content/.content/patterns/layouts/default.json +0 -14
  246. package/templates/nextjs/starter-content/src/components/elements/AtomicBlock.tsx +0 -13
  247. package/templates/nextjs/template/src/cms/contracts/layouts/index.ts +0 -16
  248. /package/dist/{types/layout.js → cli/mintlify-render.d.mts} +0 -0
  249. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
  250. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +0 -0
  251. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
  252. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
  253. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +0 -0
  254. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
@@ -5,10 +5,9 @@
5
5
  // - Consent signal extraction (GPC/DNT + portable consent envelope)
6
6
  // - Identity cookie verification and minting (aci_id)
7
7
  // - Claims cookie verification (aci_claims)
8
- // - Cookie mutation helpers
9
8
  //
10
9
  // Shared by every static edge adapter; this logic has no SDK host wrapper.
11
- // internal/domain/audience/runtime/identity.ts imports from here as well.
10
+ // internal/domain/audience/runtime/collect.ts imports from here as well.
12
11
  const encoder = new TextEncoder();
13
12
  const decoder = new TextDecoder();
14
13
  // ── Constants ──
@@ -59,6 +58,20 @@ export function consentSignals(request, cookies) {
59
58
  privacyDenied,
60
59
  };
61
60
  }
61
+ // identityIssuancePermitted is the one rule for issuing a first-party identity
62
+ // on a request that carries none (ADR 0048). Privacy signals deny outright. A
63
+ // recorded consent choice — any envelope, even a malformed one — decides by
64
+ // itself, so an explicit denial holds in an opt-out country. Only a visitor
65
+ // with no recorded choice falls back to the release's opt-out countries, and
66
+ // an unknown country is never one of them. Personalization uses of the
67
+ // identity (claims, memberships, provider bootstrap) stay gated on the grant.
68
+ export function identityIssuancePermitted(consent, country, optOutCountries) {
69
+ if (consent.privacyDenied)
70
+ return false;
71
+ if (consent.hasConsent || consent.consentInvalid)
72
+ return consent.allowsIdentityTier;
73
+ return country !== '' && optOutCountries.includes(country);
74
+ }
62
75
  export function parseConsent(raw, decodeCookieValue = true) {
63
76
  let decoded = trimConsentWhitespace(raw);
64
77
  if (!decoded)
@@ -127,19 +140,46 @@ export function randomIdentity(random) {
127
140
  }
128
141
  export async function identityHashHex(namespace, value) {
129
142
  const digest = await crypto.subtle.digest('SHA-256', encoder.encode(`${namespace.trim().toLowerCase()}\x00${value.trim()}`));
130
- return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
143
+ return bytesToHex(new Uint8Array(digest));
131
144
  }
132
- export async function sha256Hex(value) {
133
- 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('');
145
+ function bytesToHex(bytes) {
146
+ let value = '';
147
+ for (const byte of bytes)
148
+ value += byte.toString(16).padStart(2, '0');
149
+ return value;
135
150
  }
136
151
  // ── Claims cookie (aci_claims) ──
152
+ // verifyClaims is the PERSONALIZATION grade: signature, expiry, and the
153
+ // ADR 0059 §6 issuer-marker gate — a claim without a fresh marker is never
154
+ // trusted for membership selection. Existing signed cookies expire by their
155
+ // own TTL; their old contents are not read during the transition.
137
156
  export async function verifyClaims(value, keys, now) {
157
+ const payload = await verifyClaimsAuthenticity(value, keys, now);
158
+ if (payload === null || claimsIssuer(payload) === undefined)
159
+ return null;
160
+ return payload;
161
+ }
162
+ // verifyClaimsEvidence is the EVIDENCE grade (anonymous-experiments REQ-6):
163
+ // signature and expiry WITHOUT the issuer gate. The collector's
164
+ // withdrawal/suppression trigger only needs proof that identity-tier state
165
+ // existed — an authentically signed, unexpired pre-cutover (unmarked) cookie
166
+ // is exactly that proof, and the Go collector accepts it the same way (Go
167
+ // ClaimsVerifier.VerifyClaims has no marker gate). Evidence never feeds
168
+ // membership selection, so the §6 gate does not apply here.
169
+ export async function verifyClaimsEvidence(value, keys, now) {
170
+ return verifyClaimsAuthenticity(value, keys, now);
171
+ }
172
+ // verifyClaimsAuthenticity is the shared core: envelope, keyed signature,
173
+ // and expiry — no policy gate. Every structural failure degrades to null
174
+ // (never an error), matching Go's no-oracle posture.
175
+ async function verifyClaimsAuthenticity(value, keys, now) {
138
176
  const parts = value.trim().split('.');
139
177
  if (parts.length !== 4 || parts[0] !== 'v1')
140
178
  return null;
141
179
  const [, kid, body, signature] = parts;
142
- const secret = keys[kid];
180
+ // The kid is viewer-controlled: select only an owned key, never an inherited
181
+ // Object.prototype member such as `__proto__` or `constructor`.
182
+ const secret = Object.hasOwn(keys, kid) ? keys[kid] : undefined;
143
183
  if (!secret || !await verifyHMAC(`v1.${kid}.${body}`, signature, secret))
144
184
  return null;
145
185
  try {
@@ -152,38 +192,102 @@ export async function verifyClaims(value, keys, now) {
152
192
  return null;
153
193
  }
154
194
  }
155
- // ── Cookie mutation helpers ──
156
- export function cookieMutation(name, value, maxAge, now, secure, httpOnly) {
157
- return {
158
- name,
159
- value,
160
- options: { path: '/', httpOnly, secure, sameSite: 'lax', maxAge, expires: new Date(now.getTime() + maxAge * 1000) },
161
- };
195
+ /**
196
+ * Exposes the payload's `iss` marker. Malformed/unknown markers — and
197
+ * pre-0059 unmarked payloads — yield undefined; since the §6 cutover,
198
+ * verifyClaims rejects those payloads outright, so a verified payload always
199
+ * carries a marker. The marker identifies the minting path; it does not
200
+ * grant membership by itself.
201
+ */
202
+ export function claimsIssuer(payload) {
203
+ const iss = payload?.iss;
204
+ return iss === 'aci' || iss === 'edge' ? iss : undefined;
162
205
  }
163
- export function expireCookie(name, secure) {
164
- return { name, value: '', options: { path: '/', httpOnly: true, secure, sameSite: 'lax', maxAge: 0, expires: new Date(0) } };
206
+ /** audience.MaxClaimsCookieBytes — the hard 1 KiB aci_claims budget. */
207
+ export const MAX_CLAIMS_COOKIE_BYTES = 1024;
208
+ /** audience.DefaultClaimsTTL — the 24-hour signed-lifetime ceiling. */
209
+ export const DEFAULT_CLAIMS_TTL_SECONDS = 24 * 60 * 60;
210
+ /**
211
+ * Mint rejection: oversize, a malformed key, or a missing issuer marker.
212
+ * Callers fail closed on it — the edge never truncates or subsets
213
+ * memberships to fit (ADR 0059 §2/§3).
214
+ */
215
+ export class ClaimsMintError extends Error {
216
+ name = 'ClaimsMintError';
165
217
  }
166
- // ── 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));
218
+ /**
219
+ * Mints one signed aci_claims value: v1.{kid}.{base64url(payload)}.{hmac},
220
+ * byte-identical to Go's ClaimsIssuer.IssueClaims — payload field order is
221
+ * aud (omitted when empty), seg (omitted when empty), iat, exp, iss, and the
222
+ * TTL clamps to the 24-hour ceiling. The exact bytes are pinned against the
223
+ * Go golden in claims_contract.test.ts. Issuance requires an explicit issuer
224
+ * marker; the marker identifies the minting path, it does not grant
225
+ * membership by itself.
226
+ */
227
+ export async function issueClaims(memberships, issuer, key, now, ttlSeconds) {
228
+ const kid = key.kid.trim();
229
+ if (!key.secret)
230
+ throw new ClaimsMintError('claims issuer requires a secret');
231
+ if (!kid)
232
+ throw new ClaimsMintError('claims issuer requires a kid');
233
+ if (kid.includes('.'))
234
+ throw new ClaimsMintError("claims kid must not contain '.'");
235
+ if (issuer !== 'aci' && issuer !== 'edge') {
236
+ throw new ClaimsMintError('claims issuer requires an explicit issuer marker');
171
237
  }
172
- catch {
173
- return false;
238
+ const ttl = ttlSeconds === undefined || ttlSeconds <= 0 || ttlSeconds > DEFAULT_CLAIMS_TTL_SECONDS
239
+ ? DEFAULT_CLAIMS_TTL_SECONDS
240
+ : Math.floor(ttlSeconds);
241
+ const iat = Math.floor(now.getTime() / 1000);
242
+ const body = JSON.stringify({
243
+ ...(memberships.aud?.length ? { aud: memberships.aud } : {}),
244
+ ...(memberships.seg?.length ? { seg: memberships.seg } : {}),
245
+ iat,
246
+ exp: iat + ttl,
247
+ iss: issuer,
248
+ });
249
+ const encoded = base64URLEncode(encoder.encode(body));
250
+ const value = `v1.${kid}.${encoded}.${await hmacSHA256(`v1.${kid}.${encoded}`, key.secret)}`;
251
+ if (encoder.encode(value).byteLength > MAX_CLAIMS_COOKIE_BYTES) {
252
+ throw new ClaimsMintError(`claims cookie exceeds the ${MAX_CLAIMS_COOKIE_BYTES}-byte budget (aud=${memberships.aud?.length ?? 0} seg=${memberships.seg?.length ?? 0})`);
174
253
  }
254
+ return value;
255
+ }
256
+ // ── Internal crypto helpers ──
257
+ // verifyHMAC returns a verdict about the signature, which is the only visitor
258
+ // input here: a malformed or wrong signature is `false`. A fault importing the
259
+ // key or computing the MAC — an empty or misconfigured secret, a WebCrypto
260
+ // failure — is not a verdict and propagates to the caller's documented failure
261
+ // boundary (hosting D-14's logged private 503, the collect handler's observed
262
+ // 500). Folding it into `false` would silently read every visitor as
263
+ // unidentified.
264
+ export async function verifyHMAC(value, signature, secret) {
265
+ const signatureBytes = decodeSignature(signature);
266
+ if (!signatureBytes)
267
+ return false;
268
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
269
+ return await crypto.subtle.verify('HMAC', key, signatureBytes, encoder.encode(value));
270
+ }
271
+ // Signatures are unpadded base64url, the only encoding the Go signers emit. A
272
+ // value outside that grammar (or of a length no base64 string can have) is
273
+ // malformed input; within it, decoding cannot fail.
274
+ const BASE64URL_SIGNATURE = /^[A-Za-z0-9_-]+$/;
275
+ function decodeSignature(signature) {
276
+ if (!BASE64URL_SIGNATURE.test(signature) || signature.length % 4 === 1)
277
+ return null;
278
+ return base64URLDecode(signature);
175
279
  }
176
- async function hmacSHA256(value, secret) {
280
+ export async function hmacSHA256(value, secret) {
177
281
  const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
178
282
  return base64URLEncode(new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(value))));
179
283
  }
180
- function base64URLEncode(bytes) {
284
+ export function base64URLEncode(bytes) {
181
285
  let binary = '';
182
286
  for (const byte of bytes)
183
287
  binary += String.fromCharCode(byte);
184
288
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
185
289
  }
186
- function base64URLDecode(value) {
290
+ export function base64URLDecode(value) {
187
291
  const padded = value.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - value.length % 4) % 4);
188
292
  const binary = atob(padded);
189
293
  const bytes = new Uint8Array(binary.length);
@@ -1,5 +1,5 @@
1
1
  export * from './types.js';
2
2
  export * from './personalization-core.js';
3
3
  export * from './identity.js';
4
- export * from './compose.js';
4
+ export * from './mimic_token.js';
5
5
  export { resolvePersonalizationRequest, } from './resolve.js';
@@ -3,5 +3,5 @@
3
3
  export * from './types.js';
4
4
  export * from './personalization-core.js';
5
5
  export * from './identity.js';
6
- export * from './compose.js';
6
+ export * from './mimic_token.js';
7
7
  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;