mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-10 17:02:22 +03:00
Removes stale nested browser-pool lock entries pinning Playwright 1.61.1/@types/node 22 despite the workspace declaring 1.62.1/26, and extends the lockfile gate with npm ls --workspaces --depth=0 so future manifest/lock drift fails visibly. 21/21 focused tests passing. Thanks!
187 lines
6.2 KiB
JavaScript
187 lines
6.2 KiB
JavaScript
#!/usr/bin/env node
|
|
// scripts/check/check-lockfile.mjs
|
|
// Gate de política de lockfile (CLAUDE.md — extensão Hard Rule #1).
|
|
//
|
|
// Objetivo: detectar supply-chain poisoning no package-lock.json antes que código
|
|
// malicioso entre no repo. Verifica:
|
|
// --validate-https → toda URL "resolved" deve usar HTTPS (bloqueia http://)
|
|
// --validate-integrity → todo pacote deve ter hash de integridade sha512
|
|
// --allowed-hosts npm → apenas registry.npmjs.org é host permitido
|
|
// npm ls --workspaces → entradas do lockfile satisfazem cada workspace
|
|
//
|
|
// Complementa check-deps (Fase 2 / allowlist de nomes): aquele garante que só
|
|
// nomes aprovados entram; este garante que os pacotes instalados vieram do registry
|
|
// legítimo com integridade verificável.
|
|
//
|
|
// Referência: PLANO-QUALITY-GATES-FASE7.md, Task 7.7.
|
|
// Tool: lockfile-lint v5 (node_modules/.bin/lockfile-lint).
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
import path from "node:path";
|
|
import fs from "node:fs";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
const ROOT = process.cwd();
|
|
|
|
/**
|
|
* Returns the canonical lockfile-lint configuration used by this gate.
|
|
* Exporting this object makes the policy auditable and unit-testable without
|
|
* spawning a child process.
|
|
*
|
|
* @returns {{
|
|
* lockfilePath: string,
|
|
* type: string,
|
|
* validateHttps: boolean,
|
|
* validateIntegrity: boolean,
|
|
* allowedHosts: string[],
|
|
* }}
|
|
*/
|
|
export function getLockfileLintConfig() {
|
|
return {
|
|
lockfilePath: path.join(ROOT, "package-lock.json"),
|
|
type: "npm",
|
|
validateHttps: true,
|
|
validateIntegrity: true,
|
|
// Only the official npm registry is permitted.
|
|
// registry.npmjs.org resolves to the "npm" shorthand in lockfile-lint.
|
|
// If the project ever adopts a scoped/private registry, add its hostname here
|
|
// and document the justification.
|
|
allowedHosts: ["npm"],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Builds the argv array to pass to the lockfile-lint binary, derived from
|
|
* the config returned by getLockfileLintConfig().
|
|
*
|
|
* @param {ReturnType<typeof getLockfileLintConfig>} cfg
|
|
* @returns {string[]}
|
|
*/
|
|
export function buildLockfileLintArgs(cfg) {
|
|
const args = ["--path", cfg.lockfilePath, "--type", cfg.type];
|
|
if (cfg.validateHttps) args.push("--validate-https");
|
|
if (cfg.validateIntegrity) args.push("--validate-integrity");
|
|
if (cfg.allowedHosts.length) {
|
|
args.push("--allowed-hosts", ...cfg.allowedHosts);
|
|
}
|
|
return args;
|
|
}
|
|
|
|
/**
|
|
* Returns the cross-platform npm command that verifies direct workspace
|
|
* dependencies from package-lock.json, independent of the installed tree.
|
|
*
|
|
* @param {NodeJS.Platform} [platform]
|
|
* @param {string | undefined} [comSpec]
|
|
* @returns {{ command: string, args: string[] }}
|
|
*/
|
|
export function getWorkspaceDependencyCheckCommand(
|
|
platform = process.platform,
|
|
comSpec = process.env.ComSpec
|
|
) {
|
|
const npmArgs = ["ls", "--workspaces", "--depth=0", "--package-lock-only"];
|
|
if (platform === "win32") {
|
|
return {
|
|
command: comSpec || "cmd.exe",
|
|
args: ["/d", "/s", "/c", "npm.cmd", ...npmArgs],
|
|
};
|
|
}
|
|
|
|
return {
|
|
command: "npm",
|
|
args: npmArgs,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Executes the workspace dependency consistency check while keeping the process
|
|
* boundary injectable for deterministic success and failure tests.
|
|
*
|
|
* @param {{
|
|
* platform?: NodeJS.Platform,
|
|
* comSpec?: string,
|
|
* execFile?: typeof execFileSync,
|
|
* }} [options]
|
|
* @returns {{ ok: true } | { ok: false, stdout: string, stderr: string }}
|
|
*/
|
|
export function runWorkspaceDependencyCheck(options = {}) {
|
|
const {
|
|
platform = process.platform,
|
|
comSpec = process.env.ComSpec,
|
|
execFile = execFileSync,
|
|
} = options;
|
|
const workspaceCheck = getWorkspaceDependencyCheckCommand(platform, comSpec);
|
|
|
|
try {
|
|
execFile(workspaceCheck.command, workspaceCheck.args, {
|
|
cwd: ROOT,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
return { ok: true };
|
|
} catch (err) {
|
|
return {
|
|
ok: false,
|
|
stdout: err.stdout ?? "",
|
|
stderr: err.stderr ?? "",
|
|
};
|
|
}
|
|
}
|
|
|
|
function main() {
|
|
const cfg = getLockfileLintConfig();
|
|
|
|
if (!fs.existsSync(cfg.lockfilePath)) {
|
|
console.error(
|
|
`[check-lockfile] FAIL — lockfile not found: ${cfg.lockfilePath}\n` +
|
|
" → Run `npm install` to generate package-lock.json"
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const bin = path.join(ROOT, "node_modules", ".bin", "lockfile-lint");
|
|
if (!fs.existsSync(bin)) {
|
|
console.error(
|
|
`[check-lockfile] FAIL — lockfile-lint binary not found at:\n ${bin}\n` +
|
|
" → Run `npm install` to install dev dependencies"
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const args = buildLockfileLintArgs(cfg);
|
|
|
|
try {
|
|
const output = execFileSync(bin, args, { encoding: "utf8" });
|
|
// lockfile-lint outputs a green ✔ message on success
|
|
console.log("[check-lockfile] OK —", output.trim());
|
|
} catch (err) {
|
|
const stdout = err.stdout ?? "";
|
|
const stderr = err.stderr ?? "";
|
|
console.error("[check-lockfile] FAIL — lockfile-lint found policy violations:");
|
|
if (stdout) console.error(stdout);
|
|
if (stderr) console.error(stderr);
|
|
console.error(
|
|
"\n Possible causes:\n" +
|
|
" • A package was resolved from a non-HTTPS URL (http:// poisoning attempt)\n" +
|
|
" • A package is missing its integrity hash (tampered or legacy entry)\n" +
|
|
" • A package was resolved from a host other than registry.npmjs.org\n" +
|
|
" If a scoped/private registry is intentionally used, add its hostname\n" +
|
|
" to getLockfileLintConfig().allowedHosts in scripts/check/check-lockfile.mjs"
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const workspaceResult = runWorkspaceDependencyCheck();
|
|
if (workspaceResult.ok) {
|
|
console.log("[check-lockfile] OK — workspace lock entries match their manifests");
|
|
} else {
|
|
console.error("[check-lockfile] FAIL — workspace lock entries are inconsistent:");
|
|
if (workspaceResult.stdout) console.error(workspaceResult.stdout);
|
|
if (workspaceResult.stderr) console.error(workspaceResult.stderr);
|
|
console.error("\n → Regenerate the affected lock entries and verify with a clean `npm ci`");
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main();
|