fix(voice): validate Codex OAuth and report unknown usage

This commit is contained in:
devv-shayan committed 2026-09-27 21:09:26 +05:00
1 parent 881e24485c
commit a398a25934
8 files changed
+265 -19

No files matched your search

+78 -7
View File
@@ -1,5 +1,10 @@
import { spawn } from 'node:child_process';
import { accessSync, constants as fsConstants, readFileSync } from 'node:fs';
import {
accessSync,
constants as fsConstants,
readFileSync,
statSync,
} from 'node:fs';
import os from 'node:os';
import path from 'node:path';
@@ -16,22 +21,88 @@ export function codexAuthPath({
);
}
/** 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.',
);
}
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 new Error(
'ChatGPT sign-in is unavailable. Sign in to Codex or the ChatGPT desktop app and try again.',
);
throw new Error(unavailable);
}
const token = String(parsed?.tokens?.access_token || '').trim();
if (!token) {
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(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;
const refresh = parsed.tokens?.refresh_token;
if (
!chatGptMode ||
typeof token !== 'string' ||
!token.trim() ||
typeof refresh !== 'string' ||
!refresh.trim()
) {
throw new Error(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 new Error(unavailable);
}
let payload;
try {
payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
} catch {
throw new Error(unavailable);
}
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new Error(unavailable);
}
let expiresAtMs;
if (Object.hasOwn(payload, 'exp')) {
if (typeof payload.exp !== 'number' || !Number.isFinite(payload.exp)) {
throw new Error(unavailable);
}
expiresAtMs = payload.exp * 1000;
} else {
try {
expiresAtMs = statFile(authPath).mtimeMs + 60 * 60 * 1000;
} catch {
throw new Error(unavailable);
}
}
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs + 60_000) {
throw new Error(
'ChatGPT sign-in is unavailable. Sign in to Codex or the ChatGPT desktop app and try again.',
'ChatGPT sign-in has expired. Run codex login and try again.',
);
}
return token;
+28
View File
@@ -14,6 +14,7 @@ import {
import { realtimeInstructions } from './instructions.js';
import { GEV_REALTIME_TOOLS } from './tools.js';
import {
preferCodexOAuth,
readCodexOAuthAccessToken,
startCodexChatGptLogin,
} from './codex-auth.js';
@@ -174,6 +175,7 @@ function createRealtimeTokenHandler({
fetchImpl = (...args) => fetch(...args),
resolveApiKey = () => process.env.OPENAI_API_KEY,
resolveOAuthAccessToken = () => readCodexOAuthAccessToken(),
environment = process.env,
models = {},
} = {}) {
return async (req, res) => {
@@ -194,6 +196,28 @@ function createRealtimeTokenHandler({
return;
}
let oauthPinned;
try {
oauthPinned = preferCodexOAuth(environment);
} catch (error) {
res.statusCode = 503;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: error.message }));
return;
}
if (oauthPinned && authMode !== 'oauth') {
res.statusCode = 503;
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
error:
'ChatGPT OAuth is required by GEV_PREFER_CODEX_OAUTH. Select USE CHATGPT OAUTH in Provider Settings.',
code: 'CODEX_OAUTH_REQUIRED',
}),
);
return;
}
// Opt-in per-IP throttle (GEV_RATELIMIT_OPENAI_PER_MIN). No-op when unset.
if (!enforceOptInRateLimit(openAiRateLimiter(), req, res)) return;
@@ -332,6 +356,10 @@ function createRealtimeTokenHandler({
// case where a bogus ?tier= was silently downgraded to standard.
res.setHeader('X-GEV-Voice-Tier', tier);
res.setHeader('X-GEV-Voice-Model', model);
res.setHeader(
'X-GEV-Voice-Auth',
authMode === 'oauth' ? 'codex-oauth' : 'env',
);
if (requestedTier && !isKnownVoiceTier(requestedTier)) {
res.setHeader('X-GEV-Voice-Tier-Fallback', '1');
}