feat(voice): add ChatGPT OAuth auth option

This commit is contained in:
devv-shayan committed 2026-09-27 20:58:08 +05:00
1 parent b210ab0fe4
commit 881e24485c
23 files changed
+918 -34

No files matched your search

+215 -17
View File
@@ -13,12 +13,167 @@ import {
} from './constants.js';
import { realtimeInstructions } from './instructions.js';
import { GEV_REALTIME_TOOLS } from './tools.js';
import {
readCodexOAuthAccessToken,
startCodexChatGptLogin,
} from './codex-auth.js';
const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
function isLoopbackRequest(req) {
return LOOPBACK_ADDRESSES.has(String(req.socket?.remoteAddress || ''));
}
function isSameOriginRequest(req) {
const origin = String(req.headers?.origin || '').trim();
if (!origin) return true;
const host = String(req.headers?.host || '').trim();
if (!host) return false;
const protocol = req.socket?.encrypted ? 'https:' : 'http:';
try {
return new URL(origin).origin === new URL(`${protocol}//${host}`).origin;
} catch {
return false;
}
}
function requestQuery(req) {
try {
return new URL(req.url || '', 'http://localhost').searchParams;
} catch {
return new URLSearchParams();
}
}
function resolveRealtimeAuthMode(query) {
const requested = String(query.get('auth') || 'api-key').toLowerCase();
if (requested === 'api-key' || requested === 'oauth') return requested;
return null;
}
function createRealtimeOAuthStatusHandler({
resolveOAuthAccessToken = () => readCodexOAuthAccessToken(),
} = {}) {
return (req, res) => {
res.setHeader('Cache-Control', 'no-store');
res.setHeader('Content-Type', 'application/json');
if (req.method !== 'GET') {
res.statusCode = 405;
res.end(JSON.stringify({ error: 'Method not allowed' }));
return;
}
if (!isLoopbackRequest(req)) {
res.statusCode = 403;
res.end(
JSON.stringify({
available: false,
error: 'ChatGPT OAuth is available only from this machine',
}),
);
return;
}
try {
const token = resolveOAuthAccessToken();
if (!token) throw new Error('ChatGPT sign-in is unavailable');
res.statusCode = 200;
res.end(JSON.stringify({ available: true }));
} catch (error) {
res.statusCode = 200;
res.end(
JSON.stringify({
available: false,
error: error?.message || 'ChatGPT sign-in is unavailable',
}),
);
}
};
}
function createRealtimeOAuthLoginHandler({
resolveOAuthAccessToken = () => readCodexOAuthAccessToken(),
startOAuthLogin = () => startCodexChatGptLogin(),
now = () => Date.now(),
restartCooldownMs = 30_000,
} = {}) {
let lastStartedAt = 0;
return async (req, res) => {
res.setHeader('Cache-Control', 'no-store');
res.setHeader('Content-Type', 'application/json');
if (req.method !== 'POST') {
res.statusCode = 405;
res.end(JSON.stringify({ error: 'Method not allowed' }));
return;
}
if (!isLoopbackRequest(req)) {
res.statusCode = 403;
res.end(
JSON.stringify({
available: false,
error: 'ChatGPT OAuth sign-in is available only from this machine',
}),
);
return;
}
if (!isSameOriginRequest(req)) {
res.statusCode = 403;
res.end(
JSON.stringify({
available: false,
error: 'Cross-origin ChatGPT OAuth sign-in is not allowed',
}),
);
return;
}
try {
if (resolveOAuthAccessToken()) {
res.statusCode = 200;
res.end(
JSON.stringify({ available: true, started: false, pending: false }),
);
return;
}
} catch {
// No usable local token yet; launch the supported Codex sign-in flow.
}
const currentTime = now();
if (
lastStartedAt > 0 &&
currentTime - lastStartedAt < Math.max(0, restartCooldownMs)
) {
res.statusCode = 202;
res.end(
JSON.stringify({ available: false, started: false, pending: true }),
);
return;
}
try {
await startOAuthLogin();
lastStartedAt = currentTime;
res.statusCode = 202;
res.end(
JSON.stringify({ available: false, started: true, pending: true }),
);
} catch (error) {
res.statusCode = 503;
res.end(
JSON.stringify({
available: false,
error: error?.message || 'Could not start ChatGPT sign-in',
}),
);
}
};
}
function createRealtimeTokenHandler({
annotationGuidance,
endpoint = 'https://api.openai.com/v1/realtime/client_secrets',
fetchImpl = (...args) => fetch(...args),
resolveApiKey = () => process.env.OPENAI_API_KEY,
resolveOAuthAccessToken = () => readCodexOAuthAccessToken(),
models = {},
} = {}) {
return async (req, res) => {
@@ -30,15 +185,62 @@ function createRealtimeTokenHandler({
return;
}
const query = requestQuery(req);
const authMode = resolveRealtimeAuthMode(query);
if (!authMode) {
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Unsupported cloud voice auth mode' }));
return;
}
// Opt-in per-IP throttle (GEV_RATELIMIT_OPENAI_PER_MIN). No-op when unset.
if (!enforceOptInRateLimit(openAiRateLimiter(), req, res)) return;
const apiKey = resolveApiKey();
if (!apiKey) {
res.statusCode = 503;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'OPENAI_API_KEY is not set' }));
return;
let bearer;
if (authMode === 'oauth') {
if (!isLoopbackRequest(req)) {
res.statusCode = 403;
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
error: 'ChatGPT OAuth voice is available only from this machine',
}),
);
return;
}
try {
bearer = resolveOAuthAccessToken();
} catch (error) {
res.statusCode = 503;
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
error: error?.message || 'ChatGPT sign-in is unavailable',
code: 'CODEX_OAUTH_UNAVAILABLE',
}),
);
return;
}
if (!bearer) {
res.statusCode = 503;
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
error: 'ChatGPT sign-in is unavailable',
code: 'CODEX_OAUTH_UNAVAILABLE',
}),
);
return;
}
} else {
bearer = resolveApiKey();
if (!bearer) {
res.statusCode = 503;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'OPENAI_API_KEY is not set' }));
return;
}
}
// Voice model tier, requested by the client as ?tier=standard|mini.
@@ -47,15 +249,7 @@ function createRealtimeTokenHandler({
// bad querystring degrades to a normal session rather than a dead mic.
// The env overrides stay authoritative per tier (see .env.example) —
// a wrong upstream model id is then a config fix, not a code change.
const requestedTier = (() => {
try {
return new URL(req.url || '', 'http://localhost').searchParams.get(
'tier',
);
} catch {
return null;
}
})();
const requestedTier = query.get('tier');
const tier = resolveVoiceModel(requestedTier).tier;
const model =
tier === 'mini'
@@ -124,7 +318,7 @@ function createRealtimeTokenHandler({
redirect: 'error',
signal: AbortSignal.timeout(30_000),
headers: {
Authorization: `Bearer ${apiKey}`,
Authorization: `Bearer ${bearer}`,
'Content-Type': 'application/json',
'OpenAI-Safety-Identifier': 'gev-local-dev',
},
@@ -167,4 +361,8 @@ function createRealtimeTokenHandler({
};
}
export { createRealtimeTokenHandler };
export {
createRealtimeOAuthLoginHandler,
createRealtimeOAuthStatusHandler,
createRealtimeTokenHandler,
};