Files
maestro/scripts/security/rebuild-native.mjs
T
oss-sync 57702b4f34
CI / build-and-test (push) Canceled after 0s
sync: update from private repo (2336949a)
2026-09-17 11:29:31 +00:00

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;
});
}