@gradial/aci 0.1.46 → 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 (56) 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/cli/emit-error-pages.d.mts +39 -0
  7. package/dist/cli/emit-error-pages.mjs +402 -0
  8. package/dist/content/default-error-page-html.d.ts +2 -0
  9. package/dist/content/default-error-page-html.js +6 -0
  10. package/dist/content/default-error-page.d.ts +23 -0
  11. package/dist/content/default-error-page.js +34 -0
  12. package/dist/content/error-page-kind.d.ts +57 -0
  13. package/dist/content/error-page-kind.js +106 -0
  14. package/dist/content/error-pages.d.ts +88 -0
  15. package/dist/content/error-pages.js +120 -0
  16. package/dist/content/index.d.ts +2 -0
  17. package/dist/content/index.js +2 -0
  18. package/dist/content/types.d.ts +12 -0
  19. package/dist/next/config.js +17 -0
  20. package/dist/next/error-page-instrumentation.d.ts +23 -0
  21. package/dist/next/error-page-instrumentation.js +36 -0
  22. package/dist/next/error-page-marker.d.ts +11 -0
  23. package/dist/next/error-page-marker.js +20 -0
  24. package/dist/next/error-pages.d.ts +52 -0
  25. package/dist/next/error-pages.js +94 -0
  26. package/dist/next/error-seat-payload.d.ts +33 -0
  27. package/dist/next/error-seat-payload.js +143 -0
  28. package/dist/next/error-seat.d.ts +57 -0
  29. package/dist/next/error-seat.js +83 -0
  30. package/dist/next/index.d.ts +4 -0
  31. package/dist/next/index.js +8 -0
  32. package/package.json +14 -8
  33. package/src/cli/emit-error-pages.mjs +424 -0
  34. package/templates/astro/template/package.json.tmpl +1 -1
  35. package/templates/astro/template/src/aci/error-page-instrumentation.mjs +33 -0
  36. package/templates/astro/template/src/pages/404.astro +54 -0
  37. package/templates/astro/template/src/pages/500.astro +53 -0
  38. package/templates/astro/template/src/pages/[...slug].astro +15 -2
  39. package/templates/common/.agents/skills/authoring/SKILL.md +71 -0
  40. package/templates/common/.content/config/robots.txt +4 -0
  41. package/templates/common/.content/config/site.json.tmpl +4 -0
  42. package/templates/common/.content/pages/404/_index.json +31 -0
  43. package/templates/common/.content/pages/error/_index.json +31 -0
  44. package/templates/common/.content/schemas/site.schema.json +9 -0
  45. package/templates/common/starter-content/.content/config/robots.txt +2 -0
  46. package/templates/common/starter-content/.content/config/site.json +4 -0
  47. package/templates/common/starter-content/.content/pages/404/_index.json +31 -0
  48. package/templates/common/starter-content/.content/pages/about/_index.json +3 -6
  49. package/templates/common/starter-content/.content/pages/error/_index.json +31 -0
  50. package/templates/common/starter-content/.content/pages/home/_index.json +10 -19
  51. package/templates/common/starter-content/.content/schemas/site.schema.json +9 -0
  52. package/templates/nextjs/template/package.json.tmpl +1 -1
  53. package/templates/nextjs/template/src/aci/error-page-instrumentation.d.mts +4 -0
  54. package/templates/nextjs/template/src/aci/error-page-instrumentation.mjs +38 -0
  55. package/templates/nextjs/template/src/app/error.tsx +18 -0
  56. package/templates/nextjs/template/src/app/not-found.tsx +13 -0
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shipped branded-neutral default error page (authorable-error-pages
3
+ * REQ-4/D-5). Materialized with a loud warning when a site has no
4
+ * `errorPages` binding under the warn-then-enforce posture. Self-contained
5
+ * by construction: inline styles only, no external references (REQ-14).
6
+ *
7
+ * The human-editable source of truth is `assets/error-pages/default.html`;
8
+ * it is embedded here as a generated constant (default-error-page-html.ts)
9
+ * because bundled serverless functions cannot read package assets at
10
+ * runtime — webpack rewrites module-URL resolution and output file tracing
11
+ * does not carry the .html into the function bundle, so a runtime file read
12
+ * crashes the very error surface this page exists to protect.
13
+ */
14
+ /** Returns the shipped branded-neutral default error page HTML. */
15
+ export declare function defaultErrorPageHtml(): string;
16
+ /**
17
+ * The shipped default page's `<body>` content as an embeddable fragment: the
18
+ * body element renamed to a `<div>` (its inline styles preserved) so
19
+ * framework error seats can inline the default INSIDE an existing document —
20
+ * the not-found seat and the client error seat render within the app's root
21
+ * layout, where a nested `<body>`/`<html>` would be invalid.
22
+ */
23
+ export declare function defaultErrorPageFragmentHtml(): string;
@@ -0,0 +1,34 @@
1
+ import { DEFAULT_ERROR_PAGE_HTML } from './default-error-page-html.js';
2
+ /**
3
+ * Shipped branded-neutral default error page (authorable-error-pages
4
+ * REQ-4/D-5). Materialized with a loud warning when a site has no
5
+ * `errorPages` binding under the warn-then-enforce posture. Self-contained
6
+ * by construction: inline styles only, no external references (REQ-14).
7
+ *
8
+ * The human-editable source of truth is `assets/error-pages/default.html`;
9
+ * it is embedded here as a generated constant (default-error-page-html.ts)
10
+ * because bundled serverless functions cannot read package assets at
11
+ * runtime — webpack rewrites module-URL resolution and output file tracing
12
+ * does not carry the .html into the function bundle, so a runtime file read
13
+ * crashes the very error surface this page exists to protect.
14
+ */
15
+ /** Returns the shipped branded-neutral default error page HTML. */
16
+ export function defaultErrorPageHtml() {
17
+ return DEFAULT_ERROR_PAGE_HTML;
18
+ }
19
+ /**
20
+ * The shipped default page's `<body>` content as an embeddable fragment: the
21
+ * body element renamed to a `<div>` (its inline styles preserved) so
22
+ * framework error seats can inline the default INSIDE an existing document —
23
+ * the not-found seat and the client error seat render within the app's root
24
+ * layout, where a nested `<body>`/`<html>` would be invalid.
25
+ */
26
+ export function defaultErrorPageFragmentHtml() {
27
+ const match = defaultErrorPageHtml().match(/<body([^>]*)>([\s\S]*?)<\/body>/i);
28
+ if (!match) {
29
+ // The shipped asset is package-controlled; this is a packaging bug, but
30
+ // error surfaces must never crash — fall back to the raw document body.
31
+ return defaultErrorPageHtml();
32
+ }
33
+ return `<div${match[1]}>${match[2]}</div>`;
34
+ }
@@ -0,0 +1,57 @@
1
+ export type ErrorPageKind = 'notFound' | 'catchAll';
2
+ /** D-14 page_type values for the error-surface `page_view` event. */
3
+ export type ErrorPageType = 'not_found' | 'error';
4
+ /** Maps a binding kind to its D-14 page_type. */
5
+ export declare function errorPageType(kind: ErrorPageKind): ErrorPageType;
6
+ declare global {
7
+ interface Window {
8
+ dataLayer?: Array<Record<string, unknown>>;
9
+ }
10
+ }
11
+ /**
12
+ * The D-15 policy shape: the dataLayer push for one error-surface view.
13
+ * Site policy lives in the tenant-owned scaffolded module
14
+ * `src/aci/error-page-instrumentation.mjs` (`pushErrorPageView`); the SDK
15
+ * ships `defaultPushErrorPageView` as the scaffolded default's twin. A push
16
+ * MUST be self-contained (browser globals only — no imports, no closed-over
17
+ * identifiers): the Astro seats and the 500.html emitter execute it
18
+ * STRINGIFIED via `serializeErrorPageInstrumentation`.
19
+ */
20
+ export type ErrorPagePush = (pageType: ErrorPageType) => void;
21
+ /**
22
+ * The SDK's copy of the D-14 default payload push — the same body the
23
+ * scaffolded policy module ships with. Used when no tenant module is in
24
+ * play: the Next `ErrorPageInstrumentation` component's default, the Astro
25
+ * seats' fallback, and the emitter's fallback tag. Self-contained by the
26
+ * ErrorPagePush contract (it serializes via toString()).
27
+ */
28
+ export declare function defaultPushErrorPageView(pageType: ErrorPageType): void;
29
+ /**
30
+ * SDK MECHANISM (D-15, code-organization R-14): serializes a self-contained
31
+ * push function into the inline analytics `<script>` tag the Astro seats
32
+ * inject via `<Fragment set:html>` and the Next emit-error-pages CLI stamps
33
+ * into `public/500.html`. The marker attribute and IIFE wrapper are
34
+ * SDK-owned platform contract (D-14: `data-aci-error-page` is the STABLE
35
+ * GTM suppression signal); the function body is site policy. `</script`,
36
+ * `<script` and `<!--` occurrences in the body are escaped (the script-data
37
+ * escaping table) so a push can neither break out of the tag nor walk the
38
+ * HTML parser into the escaped-script-data states (FIND-007).
39
+ *
40
+ * A non-function push degrades to the default tag with a warning
41
+ * (FIND-002): this is the runtime mechanism boundary and error surfaces
42
+ * must not crash over a broken policy value — the emitter keeps its own
43
+ * fail-loud stance at build time.
44
+ */
45
+ export declare function serializeErrorPageInstrumentation(push: ErrorPagePush, kind: ErrorPageKind): string;
46
+ /**
47
+ * The SDK DEFAULT analytics tag (D-15 amended D-14): the serialized
48
+ * `defaultPushErrorPageView`. Consumers fall back to it when no tenant
49
+ * policy module is provided — the Astro seats without
50
+ * `options.pushErrorPageView` and the emitter when
51
+ * `src/aci/error-page-instrumentation.mjs` is absent. The Next 'use client'
52
+ * component (`next/error-page-instrumentation.tsx`) cannot consume a script
53
+ * tag (RSC skips inline scripts); it calls the same default function
54
+ * directly, and the parity test beside it holds both deliveries in
55
+ * lockstep.
56
+ */
57
+ export declare function errorPageInstrumentationTag(kind: ErrorPageKind): string;
@@ -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';
@@ -85,9 +85,21 @@ export interface KernelSiteConfig {
85
85
  description?: string;
86
86
  siteName?: string;
87
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;
88
96
  /** Personalization collect-tag config (design/04); absent = disabled. */
89
97
  personalization?: SitePersonalizationConfig;
90
98
  }
99
+ export interface SiteErrorPages {
100
+ notFound: string;
101
+ catchAll: string;
102
+ }
91
103
  export interface KernelRouteMetadata {
92
104
  title: string;
93
105
  description: string;
@@ -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;
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+ import { useEffect } from 'react';
3
+ // Client-safe leaf import — see error-page-marker.tsx.
4
+ import { defaultPushErrorPageView, errorPageType, } from '../content/error-page-kind.js';
5
+ /**
6
+ * Error-surface analytics MECHANISM (authorable-error-pages REQ-12 / D-14 /
7
+ * D-15): calls the site's push function exactly ONCE per error view with
8
+ * the D-14 page_type. The push BODY is site policy — the seats forward the
9
+ * tenant-owned scaffolded module's `pushErrorPageView` via `push`; without
10
+ * it, `defaultPushErrorPageView` (the scaffolded default's twin) runs.
11
+ *
12
+ * Delivery MUST be a 'use client' useEffect: the not-found seat is
13
+ * client-rendered from the RSC flight payload, where React skips execution
14
+ * of inline script elements (spike A learning 3). The SDK error seats mount
15
+ * this only with error render context; ordinary routes — including direct
16
+ * GET /404/ and /error/ previews — never render it (REQ-12).
17
+ *
18
+ * Renders nothing. The seats (createNotFound, ErrorSeat) render the inert
19
+ * `data-aci-error-page` marker themselves — server-side where possible, so
20
+ * gates can assert it in fetched documents (a client component's output
21
+ * never appears in the serialized flight text).
22
+ */
23
+ export function ErrorPageInstrumentation({ kind, push, }) {
24
+ const pageType = errorPageType(kind);
25
+ useEffect(() => {
26
+ let effectivePush = push ?? defaultPushErrorPageView;
27
+ if (typeof effectivePush !== 'function') {
28
+ // FIND-002: a non-function push (broken config plumbing) must degrade
29
+ // to the default, never crash the error surface itself.
30
+ console.warn('[aci error-pages] pushErrorPageView is not a function; using the SDK default push (D-15)');
31
+ effectivePush = defaultPushErrorPageView;
32
+ }
33
+ effectivePush(pageType);
34
+ }, [pageType, push]);
35
+ return null;
36
+ }
@@ -0,0 +1,11 @@
1
+ import { type ErrorPageKind } from '../content/error-page-kind.js';
2
+ /**
3
+ * Inert, invisible marker naming the error surface
4
+ * (`data-aci-error-page="not_found"|"error"`). Rendered by the seats next to
5
+ * ErrorPageInstrumentation as the fetch-level assertion hook for build and
6
+ * serve gates; its absence on ordinary routes proves direct /404/ and
7
+ * /error/ visits carry no error-typed instrumentation (REQ-12).
8
+ */
9
+ export declare function ErrorPageMarker({ kind }: {
10
+ kind: ErrorPageKind;
11
+ }): import("react").JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Deliberately NO 'use client' directive: this module is shared by the
3
+ // server-rendered not-found seat and the client-only error seat. At the
4
+ // not-found seat the marker must be a SERVER-rendered element so it lands in
5
+ // the RSC flight payload — a client component's rendered output never
6
+ // appears in the serialized document, which is exactly why the build/serve
7
+ // gates could not assert a marker owned by ErrorPageInstrumentation.
8
+ // Import the client-safe leaf directly: content/error-pages.js reaches
9
+ // node:path via routes.js, which webpack rejects in browser bundles.
10
+ import { errorPageType } from '../content/error-page-kind.js';
11
+ /**
12
+ * Inert, invisible marker naming the error surface
13
+ * (`data-aci-error-page="not_found"|"error"`). Rendered by the seats next to
14
+ * ErrorPageInstrumentation as the fetch-level assertion hook for build and
15
+ * serve gates; its absence on ordinary routes proves direct /404/ and
16
+ * /error/ visits carry no error-typed instrumentation (REQ-12).
17
+ */
18
+ export function ErrorPageMarker({ kind }) {
19
+ return _jsx("span", { hidden: true, "data-aci-error-page": errorPageType(kind) });
20
+ }
@@ -0,0 +1,52 @@
1
+ import type { ReactElement } from 'react';
2
+ import { type FetchConfig } from './server.js';
3
+ import { type ErrorPageKind, type ErrorPagePush, type ResolvedErrorPage } from '../content/error-pages.js';
4
+ import type { KernelSiteConfig } from '../content/types.js';
5
+ import type { Registry } from '../registry.js';
6
+ /**
7
+ * Next runtime seams for authorable error pages (REQ-6, 02-design "SDK
8
+ * core"): `resolveErrorPage(kind)` + `renderPageForRoute(route)` — the pair
9
+ * both spike wrappers hand-rolled — plus `createNotFound(registry)`, which
10
+ * composes them so the starter's `not-found.tsx` stays thin.
11
+ */
12
+ export interface NextResolvedErrorPage extends ResolvedErrorPage {
13
+ siteConfig: KernelSiteConfig | null;
14
+ }
15
+ /**
16
+ * Resolves one error-page binding from the active site config under the
17
+ * warn posture (D-5). Never throws: an unreachable site config degrades to
18
+ * the shipped default — an error surface must not re-enter the failing
19
+ * system (ANTI-2).
20
+ */
21
+ export declare function resolveErrorPage(kind: ErrorPageKind, config?: FetchConfig): Promise<NextResolvedErrorPage>;
22
+ /**
23
+ * Renders the content page bound to `route` through the normal page
24
+ * pipeline (createPage: layout slots, fragments, registry blocks, collect/
25
+ * assignments tags), resolving to the same tree the catch-all route would
26
+ * serve. Throws whatever the pipeline throws — including the notFound()
27
+ * control error for missing content — so callers at error seats must wrap
28
+ * it (createNotFound does).
29
+ */
30
+ export declare function renderPageForRoute(registry: Registry, route: string): Promise<ReactElement>;
31
+ /**
32
+ * The shipped branded-neutral default error page (REQ-4/D-5) as an inline
33
+ * fragment — used when no binding exists or the bound content cannot be
34
+ * rendered. Self-contained by construction (inline styles only, REQ-14).
35
+ */
36
+ export declare function DefaultErrorPageContent(): import("react").JSX.Element;
37
+ /**
38
+ * Builds the component for Next's native not-found seat
39
+ * (`src/app/not-found.tsx`). The SDK catch-all invokes `notFound()` on
40
+ * missing content, so this renders with a real HTTP 404 (REQ-3/REQ-6).
41
+ *
42
+ * Fallback contract: if the bound notFound content is itself missing or
43
+ * fails to render at request time, the shipped default renders INLINE —
44
+ * the seat never re-invokes notFound() (a recursive seat was the spike's
45
+ * kill-list item) and never throws.
46
+ *
47
+ * `pushErrorPageView` (D-15): the tenant-owned policy module's push,
48
+ * forwarded to ErrorPageInstrumentation; omitted → the SDK default.
49
+ */
50
+ export declare function createNotFound(registry: Registry, config?: FetchConfig & {
51
+ pushErrorPageView?: ErrorPagePush;
52
+ }): () => Promise<ReactElement>;