34 lines
2.0 KiB
JavaScript
34 lines
2.0 KiB
JavaScript
// 发布护栏: 包发布后匿名可读 → 必须无敏感信息、体积受控、文件白名单。任一失败 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("✅ 护栏通过(白名单/敏感信息/体积)")
|