@universalcms/connect 0.1.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +699 -37
  2. package/dist/angular-runtime.d.mts +4 -0
  3. package/dist/angular-runtime.mjs +7 -0
  4. package/dist/angular-setup.mjs +124 -0
  5. package/dist/astro-runtime.d.mts +6 -0
  6. package/dist/astro-runtime.mjs +255 -0
  7. package/dist/astro-setup.mjs +215 -0
  8. package/dist/astro-source.mjs +451 -0
  9. package/dist/astro-website.d.mts +5 -0
  10. package/dist/astro-website.mjs +42 -0
  11. package/dist/cli.mjs +418 -15
  12. package/dist/connector-runtime.mjs +130 -0
  13. package/dist/content-next-setup.mjs +207 -0
  14. package/dist/content-registry-validation.mjs +40 -0
  15. package/dist/content-registry.mjs +44 -0
  16. package/dist/content-setup.mjs +13 -0
  17. package/dist/content-storefront-setup.mjs +155 -0
  18. package/dist/content-vite-setup.mjs +107 -0
  19. package/dist/core.mjs +32 -51
  20. package/dist/framework-ownership.mjs +68 -0
  21. package/dist/framework.mjs +54 -0
  22. package/dist/guided.mjs +514 -0
  23. package/dist/native-adapters.mjs +31 -0
  24. package/dist/native-chrome-model.d.mts +3 -0
  25. package/dist/native-chrome-model.mjs +14 -0
  26. package/dist/native-chrome-review.mjs +49 -0
  27. package/dist/native-chrome-source.mjs +69 -0
  28. package/dist/native-route-review.mjs +152 -0
  29. package/dist/native-routes.mjs +27 -0
  30. package/dist/native-website-model.d.mts +18 -0
  31. package/dist/native-website-model.mjs +67 -0
  32. package/dist/native-website-server.d.mts +1 -0
  33. package/dist/native-website-server.mjs +35222 -0
  34. package/dist/native-website-server.mjs.LEGAL.txt +56 -0
  35. package/dist/native-website.d.mts +3 -0
  36. package/dist/native-website.mjs +120 -0
  37. package/dist/native-website.mjs.LEGAL.txt +56 -0
  38. package/dist/next-publication.d.mts +12 -0
  39. package/dist/next-publication.mjs +107 -0
  40. package/dist/next-settings-layout.mjs +233 -0
  41. package/dist/next-source.d.mts +8 -0
  42. package/dist/next-source.mjs +93 -0
  43. package/dist/page-discovery.mjs +236 -0
  44. package/dist/page-mapping-review.mjs +160 -0
  45. package/dist/preflight-recovery.mjs +130 -0
  46. package/dist/preflight.mjs +138 -0
  47. package/dist/project-files.mjs +56 -0
  48. package/dist/public-bindings.d.mts +3 -0
  49. package/dist/public-bindings.mjs +28 -0
  50. package/dist/publication-browser.d.mts +5 -0
  51. package/dist/publication-browser.mjs +258 -0
  52. package/dist/publication-check.mjs +141 -0
  53. package/dist/publication-document.d.mts +2 -0
  54. package/dist/publication-document.mjs +59 -0
  55. package/dist/publication-next-setup.mjs +134 -0
  56. package/dist/publication-setup.mjs +138 -0
  57. package/dist/publication-verification.mjs +85 -0
  58. package/dist/publication.css +6 -0
  59. package/dist/publication.d.mts +31 -0
  60. package/dist/publication.mjs +320 -0
  61. package/dist/react-publication.d.mts +22 -0
  62. package/dist/react-publication.mjs +301 -0
  63. package/dist/react-website.d.mts +15 -0
  64. package/dist/react-website.mjs +735 -0
  65. package/dist/reviewed-commerce-ownership.mjs +26 -0
  66. package/dist/reviewed-commerce-settings-setup.mjs +243 -0
  67. package/dist/reviewed-native-ownership.mjs +118 -0
  68. package/dist/reviewed-storefront-source.mjs +227 -0
  69. package/dist/runtime-dependencies.mjs +59 -0
  70. package/dist/settings-only-setup.mjs +100 -0
  71. package/dist/settings-only.d.mts +4 -0
  72. package/dist/settings-only.mjs +49 -0
  73. package/dist/setup-environment.mjs +70 -0
  74. package/dist/source-bindings.mjs +77 -0
  75. package/dist/source-check.mjs +149 -0
  76. package/dist/source-collect.mjs +80 -0
  77. package/dist/source-config.mjs +20 -0
  78. package/dist/source-extract.mjs +514 -0
  79. package/dist/source-finalize.mjs +52 -0
  80. package/dist/source-routes.mjs +65 -0
  81. package/dist/source-setup.mjs +490 -0
  82. package/dist/staged-setup.mjs +287 -0
  83. package/dist/static-runtime.mjs +67 -0
  84. package/dist/static-setup.mjs +136 -0
  85. package/dist/svelte-runtime.d.mts +27 -0
  86. package/dist/svelte-runtime.mjs +40 -0
  87. package/dist/svelte-setup.mjs +135 -0
  88. package/dist/svelte-template.mjs +48 -0
  89. package/dist/templates.mjs +167 -3
  90. package/dist/vendor/astro-compiler/LICENSE +53 -0
  91. package/dist/vendor/astro-compiler/astro.wasm +0 -0
  92. package/dist/vendor/astro-compiler/chunk-W5DTLHV4.js +1 -0
  93. package/dist/vendor/astro-compiler/node/index.js +1 -0
  94. package/dist/vendor/project-analysis.mjs +58 -4
  95. package/dist/vendor/react-LICENSE.txt +21 -0
  96. package/dist/vendor/react-document.mjs +124 -0
  97. package/dist/vendor/react-dom-LICENSE.txt +21 -0
  98. package/dist/vendor/react-form.mjs +63 -0
  99. package/dist/vendor/react-safety.mjs +47 -0
  100. package/dist/vendor/source-manifest-provider.mjs +104 -0
  101. package/dist/vendor/typescript-LICENSE.txt +55 -0
  102. package/dist/vendor/typescript.cjs +200276 -0
  103. package/dist/vite-publication-server.d.mts +3 -0
  104. package/dist/vite-publication-server.mjs +82 -0
  105. package/dist/vite-publication.d.mts +15 -0
  106. package/dist/vite-publication.mjs +241 -0
  107. package/dist/vite-source-discovery.mjs +315 -0
  108. package/dist/vite-source.d.mts +30 -0
  109. package/dist/vite-source.mjs +141 -0
  110. package/dist/vue-runtime.d.mts +10 -0
  111. package/dist/vue-runtime.mjs +50 -0
  112. package/dist/vue-server.d.mts +2 -0
  113. package/dist/vue-server.mjs +58 -0
  114. package/dist/vue-setup.mjs +127 -0
  115. package/dist/vue-source.mjs +168 -0
  116. package/dist/website-check.mjs +41 -0
  117. package/dist/website-http.mjs +149 -0
  118. package/dist/website-model.d.mts +20 -0
  119. package/dist/website-model.mjs +155 -0
  120. package/dist/website-next.d.mts +2 -0
  121. package/dist/website-next.mjs +29 -0
  122. package/dist/website-node.d.mts +3 -0
  123. package/dist/website-node.mjs +50 -0
  124. package/dist/website-runtime.d.mts +13 -0
  125. package/dist/website-runtime.mjs +404 -0
  126. package/dist/website-schema.mjs +22175 -0
  127. package/dist/website-setup.mjs +256 -0
  128. package/dist/website.css +49 -0
  129. package/dist/wizard.mjs +237 -38
  130. package/package.json +44 -7
@@ -0,0 +1,31 @@
1
+ export type PublicationRoute = Readonly<{ path: string; kind: string; slug: string; locale: string }>;
2
+ export type PublicationConfiguration = Readonly<{
3
+ version: 1; apiOrigin: string; siteId: string; environmentId: string; websiteOrigin: string;
4
+ routes: readonly PublicationRoute[];
5
+ }>;
6
+ export type PublicationResult = Readonly<{ state: 'published' | 'missing' | 'unmanaged' | 'unavailable'; status: 200 | 404 | 503 }>;
7
+ export type PublishedDocument = { schemaVersion: 1; title: string; excerpt: string; blocks: Array<{ id: string; type: string; version: 1; text: string; data?: Record<string, unknown>; style?: Record<string, unknown> }>; fields?: Record<string, unknown>; taxonomy?: { categories?: string[]; tags?: string[] } };
8
+ export type PublishedDocumentEntry = Readonly<{ id: string; kind: string; slug: string; locale: string; version: number; revisionId?: string; publishedAt: string; document: PublishedDocument }>;
9
+ export type PublishedDocumentResult = Readonly<{ state: 'published'; status: 200; entry: PublishedDocumentEntry }> | Readonly<{ state: 'missing' | 'unmanaged' | 'unavailable'; status: 200 | 404 | 503; entry?: never }>;
10
+ export type PublicationReaderOptions = {
11
+ configuration: PublicationConfiguration; env?: Record<string, string | undefined>; fetcher?: typeof fetch;
12
+ timeoutMs?: number; maxResponseBytes?: number; maxConcurrent?: number;
13
+ };
14
+ export const PUBLICATION_TIMEOUT_MS: 10000;
15
+ export const PUBLICATION_MAX_RESPONSE_BYTES: number;
16
+ export const PUBLICATION_MAX_CONCURRENT: 16;
17
+ export const PUBLICATION_MAX_ROUTES: 1000;
18
+ /** Pathname only. Throws on invalid/ambiguous encodings; normalizes one trailing slash. */
19
+ export function normalizePublicationPath(pathname: string): string;
20
+ /** Native private/system/assets paths remain outside public page ownership. */
21
+ export function isExcludedPublicationPath(pathname: string): boolean;
22
+ export function validatePublicationConfig(value: unknown): PublicationConfiguration;
23
+ /** Throws on invalid paths. Unmanaged, well-formed paths return null. */
24
+ export function resolvePublicationRoute(configuration: PublicationConfiguration, pathname: string): PublicationRoute | null;
25
+ export function createPublicationChecker(options: PublicationReaderOptions): (pathname: string) => Promise<PublicationResult>;
26
+ /** Private server read. Only a validated published response includes an entry. */
27
+ export function createPublishedDocumentReader(options: PublicationReaderOptions): (pathname: string) => Promise<PublishedDocumentResult>;
28
+ export function publicationRevision(entry: Pick<PublishedDocumentEntry, 'id' | 'version' | 'revisionId'>): string;
29
+ /** Removes custom fields, unknown properties and private form destinations. */
30
+ export function publicPublishedEntry(entry: PublishedDocumentEntry): PublishedDocumentEntry;
31
+ export function publicationErrorHtml(status: number): string;
@@ -0,0 +1,320 @@
1
+ /** Server-side availability checks for explicitly reviewed original website routes.
2
+ * No delivery payload or credential is returned to website/browser adapters.
3
+ */
4
+ export const PUBLICATION_TIMEOUT_MS = 10_000;
5
+ export const PUBLICATION_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
6
+ export const PUBLICATION_MAX_CONCURRENT = 16;
7
+ export const PUBLICATION_MAX_ROUTES = 1000;
8
+
9
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
+ // The two UUIDs identify the organization and key, not the website/environment.
11
+ const keyPattern = /^cms_server\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[A-Za-z0-9_-]{43}$/;
12
+ const slugPattern = /^[a-z0-9]+(?:[-/][a-z0-9]+)*$/;
13
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value));
14
+ const own = (value, key) => Object.hasOwn(value, key);
15
+ const fail = () => { throw new Error('Invalid website publication configuration or request.'); };
16
+ const boundedString = (value, max) => typeof value === 'string' && value.length <= max;
17
+ const integer = (value, min, max) => Number.isSafeInteger(value) && value >= min && value <= max;
18
+ const exactKeys = (value, keys) => object(value) && Object.keys(value).length === keys.length && Object.keys(value).every(key => keys.includes(key));
19
+ const secretPattern = /cms_server\.|sk-[A-Za-z0-9_-]{12,}|AIza[A-Za-z0-9_-]{20,}/i;
20
+ const privateSegments = new Set(['api', 'admin', 'account', 'auth', 'login', 'logout', 'signin', 'signout', 'sign-in', 'sign-out', 'signup', 'sign-up', 'register', 'dashboard', 'profile', 'settings', 'billing', 'checkout', 'cart', 'orders', 'portal', 'patient', 'patients', 'patient-vault', 'vault', 'records', 'medical-records', 'health-records', 'preview', 'draft-preview', 'cms-preview', '_next', '_nuxt', '__vite', 'node_modules', 'src', 'public', 'assets', 'static']);
21
+ // Match the source collector's exclusions, including private compound names.
22
+ // An occurrence inside an ordinary public name such as about-account is not a match.
23
+ const protectedSegment = /^(?:admin|account|dashboard|profile|auth|login|register|checkout|orders|settings|billing|portal|patient|patients|vault|records)(?:-|$)|(?:^|-)vault$|^(?:medical|health)-records(?:-|$)/;
24
+ const privateSegment = value => privateSegments.has(value.toLowerCase()) || protectedSegment.test(value.toLowerCase());
25
+
26
+ function origin(value) {
27
+ try {
28
+ if (!boundedString(value, 2048) || /[\s\\]/.test(value) || secretPattern.test(value)) return fail();
29
+ const url = new URL(value);
30
+ if (url.username || url.password || url.search || url.hash || url.pathname !== '/' ||
31
+ !(url.protocol === 'https:' || url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) return fail();
32
+ return url.origin;
33
+ } catch { return fail(); }
34
+ }
35
+
36
+ function canonicalLocale(value) {
37
+ try {
38
+ if (!boundedString(value, 35) || !value) return fail();
39
+ const locales = Intl.getCanonicalLocales(value);
40
+ if (locales.length !== 1) return fail();
41
+ return locales[0];
42
+ } catch { return fail(); }
43
+ }
44
+
45
+ /** Accept a pathname, never a URL/query. Reject ambiguous separators before decoding.
46
+ * One trailing slash and percent-encoded ASCII route characters resolve identically.
47
+ * Framework adapters must call this before a URL parser can erase dot segments.
48
+ */
49
+ export function normalizePublicationPath(value) {
50
+ if (!boundedString(value, 1500) || !value.startsWith('/') || value.startsWith('//') || /[\\\s?#\u0000-\u001f\u007f]/.test(value) || /%(?![0-9a-f]{2})|%(?:2f|5c|2e|25|3f|23|0[0-9a-f]|1[0-9a-f]|7f)/i.test(value)) return fail();
51
+ let decoded;
52
+ try { decoded = decodeURIComponent(value); } catch { return fail(); }
53
+ if (decoded.length > 500 || !/^\/(?:[A-Za-z0-9_.@-]+(?:\/[A-Za-z0-9_.@-]+)*\/?)?$/.test(decoded) || decoded.split('/').some(segment => segment === '.' || segment === '..')) return fail();
54
+ return decoded.length > 1 && decoded.endsWith('/') ? decoded.slice(0, -1) : decoded;
55
+ }
56
+
57
+ /** Paths outside public page ownership. Adapters keep their native access policy.
58
+ * HTML files are not classified as assets: /index.html must use the home gate.
59
+ */
60
+ export function isExcludedPublicationPath(pathname) {
61
+ const path = normalizePublicationPath(pathname);
62
+ return path.split('/').some(segment => privateSegment(segment) || segment.startsWith('.') || segment.startsWith('@')) ||
63
+ /\.(?:avif|bmp|css|csv|eot|gif|ico|jpe?g|js|json|map|mjs|mp3|mp4|ogg|otf|pdf|png|svg|ttf|txt|wasm|webm|webp|woff2?|xml)$/i.test(path);
64
+ }
65
+
66
+ /** Strict public configuration. No secret, caller-controlled origin or implicit route inference. */
67
+ export function validatePublicationConfig(value) {
68
+ try {
69
+ if (!exactKeys(value, ['version', 'apiOrigin', 'siteId', 'environmentId', 'websiteOrigin', 'routes']) || value.version !== 1 ||
70
+ typeof value.siteId !== 'string' || !uuid.test(value.siteId) || typeof value.environmentId !== 'string' || !uuid.test(value.environmentId) ||
71
+ !Array.isArray(value.routes) || !integer(value.routes.length, 1, PUBLICATION_MAX_ROUTES)) return fail();
72
+ const seen = new Set();
73
+ const routes = value.routes.map(route => {
74
+ if (!exactKeys(route, ['path', 'kind', 'slug', 'locale']) || !boundedString(route.path, 500) || !/^\/(?:[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*)?$/.test(route.path) || normalizePublicationPath(route.path) !== route.path ||
75
+ route.path.includes('%') || route.path.split('/').some(privateSegment) ||
76
+ !boundedString(route.kind, 40) || !/^[a-z][a-z0-9_-]*$/.test(route.kind) ||
77
+ !boundedString(route.slug, 160) || !slugPattern.test(route.slug) || secretPattern.test(route.path + route.kind + route.slug) || seen.has(route.path)) return fail();
78
+ seen.add(route.path);
79
+ return Object.freeze({ path: route.path, kind: route.kind, slug: route.slug, locale: canonicalLocale(route.locale) });
80
+ });
81
+ return Object.freeze({ version: 1, apiOrigin: origin(value.apiOrigin), siteId: value.siteId.toLowerCase(), environmentId: value.environmentId.toLowerCase(), websiteOrigin: origin(value.websiteOrigin), routes: Object.freeze(routes) });
82
+ } catch { return fail(); }
83
+ }
84
+
85
+ /** Throws on malformed paths, returns null only for a well-formed unmanaged route. */
86
+ export function resolvePublicationRoute(configuration, pathname) {
87
+ const path = normalizePublicationPath(pathname);
88
+ return validatePublicationConfig(configuration).routes.find(route => route.path === path) ?? null;
89
+ }
90
+
91
+ const textTypes = new Set(['paragraph', 'heading', 'quote', 'list', 'code']);
92
+ const leafTypes = new Set([...textTypes, 'image', 'gallery', 'button', 'divider', 'spacer', 'faq', 'table', 'embed', 'form']);
93
+ const styleValues = {
94
+ align: ['left', 'center', 'right'], size: ['small', 'normal', 'large'], tone: ['default', 'muted', 'accent', 'inverse'],
95
+ spaceAbove: ['none', 'small', 'medium', 'large'], spaceBelow: ['none', 'small', 'medium', 'large'],
96
+ };
97
+ const optionalEnum = (value, key, choices) => !own(value, key) || choices.includes(value[key]);
98
+ const optionalString = (value, key, max) => !own(value, key) || boundedString(value[key], max);
99
+ const optionalBoolean = (value, key) => !own(value, key) || typeof value[key] === 'boolean';
100
+ const optionalInteger = (value, key, min, max) => !own(value, key) || integer(value[key], min, max);
101
+ function publicUrl(value, { internal = false, secure = false } = {}) {
102
+ if (!boundedString(value, 2048) || /[\\\u0000-\u001f\u007f]/.test(value)) return false;
103
+ if (internal && value.startsWith('/') && !value.startsWith('//')) return true;
104
+ try { const url = new URL(value); return !url.username && !url.password && (secure ? url.protocol === 'https:' : ['https:', 'http:'].includes(url.protocol)); }
105
+ catch { return false; }
106
+ }
107
+ function imageData(value) {
108
+ return object(value) && publicUrl(value.url) && boundedString(value.alt, 300) && optionalInteger(value, 'width', 1, 20000) && optionalInteger(value, 'height', 1, 20000) &&
109
+ (!own(value, 'srcset') || boundedString(value.srcset, 4000) && value.srcset.split(',').every(candidate => {
110
+ const [url, width, ...rest] = candidate.trim().split(/\s+/); return !rest.length && publicUrl(url) && /^[1-9]\d{0,4}w$/.test(width ?? '');
111
+ }));
112
+ }
113
+ function leafData(block) {
114
+ const d = block.data;
115
+ if (textTypes.has(block.type)) return block.type !== 'heading' || d === undefined || object(d) && [2, 3, 4].includes(d.level);
116
+ if (block.type === 'divider') return d === undefined || object(d);
117
+ if (!object(d)) return false;
118
+ switch (block.type) {
119
+ case 'image': return imageData(d) && optionalEnum(d, 'align', ['left', 'center', 'right', 'full']) && (!own(d, 'link') || publicUrl(d.link, { internal: true }));
120
+ case 'gallery': return Array.isArray(d.images) && integer(d.images.length, 1, 30) && d.images.every(image => imageData(image) && uuid.test(image.id)) && optionalEnum(d, 'columns', [2, 3, 4]);
121
+ case 'button': return publicUrl(d.url, { internal: true }) && optionalEnum(d, 'style', ['primary', 'secondary', 'link']) && optionalEnum(d, 'align', ['left', 'center', 'right']) && optionalBoolean(d, 'newTab');
122
+ case 'spacer': return optionalInteger(d, 'height', 8, 400);
123
+ case 'faq': return Array.isArray(d.items) && integer(d.items.length, 1, 50) && d.items.every(item => object(item) && uuid.test(item.id) && boundedString(item.question, 500) && boundedString(item.answer, 5000));
124
+ case 'table': return optionalBoolean(d, 'header') && Array.isArray(d.rows) && integer(d.rows.length, 1, 100) && d.rows.every(row => Array.isArray(row) && integer(row.length, 1, 12) && row.length === d.rows[0].length && row.every(cell => boundedString(cell, 2000)));
125
+ case 'embed': return publicUrl(d.url, { secure: true }) && optionalEnum(d, 'provider', ['youtube', 'vimeo', 'iframe']) && optionalEnum(d, 'aspect', ['16:9', '4:3', '1:1']);
126
+ case 'form': return boundedString(d.formKey, 60) && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(d.formKey) &&
127
+ Array.isArray(d.fields) && integer(d.fields.length, 1, 20) && new Set(d.fields.map(field => field?.name)).size === d.fields.length &&
128
+ d.fields.every(field => object(field) && uuid.test(field.id) && boundedString(field.name, 40) && /^[a-z][a-z0-9_]*$/.test(field.name) && boundedString(field.label, 120) && ['text', 'email', 'phone', 'textarea', 'select', 'checkbox'].includes(field.type) && optionalBoolean(field, 'required') && optionalString(field, 'placeholder', 120) && (!own(field, 'options') || Array.isArray(field.options) && field.options.length <= 30 && field.options.every(option => boundedString(option, 80)))) &&
129
+ optionalString(d, 'submitLabel', 60) && optionalString(d, 'successMessage', 500) && optionalString(d, 'consentText', 300) && optionalString(d, 'notifyEmail', 200) && optionalBoolean(d, 'subscribe');
130
+ default: return false;
131
+ }
132
+ }
133
+ /** Validate the version-1 document without rendering it or exposing its fields.
134
+ * Validation is deliberately local to the integration; API authorization remains authoritative.
135
+ */
136
+ function validDocument(document) {
137
+ if (!object(document) || document.schemaVersion !== 1 || !boundedString(document.title, 200) || !document.title.trim() ||
138
+ !boundedString(document.excerpt, 1000) || !Array.isArray(document.blocks) || document.blocks.length > 200) return false;
139
+ const ids = new Set();
140
+ function blocks(values, level, max) {
141
+ if (!Array.isArray(values) || values.length > max) return false;
142
+ return values.every(block => {
143
+ if (!object(block) || !uuid.test(block.id) || ids.has(block.id) || ids.size >= 400 || block.version !== 1 || !boundedString(block.text, 30000)) return false;
144
+ ids.add(block.id);
145
+ if (block.style !== undefined && (!object(block.style) || Object.entries(styleValues).some(([key, values]) => !optionalEnum(block.style, key, values)) ||
146
+ !optionalBoolean(block.style, 'hideOnMobile') || !optionalBoolean(block.style, 'hideOnDesktop') ||
147
+ own(block.style, 'anchor') && (!boundedString(block.style.anchor, 80) || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(block.style.anchor)))) return false;
148
+ if (leafTypes.has(block.type)) return leafData(block);
149
+ const d = block.data;
150
+ if (!object(d)) return false;
151
+ if (block.type === 'section' && level === 0) return optionalEnum(d, 'background', ['none', 'muted', 'accent', 'dark']) && optionalEnum(d, 'padding', ['none', 'small', 'medium', 'large']) && optionalEnum(d, 'width', ['normal', 'wide', 'full']) && blocks(d.blocks, 1, 100);
152
+ if (block.type === 'columns' && level < 2) return optionalEnum(d, 'layout', ['1-1', '1-2', '2-1', '1-1-1', '1-1-1-1']) && optionalEnum(d, 'gap', ['small', 'medium', 'large']) && optionalBoolean(d, 'stackOnMobile') &&
153
+ Array.isArray(d.columns) && d.columns.length === (d.layout ?? '1-1').split('-').length && d.columns.every(column => blocks(column, 2, 50));
154
+ return false;
155
+ });
156
+ }
157
+ if (!blocks(document.blocks, 0, 200)) return false;
158
+ if (own(document, 'fields') && (!object(document.fields) || Object.keys(document.fields).length > 60 || Object.keys(document.fields).some(key => key.length > 40))) return false;
159
+ if (own(document, 'taxonomy')) {
160
+ if (!object(document.taxonomy)) return false;
161
+ for (const [key, max] of [['categories', 20], ['tags', 50]]) {
162
+ const values = document.taxonomy[key] ?? [];
163
+ if (!Array.isArray(values) || values.length > max || new Set(values).size !== values.length || values.some(value => !boundedString(value, 80) || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value))) return false;
164
+ }
165
+ }
166
+ return true;
167
+ }
168
+
169
+ function publicationEntry(raw, route, config) {
170
+ const envelope = JSON.parse(raw), data = envelope?.data;
171
+ if (!object(envelope) || !object(data) || own(envelope, 'preview') || own(data, 'preview') || !Array.isArray(data.items) || !integer(data.total, 0, 1) || data.items.length !== data.total) return fail();
172
+ if (data.total === 0) return null;
173
+ const entry = data.items[0];
174
+ if (!object(entry) || !uuid.test(entry.id) || entry.kind !== route.kind || entry.slug !== route.slug || canonicalLocale(entry.locale) !== route.locale ||
175
+ !integer(entry.version, 0, Number.MAX_SAFE_INTEGER) || !boundedString(entry.publishedAt, 64) || !Number.isFinite(Date.parse(entry.publishedAt)) ||
176
+ own(entry, 'preview') || own(entry, 'status') && entry.status !== 'published' || own(entry, 'revisionId') && !uuid.test(entry.revisionId) ||
177
+ own(entry, 'siteId') && entry.siteId !== config.siteId || own(entry, 'environmentId') && entry.environmentId !== config.environmentId || !validDocument(entry.document)) return fail();
178
+ // Keep the private reader contract independent of delivery metadata and never
179
+ // reflect unknown envelope/entry properties into a website response.
180
+ return Object.freeze({ id: entry.id, kind: entry.kind, slug: entry.slug, locale: canonicalLocale(entry.locale), version: entry.version,
181
+ ...(entry.revisionId ? { revisionId: entry.revisionId } : {}), publishedAt: entry.publishedAt, document: entry.document });
182
+ }
183
+
184
+ /** Public opaque identity of the immutable publication, never the mutable draft. */
185
+ export function publicationRevision(entry) {
186
+ if (!object(entry) || !uuid.test(entry.id) || !integer(entry.version, 0, Number.MAX_SAFE_INTEGER) || own(entry, 'revisionId') && !uuid.test(entry.revisionId)) return fail();
187
+ return `${entry.id}:${entry.version}${entry.revisionId ? `:${entry.revisionId}` : ''}`;
188
+ }
189
+
190
+ const pick = (value, keys) => Object.fromEntries(keys.filter(key => own(value, key)).map(key => [key, value[key]]));
191
+ const dataKeys = {
192
+ heading: ['level'], image: ['url', 'alt', 'width', 'height', 'srcset', 'align', 'link'], gallery: ['columns'],
193
+ button: ['url', 'style', 'align', 'newTab'], spacer: ['height'], faq: [], table: ['header', 'rows'], embed: ['url', 'provider', 'aspect'],
194
+ form: ['formKey', 'submitLabel', 'successMessage', 'consentText'], section: ['background', 'padding', 'width'], columns: ['layout', 'gap', 'stackOnMobile'],
195
+ };
196
+ function publicBlock(block) {
197
+ const result = pick(block, ['id', 'type', 'version', 'text']);
198
+ if (object(block.style)) result.style = pick(block.style, [...Object.keys(styleValues), 'hideOnMobile', 'hideOnDesktop', 'anchor']);
199
+ if (object(block.data)) {
200
+ const data = pick(block.data, dataKeys[block.type] ?? []);
201
+ if (block.type === 'gallery') data.images = block.data.images.map(image => pick(image, ['id', 'url', 'alt', 'width', 'height', 'srcset']));
202
+ if (block.type === 'faq') data.items = block.data.items.map(item => pick(item, ['id', 'question', 'answer']));
203
+ if (block.type === 'form') data.fields = block.data.fields.map(field => pick(field, ['id', 'name', 'label', 'type', 'required', 'placeholder', 'options']));
204
+ if (block.type === 'section') data.blocks = block.data.blocks.map(publicBlock);
205
+ if (block.type === 'columns') data.columns = block.data.columns.map(column => column.map(publicBlock));
206
+ result.data = data;
207
+ }
208
+ return result;
209
+ }
210
+
211
+ /** Browser projection contains renderer fields only, never custom fields or form destinations. */
212
+ export function publicPublishedEntry(entry) {
213
+ publicationRevision(entry);
214
+ if (!validDocument(entry.document)) return fail();
215
+ const document = { schemaVersion: 1, title: entry.document.title, excerpt: entry.document.excerpt, blocks: entry.document.blocks.map(publicBlock) };
216
+ if (entry.document.taxonomy) document.taxonomy = pick(entry.document.taxonomy, ['categories', 'tags']);
217
+ return { ...pick(entry, ['id', 'kind', 'slug', 'locale', 'version', 'revisionId', 'publishedAt']), document };
218
+ }
219
+
220
+ function untilAborted(pending, signal) {
221
+ return new Promise((resolve, reject) => {
222
+ const cancel = () => reject(new Error('Publication check unavailable.'));
223
+ signal.addEventListener('abort', cancel, { once: true });
224
+ if (signal.aborted) cancel();
225
+ Promise.resolve(pending).then(value => { signal.removeEventListener('abort', cancel); resolve(value); }, error => { signal.removeEventListener('abort', cancel); reject(error); });
226
+ });
227
+ }
228
+ function cancelBody(response) {
229
+ try { if (response?.body && !response.body.locked) void response.body.cancel().catch(() => {}); } catch { /* Safe cleanup only. */ }
230
+ }
231
+ async function boundedBody(response, limit, signal, deadline) {
232
+ const declared = response.headers.get('content-length');
233
+ if (declared !== null && (!/^\d+$/.test(declared) || Number(declared) > limit) || !response.body) return fail();
234
+ const reader = response.body.getReader(), decoder = new TextDecoder('utf-8', { fatal: true });
235
+ let bytes = 0, text = '';
236
+ try {
237
+ for (;;) {
238
+ const chunk = await untilAborted(reader.read(), signal);
239
+ if (Date.now() >= deadline || signal.aborted) return fail();
240
+ if (chunk.done) break;
241
+ if (!(chunk.value instanceof Uint8Array) || (bytes += chunk.value.byteLength) > limit) return fail();
242
+ text += decoder.decode(chunk.value, { stream: true });
243
+ }
244
+ return text + decoder.decode();
245
+ } finally {
246
+ void reader.cancel().catch(() => {});
247
+ reader.releaseLock();
248
+ }
249
+ }
250
+ const outcome = (state, reason) => {
251
+ const result = { state, status: state === 'missing' ? 404 : state === 'unavailable' ? 503 : 200 };
252
+ if (reason) Object.defineProperty(result, 'reason', { value: reason, enumerable: false, configurable: true });
253
+ return Object.freeze(result);
254
+ };
255
+
256
+ /** Create once per server runtime. Every managed request rechecks publication;
257
+ * no cross-request response cache, retry, visitor credentials or arbitrary URL.
258
+ */
259
+ export function createPublishedDocumentReader({ configuration, env = process.env, fetcher = globalThis.fetch, timeoutMs = PUBLICATION_TIMEOUT_MS, maxResponseBytes = PUBLICATION_MAX_RESPONSE_BYTES, maxConcurrent = PUBLICATION_MAX_CONCURRENT } = {}) {
260
+ if (typeof window !== 'undefined') throw new Error('Website publication checks require a private server runtime.');
261
+ const config = validatePublicationConfig(configuration);
262
+ if (typeof fetcher !== 'function' || !integer(timeoutMs, 1, PUBLICATION_TIMEOUT_MS) || !integer(maxResponseBytes, 1, PUBLICATION_MAX_RESPONSE_BYTES) || !integer(maxConcurrent, 1, PUBLICATION_MAX_CONCURRENT)) return fail();
263
+ const routes = new Map(config.routes.map(route => [route.path, route]));
264
+ let active = 0;
265
+ return async function check(pathname) {
266
+ let route, apiOrigin, key;
267
+ try {
268
+ if (typeof window !== 'undefined') return outcome('unavailable');
269
+ route = routes.get(normalizePublicationPath(pathname));
270
+ if (!route) return outcome('unmanaged');
271
+ if (!env || typeof env !== 'object' || Array.isArray(env) || typeof (key = env.CMS_API_KEY) !== 'string' || !keyPattern.test(key) ||
272
+ env.CMS_SITE_ID !== undefined && String(env.CMS_SITE_ID).toLowerCase() !== config.siteId ||
273
+ env.CMS_ENVIRONMENT_ID !== undefined && String(env.CMS_ENVIRONMENT_ID).toLowerCase() !== config.environmentId ||
274
+ env.CMS_WEBSITE_ORIGIN !== undefined && origin(env.CMS_WEBSITE_ORIGIN) !== config.websiteOrigin || active >= maxConcurrent) {
275
+ if (!key || typeof key !== 'string' || !keyPattern.test(key)) {
276
+ console.warn('[UniversalCMS] Publication delivery check failed: CMS_API_KEY is missing or invalid in private environment.');
277
+ return outcome('unavailable', 'missing_or_invalid_key');
278
+ }
279
+ return outcome('unavailable');
280
+ }
281
+ apiOrigin = env.CMS_API_BASE_URL === undefined ? config.apiOrigin : origin(env.CMS_API_BASE_URL);
282
+ } catch { return outcome('unavailable'); }
283
+ const url = new URL(`/api/v1/delivery/sites/${config.siteId}/content`, apiOrigin);
284
+ url.search = new URLSearchParams({ environmentId: config.environmentId, kind: route.kind, slug: route.slug, locale: route.locale, limit: '2' }).toString();
285
+ const controller = new AbortController(), deadline = Date.now() + timeoutMs;
286
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
287
+ let response;
288
+ active++;
289
+ try {
290
+ const pending = Promise.resolve(fetcher(url, {
291
+ method: 'GET', headers: { Authorization: `Bearer ${key}`, Accept: 'application/json' },
292
+ credentials: 'omit', redirect: 'error', cache: 'no-store', referrerPolicy: 'no-referrer', signal: controller.signal,
293
+ })).then(value => { if (controller.signal.aborted) cancelBody(value); return value; });
294
+ response = await untilAborted(pending, controller.signal);
295
+ if (response && [401, 403].includes(response.status)) {
296
+ console.warn(`[UniversalCMS] Delivery API rejected CMS_API_KEY (HTTP ${response.status} Unauthorized) for site ${config.siteId} and environment ${config.environmentId}. Verify private server key in .env.local.`);
297
+ return outcome('unavailable', 'unauthorized');
298
+ }
299
+ if (controller.signal.aborted || Date.now() >= deadline || !(response instanceof Response) || response.status !== 200 || response.redirected ||
300
+ ['opaque', 'opaqueredirect'].includes(response.type) || response.url && response.url !== url.href ||
301
+ response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() !== 'application/json') return outcome('unavailable');
302
+ const entry = publicationEntry(await boundedBody(response, maxResponseBytes, controller.signal, deadline), route, config);
303
+ if (controller.signal.aborted || Date.now() >= deadline) return outcome('unavailable');
304
+ return entry ? Object.freeze({ ...outcome('published'), entry }) : outcome('missing');
305
+ } catch { return outcome('unavailable'); }
306
+ finally { active--; clearTimeout(timer); controller.abort(); cancelBody(response); }
307
+ };
308
+ }
309
+
310
+ /** Availability-only compatibility API. Content never escapes through this result. */
311
+ export function createPublicationChecker(options) {
312
+ const read = createPublishedDocumentReader(options);
313
+ return async pathname => outcome((await read(pathname)).state);
314
+ }
315
+
316
+ /** Constant HTML only: no website, API, route, body or exception text is reflected. */
317
+ export function publicationErrorHtml(status) {
318
+ const missing = status === 404;
319
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><title>${missing ? '404 — Page not found' : 'Content temporarily unavailable'}</title></head><body><main><h1>${missing ? '404' : 'Content temporarily unavailable'}</h1><p>${missing ? 'Page not found.' : 'Please try again later.'}</p></main></body></html>`;
320
+ }
@@ -0,0 +1,22 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { PublishedDocument, PublishedDocumentEntry } from './publication.mjs';
3
+ export type Inventory = { publicSource?: 1; fingerprints?: { title: string; excerpt: string; blocks: Record<string,string> };
4
+ identity: { kind: string; slug: string; locale: string };
5
+ baseline: PublishedDocument;
6
+ titleBlockId?: string;
7
+ excerptBlockId?: string;
8
+ bindings: Record<string, { variants: { values: Array<string | number | boolean | null>; id: string }[] }>;
9
+ };
10
+ export function PublishedDocumentProvider(props: { initialEntry?: PublishedDocumentEntry; pathname: string; route: Inventory; children: ReactNode; formEndpoint?: string }): ReactNode;
11
+ /** Internal composition boundary: website settings supply head metadata. */
12
+ export function PublicationMetadataOwner(props: { children: ReactNode }): ReactNode;
13
+ export function CmsBound(props: { slot: string; selector: Array<string | number | boolean | null>; children: ReactNode }): ReactNode;
14
+ /** includeUnbound also renders baseline blocks that have no native binding. */
15
+ export function CmsAdditionalContent(props?: { includeUnbound?: boolean }): ReactNode;
16
+ /** Render all current published blocks with the shared safe block/form policy. */
17
+ export function CmsPublishedDocument(): ReactNode;
18
+ /** The current validated public snapshot for reviewed native component mappings. */
19
+ export function usePublishedDocument(): { entry: PublishedDocumentEntry; route: Inventory; blocks: ReadonlyMap<string, PublishedDocument['blocks'][number]> };
20
+ /** Search current published labels while keeping native row IDs and handlers. */
21
+ export function useCmsSearchText(): (selector: Array<string | number | boolean | null>) => string;
22
+ export function validateEntry(value: unknown, identity: { kind: string; slug: string; locale: string }): PublishedDocumentEntry;