mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): add bounded local web console
This commit is contained in:
@@ -3323,6 +3323,9 @@ function auditPackageScripts(packagePath, manifest, findings) {
|
||||
|
||||
function auditPackageFiles(packagePath, manifest, findings) {
|
||||
const expected = ['dist/**/*.js', 'dist/**/*.d.ts'];
|
||||
if (packagePath === 'packages/ql3-local-api') {
|
||||
expected.unshift('assets/console/*');
|
||||
}
|
||||
if (packagePath === 'packages/ql3-cluster-admin') {
|
||||
expected.push('assets/copilot-console/*');
|
||||
}
|
||||
|
||||
@@ -352,6 +352,44 @@ function auditLocalApiExecutable(artifactDirectory) {
|
||||
) {
|
||||
fail('local API executable help output is invalid');
|
||||
}
|
||||
const consoleDirectory = path.join(packageDirectory, 'assets', 'console');
|
||||
const expectedAssets = ['console.css', 'console.js', 'index.html'];
|
||||
const actualAssets = fs.readdirSync(consoleDirectory).sort();
|
||||
if (JSON.stringify(actualAssets) !== JSON.stringify(expectedAssets)) {
|
||||
fail(
|
||||
`local Console asset closure is invalid: ${JSON.stringify(actualAssets)}`,
|
||||
);
|
||||
}
|
||||
let totalBytes = 0;
|
||||
const contents = {};
|
||||
for (const name of expectedAssets) {
|
||||
const assetPath = path.join(consoleDirectory, name);
|
||||
const stat = fs.lstatSync(assetPath);
|
||||
if (
|
||||
!stat.isFile() ||
|
||||
stat.isSymbolicLink() ||
|
||||
stat.size < 100 ||
|
||||
stat.size > 96 * 1024
|
||||
) {
|
||||
fail(`local Console asset is invalid: ${name}`);
|
||||
}
|
||||
totalBytes += stat.size;
|
||||
contents[name] = fs.readFileSync(assetPath, 'utf8');
|
||||
}
|
||||
if (
|
||||
totalBytes > 192 * 1024 ||
|
||||
!contents['index.html'].includes('href="/console.css"') ||
|
||||
!contents['index.html'].includes('src="/console.js"') ||
|
||||
/<(?:script|style)(?:\s|>)[^>]*>\s*[^<\s]/u.test(contents['index.html']) ||
|
||||
/\b(?:localStorage|sessionStorage|innerHTML|eval)\b/u.test(
|
||||
contents['console.js'],
|
||||
) ||
|
||||
/https?:\/\//u.test(contents['index.html']) ||
|
||||
/https?:\/\//u.test(contents['console.css']) ||
|
||||
/https?:\/\//u.test(contents['console.js'])
|
||||
) {
|
||||
fail('local Console offline or credential-custody contract is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
function packageRootFromEntry(entryPath, packageName) {
|
||||
@@ -672,6 +710,9 @@ function main() {
|
||||
entrySpecifiers,
|
||||
excludedInternalPackages:
|
||||
options.application && !options.ai ? ['@qinglong/ai'] : [],
|
||||
retainedJavaScriptFiles: options.api
|
||||
? ['local-api/assets/console/console.js']
|
||||
: [],
|
||||
},
|
||||
);
|
||||
const prunedMcpExternalDevelopment = options.mcp
|
||||
|
||||
@@ -89,7 +89,10 @@ function projectRuntimeExports(value, requiredKeys = null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.filter(([key]) => {
|
||||
if (key !== '.' && !/^\.\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(key)) {
|
||||
if (
|
||||
key !== '.' &&
|
||||
!/^\.\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(key)
|
||||
) {
|
||||
fail(`runtime package export key ${key} is invalid`);
|
||||
}
|
||||
return requiredKeys === null || requiredKeys.has(key);
|
||||
@@ -127,7 +130,9 @@ function normalizeEntrySpecifiers(options) {
|
||||
Array.isArray(options) ||
|
||||
Reflect.ownKeys(options).some(
|
||||
(key) =>
|
||||
key !== 'entrySpecifiers' && key !== 'excludedInternalPackages',
|
||||
key !== 'entrySpecifiers' &&
|
||||
key !== 'excludedInternalPackages' &&
|
||||
key !== 'retainedJavaScriptFiles',
|
||||
) ||
|
||||
!Array.isArray(options.entrySpecifiers) ||
|
||||
options.entrySpecifiers.length > 64
|
||||
@@ -147,6 +152,41 @@ function normalizeEntrySpecifiers(options) {
|
||||
return Object.freeze(entries);
|
||||
}
|
||||
|
||||
function normalizeRetainedJavaScriptFiles(options, inventory) {
|
||||
if (options === undefined || options.retainedJavaScriptFiles === undefined) {
|
||||
return Object.freeze([]);
|
||||
}
|
||||
if (
|
||||
!Array.isArray(options.retainedJavaScriptFiles) ||
|
||||
options.retainedJavaScriptFiles.length > 64
|
||||
) {
|
||||
fail('retained JavaScript file options are invalid');
|
||||
}
|
||||
const inventoryPaths = new Set(inventory.files.map(({ path: file }) => file));
|
||||
const retained = [];
|
||||
const seen = new Set();
|
||||
for (const value of options.retainedJavaScriptFiles) {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
Buffer.byteLength(value, 'utf8') > 256 ||
|
||||
!/^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[A-Za-z0-9._-]+)+\.js$/u.test(value) ||
|
||||
seen.has(value)
|
||||
) {
|
||||
fail('retained JavaScript file is invalid');
|
||||
}
|
||||
seen.add(value);
|
||||
const resolved = path.resolve(inventory.resolved, ...value.split('/'));
|
||||
if (
|
||||
!resolved.startsWith(`${inventory.resolved}${path.sep}`) ||
|
||||
!inventoryPaths.has(resolved)
|
||||
) {
|
||||
fail('retained JavaScript file is not installed');
|
||||
}
|
||||
retained.push(resolved);
|
||||
}
|
||||
return Object.freeze(retained);
|
||||
}
|
||||
|
||||
function normalizeExcludedInternalPackages(options) {
|
||||
if (options === undefined || options.excludedInternalPackages === undefined) {
|
||||
return Object.freeze([]);
|
||||
@@ -195,14 +235,18 @@ function collectStaticModuleSpecifiers(files) {
|
||||
for (const match of contents.matchAll(STATIC_MODULE_SPECIFIER_PATTERN)) {
|
||||
const specifier = match[2];
|
||||
const start = match.index + match[0].indexOf(specifier);
|
||||
literalRanges.push(Object.freeze({ start, end: start + specifier.length }));
|
||||
literalRanges.push(
|
||||
Object.freeze({ start, end: start + specifier.length }),
|
||||
);
|
||||
literalCalls.add(match.index);
|
||||
specifiers.push(specifier);
|
||||
}
|
||||
MODULE_LOAD_START_PATTERN.lastIndex = 0;
|
||||
for (const match of contents.matchAll(MODULE_LOAD_START_PATTERN)) {
|
||||
if (!literalCalls.has(match.index)) {
|
||||
fail(`runtime JavaScript contains a non-literal module load at ${file.path}`);
|
||||
fail(
|
||||
`runtime JavaScript contains a non-literal module load at ${file.path}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
let occurrence = contents.indexOf('@qinglong/');
|
||||
@@ -212,7 +256,9 @@ function collectStaticModuleSpecifiers(files) {
|
||||
({ start, end }) => occurrence >= start && occurrence < end,
|
||||
)
|
||||
) {
|
||||
fail(`runtime JavaScript contains an unproved internal specifier at ${file.path}`);
|
||||
fail(
|
||||
`runtime JavaScript contains an unproved internal specifier at ${file.path}`,
|
||||
);
|
||||
}
|
||||
occurrence = contents.indexOf('@qinglong/', occurrence + 1);
|
||||
}
|
||||
@@ -269,8 +315,8 @@ function manifestBinTargets(record) {
|
||||
typeof bin === 'string'
|
||||
? [bin]
|
||||
: bin && typeof bin === 'object' && !Array.isArray(bin)
|
||||
? Object.values(bin)
|
||||
: null;
|
||||
? Object.values(bin)
|
||||
: null;
|
||||
if (!values || values.some((value) => typeof value !== 'string')) {
|
||||
fail(`runtime package bin is invalid for ${record.manifest.name}`);
|
||||
}
|
||||
@@ -297,10 +343,11 @@ function canonicalRelativeSpecifier(specifier) {
|
||||
else while (remainder.startsWith('../')) remainder = remainder.slice(3);
|
||||
return (
|
||||
remainder.length > 0 &&
|
||||
remainder.split('/').every(
|
||||
(segment) =>
|
||||
segment.length > 0 && segment !== '.' && segment !== '..',
|
||||
)
|
||||
remainder
|
||||
.split('/')
|
||||
.every(
|
||||
(segment) => segment.length > 0 && segment !== '.' && segment !== '..',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -373,6 +420,10 @@ function pruneRuntimeArtifact(directory, options) {
|
||||
normalizeExcludedInternalPackages(options),
|
||||
);
|
||||
const inventory = inventoryRuntimeArtifact(directory);
|
||||
const retainedJavaScriptFiles = normalizeRetainedJavaScriptFiles(
|
||||
options,
|
||||
inventory,
|
||||
);
|
||||
const manifests = inventory.files.filter(
|
||||
(file) =>
|
||||
path.basename(file.path) === 'package.json' &&
|
||||
@@ -523,6 +574,9 @@ function pruneRuntimeArtifact(directory, options) {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const file of retainedJavaScriptFiles) {
|
||||
enqueueJavascript(file, 'explicit runtime asset');
|
||||
}
|
||||
for (const specifier of entrySpecifiers) requireExport(specifier);
|
||||
|
||||
while (pendingJavascript.length > 0) {
|
||||
@@ -536,11 +590,7 @@ function pruneRuntimeArtifact(directory, options) {
|
||||
requireExport(specifier, filePath);
|
||||
} else if (specifier.startsWith('.')) {
|
||||
enqueueJavascript(
|
||||
resolveRelativeRuntimeTarget(
|
||||
filePath,
|
||||
specifier,
|
||||
owner.packageRoot,
|
||||
),
|
||||
resolveRelativeRuntimeTarget(filePath, specifier, owner.packageRoot),
|
||||
`relative import from ${filePath}`,
|
||||
);
|
||||
}
|
||||
@@ -567,13 +617,8 @@ function pruneRuntimeArtifact(directory, options) {
|
||||
for (const record of packageRecords) {
|
||||
const { file, before, manifest, packageRoot } = record;
|
||||
const required = requiredExports.get(manifest.name);
|
||||
const retainMain =
|
||||
!manifest.exports || required.has('.');
|
||||
const projected = projectRuntimeManifest(
|
||||
manifest,
|
||||
required,
|
||||
retainMain,
|
||||
);
|
||||
const retainMain = !manifest.exports || required.has('.');
|
||||
const projected = projectRuntimeManifest(manifest, required, retainMain);
|
||||
if (projected.exports) {
|
||||
for (const target of Object.values(projected.exports)) {
|
||||
validateRuntimeExportTarget(packageRoot, target);
|
||||
|
||||
Reference in New Issue
Block a user