import type { IOpenAiChatGptAuthCredentials, IOpenAiChatGptAuthOptions, IOpenAiChatGptCompleteDeviceCodeOptions, IOpenAiChatGptDeviceCode, IOpenAiChatGptDeviceCodePollOptions, IOpenAiChatGptTokenData, IOpenAiChatGptTokenInfo, } from './smartai.interfaces.js'; export const OPENAI_CHATGPT_AUTH_ISSUER = 'https://auth.openai.com'; export const OPENAI_CHATGPT_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'; export const OPENAI_CHATGPT_CODEX_BASE_URL = 'https://chatgpt.com/backend-api/codex'; export const OPENAI_CHATGPT_DEFAULT_ORIGINATOR = 'smartai'; const DEVICE_CODE_TIMEOUT_MS = 15 * 60 * 1000; export class OpenAiChatGptAuthError extends Error { public status?: number; public body?: string; constructor(message: string, options: { status?: number; body?: string } = {}) { super(message); this.name = 'OpenAiChatGptAuthError'; this.status = options.status; this.body = options.body; } } export interface IOpenAiChatGptAuthorizationCode { authorizationCode: string; codeChallenge: string; codeVerifier: string; } interface IOpenAiChatGptTokenResponse { id_token?: unknown; access_token?: unknown; refresh_token?: unknown; } function getFetch(options: IOpenAiChatGptAuthOptions): typeof fetch { const fetchFunction = options.fetch ?? globalThis.fetch; if (!fetchFunction) { throw new OpenAiChatGptAuthError('fetch is not available for OpenAI ChatGPT authentication.'); } return fetchFunction; } function getIssuer(options: IOpenAiChatGptAuthOptions): string { return (options.issuer ?? OPENAI_CHATGPT_AUTH_ISSUER).replace(/\/+$/, ''); } function getClientId(options: IOpenAiChatGptAuthOptions): string { return options.clientId ?? OPENAI_CHATGPT_CLIENT_ID; } function asString(value: unknown, name: string): string { if (typeof value !== 'string' || value.length === 0) { throw new OpenAiChatGptAuthError(`OpenAI ChatGPT auth response is missing ${name}.`); } return value; } function asOptionalString(value: unknown): string | undefined { return typeof value === 'string' && value.length > 0 ? value : undefined; } function asIntervalSeconds(value: unknown): number { const interval = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10); if (!Number.isFinite(interval) || interval <= 0) { throw new OpenAiChatGptAuthError('OpenAI ChatGPT device-code response has an invalid interval.'); } return interval; } async function readJson(response: Response, context: string): Promise { const body = await response.text(); if (!response.ok) { throw new OpenAiChatGptAuthError(`${context} failed with status ${response.status}.`, { status: response.status, body, }); } try { return body ? JSON.parse(body) : {}; } catch (error) { throw new OpenAiChatGptAuthError(`${context} returned invalid JSON: ${(error as Error).message}`, { status: response.status, body, }); } } async function postJson(url: string, body: unknown, options: IOpenAiChatGptAuthOptions): Promise { const response = await getFetch(options)(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); return readJson(response, `POST ${url}`); } async function postForm(url: string, body: URLSearchParams, options: IOpenAiChatGptAuthOptions): Promise { const response = await getFetch(options)(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString(), }); return readJson(response, `POST ${url}`); } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } function parseJwtPayload(jwt: string): Record { const parts = jwt.split('.'); if (parts.length !== 3 || !parts[1]) { throw new OpenAiChatGptAuthError('OpenAI ChatGPT auth returned an invalid token.'); } try { return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')) as Record; } catch (error) { throw new OpenAiChatGptAuthError(`OpenAI ChatGPT token could not be parsed: ${(error as Error).message}`); } } export function parseOpenAiChatGptTokenInfo(token: string): IOpenAiChatGptTokenInfo { const claims = parseJwtPayload(token); const profile = claims['https://api.openai.com/profile'] as Record | undefined; const auth = claims['https://api.openai.com/auth'] as Record | undefined; const expiresAtSeconds = typeof claims.exp === 'number' ? claims.exp : undefined; return { email: asOptionalString(claims.email) ?? asOptionalString(profile?.email), chatgptPlanType: asOptionalString(auth?.chatgpt_plan_type), chatgptUserId: asOptionalString(auth?.chatgpt_user_id) ?? asOptionalString(auth?.user_id), chatgptAccountId: asOptionalString(auth?.chatgpt_account_id), chatgptAccountIsFedramp: auth?.chatgpt_account_is_fedramp === true, expiresAt: expiresAtSeconds ? new Date(expiresAtSeconds * 1000).toISOString() : undefined, rawJwt: token, }; } function createTokenData( response: IOpenAiChatGptTokenResponse, existingTokenData?: IOpenAiChatGptTokenData, ): IOpenAiChatGptTokenData { const accessToken = asOptionalString(response.access_token) ?? existingTokenData?.accessToken; const refreshToken = asOptionalString(response.refresh_token) ?? existingTokenData?.refreshToken; const idToken = asOptionalString(response.id_token) ?? existingTokenData?.idToken; if (!accessToken) { throw new OpenAiChatGptAuthError('OpenAI ChatGPT auth response is missing access_token.'); } if (!refreshToken) { throw new OpenAiChatGptAuthError('OpenAI ChatGPT auth response is missing refresh_token.'); } const tokenInfo = parseOpenAiChatGptTokenInfo(idToken ?? accessToken); return { accessToken, refreshToken, idToken, accountId: tokenInfo.chatgptAccountId, tokenInfo, }; } export async function requestOpenAiChatGptDeviceCode( options: IOpenAiChatGptAuthOptions = {}, ): Promise { const issuer = getIssuer(options); const response = await postJson(`${issuer}/api/accounts/deviceauth/usercode`, { client_id: getClientId(options), }, options) as Record; return { verificationUrl: `${issuer}/codex/device`, userCode: asString(response.user_code ?? response.usercode, 'user_code'), deviceAuthId: asString(response.device_auth_id, 'device_auth_id'), intervalSeconds: asIntervalSeconds(response.interval), }; } export async function pollOpenAiChatGptDeviceCode( deviceCode: IOpenAiChatGptDeviceCode, options: IOpenAiChatGptDeviceCodePollOptions = {}, ): Promise { const issuer = getIssuer(options); const pollUrl = `${issuer}/api/accounts/deviceauth/token`; const timeoutMs = options.timeoutMs ?? DEVICE_CODE_TIMEOUT_MS; const sleepFunction = options.sleep ?? sleep; const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { const response = await getFetch(options)(pollUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ device_auth_id: deviceCode.deviceAuthId, user_code: deviceCode.userCode, }), }); if (response.ok) { const body = await readJson(response, `POST ${pollUrl}`) as Record; return { authorizationCode: asString(body.authorization_code, 'authorization_code'), codeChallenge: asString(body.code_challenge, 'code_challenge'), codeVerifier: asString(body.code_verifier, 'code_verifier'), }; } if (response.status !== 403 && response.status !== 404) { const body = await response.text(); throw new OpenAiChatGptAuthError(`OpenAI ChatGPT device-code polling failed with status ${response.status}.`, { status: response.status, body, }); } await response.arrayBuffer().catch(() => undefined); const remaining = timeoutMs - (Date.now() - startedAt); await sleepFunction(Math.min(deviceCode.intervalSeconds * 1000, Math.max(remaining, 0))); } throw new OpenAiChatGptAuthError('OpenAI ChatGPT device-code login timed out.'); } export async function exchangeOpenAiChatGptAuthorizationCode( authorizationCode: IOpenAiChatGptAuthorizationCode, options: IOpenAiChatGptAuthOptions = {}, ): Promise { const issuer = getIssuer(options); const response = await postForm(`${issuer}/oauth/token`, new URLSearchParams({ grant_type: 'authorization_code', code: authorizationCode.authorizationCode, redirect_uri: `${issuer}/deviceauth/callback`, client_id: getClientId(options), code_verifier: authorizationCode.codeVerifier, }), options) as IOpenAiChatGptTokenResponse; return createTokenData(response); } export function ensureOpenAiChatGptWorkspaceAllowed( tokenData: IOpenAiChatGptTokenData, forcedChatGptWorkspaceId?: string, ): void { if (!forcedChatGptWorkspaceId) { return; } if (tokenData.tokenInfo.chatgptAccountId !== forcedChatGptWorkspaceId) { throw new OpenAiChatGptAuthError(`OpenAI ChatGPT login is restricted to workspace ${forcedChatGptWorkspaceId}.`); } } export async function completeOpenAiChatGptDeviceCodeLogin( deviceCode: IOpenAiChatGptDeviceCode, options: IOpenAiChatGptCompleteDeviceCodeOptions = {}, ): Promise { const authorizationCode = await pollOpenAiChatGptDeviceCode(deviceCode, options); const tokenData = await exchangeOpenAiChatGptAuthorizationCode(authorizationCode, options); ensureOpenAiChatGptWorkspaceAllowed(tokenData, options.forcedChatGptWorkspaceId); return tokenData; } export async function refreshOpenAiChatGptTokenData( tokenData: IOpenAiChatGptTokenData, options: IOpenAiChatGptAuthOptions = {}, ): Promise { const issuer = getIssuer(options); const response = await postJson(`${issuer}/oauth/token`, { client_id: getClientId(options), grant_type: 'refresh_token', refresh_token: tokenData.refreshToken, }, options) as IOpenAiChatGptTokenResponse; return createTokenData({ id_token: response.id_token ?? tokenData.idToken, access_token: response.access_token ?? tokenData.accessToken, refresh_token: response.refresh_token ?? tokenData.refreshToken, }, tokenData); } export function createOpenAiChatGptProviderSettings(credentials: IOpenAiChatGptAuthCredentials): { apiKey: string; baseURL: string; headers: Record; } { const accountId = credentials.accountId ?? credentials.tokenInfo?.chatgptAccountId; const isFedrampAccount = credentials.tokenInfo?.chatgptAccountIsFedramp === true; const headers: Record = { originator: credentials.originator ?? OPENAI_CHATGPT_DEFAULT_ORIGINATOR, }; if (accountId) { headers['ChatGPT-Account-ID'] = accountId; } if (isFedrampAccount) { headers['X-OpenAI-Fedramp'] = 'true'; } return { apiKey: credentials.accessToken, baseURL: credentials.baseUrl ?? OPENAI_CHATGPT_CODEX_BASE_URL, headers, }; }