@gradial/aci 0.1.52-rc.3 → 0.1.52-rc.34-e2e.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -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 -194
- package/dist/audience-data-models.js +314 -51
- package/dist/cli/compile-registry.d.mts +5 -1
- package/dist/cli/compile-registry.mjs +31 -46
- package/dist/cli/mintlify-render.mjs +8 -0
- package/dist/cli/validate-content.d.mts +31 -1
- package/dist/cli/validate-content.mjs +50 -12
- 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 +67 -20
- 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 +33 -4
- package/dist/content/resolve-slots.js +127 -19
- package/dist/content/types.d.ts +7 -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/compose.d.ts +24 -0
- package/dist/internal/domain/edge/compose.js +89 -0
- package/dist/internal/domain/edge/identity.d.ts +5 -0
- package/dist/internal/domain/edge/identity.js +50 -14
- package/dist/internal/domain/edge/index.d.ts +2 -0
- package/dist/internal/domain/edge/index.js +2 -0
- 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 +789 -198
- package/dist/internal/domain/edge/personalization_index.d.ts +17 -0
- package/dist/internal/domain/edge/personalization_index.js +56 -0
- package/dist/internal/domain/edge/record/codec.d.ts +6 -32
- package/dist/internal/domain/edge/record/codec.js +86 -168
- 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 +93 -41
- package/dist/internal/domain/edge/types.js +3 -3
- package/dist/internal/domain/edge/variant_key.d.ts +3 -0
- package/dist/internal/domain/edge/variant_key.js +44 -0
- package/dist/internal/platform/collectionstore/errors.d.ts +24 -0
- package/dist/internal/platform/collectionstore/errors.js +48 -0
- package/dist/mimic/core.d.ts +2 -0
- package/dist/mimic/core.js +31 -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/hosted-page-target.d.ts +7 -0
- package/dist/next/hosted-page-target.js +39 -0
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +3 -1
- package/dist/next/page.d.ts +29 -6
- package/dist/next/page.js +47 -25
- package/dist/next/release-context.d.ts +29 -0
- package/dist/next/release-context.js +55 -0
- package/dist/next/render.d.ts +1 -1
- package/dist/next/render.js +15 -5
- package/dist/next/server.d.ts +8 -0
- package/dist/next/server.js +44 -3
- package/dist/next/vercel-edge-projection-v1.d.ts +0 -9
- package/dist/next/vercel-edge-projection-v1.js +9 -539
- 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/VideoPlayer.js +2 -2
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +2 -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.d.ts +2 -0
- package/dist/types/block-ref.js +3 -2
- package/dist/types/component.d.ts +9 -0
- package/dist/types/config.d.ts +0 -1
- package/dist/types/image.d.ts +54 -17
- package/dist/types/image.js +102 -18
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/media.d.ts +14 -6
- package/dist/types/page.d.ts +0 -1
- package/dist/types/video.d.ts +7 -3
- package/migrations/conformance-registry.md +22 -0
- package/package.json +21 -7
- package/src/cli/compile-registry.mjs +30 -42
- package/src/cli/mintlify-render.mjs +9 -0
- package/src/cli/validate-content.mjs +52 -13
- package/src/types/block-ref.test.ts +60 -0
- package/src/types/block-ref.ts +4 -2
- package/src/types/component.ts +10 -0
- package/src/types/config.ts +0 -1
- package/src/types/image.ts +133 -29
- package/src/types/index.ts +0 -1
- package/src/types/page.ts +0 -1
- package/src/types/video.ts +1 -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/atomicBlocks.ts +7 -6
- package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
- package/templates/astro/starter-content/src/fixtures/sections.ts +3 -3
- 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 +203 -43
- package/templates/common/.agents/skills/authoring/references/block-registry.md +19 -3
- package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
- 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 +217 -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/{home/_index.json → _index.json} +10 -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/.rules-baseline.json +8 -0
- package/templates/common/starter-content/.content/config/site.json +1 -1
- package/templates/common/starter-content/.content/content-rules/no-public-resource-paths.json +6 -0
- package/templates/common/starter-content/.content/dam/README.md +74 -0
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/asset.json +1 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +2 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/asset.json +1 -1
- package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +2 -1
- package/templates/common/starter-content/.content/fragments/navbar/_index.json +23 -20
- package/templates/common/starter-content/.content/fragments/site-footer/_index.json +59 -56
- package/templates/common/starter-content/.content/pages/404/_index.json +2 -2
- package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +23 -3
- package/templates/common/starter-content/.content/pages/about/_index.json +5 -2
- package/templates/common/starter-content/.content/pages/error/_index.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/button.json +55 -0
- package/templates/common/starter-content/.content/patterns/components/container.json +67 -0
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +30 -0
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +59 -0
- package/templates/common/starter-content/.content/patterns/components/home_hero.json +151 -0
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +42 -0
- package/templates/common/starter-content/.content/patterns/components/site-footer.json +203 -0
- package/templates/common/starter-content/.content/patterns/components/site-navigation.json +115 -0
- package/templates/common/starter-content/.content/patterns/components/stats_bar.json +45 -0
- 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 +9 -0
- 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/next/collect-route.d.ts +0 -19
- package/dist/next/collect-route.js +0 -459
- 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-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/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/.content/overlays/audience/preference-blue/pages/{home/_index.json → _index.json} +0 -0
- /package/templates/common/.content/overlays/audience/preference-red/pages/{home/_index.json → _index.json} +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_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_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
package/dist/content/provider.js
CHANGED
|
@@ -24,8 +24,11 @@ export class LayoutNotFoundError extends Error {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
import { getCachedSiteConfig } from './cache.js';
|
|
27
|
-
export async function loadRenderInput(provider, route = '/', options
|
|
27
|
+
export async function loadRenderInput(provider, route = '/', options) {
|
|
28
28
|
const normalized = normalizeRoute(route);
|
|
29
|
+
if (typeof options?.variantKey !== 'string' || !options.variantKey.trim() || options.variantKey !== options.variantKey.trim()) {
|
|
30
|
+
throw new Error(`ACI render variant identity is missing or invalid for route ${normalized}; supply an explicit variantKey`);
|
|
31
|
+
}
|
|
29
32
|
const [siteConfig, page] = await Promise.all([
|
|
30
33
|
getCachedSiteConfig(provider),
|
|
31
34
|
provider.getPage(normalized).catch((error) => {
|
|
@@ -36,6 +39,7 @@ export async function loadRenderInput(provider, route = '/', options = {}) {
|
|
|
36
39
|
]);
|
|
37
40
|
return {
|
|
38
41
|
route: normalized,
|
|
42
|
+
variantKey: options.variantKey,
|
|
39
43
|
domain: options.domain || siteConfig.domain,
|
|
40
44
|
locale: (options.locale || siteConfig.defaultLocale || 'en-us').toLowerCase(),
|
|
41
45
|
siteConfig,
|
|
@@ -1,9 +1,38 @@
|
|
|
1
1
|
import type { LayoutEntry } from './provider.js';
|
|
2
|
-
import type { KernelPageBlock } from './types.js';
|
|
3
|
-
export interface
|
|
4
|
-
|
|
2
|
+
import type { KernelPageBlock, KernelPageNode } from './types.js';
|
|
3
|
+
export interface FragmentBlock {
|
|
4
|
+
$type: 'component';
|
|
5
5
|
component: string;
|
|
6
6
|
props?: Record<string, unknown>;
|
|
7
7
|
}
|
|
8
|
+
export interface FragmentData {
|
|
9
|
+
fragmentId?: string;
|
|
10
|
+
id?: string;
|
|
11
|
+
block?: FragmentBlock;
|
|
12
|
+
component?: string;
|
|
13
|
+
props?: Record<string, unknown>;
|
|
14
|
+
}
|
|
8
15
|
export type FragmentLoader = (id: string) => Promise<FragmentData>;
|
|
9
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Shared Go/TS limit contract (sdk I-21). These values are pinned against
|
|
18
|
+
* internal/domain/content/compiler/testdata/limit-conformance.v1.json by
|
|
19
|
+
* limit-conformance.test.ts (TS) and limit_conformance_test.go (Go); the
|
|
20
|
+
* compiler's MaxFragmentDepth/MaxExpandedBlocks must match.
|
|
21
|
+
*/
|
|
22
|
+
/** Bounds a selected fragment reference chain, counting its root fragment. */
|
|
23
|
+
export declare const MAX_FRAGMENT_DEPTH = 10;
|
|
24
|
+
/** Bounds rendered component occurrences per page, including repeated placements. */
|
|
25
|
+
export declare const MAX_EXPANDED_BLOCKS = 4096;
|
|
26
|
+
/** Failures in a selected fragment are never eligible for local-content fallback. */
|
|
27
|
+
export declare class FragmentResolutionError extends Error {
|
|
28
|
+
constructor(message: string, options?: ErrorOptions);
|
|
29
|
+
}
|
|
30
|
+
export declare const fragmentRootScope: unique symbol;
|
|
31
|
+
export declare function preserveFragmentScope<T extends object>(source: object, target: T): T;
|
|
32
|
+
export declare function resolveSlots(plan: LayoutEntry, pageRegions: Record<string, KernelPageNode[]>, getFragment: FragmentLoader): Promise<Record<string, KernelPageBlock[]>>;
|
|
33
|
+
/** The config-time error seat uses the identical traversal with synchronous file reads. */
|
|
34
|
+
export declare function resolveSlotsSync(plan: LayoutEntry, pageRegions: Record<string, KernelPageNode[]>, getFragment: (id: string) => FragmentData): Record<string, KernelPageBlock[]>;
|
|
35
|
+
/** Resolve compiled references through the exact fragment bindings selected for this page. */
|
|
36
|
+
export declare function resolveFragmentNodes(nodes: readonly KernelPageNode[], getFragment: FragmentLoader): Promise<KernelPageBlock[]>;
|
|
37
|
+
/** Use the same fragment root identity for layout defaults and region placements. */
|
|
38
|
+
export declare function fragmentToBlock(fragment: FragmentData, id: string): KernelPageBlock;
|
|
@@ -1,24 +1,132 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shared Go/TS limit contract (sdk I-21). These values are pinned against
|
|
3
|
+
* internal/domain/content/compiler/testdata/limit-conformance.v1.json by
|
|
4
|
+
* limit-conformance.test.ts (TS) and limit_conformance_test.go (Go); the
|
|
5
|
+
* compiler's MaxFragmentDepth/MaxExpandedBlocks must match.
|
|
6
|
+
*/
|
|
7
|
+
/** Bounds a selected fragment reference chain, counting its root fragment. */
|
|
8
|
+
export const MAX_FRAGMENT_DEPTH = 10;
|
|
9
|
+
/** Bounds rendered component occurrences per page, including repeated placements. */
|
|
10
|
+
export const MAX_EXPANDED_BLOCKS = 4096;
|
|
11
|
+
/** Failures in a selected fragment are never eligible for local-content fallback. */
|
|
12
|
+
export class FragmentResolutionError extends Error {
|
|
13
|
+
constructor(message, options) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.name = 'FragmentResolutionError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Runtime-only provenance keeps each fragment placement's authored IDs in its
|
|
19
|
+
// own validation scope. It is never emitted into compiled JSON or HTML.
|
|
20
|
+
export const fragmentRootScope = Symbol('aci.fragment-root-scope');
|
|
21
|
+
export function preserveFragmentScope(source, target) {
|
|
22
|
+
return Object.hasOwn(source, fragmentRootScope)
|
|
23
|
+
? Object.defineProperty(target, fragmentRootScope, { value: true })
|
|
24
|
+
: target;
|
|
25
|
+
}
|
|
26
|
+
export async function resolveSlots(plan, pageRegions, getFragment) {
|
|
27
|
+
return resolveAsync(walkSlots(plan, pageRegions), getFragment);
|
|
28
|
+
}
|
|
29
|
+
/** The config-time error seat uses the identical traversal with synchronous file reads. */
|
|
30
|
+
export function resolveSlotsSync(plan, pageRegions, getFragment) {
|
|
31
|
+
const walk = walkSlots(plan, pageRegions);
|
|
32
|
+
let step = walk.next();
|
|
33
|
+
while (!step.done) {
|
|
34
|
+
let fragment;
|
|
35
|
+
try {
|
|
36
|
+
fragment = getFragment(step.value);
|
|
8
37
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const blocks = [];
|
|
12
|
-
for (const ref of defaults) {
|
|
13
|
-
const fragment = await getFragment(ref.fragmentId);
|
|
14
|
-
blocks.push({
|
|
15
|
-
id: fragment.id,
|
|
16
|
-
component: fragment.component,
|
|
17
|
-
props: fragment.props || {},
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
resolved[slot.name] = blocks;
|
|
38
|
+
catch (cause) {
|
|
39
|
+
throw bindingError(step.value, cause);
|
|
21
40
|
}
|
|
41
|
+
step = walk.next(fragment);
|
|
42
|
+
}
|
|
43
|
+
return step.value;
|
|
44
|
+
}
|
|
45
|
+
/** Resolve compiled references through the exact fragment bindings selected for this page. */
|
|
46
|
+
export async function resolveFragmentNodes(nodes, getFragment) {
|
|
47
|
+
return resolveAsync(walkNodes(nodes, { blocks: 0 }), getFragment);
|
|
48
|
+
}
|
|
49
|
+
async function resolveAsync(walk, getFragment) {
|
|
50
|
+
let step = walk.next();
|
|
51
|
+
while (!step.done) {
|
|
52
|
+
let fragment;
|
|
53
|
+
try {
|
|
54
|
+
fragment = await getFragment(step.value);
|
|
55
|
+
}
|
|
56
|
+
catch (cause) {
|
|
57
|
+
throw bindingError(step.value, cause);
|
|
58
|
+
}
|
|
59
|
+
step = walk.next(fragment);
|
|
60
|
+
}
|
|
61
|
+
return step.value;
|
|
62
|
+
}
|
|
63
|
+
function bindingError(id, cause) {
|
|
64
|
+
return new FragmentResolutionError(`ACI fragment ${id} selected binding failed: ${String(cause)}`, { cause });
|
|
65
|
+
}
|
|
66
|
+
function* walkSlots(plan, pageRegions) {
|
|
67
|
+
const resolved = {};
|
|
68
|
+
const state = { blocks: 0 };
|
|
69
|
+
for (const slot of plan.slots) {
|
|
70
|
+
const pageBlocks = pageRegions[slot.name];
|
|
71
|
+
const nodes = pageBlocks?.length ? pageBlocks : plan.defaults?.[slot.name];
|
|
72
|
+
if (nodes?.length)
|
|
73
|
+
resolved[slot.name] = yield* walkNodes(nodes, state);
|
|
22
74
|
}
|
|
23
75
|
return resolved;
|
|
24
76
|
}
|
|
77
|
+
function* walkNodes(nodes, state) {
|
|
78
|
+
const blocks = [];
|
|
79
|
+
for (const node of nodes)
|
|
80
|
+
blocks.push((yield* walkValue(node, [], state)));
|
|
81
|
+
return blocks;
|
|
82
|
+
}
|
|
83
|
+
// Active ancestry belongs to each placement, while the block budget belongs to
|
|
84
|
+
// the whole rendered page. A repeated fragment in another branch is valid.
|
|
85
|
+
function* walkValue(value, ancestry, state) {
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
const result = [];
|
|
88
|
+
for (const child of value)
|
|
89
|
+
result.push(yield* walkValue(child, ancestry, state));
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
if (value === null || typeof value !== 'object')
|
|
93
|
+
return value;
|
|
94
|
+
const node = value;
|
|
95
|
+
if (node.kind === 'fragment-ref') {
|
|
96
|
+
const id = node.fragmentId;
|
|
97
|
+
if (typeof id !== 'string' || !id)
|
|
98
|
+
throw new FragmentResolutionError('ACI fragment reference has no fragmentId');
|
|
99
|
+
const chain = [...ancestry, id];
|
|
100
|
+
if (ancestry.includes(id))
|
|
101
|
+
throw new FragmentResolutionError(`ACI fragment cycle: ${chain.join(' -> ')}`);
|
|
102
|
+
if (chain.length > MAX_FRAGMENT_DEPTH)
|
|
103
|
+
throw new FragmentResolutionError(`ACI fragment depth exceeds ${MAX_FRAGMENT_DEPTH}: ${chain.join(' -> ')}`);
|
|
104
|
+
const fragment = yield id;
|
|
105
|
+
const block = (yield* walkValue(fragmentToBlock(fragment, id), chain, state));
|
|
106
|
+
return Object.defineProperty(block, fragmentRootScope, { value: true });
|
|
107
|
+
}
|
|
108
|
+
const isBlock = node.$type === 'component'
|
|
109
|
+
|| (typeof node.component === 'string' && node.component.length > 0
|
|
110
|
+
&& node.props !== null && typeof node.props === 'object' && !Array.isArray(node.props));
|
|
111
|
+
if (isBlock && ++state.blocks > MAX_EXPANDED_BLOCKS) {
|
|
112
|
+
throw new FragmentResolutionError(`ACI rendered page exceeds ${MAX_EXPANDED_BLOCKS} component blocks`);
|
|
113
|
+
}
|
|
114
|
+
const result = {};
|
|
115
|
+
for (const [key, child] of Object.entries(node))
|
|
116
|
+
result[key] = yield* walkValue(child, ancestry, state);
|
|
117
|
+
return preserveFragmentScope(node, result);
|
|
118
|
+
}
|
|
119
|
+
/** Use the same fragment root identity for layout defaults and region placements. */
|
|
120
|
+
export function fragmentToBlock(fragment, id) {
|
|
121
|
+
const block = fragment?.block ?? fragment;
|
|
122
|
+
if (!block || typeof block.component !== 'string' || !block.component
|
|
123
|
+
|| (block.props !== undefined && (typeof block.props !== 'object' || block.props === null || Array.isArray(block.props)))) {
|
|
124
|
+
throw new FragmentResolutionError(`ACI fragment ${id} has no component block`);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
$type: 'component',
|
|
128
|
+
id: fragment.id ?? fragment.fragmentId ?? id,
|
|
129
|
+
component: block.component,
|
|
130
|
+
props: block.props || {},
|
|
131
|
+
};
|
|
132
|
+
}
|
package/dist/content/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExperimentAssignmentContext, SitePersonalizationConfig } from './personalization.js';
|
|
2
|
+
import type { FragmentRefNode } from '../types/page.js';
|
|
2
3
|
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'outline' | 'ghost';
|
|
3
4
|
export interface KernelImage {
|
|
4
5
|
src: string;
|
|
@@ -42,11 +43,14 @@ export interface KernelPageSeo {
|
|
|
42
43
|
canonical?: string;
|
|
43
44
|
}
|
|
44
45
|
export interface KernelPageBlock {
|
|
46
|
+
$type: 'component';
|
|
45
47
|
id: string;
|
|
46
48
|
component: string;
|
|
47
49
|
props: Record<string, unknown>;
|
|
48
50
|
}
|
|
51
|
+
export type KernelPageNode = KernelPageBlock | FragmentRefNode;
|
|
49
52
|
export interface KernelAtomicBlock {
|
|
53
|
+
$type: 'component';
|
|
50
54
|
id: string;
|
|
51
55
|
component: string;
|
|
52
56
|
props: Record<string, unknown>;
|
|
@@ -57,7 +61,7 @@ export interface KernelPage {
|
|
|
57
61
|
status: string;
|
|
58
62
|
layout: string;
|
|
59
63
|
metadata: KernelPageSeo;
|
|
60
|
-
regions: Record<string,
|
|
64
|
+
regions: Record<string, KernelPageNode[]>;
|
|
61
65
|
/**
|
|
62
66
|
* Build-time experiment assignment context (personalization-reporting
|
|
63
67
|
* design/04); present only on experiment variant payloads. The starters
|
|
@@ -108,6 +112,8 @@ export interface KernelRouteMetadata {
|
|
|
108
112
|
}
|
|
109
113
|
export interface RenderInput<TPage extends KernelPage = KernelPage, TSiteConfig extends KernelSiteConfig = KernelSiteConfig> {
|
|
110
114
|
route: string;
|
|
115
|
+
/** Selected render tuple. Unpersonalized renders explicitly use "base". */
|
|
116
|
+
variantKey: string;
|
|
111
117
|
domain: string;
|
|
112
118
|
locale: string;
|
|
113
119
|
siteConfig: TSiteConfig;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export declare const expressionCapabilities: {
|
|
2
|
+
readonly schemaVersion: 1;
|
|
3
|
+
readonly contract: "personalization-expression";
|
|
4
|
+
readonly maxConditions: 64;
|
|
5
|
+
readonly maxDepth: 5;
|
|
6
|
+
readonly maxInValues: 32;
|
|
7
|
+
readonly grammars: {
|
|
8
|
+
readonly number: "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$";
|
|
9
|
+
readonly timestamp: "^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$";
|
|
10
|
+
};
|
|
11
|
+
readonly operators: readonly [{
|
|
12
|
+
readonly name: "equals";
|
|
13
|
+
readonly operand: "scalar";
|
|
14
|
+
readonly compact: "eq";
|
|
15
|
+
}, {
|
|
16
|
+
readonly name: "contains";
|
|
17
|
+
readonly operand: "scalar";
|
|
18
|
+
readonly compact: "ct";
|
|
19
|
+
}, {
|
|
20
|
+
readonly name: "startsWith";
|
|
21
|
+
readonly operand: "scalar";
|
|
22
|
+
readonly compact: "sw";
|
|
23
|
+
}, {
|
|
24
|
+
readonly name: "exists";
|
|
25
|
+
readonly operand: "optionalBoolean";
|
|
26
|
+
readonly compact: "ex";
|
|
27
|
+
}, {
|
|
28
|
+
readonly name: "greaterThan";
|
|
29
|
+
readonly operand: "number";
|
|
30
|
+
readonly compact: "gt";
|
|
31
|
+
}, {
|
|
32
|
+
readonly name: "greaterThanOrEqual";
|
|
33
|
+
readonly operand: "number";
|
|
34
|
+
readonly compact: "ge";
|
|
35
|
+
}, {
|
|
36
|
+
readonly name: "lessThan";
|
|
37
|
+
readonly operand: "number";
|
|
38
|
+
readonly compact: "lt";
|
|
39
|
+
}, {
|
|
40
|
+
readonly name: "lessThanOrEqual";
|
|
41
|
+
readonly operand: "number";
|
|
42
|
+
readonly compact: "le";
|
|
43
|
+
}, {
|
|
44
|
+
readonly name: "before";
|
|
45
|
+
readonly operand: "timestamp";
|
|
46
|
+
readonly compact: "bf";
|
|
47
|
+
}, {
|
|
48
|
+
readonly name: "after";
|
|
49
|
+
readonly operand: "timestamp";
|
|
50
|
+
readonly compact: "af";
|
|
51
|
+
}, {
|
|
52
|
+
readonly name: "between";
|
|
53
|
+
readonly operand: "range";
|
|
54
|
+
readonly compact: "bt";
|
|
55
|
+
readonly range: {
|
|
56
|
+
readonly memberTypes: readonly ["number", "timestamp"];
|
|
57
|
+
readonly homogeneous: true;
|
|
58
|
+
readonly inclusive: true;
|
|
59
|
+
};
|
|
60
|
+
}, {
|
|
61
|
+
readonly name: "in";
|
|
62
|
+
readonly operand: "scalarList";
|
|
63
|
+
readonly compact: "in";
|
|
64
|
+
readonly maxValues: 32;
|
|
65
|
+
}];
|
|
66
|
+
readonly scopes: readonly [{
|
|
67
|
+
readonly scope: "request";
|
|
68
|
+
readonly allowsEmpty: false;
|
|
69
|
+
readonly fields: readonly [{
|
|
70
|
+
readonly field: "referrer";
|
|
71
|
+
}, {
|
|
72
|
+
readonly field: "query.*";
|
|
73
|
+
}, {
|
|
74
|
+
readonly field: "header.*";
|
|
75
|
+
}, {
|
|
76
|
+
readonly field: "cookie.*";
|
|
77
|
+
}, {
|
|
78
|
+
readonly field: "geo.*";
|
|
79
|
+
}, {
|
|
80
|
+
readonly field: "utm.*";
|
|
81
|
+
}, {
|
|
82
|
+
readonly field: "device.class";
|
|
83
|
+
readonly operators: readonly ["equals", "in", "exists"];
|
|
84
|
+
readonly values: readonly ["desktop", "mobile", "tablet", "unknown"];
|
|
85
|
+
}, {
|
|
86
|
+
readonly field: "time.now";
|
|
87
|
+
readonly operators: readonly ["before", "after", "between", "exists"];
|
|
88
|
+
readonly instant: true;
|
|
89
|
+
}, {
|
|
90
|
+
readonly field: "audience.memberships";
|
|
91
|
+
readonly operators: readonly ["equals", "in", "exists"];
|
|
92
|
+
}];
|
|
93
|
+
}, {
|
|
94
|
+
readonly scope: "event";
|
|
95
|
+
readonly allowsEmpty: true;
|
|
96
|
+
readonly fields: readonly [{
|
|
97
|
+
readonly field: "referrer";
|
|
98
|
+
}, {
|
|
99
|
+
readonly field: "page.path";
|
|
100
|
+
}, {
|
|
101
|
+
readonly field: "properties.*";
|
|
102
|
+
}, {
|
|
103
|
+
readonly field: "utm.*";
|
|
104
|
+
}];
|
|
105
|
+
}];
|
|
106
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// GENERATED by scripts/generate-expression-capabilities.go — DO NOT EDIT.
|
|
2
|
+
// Regenerate: make expression-capabilities
|
|
3
|
+
//
|
|
4
|
+
// The published capability discovery schema for the personalization
|
|
5
|
+
// expression contract (ADR 0038), generated from the Go vocabulary tables
|
|
6
|
+
// and drift-gated in CI against the canonical JSON artifact at
|
|
7
|
+
// internal/domain/personalization/expression/schema/.
|
|
8
|
+
export const expressionCapabilities = {
|
|
9
|
+
"schemaVersion": 1,
|
|
10
|
+
"contract": "personalization-expression",
|
|
11
|
+
"maxConditions": 64,
|
|
12
|
+
"maxDepth": 5,
|
|
13
|
+
"maxInValues": 32,
|
|
14
|
+
"grammars": {
|
|
15
|
+
"number": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$",
|
|
16
|
+
"timestamp": "^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$"
|
|
17
|
+
},
|
|
18
|
+
"operators": [
|
|
19
|
+
{
|
|
20
|
+
"name": "equals",
|
|
21
|
+
"operand": "scalar",
|
|
22
|
+
"compact": "eq"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "contains",
|
|
26
|
+
"operand": "scalar",
|
|
27
|
+
"compact": "ct"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "startsWith",
|
|
31
|
+
"operand": "scalar",
|
|
32
|
+
"compact": "sw"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "exists",
|
|
36
|
+
"operand": "optionalBoolean",
|
|
37
|
+
"compact": "ex"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "greaterThan",
|
|
41
|
+
"operand": "number",
|
|
42
|
+
"compact": "gt"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "greaterThanOrEqual",
|
|
46
|
+
"operand": "number",
|
|
47
|
+
"compact": "ge"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "lessThan",
|
|
51
|
+
"operand": "number",
|
|
52
|
+
"compact": "lt"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "lessThanOrEqual",
|
|
56
|
+
"operand": "number",
|
|
57
|
+
"compact": "le"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "before",
|
|
61
|
+
"operand": "timestamp",
|
|
62
|
+
"compact": "bf"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "after",
|
|
66
|
+
"operand": "timestamp",
|
|
67
|
+
"compact": "af"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "between",
|
|
71
|
+
"operand": "range",
|
|
72
|
+
"compact": "bt",
|
|
73
|
+
"range": {
|
|
74
|
+
"memberTypes": [
|
|
75
|
+
"number",
|
|
76
|
+
"timestamp"
|
|
77
|
+
],
|
|
78
|
+
"homogeneous": true,
|
|
79
|
+
"inclusive": true
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "in",
|
|
84
|
+
"operand": "scalarList",
|
|
85
|
+
"compact": "in",
|
|
86
|
+
"maxValues": 32
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"scopes": [
|
|
90
|
+
{
|
|
91
|
+
"scope": "request",
|
|
92
|
+
"allowsEmpty": false,
|
|
93
|
+
"fields": [
|
|
94
|
+
{
|
|
95
|
+
"field": "referrer"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"field": "query.*"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"field": "header.*"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"field": "cookie.*"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"field": "geo.*"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"field": "utm.*"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"field": "device.class",
|
|
114
|
+
"operators": [
|
|
115
|
+
"equals",
|
|
116
|
+
"in",
|
|
117
|
+
"exists"
|
|
118
|
+
],
|
|
119
|
+
"values": [
|
|
120
|
+
"desktop",
|
|
121
|
+
"mobile",
|
|
122
|
+
"tablet",
|
|
123
|
+
"unknown"
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"field": "time.now",
|
|
128
|
+
"operators": [
|
|
129
|
+
"before",
|
|
130
|
+
"after",
|
|
131
|
+
"between",
|
|
132
|
+
"exists"
|
|
133
|
+
],
|
|
134
|
+
"instant": true
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"field": "audience.memberships",
|
|
138
|
+
"operators": [
|
|
139
|
+
"equals",
|
|
140
|
+
"in",
|
|
141
|
+
"exists"
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"scope": "event",
|
|
148
|
+
"allowsEmpty": true,
|
|
149
|
+
"fields": [
|
|
150
|
+
{
|
|
151
|
+
"field": "referrer"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"field": "page.path"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"field": "properties.*"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"field": "utm.*"
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
export { defineComponentContract } from './define-component.js';
|
|
2
|
-
export { defineLayoutContract, slot, fragmentDefault } from './define-layout.js';
|
|
3
2
|
export { createRegistry, registryLookup } from './registry.js';
|
|
4
3
|
export type { Registry, RegistryEntry, AnyComponent } from './registry.js';
|
|
5
4
|
export { blockRef, blockRefArray } from './types/block-ref.js';
|
|
6
5
|
export type { BlockRef, BlockRefEntry, BlockRefMeta, BlockRefUnion } from './types/block-ref.js';
|
|
7
|
-
export type { ComponentContract, ComponentContractID, ComponentContractValidator, ComponentRegistry, ComponentValidationContext, ComponentValidationIssue, InferComponentProps, } from './types/component.js';
|
|
6
|
+
export type { ComponentContract, ComponentContractID, ComponentContractValidator, ComponentRegistry, ComponentValidationContext, ComponentValidationIssue, InferComponentProps, RootIdentityProps, } from './types/component.js';
|
|
8
7
|
export { ImageSchema, imageAttrs, primaryImageSource, type Image, type ImageSource, type ImageAttrs, type ImageSlotContract, type SlotOutput, } from './types/image.js';
|
|
9
8
|
export { LinkSchema, type Link, type LinkTarget, type LinkResolverContext, resolveLink, resolveHref, assertSafeHref, normalizeLinkRel, } from './types/link.js';
|
|
10
9
|
export { HeadingSchema, InlineContentSchema, RichTextValueSchema, type Heading, type InlineContent, type RichTextValue, } from './types/rich-text.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { defineComponentContract } from './define-component.js';
|
|
2
|
-
export { defineLayoutContract, slot, fragmentDefault } from './define-layout.js';
|
|
3
2
|
export { createRegistry, registryLookup } from './registry.js';
|
|
4
3
|
export { blockRef, blockRefArray } from './types/block-ref.js';
|
|
5
4
|
export { ImageSchema, imageAttrs, primaryImageSource, } from './types/image.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ObjectReader {
|
|
2
|
+
readObject(path: string): Promise<Uint8Array>;
|
|
3
|
+
}
|
|
4
|
+
export interface ComposeObjectRef {
|
|
5
|
+
path: string;
|
|
6
|
+
digest: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ComposeSlot {
|
|
9
|
+
name: string;
|
|
10
|
+
required: boolean;
|
|
11
|
+
placeholder: string;
|
|
12
|
+
parts: ComposeObjectRef[];
|
|
13
|
+
}
|
|
14
|
+
export interface ComposeEnvelope {
|
|
15
|
+
schema: 'gradial.compose-envelope.v1';
|
|
16
|
+
algorithm: 'aci-html-compose-v1';
|
|
17
|
+
shell: ComposeObjectRef;
|
|
18
|
+
slots: ComposeSlot[];
|
|
19
|
+
}
|
|
20
|
+
export declare function composeEnvelope(reader: ObjectReader, envelopePath: string, envelopeDigest: string): Promise<{
|
|
21
|
+
html: string;
|
|
22
|
+
contentType: string;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function validateEnvelope(envelope: unknown): ComposeEnvelope;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
2
|
+
export async function composeEnvelope(reader, envelopePath, envelopeDigest) {
|
|
3
|
+
const envelopeBytes = await reader.readObject(envelopePath);
|
|
4
|
+
await requireDigest(envelopeBytes, envelopeDigest, 'compose envelope');
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = JSON.parse(decoder.decode(envelopeBytes));
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
throw new Error('compose envelope is malformed');
|
|
11
|
+
}
|
|
12
|
+
const envelope = validateEnvelope(parsed);
|
|
13
|
+
const shellBytes = await reader.readObject(envelope.shell.path);
|
|
14
|
+
await requireDigest(shellBytes, envelope.shell.digest, 'compose shell');
|
|
15
|
+
let html = decoder.decode(shellBytes);
|
|
16
|
+
for (const slot of envelope.slots) {
|
|
17
|
+
if (occurrences(html, slot.placeholder) !== 1) {
|
|
18
|
+
throw new Error(`compose slot ${JSON.stringify(slot.name)} placeholder must occur exactly once`);
|
|
19
|
+
}
|
|
20
|
+
if (slot.required && slot.parts.length === 0) {
|
|
21
|
+
throw new Error(`compose slot ${JSON.stringify(slot.name)} is required`);
|
|
22
|
+
}
|
|
23
|
+
const parts = [];
|
|
24
|
+
for (const part of slot.parts) {
|
|
25
|
+
const bytes = await reader.readObject(part.path);
|
|
26
|
+
await requireDigest(bytes, part.digest, `compose slot ${slot.name}`);
|
|
27
|
+
parts.push(decoder.decode(bytes));
|
|
28
|
+
}
|
|
29
|
+
html = html.replace(slot.placeholder, parts.join(''));
|
|
30
|
+
}
|
|
31
|
+
if (html.includes('<!--ACI_SLOT:')) {
|
|
32
|
+
throw new Error('compose shell has unresolved slots');
|
|
33
|
+
}
|
|
34
|
+
return { html, contentType: 'text/html; charset=utf-8' };
|
|
35
|
+
}
|
|
36
|
+
export function validateEnvelope(envelope) {
|
|
37
|
+
if (!envelope || typeof envelope !== 'object') {
|
|
38
|
+
throw new Error('compose envelope is malformed');
|
|
39
|
+
}
|
|
40
|
+
const obj = envelope;
|
|
41
|
+
if (obj.schema !== 'gradial.compose-envelope.v1'
|
|
42
|
+
|| obj.algorithm !== 'aci-html-compose-v1'
|
|
43
|
+
|| !Array.isArray(obj.slots)) {
|
|
44
|
+
throw new Error('compose envelope is malformed');
|
|
45
|
+
}
|
|
46
|
+
const env = envelope;
|
|
47
|
+
validateRef(env.shell);
|
|
48
|
+
const names = new Set();
|
|
49
|
+
for (const slot of env.slots) {
|
|
50
|
+
if (!slot || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(slot.name) || names.has(slot.name)) {
|
|
51
|
+
throw new Error('compose slot is malformed');
|
|
52
|
+
}
|
|
53
|
+
names.add(slot.name);
|
|
54
|
+
if (slot.placeholder !== `<!--ACI_SLOT:${slot.name}-->` || !Array.isArray(slot.parts)) {
|
|
55
|
+
throw new Error(`compose slot ${JSON.stringify(slot.name)} is malformed`);
|
|
56
|
+
}
|
|
57
|
+
for (const part of slot.parts)
|
|
58
|
+
validateRef(part);
|
|
59
|
+
}
|
|
60
|
+
return env;
|
|
61
|
+
}
|
|
62
|
+
function validateRef(ref) {
|
|
63
|
+
const path = String(ref?.path || '');
|
|
64
|
+
if (!path || path.includes('\\') || path.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
65
|
+
throw new Error('compose object path is invalid');
|
|
66
|
+
}
|
|
67
|
+
if (!/^[0-9a-f]{64}$/.test(String(ref.digest || '').toLowerCase())) {
|
|
68
|
+
throw new Error('compose object digest is invalid');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function requireDigest(bytes, want, label) {
|
|
72
|
+
const ab = new ArrayBuffer(bytes.byteLength);
|
|
73
|
+
new Uint8Array(ab).set(bytes);
|
|
74
|
+
const actual = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', ab)), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
75
|
+
if (actual !== want.toLowerCase())
|
|
76
|
+
throw new Error(`${label} digest mismatch`);
|
|
77
|
+
}
|
|
78
|
+
function occurrences(value, needle) {
|
|
79
|
+
let count = 0;
|
|
80
|
+
let offset = 0;
|
|
81
|
+
while (true) {
|
|
82
|
+
offset = value.indexOf(needle, offset);
|
|
83
|
+
if (offset < 0)
|
|
84
|
+
break;
|
|
85
|
+
count += 1;
|
|
86
|
+
offset += needle.length;
|
|
87
|
+
}
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { IdentitySigningKeys, SigningKey, CookieMutation, ConsentSnapshot,
|
|
|
2
2
|
export declare const DEFAULT_IDENTITY_TTL_SECONDS: number;
|
|
3
3
|
export declare function parseCookieHeader(value: string | null): Map<string, string>;
|
|
4
4
|
export declare function consentSignals(request: Request, cookies: Map<string, string>): ConsentSignals;
|
|
5
|
+
export declare function identityIssuancePermitted(consent: ConsentSignals, country: string, optOutCountries: readonly string[]): boolean;
|
|
5
6
|
export declare function parseConsent(raw: string, decodeCookieValue?: boolean): ConsentSnapshot | null;
|
|
6
7
|
export declare function trimConsentWhitespace(value: string): string;
|
|
7
8
|
export declare function verifyIdentityCookie(value: string, keys: IdentitySigningKeys): Promise<string | null>;
|
|
@@ -12,3 +13,7 @@ export declare function sha256Hex(value: string): Promise<string>;
|
|
|
12
13
|
export declare function verifyClaims(value: string, keys: Record<string, string>, now: Date): Promise<Record<string, unknown> | null>;
|
|
13
14
|
export declare function cookieMutation(name: string, value: string, maxAge: number, now: Date, secure: boolean, httpOnly: boolean): CookieMutation;
|
|
14
15
|
export declare function expireCookie(name: string, secure: boolean): CookieMutation;
|
|
16
|
+
export declare function verifyHMAC(value: string, signature: string, secret: string): Promise<boolean>;
|
|
17
|
+
export declare function hmacSHA256(value: string, secret: string): Promise<string>;
|
|
18
|
+
export declare function base64URLEncode(bytes: Uint8Array): string;
|
|
19
|
+
export declare function base64URLDecode(value: string): Uint8Array;
|