Files
stack/packages/webui/src/proxy-http.mjs
T

57 lines
4.0 KiB
JavaScript

// Native Console routes: fixed path -> one owner human-transport call. Never
// imports a management client, reads proxy settings, or opens a proxy connection.
const routes = Object.freeze({
'/api/proxy/accounts': ['GET', 'proxy.accounts', []],
'/api/proxy/keys': ['GET', 'proxy.keys', []],
'/api/proxy/plugins': ['GET', 'plugins', []],
'/api/proxy/plugins/set': ['POST', 'plugins.set', ['revision', 'plugins']],
'/api/proxy/login/start': ['POST', 'proxy.login.start', ['provider']],
'/api/proxy/login/submit': ['POST', 'proxy.login.submit', ['redirect_url']],
'/api/proxy/login/status': ['POST', 'proxy.login.status', ['state']],
'/api/proxy/login/cancel': ['POST', 'proxy.login.cancel', ['state']],
'/api/proxy/apikey/add': ['POST', 'proxy.apikey.add', ['provider', 'key']],
'/api/proxy/priority/set': ['POST', 'proxy.priority.set', ['accountId', 'priority']],
'/api/proxy/group/models/set': ['POST', 'proxy.group.models.set', ['accountId', 'models']],
});
const codes = ['not-configured', 'human-required', 'provider-not-allowed', 'login-state-unknown',
'callback-url-invalid', 'apikey-invalid', 'priority-invalid', 'priority-unconfirmed', 'priority-write-refused', 'models-invalid', 'models-ambiguous', 'models-write-refused', 'models-unconfirmed', 'proxy-down', 'invalid-request'];
const json = (res, status, value) => {
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(value));
};
export async function proxyHTTP(req, res, { path, call, named, tester, granted, send, origin, user, log }) {
const route = routes[path];
const done = (status, value) => {
// No request body, query, login state, authorization URL or exception text.
log(`${new Date().toISOString()} ${route?.[0] ?? 'GET'} ${route ? path : '/api/proxy/unknown'} ${status} user=${JSON.stringify(user ?? '')}`);
return json(res, status, value);
};
if (tester || granted || !named) return done(403, { error: 'human-required' });
if (!route) return done(404, { error: 'not found' });
if (req.method !== route[0]) return done(405, { error: 'method not allowed' });
if (req.method === 'POST') {
if (!send) return done(403, { error: 'read-only' });
if (req.headersDistinct.origin?.length !== 1 || req.headers.origin !== origin) return done(403, { error: 'cross-origin request refused' });
if (req.headersDistinct['sec-fetch-site']?.length !== 1 || req.headers['sec-fetch-site'] !== 'same-origin') return done(403, { error: 'cross-site request refused' });
}
if (!call) return done(503, { error: 'not-configured' });
let args = {};
if (req.method === 'POST') {
try {
if ((req.headers['content-type'] ?? '').split(';')[0].trim().toLowerCase() !== 'application/json') throw Error();
const chunks = []; let size = 0;
for await (const chunk of req) {
size += chunk.length; if (size > 16384) throw Error(); chunks.push(chunk);
}
args = JSON.parse(Buffer.concat(chunks));
if (!args || typeof args !== 'object' || Array.isArray(args) || Object.keys(args).length !== route[2].length
|| route[2].some(k => !Object.hasOwn(args, k))) throw Error();
} catch { return done(400, { error: 'invalid-request' }); }
}
if (path === '/api/proxy/plugins/set' && (!args.plugins || typeof args.plugins !== 'object' || Array.isArray(args.plugins) || Object.keys(args.plugins).length !== 1 || !['harness', 'model-provider'].includes(Object.keys(args.plugins)[0]) || !Array.isArray(Object.values(args.plugins)[0]) || !Object.values(args.plugins)[0].every(name => typeof name === 'string'))) return done(400, { error: 'invalid-request' });
try { return done(200, await call(route[1], args)); }
catch (error) {
const code = codes.includes(error.code) || (path === '/api/proxy/plugins/set' && typeof error.code === 'string' && /^[a-z-]{1,64}$/.test(error.code)) ? error.code : 'outcome-unknown';
return done(code === 'human-required' ? 403 : ['not-configured', 'proxy-down', 'outcome-unknown'].includes(code) ? 503 : 400, { error: code });
}
}