// 发布护栏: 包发布后匿名可读 → 必须无敏感信息、体积受控、文件白名单。任一失败 exit 1。 import { execSync } from "node:child_process" import { readFileSync } from "node:fs" const MAX_PACKED_KB = 150, MAX_UNPACKED_KB = 600, MAX_FILES = 60 const ALLOWED = [/^package\.json$/, /^README\.md$/, /^src\/.+\.(tsx?|css)$/] const SECRET_PATTERNS = [ [/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "私钥"], [/\b[0-9a-f]{40}\b/i, "40位hex(疑似 Gitea/GitHub token)"], [/glpat-[\w-]{20,}/, "GitLab PAT"], [/ghp_[A-Za-z0-9]{30,}/, "GitHub PAT"], [/\bAKIA[0-9A-Z]{16}\b/, "AWS key"], [/sk-[A-Za-z0-9]{32,}/, "API key(sk-)"], [/_authToken\s*=\s*(?!\$\{)\S+/, ".npmrc 明文 token"], [/(password|passwd|secret|api[_-]?key|token)\s*[:=]\s*["'][^"'$\s]{8,}["']/i, "疑似硬编码凭证"], [/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\./, "JWT"], [/https?:\/\/[^\s/:@]+:[^\s/@]+@/, "URL 内嵌账号密码"], ] const fail = [] const out = JSON.parse(execSync("npm pack --dry-run --json --ignore-scripts", { encoding: "utf8" }))[0] const kb = (n) => (n / 1024).toFixed(1) console.log(`文件数 ${out.files.length} | 打包 ${kb(out.size)}KB | 解压 ${kb(out.unpackedSize)}KB`) if (out.files.length > MAX_FILES) fail.push(`文件数 ${out.files.length} > ${MAX_FILES}`) if (out.size / 1024 > MAX_PACKED_KB) fail.push(`打包体积 ${kb(out.size)}KB > ${MAX_PACKED_KB}KB`) if (out.unpackedSize / 1024 > MAX_UNPACKED_KB) fail.push(`解压体积 ${kb(out.unpackedSize)}KB > ${MAX_UNPACKED_KB}KB`) for (const f of out.files) { if (!ALLOWED.some((r) => r.test(f.path))) { fail.push(`不在白名单: ${f.path}`); continue } const txt = readFileSync(f.path, "utf8") for (const [re, label] of SECRET_PATTERNS) { const m = txt.match(re) if (m) fail.push(`${f.path}: ${label} → ${m[0].slice(0, 12)}…`) } } if (fail.length) { console.error("\n❌ 发布被拦截:\n - " + fail.join("\n - ")); process.exit(1) } console.log("✅ 护栏通过(白名单/敏感信息/体积)")