// Bus-only in-memory management client. Values never enter receipts or errors. import http from 'node:http'; import { ProxyError, refuse, sha256, regularBytes, validManagementKey } from './common.mjs'; import { lstatSync } from 'node:fs'; import { join } from 'node:path'; const KEY_PATH = '/v8/management/config/access/api-keys'; const validKeys = value => Array.isArray(value) && value.length <= 100000 && value.every(k => typeof k === 'string' && k.length > 0 && k.length <= 4096 && !/[\r\n\x00]/.test(k)); export class ManagementClient { #base; #key; #receipt; #timeout; constructor({ baseUrl, key, receipt = () => {}, timeout = 5000 }) { let u; try { u = new URL(baseUrl); } catch { refuse('proxy-client-invalid'); } if (u.protocol !== 'http:' || u.hostname !== '127.0.0.1' || !u.port || Number(u.port) < 1024 || u.port === '8317' || u.username || u.password || u.pathname !== '/' || u.search || u.hash || !validManagementKey(key) || typeof receipt !== 'function' || !Number.isSafeInteger(timeout) || timeout <= 0) refuse('proxy-client-invalid'); this.#base = u.origin; this.#key = key; this.#receipt = receipt; this.#timeout = timeout; } #request(method, endpoint, payload, onStatus) { // node:http addresses the fixed numeric loopback directly. An ambient fetch // dispatcher or HTTP_PROXY cannot send the Authorization header elsewhere. let deadline; return new Promise((resolve, reject) => { let received = false; const req = http.request(this.#base + endpoint, { method, headers: { Authorization: `Bearer ${this.#key}`, ...(payload !== undefined ? { 'Content-Type': 'application/json' } : {}) }, }, response => { received = true; onStatus(response.statusCode); const error = response.statusCode >= 300 && response.statusCode < 400 ? 'proxy-client-redirect' : response.statusCode < 200 || response.statusCode >= 300 ? 'proxy-client-refused' : null; if (error) { response.destroy(); reject(new ProxyError(error)); return; } let size = 0; const chunks = []; response.on('data', chunk => { size += chunk.length; if (size > 8 * 1024 * 1024) { response.destroy(); reject(new ProxyError('proxy-client-response')); } else chunks.push(chunk); }); response.on('aborted', () => reject(new ProxyError('proxy-client-response'))); response.on('error', () => reject(new ProxyError('proxy-client-response'))); response.on('end', () => { try { resolve(JSON.parse(Buffer.concat(chunks))); } catch { reject(new ProxyError('proxy-client-response')); } }); }); req.on('error', () => reject(new ProxyError(received ? 'proxy-client-response' : 'proxy-client-down'))); deadline = setTimeout(() => { req.destroy(); reject(new ProxyError('proxy-client-down')); }, this.#timeout); req.end(payload === undefined ? undefined : JSON.stringify(payload)); }).finally(() => clearTimeout(deadline)); } async #call(method, keys) { let status = 0, ids = []; try { const body = await this.#request(method, KEY_PATH, keys, value => { status = value; }); if (method === 'GET') { if (!validKeys(body)) refuse('proxy-client-response'); ids = body.map(sha256).map(h => h.slice(0, 12)); return body; } if (!body || body.status !== 'ok') refuse('proxy-client-response'); ids = keys.map(sha256).map(h => h.slice(0, 12)); return { ok: true }; } finally { this.#receipt({ call: `${method} ${KEY_PATH}`, status, keyIds: ids }); } } getKeys() { return this.#call('GET'); } putKeys(keys) { if (!validKeys(keys)) refuse('proxy-client-invalid'); return this.#call('PUT', keys); } // Fixed management operations for the bus's native page. Queries contain login // states; neither queries nor request/response bodies belong in a receipt. async #page(method, endpoint, payload, expectedStatus = null) { let status = 0; try { const answer = await this.#request(method, endpoint, payload, value => { status = value; }); if (expectedStatus !== null && status !== expectedStatus) refuse('proxy-client-refused'); return answer; } catch (error) { if (method === 'GET' && endpoint === '/v8/management/config/api-keys/claude' && status === 404) return []; throw error; } finally { this.#receipt({ call: `${method} ${endpoint.split('?')[0]}`, status, keyIds: [] }); } } // The queue drains at most 100 events. Credential-bearing strings exist only // inside this bounded read, never in a plug-in result, receipt, store or view. async readUsageQueue() { const events = await this.#page('GET', '/v8/management/observability/usage/queue?count=100'); if (!Array.isArray(events) || events.length > 100) refuse('proxy-client-response'); const string = v => typeof v === 'string' && v.length > 0 && v.length <= 4096 && !/[\r\n\x00]/.test(v); const amount = v => v === undefined ? 0 : Number.isSafeInteger(v) && v >= 0 ? v : refuse('proxy-client-response'); return events.map(event => { if (!event || !string(event.api_key) || !string(event.source) || typeof event.failed !== 'boolean' || !event.tokens || typeof event.tokens !== 'object' || Array.isArray(event.tokens)) refuse('proxy-client-response'); const statusCode = event.fail?.status_code ?? null; if (statusCode !== null && (!Number.isSafeInteger(statusCode) || statusCode < 0 || statusCode > 599)) refuse('proxy-client-response'); return { keyId: sha256(event.api_key).slice(0, 12), accountId: sha256(event.source).slice(0, 12), managed: event.api_key.startsWith('msk-'), failed: event.failed, statusCode, input: amount(event.tokens.input_tokens), output: amount(event.tokens.output_tokens), cacheWrite: amount(event.tokens.cache_creation_tokens), cacheRead: amount(event.tokens.cache_read_tokens) }; }); } getCredentials() { return this.#page('GET', '/v8/management/credentials'); } getCredentialModels(name) { if (typeof name !== 'string' || !name || name.length > 512 || /[\r\n\x00]/.test(name)) refuse('proxy-client-invalid'); return this.#page('GET', `/v8/management/credentials/models?name=${encodeURIComponent(name)}`); } getUpstreamGroups() { return this.#page('GET', '/v8/management/config/api-keys/claude'); } putUpstreamGroups(groups) { // auth_index is derived response metadata, not writable provider config. const payload = groups.map(group => ({ ...group, keys: group.keys.map(({ auth_index, ...key }) => key) })); return this.#page('PUT', '/v8/management/config/api-keys/claude', payload, 200); } patchCredential(name, priority) { return this.#page('PATCH', '/v8/management/credentials/fields', { name, priority }, 200); } oauthStart(provider) { if (!['claude', 'codex'].includes(provider)) refuse('proxy-client-invalid'); return this.#page('GET', `/v8/management/oauth/auth-url?provider=${provider}`); } oauthCallback(redirect_url) { return this.#page('POST', '/v8/management/oauth/callback', { redirect_url }); } oauthStatus(state) { return this.#page('GET', `/v8/management/oauth/status?state=${encodeURIComponent(state)}`); } oauthCancel(state) { return this.#page('DELETE', `/v8/management/oauth/session?state=${encodeURIComponent(state)}`); } // Client-key authentication only. Never send the management key to the model route, // follow a Location, consume/log the response body or trust an ambient dispatcher. async probeKey(key, { timeout = this.#timeout } = {}) { if (!validManagementKey(key) || !Number.isFinite(timeout) || timeout <= 0) refuse('proxy-client-invalid'); let timer, status = 0; try { return await new Promise((resolve, reject) => { const req = http.request(this.#base + '/v1/models', { method: 'GET', headers: { Authorization: `Bearer ${key}` } }, response => { status = response.statusCode; response.destroy(); resolve(status); }); req.on('error', () => reject(new ProxyError('proxy-down'))); timer = setTimeout(() => { req.destroy(); reject(new ProxyError('proxy-down')); }, timeout); req.end(); }); } finally { clearTimeout(timer); this.#receipt({ call: 'GET /v1/models', status, keyIds: [sha256(key).slice(0, 12)] }); } } } // Trusted bus boot only: no CLI/Console/session exports this value or client. export async function loadManagementClient({ configDir, proxy, receipt }) { const file = join(configDir, 'secrets', 'proxy-management.key'); let key; try { const stat = lstatSync(file); if ((stat.mode & 0o777) !== 0o600 || !stat.isFile() || stat.isSymbolicLink()) refuse('proxy-management-key-invalid'); key = regularBytes(file, 'proxy-management-key-invalid').toString('utf8'); if (!validManagementKey(key)) refuse('proxy-management-key-invalid'); } catch { refuse('proxy-management-key-invalid'); } return new ManagementClient({ baseUrl: `http://127.0.0.1:${proxy.port}`, key, receipt }); }