@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.
- package/CHANGELOG.md +126 -0
- package/README.md +86 -5
- package/dist/astro/error-page.js +1 -0
- package/dist/astro/index.js +4 -0
- package/dist/astro/page.d.ts +5 -2
- package/dist/astro/page.js +35 -23
- package/dist/astro/preview.js +2 -17
- package/dist/audience-data-models.d.ts +308 -197
- package/dist/audience-data-models.js +313 -51
- package/dist/cli/compile-registry.mjs +6 -34
- package/dist/cli/mintlify-render.mjs +8 -0
- package/dist/collect/aci.js +1 -1
- package/dist/collect/aci.js.sha256 +1 -1
- package/dist/collect/api.d.ts +6 -7
- package/dist/collect/api.js +21 -8
- package/dist/collect/assignments.d.ts +7 -9
- package/dist/collect/assignments.js +23 -32
- package/dist/collect/batcher.d.ts +9 -7
- package/dist/collect/batcher.js +6 -5
- package/dist/collect/consent/envelope.d.ts +7 -1
- package/dist/collect/consent/envelope.js +3 -1
- package/dist/collect/consent/mapping.d.ts +2 -0
- package/dist/collect/consent/mapping.js +1 -0
- package/dist/collect/consenthold.d.ts +38 -0
- package/dist/collect/consenthold.js +82 -0
- package/dist/collect/declarative.js +7 -5
- package/dist/collect/gapbuffer.d.ts +13 -4
- package/dist/collect/gapbuffer.js +21 -10
- package/dist/collect/index.js +78 -21
- package/dist/collect/pageviews.d.ts +11 -11
- package/dist/collect/pageviews.js +27 -22
- package/dist/consent-banner/index.d.ts +17 -0
- package/dist/consent-banner/index.js +23 -0
- package/dist/content/index.d.ts +2 -1
- package/dist/content/index.js +4 -1
- package/dist/content/provider.d.ts +16 -2
- package/dist/content/provider.js +5 -1
- package/dist/content/resolve-slots.d.ts +24 -2
- package/dist/content/resolve-slots.js +127 -21
- package/dist/content/types.d.ts +5 -1
- package/dist/edge/expression-capabilities.v1.d.ts +106 -0
- package/dist/edge/expression-capabilities.v1.js +165 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -1
- package/dist/internal/domain/edge/identity.d.ts +47 -4
- package/dist/internal/domain/edge/identity.js +130 -26
- package/dist/internal/domain/edge/index.d.ts +1 -1
- package/dist/internal/domain/edge/index.js +1 -1
- package/dist/internal/domain/edge/mimic_token.d.ts +14 -0
- package/dist/internal/domain/edge/mimic_token.js +125 -0
- package/dist/internal/domain/edge/personalization-core.d.ts +61 -22
- package/dist/internal/domain/edge/personalization-core.js +790 -200
- package/dist/internal/domain/edge/personalization_index.d.ts +17 -0
- package/dist/internal/domain/edge/personalization_index.js +55 -0
- package/dist/internal/domain/edge/resolve.js +76 -39
- package/dist/internal/domain/edge/testdata/flat_only_validator_v1.d.ts +10 -0
- package/dist/internal/domain/edge/testdata/flat_only_validator_v1.js +253 -0
- package/dist/internal/domain/edge/types.d.ts +82 -78
- package/dist/internal/domain/edge/types.js +3 -3
- package/dist/internal/domain/edge/variant_key.d.ts +4 -0
- package/dist/internal/domain/edge/variant_key.js +56 -0
- package/dist/internal/platform/collectionstore/errors.d.ts +24 -0
- package/dist/internal/platform/collectionstore/errors.js +48 -0
- package/dist/mintlify/blocks.d.ts +6 -0
- package/dist/mintlify/blocks.js +201 -0
- package/dist/mintlify/config.d.ts +3 -0
- package/dist/mintlify/config.js +77 -0
- package/dist/mintlify/index.d.ts +3 -0
- package/dist/mintlify/index.js +2 -0
- package/dist/mintlify/json.d.ts +8 -0
- package/dist/mintlify/json.js +37 -0
- package/dist/mintlify/materialize.d.ts +14 -0
- package/dist/mintlify/materialize.js +254 -0
- package/dist/mintlify/types.d.ts +74 -0
- package/dist/mintlify/types.js +1 -0
- package/dist/mintlify/validate.d.ts +7 -0
- package/dist/mintlify/validate.js +256 -0
- package/dist/next/assignments-navigation.d.ts +4 -0
- package/dist/next/assignments-navigation.js +18 -0
- package/dist/next/assignments-script.d.ts +3 -2
- package/dist/next/assignments-script.js +5 -5
- package/dist/next/error-pages.d.ts +4 -0
- package/dist/next/error-pages.js +4 -0
- package/dist/next/error-seat-payload.d.ts +2 -1
- package/dist/next/error-seat-payload.js +14 -24
- package/dist/next/error-seat.js +7 -2
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +3 -1
- package/dist/next/page.d.ts +2 -1
- package/dist/next/page.js +15 -4
- package/dist/next/release-context.d.ts +30 -0
- package/dist/next/release-context.js +56 -0
- package/dist/next/render.d.ts +1 -1
- package/dist/next/render.js +15 -5
- package/dist/next/server.d.ts +9 -3
- package/dist/next/server.js +17 -32
- package/dist/preview/core.d.ts +1 -0
- package/dist/preview/core.js +27 -0
- package/dist/react/VideoPlayer.d.ts +3 -3
- package/dist/react/index.d.ts +3 -3
- package/dist/registry.d.ts +4 -2
- package/dist/testing/conformance/checks.d.ts +2 -2
- package/dist/testing/conformance/checks.js +2 -0
- package/dist/testing/conformance/index.d.ts +1 -0
- package/dist/testing/conformance/index.js +1 -0
- package/dist/testing/conformance/next-stubs.d.ts +6 -0
- package/dist/testing/conformance/next-stubs.js +9 -0
- package/dist/testing/conformance/preset.d.ts +3 -2
- package/dist/testing/conformance/preset.js +15 -8
- package/dist/testing/conformance/root-identity.d.ts +31 -0
- package/dist/testing/conformance/root-identity.js +361 -0
- package/dist/testing/conformance/stubs/next-image.d.ts +7 -0
- package/dist/testing/conformance/stubs/next-image.js +5 -0
- package/dist/testing/conformance/stubs/next-link.d.ts +7 -0
- package/dist/testing/conformance/stubs/next-link.js +4 -0
- package/dist/testing/conformance/types.d.ts +5 -2
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +4 -3
- package/dist/types/block-ref.js +2 -2
- package/dist/types/component.d.ts +9 -0
- package/dist/types/config.d.ts +0 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/page.d.ts +0 -1
- package/migrations/conformance-registry.md +22 -0
- package/migrations/experiment-goal-ids.md +43 -0
- package/package.json +21 -7
- package/src/cli/compile-registry.mjs +6 -30
- package/src/cli/mintlify-render.mjs +9 -0
- package/src/types/block-ref.test.ts +16 -1
- package/src/types/block-ref.ts +2 -2
- package/src/types/component.ts +10 -0
- package/src/types/config.ts +0 -1
- package/src/types/index.ts +0 -1
- package/src/types/page.ts +0 -1
- package/templates/astro/starter-content/src/components/chrome/Footer.astro +5 -3
- package/templates/astro/starter-content/src/components/chrome/Header.astro +5 -3
- package/templates/astro/starter-content/src/components/elements/Button.astro +5 -2
- package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +5 -3
- package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/HomeHero.astro +5 -3
- package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -3
- package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -3
- package/templates/astro/starter-content/src/fixtures/sections.ts +1 -1
- package/templates/astro/starter-content/src/stories/CompositionStoryFrame.astro +7 -2
- package/templates/astro/template/.aci.yaml.tmpl +0 -1
- package/templates/astro/template/package.json.tmpl +5 -5
- package/templates/astro/template/src/layouts/SiteLayout.astro +2 -2
- package/templates/common/.agents/skills/aci/SKILL.md +58 -1
- package/templates/common/.agents/skills/authoring/SKILL.md +194 -42
- package/templates/common/.agents/skills/authoring/references/block-registry.md +19 -3
- package/templates/common/.agents/skills/authoring/references/contracts.md +9 -30
- package/templates/common/.agents/skills/authoring/references/repo-structure.md +5 -4
- package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +3 -3
- package/templates/common/.agents/skills/dam-delivery/SKILL.md +216 -0
- package/templates/common/.agents/skills/dam-delivery/references/permalink-grammar.md +167 -0
- package/templates/common/.agents/skills/migration/references/canon-migration.md +18 -67
- package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
- package/templates/common/.content/fragments/navbar/_index.json +24 -0
- package/templates/common/.content/fragments/site-footer/_index.json +61 -0
- package/templates/common/.content/pages/404/_index.json +4 -2
- package/templates/common/.content/pages/_index.json +0 -1
- package/templates/common/.content/pages/error/_index.json +4 -2
- package/templates/common/.content/schemas/site.schema.json +11 -1
- package/templates/common/.content/templates/error.json +24 -0
- package/templates/common/.content/templates/home.json +28 -0
- package/templates/common/.content/templates/site.json +8 -0
- package/templates/common/.content/templates/standard.json +24 -0
- package/templates/common/.nvmrc +1 -1
- package/templates/common/starter-content/.content/config/site.json +1 -1
- package/templates/common/starter-content/.content/dam/README.md +74 -0
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/asset.json +1 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/asset.json +1 -1
- package/templates/common/starter-content/.content/pages/404/_index.json +1 -2
- package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +14 -3
- package/templates/common/starter-content/.content/pages/about/_index.json +2 -2
- package/templates/common/starter-content/.content/pages/error/_index.json +1 -2
- package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/container.json +8 -3
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +5 -3
- package/templates/common/starter-content/.content/patterns/components/home_hero.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/site-footer.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/site-navigation.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/stats_bar.json +2 -2
- package/templates/common/starter-content/.content/schemas/site.schema.json +11 -1
- package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/elements/Button.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +4 -3
- package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +4 -2
- package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +4 -3
- package/templates/nextjs/template/.aci.yaml +0 -1
- package/templates/nextjs/template/.aci.yaml.tmpl +0 -1
- package/templates/nextjs/template/package.json.tmpl +4 -3
- package/templates/nextjs/template/src/app/_components/c15t-bridge.ts +30 -0
- package/templates/nextjs/template/src/app/_components/c15t-consent-ui.tsx +37 -0
- package/templates/nextjs/template/src/app/_components/consent-manager.css +63 -0
- package/templates/nextjs/template/src/app/_components/consent-manager.tsx +76 -0
- package/templates/nextjs/template/src/app/fonts/inter/OFL.txt +93 -0
- package/templates/nextjs/template/src/app/fonts/inter/inter-latin.woff2 +0 -0
- package/templates/nextjs/template/src/app/fonts/space-grotesk/OFL.txt +93 -0
- package/templates/nextjs/template/src/app/fonts/space-grotesk/space-grotesk-latin.woff2 +0 -0
- package/templates/nextjs/template/src/app/layout.tsx +12 -7
- package/templates/nextjs/template/tests/conformance.test.ts.tmpl +1 -0
- package/dist/define-layout.d.ts +0 -6
- package/dist/define-layout.js +0 -14
- package/dist/edge/personalization-core.d.ts +0 -11
- package/dist/edge/personalization-core.js +0 -269
- package/dist/internal/domain/edge/compose.d.ts +0 -24
- package/dist/internal/domain/edge/compose.js +0 -89
- package/dist/internal/domain/edge/record/codec.d.ts +0 -71
- package/dist/internal/domain/edge/record/codec.js +0 -401
- package/dist/next/collect-route.d.ts +0 -19
- package/dist/next/collect-route.js +0 -459
- package/dist/next/edge-config.d.ts +0 -1
- package/dist/next/edge-config.js +0 -92
- package/dist/next/envelope-digest.d.ts +0 -21
- package/dist/next/envelope-digest.js +0 -58
- package/dist/next/middleware.d.ts +0 -40
- package/dist/next/middleware.js +0 -475
- package/dist/next/personalization-host.d.ts +0 -134
- package/dist/next/personalization-host.js +0 -270
- package/dist/next/root-layout.d.ts +0 -4
- package/dist/next/root-layout.js +0 -5
- package/dist/next/vercel-collect-sdk-route.d.ts +0 -27
- package/dist/next/vercel-collect-sdk-route.js +0 -181
- package/dist/next/vercel-edge-projection-v1.d.ts +0 -24
- package/dist/next/vercel-edge-projection-v1.js +0 -880
- package/dist/next/vercel-personalization.d.ts +0 -4
- package/dist/next/vercel-personalization.js +0 -4
- package/dist/next/vercel-s3-personalization.d.ts +0 -79
- package/dist/next/vercel-s3-personalization.js +0 -570
- package/dist/types/layout.d.ts +0 -24
- package/src/types/layout.ts +0 -29
- package/templates/astro/starter-content/src/render/RenderPage.astro +0 -67
- package/templates/astro/template/src/cms/contracts/layouts/index.ts +0 -18
- package/templates/common/starter-content/.content/patterns/layouts/default.json +0 -14
- package/templates/nextjs/starter-content/src/components/elements/AtomicBlock.tsx +0 -13
- package/templates/nextjs/template/src/cms/contracts/layouts/index.ts +0 -16
- /package/dist/{types/layout.js → cli/mintlify-render.d.mts} +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +0 -0
- /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
|
@@ -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,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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
/**
|
package/dist/next/error-pages.js
CHANGED
|
@@ -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
|
-
* -
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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).
|
package/dist/next/error-seat.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/next/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/next/index.js
CHANGED
|
@@ -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';
|
package/dist/next/page.d.ts
CHANGED
|
@@ -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
|
|
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] ===
|
|
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
|
+
}
|
package/dist/next/render.d.ts
CHANGED
|
@@ -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[]>;
|