import { join } from 'node:path'; import { configDir } from '../../organization/src/organization-config.mjs'; import { createProxyLock } from '../../bus/src/proxy-lock.mjs'; import { loadManagementClient } from '../../model-proxy/src/client.mjs'; import { CliError } from './errors.mjs'; import { orgSelector } from './org-selector.mjs'; import { refuseInsideAgent } from './transport.mjs'; import { fetchPinned } from '../../model-proxy/src/fetch.mjs'; import { validManagementKey, proxyTiming } from '../../model-proxy/src/common.mjs'; const failure = (code, exitCode = 2) => Object.assign(new CliError(code, exitCode), { code }); // A raw terminal suppresses echo; input is never argv, env, readline history or stdout. export function promptProxyKey(io, label) { if (!io.stdin.isTTY || !io.stdout.isTTY || typeof io.stdin.setRawMode !== 'function') throw failure('proxy-terminal-required', 3); io.stderr.write(label); const wasRaw = io.stdin.isRaw; return new Promise((resolve, reject) => { let value = ''; const finish = (error) => { io.stdin.off('data', data); io.stdin.off('end', end); io.stdin.off('error', failed); io.stdin.setRawMode(wasRaw); io.stdin.pause(); io.stderr.write('\n'); error ? reject(error) : resolve(value); }; const end = () => finish(failure('proxy-key-invalid')); const failed = () => finish(failure('proxy-key-invalid')); const data = chunk => { for (const char of chunk.toString()) { if (char === '\u0003' || char === '\u0004') return finish(failure('proxy-key-invalid')); if (char === '\r' || char === '\n') return finish(); if (char === '\u007f' || char === '\b') value = value.slice(0, -1); else value += char; if (value.length > 4096) return finish(failure('proxy-key-invalid')); } }; io.stdin.setRawMode(true); io.stdin.on('data', data); io.stdin.once('end', end); io.stdin.once('error', failed); io.stdin.resume(); }); } export async function proxyCommand(args, io, { system, callFor, prompt = promptProxyKey, fetch = fetchPinned }) { const [verb, ...rest] = args; if (args.includes('--tester')) throw failure('human-required', 3); if (verb === 'init' || verb === 'fetch' || verb === 'fence') { try { refuseInsideAgent(io.env); } catch { throw failure('human-required', 3); } } if (verb === 'fence') { const [action, ...flags] = rest; if (action === 'clear' && flags.length === 0) throw failure('proxy-restart-required'); if (!(action === 'status' && (flags.length === 0 || (flags.length === 1 && flags[0] === '--json'))) && !(action === 'clear' && flags.length === 1 && flags[0] === '--proxy-restarted')) throw failure('proxy-usage', 4); try { const config = typeof system === 'function' ? system() : system; if (!config.proxy) throw failure('proxy-config-invalid'); const dir = configDir(io.env); const lock = createProxyLock({ stateDir: join(config.dataRoot, 'proxy'), identity: `${dir}:${config.proxy.port}` }); let result; if (action === 'status') result = { uncertain: lock.status() }; else { const client = await loadManagementClient({ configDir: dir, proxy: config.proxy }); result = await lock.clear({ restarted: true, read: () => client.getKeys() }); } io.stdout.write(JSON.stringify(result) + '\n'); return; } catch (error) { if (error instanceof CliError) throw error; throw failure(/^proxy-[a-z-]+$/.test(error.code ?? '') ? error.code : 'proxy-down'); } } if (!['fetch', 'init'].includes(verb)) throw failure('proxy-usage', 4); const parsed = { flags: new Set(), values: {} }; let port; // --org and --business are one selector in this parser's scope (rename design r2). const selector = orgSelector(() => failure('proxy-usage', 4)); for (let i = 0; i < rest.length; i++) { const flag = rest[i]; if (verb === 'init' && selector.owns(flag)) { selector.take(flag, rest[i + 1]); i++; continue; } if (verb !== 'init' || !['--port', '--rotate-grace-minutes', '--first-output-seconds'].includes(flag) || parsed.values[flag] !== undefined || !rest[i + 1]) throw failure('proxy-usage', 4); parsed.values[flag] = rest[++i]; } if (selector.given) parsed.values['--business'] = selector.value; if (parsed.values['--port'] !== undefined) { if (!/^[0-9]+$/.test(parsed.values['--port'])) throw failure('proxy-config-invalid'); port = Number(parsed.values['--port']); if (port < 1024 || port > 65535 || port === 8317) throw failure('proxy-config-invalid'); } const timing = {}; for (const [flag, field] of [['--rotate-grace-minutes', 'rotateGraceMinutes'], ['--first-output-seconds', 'firstOutputSeconds']]) if (parsed.values[flag] !== undefined) timing[field] = /^[0-9]+$/.test(parsed.values[flag]) ? Number(parsed.values[flag]) : NaN; try { proxyTiming(timing); } catch (error) { throw failure(error.code); } try { const config = typeof system === 'function' ? system() : system; if (verb === 'fetch') { const result = await fetch({ dataRoot: config.dataRoot }); io.stdout.write(JSON.stringify({ binary: result.binary, panel: result.panel }) + '\n'); return; } if (!io.stdin.isTTY || !io.stdout.isTTY) throw failure('proxy-terminal-required', 3); // Establish that the CLI is eligible before reading sensitive input. The bus verifies the proof. const call = callFor(parsed); const first = await prompt(io, 'Management key: '), second = await prompt(io, 'Repeat management key: '); if (first !== second) throw failure('proxy-key-mismatch'); if (!validManagementKey(first)) throw failure('proxy-key-invalid'); const result = await call('proxy.init', { key: first, ...(port === undefined ? {} : { port }), ...timing }); io.stdout.write(JSON.stringify(result) + '\n'); } catch (e) { if (e instanceof CliError) throw e; throw failure(/^proxy-[a-z-]+$/.test(e.code ?? '') ? e.code : verb === 'fetch' ? 'proxy-fetch-failed' : 'proxy-init-failed'); } }