mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
The login child inherited the whole server environment, including OPENAI_API_KEY and every other provider credential. Pass only what Codex needs to find its home, open a browser and reach OpenAI through a proxy.
361 lines
11 KiB
JavaScript
361 lines
11 KiB
JavaScript
import { spawn } from 'node:child_process';
|
|
import {
|
|
accessSync,
|
|
constants as fsConstants,
|
|
readFileSync,
|
|
statSync,
|
|
} from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
export function codexAuthPath({
|
|
environment = process.env,
|
|
home = os.homedir(),
|
|
} = {}) {
|
|
const explicit = String(environment.CODEX_AUTH_JSON || '').trim();
|
|
if (explicit) return path.resolve(explicit);
|
|
const codexHome = String(environment.CODEX_HOME || '').trim();
|
|
return path.join(
|
|
codexHome ? path.resolve(codexHome) : path.join(home, '.codex'),
|
|
'auth.json',
|
|
);
|
|
}
|
|
|
|
/** An optional operator lock that refuses the metered API-key voice lane. */
|
|
export function preferCodexOAuth(environment = process.env) {
|
|
if (!Object.hasOwn(environment, 'GEV_PREFER_CODEX_OAUTH')) return false;
|
|
const value = String(environment.GEV_PREFER_CODEX_OAUTH || '')
|
|
.trim()
|
|
.toLowerCase();
|
|
if (['1', 'true', 'yes', 'on'].includes(value)) return true;
|
|
if (['0', 'false', 'no', 'off'].includes(value)) return false;
|
|
throw new Error(
|
|
'GEV_PREFER_CODEX_OAUTH must be true or false. Refusing to choose a voice auth mode.',
|
|
);
|
|
}
|
|
|
|
function codexOAuthError(message, code = 'CODEX_OAUTH_UNAVAILABLE') {
|
|
const error = new Error(message);
|
|
error.code = code;
|
|
return error;
|
|
}
|
|
|
|
export function readCodexOAuthAccessToken({
|
|
authPath = codexAuthPath(),
|
|
readFile = readFileSync,
|
|
statFile = statSync,
|
|
nowMs = Date.now(),
|
|
} = {}) {
|
|
const unavailable =
|
|
'ChatGPT sign-in is unavailable. Sign in to Codex or the ChatGPT desktop app and try again.';
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(readFile(authPath, 'utf8'));
|
|
} catch {
|
|
throw codexOAuthError(unavailable);
|
|
}
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
throw codexOAuthError(unavailable);
|
|
}
|
|
const mode = String(parsed.auth_mode || '')
|
|
.trim()
|
|
.toLowerCase();
|
|
const chatGptMode = mode
|
|
? mode === 'chatgpt' || mode === 'chatgptauthtokens'
|
|
: typeof parsed.OPENAI_API_KEY !== 'string';
|
|
const token = parsed.tokens?.access_token;
|
|
if (!chatGptMode || typeof token !== 'string' || !token.trim()) {
|
|
throw codexOAuthError(unavailable);
|
|
}
|
|
|
|
// This only checks token shape and expiry. OpenAI verifies the signature when
|
|
// minting the short-lived Realtime credential; we never modify auth.json.
|
|
const parts = token.split('.');
|
|
if (
|
|
parts.length !== 3 ||
|
|
parts.some((part) => !/^[A-Za-z0-9_-]+$/.test(part))
|
|
) {
|
|
throw codexOAuthError(unavailable);
|
|
}
|
|
let payload;
|
|
try {
|
|
payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
} catch {
|
|
throw codexOAuthError(unavailable);
|
|
}
|
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
throw codexOAuthError(unavailable);
|
|
}
|
|
let expiresAtMs;
|
|
if (Object.hasOwn(payload, 'exp')) {
|
|
if (typeof payload.exp !== 'number' || !Number.isFinite(payload.exp)) {
|
|
throw codexOAuthError(unavailable);
|
|
}
|
|
expiresAtMs = payload.exp * 1000;
|
|
} else {
|
|
try {
|
|
expiresAtMs = statFile(authPath).mtimeMs + 60 * 60 * 1000;
|
|
} catch {
|
|
throw codexOAuthError(unavailable);
|
|
}
|
|
}
|
|
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs + 60_000) {
|
|
throw codexOAuthError(
|
|
'ChatGPT sign-in has expired. Re-authenticate in Provider Settings or run codex login and try again.',
|
|
'CODEX_OAUTH_REAUTH_REQUIRED',
|
|
);
|
|
}
|
|
return token;
|
|
}
|
|
|
|
export function resolveCodexExecutable({
|
|
environment = process.env,
|
|
home = os.homedir(),
|
|
access = accessSync,
|
|
} = {}) {
|
|
const explicit = String(environment.CODEX_BIN || '').trim();
|
|
if (explicit) return explicit;
|
|
|
|
for (const candidate of [
|
|
path.join(home, '.local', 'bin', 'codex'),
|
|
path.join(
|
|
home,
|
|
'.codex',
|
|
'packages',
|
|
'standalone',
|
|
'current',
|
|
'bin',
|
|
'codex',
|
|
),
|
|
]) {
|
|
try {
|
|
access(candidate, fsConstants.X_OK);
|
|
return candidate;
|
|
} catch {
|
|
// Fall through to the next known location, then PATH.
|
|
}
|
|
}
|
|
return 'codex';
|
|
}
|
|
|
|
// What `codex login` needs to find its home, open a browser and reach OpenAI.
|
|
// The server's own environment also holds every provider secret, so the
|
|
// child receives only these names.
|
|
const CODEX_LOGIN_ENV =
|
|
/^(?:PATH|PATHEXT|HOME|USER|USERNAME|LOGNAME|SHELL|LANG|LANGUAGE|LC_[A-Z_]+|TERM|TZ|TMPDIR|TEMP|TMP|XDG_[A-Z_]+|DISPLAY|WAYLAND_DISPLAY|DBUS_SESSION_BUS_ADDRESS|BROWSER|WSL_[A-Z_]+|WSLENV|(?:HTTPS?|ALL|NO)_PROXY|SSL_CERT_(?:FILE|DIR)|NODE_EXTRA_CA_CERTS|SYSTEMROOT|WINDIR|COMSPEC|USERPROFILE|APPDATA|LOCALAPPDATA|PROGRAMDATA|HOMEDRIVE|HOMEPATH|CODEX_[A-Z_]+)$/i;
|
|
|
|
/** The environment `codex login` runs with: the allowlist, then `CODEX_HOME`. */
|
|
export function codexLoginEnvironment(environment, codexHome) {
|
|
const env = {};
|
|
for (const [name, value] of Object.entries(environment || {}))
|
|
if (CODEX_LOGIN_ENV.test(name) && value !== undefined) env[name] = value;
|
|
env.CODEX_HOME = codexHome;
|
|
return env;
|
|
}
|
|
|
|
export function startCodexChatGptLogin({
|
|
spawnImpl = spawn,
|
|
environment = process.env,
|
|
home = os.homedir(),
|
|
executable = resolveCodexExecutable({ environment, home }),
|
|
} = {}) {
|
|
// Codex owns its credential storage. An explicit read path must agree with
|
|
// the CLI's write path; never silently sign in to another credential store.
|
|
const configuredHome = String(environment.CODEX_HOME || '').trim();
|
|
const loginHome = configuredHome
|
|
? path.resolve(configuredHome)
|
|
: path.join(home, '.codex');
|
|
if (
|
|
codexAuthPath({ environment, home }) !== path.join(loginHome, 'auth.json')
|
|
) {
|
|
return Promise.reject(
|
|
codexOAuthError(
|
|
'CODEX_AUTH_JSON does not match the Codex login storage path. Set CODEX_HOME to the matching directory or sign in to Codex separately.',
|
|
'CODEX_OAUTH_STORAGE_MISMATCH',
|
|
),
|
|
);
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let child;
|
|
let spawned = false;
|
|
let finish;
|
|
const completion = new Promise((done) => {
|
|
finish = done;
|
|
});
|
|
const fail = () => {
|
|
reject(
|
|
codexOAuthError(
|
|
'Could not start ChatGPT sign-in. Check that Codex is installed, or set CODEX_BIN to its executable.',
|
|
'CODEX_OAUTH_LOGIN_FAILED',
|
|
),
|
|
);
|
|
finish({ exitCode: null });
|
|
};
|
|
try {
|
|
child = spawnImpl(executable, ['login'], {
|
|
cwd: home,
|
|
detached: true,
|
|
stdio: 'ignore',
|
|
env: codexLoginEnvironment(environment, loginHome),
|
|
});
|
|
} catch {
|
|
fail();
|
|
return;
|
|
}
|
|
|
|
child.once('error', () => {
|
|
if (!spawned) fail();
|
|
else finish({ exitCode: null });
|
|
});
|
|
child.once('exit', (exitCode) => finish({ exitCode }));
|
|
child.once('spawn', () => {
|
|
spawned = true;
|
|
child.unref?.();
|
|
resolve({
|
|
completion,
|
|
cancel() {
|
|
try {
|
|
child.kill?.('SIGTERM');
|
|
} catch {
|
|
/* Already stopped. */
|
|
}
|
|
},
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/** One local login owner shared by the launch and status endpoints. */
|
|
export function createCodexOAuthLogin({
|
|
resolveOAuthAccessToken = () => readCodexOAuthAccessToken(),
|
|
startOAuthLogin = () => startCodexChatGptLogin(),
|
|
loginTimeoutMs = 120_000,
|
|
setTimer = setTimeout,
|
|
clearTimer = clearTimeout,
|
|
} = {}) {
|
|
let active = null;
|
|
let failure = null;
|
|
let disposed = false;
|
|
|
|
function credentials() {
|
|
try {
|
|
if (resolveOAuthAccessToken()) return { available: true };
|
|
} catch (error) {
|
|
const code = error?.code || 'CODEX_OAUTH_UNAVAILABLE';
|
|
return {
|
|
available: false,
|
|
error: error?.message || 'ChatGPT sign-in is unavailable',
|
|
code,
|
|
reauthRequired: code === 'CODEX_OAUTH_REAUTH_REQUIRED',
|
|
};
|
|
}
|
|
return {
|
|
available: false,
|
|
error: 'ChatGPT sign-in is unavailable',
|
|
code: 'CODEX_OAUTH_UNAVAILABLE',
|
|
reauthRequired: false,
|
|
};
|
|
}
|
|
|
|
function status() {
|
|
const current = credentials();
|
|
if (current.available) return current;
|
|
if (failure) return { available: false, ...failure, loginFailed: true };
|
|
return active ? { ...current, pending: true } : current;
|
|
}
|
|
|
|
function finish(attempt, error) {
|
|
if (active !== attempt) return;
|
|
clearTimer(attempt.timer);
|
|
active = null;
|
|
failure = error;
|
|
}
|
|
|
|
async function start() {
|
|
if (disposed)
|
|
throw codexOAuthError(
|
|
'ChatGPT sign-in is unavailable because the server has stopped.',
|
|
'CODEX_OAUTH_LOGIN_FAILED',
|
|
);
|
|
if (credentials().available)
|
|
return { available: true, started: false, pending: false };
|
|
if (active) return { available: false, started: false, pending: true };
|
|
|
|
// Reserve the attempt before awaiting spawn so concurrent POSTs share it.
|
|
const attempt = { process: null, timer: null };
|
|
active = attempt;
|
|
failure = null;
|
|
attempt.timer = setTimer(() => {
|
|
finish(attempt, {
|
|
code: 'CODEX_OAUTH_LOGIN_TIMEOUT',
|
|
error:
|
|
'ChatGPT sign-in timed out. Click USE CHATGPT OAUTH to try again.',
|
|
});
|
|
attempt.process?.cancel();
|
|
}, loginTimeoutMs);
|
|
attempt.timer?.unref?.();
|
|
|
|
try {
|
|
const login = await startOAuthLogin();
|
|
if (!login?.completion || typeof login.cancel !== 'function')
|
|
throw new Error('Invalid login process');
|
|
attempt.process = login;
|
|
if (active !== attempt) {
|
|
login.cancel();
|
|
return { ...status(), started: false, pending: false };
|
|
}
|
|
Promise.resolve(login.completion).then(
|
|
(result) => {
|
|
if (active !== attempt) return;
|
|
if (result?.exitCode !== 0) {
|
|
finish(attempt, {
|
|
code: 'CODEX_OAUTH_LOGIN_FAILED',
|
|
error:
|
|
'ChatGPT sign-in did not complete. Try again in Provider Settings, or run codex login separately.',
|
|
});
|
|
} else if (!credentials().available) {
|
|
finish(attempt, {
|
|
code: 'CODEX_OAUTH_STORAGE_UNAVAILABLE',
|
|
error:
|
|
'Codex sign-in finished, but its configured auth.json has no usable ChatGPT token. Check CODEX_HOME and Codex credential storage. GEV can read file storage only.',
|
|
});
|
|
} else finish(attempt, null);
|
|
},
|
|
() =>
|
|
finish(attempt, {
|
|
code: 'CODEX_OAUTH_LOGIN_FAILED',
|
|
error:
|
|
'ChatGPT sign-in did not complete. Try again in Provider Settings.',
|
|
}),
|
|
);
|
|
return { available: false, started: true, pending: true };
|
|
} catch (error) {
|
|
const code =
|
|
error?.code === 'CODEX_OAUTH_STORAGE_MISMATCH'
|
|
? error.code
|
|
: 'CODEX_OAUTH_LOGIN_FAILED';
|
|
finish(attempt, {
|
|
code,
|
|
error:
|
|
code === 'CODEX_OAUTH_STORAGE_MISMATCH'
|
|
? error.message
|
|
: 'Could not start ChatGPT sign-in. Check that Codex is installed, or set CODEX_BIN to its executable.',
|
|
});
|
|
return { ...status(), started: false, pending: false };
|
|
}
|
|
}
|
|
|
|
return {
|
|
status,
|
|
start,
|
|
dispose() {
|
|
disposed = true;
|
|
if (!active) return;
|
|
const attempt = active;
|
|
finish(attempt, null);
|
|
attempt.process?.cancel();
|
|
},
|
|
};
|
|
}
|