import fs from 'node:fs'; import path from 'node:path'; /** * SESSION BROKER COVERAGE (H5 Session architecture Patch 2 / §5.7) * * Business code must not call userAuth session ownership helpers directly. * Use session-broker.mjs facade via createSessionAccess / resolveSessionAccess. * * See docs/h5-session-architecture-20260706.md §5.7 */ /** @typedef {{ file: string, line: number, rule: string, text: string }} SessionBrokerCoverageViolation */ export const SESSION_BROKER_FACADE_FILE = 'session-broker.mjs'; export const SESSION_BROKER_DEFINITION_FILE = 'user-auth.mjs'; /** Files allowed to call userAuth.registerAgentSession / ownsSession directly. */ export const SESSION_BROKER_DIRECT_ALLOWLIST = new Set([ SESSION_BROKER_FACADE_FILE, SESSION_BROKER_DEFINITION_FILE, ]); /** Patch 2 modules that must route session ownership through sessionAccess. */ export const SESSION_BROKER_PATCH2_FILES = [ 'tkmind-proxy.mjs', 'agent-run-routes.mjs', 'agent-run-gateway.mjs', 'direct-chat-service.mjs', 'server.mjs', 'wechat-mp.mjs', 'mindspace-page-edit-session.mjs', 'mindspace-agent-runner.mjs', 'mindspace-conversation-package-routes.mjs', ]; export const SESSION_BROKER_FORBIDDEN_CALLS = [ { id: 'register-agent-session', pattern: /\buserAuth\.registerAgentSession\s*\(/ }, { id: 'owns-session', pattern: /\buserAuth\.ownsSession\s*\(/ }, { id: 'get-session-target', pattern: /\buserAuth\.getSessionTarget\s*\(/ }, { id: 'unregister-agent-session', pattern: /\buserAuth\.unregisterAgentSession\s*\(/ }, ]; export const SESSION_BROKER_PATCH2_MARKERS = [ /\bresolveSessionAccess\b/, /\bcreateSessionAccess\b/, /\bsessionAccess\b/, ]; export const SESSION_BROKER_SCAN_IGNORE = [ /^scripts\//, /^mindspace-service\//, /^\.runtime\//, /^node_modules\//, /\.test\.mjs$/, /\.test\.ts$/, ]; export const SESSION_BROKER_LINE_ALLOW = [ /SESSION BROKER COVERAGE/, /§5\.7/, /session-broker\.mjs/, /createSessionAccess/, /resolveSessionAccess/, /sessionAccess\./, /getSessionAccess/, ]; function relativePosix(rootDir, absolutePath) { return path.relative(rootDir, absolutePath).split(path.sep).join('/'); } function isIgnoredScanPath(relativePath) { return SESSION_BROKER_SCAN_IGNORE.some((pattern) => pattern.test(relativePath)); } function isTestFile(relativePath) { return /\.test\.mjs$/.test(relativePath); } function isLineAllowed(line) { return SESSION_BROKER_LINE_ALLOW.some((pattern) => pattern.test(line)); } function scanDirectUserAuthCalls(relativePath, content) { /** @type {SessionBrokerCoverageViolation[]} */ const violations = []; if (SESSION_BROKER_DIRECT_ALLOWLIST.has(relativePath) || isTestFile(relativePath)) { return violations; } const lines = content.split('\n'); for (let index = 0; index < lines.length; index += 1) { const line = lines[index]; if (!line.trim() || line.trim().startsWith('//') || line.trim().startsWith('*')) continue; if (isLineAllowed(line)) continue; for (const rule of SESSION_BROKER_FORBIDDEN_CALLS) { if (!rule.pattern.test(line)) continue; violations.push({ file: relativePath, line: index + 1, rule: rule.id, text: line.trim(), }); } } return violations; } function checkPatch2ModuleWiring(rootDir) { /** @type {SessionBrokerCoverageViolation[]} */ const violations = []; for (const relativePath of SESSION_BROKER_PATCH2_FILES) { const absolutePath = path.join(rootDir, relativePath); if (!fs.existsSync(absolutePath)) { violations.push({ file: relativePath, line: 0, rule: 'missing-patch2-file', text: 'Patch 2 module missing from repository', }); continue; } const content = fs.readFileSync(absolutePath, 'utf8'); if (!SESSION_BROKER_PATCH2_MARKERS.some((pattern) => pattern.test(content))) { violations.push({ file: relativePath, line: 0, rule: 'missing-session-access-wiring', text: 'Expected resolveSessionAccess/createSessionAccess/sessionAccess wiring', }); } } return violations; } export function checkSessionBrokerCoverage(rootDir, { scanRepo = true } = {}) { /** @type {SessionBrokerCoverageViolation[]} */ const violations = [...checkPatch2ModuleWiring(rootDir)]; if (!scanRepo) return violations; const queue = [rootDir]; while (queue.length > 0) { const current = queue.pop(); const entries = fs.readdirSync(current, { withFileTypes: true }); for (const entry of entries) { if (entry.name === 'node_modules' || entry.name === '.git') continue; const absolutePath = path.join(current, entry.name); const relativePath = relativePosix(rootDir, absolutePath); if (entry.isDirectory()) { queue.push(absolutePath); continue; } if (!entry.name.endsWith('.mjs')) continue; if (isIgnoredScanPath(relativePath)) continue; const content = fs.readFileSync(absolutePath, 'utf8'); violations.push(...scanDirectUserAuthCalls(relativePath, content)); } } return violations; } export function formatSessionBrokerCoverageViolations(violations) { return violations .map((item) => `${item.file}:${item.line} [${item.rule}] ${item.text}`) .join('\n'); }