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; }