// License allowlist gate (issue #202): reads `pnpm licenses list --json` // from stdin and fails when any dependency's license is outside the // allowlist below. The allowlist is the documented policy — extending it is // a deliberate, reviewed act, not a build fix. // // Usage: pnpm licenses list --json | node scripts/check-licenses.mjs // Permissive licenses only, plus two consciously admitted cases: // - MPL-2.0: file-level copyleft; we consume MPL packages (axe-core, // dev-only) unmodified, which triggers no obligations beyond source // availability of the (unmodified) files themselves. // - CC-BY-4.0: attribution license used for data packages (browser // compatibility data); attribution is satisfied by the license report. const ALLOWED = new Set([ 'MIT', 'MIT-0', 'ISC', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', '0BSD', 'BlueOak-1.0.0', 'CC0-1.0', 'CC-BY-4.0', 'Python-2.0', 'Unlicense', 'MPL-2.0', 'Zlib', ]); // Packages whose license METADATA is missing or wrong upstream, verified by // hand against the shipped license text. Key = package name, value = the // verification note an auditor reads. const EXCEPTIONS = new Map([ [ 'khroma', 'MIT — upstream ships the MIT text as its `license` file but omits the package.json license field (reported as "Unknown")', ], ]); /** SPDX-light evaluation, sufficient for the expressions pnpm emits today: * parentheses stripped, OR satisfied by any allowed alternative, AND by all * parts. Nested mixed expressions would need a real parser — they fail * closed here, which is the safe direction. */ function isAllowed(expression) { const clean = expression.replace(/[()]/g, ' ').trim(); if (/\sOR\s/.test(clean)) { return clean.split(/\s+OR\s+/).some((part) => isAllowed(part)); } return clean.split(/\s+AND\s+/).every((part) => ALLOWED.has(part.trim())); } const input = await new Promise((resolve, reject) => { let data = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (chunk) => (data += chunk)); process.stdin.on('end', () => resolve(data)); process.stdin.on('error', reject); }); const byLicense = JSON.parse(input); const violations = []; let packages = 0; for (const [license, entries] of Object.entries(byLicense)) { for (const entry of entries) { packages += 1; if (isAllowed(license)) continue; if (EXCEPTIONS.has(entry.name)) continue; violations.push({ name: entry.name, license }); } } if (violations.length > 0) { console.error('licenses outside the documented allowlist:'); for (const v of violations) console.error(` ${v.name}: ${v.license}`); console.error( 'Either the dependency goes, or the allowlist/exception table in scripts/check-licenses.mjs is extended in a reviewed change.', ); process.exit(1); } console.log( `license gate: ${packages} packages, ${Object.keys(byLicense).length} distinct license expressions, all within the allowlist (${EXCEPTIONS.size} documented exception${EXCEPTIONS.size === 1 ? '' : 's'})`, );