61 lines
2.7 KiB
JavaScript
61 lines
2.7 KiB
JavaScript
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
|
||
const AGENT_PAGE_CODE_EXTENSIONS = new Set(['.html', '.htm', '.js', '.mjs', '.jsx', '.ts', '.tsx']);
|
||
const PROHIBITED_BROWSER_STORAGE_PATTERNS = [
|
||
{ api: 'localStorage', pattern: /\blocalStorage\b/i },
|
||
{ api: 'sessionStorage', pattern: /\bsessionStorage\b/i },
|
||
{ api: 'IndexedDB', pattern: /\b(?:indexedDB|IDBDatabase|IDBObjectStore|IDBTransaction)\b/i },
|
||
];
|
||
|
||
export function isAgentPageCodePath(relativePath) {
|
||
return AGENT_PAGE_CODE_EXTENSIONS.has(path.extname(String(relativePath ?? '')).toLowerCase());
|
||
}
|
||
|
||
export function detectProhibitedBrowserStorage(content, { relativePath = '' } = {}) {
|
||
if (!isAgentPageCodePath(relativePath)) return [];
|
||
let source = String(content ?? '');
|
||
// Fold adjacent string literals so computed-property obfuscation such as
|
||
// window['local' + 'Storage'] cannot bypass the delivery policy.
|
||
for (let pass = 0; pass < 8; pass += 1) {
|
||
const folded = source.replace(
|
||
/(['"])([^'"\\]*)\1\s*\+\s*(['"])([^'"\\]*)\3/g,
|
||
(_match, quote, left, _rightQuote, right) => `${quote}${left}${right}${quote}`,
|
||
);
|
||
if (folded === source) break;
|
||
source = folded;
|
||
}
|
||
return PROHIBITED_BROWSER_STORAGE_PATTERNS
|
||
.filter(({ pattern }) => pattern.test(source))
|
||
.map(({ api }) => api);
|
||
}
|
||
|
||
export function assertNoProhibitedBrowserStorage(content, { relativePath = '' } = {}) {
|
||
const APIs = detectProhibitedBrowserStorage(content, { relativePath });
|
||
if (APIs.length === 0) return;
|
||
const error = new Error(
|
||
`${relativePath || '页面代码'} 禁止使用 ${APIs.join(', ')} 保存数据。`
|
||
+ '用户业务数据必须通过 private_data_* 工具写入该用户专属 PostgreSQL schema,'
|
||
+ 'HTML 必须通过 Page Data API 访问;请改写后再保存。',
|
||
);
|
||
error.code = 'BROWSER_STORAGE_FORBIDDEN';
|
||
error.apis = APIs;
|
||
error.relativePath = relativePath || null;
|
||
throw error;
|
||
}
|
||
|
||
export function scanWorkspaceFilesForProhibitedBrowserStorage({ publishDir, relativePaths = [] } = {}) {
|
||
const root = path.resolve(String(publishDir ?? ''));
|
||
if (!root) return [];
|
||
const violations = [];
|
||
for (const relativePath of new Set(relativePaths.map((item) => String(item ?? '').trim()).filter(Boolean))) {
|
||
if (!isAgentPageCodePath(relativePath)) continue;
|
||
const absolutePath = path.resolve(root, relativePath);
|
||
if (absolutePath !== root && !absolutePath.startsWith(`${root}${path.sep}`)) continue;
|
||
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) continue;
|
||
const apis = detectProhibitedBrowserStorage(fs.readFileSync(absolutePath, 'utf8'), { relativePath });
|
||
if (apis.length > 0) violations.push({ relativePath, apis });
|
||
}
|
||
return violations;
|
||
}
|