@vigilbase/cli
Version:
Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.
309 lines (308 loc) • 14.8 kB
JavaScript
/**
* The Vigilbase-managed install of Cloudflare's cf (spec "one command", decision 5).
*
* When no usable cf is found, the full flow may install the pinned release
* into `<configDir>/cf/<version>/` with the npm that ships with this Node:
*
* - npm runs as `process.execPath <npm-cli.js> install --prefix <staging> …`
* with no shell, from the fresh 0700 staging directory, with the user's
* environment (npm needs their npmrc, proxy and CA settings) minus every
* `CF_*` / `CLOUDFLARE_*` variable and, defensively, any variable whose
* value holds a session code, a verification token or `--email`. (npx 10
* passes arguments only in argv, not in the environment:
* `npm_lifecycle_script` is just the bin name.)
* - `--ignore-scripts`: cf runs `--version`, `--dry-run` and API reads without
* lifecycle scripts. `--omit=optional`: the optional dependencies are the
* workerd and sharp platform binaries (about 150 MB). In cf 1.0.0-beta.13
* they are loaded only by `--local` mode and the `workers`, `deploy` and
* `previews` command trees (static and dynamic import graph of
* `dist/*.mjs`), none of which the checkup runs; see LOCAL.md.
* - Integrity is pinned: after npm exits, the staging lockfile must record
* `node_modules/cf` at exactly the pinned version and npm `dist.integrity`,
* and the installed package.json must agree; otherwise the staging
* directory is deleted (CF_INSTALL_FAILED). Only then is it renamed into
* place, so a half-finished install is never used. `locateCf` still applies
* its identity checks to the result.
* - Ctrl-C while npm runs kills npm and removes the staging directory. Staging
* directories left by a crash or a closed terminal are removed by the next
* install once they are older than the npm timeout (a concurrent install is
* always younger), and by `logout`.
*/
import { spawn as nodeSpawn } from 'node:child_process';
import { existsSync } from 'node:fs';
import { chmod, lstat, mkdir, mkdtemp, readdir, readFile, realpath, rename, rm, stat } from 'node:fs/promises';
import path from 'node:path';
import { LocalCheckupError } from '../errors.js';
/** The cf release the CLI installs; `integrity` is npm's `dist.integrity` for it. */
export const MANAGED_CF = Object.freeze({
version: '1.0.0-beta.13',
integrity: 'sha512-dCjpzCJvjudIu8IQbXCJfy9diFoyHU84D/30K4lJp7jUaBgvjJypsMFzLqvUFOuwhF1ggR/igsLqXyehAOR2FQ==',
/** Measured `du` of the install without optional dependencies (darwin-arm64: 73 MB). */
approxSize: 'about 75 MB',
});
export const NPM_INSTALL_TIMEOUT_MS = 10 * 60_000;
/** A staging directory older than this is a leftover, never a running install. */
const STALE_STAGING_MS = NPM_INSTALL_TIMEOUT_MS + 60_000;
const STAGING_PREFIX = '.staging-';
const NPM_STDERR_KEEP_BYTES = 64 * 1024;
const MAX_LOCKFILE_BYTES = 16 * 1024 * 1024;
export const NPM_STDERR_TAIL_LINES = 20;
/** A Vigilbase session code or verification token, or a command line naming `--email` (were a launcher to export one). */
const VIGILBASE_SECRET_MARKER = /vbc[sv]1_|--email\b/;
/** `<configDir>/cf`: every managed version, plus any leftover staging directory. */
export function managedCfRoot(configDir) {
return path.join(configDir, 'cf');
}
export function managedCfInstallDir(configDir, version = MANAGED_CF.version) {
return path.join(managedCfRoot(configDir), version);
}
export function managedCfPackageDir(installDir) {
return path.join(installDir, 'node_modules', 'cf');
}
/** The npm arguments after `process.execPath`. */
export function npmInstallArgv(npmCli, prefix, version = MANAGED_CF.version) {
return [npmCli, 'install', '--prefix', prefix, '--no-audit', '--no-fund', '--ignore-scripts', '--omit=optional', `cf@${version}`];
}
/**
* The npm to run: `npm_execpath` when it names an existing `npm-cli.js` (or
* `npx-cli.js`, whose sibling is used), otherwise the npm bundled beside this
* Node. `null` when neither exists.
*/
export function resolveNpmCli(options = { env: process.env }) {
const platform = options.platform ?? process.platform;
const p = platform === 'win32' ? path.win32 : path.posix;
const exists = options.exists ?? existsSync;
const fromEnv = options.env.npm_execpath;
if (fromEnv && p.isAbsolute(fromEnv)) {
const name = p.basename(fromEnv);
const candidate = name === 'npm-cli.js' ? fromEnv : name === 'npx-cli.js' ? p.join(p.dirname(fromEnv), 'npm-cli.js') : null;
if (candidate && exists(candidate))
return candidate;
}
const nodeDir = p.dirname(options.execPath ?? process.execPath);
const bundled = platform === 'win32'
? p.join(nodeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js')
: p.resolve(nodeDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js');
return exists(bundled) ? bundled : null;
}
/** The user's environment minus Cloudflare variables and anything carrying a session code, token or `--email`. */
export function npmChildEnv(parent) {
const env = {};
for (const [name, value] of Object.entries(parent)) {
if (typeof value !== 'string')
continue;
if (/^(?:CF_|CLOUDFLARE_)/i.test(name))
continue;
if (VIGILBASE_SECRET_MARKER.test(value))
continue;
env[name] = value;
}
env.npm_config_update_notifier = 'false';
return env;
}
async function readJsonCapped(file) {
const info = await stat(file).catch(() => null);
if (!info || !info.isFile() || info.size > MAX_LOCKFILE_BYTES)
return undefined;
try {
return JSON.parse(await readFile(file, 'utf8'));
}
catch {
return undefined;
}
}
/**
* True when every lockfile npm wrote records `node_modules/cf` at the pinned
* version and integrity (at least one must exist), and the installed
* package.json has that version.
*/
export async function verifyManagedInstall(installDir, pin = MANAGED_CF) {
let lockfiles = 0;
for (const file of [path.join(installDir, 'package-lock.json'), path.join(installDir, 'node_modules', '.package-lock.json')]) {
if (!(await stat(file).catch(() => null)))
continue;
lockfiles++;
const lock = await readJsonCapped(file);
const entry = lock?.packages?.['node_modules/cf'];
if (!entry || entry.version !== pin.version || entry.integrity !== pin.integrity)
return false;
}
if (lockfiles === 0)
return false;
const manifest = await readJsonCapped(path.join(managedCfPackageDir(installDir), 'package.json'));
return manifest?.version === pin.version;
}
/** The managed cf package directory when a verified install exists, else null. */
export async function findManagedCf(configDir) {
const installDir = managedCfInstallDir(configDir);
const info = await lstat(installDir).catch(() => null);
if (!info || !info.isDirectory() || info.isSymbolicLink())
return null;
return (await verifyManagedInstall(installDir)) ? managedCfPackageDir(installDir) : null;
}
/** Create `dir` 0700 if missing; refuse a symlink or non-directory in its place. */
export async function ensurePrivateDir(dir) {
await mkdir(path.dirname(dir), { recursive: true, mode: 0o700 });
await mkdir(dir, { mode: 0o700 }).catch((error) => {
if (error.code !== 'EEXIST')
throw error;
});
const info = await lstat(dir);
if (!info.isDirectory() || info.isSymbolicLink()) {
throw new Error(`${dir} must be a directory, not a symbolic link or file.`);
}
if (process.platform !== 'win32' && (info.mode & 0o777) !== 0o700)
await chmod(dir, 0o700);
}
/** A CF_INSTALL_FAILED error that carries the redacted tail of npm's stderr. */
export class CfInstallError extends LocalCheckupError {
npmOutput;
constructor(message, npmOutput = []) {
super('CF_INSTALL_FAILED', message);
this.npmOutput = npmOutput;
}
}
const SECRET_LINE = /_auth|_password|authorization|bearer|npm_[A-Za-z0-9]{20,}|vbc[sv]1_|--email\b/i;
/** The last lines of npm's stderr, dropping any line that could carry a registry credential. */
export function npmStderrTail(stderr, lines = NPM_STDERR_TAIL_LINES) {
return stderr.replace(/\r\n?/g, '\n').split('\n')
.map((line) => line.trimEnd())
.filter((line) => line.trim() !== '' && !SECRET_LINE.test(line))
.slice(-lines);
}
function runNpm(spawn, args, options) {
return new Promise((resolve) => {
let child;
try {
child = spawn(process.execPath, args, { shell: false, cwd: options.cwd, env: options.env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
}
catch {
resolve({ exitCode: null, stderr: '', timedOut: false, spawnFailed: true });
return;
}
options.onSpawn(child);
let stderr = '';
let timedOut = false;
let settled = false;
const timer = setTimeout(() => {
timedOut = true;
child.kill('SIGKILL');
}, options.timeoutMs);
timer.unref?.();
child.stdout?.on('data', () => undefined);
child.stderr?.on('data', (chunk) => {
stderr += chunk.toString();
if (stderr.length > NPM_STDERR_KEEP_BYTES)
stderr = stderr.slice(-NPM_STDERR_KEEP_BYTES);
});
const finish = (exitCode, spawnFailed) => {
if (settled)
return;
settled = true;
clearTimeout(timer);
resolve({ exitCode, stderr, timedOut, spawnFailed });
};
child.once('error', () => finish(null, true));
child.once('close', (code) => finish(typeof code === 'number' ? code : null, false));
});
}
/** Remove `.staging-*` leftovers older than `maxAgeMs`; a younger one may belong to a concurrent install. */
export async function removeStaleStaging(root, maxAgeMs = STALE_STAGING_MS, now = Date.now()) {
const names = await readdir(root).catch(() => []);
const removed = [];
for (const name of names.filter((candidate) => candidate.startsWith(STAGING_PREFIX)).sort()) {
const target = path.join(root, name);
const info = await lstat(target).catch(() => null);
if (!info || now - info.mtimeMs <= maxAgeMs)
continue;
await rm(target, { recursive: true, force: true }).then(() => removed.push(name), () => undefined);
}
return removed;
}
/**
* Ctrl-C while npm runs: kill npm, wait briefly for it to exit, remove the
* staging directory, then exit 130 (as `withCfRunner` does for cf).
*/
function onInterrupt(staging, running, before) {
return () => {
before?.();
const child = running();
const closed = child && child.exitCode === null && child.signalCode === null
? new Promise((resolve) => {
child.once('close', () => resolve());
setTimeout(resolve, 2_000).unref?.();
})
: Promise.resolve();
child?.kill('SIGKILL');
void closed
.then(() => rm(staging, { recursive: true, force: true, maxRetries: 3 }))
.catch(() => undefined)
.finally(() => process.exit(130));
};
}
/**
* Install the pinned cf into `<configDir>/cf/<version>/` and return its
* package directory. Throws CF_INSTALL_FAILED (a `CfInstallError`) when npm
* is missing or fails, or the result does not match the pin; the staging
* directory is always removed (on Ctrl-C too).
*/
export async function installManagedCf(options) {
const npmCli = options.npmCli !== undefined ? options.npmCli : resolveNpmCli({ env: options.env });
if (!npmCli) {
throw new CfInstallError('npm was not found beside this Node.js, so cf cannot be installed automatically.');
}
const root = managedCfRoot(options.configDir);
await ensurePrivateDir(root);
await removeStaleStaging(root);
// realpath: npm keys its lockfile relative to the real prefix, so a symlinked path would not match `node_modules/cf`.
const staging = await realpath(await mkdtemp(path.join(root, `${STAGING_PREFIX}${MANAGED_CF.version}-`)));
let npmChild = null;
const interrupted = onInterrupt(staging, () => npmChild, options.onInterrupt);
process.once('SIGINT', interrupted);
try {
if (process.platform !== 'win32')
await chmod(staging, 0o700);
const result = await runNpm(options.spawn ?? nodeSpawn, npmInstallArgv(npmCli, staging), {
cwd: staging,
env: npmChildEnv(options.env),
timeoutMs: options.timeoutMs ?? NPM_INSTALL_TIMEOUT_MS,
onSpawn: (child) => { npmChild = child; },
});
if (result.spawnFailed)
throw new CfInstallError('npm could not be started, so cf was not installed.');
if (result.timedOut)
throw new CfInstallError(`npm did not finish installing cf within ${Math.round((options.timeoutMs ?? NPM_INSTALL_TIMEOUT_MS) / 60_000)} minutes.`, npmStderrTail(result.stderr));
if (result.exitCode !== 0)
throw new CfInstallError(`npm could not install cf@${MANAGED_CF.version} (exit ${result.exitCode ?? 'signal'}).`, npmStderrTail(result.stderr));
if (!(await verifyManagedInstall(staging))) {
throw new CfInstallError(`The installed cf does not match the pinned cf@${MANAGED_CF.version} integrity; it was deleted.`);
}
const installDir = managedCfInstallDir(options.configDir);
// A leftover install that failed verification is replaced; a verified one (a concurrent run) is kept unless it is being replaced.
if (!options.replace && await findManagedCf(options.configDir))
return managedCfPackageDir(installDir);
await rm(installDir, { recursive: true, force: true });
try {
await rename(staging, installDir);
}
catch {
if (await findManagedCf(options.configDir))
return managedCfPackageDir(installDir);
throw new CfInstallError('The installed cf could not be moved into place.');
}
return managedCfPackageDir(installDir);
}
finally {
process.removeListener('SIGINT', interrupted);
await rm(staging, { recursive: true, force: true }).catch(() => undefined);
}
}
/** `logout`: remove every managed cf version (and any staging leftovers). */
export async function removeManagedCf(configDir) {
const root = managedCfRoot(configDir);
const info = await lstat(root).catch(() => null);
if (!info)
return { path: root, removed: false };
await rm(root, { recursive: true, force: true });
return { path: root, removed: true };
}