@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,141 @@
1
+ /** Bounded, read-only check of the running adapter and an original website URL.
2
+ * This command never sends a delivery credential or changes CMS content.
3
+ */
4
+ import { readSourceSettings } from './source-setup.mjs';
5
+ import { validatePublicationConfig, normalizePublicationPath } from './publication.mjs';
6
+
7
+ const names = ['websiteOrigin', 'adminOrigin', 'siteId', 'environmentId'];
8
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
+ const revision = /^[0-9a-f-]{36}:\d{1,16}(?::[0-9a-f-]{36})?$/;
10
+ const fail = status => { throw { status }; };
11
+ const messages = {
12
+ 'configuration-error': 'Use the exact website origin, CMS admin origin, site and environment from Developer setup.',
13
+ 'missing-adapter': 'The running website has no managed publication diagnostic at this exact origin. Confirm the website folder and port, then run scoped preflight to diagnose missing setup. Repair an existing adapter in place using its reviewed validation; preserve custom routes and layouts. For a new supported installation, complete setup only after preflight passes. Restart or deploy the completed adapter, then retry. Saving a key or importing content does not install this endpoint.',
14
+ 'request-rejected': 'The running adapter rejected these origins or scope. Use the matching website folder and exact saved connection.',
15
+ 'unavailable': 'Private published delivery is unavailable. Check the website server key and API connection; no content was imported or published.',
16
+ 'invalid-response': 'The running publication diagnostic was redirected, invalid, oversized or did not match this connection.',
17
+ 'bindings-incomplete': 'Publication checks are installed, but reviewed native content bindings are incomplete.',
18
+ 'route-mismatch': 'The original website URL did not return its expected publication status and scoped runtime headers. Review routing, caches and deployment before claiming connection.',
19
+ 'source-closure-unverified': 'The running source endpoint did not confirm that its public export is closed for this connection. Restart or deploy after finalize-source, then review /api/cms-source before claiming migration is complete.',
20
+ timeout: 'The website did not finish publication verification within 10 seconds.',
21
+ 'transport-error': 'The website could not be reached at the saved origin. Start or deploy it, then retry.',
22
+ checked: 'The running publication adapter, registered bindings and this original URL passed. Actual rendered content still needs browser verification.',
23
+ };
24
+ const result = (status, details = {}) => ({ status, checked: status === 'checked', renderedVerified: false, ...details, message: messages[status] });
25
+ export function publicationCachePolicy(headers, websiteOrigin) {
26
+ const directives = (headers.get('cache-control') ?? '').toLowerCase().split(',').map(value => value.trim());
27
+ if (directives.some(value => value === 'public' || /^stale-/.test(value) || /^(?:s-maxage|max-age)=/.test(value) && !/^(?:s-maxage|max-age)=0$/.test(value))) return false;
28
+ if (directives.includes('no-store')) return true;
29
+ // Next development overwrites the proxy's Cache-Control. Accept only its
30
+ // exact revalidation policy on HTTP loopback with both CDN stores disabled.
31
+ const url = new URL(websiteOrigin);
32
+ return url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname) &&
33
+ directives.length === 2 && directives.includes('no-cache') && directives.includes('must-revalidate') &&
34
+ ['cdn-cache-control', 'vercel-cdn-cache-control'].every(name => headers.get(name)?.trim().toLowerCase() === 'no-store');
35
+ }
36
+ function origin(value) {
37
+ try {
38
+ if (typeof value !== 'string' || value.length > 2048 || /[\s\\]|cms_server\.|sk-[\w-]{12,}|AIza[\w-]{20,}/i.test(value)) return null;
39
+ const url = new URL(value);
40
+ return !url.username && !url.password && !url.search && !url.hash && url.pathname === '/' &&
41
+ (url.protocol === 'https:' || url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) ? url.origin : null;
42
+ } catch { return null; }
43
+ }
44
+ async function body(response, maximum, signal) {
45
+ const declared = response.headers.get('content-length');
46
+ if (declared !== null && (!/^\d+$/.test(declared) || Number(declared) > maximum) || !response.body) fail('invalid-response');
47
+ const reader = response.body.getReader(), decoder = new TextDecoder('utf-8', { fatal: true });
48
+ let text = '', bytes = 0;
49
+ try {
50
+ for (;;) {
51
+ const next = await abortable(reader.read(), signal);
52
+ if (next.done) break;
53
+ if ((bytes += next.value.byteLength) > maximum) fail('invalid-response');
54
+ text += decoder.decode(next.value, { stream: true });
55
+ }
56
+ return text + decoder.decode();
57
+ } finally { void reader.cancel().catch(() => {}); reader.releaseLock(); }
58
+ }
59
+ function abortable(pending, signal) {
60
+ return new Promise((resolve, reject) => {
61
+ const stop = () => reject({ status: 'timeout' });
62
+ signal.addEventListener('abort', stop, { once: true });
63
+ if (signal.aborted) stop();
64
+ Promise.resolve(pending).then(value => { signal.removeEventListener('abort', stop); resolve(value); }, error => { signal.removeEventListener('abort', stop); reject(error); });
65
+ });
66
+ }
67
+
68
+ export async function checkPublication(directory, options = {}, { fetcher = globalThis.fetch, timeoutMs = 10000 } = {}) {
69
+ if (!options || typeof options !== 'object' || Array.isArray(options) || Object.keys(options).some(name => ![...names, 'path'].includes(name)) ||
70
+ typeof fetcher !== 'function' || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 10000) return result('configuration-error');
71
+ let defaults = {};
72
+ try { if (names.some(name => options[name] === undefined)) defaults = await readSourceSettings(directory); } catch { return result('configuration-error'); }
73
+ const binding = Object.fromEntries(names.map(name => [name, options[name] ?? defaults[name]]));
74
+ binding.websiteOrigin = origin(binding.websiteOrigin); binding.adminOrigin = origin(binding.adminOrigin);
75
+ if (!binding.websiteOrigin || !binding.adminOrigin || binding.websiteOrigin === binding.adminOrigin || !['siteId', 'environmentId'].every(name => typeof binding[name] === 'string' && uuid.test(binding[name]))) return result('configuration-error');
76
+ binding.siteId = binding.siteId.toLowerCase(); binding.environmentId = binding.environmentId.toLowerCase();
77
+ let requestedPath;
78
+ try { if (options.path !== undefined) requestedPath = normalizePublicationPath(options.path); } catch { return result('configuration-error'); }
79
+ const url = new URL('/api/universalcms/publication', binding.websiteOrigin);
80
+ url.search = new URLSearchParams({ check: 'connection', siteId: binding.siteId, environmentId: binding.environmentId, ...(requestedPath ? { path: requestedPath } : {}) }).toString();
81
+ const controller = new AbortController(), timer = setTimeout(() => controller.abort(), timeoutMs);
82
+ let response;
83
+ const get = async target => {
84
+ const pending = Promise.resolve(fetcher(target, { method: 'GET', headers: { Accept: '*/*', Origin: binding.adminOrigin },
85
+ credentials: 'omit', redirect: 'error', cache: 'no-store', referrerPolicy: 'no-referrer', signal: controller.signal })).then(value => {
86
+ if (controller.signal.aborted) void value?.body?.cancel().catch(() => {});
87
+ return value;
88
+ });
89
+ const value = await abortable(pending, controller.signal);
90
+ if (!(value instanceof Response) || value.redirected || ['opaque', 'opaqueredirect'].includes(value.type) || value.url && value.url !== target.href) fail('invalid-response');
91
+ response = value;
92
+ return value;
93
+ };
94
+ try {
95
+ const probe = await get(url);
96
+ if (probe.status === 404) fail('missing-adapter');
97
+ if ([401, 403].includes(probe.status)) fail('request-rejected');
98
+ if (probe.status === 503) fail('unavailable');
99
+ if (probe.status !== 200 || probe.headers.get('content-type')?.split(';')[0] !== 'application/json' || probe.headers.get('access-control-allow-origin') !== binding.adminOrigin || probe.headers.has('access-control-allow-credentials')) fail('invalid-response');
100
+ const value = JSON.parse(await body(probe, 512 * 1024, controller.signal));
101
+ if (!value || Object.keys(value).sort().join() !== 'bindingFingerprint,check,contentBindings,coverage,environmentId,publication,routes,runtime,siteId,sourceExport,version' ||
102
+ value.version !== 1 || value.siteId !== binding.siteId || value.environmentId !== binding.environmentId || value.publication !== true || value.runtime !== 'server' || !['open', 'closed'].includes(value.sourceExport) ||
103
+ typeof value.contentBindings !== 'boolean' || !(value.bindingFingerprint === null || typeof value.bindingFingerprint === 'string' && /^[a-f0-9]{64}$/.test(value.bindingFingerprint))) fail('invalid-response');
104
+ try { validatePublicationConfig({ version: 1, apiOrigin: binding.websiteOrigin, websiteOrigin: binding.websiteOrigin, siteId: binding.siteId, environmentId: binding.environmentId, routes: value.routes }); } catch { fail('invalid-response'); }
105
+ if (!value.coverage || Object.keys(value.coverage).sort().join() !== 'complete,unmappedRoutes' || typeof value.coverage.complete !== 'boolean' || !Number.isInteger(value.coverage.unmappedRoutes) || value.coverage.unmappedRoutes < 0 || value.coverage.unmappedRoutes > value.routes.length ||
106
+ !value.check || Object.keys(value.check).some(name => !['path', 'state', 'revision'].includes(name)) || !value.routes.some(route => route.path === value.check.path) || requestedPath && value.check.path !== requestedPath ||
107
+ !['published', 'unpublished', 'unavailable'].includes(value.check.state) || value.check.state === 'published' && !revision.test(value.check.revision ?? '') || value.check.state !== 'published' && value.check.revision !== undefined) fail('invalid-response');
108
+ if (value.check.state === 'unavailable') fail('unavailable');
109
+ if (!value.contentBindings || !value.bindingFingerprint || !value.coverage.complete || value.coverage.unmappedRoutes) fail('bindings-incomplete');
110
+ const route = await get(new URL(value.check.path, binding.websiteOrigin)), expected = value.check.state === 'published' ? 200 : 404;
111
+ if (route.status !== expected || route.headers.get('content-type')?.split(';')[0] !== 'text/html' ||
112
+ route.headers.get('access-control-allow-origin') !== binding.adminOrigin || route.headers.has('access-control-allow-credentials') ||
113
+ route.headers.get('x-universalcms-site') !== binding.siteId || route.headers.get('x-universalcms-environment') !== binding.environmentId ||
114
+ route.headers.get('x-universalcms-route') !== encodeURIComponent(value.check.path) || route.headers.get('x-universalcms-state') !== value.check.state ||
115
+ value.check.state === 'published' && route.headers.get('x-universalcms-revision') !== value.check.revision || !publicationCachePolicy(route.headers, binding.websiteOrigin)) fail('route-mismatch');
116
+ await body(route, 2 * 1024 * 1024, controller.signal);
117
+ let sourceExportVerified = false;
118
+ if (value.sourceExport === 'closed') {
119
+ // Loaded configuration cannot prove that an older/custom source endpoint
120
+ // honored closure. Read the actual scoped endpoint on this deployment.
121
+ const sourceUrl = new URL('/api/cms-source', binding.websiteOrigin);
122
+ sourceUrl.search = new URLSearchParams({ siteId: binding.siteId, environmentId: binding.environmentId }).toString();
123
+ try {
124
+ const source = await get(sourceUrl);
125
+ if (source.status !== 410 || source.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() !== 'application/json' ||
126
+ source.headers.get('access-control-allow-origin') !== binding.adminOrigin || source.headers.has('access-control-allow-credentials') ||
127
+ !source.headers.get('cache-control')?.toLowerCase().split(',').map(value => value.trim()).includes('no-store') ||
128
+ !publicationCachePolicy(source.headers, binding.websiteOrigin)) fail('source-closure-unverified');
129
+ const closed = JSON.parse(await body(source, 1024, controller.signal));
130
+ if (!closed || Object.keys(closed).sort().join() !== 'environmentId,siteId,sourceExport,version' || closed.version !== 1 ||
131
+ closed.siteId !== binding.siteId || closed.environmentId !== binding.environmentId || closed.sourceExport !== 'closed') fail('source-closure-unverified');
132
+ sourceExportVerified = true;
133
+ } catch (error) {
134
+ if (controller.signal.aborted) throw error;
135
+ fail('source-closure-unverified');
136
+ }
137
+ }
138
+ return result('checked', { originalRouteVerified: true, path: value.check.path, state: value.check.state, sourceExport: value.sourceExport, sourceExportVerified, bindingFingerprint: value.bindingFingerprint });
139
+ } catch (error) { return result(controller.signal.aborted ? 'timeout' : error?.status && messages[error.status] ? error.status : error instanceof SyntaxError ? 'invalid-response' : 'transport-error'); }
140
+ finally { clearTimeout(timer); controller.abort(); try { if (response?.body && !response.body.locked) void response.body.cancel().catch(() => {}); } catch {} }
141
+ }
@@ -0,0 +1,2 @@
1
+ import type { PublishedDocumentEntry } from './publication.mjs';
2
+ export function validatePublishedDocumentEntry(value: unknown, identity: { kind: string; slug: string; locale: string }): PublishedDocumentEntry;
@@ -0,0 +1,59 @@
1
+ const plain = (value) => !!value && typeof value === "object" && !Array.isArray(value);
2
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3
+ const supported = new Set(['paragraph', 'heading', 'quote', 'list', 'code', 'image', 'gallery', 'button', 'divider', 'spacer', 'faq', 'table', 'embed', 'form', 'section', 'columns']);
4
+ /** The same-origin endpoint is the only browser authority. Reject malformed,
5
+ * cross-route, excessive, and ambiguous trees before rendering any native UI. */
6
+ export function validatePublishedDocumentEntry(value, identity) {
7
+ if (!plain(value) || value.preview === true || value.status !== undefined && value.status !== 'published' || !uuid.test(value.id) || !Number.isSafeInteger(value.version) || value.version < 1 || value.kind !== identity.kind || value.slug !== identity.slug || value.locale !== identity.locale || value.revisionId !== undefined && !uuid.test(value.revisionId) || typeof value.publishedAt !== 'string' || !Number.isFinite(Date.parse(value.publishedAt)))
8
+ throw Error('Invalid published entry');
9
+ const doc = value.document;
10
+ if (!plain(doc) || doc.schemaVersion !== 1 || typeof doc.title !== 'string' || doc.title.length > 200 || typeof doc.excerpt !== 'string' || doc.excerpt.length > 1000 || !Array.isArray(doc.blocks))
11
+ throw Error('Invalid published document');
12
+ const ids = new Set();
13
+ let count = 0, nodes = 0;
14
+ function safeData(item, depth = 0) {
15
+ if (++nodes > 25000 || depth > 12)
16
+ throw Error('Invalid document budget');
17
+ if (item === null || typeof item === 'boolean' || typeof item === 'number' && Number.isFinite(item))
18
+ return;
19
+ if (typeof item === 'string' && item.length <= 30000)
20
+ return;
21
+ if (Array.isArray(item) && item.length <= 400) {
22
+ for (const child of item)
23
+ safeData(child, depth + 1);
24
+ return;
25
+ }
26
+ if (plain(item) && Object.keys(item).length <= 50 && !Object.keys(item).some(key => ['__proto__', 'constructor', 'prototype', 'notifyEmail', 'subscribe'].includes(key))) {
27
+ for (const child of Object.values(item))
28
+ safeData(child, depth + 1);
29
+ return;
30
+ }
31
+ throw Error('Invalid document data');
32
+ }
33
+ function inspect(blocks, parent = 'root') {
34
+ if (blocks.length > 400)
35
+ throw Error('Invalid block budget');
36
+ for (const block of blocks) {
37
+ if (!plain(block) || ++count > 400 || !uuid.test(block.id) || ids.has(block.id) || block.version !== 1 || !supported.has(block.type) || typeof block.text !== 'string' || block.text.length > 30000 || block.data !== undefined && !plain(block.data) || block.style !== undefined && !plain(block.style))
38
+ throw Error('Invalid published block');
39
+ ids.add(block.id);
40
+ if (block.data)
41
+ safeData(block.data);
42
+ if (block.style)
43
+ safeData(block.style);
44
+ if (block.type === 'section') {
45
+ if (parent !== 'root' || !Array.isArray(block.data?.blocks))
46
+ throw Error('Invalid section');
47
+ inspect(block.data.blocks, 'section');
48
+ }
49
+ if (block.type === 'columns') {
50
+ if (parent === 'columns' || !Array.isArray(block.data?.columns) || block.data.columns.length > 4 || block.data.columns.some((column) => !Array.isArray(column)))
51
+ throw Error('Invalid columns');
52
+ for (const column of block.data.columns)
53
+ inspect(column, 'columns');
54
+ }
55
+ }
56
+ }
57
+ inspect(doc.blocks);
58
+ return value;
59
+ }
@@ -0,0 +1,134 @@
1
+ /** Bounded source edits for the original Next routes. Never execute project code. */
2
+ import ts from './vendor/typescript.cjs';
3
+ import { projectRoot, readProjectFile, SetupError } from './project-files.mjs';
4
+ import { inspectSourceRoutes } from './source-routes.mjs';
5
+ import { validatePublicationConfig } from './publication.mjs';
6
+
7
+ const script = '<script id="universalcms-publication-client" src="/.well-known/universalcms-publication-client.js"></script>';
8
+ const fail = message => { throw new SetupError(message); };
9
+ const unbox = node => {
10
+ while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node))) node = node.expression;
11
+ return node;
12
+ };
13
+
14
+ /** Exact bytes are also recognized by source discovery; arbitrary proxies remain private. */
15
+ export function nextPublicationProxyTemplate(appDirectory, previous = false) {
16
+ if (!['app', 'src/app'].includes(appDirectory)) throw Error('Invalid application directory.');
17
+ const prefix = appDirectory === 'src/app' ? '../' : './';
18
+ const source = `// Generated by @universalcms/connect. Publication availability only; native application policy remains authoritative.\nimport { NextResponse, type NextRequest } from 'next/server';\nimport { createNextPublicationProxy } from '@universalcms/connect/next-publication';\nimport configuration from '${prefix}.universalcms/publication.config.json';\nimport sourceConfiguration from '${prefix}.universalcms/source.config.json';\nimport contentBindings from '${prefix}.universalcms/content.bindings.json';\n\nconst handle = createNextPublicationProxy({ configuration, sourceConfiguration, contentBindings });\nexport async function proxy(request: NextRequest) {\n const result = await handle(request);\n if (result.response) return result.response;\n const response = NextResponse.next({ request: { headers: result.requestHeaders } });\n for (const [name, value] of Object.entries(result.headers)) response.headers.set(name, value);\n return response;\n}\n\n// All document and RSC requests use the same gate, including encoded paths.\nexport const config = { matcher: ['/((?!_next/static/|_next/image/).*)'] };\n`;
19
+ return previous ? source.replace(`import sourceConfiguration from '${prefix}.universalcms/source.config.json';\nimport contentBindings from '${prefix}.universalcms/content.bindings.json';\n`, '').replace('createNextPublicationProxy({ configuration, sourceConfiguration, contentBindings })', 'createNextPublicationProxy({ configuration })') : source;
20
+ }
21
+
22
+ /** The settings extension keeps the exact publication proxy boundary. */
23
+ export function nextWebsiteProxyTemplate(appDirectory, { installedSections } = {}) {
24
+ if (installedSections !== undefined && (!Array.isArray(installedSections) || new Set(installedSections).size !== installedSections.length || installedSections.some(name => !['branding','social','seo','languages','analytics','navigation','experience','scripts','redirects','authors','templates','taxonomies'].includes(name)))) throw Error('Invalid installed website sections.');
25
+ let source = nextPublicationProxyTemplate(appDirectory)
26
+ .replaceAll('createNextPublicationProxy', 'createNextWebsiteProxy')
27
+ .replace('@universalcms/connect/next-publication', '@universalcms/connect/website-next');
28
+ if (installedSections !== undefined) source = source.replace('createNextWebsiteProxy({ configuration, sourceConfiguration, contentBindings })', `createNextWebsiteProxy({ configuration, sourceConfiguration, contentBindings, installedSections: ${JSON.stringify(installedSections)} })`);
29
+ return source;
30
+ }
31
+
32
+ function injectLayout(source, filename) {
33
+ const file = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
34
+ if (file.parseDiagnostics.length || file.statements.some(statement => ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression) && statement.expression.text === 'use client')) fail('Publication setup requires a valid server root layout. Preserve this layout and integrate manually.');
35
+ let component;
36
+ for (const statement of file.statements) {
37
+ if (ts.isFunctionDeclaration(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)) component = statement;
38
+ if (ts.isExportAssignment(statement)) {
39
+ const value = unbox(statement.expression);
40
+ if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) component = value;
41
+ else if (ts.isIdentifier(value)) {
42
+ component = file.statements.find(candidate => ts.isFunctionDeclaration(candidate) && candidate.name?.text === value.text);
43
+ if (!component) for (const declaration of file.statements.filter(ts.isVariableStatement).flatMap(statement => [...statement.declarationList.declarations])) {
44
+ if (ts.isIdentifier(declaration.name) && declaration.name.text === value.text) component = unbox(declaration.initializer);
45
+ }
46
+ }
47
+ }
48
+ }
49
+ let root;
50
+ if (component?.body && ts.isBlock(component.body)) {
51
+ const returns = component.body.statements.filter(ts.isReturnStatement);
52
+ if (returns.length === 1) root = unbox(returns[0].expression);
53
+ } else root = unbox(component?.body);
54
+ if (!root || !ts.isJsxElement(root) || root.openingElement.tagName.getText(file) !== 'html') fail('Publication setup requires one literal html/body root layout. Dynamic or wrapped roots need manual integration.');
55
+ const elements = root.children.filter(ts.isJsxElement);
56
+ const heads = elements.filter(node => node.openingElement.tagName.getText(file) === 'head');
57
+ const bodies = elements.filter(node => node.openingElement.tagName.getText(file) === 'body');
58
+ if (heads.length > 1 || bodies.length !== 1 || elements.length !== heads.length + bodies.length || root.children.some(node => !ts.isJsxElement(node) && !ts.isJsxText(node) && !(ts.isJsxExpression(node) && !node.expression))) fail('Publication setup needs ordinary head/body layout children. Existing layout was preserved.');
59
+ const markerNodes = [];
60
+ function visit(node) {
61
+ if ((ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) && node.getText(file).includes('universalcms-publication-client')) {
62
+ const tag = ts.isJsxElement(node) ? node.openingElement : node;
63
+ if (tag.tagName.getText(file) === 'script') markerNodes.push(node);
64
+ }
65
+ ts.forEachChild(node, visit);
66
+ }
67
+ visit(root);
68
+ if (markerNodes.length) {
69
+ if (markerNodes.length !== 1 || markerNodes[0].getText(file) !== script || !heads.length || !heads[0].children.includes(markerNodes[0])) fail('The existing publication script was customized. Preserve your edits and integrate manually.');
70
+ return source;
71
+ }
72
+ if (source.includes('universalcms-publication-client')) fail('A publication script marker already exists outside the supported head. Review it manually.');
73
+ const index = heads.length ? heads[0].openingElement.end : root.openingElement.end;
74
+ const inserted = heads.length ? script : `<head>${script}</head>`;
75
+ return source.slice(0, index) + inserted + source.slice(index);
76
+ }
77
+
78
+ export async function planNextPublication(directory, configuration, { websiteSettings = false, installedSections } = {}) {
79
+ const root = await projectRoot(directory);
80
+ const config = validatePublicationConfig(configuration);
81
+ let manifest;
82
+ try { manifest = JSON.parse(await readProjectFile(root, 'package.json')); } catch { fail('Publication setup needs a valid website package.json.'); }
83
+ const next = manifest?.dependencies?.next ?? manifest?.devDependencies?.next;
84
+ if (manifest?.workspaces || typeof next !== 'string' || !/^(?:\^|~)?16\.\d+\.\d+$/.test(next)) fail('Automatic publication control supports one Next.js 16 App Router website.');
85
+ for (const extension of ['js', 'mjs', 'cjs', 'ts']) {
86
+ const source = await readProjectFile(root, `next.config.${extension}`);
87
+ if (source && /\b(?:pageExtensions|basePath|rewrites|redirects|i18n|headers|adapterPath|skipProxyUrlNormalize|skipMiddlewareUrlNormalize)\b|\boutput\s*:\s*['"]export['"]|\bruntime\s*:\s*['"]edge['"]/.test(source)) fail('Custom routes, headers, URL normalization or static/Edge hosting need manual publication integration. Cache Components is supported.');
88
+ }
89
+ let inventory;
90
+ try { inventory = await inspectSourceRoutes(root); } catch { fail('The original Next route inventory needs manual review before publication control.'); }
91
+ const native = new Set(inventory.routes.map(route => `/${route}`));
92
+ if (config.routes.some(route => !native.has(route.path))) fail('Each publication mapping must refer to an existing reviewed public route. Unknown routes were preserved.');
93
+ const roots = [...new Set(Object.values(inventory.files).map(filename => filename.startsWith('src/app/') ? 'src/app' : 'app'))];
94
+ if (roots.length !== 1) fail('Publication control requires exactly one App Router directory.');
95
+ const appDirectory = roots[0];
96
+ const proxyPath = appDirectory === 'src/app' ? 'src/proxy.ts' : 'proxy.ts';
97
+ const proxy = websiteSettings ? nextWebsiteProxyTemplate(appDirectory, {installedSections}) : nextPublicationProxyTemplate(appDirectory);
98
+ for (const stem of ['middleware', 'src/middleware', 'proxy', 'src/proxy']) for (const extension of ['js', 'ts']) {
99
+ const filename = `${stem}.${extension}`, current = await readProjectFile(root, filename);
100
+ const recognized = [proxy, nextWebsiteProxyTemplate(appDirectory), nextWebsiteProxyTemplate(appDirectory, {installedSections}), nextPublicationProxyTemplate(appDirectory), nextPublicationProxyTemplate(appDirectory, true)];
101
+ if (current !== null && (filename !== proxyPath || !recognized.includes(current))) fail('An existing middleware or proxy needs manual integration. It was not replaced.');
102
+ }
103
+ // A broad API route or static asset must never be silently shadowed by our endpoints.
104
+ for (const filename of [
105
+ 'public/.well-known/universalcms-publication-client.js',
106
+ ...['js', 'jsx', 'ts', 'tsx'].flatMap(extension => [
107
+ `${appDirectory}/api/universalcms/publication/route.${extension}`,
108
+ `${appDirectory}/api/universalcms/document/route.${extension}`,
109
+ `${appDirectory}/api/[...slug]/route.${extension}`,
110
+ `${appDirectory}/api/[[...slug]]/route.${extension}`,
111
+ `${appDirectory}/api/universalcms/[...slug]/route.${extension}`,
112
+ `${appDirectory}/api/universalcms/[[...slug]]/route.${extension}`,
113
+ ]),
114
+ ]) if (await readProjectFile(root, filename) !== null) fail('The publication endpoint overlaps an existing website route or asset. Integrate it manually.');
115
+ const layouts = [];
116
+ for (const extension of ['js', 'jsx', 'ts', 'tsx']) {
117
+ const filename = `${appDirectory}/layout.${extension}`, source = await readProjectFile(root, filename);
118
+ if (source !== null) layouts.push({ filename, source });
119
+ }
120
+ if (layouts.length !== 1) fail('Publication setup needs exactly one server root layout.');
121
+ const { filename, source } = layouts[0];
122
+ if (/\bruntime\s*=\s*['"]edge['"]/.test(source)) fail('Publication setup requires a Node-capable Next deployment.');
123
+ const updated = injectLayout(source, filename);
124
+ const currentProxy = await readProjectFile(root, proxyPath);
125
+ return {
126
+ root, framework: 'next', appDirectory,
127
+ files: [
128
+ { path: proxyPath, content: proxy, state: currentProxy === null ? 'create' : currentProxy === proxy ? 'unchanged' : 'update', ...(currentProxy !== null && currentProxy !== proxy ? { previous: currentProxy } : {}) },
129
+ { path: filename, content: updated, state: updated === source ? 'unchanged' : 'update', ...(updated === source ? {} : { previous: source }) },
130
+ ],
131
+ unmanagedRoutes: inventory.routes.map(route => `/${route}`).filter(route => !config.routes.some(item => item.path === route)),
132
+ unresolvedRoutes: inventory.unresolved,
133
+ };
134
+ }
@@ -0,0 +1,138 @@
1
+ /** Original-route publication setup. Public mappings only; no website code execution. */
2
+ import { lstat } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { projectRoot, readProjectFile, SetupError } from './core.mjs';
5
+ import { inspectSourceRoutes } from './source-routes.mjs';
6
+ import { validatePublicationConfig } from './publication.mjs';
7
+ import { planNextPublication } from './publication-next-setup.mjs';
8
+ import { assertNextUnresolvedRouteOwnership, inspectOwnedDemo } from './content-next-setup.mjs';
9
+ import { inspectReviewedNativeOwnership } from './reviewed-native-ownership.mjs';
10
+ import { inspectReviewedCommerceOwnership } from './reviewed-commerce-ownership.mjs';
11
+ import { validateInstalledAstro } from './astro-setup.mjs';
12
+ import { viteSourceWrapper } from './source-setup.mjs';
13
+ import { isNativeFramework, nativeFramework, validateInstalledNative } from './native-adapters.mjs';
14
+
15
+ export const PUBLICATION_CONFIG_FILE = '.universalcms/publication.config.json';
16
+ export const PUBLICATION_ROUTES_FILE = '.universalcms/publication.routes.json';
17
+ const json = value => `${JSON.stringify(value, null, 2)}\n`;
18
+ const fail = message => { throw new SetupError(message); };
19
+ const identity = row => {
20
+ try { return `${row.kind}:${Intl.getCanonicalLocales(row.locale)[0]}:${row.slug}`; }
21
+ catch { fail('Public source identities need a valid content locale.'); }
22
+ };
23
+
24
+ export async function readPublicationConfiguration(root) {
25
+ const text = await readProjectFile(root, PUBLICATION_CONFIG_FILE);
26
+ if (text === null) return null;
27
+ if ((await lstat(path.join(root, PUBLICATION_CONFIG_FILE))).nlink !== 1) fail('Publication configuration must be an ordinary file without hard links.');
28
+ try { return validatePublicationConfig(JSON.parse(text)); }
29
+ catch { fail('The original-route publication configuration is invalid. Review it before continuing.'); }
30
+ }
31
+
32
+ export function viteServerTemplate() {
33
+ return `// Generated by @universalcms/connect. Run from the website root after its build.\nimport { createVitePublicationServer } from '@universalcms/connect/vite-publication-server';\nimport configuration from './publication.config.json' with { type: 'json' };\nimport sourceConfiguration from './source.config.json' with { type: 'json' };\nimport contentBindings from './content.bindings.json' with { type: 'json' };\n\nconst port = Number(process.env.PORT ?? (new URL(configuration.websiteOrigin).port || 3000));\nif (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Configure a valid website PORT.');\nconst server = createVitePublicationServer({ root: process.cwd(), configuration, sourceConfiguration, contentBindings });\nserver.listen(port, process.env.HOST ?? '0.0.0.0');\n`;
34
+ }
35
+
36
+ async function ownedFile(root, name, content, previous) {
37
+ const current = await readProjectFile(root, name);
38
+ if (current !== null && (await lstat(path.join(root, name))).nlink !== 1) fail('Publication setup cannot replace linked files.');
39
+ if (current !== null && current !== content && current !== previous) fail('A publication integration file contains custom changes. Preserve them and review the integration manually.');
40
+ return { path: name, content, state: current === content ? 'unchanged' : current === null ? 'create' : 'update', ...(current !== null && current !== content ? { previous: current } : {}) };
41
+ }
42
+
43
+ export async function planPublicationSetup(directory, connection, source) {
44
+ const root = await projectRoot(directory);
45
+ if (!source || source.retainedAdapter || !source.identities?.length) fail('Original-route publication setup needs a complete public source inventory. Review custom adapters or missing mappings before continuing.');
46
+ if (source.framework !== 'vite' && source.framework !== 'astro' && !isNativeFramework(source.framework)) {
47
+ const inventory = await inspectSourceRoutes(root);
48
+ const native = await inspectReviewedNativeOwnership(root) ?? await inspectReviewedCommerceOwnership(root);
49
+ assertNextUnresolvedRouteOwnership(inventory, native, await inspectOwnedDemo(root));
50
+ }
51
+ const prior = await readPublicationConfiguration(root);
52
+ const customText = await readProjectFile(root, PUBLICATION_ROUTES_FILE);
53
+ let routes;
54
+ if (customText !== null) {
55
+ if ((await lstat(path.join(root, PUBLICATION_ROUTES_FILE))).nlink !== 1) fail('Publication route mappings cannot be linked files.');
56
+ try { routes = JSON.parse(customText); if (!Array.isArray(routes)) throw Error(); }
57
+ catch { fail('Use a JSON array of explicit path, kind, slug and locale mappings in .universalcms/publication.routes.json.'); }
58
+ } else if (source.framework === 'astro' || isNativeFramework(source.framework)) {
59
+ routes = source.routes;
60
+ } else if (source.framework === 'vite') {
61
+ // The source contract uses original nested slugs. Display every mapping for
62
+ // review before Save; custom paths can be supplied in publication.routes.json.
63
+ routes = source.identities.map(entry => ({ path: entry.kind === 'page' && entry.slug === 'home' ? '/' : `/${entry.slug}`, ...entry }));
64
+ } else {
65
+ const inventory = await inspectSourceRoutes(root);
66
+ if (inventory.routes.includes('') && inventory.routes.includes('home')) fail('The home page and /home need explicit, distinct publication identities.');
67
+ const known = new Map(inventory.routes.map(route => [route || 'home', `/${route}`]));
68
+ routes = source.identities.map(entry => {
69
+ if (entry.kind !== 'page' || !known.has(entry.slug)) fail('A source entry has no exact original Next.js route. Add explicit publication.routes.json mappings before setup.');
70
+ return { path: known.get(entry.slug), ...entry };
71
+ });
72
+ }
73
+ let config;
74
+ try { config = validatePublicationConfig({ version: 1, apiOrigin: connection.apiOrigin, siteId: connection.siteId, environmentId: connection.environmentId, websiteOrigin: connection.websiteOrigin, routes }); }
75
+ catch { fail('Publication mappings must use unique public website paths and valid content identities. Review .universalcms/publication.routes.json; private, API, preview and framework routes cannot be managed.'); }
76
+ const sourceIdentities = new Set(source.identities.map(identity));
77
+ const mappedIdentities = new Set(config.routes.map(identity));
78
+ if ([...sourceIdentities].some(key => !mappedIdentities.has(key)) || [...mappedIdentities].some(key => !sourceIdentities.has(key))) fail('Every exported page/post needs an exact original URL mapping, and every mapping must refer to an exported identity.');
79
+ const homepage = source.identities.find(entry => entry.kind === 'page' && entry.slug === 'home');
80
+ if (homepage && !config.routes.some(route => route.path === '/' && identity(route) === identity(homepage))) fail('The exported home page must be mapped to /.');
81
+ // Reruns cannot silently remove protection from an already managed URL.
82
+ if (prior && prior.routes.some(route => !config.routes.some(next => next.path === route.path && next.kind === route.kind && next.slug === route.slug))) fail('Previously managed routes were removed or remapped. Review publication.config.json explicitly before changing route ownership.');
83
+ const files = [await ownedFile(root, PUBLICATION_CONFIG_FILE, json(config), prior ? await readProjectFile(root, PUBLICATION_CONFIG_FILE) : undefined)];
84
+ if (customText !== null) files.push({ path: PUBLICATION_ROUTES_FILE, content: customText, state: 'unchanged' });
85
+ if (source.framework === 'vite') {
86
+ // These server-owned paths must not silently replace existing public files
87
+ // (including extensionless files or directories used by a custom host).
88
+ for (const filename of ['public/api/universalcms/document', 'public/api/universalcms/publication', 'public/.well-known/universalcms-publication-client.js']) {
89
+ let exists = false;
90
+ try { await lstat(path.join(root, filename)); exists = true; }
91
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
92
+ if (exists) fail('A publication endpoint overlaps an existing public website asset. Preserve it and integrate manually.');
93
+ }
94
+ files.push(await ownedFile(root, '.universalcms/serve.mjs', viteServerTemplate(), viteServerTemplate().replace("import contentBindings from './content.bindings.json' with { type: 'json' };\n", '').replace('configuration, sourceConfiguration, contentBindings', 'configuration, sourceConfiguration')));
95
+ }
96
+ else if (source.framework !== 'astro' && !isNativeFramework(source.framework)) files.push(...(await planNextPublication(root, config)).files);
97
+ return { root, config, framework: source.framework ?? 'next', files };
98
+ }
99
+
100
+ /** Recheck installed publication files before private key entry. */
101
+ export async function planInstalledPublication(directory) {
102
+ const root = await projectRoot(directory), config = await readPublicationConfiguration(root);
103
+ if (!config) {
104
+ // A deleted configuration must not turn an installed original-route guard
105
+ // into an apparently valid demo-only key setup. Inspect fixed public files,
106
+ // never import website code or inspect its private environment.
107
+ for(const name of ['.universalcms/website.integration.json','.universalcms/content.bindings.json'])if(await readProjectFile(root,name)!==null)fail('The original-route publication configuration is missing. Restore the reviewed configuration before saving a server key.');
108
+ const names = ['.universalcms/astro.middleware.mjs', '.universalcms/source.vite.config.mjs', '.universalcms/serve.mjs',
109
+ ...['vue','nuxt','angular','svelte','static'].flatMap(name=>[`.universalcms/${name}.serve.mjs`,`.universalcms/${name}.runtime.mjs`]), 'src/hooks.server.ts', 'src/server.ts', 'proxy.ts', 'src/proxy.ts',
110
+ ...['app', 'src/app'].flatMap(root => ['js', 'jsx', 'ts', 'tsx'].map(extension => `${root}/layout.${extension}`))];
111
+ for (const name of names) {
112
+ const content = await readProjectFile(root, name);
113
+ if (content !== null && /@universalcms\/connect\/(?:next-publication|vite-publication|(?:astro|vue|nuxt|angular|svelte|static)-runtime|vue-server)|universalcms-publication-client/.test(content)) fail('The original-route publication configuration is missing. Rerun setup or restore the reviewed configuration before saving a server key.');
114
+ }
115
+ const manifest = await readProjectFile(root, 'package.json');
116
+ if (manifest !== null && /\.universalcms\/(?:serve|(?:vue|nuxt|angular|svelte|static)\.serve)\.mjs/.test(manifest)) fail('The original-route publication configuration is missing. Rerun setup or restore the reviewed configuration before saving a server key.');
117
+ return null;
118
+ }
119
+ const manifest = JSON.parse(await readProjectFile(root, 'package.json'));
120
+ if (await readProjectFile(root, '.universalcms/website.integration.json') !== null) {
121
+ const { planWebsiteSettings } = await import('./website-setup.mjs');
122
+ return planWebsiteSettings(root);
123
+ }
124
+ const next = manifest.dependencies?.next ?? manifest.devDependencies?.next;
125
+ const files = [await ownedFile(root, PUBLICATION_CONFIG_FILE, json(config))];
126
+ if (nativeFramework(manifest)) files.push(...(await validateInstalledNative(root, nativeFramework(manifest), config)).files);
127
+ else if (manifest.dependencies?.astro || manifest.devDependencies?.astro) files.push(...(await validateInstalledAstro(root, config)).files);
128
+ else if (next) files.push(...(await planNextPublication(root, config)).files);
129
+ else {
130
+ files.push(await ownedFile(root, '.universalcms/serve.mjs', viteServerTemplate(), viteServerTemplate().replace("import contentBindings from './content.bindings.json' with { type: 'json' };\n", '').replace('configuration, sourceConfiguration, contentBindings', 'configuration, sourceConfiguration')));
131
+ const wrapper = await readProjectFile(root, '.universalcms/source.vite.config.mjs');
132
+ if (wrapper !== viteSourceWrapper(true)) fail('The Vite publication plugin was changed or is not installed. Rerun setup or review it before saving a key.');
133
+ const suffix = ' --config .universalcms/source.vite.config.mjs';
134
+ if (!manifest.scripts?.dev?.endsWith(suffix) || !manifest.scripts?.build?.endsWith(suffix) || manifest.scripts.start !== 'node .universalcms/serve.mjs') fail('The Vite publication startup/build scripts changed. Review them before saving a key.');
135
+ files.push({ path: '.universalcms/source.vite.config.mjs', content: wrapper, state: 'unchanged' }, { path: 'package.json', content: await readProjectFile(root, 'package.json'), state: 'unchanged' });
136
+ }
137
+ return { root, config, files };
138
+ }
@@ -0,0 +1,85 @@
1
+ /** Public, credential-free diagnostics for a privately installed publication adapter.
2
+ * A registry fingerprint proves which binding metadata the running adapter loaded;
3
+ * it is deliberately not a claim that a browser rendered those bindings.
4
+ */
5
+ import { createHash } from 'node:crypto';
6
+ import { validateContentRegistry } from './content-registry-validation.mjs';
7
+ import { validateSourceConfiguration } from './source-config.mjs';
8
+ import { normalizePublicationPath, publicationRevision } from './publication.mjs';
9
+
10
+ export const VERIFICATION_HEADERS = ['X-UniversalCMS-Site', 'X-UniversalCMS-Environment', 'X-UniversalCMS-Route', 'X-UniversalCMS-State', 'X-UniversalCMS-Revision', 'CDN-Cache-Control', 'Vercel-CDN-Cache-Control'];
11
+ const names = ['path', 'kind', 'slug', 'locale'];
12
+ const sameRoute = (a, b) => names.every(name => a[name] === b[name]);
13
+
14
+ export function publicationVerification(configuration, sourceConfiguration, contentBindings) {
15
+ if (!sourceConfiguration) return null;
16
+ const source = validateSourceConfiguration(sourceConfiguration);
17
+ if (['siteId', 'environmentId', 'websiteOrigin'].some(name => configuration[name] !== source[name])) throw Error('Publication diagnostics require matching source configuration.');
18
+ let registry = null;
19
+ try { registry = validateContentRegistry(contentBindings); } catch { /* Invalid bindings cannot report readiness. */ }
20
+ const unmappedRoutes = configuration.routes.filter(route => !registry?.routes.some(row => sameRoute(row, route))).length;
21
+ const complete = !!registry && !unmappedRoutes && registry.routes.length === configuration.routes.length;
22
+ return Object.freeze({
23
+ adminOrigin: source.adminOrigin,
24
+ // Validated profiles retain reviewed native paths and bounded single-segment
25
+ // patterns. Incomplete or overlapping registries grant no native ownership.
26
+ nativePaths: Object.freeze(complete ? [...(registry.nativePaths ?? [])] : []),
27
+ nativePatterns: Object.freeze(complete ? [...(registry.nativePatterns ?? [])] : []),
28
+ body: Object.freeze({ version: 1, siteId: configuration.siteId, environmentId: configuration.environmentId,
29
+ publication: true, contentBindings: complete, runtime: 'server',
30
+ sourceExport: source.exportClosed === true ? 'closed' : 'open',
31
+ bindingFingerprint: registry ? createHash('sha256').update(JSON.stringify(registry)).digest('hex') : null,
32
+ coverage: { complete, unmappedRoutes }, routes: configuration.routes.map(route => ({ ...route })),
33
+ }),
34
+ });
35
+ }
36
+
37
+ export function verificationCors(verification, origin) {
38
+ return verification && origin === verification.adminOrigin ? {
39
+ 'Access-Control-Allow-Origin': verification.adminOrigin, Vary: 'Origin',
40
+ 'Access-Control-Expose-Headers': VERIFICATION_HEADERS.join(', '),
41
+ } : {};
42
+ }
43
+
44
+ /** Resolve only a fixed diagnostic with exact scope. No visitor-supplied origin,
45
+ * credential, content or executable config is ever accepted by this endpoint.
46
+ */
47
+ export function verificationRequest({ configuration, verification, url, method, headers }) {
48
+ if (url.searchParams.get('check') !== 'connection') return null;
49
+ if (!verification) return { status: 404 };
50
+ const origin = headers.get('origin');
51
+ const host = headers.get('host');
52
+ const website = new URL(configuration.websiteOrigin);
53
+ const internalLoopback = host === website.host && ['127.0.0.1', '[::1]'].includes(website.hostname) && url.hostname === 'localhost' && url.protocol === website.protocol && url.port === website.port;
54
+ if (url.origin !== configuration.websiteOrigin && !internalLoopback || host && host !== website.host ||
55
+ origin !== verification.adminOrigin || headers.has('authorization') || headers.has('cookie') ||
56
+ url.searchParams.get('siteId') !== configuration.siteId || url.searchParams.get('environmentId') !== configuration.environmentId) return { status: 403 };
57
+ if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) return { status: 405 };
58
+ const allowed = ['check', 'siteId', 'environmentId', 'path'];
59
+ if ([...url.searchParams.keys()].some(name => !allowed.includes(name) || url.searchParams.getAll(name).length !== 1)) return { status: 400 };
60
+ if (method === 'OPTIONS') {
61
+ if (!['GET', 'HEAD'].includes(headers.get('access-control-request-method')) || headers.has('access-control-request-headers')) return { status: 403 };
62
+ return { status: 204, headers: { ...verificationCors(verification, origin), 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS' } };
63
+ }
64
+ let pathname;
65
+ try { pathname = url.searchParams.has('path') ? normalizePublicationPath(url.searchParams.get('path')) : configuration.routes[0].path; } catch { return { status: 400 }; }
66
+ if (!configuration.routes.some(route => route.path === pathname)) return { status: 400 };
67
+ return { pathname, headers: verificationCors(verification, origin) };
68
+ }
69
+
70
+ export function verificationBody(verification, pathname, result) {
71
+ return { ...verification.body, check: { path: pathname,
72
+ state: result.state === 'published' ? 'published' : result.state === 'missing' ? 'unpublished' : 'unavailable',
73
+ ...(result.state === 'published' && result.entry ? { revision: publicationRevision(result.entry) } : {}),
74
+ } };
75
+ }
76
+
77
+ export function publicationRouteHeaders(configuration, pathname, result, verification, origin, credentialed = false) {
78
+ return {
79
+ 'X-UniversalCMS-Site': configuration.siteId, 'X-UniversalCMS-Environment': configuration.environmentId,
80
+ 'X-UniversalCMS-Route': encodeURIComponent(pathname),
81
+ 'X-UniversalCMS-State': result.state === 'published' ? 'published' : result.state === 'missing' ? 'unpublished' : 'unavailable',
82
+ ...(result.state === 'published' && result.entry ? { 'X-UniversalCMS-Revision': publicationRevision(result.entry) } : {}),
83
+ ...(!credentialed ? verificationCors(verification, origin) : {}),
84
+ };
85
+ }
@@ -0,0 +1,6 @@
1
+ .cms-document{overflow-wrap:anywhere}.cms-document img{max-width:100%;height:auto}.cms-document pre,.cms-table-scroll{overflow-x:auto}.cms-document figure{margin:0}.cms-document blockquote{border-inline-start:3px solid currentColor;padding-inline-start:1rem}.cms-document table{border-collapse:collapse;width:100%}.cms-document th,.cms-document td{border:1px solid #8886;padding:.6rem;text-align:start}.cms-gallery,.cms-columns{display:grid;gap:1rem}.cms-grid-2,.cms-layout-1-1{grid-template-columns:repeat(2,minmax(0,1fr))}.cms-grid-3,.cms-layout-1-1-1{grid-template-columns:repeat(3,minmax(0,1fr))}.cms-grid-4,.cms-layout-1-1-1-1{grid-template-columns:repeat(4,minmax(0,1fr))}.cms-layout-1-2{grid-template-columns:minmax(0,1fr) minmax(0,2fr)}.cms-layout-2-1{grid-template-columns:minmax(0,2fr) minmax(0,1fr)}.cms-gap-small{gap:.5rem}.cms-gap-large{gap:2rem}.cms-embed{border:0;width:100%;height:auto}.cms-button{display:inline-block;padding:.65rem 1rem;border:1px solid currentColor;border-radius:.4rem}.cms-button-link{border:0;padding:0}.cms-form fieldset{border:0;padding:0;display:grid;gap:1rem}.cms-form-field{display:grid;gap:.35rem}.cms-form input,.cms-form textarea,.cms-form select{font:inherit;max-width:100%;padding:.6rem}.cms-form input[type=checkbox]{margin-inline-end:.5rem}.cms-selected{outline:2px solid #2563eb;outline-offset:3px}.cms-align-left{text-align:left}.cms-align-center{text-align:center}.cms-align-right{text-align:right}.cms-size-small{font-size:.875em}.cms-size-large{font-size:1.25em}.cms-tone-muted{opacity:.75}.cms-tone-accent{color:var(--cms-accent,#2563eb)}.cms-tone-inverse,.cms-section-dark{background:#20242c;color:#fff}.cms-section-muted{background:#8881}.cms-section-accent{background:var(--cms-accent,#2563eb);color:#fff}.cms-padding-small{padding:.75rem}.cms-padding-medium{padding:1.5rem}.cms-padding-large{padding:3rem}.cms-width-normal{max-width:70rem;margin-inline:auto}.cms-width-wide{max-width:90rem;margin-inline:auto}.cms-spaceAbove-small{margin-top:.5rem}.cms-spaceAbove-medium{margin-top:1rem}.cms-spaceAbove-large{margin-top:2rem}.cms-spaceBelow-small{margin-bottom:.5rem}.cms-spaceBelow-medium{margin-bottom:1rem}.cms-spaceBelow-large{margin-bottom:2rem}.cms-spaceAbove-none{margin-top:0}.cms-spaceBelow-none{margin-bottom:0}@media(min-width:641px){.cms-hideOnDesktop-true{display:none}}@media(max-width:640px){.cms-hideOnMobile-true{display:none}.cms-stack,.cms-gallery{grid-template-columns:minmax(0,1fr)}.cms-padding-large{padding:1.5rem}}
2
+
3
+ .cms-document .cms-block,.cms-document .cms-column{min-width:0}.cms-block-paragraph>p,.cms-block-heading>h2,.cms-block-heading>h3,.cms-block-heading>h4,.cms-block-quote>blockquote,.cms-document figcaption,.cms-block-faq p{white-space:pre-line}.cms-image-align-left{text-align:left}.cms-image-align-center{text-align:center}.cms-image-align-right{text-align:right}.cms-image-align-full img{width:100%}.cms-document .cms-image-align-left img,.cms-document .cms-image-align-center img,.cms-document .cms-image-align-right img{display:inline-block}.cms-gallery img{width:100%;height:auto}.cms-table-scroll:focus-visible{outline:2px solid var(--cms-accent,#2563eb);outline-offset:3px}
4
+
5
+ @media(max-width:767px){.cms-hide-mobile{display:none!important}}@media(min-width:768px){.cms-hide-desktop{display:none!important}}
6
+ .cms-additional-content{max-width:80rem;margin:2rem auto;padding:1rem}.cms-inline-bound{display:contents}