2026-03-03 09:25:10 -06:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
|
|
|
|
|
* check-forbidden-tokens.mjs
|
|
|
|
|
|
*
|
|
|
|
|
|
* Scans the codebase for forbidden tokens before publishing to npm.
|
|
|
|
|
|
* Mirrors the git pre-commit hook logic, but runs against the full
|
|
|
|
|
|
* working tree (not just staged changes).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Token list: .git/hooks/forbidden-tokens.txt (one per line, # comments ok).
|
2026-03-11 22:17:21 -05:00
|
|
|
|
* If the file is missing, the check still uses the active local username when
|
|
|
|
|
|
* available. If username detection fails, the check degrades gracefully.
|
2026-03-03 09:25:10 -06:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { execSync } from "node:child_process";
|
|
|
|
|
|
import { existsSync, readFileSync } from "node:fs";
|
2026-03-11 22:17:21 -05:00
|
|
|
|
import os from "node:os";
|
2026-03-03 09:25:10 -06:00
|
|
|
|
import { resolve } from "node:path";
|
2026-03-11 22:17:21 -05:00
|
|
|
|
import { fileURLToPath } from "node:url";
|
2026-03-03 09:25:10 -06:00
|
|
|
|
|
2026-03-11 22:17:21 -05:00
|
|
|
|
function uniqueNonEmpty(values) {
|
|
|
|
|
|
return Array.from(new Set(values.map((value) => value?.trim() ?? "").filter(Boolean)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveDynamicForbiddenTokens(env = process.env, osModule = os) {
|
|
|
|
|
|
const candidates = [env.USER, env.LOGNAME, env.USERNAME];
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
candidates.push(osModule.userInfo().username);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// Some environments do not expose userInfo; env vars are enough fallback.
|
|
|
|
|
|
}
|
2026-03-03 09:25:10 -06:00
|
|
|
|
|
2026-03-11 22:17:21 -05:00
|
|
|
|
return uniqueNonEmpty(candidates);
|
2026-03-03 09:25:10 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 22:17:21 -05:00
|
|
|
|
export function readForbiddenTokensFile(tokensFile) {
|
|
|
|
|
|
if (!existsSync(tokensFile)) return [];
|
2026-03-03 09:25:10 -06:00
|
|
|
|
|
2026-03-11 22:17:21 -05:00
|
|
|
|
return readFileSync(tokensFile, "utf8")
|
|
|
|
|
|
.split("\n")
|
|
|
|
|
|
.map((line) => line.trim())
|
|
|
|
|
|
.filter((line) => line && !line.startsWith("#"));
|
2026-03-03 09:25:10 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 22:17:21 -05:00
|
|
|
|
export function resolveForbiddenTokens(tokensFile, env = process.env, osModule = os) {
|
|
|
|
|
|
return uniqueNonEmpty([
|
|
|
|
|
|
...resolveDynamicForbiddenTokens(env, osModule),
|
|
|
|
|
|
...readForbiddenTokensFile(tokensFile),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
2026-03-03 09:25:10 -06:00
|
|
|
|
|
2026-03-11 22:17:21 -05:00
|
|
|
|
export function runForbiddenTokenCheck({
|
|
|
|
|
|
repoRoot,
|
|
|
|
|
|
tokens,
|
|
|
|
|
|
exec = execSync,
|
|
|
|
|
|
log = console.log,
|
|
|
|
|
|
error = console.error,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
if (tokens.length === 0) {
|
|
|
|
|
|
log(" ℹ Forbidden tokens list is empty — skipping check.");
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let found = false;
|
|
|
|
|
|
|
|
|
|
|
|
for (const token of tokens) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = exec(
|
|
|
|
|
|
`git grep -in --no-color -- ${JSON.stringify(token)} -- ':!pnpm-lock.yaml' ':!.git'`,
|
|
|
|
|
|
{ encoding: "utf8", cwd: repoRoot, stdio: ["pipe", "pipe", "pipe"] },
|
|
|
|
|
|
);
|
|
|
|
|
|
if (result.trim()) {
|
|
|
|
|
|
if (!found) {
|
|
|
|
|
|
error("ERROR: Forbidden tokens found in tracked files:\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
found = true;
|
|
|
|
|
|
const lines = result.trim().split("\n");
|
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
|
error(` ${line}`);
|
|
|
|
|
|
}
|
2026-03-03 09:25:10 -06:00
|
|
|
|
}
|
2026-03-11 22:17:21 -05:00
|
|
|
|
} catch {
|
|
|
|
|
|
// git grep returns exit code 1 when no matches — that's fine
|
2026-03-03 09:25:10 -06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-11 22:17:21 -05:00
|
|
|
|
|
|
|
|
|
|
if (found) {
|
|
|
|
|
|
error("\nBuild blocked. Remove the forbidden token(s) before publishing.");
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log(" ✓ No forbidden tokens found.");
|
|
|
|
|
|
return 0;
|
2026-03-03 09:25:10 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 22:17:21 -05:00
|
|
|
|
function resolveRepoPaths(exec = execSync) {
|
|
|
|
|
|
const repoRoot = exec("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
|
|
|
|
|
|
const gitDir = exec("git rev-parse --git-dir", { encoding: "utf8", cwd: repoRoot }).trim();
|
|
|
|
|
|
return {
|
|
|
|
|
|
repoRoot,
|
|
|
|
|
|
tokensFile: resolve(repoRoot, gitDir, "hooks/forbidden-tokens.txt"),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function main() {
|
|
|
|
|
|
const { repoRoot, tokensFile } = resolveRepoPaths();
|
|
|
|
|
|
const tokens = resolveForbiddenTokens(tokensFile);
|
|
|
|
|
|
process.exit(runForbiddenTokenCheck({ repoRoot, tokens }));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isMainModule = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
|
|
|
|
|
|
|
|
|
|
if (isMainModule) {
|
|
|
|
|
|
main();
|
2026-03-03 09:25:10 -06:00
|
|
|
|
}
|