Files
stack/packages/bus/src/proxy-page.mjs

238 lines
15 KiB
JavaScript

// P4: bus-only native Console operations. No upstream credential is persisted here.
import { randomBytes } from 'node:crypto';
import { isDeepStrictEqual } from 'node:util';
import { sha256, refuse } from '../../model-proxy/src/common.mjs';
import { PROXY_PAGE_CODES } from './proxy-page-vocabulary.mjs';
import { ZAI_URL, MODEL_ID } from '../../plugins/model-provider/cliproxy/index.mjs';
export { PROXY_PAGE_VERBS, PROXY_PAGE_CODES } from './proxy-page-vocabulary.mjs';
const object = x => x !== null && typeof x === 'object' && !Array.isArray(x);
function fields(args, names) {
if (!object(args) || Object.keys(args).length !== names.length || names.some(k => !Object.hasOwn(args, k))) refuse('invalid-request');
}
const priorityOk = n => Number.isInteger(n) && n >= -1000 && n <= 1000;
const identifier = x => typeof x === 'string' && x.length > 0 && x.length <= 512;
const stateOk = x => typeof x === 'string' && /^[a-zA-Z0-9_-]{1,512}$/.test(x);
const keyId = value => sha256(value).slice(0, 12);
const providerName = provider => ({ claude: 'Claude', codex: 'Codex', 'z.ai': 'z.ai', other: 'Other' })[provider];
// Credential labels/emails and group names are arbitrary upstream strings. A stable
// generated display name provides identification without reflecting a misplaced token.
function view(id, provider, status, priority) {
return { accountId: keyId(id), provider, displayName: `${providerName(provider)} account ${keyId(id)}`,
status, priority: priorityOk(priority) ? priority : 0 };
}
export class ProxyPage {
#client; #tag; #records; #lock; #org; #dirty = false; #now; #timeout; #onChange; #pending = new Map(); #writes = Promise.resolve();
constructor({ client, instanceTag, keyRecords = () => [], now = Date.now, loginTimeoutSeconds = 300, onChange = () => {}, lock = null, org = null }) {
if (!client || !/^[a-zA-Z0-9_-]{1,64}$/.test(instanceTag) || typeof keyRecords !== 'function' || typeof now !== 'function'
|| typeof onChange !== 'function' || !Number.isSafeInteger(loginTimeoutSeconds) || loginTimeoutSeconds <= 0) throw new TypeError('invalid proxy page settings');
this.#lock = lock; this.#org = org;
this.#client = client; this.#tag = instanceTag; this.#records = keyRecords; this.#now = now; this.#timeout = loginTimeoutSeconds * 1000; this.#onChange = onChange;
}
// Serializes read-modify-write provider config within the bus. P2 owns the
// independent client-key list. Store's lock admits only one bus per data root.
#write(operation) {
const next = this.#writes.then(() => {
const run = async () => {
this.#dirty = false; this.#onChange('begin');
try { const result = await operation(); if (this.#dirty) this.#lock.confirm(); return result; }
finally { this.#onChange('end'); }
};
return this.#lock ? this.#lock.run(run) : run();
}); this.#writes = next.catch(() => {}); return next;
}
#beforeMutation(op) { if (this.#lock) { this.#lock.fence({ org: this.#org, op }); this.#dirty = true; } }
async #accounts() {
const response = await this.#client.getCredentials();
const groups = await this.#client.getUpstreamGroups();
if (!Array.isArray(response?.files) || !Array.isArray(groups)) refuse('proxy-down');
const rows = [];
for (const row of response.files) {
if (!identifier(row?.id) || !identifier(row?.name)) continue;
const provider = ['claude', 'codex'].includes(row.provider) ? row.provider : 'other';
const status = row.disabled === true ? 'disabled' : row.unavailable === true ? 'unavailable'
: ['ready', 'active', 'error', 'disabled', 'unavailable'].includes(row.status) ? row.status : 'unknown';
rows.push({ ...view(row.id, provider, status, row.priority), source: 'oauth', name: row.name });
}
for (const [groupIndex, group] of groups.entries()) {
if (!Array.isArray(group?.keys)) continue;
for (const [keyIndex, key] of group.keys.entries()) {
// auth_index is generated by the proxy. Never fall back to hashing api-key.
if (!identifier(key?.auth_index)) continue;
const provider = group['base-url'] === ZAI_URL ? 'z.ai' : 'other';
const status = key.disabled === true || group.disabled === true ? 'disabled' : 'active';
const configured = group.models ?? [];
if (!Array.isArray(configured) || configured.length > 32 || configured.some(m => !m || typeof (m.alias || m.name) !== 'string' || !MODEL_ID.test(m.alias || m.name))) refuse('proxy-down');
rows.push({ ...view(key.auth_index, provider, status, key.priority ?? group.priority), source: 'apikey', groupIndex, keyIndex, models: configured.map(m => m.alias || m.name) });
}
}
return { rows, groups };
}
#public(row) {
const { accountId, provider, displayName, status, priority } = row;
return { accountId, provider, displayName, status, priority, ...(row.source === 'apikey' && provider === 'z.ai' ? { models: row.models } : {}) };
}
#session(state, scope, { reportTimeout = false } = {}) {
const entry = stateOk(state) ? this.#pending.get(state) : null;
if (!entry || entry.business !== scope.business || entry.human !== scope.human) refuse('login-state-unknown');
if (entry.expires <= this.#now()) {
this.#pending.delete(state);
if (reportTimeout) return null;
refuse('login-state-unknown');
}
return entry;
}
async request(verb, args, scope) {
try { return await this.#request(verb, args, scope); }
catch (error) {
// A client or fixture may put request values in its errors. Only locally
// enumerated refusal codes cross the broker, never a raw exception/body.
try { this.#lock?.assertCertain(); } catch { refuse('proxy-uncertain'); }
refuse(PROXY_PAGE_CODES.includes(error.code) ? error.code : 'proxy-down');
}
}
async #request(verb, args, scope) {
if (verb === 'proxy.accounts') {
fields(args, []);
return { accounts: (await this.#accounts()).rows.map(row => this.#public(row)) };
}
if (verb === 'proxy.login.start') {
fields(args, ['provider']);
if (!['claude', 'codex'].includes(args.provider)) refuse('provider-not-allowed');
for (const [state, value] of this.#pending) if (value.expires <= this.#now()) this.#pending.delete(state);
if (this.#pending.size >= 128) refuse('proxy-down');
const answer = await (this.#lock ? this.#write(() => this.#client.oauthStart(args.provider)) : this.#client.oauthStart(args.provider));
let url; try { url = new URL(answer.url); } catch { refuse('proxy-down'); }
const host = args.provider === 'claude' ? 'claude.ai' : 'auth.openai.com';
if (url.protocol !== 'https:' || url.hostname !== host || url.username || url.password || url.port
|| url.href.length > 8192 || !stateOk(answer.state) || url.searchParams.getAll('state').length !== 1
|| url.searchParams.get('state') !== answer.state || this.#pending.has(answer.state)) refuse('proxy-down');
this.#pending.set(answer.state, { business: scope.business, human: scope.human, expires: this.#now() + this.#timeout });
return { url: url.href, state: answer.state };
}
if (verb === 'proxy.login.submit') {
fields(args, ['redirect_url']);
let url;
try {
if (typeof args.redirect_url !== 'string' || args.redirect_url.length > 8192) throw Error();
url = new URL(args.redirect_url);
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash || url.searchParams.getAll('state').length !== 1) throw Error();
} catch { refuse('callback-url-invalid'); }
const state = url.searchParams.get('state'); this.#session(state, scope);
await (this.#lock ? this.#write(async () => {
this.#beforeMutation('proxy.login.submit');
await this.#client.oauthCallback(args.redirect_url);
const status = await this.#client.oauthStatus(state);
if (!['wait', 'ok', 'error'].includes(status?.status)) refuse('proxy-down');
}) : this.#client.oauthCallback(args.redirect_url));
// Callback acceptance is explicitly not proof of credential persistence.
return { status: 'wait' };
}
if (verb === 'proxy.login.status') {
fields(args, ['state']);
if (!this.#session(args.state, scope, { reportTimeout: true })) return { status: 'error', reason: 'login-timeout' };
const answer = await (this.#lock ? this.#write(async () => {
const result = await this.#client.oauthStatus(args.state);
if (result?.status === 'ok') this.#beforeMutation('proxy.login.complete');
return result;
}) : this.#client.oauthStatus(args.state));
if (answer?.status === 'wait') return { status: 'wait' };
if (!['ok', 'error'].includes(answer?.status)) refuse('proxy-down');
this.#pending.delete(args.state);
if (answer.status === 'ok') this.#onChange('invalidate');
return answer.status === 'ok' ? { status: 'ok' } : { status: 'error', reason: 'login-failed' };
}
if (verb === 'proxy.login.cancel') {
fields(args, ['state']); this.#session(args.state, scope);
await (this.#lock ? this.#write(() => this.#client.oauthCancel(args.state)) : this.#client.oauthCancel(args.state)); this.#pending.delete(args.state);
return { status: 'cancelled' };
}
if (verb === 'proxy.apikey.add') {
fields(args, ['provider', 'key']);
if (args.provider !== 'z.ai') refuse('provider-not-allowed');
if (typeof args.key !== 'string' || !/^[\x21-\x7e]{1,4096}$/.test(args.key)) refuse('apikey-invalid');
return this.#write(async () => {
const groups = await this.#client.getUpstreamGroups();
if (!Array.isArray(groups)) refuse('proxy-down');
const group = { name: `mosaic-zai-${randomBytes(6).toString('hex')}`, 'base-url': ZAI_URL,
priority: 0, keys: [{ 'api-key': args.key }] };
if (this.#lock && !isDeepStrictEqual(await this.#client.getUpstreamGroups(), groups)) refuse('proxy-down');
this.#beforeMutation('proxy.apikey.add');
await this.#client.putUpstreamGroups([...groups, group]);
if (this.#lock && !(await this.#client.getUpstreamGroups()).some(g => g.name === group.name && g['base-url'] === group['base-url'] && g.keys?.some(k => k['api-key'] === args.key))) refuse('proxy-down');
return { added: true };
});
}
if (verb === 'proxy.group.models.set') {
fields(args, ['accountId', 'models']);
if (typeof args.accountId !== 'string' || !/^[a-f0-9]{12}$/.test(args.accountId) || !Array.isArray(args.models) || args.models.length > 32 || args.models.some(m => typeof m !== 'string' || !MODEL_ID.test(m)) || new Set(args.models).size !== args.models.length) refuse('models-invalid');
return this.#write(async () => {
const { rows, groups } = await this.#accounts();
const original = this.#lock ? structuredClone(groups) : null;
const matching = new Set(rows.filter(r => r.source === 'apikey' && (r.accountId === args.accountId || (typeof groups[r.groupIndex]?.name === 'string' && keyId(groups[r.groupIndex].name) === args.accountId))).map(r => r.groupIndex));
if (matching.size !== 1) refuse('models-invalid');
const index = [...matching][0];
const existing = groups[index].models ?? [];
const byId = new Map();
for (const entry of existing) {
const display = entry.alias || entry.name;
if (byId.has(display)) refuse('models-ambiguous');
byId.set(display, entry);
}
const desired = args.models.map(name => structuredClone(byId.get(name) ?? { name }));
groups[index].models = desired;
if (this.#lock && !isDeepStrictEqual(await this.#client.getUpstreamGroups(), original)) refuse('proxy-down');
this.#beforeMutation('proxy.group.models.set');
try { await this.#client.putUpstreamGroups(groups); }
catch (error) { refuse(error.code === 'proxy-client-refused' || error.code === 'proxy-client-redirect' ? 'models-write-refused' : 'proxy-down'); }
const after = await this.#accounts();
const row = after.rows.find(r => r.source === 'apikey' && (r.accountId === args.accountId || (typeof after.groups[r.groupIndex]?.name === 'string' && keyId(after.groups[r.groupIndex].name) === args.accountId)));
const actual = row && after.groups[row.groupIndex].models;
const pair = entry => ({ name: entry.name, ...(entry.alias !== undefined ? { alias: entry.alias } : {}) });
if (!row || JSON.stringify(actual?.map(pair)) !== JSON.stringify(desired.map(pair))) refuse('models-unconfirmed');
return { accountId: args.accountId, models: args.models };
});
}
if (verb === 'proxy.priority.set') {
fields(args, ['accountId', 'priority']);
if (typeof args.accountId !== 'string' || !/^[a-f0-9]{12}$/.test(args.accountId) || !priorityOk(args.priority)) refuse('priority-invalid');
return this.#write(async () => {
const { rows, groups } = await this.#accounts();
const original = this.#lock ? structuredClone(groups) : null;
const matches = rows.filter(row => row.accountId === args.accountId);
if (matches.length !== 1) refuse('priority-invalid');
const row = matches[0];
if (this.#lock && !isDeepStrictEqual(await this.#client.getUpstreamGroups(), original)) refuse('proxy-down');
this.#beforeMutation('proxy.priority.set');
try {
if (row.source === 'oauth') await this.#client.patchCredential(row.name, args.priority);
else {
groups[row.groupIndex].keys[row.keyIndex].priority = args.priority;
await this.#client.putUpstreamGroups(groups);
}
} catch (error) {
refuse(error.code === 'proxy-client-refused' || error.code === 'proxy-client-redirect' ? 'priority-write-refused' : 'proxy-down');
}
const after = (await this.#accounts()).rows.find(r => r.accountId === args.accountId);
if (!after || after.priority !== args.priority) refuse('priority-unconfirmed');
return { accountId: row.accountId, priority: args.priority };
});
}
if (verb === 'proxy.keys') {
fields(args, []);
const values = await this.#client.getKeys();
if (!Array.isArray(values) || values.some(k => typeof k !== 'string')) refuse('proxy-down');
const records = await this.#records(scope.business);
const prefix = this.#org === null ? `msk-${this.#tag}-` : `mso-${this.#tag}-${this.#org}-`;
return { keys: values.map(value => {
const id = keyId(value), suffix = value.startsWith(prefix) ? value.slice(prefix.length) : null;
const sentinel = suffix !== null && (this.#org === null ? suffix.startsWith('sentinel-') : /^sentinel-[A-Za-z0-9_-]{43}$/.test(suffix));
const managed = suffix !== null && (this.#org === null || /^key-[A-Za-z0-9_-]{43}$/.test(suffix));
const record = records.find(r => r.keyId === id);
return { keyId: id, management: sentinel ? 'sentinel' : managed ? 'stack-managed' : 'not stack-managed',
agent: record?.agent ?? null, issuedAt: record?.issuedAt ?? null, state: record?.state ?? null };
}) };
}
refuse('invalid-request');
}
}