@vigilbase/cli
Version:
Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.
201 lines (200 loc) • 10.4 kB
JavaScript
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);
}