import crypto from 'node:crypto'; import fs from 'node:fs'; import fsp from 'node:fs/promises'; import path from 'node:path'; export const REQUIRED_PORTAL_RUNTIME_PATHS = Object.freeze([ 'server.mjs', 'memind-canary-proxy.mjs', 'deepseek-no-think-proxy.mjs', 'wechat-mp.bundle.mjs', 'mindspace-sandbox-mcp.mjs', 'tkmind-search-mcp.mjs', 'tkmind-excel-mcp.mjs', 'mindspace-public-links.mjs', 'dist', 'package.json', 'scripts/run-memind-portal-prod.sh', 'scripts/run-memind-portal-candidate.sh', 'scripts/run-memind-canary-proxy-prod.sh', 'scripts/run-deepseek-compat-proxy-candidate.sh', 'scripts/goosed-canary.compose.yml', 'scripts/check-mindspace-public-links.mjs', 'scripts/load-env.mjs', 'scripts/wechat-mp-menu.mjs', 'scripts/memind-portal-tunnel.sh', ]); export const FORBIDDEN_PORTAL_RUNTIME_PATHS = Object.freeze([ '.env', '.git', 'data', 'dist/dev', 'dist/hello-john.html', 'users', 'logs', 'MindSpace', '.tailscale', 'public/plaza-covers', ]); export async function removeForbiddenPortalRuntimePaths(artifactPath) { const root = path.resolve(artifactPath); const segments = root.split(path.sep); if (path.basename(root) !== 'portal' || !segments.includes('.runtime')) { throw new Error('Refusing to sanitize a path outside an explicit .runtime/portal directory'); } for (const relative of FORBIDDEN_PORTAL_RUNTIME_PATHS) { await fsp.rm(path.join(root, relative), { recursive: true, force: true }); } } export function assertPortalRuntimePath(artifactPath, { repoRoot, } = {}) { const expected = path.resolve(repoRoot, '.runtime', 'portal'); const actual = path.resolve(artifactPath); if (actual !== expected) { throw new Error(`Portal release gate artifact must be ${expected}`); } return actual; } async function sha256File(filePath) { const hash = crypto.createHash('sha256'); await new Promise((resolve, reject) => { const stream = fs.createReadStream(filePath); stream.on('data', (chunk) => hash.update(chunk)); stream.on('error', reject); stream.on('end', resolve); }); return hash.digest('hex'); } async function walk(root, current = root) { const entries = await fsp.readdir(current, { withFileTypes: true }); entries.sort((a, b) => a.name.localeCompare(b.name)); const result = []; for (const entry of entries) { const absolute = path.join(current, entry.name); const relative = path.relative(root, absolute).split(path.sep).join('/'); const stat = await fsp.lstat(absolute); if (stat.isDirectory()) { result.push({ type: 'dir', path: relative, mode: stat.mode & 0o777 }); result.push(...await walk(root, absolute)); } else if (stat.isFile()) { result.push({ type: 'file', path: relative, mode: stat.mode & 0o777, size: stat.size, sha256: await sha256File(absolute), }); } else if (stat.isSymbolicLink()) { const target = await fsp.readlink(absolute); const resolved = path.resolve(path.dirname(absolute), target); const relativeResolved = path.relative(root, resolved); if (path.isAbsolute(target) || relativeResolved.startsWith(`..${path.sep}`) || relativeResolved === '..') { throw new Error(`Artifact symlink escapes root: ${relative}`); } result.push({ type: 'symlink', path: relative, target }); } else { throw new Error(`Unsupported artifact entry: ${relative}`); } } return result; } export async function hashArtifact(artifactPath) { const absolute = path.resolve(artifactPath); const stat = await fsp.stat(absolute); if (stat.isFile()) { return { sha256: await sha256File(absolute), kind: 'file', files: 1, bytes: stat.size, }; } if (!stat.isDirectory()) { throw new Error(`Artifact must be a file or directory: ${absolute}`); } const entries = await walk(absolute); const hash = crypto.createHash('sha256'); for (const entry of entries) { hash.update(JSON.stringify(entry)); hash.update('\n'); } return { sha256: hash.digest('hex'), kind: 'directory-tree', files: entries.filter((entry) => entry.type === 'file').length, bytes: entries.reduce((sum, entry) => sum + (entry.size ?? 0), 0), entries, }; } export async function inspectPortalRuntime(artifactPath) { const root = path.resolve(artifactPath); const stat = await fsp.stat(root); if (!stat.isDirectory()) { throw new Error('Portal runtime inspection requires a directory artifact'); } const missing = []; for (const relative of REQUIRED_PORTAL_RUNTIME_PATHS) { try { await fsp.access(path.join(root, relative)); } catch { missing.push(relative); } } const forbidden = []; for (const relative of FORBIDDEN_PORTAL_RUNTIME_PATHS) { try { await fsp.access(path.join(root, relative)); forbidden.push(relative); } catch { // Expected: runtime artifacts must not contain persisted production state. } } return { missing, forbidden, passed: missing.length === 0 && forbidden.length === 0, }; }