mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-07-24 16:43:47 +00:00
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Pre-commit / pre-commit (push) Has been cancelled
Test / Run Web + Local Brain Smoke (push) Has been cancelled
Test / Run Frontend Guardrails (push) Has been cancelled
Test / Run Python Tests (push) Has been cancelled
Co-authored-by: Douglas <douglas.ym.lai@gmail.com> Co-authored-by: Douglas Lai <115660088+Douglasymlai@users.noreply.github.com> Co-authored-by: Tao Sun <168447269+fengju0213@users.noreply.github.com> Co-authored-by: Weijie Bai <happy.regina.bai@gmail.com>
250 lines
7.3 KiB
TypeScript
250 lines
7.3 KiB
TypeScript
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
|
|
import log from 'electron-log';
|
|
import crypto from 'node:crypto';
|
|
import http from 'node:http';
|
|
import { URL } from 'node:url';
|
|
import { codexOAuthClientId, codexOAuthTokenUrl } from './codexOAuth';
|
|
import { loadCodexCredential, saveCodexCredential } from './credentialStore';
|
|
import { expiresAtFromTokenPayload } from './oauthToken';
|
|
import type { CodexCredential, CodexResolverRuntime } from './types';
|
|
|
|
const RESOLVER_SECRET_HEADER = 'x-eigent-resolver-secret';
|
|
const EXPIRES_SOON_MS = 60_000;
|
|
|
|
let runtime: CodexResolverRuntime | null = null;
|
|
|
|
function writeJson(
|
|
res: http.ServerResponse,
|
|
statusCode: number,
|
|
body: Record<string, unknown>
|
|
) {
|
|
res.writeHead(statusCode, {
|
|
'content-type': 'application/json; charset=utf-8',
|
|
'cache-control': 'no-store',
|
|
});
|
|
res.end(JSON.stringify(body));
|
|
}
|
|
|
|
function readRequestJson(req: http.IncomingMessage): Promise<any> {
|
|
return new Promise((resolve, reject) => {
|
|
let body = '';
|
|
req.setEncoding('utf8');
|
|
req.on('data', (chunk) => {
|
|
body += chunk;
|
|
if (body.length > 32_768) {
|
|
reject(new Error('request_body_too_large'));
|
|
req.destroy();
|
|
}
|
|
});
|
|
req.on('end', () => {
|
|
if (!body) {
|
|
resolve({});
|
|
return;
|
|
}
|
|
try {
|
|
resolve(JSON.parse(body));
|
|
} catch {
|
|
reject(new Error('invalid_json'));
|
|
}
|
|
});
|
|
req.on('error', reject);
|
|
});
|
|
}
|
|
|
|
function isExpired(expiresAt?: string | null): boolean {
|
|
if (!expiresAt) return false;
|
|
const timestamp = Date.parse(expiresAt);
|
|
if (!Number.isFinite(timestamp)) return true;
|
|
return timestamp <= Date.now() + EXPIRES_SOON_MS;
|
|
}
|
|
|
|
function hasValidResolverSecret(
|
|
providedHeader: string | string[] | undefined,
|
|
secret: string
|
|
): boolean {
|
|
if (typeof providedHeader !== 'string') return false;
|
|
const provided = Buffer.from(providedHeader);
|
|
const expected = Buffer.from(secret);
|
|
if (provided.length !== expected.length) return false;
|
|
return crypto.timingSafeEqual(provided, expected);
|
|
}
|
|
|
|
async function refreshCodexCredential(
|
|
email: string,
|
|
credential: CodexCredential
|
|
): Promise<CodexCredential | null> {
|
|
if (!credential.refresh_token) return null;
|
|
const tokenUrl = codexOAuthTokenUrl();
|
|
const clientId = codexOAuthClientId();
|
|
|
|
const body = new URLSearchParams();
|
|
body.set('grant_type', 'refresh_token');
|
|
body.set('client_id', clientId);
|
|
body.set('refresh_token', credential.refresh_token);
|
|
|
|
const response = await fetch(tokenUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
accept: 'application/json',
|
|
'content-type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body,
|
|
});
|
|
if (!response.ok) return null;
|
|
|
|
const payload = (await response.json()) as Record<string, unknown>;
|
|
if (typeof payload.access_token !== 'string' || !payload.access_token) {
|
|
return null;
|
|
}
|
|
|
|
const refreshed: CodexCredential = {
|
|
...credential,
|
|
access_token: payload.access_token,
|
|
refresh_token:
|
|
typeof payload.refresh_token === 'string'
|
|
? payload.refresh_token
|
|
: credential.refresh_token,
|
|
token_type:
|
|
typeof payload.token_type === 'string'
|
|
? payload.token_type
|
|
: credential.token_type || 'Bearer',
|
|
expires_at: expiresAtFromTokenPayload(payload),
|
|
status: 'connected',
|
|
updated_at: new Date().toISOString(),
|
|
};
|
|
saveCodexCredential(email, refreshed);
|
|
return refreshed;
|
|
}
|
|
|
|
async function handleTokenRequest(
|
|
req: http.IncomingMessage,
|
|
res: http.ServerResponse,
|
|
secret: string
|
|
) {
|
|
const providedSecret = req.headers[RESOLVER_SECRET_HEADER];
|
|
if (!hasValidResolverSecret(providedSecret, secret)) {
|
|
writeJson(res, 401, { error_code: 'unauthorized' });
|
|
return;
|
|
}
|
|
|
|
let body: any;
|
|
try {
|
|
body = await readRequestJson(req);
|
|
} catch (error: any) {
|
|
writeJson(res, 400, { error_code: error.message || 'invalid_request' });
|
|
return;
|
|
}
|
|
|
|
const email = typeof body.email === 'string' ? body.email : '';
|
|
const forceRefresh = body.force_refresh === true;
|
|
if (!email) {
|
|
writeJson(res, 400, { error_code: 'email_required' });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let credential = loadCodexCredential(email);
|
|
if (!credential?.access_token) {
|
|
writeJson(res, 404, { error_code: 'not_connected' });
|
|
return;
|
|
}
|
|
|
|
if (forceRefresh || isExpired(credential.expires_at)) {
|
|
credential = await refreshCodexCredential(email, credential);
|
|
}
|
|
|
|
if (!credential || isExpired(credential.expires_at)) {
|
|
writeJson(res, 401, {
|
|
error_code: forceRefresh ? 'token_refresh_failed' : 'token_expired',
|
|
status: 'expired',
|
|
expires_at: credential?.expires_at ?? null,
|
|
});
|
|
return;
|
|
}
|
|
|
|
writeJson(res, 200, {
|
|
access_token: credential.access_token,
|
|
token_type: credential.token_type || 'Bearer',
|
|
expires_at: credential.expires_at ?? null,
|
|
status: credential.status,
|
|
account_label: credential.account_label ?? null,
|
|
});
|
|
} catch (error: any) {
|
|
log.warn('[CODEX RESOLVER] Failed to resolve local credential:', error);
|
|
writeJson(res, 500, { error_code: 'credential_unavailable' });
|
|
}
|
|
}
|
|
|
|
export async function ensureCodexResolverServer(): Promise<CodexResolverRuntime> {
|
|
if (runtime) return runtime;
|
|
|
|
const secret = crypto.randomBytes(32).toString('base64url');
|
|
const server = http.createServer((req, res) => {
|
|
const requestUrl = new URL(req.url || '/', 'http://127.0.0.1');
|
|
|
|
if (requestUrl.pathname !== '/codex/token') {
|
|
writeJson(res, 404, { error_code: 'not_found' });
|
|
return;
|
|
}
|
|
|
|
if (req.method !== 'POST') {
|
|
writeJson(res, 405, { error_code: 'method_not_allowed' });
|
|
return;
|
|
}
|
|
|
|
void handleTokenRequest(req, res, secret);
|
|
});
|
|
|
|
const port = await new Promise<number>((resolve, reject) => {
|
|
server.once('error', reject);
|
|
server.listen(0, '127.0.0.1', () => {
|
|
server.off('error', reject);
|
|
const address = server.address();
|
|
if (!address || typeof address === 'string') {
|
|
reject(new Error('invalid_resolver_address'));
|
|
return;
|
|
}
|
|
resolve(address.port);
|
|
});
|
|
});
|
|
|
|
runtime = {
|
|
url: `http://127.0.0.1:${port}`,
|
|
secret,
|
|
close: () =>
|
|
new Promise<void>((resolve, reject) => {
|
|
server.close((error) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
runtime = null;
|
|
resolve();
|
|
});
|
|
}),
|
|
};
|
|
|
|
log.info('[CODEX RESOLVER] Local resolver started', { port });
|
|
return runtime;
|
|
}
|
|
|
|
export async function getCodexResolverEnv(): Promise<Record<string, string>> {
|
|
const resolver = await ensureCodexResolverServer();
|
|
return {
|
|
CODEX_RESOLVER_URL: resolver.url,
|
|
CODEX_RESOLVER_SECRET: resolver.secret,
|
|
};
|
|
}
|