Files
EJClaw/src/credential-proxy.ts
Eyejoker d01f98bd61 feat: unify 3 bot services into single process
- Add UNIFIED_MODE flag (default on, disable with UNIFIED_MODE=0)
- Register all 3 Discord bots in one process (claude, codex, review)
- Load all registered groups regardless of agent_type (codex/owner priority)
- Start credential proxy and container cleanup at unified startup
- shouldServiceProcessChat returns true in unified mode
- Add findChannelByName for role-based response routing
- Backward compatible: UNIFIED_MODE=0 restores per-service behavior
2026-03-29 18:37:17 +09:00

171 lines
4.9 KiB
TypeScript

/**
* Credential proxy for reviewer container isolation.
* Containers connect here instead of directly to AI APIs.
* The proxy injects real credentials so containers never see them.
*
* Supports both Claude (Anthropic) and Codex (OpenAI) APIs:
* Claude API key: Proxy injects x-api-key on every request.
* Claude OAuth: Proxy replaces placeholder Bearer token with real one.
* Codex: Proxy injects Authorization: Bearer {OPENAI_API_KEY}.
*/
import { createServer, Server } from 'http';
import { request as httpsRequest } from 'https';
import { request as httpRequest, RequestOptions } from 'http';
import { getEnv } from './env.js';
import { logger } from './logger.js';
export type AuthMode = 'api-key' | 'oauth';
export function startCredentialProxy(
port: number,
host = '127.0.0.1',
): Promise<Server> {
const anthropicApiKey = getEnv('ANTHROPIC_API_KEY') || '';
const oauthToken =
getEnv('CLAUDE_CODE_OAUTH_TOKEN') || getEnv('ANTHROPIC_AUTH_TOKEN') || '';
const anthropicBaseUrl =
getEnv('ANTHROPIC_BASE_URL') || 'https://api.anthropic.com';
const openaiApiKey = getEnv('OPENAI_API_KEY') || '';
const authMode: AuthMode = anthropicApiKey ? 'api-key' : 'oauth';
const anthropicUrl = new URL(anthropicBaseUrl);
const anthropicIsHttps = anthropicUrl.protocol === 'https:';
const makeAnthropicRequest = anthropicIsHttps ? httpsRequest : httpRequest;
return new Promise((resolve, reject) => {
const server = createServer((req, res) => {
const chunks: Buffer[] = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
const body = Buffer.concat(chunks);
// Route: Codex/OpenAI requests go to OpenAI API
const isOpenAI =
req.headers['x-ejclaw-provider'] === 'openai' ||
(req.url || '').startsWith('/v1/chat/completions');
if (isOpenAI) {
proxyToOpenAI(req, res, body, openaiApiKey);
return;
}
// Default: Anthropic/Claude API
const headers: Record<string, string | number | string[] | undefined> =
{
...(req.headers as Record<string, string>),
host: anthropicUrl.host,
'content-length': body.length,
};
delete headers['connection'];
delete headers['keep-alive'];
delete headers['transfer-encoding'];
delete headers['x-ejclaw-provider'];
if (authMode === 'api-key') {
delete headers['x-api-key'];
headers['x-api-key'] = anthropicApiKey;
} else if (headers['authorization']) {
delete headers['authorization'];
if (oauthToken) {
headers['authorization'] = `Bearer ${oauthToken}`;
}
}
const upstream = makeAnthropicRequest(
{
hostname: anthropicUrl.hostname,
port: anthropicUrl.port || (anthropicIsHttps ? 443 : 80),
path: req.url,
method: req.method,
headers,
} as RequestOptions,
(upRes) => {
res.writeHead(upRes.statusCode!, upRes.headers);
upRes.pipe(res);
},
);
upstream.on('error', (err) => {
logger.error(
{ err, url: req.url },
'Credential proxy upstream error (Anthropic)',
);
if (!res.headersSent) {
res.writeHead(502);
res.end('Bad Gateway');
}
});
upstream.write(body);
upstream.end();
});
});
server.listen(port, host, () => {
logger.info({ port, host, authMode }, 'Credential proxy started');
resolve(server);
});
server.on('error', reject);
});
}
function proxyToOpenAI(
req: import('http').IncomingMessage,
res: import('http').ServerResponse,
body: Buffer,
apiKey: string,
): void {
const headers: Record<string, string | number | string[] | undefined> = {
...(req.headers as Record<string, string>),
host: 'api.openai.com',
'content-length': body.length,
};
delete headers['connection'];
delete headers['keep-alive'];
delete headers['transfer-encoding'];
delete headers['x-ejclaw-provider'];
// Inject real OpenAI API key
delete headers['authorization'];
if (apiKey) {
headers['authorization'] = `Bearer ${apiKey}`;
}
const upstream = httpsRequest(
{
hostname: 'api.openai.com',
port: 443,
path: req.url,
method: req.method,
headers,
} as RequestOptions,
(upRes) => {
res.writeHead(upRes.statusCode!, upRes.headers);
upRes.pipe(res);
},
);
upstream.on('error', (err) => {
logger.error(
{ err, url: req.url },
'Credential proxy upstream error (OpenAI)',
);
if (!res.headersSent) {
res.writeHead(502);
res.end('Bad Gateway');
}
});
upstream.write(body);
upstream.end();
}
export function detectAuthMode(): AuthMode {
return getEnv('ANTHROPIC_API_KEY') ? 'api-key' : 'oauth';
}