@gradial/aci 0.1.45 → 0.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +4 -4
  2. package/dist/astro/error-page.d.ts +56 -0
  3. package/dist/astro/error-page.js +79 -0
  4. package/dist/astro/index.d.ts +4 -0
  5. package/dist/astro/index.js +9 -0
  6. package/dist/audience-data-models.d.ts +142 -2
  7. package/dist/audience-data-models.js +2 -0
  8. package/dist/cli/compile-registry.d.mts +1 -5
  9. package/dist/cli/compile-registry.mjs +10 -25
  10. package/dist/cli/emit-error-pages.d.mts +39 -0
  11. package/dist/cli/emit-error-pages.mjs +402 -0
  12. package/dist/cli/validate-content.d.mts +1 -31
  13. package/dist/cli/validate-content.mjs +12 -50
  14. package/dist/cli/verify-next-cdn-routes.mjs +1 -0
  15. package/dist/content/default-error-page-html.d.ts +2 -0
  16. package/dist/content/default-error-page-html.js +6 -0
  17. package/dist/content/default-error-page.d.ts +23 -0
  18. package/dist/content/default-error-page.js +34 -0
  19. package/dist/content/error-page-kind.d.ts +57 -0
  20. package/dist/content/error-page-kind.js +106 -0
  21. package/dist/content/error-pages.d.ts +88 -0
  22. package/dist/content/error-pages.js +120 -0
  23. package/dist/content/index.d.ts +2 -0
  24. package/dist/content/index.js +2 -0
  25. package/dist/content/provider.d.ts +1 -1
  26. package/dist/content/resolve-slots.js +0 -1
  27. package/dist/content/types.d.ts +12 -2
  28. package/dist/internal/render-proof.d.ts +1 -0
  29. package/dist/internal/render-proof.js +197 -0
  30. package/dist/next/config.js +17 -0
  31. package/dist/next/error-page-instrumentation.d.ts +23 -0
  32. package/dist/next/error-page-instrumentation.js +36 -0
  33. package/dist/next/error-page-marker.d.ts +11 -0
  34. package/dist/next/error-page-marker.js +20 -0
  35. package/dist/next/error-pages.d.ts +52 -0
  36. package/dist/next/error-pages.js +94 -0
  37. package/dist/next/error-seat-payload.d.ts +33 -0
  38. package/dist/next/error-seat-payload.js +143 -0
  39. package/dist/next/error-seat.d.ts +57 -0
  40. package/dist/next/error-seat.js +83 -0
  41. package/dist/next/index.d.ts +4 -0
  42. package/dist/next/index.js +8 -0
  43. package/dist/next/middleware.js +9 -54
  44. package/dist/next/page.js +9 -4
  45. package/dist/next/personalization-host.d.ts +5 -0
  46. package/dist/next/server.js +88 -75
  47. package/dist/next/vercel-collect-sdk-route.js +6 -30
  48. package/dist/next/vercel-edge-projection-v1.d.ts +24 -0
  49. package/dist/next/vercel-edge-projection-v1.js +880 -0
  50. package/dist/next/vercel-s3-personalization.js +7 -11
  51. package/dist/providers/s3.d.ts +2 -4
  52. package/dist/providers/s3.js +173 -19
  53. package/dist/types/block-ref.d.ts +0 -2
  54. package/dist/types/block-ref.js +0 -1
  55. package/package.json +15 -9
  56. package/src/cli/compile-registry.mjs +10 -24
  57. package/src/cli/emit-error-pages.mjs +424 -0
  58. package/src/cli/validate-content.mjs +13 -52
  59. package/src/cli/verify-next-cdn-routes.mjs +6 -0
  60. package/src/types/block-ref.ts +0 -2
  61. package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +6 -7
  62. package/templates/astro/starter-content/src/fixtures/composition.ts +2 -3
  63. package/templates/astro/starter-content/src/fixtures/sections.ts +1 -2
  64. package/templates/astro/template/package.json.tmpl +1 -1
  65. package/templates/astro/template/src/aci/error-page-instrumentation.mjs +33 -0
  66. package/templates/astro/template/src/pages/404.astro +54 -0
  67. package/templates/astro/template/src/pages/500.astro +53 -0
  68. package/templates/astro/template/src/pages/[...slug].astro +15 -2
  69. package/templates/common/.agents/skills/authoring/SKILL.md +74 -11
  70. package/templates/common/.agents/skills/authoring/references/block-slots.md +0 -3
  71. package/templates/common/.content/config/robots.txt +4 -0
  72. package/templates/common/.content/config/site.json.tmpl +4 -0
  73. package/templates/common/.content/pages/404/_index.json +31 -0
  74. package/templates/common/.content/pages/error/_index.json +31 -0
  75. package/templates/common/.content/pages/home/_index.json +0 -10
  76. package/templates/common/.content/schemas/site.schema.json +9 -0
  77. package/templates/common/starter-content/.content/config/robots.txt +2 -0
  78. package/templates/common/starter-content/.content/config/site.json +4 -0
  79. package/templates/common/starter-content/.content/pages/404/_index.json +31 -0
  80. package/templates/common/starter-content/.content/pages/about/_index.json +0 -3
  81. package/templates/common/starter-content/.content/pages/error/_index.json +31 -0
  82. package/templates/common/starter-content/.content/pages/home/_index.json +21 -14
  83. package/templates/common/starter-content/.content/schemas/site.schema.json +9 -0
  84. package/templates/nextjs/template/package.json.tmpl +1 -1
  85. package/templates/nextjs/template/src/aci/error-page-instrumentation.d.mts +4 -0
  86. package/templates/nextjs/template/src/aci/error-page-instrumentation.mjs +38 -0
  87. package/templates/nextjs/template/src/app/error.tsx +18 -0
  88. package/templates/nextjs/template/src/app/not-found.tsx +13 -0
  89. package/src/types/block-ref.test.ts +0 -45
  90. package/templates/nextjs/template/src/app/collect/route.ts +0 -18
@@ -0,0 +1,106 @@
1
+ // Authorable error pages: the kind/page_type vocabulary (REQ-12 / D-14).
2
+ //
3
+ // This is a deliberately dependency-free CLIENT-SAFE leaf module: the Next
4
+ // error-surface components ('use client' — error-page-marker,
5
+ // error-page-instrumentation, error-seat) bundle it into the browser, so it
6
+ // must not import anything that reaches node builtins (content/error-pages.ts
7
+ // pulls content/routes.ts → node:path, which webpack rejects in client
8
+ // bundles). It is the SINGLE definition (quorum SIMPLE-005) — content/
9
+ // error-pages.ts re-exports it for server-side callers, and the next/astro
10
+ // barrels re-export from there.
11
+ /** Maps a binding kind to its D-14 page_type. */
12
+ export function errorPageType(kind) {
13
+ return kind === 'notFound' ? 'not_found' : 'error';
14
+ }
15
+ /**
16
+ * The SDK's copy of the D-14 default payload push — the same body the
17
+ * scaffolded policy module ships with. Used when no tenant module is in
18
+ * play: the Next `ErrorPageInstrumentation` component's default, the Astro
19
+ * seats' fallback, and the emitter's fallback tag. Self-contained by the
20
+ * ErrorPagePush contract (it serializes via toString()).
21
+ */
22
+ export function defaultPushErrorPageView(pageType) {
23
+ window.dataLayer = window.dataLayer || [];
24
+ window.dataLayer.push({
25
+ event: 'page_view',
26
+ page_type: pageType,
27
+ page_path: window.location.pathname,
28
+ page_location: window.location.href,
29
+ page_referrer: document.referrer || '(direct)',
30
+ page_title: document.title,
31
+ });
32
+ }
33
+ /**
34
+ * SDK MECHANISM (D-15, code-organization R-14): serializes a self-contained
35
+ * push function into the inline analytics `<script>` tag the Astro seats
36
+ * inject via `<Fragment set:html>` and the Next emit-error-pages CLI stamps
37
+ * into `public/500.html`. The marker attribute and IIFE wrapper are
38
+ * SDK-owned platform contract (D-14: `data-aci-error-page` is the STABLE
39
+ * GTM suppression signal); the function body is site policy. `</script`,
40
+ * `<script` and `<!--` occurrences in the body are escaped (the script-data
41
+ * escaping table) so a push can neither break out of the tag nor walk the
42
+ * HTML parser into the escaped-script-data states (FIND-007).
43
+ *
44
+ * A non-function push degrades to the default tag with a warning
45
+ * (FIND-002): this is the runtime mechanism boundary and error surfaces
46
+ * must not crash over a broken policy value — the emitter keeps its own
47
+ * fail-loud stance at build time.
48
+ */
49
+ export function serializeErrorPageInstrumentation(push, kind) {
50
+ const pageType = errorPageType(kind);
51
+ let effectivePush = push;
52
+ if (typeof effectivePush !== 'function') {
53
+ console.warn('[aci error-pages] pushErrorPageView is not a function; serializing the SDK default push instead (D-15)');
54
+ effectivePush = defaultPushErrorPageView;
55
+ }
56
+ const source = escapeInlineScriptSource(effectivePush.toString());
57
+ assertSerializablePushSource(source);
58
+ return `<script data-aci-error-page="${pageType}">(${source})(${JSON.stringify(pageType)});</script>`;
59
+ }
60
+ /**
61
+ * Script-data escaping (FIND-007): `</script` would close the inline tag,
62
+ * and `<!--`/`<script` open the escaped-script-data parser states where the
63
+ * real closing tag is consumed as text. All three are defused with a
64
+ * case-PRESERVING backslash after `<` — inside JS string literals `\/`,
65
+ * `\s` and `\!` decode to the original characters, so payload values
66
+ * survive byte-for-byte on every surface.
67
+ */
68
+ function escapeInlineScriptSource(source) {
69
+ return source.replace(/<(\/script|script|!--)/gi, (match) => `<\\${match.slice(1)}`);
70
+ }
71
+ /**
72
+ * Serialization smoke-eval (D-16, delta FIND-006): the tag ships the push
73
+ * STRINGIFIED, so its toString() must parse as a standalone function
74
+ * expression. Bound functions (`fn.bind(...)` stringifies to
75
+ * `function () { [native code] }`) and method-shorthand definitions
76
+ * (`{ pushErrorPageView(t) {…} }` stringifies without the `function`
77
+ * keyword) both produce source that is not an expression — the build must
78
+ * fail loudly here instead of shipping a tag that throws in every visitor's
79
+ * browser.
80
+ */
81
+ function assertSerializablePushSource(source) {
82
+ try {
83
+ // Parse-only: the function body never executes.
84
+ new Function(`return (${source});`);
85
+ }
86
+ catch (error) {
87
+ throw new Error('pushErrorPageView cannot be serialized: its toString() output does not parse as a '
88
+ + `function expression (${String(error)}). Likely causes: exporting a bound function `
89
+ + '(.bind() loses the source) or a method-shorthand definition. Export a plain '
90
+ + 'self-contained `function pushErrorPageView(pageType) {…}` instead (D-15 contract).');
91
+ }
92
+ }
93
+ /**
94
+ * The SDK DEFAULT analytics tag (D-15 amended D-14): the serialized
95
+ * `defaultPushErrorPageView`. Consumers fall back to it when no tenant
96
+ * policy module is provided — the Astro seats without
97
+ * `options.pushErrorPageView` and the emitter when
98
+ * `src/aci/error-page-instrumentation.mjs` is absent. The Next 'use client'
99
+ * component (`next/error-page-instrumentation.tsx`) cannot consume a script
100
+ * tag (RSC skips inline scripts); it calls the same default function
101
+ * directly, and the parity test beside it holds both deliveries in
102
+ * lockstep.
103
+ */
104
+ export function errorPageInstrumentationTag(kind) {
105
+ return serializeErrorPageInstrumentation(defaultPushErrorPageView, kind);
106
+ }
@@ -0,0 +1,88 @@
1
+ import type { RouteEntry } from './provider.js';
2
+ import type { KernelSiteConfig, SiteErrorPages } from './types.js';
3
+ /**
4
+ * Error-page binding validation (authorable-error-pages REQ-4/REQ-5/D-5),
5
+ * semantically paired with the Go compiler's
6
+ * `internal/domain/contentbuild/error_pages.go`. Under the warn-then-enforce
7
+ * migration posture, absent bindings are legal: they produce a loud warning
8
+ * and a `defaulted: true` result so callers fall back to the shipped
9
+ * branded-neutral default page (`defaultErrorPageHtml()`). Present bindings
10
+ * are strict in every posture: malformed routes, bindings resolving to no
11
+ * page, unpublished bound pages, and audience/experiment overlays on bound
12
+ * pages all fail closed.
13
+ *
14
+ * NOTE: `src/next/error-seat-payload.ts` carries a SYNCHRONOUS twin of the
15
+ * binding validation below (it runs inside next.config evaluation where these
16
+ * async providers cannot be awaited). Keep the REQ-5 semantics of the two in
17
+ * lockstep when editing either; extraction into one shared core is deferred.
18
+ */
19
+ /** D-5 migration posture for absent bindings; default is 'warn' today. */
20
+ export type ErrorPagePosture = 'warn' | 'enforce';
21
+ export { errorPageType, errorPageInstrumentationTag, defaultPushErrorPageView, serializeErrorPageInstrumentation, } from './error-page-kind.js';
22
+ export type { ErrorPageKind, ErrorPageType, ErrorPagePush } from './error-page-kind.js';
23
+ import type { ErrorPageKind } from './error-page-kind.js';
24
+ /**
25
+ * The one D-5 posture config surface, shared by every stack: the same
26
+ * variable the Go content build and publish materializer read.
27
+ */
28
+ export declare const ERROR_PAGES_POSTURE_ENV = "ACI_ERROR_PAGES_POSTURE";
29
+ /** Reads the D-5 posture from ACI_ERROR_PAGES_POSTURE; throws on unknown values (fail closed). */
30
+ export declare function errorPagePostureFromEnv(env?: Record<string, string | undefined>): ErrorPagePosture;
31
+ /** Mirrors the site.schema.json binding pattern: "/<segment>/". */
32
+ export declare const ERROR_PAGE_ROUTE_PATTERN: RegExp;
33
+ /**
34
+ * The slice of a content provider binding resolution needs. Both
35
+ * FileContentProvider and S3ContentProvider satisfy it.
36
+ */
37
+ export interface ErrorPageContentSource {
38
+ listRoutes(): Promise<RouteEntry[]>;
39
+ getPage<T = unknown>(route: string): Promise<T>;
40
+ }
41
+ export interface ResolveErrorPageBindingsOptions {
42
+ /** Absent-binding posture; defaults to 'warn' (D-5). */
43
+ posture?: ErrorPagePosture;
44
+ /**
45
+ * Compiled content to resolve bindings against. When omitted, only shape
46
+ * validation runs (the serve-time seam: publish status was already
47
+ * enforced at build time).
48
+ */
49
+ source?: ErrorPageContentSource;
50
+ }
51
+ export interface ErrorPageBindingsResolution {
52
+ /** Validated bindings; null when defaulted or invalid. */
53
+ bindings: SiteErrorPages | null;
54
+ /**
55
+ * True when no bindings are authored under the warn posture: callers must
56
+ * serve the shipped default page and surface `warnings` loudly.
57
+ */
58
+ defaulted: boolean;
59
+ warnings: string[];
60
+ errors: string[];
61
+ }
62
+ /**
63
+ * Validates the site's error-page bindings under the given posture.
64
+ * Never throws for validation outcomes: callers fail their build closed on a
65
+ * non-empty `errors` array.
66
+ */
67
+ export declare function resolveErrorPageBindings(siteConfig: Pick<KernelSiteConfig, 'errorPages'> | null | undefined, options?: ResolveErrorPageBindingsOptions): Promise<ErrorPageBindingsResolution>;
68
+ /**
69
+ * One kind's resolution for the serve-time seams (02-design "SDK core"):
70
+ * `route: null` + `defaulted: true` means "serve the shipped branded-neutral
71
+ * default page" (`defaultErrorPageHtml()` / `defaultErrorPageFragmentHtml()`).
72
+ */
73
+ export interface ResolvedErrorPage {
74
+ kind: ErrorPageKind;
75
+ /** Bound public route path, or null when the shipped default applies. */
76
+ route: string | null;
77
+ defaulted: boolean;
78
+ /** Loud-warning material (REQ-4) — callers surface these, e.g. console.warn. */
79
+ warnings: string[];
80
+ }
81
+ /**
82
+ * Resolves one error-page kind under the warn posture (default). Unlike the
83
+ * build-time `resolveErrorPageBindings`, this NEVER fails: error surfaces
84
+ * must not crash and must not re-enter the failing system (ANTI-2), so a
85
+ * broken binding — which the build already failed closed on (REQ-5) —
86
+ * degrades to the shipped default with the problem surfaced in `warnings`.
87
+ */
88
+ export declare function resolveErrorPage(kind: ErrorPageKind, siteConfig: Pick<KernelSiteConfig, 'errorPages'> | null | undefined, options?: ResolveErrorPageBindingsOptions): Promise<ResolvedErrorPage>;
@@ -0,0 +1,120 @@
1
+ import { normalizeRoute } from './routes.js';
2
+ // The kind/page_type vocabulary is defined once (quorum SIMPLE-005) in the
3
+ // client-safe leaf module ./error-page-kind.ts — THIS module reaches
4
+ // node:path via routes.js, so 'use client' components must import the leaf
5
+ // directly. Re-exported here as the canonical server-side import site.
6
+ export { errorPageType, errorPageInstrumentationTag, defaultPushErrorPageView, serializeErrorPageInstrumentation, } from './error-page-kind.js';
7
+ const ERROR_PAGE_KINDS = ['notFound', 'catchAll'];
8
+ /**
9
+ * The one D-5 posture config surface, shared by every stack: the same
10
+ * variable the Go content build and publish materializer read.
11
+ */
12
+ export const ERROR_PAGES_POSTURE_ENV = 'ACI_ERROR_PAGES_POSTURE';
13
+ /**
14
+ * Normalizes a raw posture value exactly like the Go side
15
+ * (`contentbuild.normalizeErrorPagePosture`): absent/empty means warn, and an
16
+ * unknown value throws so a config typo cannot silently disable enforcement.
17
+ */
18
+ function normalizeErrorPagePosture(raw) {
19
+ const value = raw ?? '';
20
+ if (value === '' || value === 'warn')
21
+ return 'warn';
22
+ if (value === 'enforce')
23
+ return 'enforce';
24
+ throw new Error(`unknown errorPages validation posture ${JSON.stringify(value)} (want "warn" or "enforce")`);
25
+ }
26
+ /** Reads the D-5 posture from ACI_ERROR_PAGES_POSTURE; throws on unknown values (fail closed). */
27
+ export function errorPagePostureFromEnv(env = process.env) {
28
+ return normalizeErrorPagePosture(env[ERROR_PAGES_POSTURE_ENV]);
29
+ }
30
+ /** Mirrors the site.schema.json binding pattern: "/<segment>/". */
31
+ export const ERROR_PAGE_ROUTE_PATTERN = /^\/(?:[A-Za-z0-9._~-]+\/)+$/;
32
+ const ERROR_PAGES_ABSENT_WARNING = 'site config has no errorPages bindings; the shipped branded-neutral default error page will be served for 404/5xx — author /404/ and /error/ pages and bind them via site.json#errorPages before the enforce posture lands (authorable-error-pages REQ-4/D-5)';
33
+ /**
34
+ * Validates the site's error-page bindings under the given posture.
35
+ * Never throws for validation outcomes: callers fail their build closed on a
36
+ * non-empty `errors` array.
37
+ */
38
+ export async function resolveErrorPageBindings(siteConfig, options = {}) {
39
+ const warnings = [];
40
+ const errors = [];
41
+ // Unknown posture values fail closed (quorum BUG-002, Go parity): a typo
42
+ // in ACI_ERROR_PAGES_POSTURE must never silently mean "warn".
43
+ let posture;
44
+ try {
45
+ posture = normalizeErrorPagePosture(options.posture);
46
+ }
47
+ catch (error) {
48
+ errors.push(error instanceof Error ? error.message : String(error));
49
+ return { bindings: null, defaulted: false, warnings, errors };
50
+ }
51
+ const raw = siteConfig?.errorPages;
52
+ if (raw == null) {
53
+ if (posture === 'enforce') {
54
+ errors.push('site config requires errorPages.notFound and errorPages.catchAll bindings (posture "enforce")');
55
+ return { bindings: null, defaulted: false, warnings, errors };
56
+ }
57
+ warnings.push(ERROR_PAGES_ABSENT_WARNING);
58
+ return { bindings: null, defaulted: true, warnings, errors };
59
+ }
60
+ // Present bindings are strict in every posture (REQ-5).
61
+ for (const kind of ERROR_PAGE_KINDS) {
62
+ const route = raw[kind];
63
+ if (typeof route !== 'string' || route === '') {
64
+ errors.push(`errorPages.${kind} binding is required`);
65
+ }
66
+ else if (!ERROR_PAGE_ROUTE_PATTERN.test(route)) {
67
+ errors.push(`errorPages.${kind} "${route}" is not a route path (want "/<segment>/")`);
68
+ }
69
+ }
70
+ if (errors.length > 0) {
71
+ return { bindings: null, defaulted: false, warnings, errors };
72
+ }
73
+ const bindings = { notFound: raw.notFound, catchAll: raw.catchAll };
74
+ if (options.source) {
75
+ const routes = await options.source.listRoutes();
76
+ const byPath = new Map(routes.map((entry) => [entry.path, entry]));
77
+ for (const kind of ERROR_PAGE_KINDS) {
78
+ const route = bindings[kind];
79
+ const normalized = normalizeRoute(route);
80
+ const entry = byPath.get(normalized);
81
+ if (!entry) {
82
+ errors.push(`errorPages.${kind} "${route}" does not resolve to any page`);
83
+ continue;
84
+ }
85
+ if (entry.variants && Object.keys(entry.variants).length > 0) {
86
+ errors.push(`errorPages.${kind} "${route}" must not have audience or experiment overlays`);
87
+ continue;
88
+ }
89
+ const page = await options.source
90
+ .getPage(normalized)
91
+ .catch(() => null);
92
+ if (!page || page.status !== 'published') {
93
+ errors.push(`errorPages.${kind} "${route}" must be a published page (status "${page?.status ?? 'unknown'}")`);
94
+ }
95
+ }
96
+ }
97
+ if (errors.length > 0) {
98
+ return { bindings: null, defaulted: false, warnings, errors };
99
+ }
100
+ return { bindings, defaulted: false, warnings, errors };
101
+ }
102
+ /**
103
+ * Resolves one error-page kind under the warn posture (default). Unlike the
104
+ * build-time `resolveErrorPageBindings`, this NEVER fails: error surfaces
105
+ * must not crash and must not re-enter the failing system (ANTI-2), so a
106
+ * broken binding — which the build already failed closed on (REQ-5) —
107
+ * degrades to the shipped default with the problem surfaced in `warnings`.
108
+ */
109
+ export async function resolveErrorPage(kind, siteConfig, options = {}) {
110
+ const resolution = await resolveErrorPageBindings(siteConfig, options);
111
+ if (resolution.errors.length > 0 || resolution.defaulted || !resolution.bindings) {
112
+ return {
113
+ kind,
114
+ route: null,
115
+ defaulted: true,
116
+ warnings: [...resolution.warnings, ...resolution.errors]
117
+ };
118
+ }
119
+ return { kind, route: resolution.bindings[kind], defaulted: false, warnings: resolution.warnings };
120
+ }
@@ -5,3 +5,5 @@ export * from './personalization.js';
5
5
  export * from './cache.js';
6
6
  export * from './resolve-slots.js';
7
7
  export * from './assets.js';
8
+ export * from './default-error-page.js';
9
+ export * from './error-pages.js';
@@ -5,3 +5,5 @@ export * from './personalization.js';
5
5
  export * from './cache.js';
6
6
  export * from './resolve-slots.js';
7
7
  export * from './assets.js';
8
+ export * from './default-error-page.js';
9
+ export * from './error-pages.js';
@@ -10,7 +10,7 @@ export interface RouteEntry {
10
10
  export interface RouteVariantEntry {
11
11
  variantKey: string;
12
12
  payloadRef: string;
13
- digest?: string;
13
+ digest: string;
14
14
  variantValues?: Record<string, string>;
15
15
  }
16
16
  export interface FragmentEntry {
@@ -12,7 +12,6 @@ export async function resolveSlots(layout, pageRegions, getFragment) {
12
12
  for (const ref of defaults) {
13
13
  const fragment = await getFragment(ref.fragmentId);
14
14
  blocks.push({
15
- $type: 'component',
16
15
  id: fragment.id,
17
16
  component: fragment.component,
18
17
  props: fragment.props || {},
@@ -42,13 +42,11 @@ export interface KernelPageSeo {
42
42
  canonical?: string;
43
43
  }
44
44
  export interface KernelPageBlock {
45
- $type: 'component';
46
45
  id: string;
47
46
  component: string;
48
47
  props: Record<string, unknown>;
49
48
  }
50
49
  export interface KernelAtomicBlock {
51
- $type: 'component';
52
50
  id: string;
53
51
  component: string;
54
52
  props: Record<string, unknown>;
@@ -87,9 +85,21 @@ export interface KernelSiteConfig {
87
85
  description?: string;
88
86
  siteName?: string;
89
87
  };
88
+ /**
89
+ * Error-page route bindings (authorable-error-pages REQ-1). Both values
90
+ * are public route paths (`/<segment>/`) resolving to ordinary published
91
+ * pages. Optional under the warn-then-enforce posture (REQ-4/D-5):
92
+ * absence is legal and produces a loud build/publish warning plus the
93
+ * shipped branded-neutral default page.
94
+ */
95
+ errorPages?: SiteErrorPages;
90
96
  /** Personalization collect-tag config (design/04); absent = disabled. */
91
97
  personalization?: SitePersonalizationConfig;
92
98
  }
99
+ export interface SiteErrorPages {
100
+ notFound: string;
101
+ catchAll: string;
102
+ }
93
103
  export interface KernelRouteMetadata {
94
104
  title: string;
95
105
  description: string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,197 @@
1
+ const RENDER_PROOF_VERSION = 'gradial.aci.renderProof.v1';
2
+ const RENDER_PROOF_KEYS = [
3
+ 'version',
4
+ 'releaseId',
5
+ 'route',
6
+ 'contentRef',
7
+ 'contentSha256',
8
+ 'variantKey',
9
+ ];
10
+ const MAX_RENDER_PROOF_CONTENT_BYTES = 4096;
11
+ const receiptBrand = Symbol('aci.verified-page-receipt');
12
+ const renderProofReceipts = new WeakMap();
13
+ /** @internal */
14
+ export const selectVerifiedPage = Symbol('aci.select-verified-page');
15
+ /** @internal */
16
+ export function createVerifiedPageReceipt(value) {
17
+ // Selection itself must fail if the coupled marker cannot be emitted.
18
+ // `getPage` also uses this capability and intentionally discards the
19
+ // receipt, so deferring the size check to React would permit markerless
20
+ // success on that public server loader.
21
+ validateVerifiedPageReceipt(value);
22
+ return Object.freeze({
23
+ ...value,
24
+ [receiptBrand]: true,
25
+ });
26
+ }
27
+ /** @internal */
28
+ export function validateVerifiedPageReceipt(value) {
29
+ encodeRenderProofContent(toRenderProof(value));
30
+ }
31
+ /** @internal */
32
+ export function attachRenderProofReceipt(input, receipt) {
33
+ if (receipt[receiptBrand] !== true) {
34
+ throw new Error('ACI render proof integrity error: unverified receipt');
35
+ }
36
+ renderProofReceipts.set(input, receipt);
37
+ }
38
+ /** @internal */
39
+ export function renderProofContentFor(input) {
40
+ const receipt = renderProofReceipts.get(input);
41
+ return receipt ? encodeRenderProofContent(toRenderProof(receipt)) : null;
42
+ }
43
+ /** @internal */
44
+ export function encodeRenderProofContent(proof) {
45
+ validateRenderProof(proof);
46
+ const canonicalJSON = JSON.stringify({
47
+ version: proof.version,
48
+ releaseId: proof.releaseId,
49
+ route: proof.route,
50
+ contentRef: proof.contentRef,
51
+ contentSha256: proof.contentSha256,
52
+ variantKey: proof.variantKey,
53
+ });
54
+ const content = Buffer.from(canonicalJSON, 'utf8').toString('base64url');
55
+ if (content.length < 1 || content.length > MAX_RENDER_PROOF_CONTENT_BYTES) {
56
+ throw new Error(`ACI render proof integrity error: marker content is ${content.length} bytes`);
57
+ }
58
+ return content;
59
+ }
60
+ /** @internal */
61
+ export function decodeRenderProofContent(content) {
62
+ if (content.length < 1
63
+ || content.length > MAX_RENDER_PROOF_CONTENT_BYTES
64
+ || !/^[A-Za-z0-9_-]+$/.test(content)) {
65
+ throw new Error('ACI render proof integrity error: invalid marker encoding');
66
+ }
67
+ const bytes = Buffer.from(content, 'base64url');
68
+ if (bytes.toString('base64url') !== content) {
69
+ throw new Error('ACI render proof integrity error: non-canonical base64url');
70
+ }
71
+ let canonicalJSON = '';
72
+ try {
73
+ canonicalJSON = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
74
+ }
75
+ catch (cause) {
76
+ throw integrityError('marker is not valid UTF-8', cause);
77
+ }
78
+ let decoded;
79
+ try {
80
+ decoded = JSON.parse(canonicalJSON);
81
+ }
82
+ catch (cause) {
83
+ throw integrityError('marker is not valid JSON', cause);
84
+ }
85
+ if (!isRecord(decoded)) {
86
+ throw new Error('ACI render proof integrity error: marker must be an object');
87
+ }
88
+ if (!sameKeys(Object.keys(decoded), RENDER_PROOF_KEYS)) {
89
+ throw new Error('ACI render proof integrity error: marker keys are not canonical');
90
+ }
91
+ const proof = {
92
+ version: decoded.version,
93
+ releaseId: decoded.releaseId,
94
+ route: decoded.route,
95
+ contentRef: decoded.contentRef,
96
+ contentSha256: decoded.contentSha256,
97
+ variantKey: decoded.variantKey,
98
+ };
99
+ validateRenderProof(proof);
100
+ if (JSON.stringify(proof) !== canonicalJSON) {
101
+ throw new Error('ACI render proof integrity error: marker JSON is not canonical');
102
+ }
103
+ return Object.freeze(proof);
104
+ }
105
+ function toRenderProof(receipt) {
106
+ return {
107
+ version: RENDER_PROOF_VERSION,
108
+ releaseId: receipt.releaseId,
109
+ route: receipt.route,
110
+ contentRef: receipt.contentRef,
111
+ contentSha256: receipt.contentSha256,
112
+ variantKey: receipt.variantKey,
113
+ };
114
+ }
115
+ function validateRenderProof(proof) {
116
+ if (!isRecord(proof)) {
117
+ throw new Error('ACI render proof integrity error: proof must be an object');
118
+ }
119
+ for (const key of RENDER_PROOF_KEYS) {
120
+ if (typeof proof[key] !== 'string') {
121
+ throw new Error(`ACI render proof integrity error: ${key} must be a string`);
122
+ }
123
+ if (hasUnpairedSurrogate(proof[key])) {
124
+ throw new Error(`ACI render proof integrity error: ${key} has invalid Unicode`);
125
+ }
126
+ }
127
+ if (proof.version !== RENDER_PROOF_VERSION) {
128
+ throw new Error('ACI render proof integrity error: unsupported version');
129
+ }
130
+ if (Buffer.byteLength(proof.releaseId, 'ascii') > 128
131
+ || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(proof.releaseId)) {
132
+ throw new Error('ACI render proof integrity error: invalid releaseId');
133
+ }
134
+ if (Buffer.byteLength(proof.route, 'utf8') > 2048
135
+ || !isCanonicalRoute(proof.route)) {
136
+ throw new Error('ACI render proof integrity error: invalid route');
137
+ }
138
+ if (Buffer.byteLength(proof.contentRef, 'utf8') > 2048
139
+ || !isCleanContentRef(proof.contentRef)) {
140
+ throw new Error('ACI render proof integrity error: invalid contentRef');
141
+ }
142
+ if (!/^[0-9a-f]{64}$/.test(proof.contentSha256)) {
143
+ throw new Error('ACI render proof integrity error: invalid contentSha256');
144
+ }
145
+ if (Buffer.byteLength(proof.variantKey, 'utf8') > 1024
146
+ || /[\u0000-\u001f\u007f]/.test(proof.variantKey)) {
147
+ throw new Error('ACI render proof integrity error: invalid variantKey');
148
+ }
149
+ }
150
+ function isCanonicalRoute(route) {
151
+ if (route === '/')
152
+ return true;
153
+ if (!route.startsWith('/')
154
+ || route.startsWith('//')
155
+ || route.endsWith('/')
156
+ || /[\u0000-\u001f\u007f]/.test(route)) {
157
+ return false;
158
+ }
159
+ return route.slice(1).split('/').every((segment) => (segment !== '' && segment !== '.' && segment !== '..'));
160
+ }
161
+ function isCleanContentRef(ref) {
162
+ if (!ref
163
+ || ref.startsWith('/')
164
+ || ref.endsWith('/')
165
+ || ref.includes('\\')
166
+ || /[\u0000-\u001f\u007f]/.test(ref)) {
167
+ return false;
168
+ }
169
+ return ref.split('/').every((segment) => (segment !== '' && segment !== '.' && segment !== '..'));
170
+ }
171
+ function isRecord(value) {
172
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
173
+ }
174
+ function sameKeys(actual, expected) {
175
+ return actual.length === expected.length
176
+ && actual.every((key, index) => key === expected[index]);
177
+ }
178
+ function hasUnpairedSurrogate(value) {
179
+ for (let index = 0; index < value.length; index += 1) {
180
+ const code = value.charCodeAt(index);
181
+ if (code >= 0xd800 && code <= 0xdbff) {
182
+ const next = value.charCodeAt(index + 1);
183
+ if (!(next >= 0xdc00 && next <= 0xdfff))
184
+ return true;
185
+ index += 1;
186
+ }
187
+ else if (code >= 0xdc00 && code <= 0xdfff) {
188
+ return true;
189
+ }
190
+ }
191
+ return false;
192
+ }
193
+ function integrityError(message, cause) {
194
+ const error = new Error(`ACI render proof integrity error: ${message}`);
195
+ error.cause = cause;
196
+ return error;
197
+ }
@@ -2,6 +2,7 @@ import { readdirSync, existsSync } from 'fs';
2
2
  import { join, relative } from 'path';
3
3
  import { createRequire } from 'module';
4
4
  import { createContentWatchWebpackPlugin } from './content-watch.js';
5
+ import { buildErrorSeatPayload } from './error-seat-payload.js';
5
6
  const require = createRequire(import.meta.url);
6
7
  export function withAci(nextConfig = {}, options = {}) {
7
8
  if (process.env.TURBOPACK) {
@@ -11,6 +12,21 @@ export function withAci(nextConfig = {}, options = {}) {
11
12
  '`next dev` / `next build` without --turbopack.');
12
13
  }
13
14
  const publicPaths = collectPublicPaths(join(process.cwd(), 'public'));
15
+ // Authorable error pages (D-9): the Next error seat is client-only, so the
16
+ // bound catchAll page's content is serialized here — while next.config is
17
+ // evaluated — and injected into the bundle via DefinePlugin below. The D-5
18
+ // posture rides ACI_ERROR_PAGES_POSTURE (the same variable the Go stack
19
+ // reads): under warn (default), absent bindings/content and non-REQ-5
20
+ // serialization failures warn and serialize the shipped default (REQ-4);
21
+ // under enforce they fail the build closed, and unknown posture values
22
+ // fail closed too. Present-but-broken bindings fail closed always (REQ-5).
23
+ const errorSeat = buildErrorSeatPayload(undefined, process.env.ACI_ERROR_PAGES_POSTURE);
24
+ if (errorSeat.errors.length > 0) {
25
+ throw new Error(`ACI errorPages bindings are invalid (site.json#errorPages):\n- ${errorSeat.errors.join('\n- ')}`);
26
+ }
27
+ for (const warning of errorSeat.warnings) {
28
+ console.warn(`\n[aci error-pages] WARNING: ${warning}\n`);
29
+ }
14
30
  const passthroughRoutes = normalizePassthroughRoutes(options.passthroughRoutes ?? []);
15
31
  const assetPrefix = options.assetPrefix || '/.gradial-dam';
16
32
  const assetOrigin = options.assetOrigin || '';
@@ -82,6 +98,7 @@ export function withAci(nextConfig = {}, options = {}) {
82
98
  '__ACI_PUBLIC_PATHS__': JSON.stringify(publicPaths),
83
99
  '__ACI_PASSTHROUGH_ROUTES__': JSON.stringify(passthroughRoutes),
84
100
  '__ACI_ASSET_PREFIX__': JSON.stringify(assetPrefix),
101
+ '__ACI_ERROR_PAGE__': JSON.stringify(errorSeat.payload),
85
102
  }));
86
103
  return typeof userWebpack === 'function'
87
104
  ? userWebpack(config, context)
@@ -0,0 +1,23 @@
1
+ import { type ErrorPageKind, type ErrorPagePush } from '../content/error-page-kind.js';
2
+ /**
3
+ * Error-surface analytics MECHANISM (authorable-error-pages REQ-12 / D-14 /
4
+ * D-15): calls the site's push function exactly ONCE per error view with
5
+ * the D-14 page_type. The push BODY is site policy — the seats forward the
6
+ * tenant-owned scaffolded module's `pushErrorPageView` via `push`; without
7
+ * it, `defaultPushErrorPageView` (the scaffolded default's twin) runs.
8
+ *
9
+ * Delivery MUST be a 'use client' useEffect: the not-found seat is
10
+ * client-rendered from the RSC flight payload, where React skips execution
11
+ * of inline script elements (spike A learning 3). The SDK error seats mount
12
+ * this only with error render context; ordinary routes — including direct
13
+ * GET /404/ and /error/ previews — never render it (REQ-12).
14
+ *
15
+ * Renders nothing. The seats (createNotFound, ErrorSeat) render the inert
16
+ * `data-aci-error-page` marker themselves — server-side where possible, so
17
+ * gates can assert it in fetched documents (a client component's output
18
+ * never appears in the serialized flight text).
19
+ */
20
+ export declare function ErrorPageInstrumentation({ kind, push, }: {
21
+ kind: ErrorPageKind;
22
+ push?: ErrorPagePush;
23
+ }): null;