@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.
- package/README.md +699 -37
- package/dist/angular-runtime.d.mts +4 -0
- package/dist/angular-runtime.mjs +7 -0
- package/dist/angular-setup.mjs +124 -0
- package/dist/astro-runtime.d.mts +6 -0
- package/dist/astro-runtime.mjs +255 -0
- package/dist/astro-setup.mjs +215 -0
- package/dist/astro-source.mjs +451 -0
- package/dist/astro-website.d.mts +5 -0
- package/dist/astro-website.mjs +42 -0
- package/dist/cli.mjs +418 -15
- package/dist/connector-runtime.mjs +130 -0
- package/dist/content-next-setup.mjs +207 -0
- package/dist/content-registry-validation.mjs +40 -0
- package/dist/content-registry.mjs +44 -0
- package/dist/content-setup.mjs +13 -0
- package/dist/content-storefront-setup.mjs +155 -0
- package/dist/content-vite-setup.mjs +107 -0
- package/dist/core.mjs +32 -51
- package/dist/framework-ownership.mjs +68 -0
- package/dist/framework.mjs +54 -0
- package/dist/guided.mjs +514 -0
- package/dist/native-adapters.mjs +31 -0
- package/dist/native-chrome-model.d.mts +3 -0
- package/dist/native-chrome-model.mjs +14 -0
- package/dist/native-chrome-review.mjs +49 -0
- package/dist/native-chrome-source.mjs +69 -0
- package/dist/native-route-review.mjs +152 -0
- package/dist/native-routes.mjs +27 -0
- package/dist/native-website-model.d.mts +18 -0
- package/dist/native-website-model.mjs +67 -0
- package/dist/native-website-server.d.mts +1 -0
- package/dist/native-website-server.mjs +35222 -0
- package/dist/native-website-server.mjs.LEGAL.txt +56 -0
- package/dist/native-website.d.mts +3 -0
- package/dist/native-website.mjs +120 -0
- package/dist/native-website.mjs.LEGAL.txt +56 -0
- package/dist/next-publication.d.mts +12 -0
- package/dist/next-publication.mjs +107 -0
- package/dist/next-settings-layout.mjs +233 -0
- package/dist/next-source.d.mts +8 -0
- package/dist/next-source.mjs +93 -0
- package/dist/page-discovery.mjs +236 -0
- package/dist/page-mapping-review.mjs +160 -0
- package/dist/preflight-recovery.mjs +130 -0
- package/dist/preflight.mjs +138 -0
- package/dist/project-files.mjs +56 -0
- package/dist/public-bindings.d.mts +3 -0
- package/dist/public-bindings.mjs +28 -0
- package/dist/publication-browser.d.mts +5 -0
- package/dist/publication-browser.mjs +258 -0
- package/dist/publication-check.mjs +141 -0
- package/dist/publication-document.d.mts +2 -0
- package/dist/publication-document.mjs +59 -0
- package/dist/publication-next-setup.mjs +134 -0
- package/dist/publication-setup.mjs +138 -0
- package/dist/publication-verification.mjs +85 -0
- package/dist/publication.css +6 -0
- package/dist/publication.d.mts +31 -0
- package/dist/publication.mjs +320 -0
- package/dist/react-publication.d.mts +22 -0
- package/dist/react-publication.mjs +301 -0
- package/dist/react-website.d.mts +15 -0
- package/dist/react-website.mjs +735 -0
- package/dist/reviewed-commerce-ownership.mjs +26 -0
- package/dist/reviewed-commerce-settings-setup.mjs +243 -0
- package/dist/reviewed-native-ownership.mjs +118 -0
- package/dist/reviewed-storefront-source.mjs +227 -0
- package/dist/runtime-dependencies.mjs +59 -0
- package/dist/settings-only-setup.mjs +100 -0
- package/dist/settings-only.d.mts +4 -0
- package/dist/settings-only.mjs +49 -0
- package/dist/setup-environment.mjs +70 -0
- package/dist/source-bindings.mjs +77 -0
- package/dist/source-check.mjs +149 -0
- package/dist/source-collect.mjs +80 -0
- package/dist/source-config.mjs +20 -0
- package/dist/source-extract.mjs +514 -0
- package/dist/source-finalize.mjs +52 -0
- package/dist/source-routes.mjs +65 -0
- package/dist/source-setup.mjs +490 -0
- package/dist/staged-setup.mjs +287 -0
- package/dist/static-runtime.mjs +67 -0
- package/dist/static-setup.mjs +136 -0
- package/dist/svelte-runtime.d.mts +27 -0
- package/dist/svelte-runtime.mjs +40 -0
- package/dist/svelte-setup.mjs +135 -0
- package/dist/svelte-template.mjs +48 -0
- package/dist/templates.mjs +167 -3
- package/dist/vendor/astro-compiler/LICENSE +53 -0
- package/dist/vendor/astro-compiler/astro.wasm +0 -0
- package/dist/vendor/astro-compiler/chunk-W5DTLHV4.js +1 -0
- package/dist/vendor/astro-compiler/node/index.js +1 -0
- package/dist/vendor/project-analysis.mjs +58 -4
- package/dist/vendor/react-LICENSE.txt +21 -0
- package/dist/vendor/react-document.mjs +124 -0
- package/dist/vendor/react-dom-LICENSE.txt +21 -0
- package/dist/vendor/react-form.mjs +63 -0
- package/dist/vendor/react-safety.mjs +47 -0
- package/dist/vendor/source-manifest-provider.mjs +104 -0
- package/dist/vendor/typescript-LICENSE.txt +55 -0
- package/dist/vendor/typescript.cjs +200276 -0
- package/dist/vite-publication-server.d.mts +3 -0
- package/dist/vite-publication-server.mjs +82 -0
- package/dist/vite-publication.d.mts +15 -0
- package/dist/vite-publication.mjs +241 -0
- package/dist/vite-source-discovery.mjs +315 -0
- package/dist/vite-source.d.mts +30 -0
- package/dist/vite-source.mjs +141 -0
- package/dist/vue-runtime.d.mts +10 -0
- package/dist/vue-runtime.mjs +50 -0
- package/dist/vue-server.d.mts +2 -0
- package/dist/vue-server.mjs +58 -0
- package/dist/vue-setup.mjs +127 -0
- package/dist/vue-source.mjs +168 -0
- package/dist/website-check.mjs +41 -0
- package/dist/website-http.mjs +149 -0
- package/dist/website-model.d.mts +20 -0
- package/dist/website-model.mjs +155 -0
- package/dist/website-next.d.mts +2 -0
- package/dist/website-next.mjs +29 -0
- package/dist/website-node.d.mts +3 -0
- package/dist/website-node.mjs +50 -0
- package/dist/website-runtime.d.mts +13 -0
- package/dist/website-runtime.mjs +404 -0
- package/dist/website-schema.mjs +22175 -0
- package/dist/website-setup.mjs +256 -0
- package/dist/website.css +49 -0
- package/dist/wizard.mjs +237 -38
- package/package.json +44 -7
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** Read-only website source diagnostic. Never sends CMS credentials or changes content. */
|
|
2
|
+
import { readSetupEnvironment } from './setup-environment.mjs';
|
|
3
|
+
import { readSourceSettings } from './source-setup.mjs';
|
|
4
|
+
import { createSourceManifestProvider } from './vendor/source-manifest-provider.mjs';
|
|
5
|
+
import { inspectSourceRoutes, compareSourceRoutes } from './source-routes.mjs';
|
|
6
|
+
|
|
7
|
+
export const SOURCE_CHECK_TIMEOUT_MS = 10_000;
|
|
8
|
+
export const SOURCE_CHECK_MAX_BYTES = 20 * 1024 * 1024;
|
|
9
|
+
export const SOURCE_CHECK_MESSAGES = Object.freeze({
|
|
10
|
+
checked: 'The source endpoint returned matching scope, exact-origin CORS and a valid manifest envelope, identity and taxonomy structure. Full block-schema validation, complete source inventory and access from the CMS browser still need verification. No content was imported.',
|
|
11
|
+
'configuration-error': 'Source was not checked. Supply --website-origin, --admin-origin, --site and --environment from Developer setup, or save matching public values locally. Use exact HTTPS origins or HTTP loopback origins. No delivery key is needed.',
|
|
12
|
+
'saved-settings-error': 'Saved connection values could not be read safely. Supply all four public flags explicitly to check the source without loading local settings.',
|
|
13
|
+
'missing-route': 'The website returned 404 for /api/cms-source. Installing the connector or saving a key does not create this endpoint. If full setup stopped on a review blocker, finish the reviewed source, publication, content and settings adapter first. For an import-only workflow, setup-source prepares a supported public export; it does not render published CMS edits or install website settings. After the chosen adapter is installed, stop the old server and restart or deploy the website, then run check-source with the exact public scope. Unsupported layouts require a reviewed source adapter.',
|
|
14
|
+
'request-rejected': 'The source endpoint rejected the request. Match its configured website origin, CMS admin origin, site and environment exactly, then restart or deploy. localhost and 127.0.0.1 are different origins. Do not add a delivery key or relax CORS.',
|
|
15
|
+
'http-error': 'The source endpoint returned an unsuccessful HTTP response. Check the website server and source adapter, then restart or deploy it.',
|
|
16
|
+
'cors-error': 'The source response must allow exactly the configured CMS admin origin and must not enable credentials. Verify the source adapter CORS configuration; a command-line response alone does not prove browser access.',
|
|
17
|
+
'not-json': 'The source endpoint did not return application/json. A website page or SPA fallback is not a source export. Mount /api/cms-source before the website fallback and restart or deploy.',
|
|
18
|
+
'scope-error': 'The source response does not match the selected site and environment. Configure the source adapter with the exact IDs from Developer setup and restart or deploy it.',
|
|
19
|
+
'manifest-error': 'The source manifest envelope, document shape, identity or taxonomy is invalid. Validate every exported entry against the v1 source contract. Keep complete content; do not replace it with samples or silently rename terms.',
|
|
20
|
+
'empty-source': 'The source endpoint returned no entries. This does not verify migration of an existing website. Prepare its complete public page and post inventory before using Connect & fetch content.',
|
|
21
|
+
'missing-pages': 'Some static page routes have no matching source export. For automatic Next.js discovery, review sync-source diagnostics and rebuild deployed websites. Protected routes, unsupported content and custom exporters need explicit public mappings. Existing CMS entries are preserved when reconnecting.',
|
|
22
|
+
'routes-unverified': 'The source route inventory could not be fully verified. Use an ordinary Next.js App Router project with default route filenames and paths. Dynamic, custom or ambiguous routes require an explicit public-content review; no pages were automatically exported or imported.',
|
|
23
|
+
'invalid-response': 'The source response was redirected, malformed, unsafe or exceeded the response budget. Serve the exact source endpoint directly with bounded valid JSON.',
|
|
24
|
+
'transport-error': 'The source endpoint could not be reached. Start the website at its saved origin and check the port and source route. A browser may also require local-network permission after this CLI check passes.',
|
|
25
|
+
timeout: 'The source endpoint did not finish within the verification deadline. Check that the website server and source adapter are running and responsive.',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
29
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
30
|
+
const invalid = Symbol('Invalid source response');
|
|
31
|
+
const result = (status, counts) => ({ status, checked: status === 'checked', browserVerified: false, fullDocumentValidation: false, ...(counts ? { counts } : {}), message: SOURCE_CHECK_MESSAGES[status] });
|
|
32
|
+
const positiveBound = (value, max) => Number.isSafeInteger(value) && value > 0 && value <= max;
|
|
33
|
+
function exactOrigin(value) {
|
|
34
|
+
if (typeof value !== 'string' || value.length > 2048 || /[\s\\]|cms_server\.|sk-[A-Za-z0-9_-]{12,}|AIza[A-Za-z0-9_-]{20,}/i.test(value)) return null;
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL(value);
|
|
37
|
+
return !url.username && !url.password && !url.search && !url.hash && url.pathname === '/' &&
|
|
38
|
+
(url.protocol === 'https:' || (url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) ? url.origin : null;
|
|
39
|
+
} catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
function untilAborted(pending, signal) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const cancel = () => reject(invalid);
|
|
44
|
+
if (signal.aborted) { cancel(); return; }
|
|
45
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
46
|
+
pending.then(value => { signal.removeEventListener('abort', cancel); resolve(value); }, error => { signal.removeEventListener('abort', cancel); reject(error); });
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function cancelBody(response) {
|
|
50
|
+
try { if (response?.body && !response.body.locked) void response.body.cancel().catch(() => {}); }
|
|
51
|
+
catch { /* Cleanup never exposes an upstream error. */ }
|
|
52
|
+
}
|
|
53
|
+
async function boundedBody(response, maxBytes, signal, deadline) {
|
|
54
|
+
const length = response.headers.get('content-length');
|
|
55
|
+
if (length !== null && (!/^\d+$/.test(length) || Number(length) > maxBytes)) throw invalid;
|
|
56
|
+
if (!response.body) throw invalid;
|
|
57
|
+
const reader = response.body.getReader(), body = new Uint8Array(maxBytes);
|
|
58
|
+
let bytes = 0;
|
|
59
|
+
try {
|
|
60
|
+
while (true) {
|
|
61
|
+
const chunk = await untilAborted(reader.read(), signal);
|
|
62
|
+
deadline();
|
|
63
|
+
if (chunk.done) break;
|
|
64
|
+
bytes += chunk.value.byteLength;
|
|
65
|
+
if (bytes > maxBytes) throw invalid;
|
|
66
|
+
body.set(chunk.value, bytes - chunk.value.byteLength);
|
|
67
|
+
}
|
|
68
|
+
try { return new TextDecoder('utf-8', { fatal: true }).decode(body.subarray(0, bytes)); }
|
|
69
|
+
catch { throw invalid; }
|
|
70
|
+
} finally { void reader.cancel().catch(() => {}); reader.releaseLock(); }
|
|
71
|
+
}
|
|
72
|
+
function sourceManifest(raw, binding, inventory) {
|
|
73
|
+
let manifest;
|
|
74
|
+
try { manifest = JSON.parse(raw); } catch { return result('manifest-error'); }
|
|
75
|
+
if (!object(manifest) || Object.keys(manifest).length !== 4 || Object.keys(manifest).some(name => !['version', 'siteId', 'environmentId', 'entries'].includes(name))) return result('manifest-error');
|
|
76
|
+
if (manifest.siteId !== binding.siteId || manifest.environmentId !== binding.environmentId) return result('scope-error');
|
|
77
|
+
// Bound nesting and reserved keys before passing the entire inventory through
|
|
78
|
+
// the same envelope/identity/taxonomy validator used by the public provider.
|
|
79
|
+
// This deliberately does not claim full authoring block-schema validation.
|
|
80
|
+
let nodes = 0;
|
|
81
|
+
const walk = (value, depth = 0) => {
|
|
82
|
+
if (++nodes > 300_000 || depth > 20) throw invalid;
|
|
83
|
+
if (value && typeof value === 'object') for (const [name, child] of Object.entries(value)) {
|
|
84
|
+
if (['__proto__', 'prototype', 'constructor'].includes(name)) throw invalid;
|
|
85
|
+
walk(child, depth + 1);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
walk(manifest);
|
|
90
|
+
createSourceManifestProvider({ manifestJson: JSON.stringify({ version: manifest.version, entries: manifest.entries }), sourceOrigin: binding.websiteOrigin, adminOrigin: binding.adminOrigin, siteId: binding.siteId, environmentId: binding.environmentId, publicContent: true });
|
|
91
|
+
} catch { return result('manifest-error'); }
|
|
92
|
+
const counts = { entries: manifest.entries.length, pages: manifest.entries.filter(entry => entry.kind === 'page').length, posts: manifest.entries.filter(entry => entry.kind === 'post').length };
|
|
93
|
+
if (counts.entries && inventory) {
|
|
94
|
+
const routeInventory = compareSourceRoutes(inventory, manifest.entries);
|
|
95
|
+
const status = routeInventory.missingPages ? 'missing-pages' : routeInventory.unresolvedRoutes ? 'routes-unverified' : 'checked';
|
|
96
|
+
return { ...result(status, counts), routeInventory };
|
|
97
|
+
}
|
|
98
|
+
return result(counts.entries ? 'checked' : 'empty-source', counts);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Explicit public options bypass local configuration, including unsupported scaffold frameworks. */
|
|
102
|
+
export async function checkSource(directory, options = {}, { fetcher = globalThis.fetch, timeoutMs = SOURCE_CHECK_TIMEOUT_MS, maxResponseBytes = SOURCE_CHECK_MAX_BYTES } = {}) {
|
|
103
|
+
const names = ['websiteOrigin', 'adminOrigin', 'siteId', 'environmentId'];
|
|
104
|
+
if (!object(options) || Object.keys(options).some(name => ![...names, 'checkRoutes'].includes(name)) || (options.checkRoutes !== undefined && typeof options.checkRoutes !== 'boolean') || typeof fetcher !== 'function' || !positiveBound(timeoutMs, SOURCE_CHECK_TIMEOUT_MS) || !positiveBound(maxResponseBytes, SOURCE_CHECK_MAX_BYTES)) return result('configuration-error');
|
|
105
|
+
let defaults = {};
|
|
106
|
+
if (names.some(name => options[name] === undefined)) {
|
|
107
|
+
try {
|
|
108
|
+
defaults = await readSourceSettings(directory);
|
|
109
|
+
// Source-only setup never needs private settings. Keep the legacy fallback
|
|
110
|
+
// only when a required public value is still missing.
|
|
111
|
+
if (names.some(name => options[name] === undefined && defaults[name] === undefined)) defaults = { ...(await readSetupEnvironment(directory)).defaults, ...defaults };
|
|
112
|
+
}
|
|
113
|
+
catch { return result('saved-settings-error'); }
|
|
114
|
+
}
|
|
115
|
+
const values = Object.fromEntries(names.map(name => [name, options[name] ?? defaults[name]]));
|
|
116
|
+
const websiteOrigin = exactOrigin(values.websiteOrigin), adminOrigin = exactOrigin(values.adminOrigin);
|
|
117
|
+
if (!websiteOrigin || !adminOrigin || !['siteId', 'environmentId'].every(name => typeof values[name] === 'string' && uuid.test(values[name]))) return result('configuration-error');
|
|
118
|
+
let inventory;
|
|
119
|
+
if (options.checkRoutes) {
|
|
120
|
+
try { inventory = await inspectSourceRoutes(directory); }
|
|
121
|
+
catch { return result('routes-unverified'); }
|
|
122
|
+
}
|
|
123
|
+
const binding = { websiteOrigin, adminOrigin, siteId: values.siteId.toLowerCase(), environmentId: values.environmentId.toLowerCase() };
|
|
124
|
+
const url = new URL('/api/cms-source', websiteOrigin);
|
|
125
|
+
url.search = new URLSearchParams({ siteId: binding.siteId, environmentId: binding.environmentId }).toString();
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
let timedOut = false, response;
|
|
128
|
+
const expires = Date.now() + timeoutMs;
|
|
129
|
+
const deadline = () => { if (Date.now() >= expires) { timedOut = true; controller.abort(); throw invalid; } };
|
|
130
|
+
const timer = setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs);
|
|
131
|
+
try {
|
|
132
|
+
response = await untilAborted(fetcher(url, {
|
|
133
|
+
method: 'GET', headers: { Accept: 'application/json', Origin: adminOrigin }, credentials: 'omit',
|
|
134
|
+
redirect: 'error', cache: 'no-store', referrerPolicy: 'no-referrer', signal: controller.signal,
|
|
135
|
+
}), controller.signal);
|
|
136
|
+
deadline();
|
|
137
|
+
if (!(response instanceof Response) || response.redirected || ['opaque', 'opaqueredirect'].includes(response.type) || (response.url && response.url !== url.href) || (response.status >= 300 && response.status < 400)) return result('invalid-response');
|
|
138
|
+
if (response.status === 404) return result('missing-route');
|
|
139
|
+
if ([401, 403].includes(response.status)) return result('request-rejected');
|
|
140
|
+
if (!response.ok) return result('http-error');
|
|
141
|
+
if (response.status !== 200) return result('invalid-response');
|
|
142
|
+
if (response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() !== 'application/json') return result('not-json');
|
|
143
|
+
if (response.headers.get('access-control-allow-origin') !== adminOrigin || response.headers.has('access-control-allow-credentials')) return result('cors-error');
|
|
144
|
+
const outcome = sourceManifest(await boundedBody(response, maxResponseBytes, controller.signal, deadline), binding, inventory);
|
|
145
|
+
deadline();
|
|
146
|
+
return outcome;
|
|
147
|
+
} catch (error) { return result(timedOut ? 'timeout' : error === invalid ? 'invalid-response' : 'transport-error'); }
|
|
148
|
+
finally { clearTimeout(timer); controller.abort(); cancelBody(response); }
|
|
149
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { projectRoot, readProjectFile } from './project-files.mjs';
|
|
2
|
+
import { inspectSourceRoutes } from './source-routes.mjs';
|
|
3
|
+
import { extractStaticPage } from './source-extract.mjs';
|
|
4
|
+
import { nextPublicationProxyTemplate } from './publication-next-setup.mjs';
|
|
5
|
+
import { readContentRegistry, readFrozenContent } from './content-registry.mjs';
|
|
6
|
+
import { inspectReviewedNativeOwnership } from './reviewed-native-ownership.mjs';
|
|
7
|
+
import { REVIEWED_NEXT_NATIVE_PROFILE } from './native-routes.mjs';
|
|
8
|
+
import { inspectReviewedCommerceOwnership } from './reviewed-commerce-ownership.mjs';
|
|
9
|
+
|
|
10
|
+
const protectedSegment = /^(?:admin|account|dashboard|profile|auth|login|register|checkout|orders|settings|billing|portal|patient|patients|vault|records)(?:-|$)|(?:^|-)vault$|^(?:medical|health)-records(?:-|$)/;
|
|
11
|
+
const accessLogic = /\b(?:cookies|headers|useSession|getServerSession|auth|requireAuth|currentUser|redirect)\s*\(/;
|
|
12
|
+
|
|
13
|
+
/** Parse public static source only. No page execution, HTML fetching or CMS writes. */
|
|
14
|
+
export async function collectSource({ root: directory, locale = 'en', skipSlugs = [], excludedRoutes = [] }) {
|
|
15
|
+
const root = await projectRoot(directory), inventory = await inspectSourceRoutes(root);
|
|
16
|
+
if (inventory.routes.includes('') && inventory.routes.includes('home')) throw Error('Ambiguous home identity');
|
|
17
|
+
const entries = [], warnings = [];
|
|
18
|
+
const content = await readContentRegistry(root);
|
|
19
|
+
let websiteSettings = null;
|
|
20
|
+
if (await readProjectFile(root, '.universalcms/website.integration.json') !== null) {
|
|
21
|
+
// Website runtimes (for example a bundled Next source route) must not bundle
|
|
22
|
+
// the setup toolchain and its framework compilers. Node resolves it on demand.
|
|
23
|
+
// A non-literal specifier keeps bundlers from copying the module as an asset.
|
|
24
|
+
const setup = new URL(['.', 'website-setup.mjs'].join('/'), import.meta.url).href;
|
|
25
|
+
const { planWebsiteSettings } = await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ /* @vite-ignore */ setup);
|
|
26
|
+
websiteSettings = await planWebsiteSettings(root);
|
|
27
|
+
}
|
|
28
|
+
const nativeOwnership = await inspectReviewedNativeOwnership(root) ?? await inspectReviewedCommerceOwnership(root);
|
|
29
|
+
// The reviewed product detail module accounts for exactly one dynamic route.
|
|
30
|
+
// Any other unresolved route remains in the diagnostic count for review.
|
|
31
|
+
const reviewedDynamic = name => nativeOwnership?.dynamicDirectories?.includes(name) || nativeOwnership?.profile === 'reviewed-icecream-commerce-2026-10-15' && name === 'src/app/menu/[id]' || websiteSettings?.files.some(file => ['js','jsx','ts','tsx'].some(ext => file.path === `${name}/page.${ext}`));
|
|
32
|
+
let skippedPages = 0, unsupportedRoutes = inventory.unresolvedPaths.filter(name => !reviewedDynamic(name)).length, bytes = 0;
|
|
33
|
+
const hasMiddleware = (await Promise.all(['middleware', 'src/middleware', 'proxy', 'src/proxy'].flatMap(name => ['js', 'ts'].map(async extension => {
|
|
34
|
+
const content = await readProjectFile(root, `${name}.${extension}`);
|
|
35
|
+
if (!content) return false;
|
|
36
|
+
if (websiteSettings?.files.some(file => file.path === `${name}.${extension}` && file.content === content)) return false;
|
|
37
|
+
// Our exact generated proxy only filters publications; it adds no private
|
|
38
|
+
// application content. Every other middleware still requires access review.
|
|
39
|
+
return !(extension === 'ts' && (name === 'proxy' && [nextPublicationProxyTemplate('app'), nextPublicationProxyTemplate('app', true)].includes(content) || name === 'src/proxy' && [nextPublicationProxyTemplate('src/app'), nextPublicationProxyTemplate('src/app', true)].includes(content)));
|
|
40
|
+
})))).some(Boolean);
|
|
41
|
+
const excludes = pathname => excludedRoutes.some(route => route === '/' || pathname === route || pathname.startsWith(`${route}/`));
|
|
42
|
+
const started = Date.now();
|
|
43
|
+
for (const route of [...inventory.routes].sort()) {
|
|
44
|
+
if (Date.now() - started > 8000) throw Error('Source inspection deadline');
|
|
45
|
+
const slug = route || 'home', filename = inventory.files[slug];
|
|
46
|
+
if (nativeOwnership?.nativePaths.includes(`/${route}`)) { skippedPages++; continue; }
|
|
47
|
+
if (skipSlugs.includes(slug) || excludes(`/${route}`)) { skippedPages++; continue; }
|
|
48
|
+
// Conservative guards, not a replacement for choosing a public-content root.
|
|
49
|
+
if (hasMiddleware || route.split('/').some(segment => protectedSegment.test(segment))) {
|
|
50
|
+
unsupportedRoutes++; warnings.push({ slug, codes: ['access-review-required'] }); continue;
|
|
51
|
+
}
|
|
52
|
+
const bound = content?.routes.find(row => row.sourceFile === filename && row.path === `/${route}` && row.kind === 'page' && row.slug === slug && row.locale === locale);
|
|
53
|
+
const source = await readProjectFile(root, filename);
|
|
54
|
+
if (source === null) throw Error('Source changed during inspection');
|
|
55
|
+
bytes += Buffer.byteLength(source);
|
|
56
|
+
if (bytes > 8 * 1024 * 1024) throw Error('Source inventory byte budget');
|
|
57
|
+
let guarded = accessLogic.test(source);
|
|
58
|
+
const parts = filename.split('/'); parts.pop();
|
|
59
|
+
while (parts.length) {
|
|
60
|
+
for (const ext of ['js', 'jsx', 'ts', 'tsx']) {
|
|
61
|
+
const layout = await readProjectFile(root, `${parts.join('/')}/layout.${ext}`);
|
|
62
|
+
if (layout && accessLogic.test(layout)) guarded = true;
|
|
63
|
+
}
|
|
64
|
+
if (parts.at(-1) === 'app') break;
|
|
65
|
+
parts.pop();
|
|
66
|
+
}
|
|
67
|
+
if (guarded) { unsupportedRoutes++; warnings.push({ slug, codes: ['access-review-required'] }); continue; }
|
|
68
|
+
// A frozen baseline does not freeze authorization. Ancestor layouts remain
|
|
69
|
+
// current application policy and must be checked before exporting its text.
|
|
70
|
+
if (bound) { entries.push((await readFrozenContent(root, bound)).entry); continue; }
|
|
71
|
+
const result = extractStaticPage({ source, slug, locale, filename });
|
|
72
|
+
if (Date.now() - started > 8000) throw Error('Source inspection deadline');
|
|
73
|
+
if (result.entry) entries.push(result.entry); else unsupportedRoutes++;
|
|
74
|
+
const codes = [...(result.warnings ?? []), ...(result.entry && content ? ['content-binding-required'] : [])];
|
|
75
|
+
if (codes.length) warnings.push({ slug, codes });
|
|
76
|
+
}
|
|
77
|
+
const snapshot = { version: 1, entries, diagnostics: { discoveredPages: inventory.routes.length, automaticPages: entries.length, skippedPages, unsupportedRoutes, warnings, ...(nativeOwnership && nativeOwnership.profile !== REVIEWED_NEXT_NATIVE_PROFILE ? { nativeOwnership: { profile:nativeOwnership.profile,paths:nativeOwnership.nativePaths } } : {}) } };
|
|
78
|
+
if (Buffer.byteLength(JSON.stringify(snapshot)) > 20 * 1024 * 1024) throw Error('Source response byte budget');
|
|
79
|
+
return snapshot;
|
|
80
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2
|
+
const slug = /^[a-z0-9]+(?:[-/][a-z0-9]+)*$/;
|
|
3
|
+
const fail = () => { throw Error('Invalid automatic source configuration'); };
|
|
4
|
+
function origin(value) {
|
|
5
|
+
if (typeof value !== 'string' || value.length > 2048 || /[\s\\]|cms_server\.|sk-[\w-]{12,}/i.test(value)) return fail();
|
|
6
|
+
const url = new URL(value);
|
|
7
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== '/' ||
|
|
8
|
+
!(url.protocol === 'https:' || url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) return fail();
|
|
9
|
+
return url.origin;
|
|
10
|
+
}
|
|
11
|
+
export function validateSourceConfiguration(value) {
|
|
12
|
+
const names = ['version', 'siteId', 'environmentId', 'websiteOrigin', 'adminOrigin', 'locale', 'skipSlugs', 'excludedRoutes', 'publicStaticPages', 'exportClosed'];
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !names.includes(key)) || (value.exportClosed !== undefined && value.exportClosed !== true) || value.version !== 1 || value.publicStaticPages !== true || typeof value.siteId !== 'string' || typeof value.environmentId !== 'string' || !uuid.test(value.siteId) || !uuid.test(value.environmentId)) return fail();
|
|
14
|
+
const locale = value.locale ?? 'en';
|
|
15
|
+
if (typeof locale !== 'string' || locale.length > 35 || Intl.getCanonicalLocales(locale).length !== 1) return fail();
|
|
16
|
+
const skipSlugs = value.skipSlugs ?? [], excludedRoutes = value.excludedRoutes ?? [];
|
|
17
|
+
if (!Array.isArray(skipSlugs) || skipSlugs.length > 1000 || skipSlugs.some(item => typeof item !== 'string' || item.length > 160 || !slug.test(item)) || new Set(skipSlugs).size !== skipSlugs.length) return fail();
|
|
18
|
+
if (!Array.isArray(excludedRoutes) || excludedRoutes.length > 1000 || excludedRoutes.some(item => typeof item !== 'string' || item.length > 161 || item !== '/' && (!item.startsWith('/') || !slug.test(item.slice(1)))) || new Set(excludedRoutes).size !== excludedRoutes.length) return fail();
|
|
19
|
+
return { version: 1, siteId: value.siteId.toLowerCase(), environmentId: value.environmentId.toLowerCase(), websiteOrigin: origin(value.websiteOrigin), adminOrigin: origin(value.adminOrigin), locale: Intl.getCanonicalLocales(locale)[0], skipSlugs: [...skipSlugs], excludedRoutes: [...excludedRoutes], publicStaticPages: true, ...(value.exportClosed ? { exportClosed: true } : {}) };
|
|
20
|
+
}
|