@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.
- package/README.md +4 -4
- package/dist/astro/error-page.d.ts +56 -0
- package/dist/astro/error-page.js +79 -0
- package/dist/astro/index.d.ts +4 -0
- package/dist/astro/index.js +9 -0
- package/dist/audience-data-models.d.ts +142 -2
- package/dist/audience-data-models.js +2 -0
- package/dist/cli/compile-registry.d.mts +1 -5
- package/dist/cli/compile-registry.mjs +10 -25
- package/dist/cli/emit-error-pages.d.mts +39 -0
- package/dist/cli/emit-error-pages.mjs +402 -0
- package/dist/cli/validate-content.d.mts +1 -31
- package/dist/cli/validate-content.mjs +12 -50
- package/dist/cli/verify-next-cdn-routes.mjs +1 -0
- package/dist/content/default-error-page-html.d.ts +2 -0
- package/dist/content/default-error-page-html.js +6 -0
- package/dist/content/default-error-page.d.ts +23 -0
- package/dist/content/default-error-page.js +34 -0
- package/dist/content/error-page-kind.d.ts +57 -0
- package/dist/content/error-page-kind.js +106 -0
- package/dist/content/error-pages.d.ts +88 -0
- package/dist/content/error-pages.js +120 -0
- package/dist/content/index.d.ts +2 -0
- package/dist/content/index.js +2 -0
- package/dist/content/provider.d.ts +1 -1
- package/dist/content/resolve-slots.js +0 -1
- package/dist/content/types.d.ts +12 -2
- package/dist/internal/render-proof.d.ts +1 -0
- package/dist/internal/render-proof.js +197 -0
- package/dist/next/config.js +17 -0
- package/dist/next/error-page-instrumentation.d.ts +23 -0
- package/dist/next/error-page-instrumentation.js +36 -0
- package/dist/next/error-page-marker.d.ts +11 -0
- package/dist/next/error-page-marker.js +20 -0
- package/dist/next/error-pages.d.ts +52 -0
- package/dist/next/error-pages.js +94 -0
- package/dist/next/error-seat-payload.d.ts +33 -0
- package/dist/next/error-seat-payload.js +143 -0
- package/dist/next/error-seat.d.ts +57 -0
- package/dist/next/error-seat.js +83 -0
- package/dist/next/index.d.ts +4 -0
- package/dist/next/index.js +8 -0
- package/dist/next/middleware.js +9 -54
- package/dist/next/page.js +9 -4
- package/dist/next/personalization-host.d.ts +5 -0
- package/dist/next/server.js +88 -75
- package/dist/next/vercel-collect-sdk-route.js +6 -30
- package/dist/next/vercel-edge-projection-v1.d.ts +24 -0
- package/dist/next/vercel-edge-projection-v1.js +880 -0
- package/dist/next/vercel-s3-personalization.js +7 -11
- package/dist/providers/s3.d.ts +2 -4
- package/dist/providers/s3.js +173 -19
- package/dist/types/block-ref.d.ts +0 -2
- package/dist/types/block-ref.js +0 -1
- package/package.json +15 -9
- package/src/cli/compile-registry.mjs +10 -24
- package/src/cli/emit-error-pages.mjs +424 -0
- package/src/cli/validate-content.mjs +13 -52
- package/src/cli/verify-next-cdn-routes.mjs +6 -0
- package/src/types/block-ref.ts +0 -2
- package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +6 -7
- package/templates/astro/starter-content/src/fixtures/composition.ts +2 -3
- package/templates/astro/starter-content/src/fixtures/sections.ts +1 -2
- package/templates/astro/template/package.json.tmpl +1 -1
- package/templates/astro/template/src/aci/error-page-instrumentation.mjs +33 -0
- package/templates/astro/template/src/pages/404.astro +54 -0
- package/templates/astro/template/src/pages/500.astro +53 -0
- package/templates/astro/template/src/pages/[...slug].astro +15 -2
- package/templates/common/.agents/skills/authoring/SKILL.md +74 -11
- package/templates/common/.agents/skills/authoring/references/block-slots.md +0 -3
- package/templates/common/.content/config/robots.txt +4 -0
- package/templates/common/.content/config/site.json.tmpl +4 -0
- package/templates/common/.content/pages/404/_index.json +31 -0
- package/templates/common/.content/pages/error/_index.json +31 -0
- package/templates/common/.content/pages/home/_index.json +0 -10
- package/templates/common/.content/schemas/site.schema.json +9 -0
- package/templates/common/starter-content/.content/config/robots.txt +2 -0
- package/templates/common/starter-content/.content/config/site.json +4 -0
- package/templates/common/starter-content/.content/pages/404/_index.json +31 -0
- package/templates/common/starter-content/.content/pages/about/_index.json +0 -3
- package/templates/common/starter-content/.content/pages/error/_index.json +31 -0
- package/templates/common/starter-content/.content/pages/home/_index.json +21 -14
- package/templates/common/starter-content/.content/schemas/site.schema.json +9 -0
- package/templates/nextjs/template/package.json.tmpl +1 -1
- package/templates/nextjs/template/src/aci/error-page-instrumentation.d.mts +4 -0
- package/templates/nextjs/template/src/aci/error-page-instrumentation.mjs +38 -0
- package/templates/nextjs/template/src/app/error.tsx +18 -0
- package/templates/nextjs/template/src/app/not-found.tsx +13 -0
- package/src/types/block-ref.test.ts +0 -45
- 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
|
+
}
|
package/dist/content/index.d.ts
CHANGED
package/dist/content/index.js
CHANGED
|
@@ -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 || {},
|
package/dist/content/types.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/dist/next/config.js
CHANGED
|
@@ -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;
|