@gradial/aci 0.1.52-rc.8 → 0.1.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -0,0 +1,256 @@
1
+ import { validateBlock, validateSnippetReferences } from './blocks.js';
2
+ import { cleanDocumentPath, cleanFilePath, isExternalReference, isMintlifyJSONValue, isObject, nonEmptyString, } from './json.js';
3
+ const CONFIG_FRAGMENT_SCHEMA_VERSION = 1;
4
+ const API_SPEC_SCHEMA_VERSION = 1;
5
+ export function validateSource(site, documents, snippets, apiSpecs) {
6
+ const paths = new Set();
7
+ for (const document of documents) {
8
+ if (paths.has(document.path))
9
+ throw new Error(`ACI Mintlify document path is duplicated: ${document.path}`);
10
+ paths.add(document.path);
11
+ }
12
+ const navigated = new Set();
13
+ const navigation = isObject(site) ? site.navigation : undefined;
14
+ validateNavigationPartitions(navigation);
15
+ for (const page of navigationPages(navigation)) {
16
+ // Links are valid Mintlify navigation items but are not ACI documents.
17
+ if (isExternalReference(page))
18
+ continue;
19
+ const normalized = cleanDocumentPath(page);
20
+ if (!normalized || normalized !== page)
21
+ throw new Error(`ACI Mintlify navigation page is invalid: ${page}`);
22
+ if (!paths.has(page))
23
+ throw new Error(`ACI Mintlify navigation references missing document: ${page}`);
24
+ if (navigated.has(page))
25
+ throw new Error(`ACI Mintlify navigation references document more than once: ${page}`);
26
+ navigated.add(page);
27
+ }
28
+ validateRedirects(isObject(site) ? site.redirects : undefined, paths);
29
+ validateSnippetReferences(documents, snippets);
30
+ validateAPISpecReferences(site, apiSpecs);
31
+ }
32
+ export function validateSite(value, label) {
33
+ if (!isObject(value) || !isMintlifyJSONValue(value)) {
34
+ throw new Error(`ACI Mintlify site is invalid: ${label}`);
35
+ }
36
+ if ('$schema' in value && value.$schema !== 'https://mintlify.com/docs.json') {
37
+ throw new Error(`ACI Mintlify site has an unsupported docs schema: ${label}`);
38
+ }
39
+ return value;
40
+ }
41
+ export function validateDocument(value, label, sourcePath) {
42
+ const metadata = isObject(value) && isObject(value.metadata) ? value.metadata : undefined;
43
+ const regions = isObject(value) && isObject(value.regions) ? value.regions : undefined;
44
+ if (!isObject(value) || value.$type !== 'page' || !nonEmptyString(value.id) || !nonEmptyString(value.status) || !nonEmptyString(sourcePath) || !metadata || !nonEmptyString(metadata.title) || !regions || !Array.isArray(regions.main)) {
45
+ throw new Error(`ACI Mintlify document is invalid: ${label}`);
46
+ }
47
+ for (const region of Object.keys(regions)) {
48
+ if (region !== 'main')
49
+ throw new Error(`ACI Mintlify document ${label} has unsupported region ${JSON.stringify(region)}; only main is supported`);
50
+ }
51
+ const documentPath = cleanDocumentPath(sourcePath);
52
+ if (!documentPath || documentPath !== sourcePath)
53
+ throw new Error(`ACI Mintlify document path is invalid: ${sourcePath}`);
54
+ const ids = new Set();
55
+ const blocks = regions.main.map((block, index) => validateBlock(block, `${label}.regions.main[${index}]`, ids));
56
+ const renderer = isObject(metadata.mintlify) ? metadata.mintlify : {};
57
+ return {
58
+ $type: 'page', id: value.id, status: value.status, path: documentPath,
59
+ metadata: { title: metadata.title,
60
+ ...(nonEmptyString(metadata.description) ? { description: metadata.description } : {}),
61
+ mintlify: {
62
+ ...(nonEmptyString(renderer.sidebarTitle) ? { sidebarTitle: renderer.sidebarTitle } : {}),
63
+ ...(nonEmptyString(renderer.icon) ? { icon: renderer.icon } : {}),
64
+ ...(validFrontmatter(renderer.frontmatter) ? { frontmatter: renderer.frontmatter } : {}),
65
+ },
66
+ }, regions: { main: blocks },
67
+ };
68
+ }
69
+ export function validateSnippet(value, label, sourcePath) {
70
+ if (!isObject(value) || value.$type !== 'fragment' || !nonEmptyString(value.id) || !nonEmptyString(sourcePath) || !isObject(value.block)) {
71
+ throw new Error(`ACI Mintlify snippet is invalid: ${label}`);
72
+ }
73
+ const snippetPath = cleanFilePath(sourcePath, '.mdx');
74
+ if (!snippetPath || snippetPath !== sourcePath)
75
+ throw new Error(`ACI Mintlify snippet path is invalid: ${sourcePath}`);
76
+ const ids = new Set();
77
+ return {
78
+ $type: 'fragment', id: value.id,
79
+ path: snippetPath, block: validateBlock(value.block, `${label}.block`, ids),
80
+ };
81
+ }
82
+ export function validateConfigFragment(value, label) {
83
+ if (!isObject(value) || value.$type !== 'mintlify.config-fragment' || value.schemaVersion !== CONFIG_FRAGMENT_SCHEMA_VERSION || !nonEmptyString(value.path) || !isMintlifyJSONValue(value.value)) {
84
+ throw new Error(`ACI Mintlify config fragment is invalid: ${label}`);
85
+ }
86
+ const fragmentPath = cleanFilePath(value.path, '.json');
87
+ if (!fragmentPath || fragmentPath !== value.path || fragmentPath === 'docs.json')
88
+ throw new Error(`ACI Mintlify config fragment path is invalid: ${value.path}`);
89
+ return { $type: 'mintlify.config-fragment', schemaVersion: CONFIG_FRAGMENT_SCHEMA_VERSION, path: fragmentPath, value: value.value };
90
+ }
91
+ export function validateAPISpec(value, label) {
92
+ if (!isObject(value) || value.$type !== 'mintlify.api-spec' || value.schemaVersion !== API_SPEC_SCHEMA_VERSION || !nonEmptyString(value.path) || !['openapi', 'asyncapi', 'graphql'].includes(String(value.format)) || !nonEmptyString(value.source)) {
93
+ throw new Error(`ACI Mintlify API specification is invalid: ${label}`);
94
+ }
95
+ const sourcePath = value.path;
96
+ const extensions = value.format === 'graphql' ? ['.graphql', '.gql', '.json'] : ['.json', '.yaml', '.yml'];
97
+ const specPath = extensions.map((extension) => cleanFilePath(sourcePath, extension)).find(Boolean) ?? '';
98
+ if (!specPath || specPath !== sourcePath)
99
+ throw new Error(`ACI Mintlify API specification path is invalid: ${sourcePath}`);
100
+ return {
101
+ $type: 'mintlify.api-spec',
102
+ schemaVersion: API_SPEC_SCHEMA_VERSION,
103
+ path: specPath,
104
+ format: value.format,
105
+ source: value.source,
106
+ };
107
+ }
108
+ function validFrontmatter(value) {
109
+ if (value === undefined)
110
+ return false;
111
+ if (!isObject(value))
112
+ throw new Error('ACI Mintlify document frontmatter is invalid');
113
+ for (const [key, entry] of Object.entries(value)) {
114
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(key) || !isMintlifyJSONValue(entry)) {
115
+ throw new Error('ACI Mintlify document frontmatter is invalid');
116
+ }
117
+ if (['title', 'description', 'sidebarTitle', 'icon'].includes(key))
118
+ throw new Error(`ACI Mintlify document frontmatter key is reserved: ${key}`);
119
+ }
120
+ return true;
121
+ }
122
+ function validateRedirects(value, documentPaths) {
123
+ if (value === undefined)
124
+ return;
125
+ if (!Array.isArray(value))
126
+ throw new Error('ACI Mintlify redirects are invalid');
127
+ const redirects = new Map();
128
+ const routes = new Set([...documentPaths].map((documentPath) => `/${documentPath}`));
129
+ for (const entry of value) {
130
+ if (!isObject(entry) || !nonEmptyString(entry.source) || !nonEmptyString(entry.destination) || (entry.permanent !== undefined && typeof entry.permanent !== 'boolean')) {
131
+ throw new Error('ACI Mintlify redirect is invalid');
132
+ }
133
+ if (!entry.source.startsWith('/') || entry.source.includes('?') || entry.source.includes('#')) {
134
+ throw new Error(`ACI Mintlify redirect source is invalid: ${entry.source}`);
135
+ }
136
+ if (!entry.destination.startsWith('/') && !isExternalReference(entry.destination)) {
137
+ throw new Error(`ACI Mintlify redirect destination is invalid: ${entry.destination}`);
138
+ }
139
+ if (redirects.has(entry.source))
140
+ throw new Error(`ACI Mintlify redirect source is duplicated: ${entry.source}`);
141
+ if (!entry.source.includes('*') && !entry.source.includes(':') && routes.has(trimRouteSlash(entry.source))) {
142
+ throw new Error(`ACI Mintlify redirect source collides with a document: ${entry.source}`);
143
+ }
144
+ redirects.set(entry.source, entry.destination);
145
+ }
146
+ for (const [source, destination] of redirects) {
147
+ if (isExternalReference(destination) || destination.includes('*') || destination.includes(':'))
148
+ continue;
149
+ const destinationPath = trimRouteSlash(destination.split(/[?#]/, 1)[0]);
150
+ if (!routes.has(destinationPath) && !redirects.has(destinationPath)) {
151
+ throw new Error(`ACI Mintlify redirect destination is missing: ${destination}`);
152
+ }
153
+ const seen = new Set([source]);
154
+ let next = destinationPath;
155
+ while (redirects.has(next)) {
156
+ if (seen.has(next))
157
+ throw new Error(`ACI Mintlify redirect cycle includes: ${next}`);
158
+ seen.add(next);
159
+ const target = redirects.get(next);
160
+ if (isExternalReference(target) || target.includes('*') || target.includes(':'))
161
+ break;
162
+ next = trimRouteSlash(target.split(/[?#]/, 1)[0]);
163
+ }
164
+ }
165
+ }
166
+ function trimRouteSlash(value) {
167
+ return value.length > 1 ? value.replace(/\/+$/, '') : value;
168
+ }
169
+ function validateAPISpecReferences(site, specs) {
170
+ const byPath = new Map();
171
+ for (const spec of specs) {
172
+ if (byPath.has(spec.path))
173
+ throw new Error(`ACI Mintlify API specification path is duplicated: ${spec.path}`);
174
+ byPath.set(spec.path, spec);
175
+ }
176
+ const visit = (value) => {
177
+ if (Array.isArray(value)) {
178
+ for (const child of value)
179
+ visit(child);
180
+ return;
181
+ }
182
+ if (!isObject(value))
183
+ return;
184
+ for (const [key, child] of Object.entries(value)) {
185
+ if (['openapi', 'asyncapi', 'graphql'].includes(key)) {
186
+ const references = typeof child === 'string' ? [child] : Array.isArray(child) ? child.filter((entry) => typeof entry === 'string') : [];
187
+ for (const reference of references) {
188
+ if (isExternalReference(reference))
189
+ continue;
190
+ const spec = byPath.get(reference);
191
+ if (!spec || spec.format !== key)
192
+ throw new Error(`ACI Mintlify API specification reference is missing or has the wrong format: ${reference}`);
193
+ }
194
+ }
195
+ visit(child);
196
+ }
197
+ };
198
+ visit(site);
199
+ }
200
+ function navigationPages(value) {
201
+ if (!isObject(value))
202
+ return [];
203
+ const pages = [];
204
+ visitNavigation(value, pages);
205
+ return pages;
206
+ }
207
+ function visitNavigation(value, pages) {
208
+ if (Array.isArray(value)) {
209
+ for (const item of value)
210
+ visitNavigation(item, pages);
211
+ return;
212
+ }
213
+ if (!isObject(value))
214
+ return;
215
+ for (const [key, child] of Object.entries(value)) {
216
+ if (key === 'pages' && Array.isArray(child)) {
217
+ for (const item of child) {
218
+ if (typeof item === 'string')
219
+ pages.push(item);
220
+ else
221
+ visitNavigation(item, pages);
222
+ }
223
+ continue;
224
+ }
225
+ visitNavigation(child, pages);
226
+ }
227
+ }
228
+ function validateNavigationPartitions(value) {
229
+ if (Array.isArray(value)) {
230
+ for (const child of value)
231
+ validateNavigationPartitions(child);
232
+ return;
233
+ }
234
+ if (!isObject(value))
235
+ return;
236
+ for (const [key, child] of Object.entries(value)) {
237
+ if ((key === 'languages' || key === 'versions') && Array.isArray(child)) {
238
+ const identityKey = key === 'languages' ? 'language' : 'version';
239
+ const identities = new Set();
240
+ let defaults = 0;
241
+ for (const entry of child) {
242
+ if (!isObject(entry) || !nonEmptyString(entry[identityKey]))
243
+ throw new Error(`ACI Mintlify ${key} navigation is invalid`);
244
+ const identity = entry[identityKey];
245
+ if (identities.has(identity))
246
+ throw new Error(`ACI Mintlify ${key} navigation is duplicated: ${identity}`);
247
+ identities.add(identity);
248
+ if (entry.default === true)
249
+ defaults += 1;
250
+ }
251
+ if (defaults > 1)
252
+ throw new Error(`ACI Mintlify navigation has multiple default ${key}`);
253
+ }
254
+ validateNavigationPartitions(child);
255
+ }
256
+ }
@@ -0,0 +1,4 @@
1
+ /** Signals from the committed page tree, including a page with no assignments. */
2
+ export declare function AssignmentsNavigation({ json }: {
3
+ json: string;
4
+ }): null;
@@ -0,0 +1,18 @@
1
+ 'use client';
2
+ import { useLayoutEffect } from 'react';
3
+ /** Signals from the committed page tree, including a page with no assignments. */
4
+ export function AssignmentsNavigation({ json }) {
5
+ // Deliberately runs on each page commit, including unchanged assignment props
6
+ // on a query navigation. The collector deduplicates hydration/StrictMode and
7
+ // same-URL/hash commits; discarded concurrent renders never run this effect.
8
+ useLayoutEffect(() => {
9
+ const win = window;
10
+ if (typeof win.aci !== 'function') {
11
+ const queue = (...args) => { (queue.q ??= []).push(args); };
12
+ win.aci = queue;
13
+ }
14
+ const assignments = json ? JSON.parse(json).experiments : [];
15
+ win.aci('navigation-complete', { url: win.location.href, assignments });
16
+ });
17
+ return null;
18
+ }
@@ -7,8 +7,9 @@ export interface AssignmentsScriptProps {
7
7
  * (`<script type="application/json" data-aci-assignments>` with a
8
8
  * {"v":1,"experiments":[…]} body, personalization-reporting design/04) when
9
9
  * the page payload carries a build-time `experiments` block, and nothing
10
- * otherwise. createPage appends it to the page tree exactly once per page;
10
+ * otherwise. Its client commit hook also signals empty pages so SPA
11
+ * attribution clears when leaving an experiment. createPage appends it to the page tree exactly once per page;
11
12
  * sites composing their own pages can render it directly. The JSON body is
12
13
  * HTML-safe (assignmentsScriptJSON \u-escapes angle brackets).
13
14
  */
14
- export declare function AssignmentsScript({ page }: AssignmentsScriptProps): import("react").JSX.Element | null;
15
+ export declare function AssignmentsScript({ page }: AssignmentsScriptProps): import("react").JSX.Element;
@@ -1,17 +1,17 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AssignmentsNavigation } from './assignments-navigation.js';
2
3
  import { assignmentsScriptJSON, } from '../content/personalization.js';
3
4
  /**
4
5
  * AssignmentsScript renders the page-level experiment assignment tag
5
6
  * (`<script type="application/json" data-aci-assignments>` with a
6
7
  * {"v":1,"experiments":[…]} body, personalization-reporting design/04) when
7
8
  * the page payload carries a build-time `experiments` block, and nothing
8
- * otherwise. createPage appends it to the page tree exactly once per page;
9
+ * otherwise. Its client commit hook also signals empty pages so SPA
10
+ * attribution clears when leaving an experiment. createPage appends it to the page tree exactly once per page;
9
11
  * sites composing their own pages can render it directly. The JSON body is
10
12
  * HTML-safe (assignmentsScriptJSON \u-escapes angle brackets).
11
13
  */
12
14
  export function AssignmentsScript({ page }) {
13
15
  const json = assignmentsScriptJSON(page);
14
- if (!json)
15
- return null;
16
- return (_jsx("script", { type: "application/json", "data-aci-assignments": "", dangerouslySetInnerHTML: { __html: json } }));
16
+ return (_jsxs(_Fragment, { children: [json ? _jsx("script", { type: "application/json", "data-aci-assignments": "", dangerouslySetInnerHTML: { __html: json } }) : null, _jsx(AssignmentsNavigation, { json: json })] }));
17
17
  }
@@ -17,6 +17,10 @@ export interface NextResolvedErrorPage extends ResolvedErrorPage {
17
17
  * warn posture (D-5). Never throws: an unreachable site config degrades to
18
18
  * the shipped default — an error surface must not re-enter the failing
19
19
  * system (ANTI-2).
20
+ *
21
+ * The seat receives no route params, so on a hosted request its release
22
+ * comes from the router's signed release context (resolveReleaseId): the
23
+ * same release the catch-all of that request was rewritten to.
20
24
  */
21
25
  export declare function resolveErrorPage(kind: ErrorPageKind, config?: FetchConfig): Promise<NextResolvedErrorPage>;
22
26
  /**
@@ -11,6 +11,10 @@ import { ErrorPageMarker } from './error-page-marker.js';
11
11
  * warn posture (D-5). Never throws: an unreachable site config degrades to
12
12
  * the shipped default — an error surface must not re-enter the failing
13
13
  * system (ANTI-2).
14
+ *
15
+ * The seat receives no route params, so on a hosted request its release
16
+ * comes from the router's signed release context (resolveReleaseId): the
17
+ * same release the catch-all of that request was rewritten to.
14
18
  */
15
19
  export async function resolveErrorPage(kind, config = {}) {
16
20
  let siteConfig = null;
@@ -13,7 +13,8 @@ import type { ErrorSeatPayload } from './error-seat.js';
13
13
  * - present-but-broken bindings — exactly the enumerated REQ-5 failures
14
14
  * (malformed route, unresolved page, unpublished page, audience/experiment
15
15
  * overlays — REQ-11) → `errors` in BOTH postures;
16
- * - ANY other exception (stale/corrupt compiled content, old manifest
16
+ * - selected fragment binding or expansion failures are errors in both postures;
17
+ * - other exceptions (stale/corrupt compiled content, old manifest
17
18
  * shapes) warns-and-defaults under warn and errors only under enforce, so
18
19
  * a stale .aci/compiled tree can never break a binding-less build.
19
20
  *
@@ -1,3 +1,4 @@
1
+ import { FragmentResolutionError, resolveSlotsSync } from '../content/resolve-slots.js';
1
2
  import { existsSync, readFileSync } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { ERROR_PAGE_ROUTE_PATTERN } from '../content/error-pages.js';
@@ -86,29 +87,13 @@ export function buildErrorSeatPayload(contentRoot, posture) {
86
87
  const route = bindings.catchAll;
87
88
  const page = readJSON(root, catchAllEntry.payloadRef);
88
89
  const layout = readJSON(root, `layouts/${page.layout}.json`);
89
- const slots = {};
90
- const slotOrder = [];
91
- for (const slot of layout.slots) {
92
- slotOrder.push(slot.name);
93
- const pageBlocks = page.regions?.[slot.name];
94
- if (pageBlocks?.length) {
95
- slots[slot.name] = pageBlocks;
96
- continue;
97
- }
98
- const defaults = layout.defaults?.[slot.name];
99
- if (!defaults?.length)
100
- continue;
101
- const blocks = [];
102
- for (const ref of defaults) {
103
- const fragmentRef = manifest.fragments[ref.fragmentId]?.payloadRef;
104
- if (!fragmentRef)
105
- continue;
106
- const fragment = readJSON(root, fragmentRef);
107
- blocks.push({ $type: 'component', id: fragment.id, component: fragment.component, props: fragment.props || {} });
108
- }
109
- if (blocks.length)
110
- slots[slot.name] = blocks;
111
- }
90
+ const slots = resolveSlotsSync(layout, page.regions ?? {}, (id) => {
91
+ const fragmentRef = manifest.fragments[id]?.payloadRef;
92
+ if (!fragmentRef)
93
+ throw new Error(`Fragment not found: ${id}`);
94
+ return readJSON(root, fragmentRef);
95
+ });
96
+ const slotOrder = layout.slots.map((slot) => slot.name);
112
97
  return {
113
98
  payload: {
114
99
  defaulted: false,
@@ -123,7 +108,12 @@ export function buildErrorSeatPayload(contentRoot, posture) {
123
108
  };
124
109
  }
125
110
  catch (error) {
126
- // Not one of the enumerated REQ-5 binding failures (those are pushed to
111
+ if (error instanceof FragmentResolutionError) {
112
+ errors.push(error.message);
113
+ return defaulted();
114
+ }
115
+ // Neither a selected-fragment failure nor an enumerated REQ-5 binding
116
+ // failure (those are pushed to
127
117
  // `errors` above): stale/corrupt compiled content or an old manifest
128
118
  // shape. Warn-and-default under the warn posture so it can never break a
129
119
  // build; fail closed only under enforce (quorum RISK-002).
@@ -69,7 +69,7 @@ function renderPayload(data, registry) {
69
69
  // async and server-oriented). Unknown components are skipped — the error
70
70
  // seat degrades, it never crashes.
71
71
  function renderClientBlocks(blocks, registry) {
72
- return blocks.map((block) => {
72
+ return blocks.map((block, index) => {
73
73
  const entry = registry.find((r) => r.id === block.component);
74
74
  if (!entry)
75
75
  return null;
@@ -78,6 +78,11 @@ function renderClientBlocks(blocks, registry) {
78
78
  if (entry.contract.blockSlots) {
79
79
  componentProps.renderChildren = (children) => renderClientBlocks(children, registry);
80
80
  }
81
- return _jsx(Component, { ...componentProps }, block.id);
81
+ // sdk I-19 (ADR 0051): the error seat is an injection seam too.
82
+ componentProps['data-aci-block'] = block.id;
83
+ componentProps['data-aci-component'] = block.component;
84
+ // Index+id composite: a repeated fragment placement repeats its authored
85
+ // id, and sibling keys must stay unique for React reconciliation.
86
+ return _jsx(Component, { ...componentProps }, `${index}:${block.id}`);
82
87
  });
83
88
  }
@@ -2,10 +2,10 @@ export { DevRefresh, type DevRefreshProps } from './dev-refresh.js';
2
2
  export { withAci, type WithAciOptions } from './config.js';
3
3
  export { isPreviewMode } from './preview-mode.js';
4
4
  export { createPage, type CreatePageOptions, type SlotPropsFn } from './page.js';
5
+ export { renderBlock, renderBlocks } from './render.js';
5
6
  export { getSiteConfig, getFragment, getLayout, getPage, getRenderInput, routeFromNextParams } from './server.js';
6
7
  export { PageNotFoundError, FragmentNotFoundError, LayoutNotFoundError } from './server.js';
7
8
  export { CollectScript, type CollectScriptProps } from './collect-script.js';
8
- export { aciMiddlewareMatcher } from './middleware.js';
9
9
  export { AssignmentsScript, type AssignmentsScriptProps } from './assignments-script.js';
10
10
  export { createNotFound, renderPageForRoute, resolveErrorPage, type NextResolvedErrorPage, } from './error-pages.js';
11
11
  export { ErrorPageInstrumentation } from './error-page-instrumentation.js';
@@ -2,10 +2,12 @@ export { DevRefresh } from './dev-refresh.js';
2
2
  export { withAci } from './config.js';
3
3
  export { isPreviewMode } from './preview-mode.js';
4
4
  export { createPage } from './page.js';
5
+ // The block renderer seam (sdk I-19, ADR 0051): exported so a site's own
6
+ // conformance test can prove its components carry root identity.
7
+ export { renderBlock, renderBlocks } from './render.js';
5
8
  export { getSiteConfig, getFragment, getLayout, getPage, getRenderInput, routeFromNextParams } from './server.js';
6
9
  export { PageNotFoundError, FragmentNotFoundError, LayoutNotFoundError } from './server.js';
7
10
  export { CollectScript } from './collect-script.js';
8
- export { aciMiddlewareMatcher } from './middleware.js';
9
11
  export { AssignmentsScript } from './assignments-script.js';
10
12
  export { createNotFound, renderPageForRoute, resolveErrorPage, } from './error-pages.js';
11
13
  export { ErrorPageInstrumentation } from './error-page-instrumentation.js';
@@ -34,7 +34,8 @@ export interface CreatePageOptions {
34
34
  * to `/__r/{releaseId}/{targetRef}`. The target ref selects the exact immutable
35
35
  * base or variant page while its prefix identifies the logical public route.
36
36
  * createPage passes both identities to the content loader, giving each target
37
- * a distinct CDN cache key without sending selection headers into Next.
37
+ * a distinct CDN cache key without sending selection headers into Next. The
38
+ * mimic response is decorated by the hosting platform after rendering.
38
39
  *
39
40
  * Dev pass-through (ACI_CONTENT_PROVIDER=file): the middleware passes
40
41
  * requests through unrewritten, so `/` arrives with an empty/undefined
package/dist/next/page.js CHANGED
@@ -32,7 +32,8 @@ import { parseHostedPageTarget } from './hosted-page-target.js';
32
32
  * to `/__r/{releaseId}/{targetRef}`. The target ref selects the exact immutable
33
33
  * base or variant page while its prefix identifies the logical public route.
34
34
  * createPage passes both identities to the content loader, giving each target
35
- * a distinct CDN cache key without sending selection headers into Next.
35
+ * a distinct CDN cache key without sending selection headers into Next. The
36
+ * mimic response is decorated by the hosting platform after rendering.
36
37
  *
37
38
  * Dev pass-through (ACI_CONTENT_PROVIDER=file): the middleware passes
38
39
  * requests through unrewritten, so `/` arrives with an empty/undefined
@@ -84,9 +85,9 @@ export function createPage(registry, options) {
84
85
  }
85
86
  return (_jsx(Fragment, { children: renderedBlocks }, slot.name));
86
87
  }));
87
- return (_jsxs(_Fragment, { children: [renderProofContent
88
+ return (_jsxs(_Fragment, { children: [renderedSlots.filter(Boolean), _jsx(AssignmentsScript, { page: input.page }), _jsx(CollectScript, { siteConfig: input.siteConfig }), renderProofContent
88
89
  ? _jsx("meta", { name: "aci-render-proof", content: renderProofContent })
89
- : null, renderedSlots.filter(Boolean), _jsx(AssignmentsScript, { page: input.page }), _jsx(CollectScript, { siteConfig: input.siteConfig })] }));
90
+ : null] }));
90
91
  }
91
92
  catch (error) {
92
93
  if (error instanceof PageNotFoundError)
@@ -100,6 +101,8 @@ export function createPage(registry, options) {
100
101
  default: CatchAllPage,
101
102
  };
102
103
  }
104
+ // Immutable render identity, shared by ordinary and platform-fetched HTML.
105
+ const SHARED_RENDER_SEGMENT = '__r';
103
106
  /**
104
107
  * Resolves the logical route and releaseId from Next.js catch-all params.
105
108
  *
@@ -119,7 +122,7 @@ export function createPage(registry, options) {
119
122
  export async function resolveRouteAndRelease(params) {
120
123
  const resolved = await params;
121
124
  const slug = resolved.slug ?? [];
122
- if (slug.length >= 2 && slug[0] === '__r') {
125
+ if (slug.length >= 2 && slug[0] === SHARED_RENDER_SEGMENT) {
123
126
  const releaseId = decodeURIComponent(slug[1]);
124
127
  if (!releaseId || releaseId.includes('/') || releaseId === '.' || releaseId === '..') {
125
128
  return { route: '/' };
@@ -133,6 +136,14 @@ export async function resolveRouteAndRelease(params) {
133
136
  pageRef: target.pageRef,
134
137
  };
135
138
  }
139
+ // These namespaces belong to native Next/static/API handlers. A missing
140
+ // file can fall through to this catch-all without the hosting router's
141
+ // release selection; it is a native 404, not a CMS content request. The
142
+ // shared resolver guards both page rendering and generateMetadata.
143
+ if (slug[0] === 'favicon.ico'
144
+ || ['assets', '_astro', '_next', 'api'].includes(slug[0])) {
145
+ notFound();
146
+ }
136
147
  const route = routeFromSlug(slug);
137
148
  return { route };
138
149
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The release identity of a request the hosting router rewrote to
3
+ * `/__r/{releaseId}/…`. The
4
+ * catch-all reads the release from that path; Next
5
+ * code that receives no route params (the not-found seat, release-less SDK
6
+ * reads such as a secondary `getPage('/blog')`) reads it from this header.
7
+ *
8
+ * Mirrors the router's signer byte for byte
9
+ * (internal/domain/hosting/runtime/edgeoutput/vercel_middleware.ts):
10
+ * `v1.<base64url HMAC-SHA256>.<releaseId>`, keyed by the deployment's
11
+ * ACI_INTERNAL_ARTIFACT_SECRET over "gradial.release-context.v1\n" +
12
+ * releaseId. The router strips any inbound copy, but requests under /api,
13
+ * /assets, /_astro, /_next and /favicon.ico bypass the router and reach Next
14
+ * with the visitor's own headers — so presence alone proves nothing and an
15
+ * unverifiable value selects no release.
16
+ */
17
+ export declare const RELEASE_CONTEXT_HEADER = "x-aci-release-context";
18
+ /** Returns the release a context value names when its signature verifies. */
19
+ export declare function verifyReleaseContext(value: string, secret: string): string | null;
20
+ export interface RoutedRelease {
21
+ /** The verified release the router dispatched this request to. */
22
+ releaseId?: string;
23
+ /** Why a present context was not used; it never selects a release. */
24
+ rejected?: string;
25
+ }
26
+ /**
27
+ * Reads the routed release of the current request. Outside a request scope
28
+ * (build, static generation, tooling) or without the header there is none.
29
+ */
30
+ export declare function routedRelease(): Promise<RoutedRelease>;
@@ -0,0 +1,56 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ import { headers } from 'next/headers.js';
3
+ /**
4
+ * The release identity of a request the hosting router rewrote to
5
+ * `/__r/{releaseId}/…`. The
6
+ * catch-all reads the release from that path; Next
7
+ * code that receives no route params (the not-found seat, release-less SDK
8
+ * reads such as a secondary `getPage('/blog')`) reads it from this header.
9
+ *
10
+ * Mirrors the router's signer byte for byte
11
+ * (internal/domain/hosting/runtime/edgeoutput/vercel_middleware.ts):
12
+ * `v1.<base64url HMAC-SHA256>.<releaseId>`, keyed by the deployment's
13
+ * ACI_INTERNAL_ARTIFACT_SECRET over "gradial.release-context.v1\n" +
14
+ * releaseId. The router strips any inbound copy, but requests under /api,
15
+ * /assets, /_astro, /_next and /favicon.ico bypass the router and reach Next
16
+ * with the visitor's own headers — so presence alone proves nothing and an
17
+ * unverifiable value selects no release.
18
+ */
19
+ export const RELEASE_CONTEXT_HEADER = 'x-aci-release-context';
20
+ const SECRET_ENV = 'ACI_INTERNAL_ARTIFACT_SECRET';
21
+ const MESSAGE_PREFIX = 'gradial.release-context.v1\n';
22
+ const RELEASE_CONTEXT_PATTERN = /^v1\.([A-Za-z0-9_-]{43})\.([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/;
23
+ /** Returns the release a context value names when its signature verifies. */
24
+ export function verifyReleaseContext(value, secret) {
25
+ const match = RELEASE_CONTEXT_PATTERN.exec(value);
26
+ if (!match || !secret)
27
+ return null;
28
+ const [, signature, releaseId] = match;
29
+ const expected = createHmac('sha256', secret).update(MESSAGE_PREFIX + releaseId).digest();
30
+ const actual = Buffer.from(signature, 'base64url');
31
+ if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
32
+ return null;
33
+ return releaseId;
34
+ }
35
+ /**
36
+ * Reads the routed release of the current request. Outside a request scope
37
+ * (build, static generation, tooling) or without the header there is none.
38
+ */
39
+ export async function routedRelease() {
40
+ let value;
41
+ try {
42
+ value = (await headers()).get(RELEASE_CONTEXT_HEADER);
43
+ }
44
+ catch {
45
+ // headers() has no request context in static generation and unit tests.
46
+ return {};
47
+ }
48
+ if (!value)
49
+ return {};
50
+ const secret = process.env[SECRET_ENV]?.trim() || '';
51
+ if (!secret) {
52
+ return { rejected: `${SECRET_ENV} is not configured, so the routed release context cannot be verified` };
53
+ }
54
+ const releaseId = verifyReleaseContext(value, secret);
55
+ return releaseId ? { releaseId } : { rejected: 'the routed release context failed verification' };
56
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ReactElement } from 'react';
2
2
  import type { KernelPageBlock } from '../content/types.js';
3
3
  import type { Registry } from '../registry.js';
4
- export declare function renderBlock(block: KernelPageBlock, registry: Registry, extraProps?: Record<string, unknown>): Promise<ReactElement>;
4
+ export declare function renderBlock(block: KernelPageBlock, registry: Registry, key: string, extraProps?: Record<string, unknown>): Promise<ReactElement>;
5
5
  export declare function renderBlocks(blocks: readonly KernelPageBlock[], registry: Registry, extraProps?: Record<string, unknown>): Promise<ReactElement[]>;