UNPKG

@vigilbase/cli

Version:

Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.

228 lines (227 loc) • 11.4 kB
/** * Locating Cloudflare's cf CLI and checking its identity (spec §F2). * * Candidates, in order: `--cf-bin`; the Vigilbase-managed install * (`<configDir>/cf/<version>`, see install.ts); absolute PATH entries holding * `cf`, then `cloudflare` (`cf.cmd` / `cloudflare.cmd` on Windows); then pnpm * and Volta global package directories, probed without spawning a package * manager. A * candidate is accepted only if its package.json says it is Cloudflare's cf * (`name`, `repository`, `bin.cf`); the Cloud Foundry CLI, which also installs * `cf`, and unresolvable shims are skipped. cf is then run as * `process.execPath <bin.cf entry>`, never through a shim or shell. */ import { promises as fsPromises } from 'node:fs'; import { homedir } from 'node:os'; import path from 'node:path'; import { CF_SUPPORTED, isSupportedCfVersion } from '../contract.js'; import { LocalCheckupError } from '../errors.js'; const nodeFs = { stat: (target) => fsPromises.stat(target), realpath: (target) => fsPromises.realpath(target), readFile: (target) => fsPromises.readFile(target, 'utf8'), readdir: (target) => fsPromises.readdir(target), }; const MIN_NODE = [22, 18]; const MAX_PACKAGE_JSON_BYTES = 1024 * 1024; const MAX_WALK_UP = 4; const MAX_PACKAGE_MANAGER_PROBES = 20; const MAX_VOLTA_DEPTH = 4; export function isNodeSupported(version = process.versions.node) { const [major, minor] = version.split('.').map((part) => Number.parseInt(part, 10)); if (!Number.isInteger(major) || !Number.isInteger(minor)) return false; return major > MIN_NODE[0] || (major === MIN_NODE[0] && minor >= MIN_NODE[1]); } /** cf runs under this Node via process.execPath; workerd ships no win32-arm64 build. */ export function assertLocalRuntimeSupported(version = process.versions.node, platform = process.platform, arch = process.arch) { if (!isNodeSupported(version)) { throw new LocalCheckupError('NODE_UNSUPPORTED', `Node.js ${version} is too old for cf; local checkups need Node.js 22.18 or newer.`); } if (!['darwin', 'linux', 'win32'].includes(platform) || (platform === 'win32' && arch === 'arm64')) { throw new LocalCheckupError('NODE_UNSUPPORTED', `cf does not support ${platform}-${arch}. Use macOS, Linux or Windows x64.`); } } function envLookup(env, platform, name) { if (platform !== 'win32') return env[name]; const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name.toUpperCase()); return key === undefined ? undefined : env[key]; } function repositoryUrl(repository) { if (typeof repository === 'string') return repository; if (repository && typeof repository === 'object' && typeof repository.url === 'string') { return repository.url; } return ''; } export async function locateCf(options) { const platform = options.platform ?? process.platform; const p = platform === 'win32' ? path.win32 : path.posix; const fs = options.fs ?? nodeFs; const cwd = options.cwd ?? process.cwd(); const home = options.home ?? (envLookup(options.env, platform, platform === 'win32' ? 'USERPROFILE' : 'HOME') || homedir()); const cwdIsHome = Boolean(home) && p.isAbsolute(home) && p.relative(home, cwd) === ''; let sawOther = false; const isFile = async (target) => (await fs.stat(target).catch(() => null))?.isFile() === true; const isDir = async (target) => (await fs.stat(target).catch(() => null))?.isDirectory() === true; const verifyPackage = async (packageDir, expectedEntry) => { const text = await fs.readFile(p.join(packageDir, 'package.json')).catch(() => null); if (text === null) return { ok: false, reason: 'missing' }; if (text.length > MAX_PACKAGE_JSON_BYTES) return { ok: false, reason: 'impostor' }; let manifest; try { manifest = JSON.parse(text); } catch { return { ok: false, reason: 'impostor' }; } const binCf = manifest.bin && typeof manifest.bin === 'object' ? manifest.bin.cf : undefined; if (manifest.name !== 'cf' || !repositoryUrl(manifest.repository).includes('github.com/cloudflare/cf') || typeof binCf !== 'string' || typeof manifest.version !== 'string') { return { ok: false, reason: 'impostor' }; } const entry = p.resolve(packageDir, binCf); const relative = p.relative(packageDir, entry); if (!relative || relative.startsWith('..') || p.isAbsolute(relative) || !(await isFile(entry))) { return { ok: false, reason: 'impostor' }; } if (expectedEntry !== undefined) { const realEntry = await fs.realpath(entry).catch(() => entry); if (realEntry !== expectedEntry) return { ok: false, reason: 'impostor' }; } return { ok: true, packageDir, entry, version: manifest.version }; }; /** A POSIX bin (symlink or file): resolve it and walk up to the nearest package.json. */ const fromPosixBin = async (bin) => { const real = await fs.realpath(bin).catch(() => null); if (real === null) return { ok: false, reason: 'missing' }; let dir = p.dirname(real); for (let depth = 0; depth < MAX_WALK_UP; depth++) { if (await isFile(p.join(dir, 'package.json'))) return verifyPackage(dir, real); const parent = p.dirname(dir); if (parent === dir) break; dir = parent; } return { ok: false, reason: 'missing' }; }; const accept = (verified, source) => { const relative = p.relative(cwd, verified.packageDir); const inside = relative === '' || (!relative.startsWith('..') && !p.isAbsolute(relative)); return { packageDir: verified.packageDir, entry: verified.entry, packageVersion: verified.version, source, insideCwd: inside && source !== 'managed' && !cwdIsHome, }; }; // 1. --cf-bin: the package directory or its bin/cf. An explicit path is not // silently replaced by another cf. if (options.cfBin !== undefined) { if (!p.isAbsolute(options.cfBin)) throw new Error('--cf-bin must be an absolute path.'); let verified; if (await isDir(options.cfBin)) verified = await verifyPackage(options.cfBin); else if (platform === 'win32') verified = await verifyPackage(p.dirname(p.dirname(options.cfBin))); else verified = await fromPosixBin(options.cfBin); if (verified.ok) return accept(verified, 'cf-bin'); if (verified.reason === 'missing' && !(await fs.stat(options.cfBin).catch(() => null))) { throw new LocalCheckupError('CF_NOT_FOUND', 'No file or directory exists at --cf-bin.'); } throw new LocalCheckupError('CF_WRONG_BINARY', '--cf-bin does not point to Cloudflare\'s cf package (name "cf", repository github.com/cloudflare/cf).'); } // 2. The Vigilbase-managed install; an unusable one falls through to the other candidates. if (options.managedDir !== undefined && p.isAbsolute(options.managedDir)) { const verified = await verifyPackage(options.managedDir); if (verified.ok) return accept(verified, 'managed'); } // 3. Absolute PATH entries; `cf` everywhere first, then `cloudflare`. const pathValue = envLookup(options.env, platform, 'PATH') ?? ''; const dirs = [...new Set(pathValue.split(platform === 'win32' ? ';' : ':'))] .filter((dir) => dir !== '' && dir !== '.' && p.isAbsolute(dir)); const names = platform === 'win32' ? ['cf.cmd', 'cloudflare.cmd'] : ['cf', 'cloudflare']; for (const name of names) { for (const dir of dirs) { const hit = p.join(dir, name); if (!(await isFile(hit))) continue; // npm's Windows global layout keeps the package beside the shim. const verified = platform === 'win32' ? await verifyPackage(p.join(dir, 'node_modules', 'cf')) : await fromPosixBin(hit); if (verified.ok) return accept(verified, 'path'); // Cloud Foundry's cf, or a shim (pnpm) without a resolvable package: keep searching. sawOther = true; } } // 4. Package-manager global directories, at most 20 filesystem probes. let probes = 0; const probe = async (work, fallback) => { if (probes >= MAX_PACKAGE_MANAGER_PROBES) return fallback; probes++; return work().catch(() => fallback); }; const pnpmHome = envLookup(options.env, platform, 'PNPM_HOME'); if (pnpmHome && p.isAbsolute(pnpmHome)) { const globalDir = p.join(pnpmHome, 'global'); const versions = await probe(() => fs.readdir(globalDir), []); for (const version of versions.sort()) { const packageDir = p.join(globalDir, version, 'node_modules', 'cf'); if (!(await probe(() => isFile(p.join(packageDir, 'package.json')), false))) continue; const verified = await verifyPackage(packageDir); if (verified.ok) return accept(verified, 'pnpm'); sawOther = true; } } const voltaHome = envLookup(options.env, platform, 'VOLTA_HOME'); if (voltaHome && p.isAbsolute(voltaHome)) { let frontier = [p.join(voltaHome, 'tools', 'image', 'packages', 'cf')]; for (let depth = 0; depth <= MAX_VOLTA_DEPTH && frontier.length > 0; depth++) { const next = []; for (const dir of frontier) { if (p.basename(dir) === 'cf' && p.basename(p.dirname(dir)) === 'node_modules' && await probe(() => isFile(p.join(dir, 'package.json')), false)) { const verified = await verifyPackage(dir); if (verified.ok) return accept(verified, 'volta'); sawOther = true; continue; } const children = await probe(() => fs.readdir(dir), []); for (const child of children.sort()) next.push(p.join(dir, child)); } frontier = next; } } if (sawOther) { throw new LocalCheckupError('CF_WRONG_BINARY', 'A program named cf was found, but it is not Cloudflare\'s cf CLI (for example the Cloud Foundry CLI or an unresolvable shim).'); } throw new LocalCheckupError('CF_NOT_FOUND', 'Cloudflare\'s cf CLI was not found on PATH or in the pnpm and Volta global directories.'); } /** Gate the version cf prints against its package.json and `CF_SUPPORTED`. */ export function checkCfVersion(printed, packageVersion) { const version = printed.trim(); if (version !== packageVersion) { throw new LocalCheckupError('CF_WRONG_BINARY', 'cf --version does not match the version in its package.json.'); } if (!isSupportedCfVersion(version)) { throw new LocalCheckupError('CF_UNSUPPORTED_VERSION', `cf ${version.slice(0, 40)} is not supported; local checkups need cf >= ${CF_SUPPORTED.min} and < ${CF_SUPPORTED.maxExclusive}.`); } return { version, tested: CF_SUPPORTED.tested.includes(version) }; }