116 lines
4.2 KiB
JavaScript
116 lines
4.2 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawnSync } from 'node:child_process';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { pathToFileURL } from 'node:url';
|
|
|
|
const LOCKFILE_PATH = 'package-lock.json';
|
|
const POLICY_PATH = 'security/install-scripts-allowlist.json';
|
|
const SUPPORTED_VERIFIERS = new Set(['better-sqlite3-memory-db']);
|
|
const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
|
|
function packageNameFromPath(path) {
|
|
const marker = "node_modules/";
|
|
const index = path.lastIndexOf(marker);
|
|
return index === -1 ? "" : path.slice(index + marker.length);
|
|
}
|
|
|
|
export function validateRebuildEntries(lockfile, approvedEntries) {
|
|
if (lockfile?.lockfileVersion !== 3 || typeof lockfile.packages !== 'object') {
|
|
throw new Error('native rebuild requires npm lockfile version 3');
|
|
}
|
|
if (!Array.isArray(approvedEntries)) {
|
|
throw new Error('native rebuild allowlist must be an array');
|
|
}
|
|
|
|
return approvedEntries.map((approval) => {
|
|
for (const field of ['path', 'name', 'version', 'integrity', 'verifier']) {
|
|
if (typeof approval?.[field] !== 'string' || approval[field] === '') {
|
|
throw new Error(`native rebuild approval is missing ${field}`);
|
|
}
|
|
}
|
|
if (!SUPPORTED_VERIFIERS.has(approval.verifier)) {
|
|
throw new Error(`unsupported native rebuild verifier: ${approval.verifier}`);
|
|
}
|
|
|
|
if (!PACKAGE_NAME.test(approval.name)) {
|
|
throw new Error("invalid package name for native rebuild: " + approval.name);
|
|
}
|
|
if (packageNameFromPath(approval.path) !== approval.name) {
|
|
throw new Error("native rebuild name mismatch for " + approval.path);
|
|
}
|
|
|
|
const locked = lockfile.packages[approval.path];
|
|
if (!locked) throw new Error(`native rebuild path missing from lockfile: ${approval.path}`);
|
|
if (!locked.hasInstallScript) {
|
|
throw new Error(`native rebuild target has no install script: ${approval.path}`);
|
|
}
|
|
if (locked.version !== approval.version) {
|
|
throw new Error(`native rebuild version mismatch for ${approval.path}`);
|
|
}
|
|
if (locked.integrity !== approval.integrity) {
|
|
throw new Error(`native rebuild integrity mismatch for ${approval.path}`);
|
|
}
|
|
|
|
return { ...approval };
|
|
});
|
|
}
|
|
|
|
function run(command, args) {
|
|
const result = spawnSync(command, args, {
|
|
env: { ...process.env, npm_config_audit: 'false' },
|
|
stdio: 'inherit',
|
|
shell: false,
|
|
});
|
|
if (result.error) throw result.error;
|
|
if (result.status !== 0) {
|
|
throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `npm rebuild` に渡す引数。
|
|
*
|
|
* `--build-from-source` は、配布済みバイナリを取ってこさせず必ず手元で組ませる
|
|
* ための指定(取得物を 1 つ減らすので安全側)。CI の runner にコンパイラ・make・
|
|
* python3 が要るようになる代わりなので、消すときはそこまで見ること。
|
|
*
|
|
* `--` は、名前が `-` で始まっていても旗として解釈させないための区切り。名前は
|
|
* すでに PACKAGE_NAME で縛ってあるので二重の守りだが、縛りのほうが緩んだときに
|
|
* ここだけで止まる形にしておく。
|
|
*/
|
|
export function rebuildArgs(name) {
|
|
return ['rebuild', '--build-from-source', '--foreground-scripts', '--no-audit', '--', name];
|
|
}
|
|
|
|
export function verify(verifier) {
|
|
if (verifier === 'better-sqlite3-memory-db') {
|
|
run(process.execPath, [
|
|
'-e',
|
|
"const Database=require('better-sqlite3'); new Database(':memory:').close()",
|
|
]);
|
|
return;
|
|
}
|
|
throw new Error(`unsupported native rebuild verifier: ${verifier}`);
|
|
}
|
|
|
|
export async function main() {
|
|
const [lockfile, policy] = await Promise.all([
|
|
readFile(LOCKFILE_PATH, 'utf8').then(JSON.parse),
|
|
readFile(POLICY_PATH, 'utf8').then(JSON.parse),
|
|
]);
|
|
const approved = validateRebuildEntries(lockfile, policy.rebuild?.[LOCKFILE_PATH]);
|
|
|
|
for (const entry of approved) {
|
|
run('npm', rebuildArgs(entry.name));
|
|
verify(entry.verifier);
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
main().catch((error) => {
|
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|