Spaces:
Runtime error
Runtime error
| // scripts/check/check-licenses.mjs | |
| // Gate de license compliance — PLANO-QUALITY-GATES-FASE7.md, Task 20. | |
| // | |
| // Política: OmniRoute é MIT. Dependências de PRODUÇÃO com licença fora da allowlist SPDX | |
| // e sem exceção registrada em .license-allowlist.json => FALHA (policy violation). | |
| // devDependencies com licença não-padrão => advisory (impressas, não falham). | |
| // | |
| // Ferramenta: license-checker-rseidelsohn v4+ (node_modules/.bin/license-checker-rseidelsohn). | |
| // | |
| // Uso: | |
| // node scripts/check/check-licenses.mjs # modo normal | |
| // node scripts/check/check-licenses.mjs --verbose # lista todos os pacotes classificados | |
| // node scripts/check/check-licenses.mjs --json # emite o raw JSON do license-checker | |
| // | |
| // Sair com código 0 = tudo OK (ou apenas advisory). | |
| // Sair com código 1 = violação de política em dep de produção. | |
| import { execFileSync } from "node:child_process"; | |
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import { pathToFileURL } from "node:url"; | |
| const ROOT = process.cwd(); | |
| const ALLOWLIST_PATH = path.join(ROOT, "config/quality/.license-allowlist.json"); | |
| const CHECKER_BIN = path.join(ROOT, "node_modules", ".bin", "license-checker-rseidelsohn"); | |
| const VERBOSE = process.argv.includes("--verbose"); | |
| const PRINT_JSON = process.argv.includes("--json"); | |
| // --------------------------------------------------------------------------- | |
| // Allowlist loading | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Loads and returns the license allowlist from .license-allowlist.json. | |
| * | |
| * @returns {{ allowed: string[], allowedExpressions: string[], exceptions: Record<string, {license:string, justification:string, risk:string}> }} | |
| */ | |
| export function loadAllowlist() { | |
| if (!fs.existsSync(ALLOWLIST_PATH)) { | |
| throw new Error( | |
| `Allowlist not found: ${ALLOWLIST_PATH}. Create .license-allowlist.json first.` | |
| ); | |
| } | |
| const raw = fs.readFileSync(ALLOWLIST_PATH, "utf-8"); | |
| const parsed = JSON.parse(raw); | |
| return { | |
| allowed: parsed.allowed ?? [], | |
| allowedExpressions: parsed.allowedExpressions ?? [], | |
| exceptions: parsed.exceptions ?? {}, | |
| }; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Classification logic (pure, testable) | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Classifies a package+license against the allowlist. | |
| * | |
| * @param {string} packageName - Package name without version, e.g. "lightningcss" | |
| * @param {string} license - License string from license-checker, e.g. "MPL-2.0" | |
| * @param {{ allowed: string[], allowedExpressions: string[], exceptions: Record<string,any> }} allowlist | |
| * @returns {{ status: "allowed" | "exception" | "denied", reason: string }} | |
| */ | |
| export function classifyLicense(packageName, license, allowlist) { | |
| const { allowed, allowedExpressions, exceptions } = allowlist; | |
| // 1. Direct SPDX match | |
| if (allowed.includes(license)) { | |
| return { status: "allowed", reason: `SPDX match: ${license}` }; | |
| } | |
| // 2. Expression match (e.g. "(MIT OR Apache-2.0)") | |
| if (allowedExpressions.includes(license)) { | |
| return { status: "allowed", reason: `allowed expression: ${license}` }; | |
| } | |
| // 3. Per-package exception (strip version suffix for lookup) | |
| const baseName = stripVersion(packageName); | |
| if (exceptions[baseName]) { | |
| const exc = exceptions[baseName]; | |
| return { | |
| status: "exception", | |
| reason: `exception: ${exc.justification} [risk=${exc.risk}]`, | |
| }; | |
| } | |
| // 4. Denied | |
| return { | |
| status: "denied", | |
| reason: `license '${license}' not in allowlist and no exception registered for '${baseName}'`, | |
| }; | |
| } | |
| /** | |
| * Strips the @version suffix from a package key returned by license-checker. | |
| * e.g. "lightningcss@1.32.0" => "lightningcss" | |
| * "@img/sharp-libvips-linux-x64@1.2.4" => "@img/sharp-libvips-linux-x64" | |
| * | |
| * @param {string} pkgKey - Package key as returned by license-checker | |
| * @returns {string} | |
| */ | |
| export function stripVersion(pkgKey) { | |
| // Handle scoped packages: @scope/name@version | |
| const scopedMatch = pkgKey.match(/^(@[^/]+\/[^@]+)(?:@.*)?$/); | |
| if (scopedMatch) return scopedMatch[1]; | |
| // Regular: name@version | |
| const regularMatch = pkgKey.match(/^([^@]+)(?:@.*)?$/); | |
| if (regularMatch) return regularMatch[1]; | |
| return pkgKey; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Runner | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Runs license-checker-rseidelsohn --production and returns parsed JSON. | |
| * | |
| * @returns {Record<string, { licenses: string, path: string }>} | |
| */ | |
| function runLicenseChecker() { | |
| if (!fs.existsSync(CHECKER_BIN)) { | |
| throw new Error( | |
| `license-checker-rseidelsohn not found at ${CHECKER_BIN}.\n` + | |
| `Install it: npm install --save-dev license-checker-rseidelsohn` | |
| ); | |
| } | |
| const output = execFileSync(CHECKER_BIN, ["--production", "--json"], { | |
| cwd: ROOT, | |
| encoding: "utf-8", | |
| maxBuffer: 32 * 1024 * 1024, // 32 MB | |
| }); | |
| return JSON.parse(output); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Main | |
| // --------------------------------------------------------------------------- | |
| function main() { | |
| /** @type {Record<string, { licenses: string, path: string }>} */ | |
| let licenseData; | |
| try { | |
| licenseData = runLicenseChecker(); | |
| } catch (err) { | |
| console.error("[check-licenses] Falha ao rodar license-checker-rseidelsohn:"); | |
| console.error(err.message ?? err); | |
| process.exitCode = 1; | |
| return; | |
| } | |
| if (PRINT_JSON) { | |
| console.log(JSON.stringify(licenseData, null, 2)); | |
| return; | |
| } | |
| /** @type {{ allowed: string[], allowedExpressions: string[], exceptions: Record<string,any> }} */ | |
| let allowlist; | |
| try { | |
| allowlist = loadAllowlist(); | |
| } catch (err) { | |
| console.error("[check-licenses]", err.message); | |
| process.exitCode = 1; | |
| return; | |
| } | |
| const violations = []; | |
| const exceptions = []; | |
| const advisory = []; | |
| let allowedCount = 0; | |
| for (const [pkgKey, info] of Object.entries(licenseData)) { | |
| const license = info.licenses ?? "UNKNOWN"; | |
| const result = classifyLicense(pkgKey, license, allowlist); | |
| if (result.status === "allowed") { | |
| allowedCount++; | |
| if (VERBOSE) { | |
| console.log(` ✓ ${pkgKey}: ${license}`); | |
| } | |
| } else if (result.status === "exception") { | |
| exceptions.push({ pkgKey, license, reason: result.reason }); | |
| } else { | |
| violations.push({ pkgKey, license, reason: result.reason }); | |
| } | |
| } | |
| const total = Object.keys(licenseData).length; | |
| // Print summary | |
| console.log(`[check-licenses] Escaneados ${total} pacotes de produção.`); | |
| console.log(` Permitidos: ${allowedCount}`); | |
| console.log(` Exceções registradas: ${exceptions.length}`); | |
| console.log(` Violações de política: ${violations.length}`); | |
| // Print exceptions (informational) | |
| if (exceptions.length > 0) { | |
| console.log( | |
| "\n[check-licenses] Exceções registradas (não bloqueantes, revisar periodicamente):" | |
| ); | |
| for (const { pkgKey, license } of exceptions) { | |
| const baseName = stripVersion(pkgKey); | |
| const exc = allowlist.exceptions[baseName]; | |
| const riskTag = exc?.risk === "medium" ? " ⚠️ RISK=medium" : ""; | |
| console.log(` ⚑ ${pkgKey}: ${license}${riskTag}`); | |
| if (exc?.risk === "medium") { | |
| console.log(` → ${exc.justification}`); | |
| } | |
| } | |
| } | |
| // Print advisory (devDep non-standard — empty here since we run --production) | |
| if (advisory.length > 0) { | |
| console.log("\n[check-licenses] Advisory (devDeps com licença não-padrão):"); | |
| for (const { pkgKey, license } of advisory) { | |
| console.log(` ℹ ${pkgKey}: ${license}`); | |
| } | |
| } | |
| // Print violations and fail | |
| if (violations.length > 0) { | |
| console.error( | |
| "\n[check-licenses] ❌ VIOLAÇÕES DE POLÍTICA — deps de produção com licença não permitida:" | |
| ); | |
| for (const { pkgKey, license, reason } of violations) { | |
| console.error(` ✗ ${pkgKey}: ${license}`); | |
| console.error(` → ${reason}`); | |
| } | |
| console.error( | |
| "\nAdicione a licença à allowlist 'allowed' em .license-allowlist.json (se SPDX-permissiva)\n" + | |
| "ou registre uma exceção por-pacote em 'exceptions' com justificativa e 'reviewAt'.\n" + | |
| "NÃO mascare copyleft forte sem registrar a justificativa. Ver PLANO-QUALITY-GATES-FASE7.md § Task 20." | |
| ); | |
| process.exitCode = 1; | |
| return; | |
| } | |
| console.log( | |
| "\n[check-licenses] ✅ Todos os pacotes de produção estão em conformidade com a política de licenças." | |
| ); | |
| } | |
| // Run only when invoked directly (not when imported by tests) | |
| const isMain = | |
| process.argv[1] === pathToFileURL(import.meta.url).pathname || | |
| process.argv[1]?.endsWith("check-licenses.mjs"); | |
| if (isMain) { | |
| main(); | |
| } | |