UNPKG

@vigilbase/cli

Version:

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

201 lines (200 loc) • 10.4 kB
import { homedir } from 'node:os'; import { isAbsolute, join } from 'node:path'; import { z } from 'zod'; import { CLI_VERSION } from '../version.js'; const DEFAULT_PUBLIC_BASE_URL = 'https://vigilbase.com'; const DEFAULT_ONE_BASE_URL = 'https://one.vigilbase.com'; /** Public client id accepted by One's device authorization plugin. */ const DEFAULT_CLIENT_ID = 'vigilbase-cli'; const originSchema = z.string().url().transform((raw) => new URL(raw)).refine((url) => (url.protocol === 'https:' || url.protocol === 'http:') && !url.username && !url.password && /^\/+$/.test(url.pathname) && !url.search && !url.hash, 'Expected an HTTP(S) origin without credentials, a path, query, or fragment.').transform((url) => url.origin); export function parseOrigin(raw, option) { const result = originSchema.safeParse(raw); if (!result.success) { // Do not echo the input: malformed URLs can contain credentials. throw new Error(`${option} must be an HTTP(S) origin without credentials, a path, query, or fragment.`); } return result.data; } const timeoutSchema = z.number().int().min(1).max(2_147_483_647); /** Default request timeout; also used when library callers omit `timeoutMs`. */ export const DEFAULT_TIMEOUT_MS = 60_000; /** localhost, 127.0.0.0/8 or [::1]: the hosts every origin option accepts for local development. */ export function isLoopbackHostname(hostname) { return hostname === 'localhost' || hostname === '[::1]' || /^127\.(?:\d{1,3}\.){2}\d{1,3}$/.test(hostname); } /** An option given as an empty string is a mistake (an unset shell variable), not a request for the default. */ function explicitValue(raw, option) { if (raw === undefined || raw === null) return undefined; if (!raw.trim()) throw new Error(`${option} is empty. Pass an origin or leave the option out.`); return raw.trim(); } /** The first set value with the name it came from; blank variables count as unset. */ function firstSet(sources) { for (const [name, value] of sources) if (value?.trim()) return [name, value.trim()]; return undefined; } export function resolvePublicBaseUrl(explicit) { const [name, raw] = firstSet([ ['--base-url', explicitValue(explicit, '--base-url')], ['VIGILBASE_BASE_URL', process.env.VIGILBASE_BASE_URL], ['VIGILBASE_API_URL', process.env.VIGILBASE_API_URL], ['VIGILBASE_PUBLIC_URL', process.env.VIGILBASE_PUBLIC_URL], ]) ?? ['--base-url', DEFAULT_PUBLIC_BASE_URL]; const origin = parseOrigin(raw, name); const url = new URL(origin); if (url.protocol !== 'https:' && !isLoopbackHostname(url.hostname)) { throw new Error(`${name} requires HTTPS; HTTP is allowed only for loopback development.`); } return origin; } export function resolveOneBaseUrl(explicit) { return validateOneOrigin(explicitValue(explicit, '--one-url') || process.env.VIGILBASE_ONE_URL?.trim() || DEFAULT_ONE_BASE_URL); } /** Validate at transport boundaries too: library callers may construct config objects. */ export function validateOneOrigin(raw) { const origin = parseOrigin(raw, '--one-url / VIGILBASE_ONE_URL'); const url = new URL(origin); if (url.protocol !== 'https:' && !isLoopbackHostname(url.hostname)) { throw new Error('--one-url / VIGILBASE_ONE_URL requires HTTPS; HTTP is allowed only for loopback development.'); } return origin; } /** Production Identity: the only issuer trusted unless --auth-url / VIGILBASE_AUTH_URL names another. */ export const DEFAULT_IDENTITY_ISSUER = 'https://auth.vigilbase.com'; /** Internal: accepted when named explicitly, but not offered to users (it isn't deployed publicly). */ export const STAGING_IDENTITY_ISSUER = 'https://identity-staging.vigilbase.com'; const IDENTITY_OPTION = '--auth-url / VIGILBASE_AUTH_URL'; function isIdentityLoopback(origin) { return isLoopbackHostname(new URL(origin).hostname); } export function resolveIdentityBaseUrl(raw) { const origin = parseOrigin(raw, IDENTITY_OPTION); if (origin !== DEFAULT_IDENTITY_ISSUER && origin !== STAGING_IDENTITY_ISSUER && !isIdentityLoopback(origin)) { throw new Error(`${IDENTITY_OPTION} must be ${DEFAULT_IDENTITY_ISSUER} or a loopback development origin.`); } return origin; } /** * The one Identity issuer this configuration trusts: --auth-url / VIGILBASE_AUTH_URL * when set, otherwise production. Staging is trusted only when configured explicitly. * Validated here too because library callers may construct config objects. */ export function trustedIdentityIssuer(config) { if (!config.identityBaseUrl) return DEFAULT_IDENTITY_ISSUER; const origin = resolveIdentityBaseUrl(config.identityBaseUrl); if (isIdentityLoopback(origin) && !isIdentityLoopback(validateOneOrigin(config.oneBaseUrl))) { throw new Error(`${IDENTITY_OPTION} can be a loopback origin only when --one-url / VIGILBASE_ONE_URL is loopback too.`); } return origin; } export function resolveClientId(explicit) { return (explicit?.trim() || process.env.VIGILBASE_CLIENT_ID?.trim() || DEFAULT_CLIENT_ID); } /** * VIGILBASE_CONFIG_DIR (or `explicit`), else $XDG_CONFIG_HOME/vigilbase, else ~/.config/vigilbase. A relative * XDG_CONFIG_HOME is ignored, as the XDG Base Directory spec says; a relative custom directory is returned as is. */ function configDirFromEnvironment(explicit, env) { const custom = explicit?.trim() || env.VIGILBASE_CONFIG_DIR?.trim(); if (custom) return custom; // Set but blank: '' is not absolute, so the store rejects it rather than falling back to another directory. if (env.VIGILBASE_CONFIG_DIR !== undefined) return ''; const xdg = env.XDG_CONFIG_HOME?.trim(); return xdg && isAbsolute(xdg) ? join(xdg, 'vigilbase') : join(homedir(), '.config', 'vigilbase'); } /** Why a config directory that isn't absolute was refused, naming the variable it came from. */ export function relativeConfigDirProblem(configDir, env = process.env) { if (!configDir && env.VIGILBASE_CONFIG_DIR !== undefined) { return { reason: 'VIGILBASE_CONFIG_DIR is empty', remedy: 'Set it to an absolute path or unset it.' }; } if (env.VIGILBASE_CONFIG_DIR === undefined && !isAbsolute(homedir()) && configDir === join(homedir(), '.config', 'vigilbase')) { const xdg = env.XDG_CONFIG_HOME?.trim() ? ', and a relative XDG_CONFIG_HOME is ignored' : ''; return { reason: `HOME (${homedir()}) is not an absolute path${xdg}`, remedy: 'Set HOME or VIGILBASE_CONFIG_DIR to an absolute path.' }; } return { reason: `${configDir} is not an absolute path`, remedy: 'Set VIGILBASE_CONFIG_DIR to an absolute path.' }; } /** The config directory, which must be absolute. */ export function resolveConfigDir(explicit, env = process.env) { const configDir = configDirFromEnvironment(explicit, env); if (!isAbsolute(configDir)) { const { reason, remedy } = relativeConfigDirProblem(configDir, env); throw new Error(`${reason}. ${remedy}`); } return configDir; } /** The origin when it is valid, otherwise the value as given: left for the transport that uses it to reject. */ function checkedLater(resolve, raw) { try { return resolve(); } catch { return raw; } } export function createConfig(options) { const checks = options?.checkOrigins ?? { public: true, one: true }; const explicitPublic = options?.publicBaseUrl ?? options?.baseUrl; const publicBaseUrl = checks.public ? resolvePublicBaseUrl(explicitPublic) : checkedLater(() => resolvePublicBaseUrl(explicitPublic), explicitPublic?.trim() || process.env.VIGILBASE_BASE_URL?.trim() || process.env.VIGILBASE_API_URL?.trim() || process.env.VIGILBASE_PUBLIC_URL?.trim() || DEFAULT_PUBLIC_BASE_URL); const oneBaseUrl = checks.one ? resolveOneBaseUrl(options?.oneBaseUrl) : checkedLater(() => resolveOneBaseUrl(options?.oneBaseUrl), options?.oneBaseUrl?.trim() || process.env.VIGILBASE_ONE_URL?.trim() || DEFAULT_ONE_BASE_URL); const authUrl = checks.one ? explicitValue(options?.identityBaseUrl, '--auth-url') || process.env.VIGILBASE_AUTH_URL?.trim() : options?.identityBaseUrl?.trim() || process.env.VIGILBASE_AUTH_URL?.trim(); const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS; if (!timeoutSchema.safeParse(timeoutMs).success) { throw new Error('Request timeout must be an integer between 1 and 2147483647 milliseconds.'); } return { publicBaseUrl, /** Alias used by the current public tools client. */ baseUrl: publicBaseUrl, oneBaseUrl, ...(authUrl ? { identityBaseUrl: checks.one ? resolveIdentityBaseUrl(authUrl) : checkedLater(() => resolveIdentityBaseUrl(authUrl), authUrl) } : {}), clientId: resolveClientId(options?.clientId), configDir: configDirFromEnvironment(options?.configDir, process.env), userAgent: `@vigilbase/cli/${CLI_VERSION} (+https://vigilbase.com; node/${process.version})`, timeoutMs, }; } /** * Config for `vigilbase local …` and `vigilbase cloudflare-checkup`. Local * checkups resolve their own submit origin (resolveSubmitOrigin) and never * read VIGILBASE_* URLs. They resolve the config directory themselves, and * only when they need it (the managed cf install, the default snapshot path), * so malformed values cannot block offline commands such as `inspect`. */ export function createLocalCommandConfig(timeoutMs) { const timeout = timeoutMs ?? DEFAULT_TIMEOUT_MS; if (!timeoutSchema.safeParse(timeout).success) { throw new Error('Request timeout must be an integer between 1 and 2147483647 milliseconds.'); } return { publicBaseUrl: DEFAULT_PUBLIC_BASE_URL, baseUrl: DEFAULT_PUBLIC_BASE_URL, oneBaseUrl: DEFAULT_ONE_BASE_URL, clientId: DEFAULT_CLIENT_ID, // Unused by local commands; never read from the environment. configDir: join(homedir(), '.config', 'vigilbase'), userAgent: `@vigilbase/cli/${CLI_VERSION} (+https://vigilbase.com; node/${process.version})`, timeoutMs: timeout, }; } /** Back-compat helper used by older call sites / tests. */ export function resolveBaseUrl(explicit) { return resolvePublicBaseUrl(explicit); }