#!/usr/bin/env node // tools/scripts/lint-no-clipboard-rce.mjs // Source: CLAUDE.md hard rule #3 + 04-CONTEXT.md SRV-12 anti-port. // // Greps apps/server/src/**/*.ts for forbidden RCE-surface tokens. Comments // are stripped before scan, so docstring references that document the // anti-port for historical context (e.g. admin-stubs.ts header) are // allowlisted. Functional code that would reintroduce the surface is not. // // Usage: node tools/scripts/lint-no-clipboard-rce.mjs // Exit: 0 clean, 1 forbidden token found. import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join } from 'node:path'; const ROOT = 'apps/server/src'; const FORBIDDEN = [ { name: 'execute_string', re: /\bexecute_string\b/ }, { name: 'eval(', re: /\beval\s*\(/ }, { name: 'new Function(', re: /\bnew\s+Function\s*\(/ }, { name: 'clipboard', re: /\bclipboard\b/ }, { name: 'vm.runIn', re: /\bvm\.runIn[A-Z]\w*/ }, { name: "child_process literal", re: /['"]child_process['"]/ }, ]; /** Strip block and line comments. Cheap, not a full parser; sufficient. */ function stripComments(src) { return src .replace(/\/\*[\s\S]*?\*\//g, '') .split('\n') .map((line) => line.replace(/\/\/.*$/, '')) .join('\n'); } function walk(dir, out = []) { for (const f of readdirSync(dir)) { const p = join(dir, f); const s = statSync(p); if (s.isDirectory()) walk(p, out); else if (p.endsWith('.ts')) out.push(p); } return out; } const files = walk(ROOT); let violations = 0; for (const f of files) { const src = stripComments(readFileSync(f, 'utf-8')); for (const rule of FORBIDDEN) { if (rule.re.test(src)) { process.stderr.write( `lint-no-clipboard-rce: ${f} contains forbidden token ${rule.name}\n`, ); violations++; } } } if (violations > 0) { process.stderr.write( `lint-no-clipboard-rce: ${violations} violation(s) in ${files.length} file(s).\n`, ); process.exit(1); } console.log(`lint-no-clipboard-rce: OK (${files.length} file(s) clean)`); process.exit(0);