feat(openai-chatgpt-auth)!: rename ChatGPT auth APIs

Add Node-only auth source helpers for SmartAI, OpenCode, and Codex credentials.
This commit is contained in:
2026-05-14 16:44:15 +00:00
parent c3664ba57f
commit 10587998f2
10 changed files with 631 additions and 144 deletions
+7
View File
@@ -2,6 +2,13 @@
## Pending
### Breaking Changes
- rename OpenAI ChatGPT/Codex subscription auth APIs from `OpenAiMax`/`openAiMaxAuth` to `OpenAiChatGpt`/`openAiChatGptAuth`
### Features
- add Node-only `@push.rocks/smartai/openai-chatgpt-auth` helpers to inspect, normalize, resolve, refresh, and write SmartAI, OpenCode, and Codex ChatGPT auth sources
## 2026-05-14 - 2.3.0
+6 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartai",
"version": "2.3.0",
"version": "3.0.0",
"private": false,
"description": "Provider registry and capability utilities for ai-sdk (Vercel AI SDK). Core export returns LanguageModel; subpath exports provide vision, audio, image, document and research capabilities.",
"main": "dist_ts/index.js",
@@ -30,6 +30,10 @@
"./research": {
"import": "./dist_ts_research/index.js",
"types": "./dist_ts_research/index.d.ts"
},
"./openai-chatgpt-auth": {
"import": "./dist_ts_openai_chatgpt_auth/index.js",
"types": "./dist_ts_openai_chatgpt_auth/index.d.ts"
}
},
"author": "Task Venture Capital GmbH",
@@ -84,6 +88,7 @@
"ts_image/**/*",
"ts_document/**/*",
"ts_research/**/*",
"ts_openai_chatgpt_auth/**/*",
"dist_*/**/*",
"assets/**/*",
".smartconfig.json",
+2 -1
View File
@@ -10,7 +10,8 @@ The package is a **provider registry** built on the Vercel AI SDK (`ai` v6). The
- Providers: anthropic, openai, google, groq, mistral, xai, perplexity, ollama
- Anthropic prompt caching via `wrapLanguageModel` middleware (enabled by default)
- Custom Ollama provider implementing `LanguageModelV3` directly (for think, num_ctx support)
- OpenAI ChatGPT/Max device-code auth in `smartai.auth.openai.ts`; `openAiMaxAuth` routes OpenAI models to the ChatGPT Codex backend
- OpenAI ChatGPT/Codex device-code auth in `smartai.auth.openai.ts`; `openAiChatGptAuth` routes OpenAI models to the ChatGPT Codex backend
- Node-only local auth source helpers live in `ts_openai_chatgpt_auth/` and support SmartAI, OpenCode, and Codex auth file formats
### Subpath Exports
- `@push.rocks/smartai/vision``analyzeImage()` using `generateText` with image content
+39 -8
View File
@@ -107,29 +107,60 @@ console.log(result.text);
OpenAI `reasoningEffort` supports `'none'`, `'minimal'`, `'low'`, `'medium'`, `'high'`, and `'xhigh'`. Model IDs are accepted as strings, so new IDs like `'gpt-5.5'` can be used before upstream model unions are updated.
### OpenAI Max / ChatGPT Auth
### OpenAI ChatGPT / Codex Auth
SmartAI can request ChatGPT subscription-backed Codex credentials with OpenAI's device-code flow. The returned credentials are passed to `getModel()` through `openAiMaxAuth`; SmartAI then routes OpenAI model calls through the ChatGPT Codex backend with the required account headers.
SmartAI can request ChatGPT subscription-backed Codex credentials with OpenAI's device-code flow. The returned credentials are passed to `getModel()` through `openAiChatGptAuth`; SmartAI then routes OpenAI model calls through the ChatGPT Codex backend with the required account headers.
```typescript
import {
completeOpenAiMaxDeviceCodeLogin,
completeOpenAiChatGptDeviceCodeLogin,
getModel,
requestOpenAiMaxDeviceCode,
requestOpenAiChatGptDeviceCode,
} from '@push.rocks/smartai';
const deviceCode = await requestOpenAiMaxDeviceCode();
const deviceCode = await requestOpenAiChatGptDeviceCode();
console.log(`Open ${deviceCode.verificationUrl} and enter ${deviceCode.userCode}`);
const openAiMaxAuth = await completeOpenAiMaxDeviceCodeLogin(deviceCode);
const openAiChatGptAuth = await completeOpenAiChatGptDeviceCodeLogin(deviceCode);
const model = getModel({
provider: 'openai',
model: 'gpt-5.5',
openAiMaxAuth,
openAiChatGptAuth,
});
```
Use `refreshOpenAiMaxTokenData(openAiMaxAuth)` before stored credentials expire, or after receiving an unauthorized response.
Use `refreshOpenAiChatGptTokenData(openAiChatGptAuth)` before stored credentials expire, or after receiving an unauthorized response.
Node.js consumers can inspect and resolve local ChatGPT auth files through the Node-only subpath. This supports SmartAI's canonical auth file, OpenCode's `~/.local/share/opencode/auth.json`, and Codex's `~/.codex/auth.json` without exposing token values in inspection results.
```typescript
import {
inspectOpenAiChatGptAuthSources,
resolveOpenAiChatGptAuth,
} from '@push.rocks/smartai/openai-chatgpt-auth';
const sources = await inspectOpenAiChatGptAuthSources({
sources: ['smartai', 'opencode', 'codex'],
});
const resolved = await resolveOpenAiChatGptAuth({
sources: ['smartai', 'opencode', 'codex'],
refresh: 'ifNeeded',
writeBack: {
smartai: true,
opencode: false,
codex: false,
},
});
if (resolved) {
const model = getModel({
provider: 'openai',
model: 'gpt-5.5',
openAiChatGptAuth: resolved.tokenData,
});
}
```
### Re-exported AI SDK Functions
+107 -23
View File
@@ -1,6 +1,15 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import * as smartai from '../ts/index.js';
import type { IOpenAiMaxTokenData } from '../ts/index.js';
import type { IOpenAiChatGptTokenData } from '../ts/index.js';
import {
inspectOpenAiChatGptAuthSources,
normalizeOpenAiChatGptAuth,
resolveOpenAiChatGptAuth,
writeOpenAiChatGptAuthFile,
} from '../ts_openai_chatgpt_auth/index.js';
interface IMockFetchRequest {
url: string;
@@ -12,10 +21,10 @@ function createJwt(payload: Record<string, unknown>): string {
return `${encode({ alg: 'none', typ: 'JWT' })}.${encode(payload)}.sig`;
}
function createTokenData(accountId = 'workspace-1'): IOpenAiMaxTokenData {
const idToken = createJwt({
function createTokenData(accountId = 'workspace-1', expiresAtSeconds = 4_102_444_800): IOpenAiChatGptTokenData {
const accessToken = createJwt({
email: 'user@example.com',
exp: 4_102_444_800,
exp: expiresAtSeconds,
'https://api.openai.com/auth': {
chatgpt_plan_type: 'pro',
chatgpt_user_id: 'user-1',
@@ -23,13 +32,12 @@ function createTokenData(accountId = 'workspace-1'): IOpenAiMaxTokenData {
chatgpt_account_is_fedramp: false,
},
});
const idTokenInfo = smartai.parseOpenAiMaxIdToken(idToken);
const tokenInfo = smartai.parseOpenAiChatGptTokenInfo(accessToken);
return {
idToken,
accessToken: 'access-token',
accessToken,
refreshToken: 'refresh-token',
accountId,
idTokenInfo,
tokenInfo,
};
}
@@ -44,7 +52,7 @@ function getHeader(init: RequestInit | undefined, name: string): string | null {
return new Headers(init?.headers).get(name);
}
tap.test('requestOpenAiMaxDeviceCode requests a user code', async () => {
tap.test('requestOpenAiChatGptDeviceCode requests a user code', async () => {
const originalFetch = globalThis.fetch;
const requests: IMockFetchRequest[] = [];
@@ -58,7 +66,7 @@ tap.test('requestOpenAiMaxDeviceCode requests a user code', async () => {
};
try {
const deviceCode = await smartai.requestOpenAiMaxDeviceCode({
const deviceCode = await smartai.requestOpenAiChatGptDeviceCode({
issuer: 'https://auth.example.test',
clientId: 'client-1',
});
@@ -76,7 +84,7 @@ tap.test('requestOpenAiMaxDeviceCode requests a user code', async () => {
}
});
tap.test('completeOpenAiMaxDeviceCodeLogin polls and exchanges OAuth tokens', async () => {
tap.test('completeOpenAiChatGptDeviceCodeLogin polls and exchanges OAuth tokens', async () => {
const originalFetch = globalThis.fetch;
const requests: IMockFetchRequest[] = [];
const tokenData = createTokenData('workspace-1');
@@ -88,7 +96,6 @@ tap.test('completeOpenAiMaxDeviceCodeLogin polls and exchanges OAuth tokens', as
code_verifier: 'verifier',
}),
jsonResponse({
id_token: tokenData.idToken,
access_token: tokenData.accessToken,
refresh_token: tokenData.refreshToken,
}),
@@ -102,7 +109,7 @@ tap.test('completeOpenAiMaxDeviceCodeLogin polls and exchanges OAuth tokens', as
};
try {
const result = await smartai.completeOpenAiMaxDeviceCodeLogin({
const result = await smartai.completeOpenAiChatGptDeviceCodeLogin({
verificationUrl: 'https://auth.example.test/codex/device',
userCode: 'ABCD-EFGH',
deviceAuthId: 'device-1',
@@ -114,9 +121,9 @@ tap.test('completeOpenAiMaxDeviceCodeLogin polls and exchanges OAuth tokens', as
sleep: async () => undefined,
});
expect(result.accessToken).toEqual('access-token');
expect(result.accessToken).toEqual(tokenData.accessToken);
expect(result.refreshToken).toEqual('refresh-token');
expect(result.idTokenInfo.chatgptAccountId).toEqual('workspace-1');
expect(result.tokenInfo.chatgptAccountId).toEqual('workspace-1');
expect(requests.length).toEqual(3);
expect(JSON.parse(String(requests[0].init?.body))).toEqual({
device_auth_id: 'device-1',
@@ -133,25 +140,25 @@ tap.test('completeOpenAiMaxDeviceCodeLogin polls and exchanges OAuth tokens', as
}
});
tap.test('refreshOpenAiMaxTokenData refreshes and preserves omitted token fields', async () => {
tap.test('refreshOpenAiChatGptTokenData refreshes and preserves omitted token fields', async () => {
const originalFetch = globalThis.fetch;
const requests: IMockFetchRequest[] = [];
const tokenData = createTokenData('workspace-1');
const refreshedToken = createTokenData('workspace-1', 4_102_445_000).accessToken;
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
requests.push({ url: String(input), init });
return jsonResponse({ access_token: 'new-access-token' });
return jsonResponse({ access_token: refreshedToken });
};
try {
const result = await smartai.refreshOpenAiMaxTokenData(tokenData, {
const result = await smartai.refreshOpenAiChatGptTokenData(tokenData, {
issuer: 'https://auth.example.test',
clientId: 'client-1',
});
expect(result.accessToken).toEqual('new-access-token');
expect(result.accessToken).toEqual(refreshedToken);
expect(result.refreshToken).toEqual('refresh-token');
expect(result.idToken).toEqual(tokenData.idToken);
expect(JSON.parse(String(requests[0].init?.body))).toEqual({
client_id: 'client-1',
grant_type: 'refresh_token',
@@ -162,14 +169,14 @@ tap.test('refreshOpenAiMaxTokenData refreshes and preserves omitted token fields
}
});
tap.test('getModel uses ChatGPT Codex backend for OpenAI Max auth', async () => {
tap.test('getModel uses ChatGPT Codex backend for OpenAI ChatGPT auth', async () => {
const originalFetch = globalThis.fetch;
let capturedRequest: IMockFetchRequest | undefined;
const tokenData = createTokenData('workspace-1');
const model = smartai.getModel({
provider: 'openai',
model: 'gpt-5.5',
openAiMaxAuth: tokenData,
openAiChatGptAuth: tokenData,
});
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
@@ -198,7 +205,7 @@ tap.test('getModel uses ChatGPT Codex backend for OpenAI Max auth', async () =>
} as any);
expect(capturedRequest?.url).toEqual('https://chatgpt.com/backend-api/codex/responses');
expect(getHeader(capturedRequest?.init, 'authorization')).toEqual('Bearer access-token');
expect(getHeader(capturedRequest?.init, 'authorization')).toEqual(`Bearer ${tokenData.accessToken}`);
expect(getHeader(capturedRequest?.init, 'chatgpt-account-id')).toEqual('workspace-1');
expect(getHeader(capturedRequest?.init, 'originator')).toEqual('smartai');
} finally {
@@ -206,4 +213,81 @@ tap.test('getModel uses ChatGPT Codex backend for OpenAI Max auth', async () =>
}
});
tap.test('normalizes OpenCode and Codex auth file formats', async () => {
const tokenData = createTokenData('workspace-2');
const opencodeAuth = normalizeOpenAiChatGptAuth({
openai: {
type: 'oauth',
access: tokenData.accessToken,
refresh: tokenData.refreshToken,
expires: Date.parse(tokenData.tokenInfo.expiresAt!),
accountId: 'workspace-2',
},
}, 'opencode');
const codexAuth = normalizeOpenAiChatGptAuth({
tokens: {
access_token: tokenData.accessToken,
refresh_token: tokenData.refreshToken,
account_id: 'workspace-2',
},
}, 'codex');
expect(opencodeAuth?.accountId).toEqual('workspace-2');
expect(opencodeAuth?.tokenInfo.chatgptAccountId).toEqual('workspace-2');
expect(codexAuth?.accountId).toEqual('workspace-2');
expect(codexAuth?.tokenInfo.chatgptAccountId).toEqual('workspace-2');
});
tap.test('inspects and resolves OpenAI ChatGPT auth sources without exposing tokens', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'smartai-auth-sources-'));
try {
const tokenData = createTokenData('workspace-3');
const opencodePath = path.join(tempDir, 'opencode-auth.json');
await fs.writeFile(opencodePath, JSON.stringify({
openai: {
type: 'oauth',
access: tokenData.accessToken,
refresh: tokenData.refreshToken,
expires: Date.parse(tokenData.tokenInfo.expiresAt!),
accountId: 'workspace-3',
},
anthropic: { type: 'oauth', access: 'keep-me' },
}));
const inspections = await inspectOpenAiChatGptAuthSources({
sources: [{ source: 'opencode', filePath: opencodePath }],
});
const resolved = await resolveOpenAiChatGptAuth({
sources: [{ source: 'opencode', filePath: opencodePath }],
});
expect(inspections).toHaveLength(1);
expect(inspections[0]!.usable).toEqual(true);
expect(JSON.stringify(inspections)).not.toInclude(tokenData.accessToken);
expect(resolved?.source).toEqual('opencode');
expect(resolved?.tokenData.accountId).toEqual('workspace-3');
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
tap.test('writes OpenCode auth while preserving unrelated providers', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'smartai-opencode-write-'));
try {
const tokenData = createTokenData('workspace-4');
const opencodePath = path.join(tempDir, 'auth.json');
await fs.writeFile(opencodePath, JSON.stringify({ anthropic: { type: 'oauth', access: 'keep-me' } }));
await writeOpenAiChatGptAuthFile(opencodePath, tokenData, 'opencode');
const written = JSON.parse(await fs.readFile(opencodePath, 'utf8')) as any;
expect(written.anthropic.access).toEqual('keep-me');
expect(written.openai.type).toEqual('oauth');
expect(written.openai.accountId).toEqual('workspace-4');
expect(written.openai.access).toEqual(tokenData.accessToken);
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
export default tap.start();
+21 -21
View File
@@ -1,13 +1,13 @@
export { getModel, getModelSetup } from './smartai.classes.smartai.js';
export type {
IOpenAiProviderOptions,
IOpenAiMaxAuthCredentials,
IOpenAiMaxAuthOptions,
IOpenAiMaxCompleteDeviceCodeOptions,
IOpenAiMaxDeviceCode,
IOpenAiMaxDeviceCodePollOptions,
IOpenAiMaxIdTokenInfo,
IOpenAiMaxTokenData,
IOpenAiChatGptAuthCredentials,
IOpenAiChatGptAuthOptions,
IOpenAiChatGptCompleteDeviceCodeOptions,
IOpenAiChatGptDeviceCode,
IOpenAiChatGptDeviceCodePollOptions,
IOpenAiChatGptTokenData,
IOpenAiChatGptTokenInfo,
ISmartAiModelSetup,
ISmartAiOptions,
TOpenAiReasoningEffort,
@@ -36,21 +36,21 @@ export type {
} from './smartai.cache.js';
export { createOllamaModel } from './smartai.provider.ollama.js';
export {
OPENAI_MAX_AUTH_ISSUER,
OPENAI_MAX_CLIENT_ID,
OPENAI_MAX_CODEX_BASE_URL,
OPENAI_MAX_DEFAULT_ORIGINATOR,
OpenAiMaxAuthError,
completeOpenAiMaxDeviceCodeLogin,
createOpenAiMaxProviderSettings,
ensureOpenAiMaxWorkspaceAllowed,
exchangeOpenAiMaxAuthorizationCode,
parseOpenAiMaxIdToken,
pollOpenAiMaxDeviceCode,
refreshOpenAiMaxTokenData,
requestOpenAiMaxDeviceCode,
OPENAI_CHATGPT_AUTH_ISSUER,
OPENAI_CHATGPT_CLIENT_ID,
OPENAI_CHATGPT_CODEX_BASE_URL,
OPENAI_CHATGPT_DEFAULT_ORIGINATOR,
OpenAiChatGptAuthError,
completeOpenAiChatGptDeviceCodeLogin,
createOpenAiChatGptProviderSettings,
ensureOpenAiChatGptWorkspaceAllowed,
exchangeOpenAiChatGptAuthorizationCode,
parseOpenAiChatGptTokenInfo,
pollOpenAiChatGptDeviceCode,
refreshOpenAiChatGptTokenData,
requestOpenAiChatGptDeviceCode,
} from './smartai.auth.openai.js';
export type { IOpenAiMaxAuthorizationCode } from './smartai.auth.openai.js';
export type { IOpenAiChatGptAuthorizationCode } from './smartai.auth.openai.js';
// Re-export commonly used ai-sdk functions for consumer convenience
export { generateText, streamText, tool, jsonSchema } from 'ai';
+85 -76
View File
@@ -1,63 +1,63 @@
import type {
IOpenAiMaxAuthCredentials,
IOpenAiMaxAuthOptions,
IOpenAiMaxCompleteDeviceCodeOptions,
IOpenAiMaxDeviceCode,
IOpenAiMaxDeviceCodePollOptions,
IOpenAiMaxIdTokenInfo,
IOpenAiMaxTokenData,
IOpenAiChatGptAuthCredentials,
IOpenAiChatGptAuthOptions,
IOpenAiChatGptCompleteDeviceCodeOptions,
IOpenAiChatGptDeviceCode,
IOpenAiChatGptDeviceCodePollOptions,
IOpenAiChatGptTokenData,
IOpenAiChatGptTokenInfo,
} from './smartai.interfaces.js';
export const OPENAI_MAX_AUTH_ISSUER = 'https://auth.openai.com';
export const OPENAI_MAX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
export const OPENAI_MAX_CODEX_BASE_URL = 'https://chatgpt.com/backend-api/codex';
export const OPENAI_MAX_DEFAULT_ORIGINATOR = 'smartai';
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 OpenAiMaxAuthError extends Error {
export class OpenAiChatGptAuthError extends Error {
public status?: number;
public body?: string;
constructor(message: string, options: { status?: number; body?: string } = {}) {
super(message);
this.name = 'OpenAiMaxAuthError';
this.name = 'OpenAiChatGptAuthError';
this.status = options.status;
this.body = options.body;
}
}
export interface IOpenAiMaxAuthorizationCode {
export interface IOpenAiChatGptAuthorizationCode {
authorizationCode: string;
codeChallenge: string;
codeVerifier: string;
}
interface IOpenAiMaxTokenResponse {
interface IOpenAiChatGptTokenResponse {
id_token?: unknown;
access_token?: unknown;
refresh_token?: unknown;
}
function getFetch(options: IOpenAiMaxAuthOptions): typeof fetch {
function getFetch(options: IOpenAiChatGptAuthOptions): typeof fetch {
const fetchFunction = options.fetch ?? globalThis.fetch;
if (!fetchFunction) {
throw new OpenAiMaxAuthError('fetch is not available for OpenAI Max authentication.');
throw new OpenAiChatGptAuthError('fetch is not available for OpenAI ChatGPT authentication.');
}
return fetchFunction;
}
function getIssuer(options: IOpenAiMaxAuthOptions): string {
return (options.issuer ?? OPENAI_MAX_AUTH_ISSUER).replace(/\/+$/, '');
function getIssuer(options: IOpenAiChatGptAuthOptions): string {
return (options.issuer ?? OPENAI_CHATGPT_AUTH_ISSUER).replace(/\/+$/, '');
}
function getClientId(options: IOpenAiMaxAuthOptions): string {
return options.clientId ?? OPENAI_MAX_CLIENT_ID;
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 OpenAiMaxAuthError(`OpenAI Max auth response is missing ${name}.`);
throw new OpenAiChatGptAuthError(`OpenAI ChatGPT auth response is missing ${name}.`);
}
return value;
}
@@ -69,7 +69,7 @@ function asOptionalString(value: unknown): string | 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 OpenAiMaxAuthError('OpenAI Max device-code response has an invalid interval.');
throw new OpenAiChatGptAuthError('OpenAI ChatGPT device-code response has an invalid interval.');
}
return interval;
}
@@ -77,7 +77,7 @@ function asIntervalSeconds(value: unknown): number {
async function readJson(response: Response, context: string): Promise<unknown> {
const body = await response.text();
if (!response.ok) {
throw new OpenAiMaxAuthError(`${context} failed with status ${response.status}.`, {
throw new OpenAiChatGptAuthError(`${context} failed with status ${response.status}.`, {
status: response.status,
body,
});
@@ -86,14 +86,14 @@ async function readJson(response: Response, context: string): Promise<unknown> {
try {
return body ? JSON.parse(body) : {};
} catch (error) {
throw new OpenAiMaxAuthError(`${context} returned invalid JSON: ${(error as Error).message}`, {
throw new OpenAiChatGptAuthError(`${context} returned invalid JSON: ${(error as Error).message}`, {
status: response.status,
body,
});
}
}
async function postJson(url: string, body: unknown, options: IOpenAiMaxAuthOptions): Promise<unknown> {
async function postJson(url: string, body: unknown, options: IOpenAiChatGptAuthOptions): Promise<unknown> {
const response = await getFetch(options)(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -102,7 +102,7 @@ async function postJson(url: string, body: unknown, options: IOpenAiMaxAuthOptio
return readJson(response, `POST ${url}`);
}
async function postForm(url: string, body: URLSearchParams, options: IOpenAiMaxAuthOptions): Promise<unknown> {
async function postForm(url: string, body: URLSearchParams, options: IOpenAiChatGptAuthOptions): Promise<unknown> {
const response = await getFetch(options)(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
@@ -118,18 +118,18 @@ function sleep(ms: number): Promise<void> {
function parseJwtPayload(jwt: string): Record<string, unknown> {
const parts = jwt.split('.');
if (parts.length !== 3 || !parts[1]) {
throw new OpenAiMaxAuthError('OpenAI Max auth returned an invalid ID token.');
throw new OpenAiChatGptAuthError('OpenAI ChatGPT auth returned an invalid token.');
}
try {
return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')) as Record<string, unknown>;
} catch (error) {
throw new OpenAiMaxAuthError(`OpenAI Max ID token could not be parsed: ${(error as Error).message}`);
throw new OpenAiChatGptAuthError(`OpenAI ChatGPT token could not be parsed: ${(error as Error).message}`);
}
}
export function parseOpenAiMaxIdToken(idToken: string): IOpenAiMaxIdTokenInfo {
const claims = parseJwtPayload(idToken);
export function parseOpenAiChatGptTokenInfo(token: string): IOpenAiChatGptTokenInfo {
const claims = parseJwtPayload(token);
const profile = claims['https://api.openai.com/profile'] as Record<string, unknown> | undefined;
const auth = claims['https://api.openai.com/auth'] as Record<string, unknown> | undefined;
const expiresAtSeconds = typeof claims.exp === 'number' ? claims.exp : undefined;
@@ -141,28 +141,37 @@ export function parseOpenAiMaxIdToken(idToken: string): IOpenAiMaxIdTokenInfo {
chatgptAccountId: asOptionalString(auth?.chatgpt_account_id),
chatgptAccountIsFedramp: auth?.chatgpt_account_is_fedramp === true,
expiresAt: expiresAtSeconds ? new Date(expiresAtSeconds * 1000).toISOString() : undefined,
rawJwt: idToken,
rawJwt: token,
};
}
function createTokenData(response: IOpenAiMaxTokenResponse): IOpenAiMaxTokenData {
const idToken = asString(response.id_token, 'id_token');
const accessToken = asString(response.access_token, 'access_token');
const refreshToken = asString(response.refresh_token, 'refresh_token');
const idTokenInfo = parseOpenAiMaxIdToken(idToken);
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 {
idToken,
accessToken,
refreshToken,
accountId: idTokenInfo.chatgptAccountId,
idTokenInfo,
idToken,
accountId: tokenInfo.chatgptAccountId,
tokenInfo,
};
}
export async function requestOpenAiMaxDeviceCode(
options: IOpenAiMaxAuthOptions = {},
): Promise<IOpenAiMaxDeviceCode> {
export async function requestOpenAiChatGptDeviceCode(
options: IOpenAiChatGptAuthOptions = {},
): Promise<IOpenAiChatGptDeviceCode> {
const issuer = getIssuer(options);
const response = await postJson(`${issuer}/api/accounts/deviceauth/usercode`, {
client_id: getClientId(options),
@@ -176,10 +185,10 @@ export async function requestOpenAiMaxDeviceCode(
};
}
export async function pollOpenAiMaxDeviceCode(
deviceCode: IOpenAiMaxDeviceCode,
options: IOpenAiMaxDeviceCodePollOptions = {},
): Promise<IOpenAiMaxAuthorizationCode> {
export async function pollOpenAiChatGptDeviceCode(
deviceCode: IOpenAiChatGptDeviceCode,
options: IOpenAiChatGptDeviceCodePollOptions = {},
): Promise<IOpenAiChatGptAuthorizationCode> {
const issuer = getIssuer(options);
const pollUrl = `${issuer}/api/accounts/deviceauth/token`;
const timeoutMs = options.timeoutMs ?? DEVICE_CODE_TIMEOUT_MS;
@@ -207,7 +216,7 @@ export async function pollOpenAiMaxDeviceCode(
if (response.status !== 403 && response.status !== 404) {
const body = await response.text();
throw new OpenAiMaxAuthError(`OpenAI Max device-code polling failed with status ${response.status}.`, {
throw new OpenAiChatGptAuthError(`OpenAI ChatGPT device-code polling failed with status ${response.status}.`, {
status: response.status,
body,
});
@@ -218,13 +227,13 @@ export async function pollOpenAiMaxDeviceCode(
await sleepFunction(Math.min(deviceCode.intervalSeconds * 1000, Math.max(remaining, 0)));
}
throw new OpenAiMaxAuthError('OpenAI Max device-code login timed out.');
throw new OpenAiChatGptAuthError('OpenAI ChatGPT device-code login timed out.');
}
export async function exchangeOpenAiMaxAuthorizationCode(
authorizationCode: IOpenAiMaxAuthorizationCode,
options: IOpenAiMaxAuthOptions = {},
): Promise<IOpenAiMaxTokenData> {
export async function exchangeOpenAiChatGptAuthorizationCode(
authorizationCode: IOpenAiChatGptAuthorizationCode,
options: IOpenAiChatGptAuthOptions = {},
): Promise<IOpenAiChatGptTokenData> {
const issuer = getIssuer(options);
const response = await postForm(`${issuer}/oauth/token`, new URLSearchParams({
grant_type: 'authorization_code',
@@ -232,60 +241,60 @@ export async function exchangeOpenAiMaxAuthorizationCode(
redirect_uri: `${issuer}/deviceauth/callback`,
client_id: getClientId(options),
code_verifier: authorizationCode.codeVerifier,
}), options) as IOpenAiMaxTokenResponse;
}), options) as IOpenAiChatGptTokenResponse;
return createTokenData(response);
}
export function ensureOpenAiMaxWorkspaceAllowed(
tokenData: IOpenAiMaxTokenData,
export function ensureOpenAiChatGptWorkspaceAllowed(
tokenData: IOpenAiChatGptTokenData,
forcedChatGptWorkspaceId?: string,
): void {
if (!forcedChatGptWorkspaceId) {
return;
}
if (tokenData.idTokenInfo.chatgptAccountId !== forcedChatGptWorkspaceId) {
throw new OpenAiMaxAuthError(`OpenAI Max login is restricted to workspace ${forcedChatGptWorkspaceId}.`);
if (tokenData.tokenInfo.chatgptAccountId !== forcedChatGptWorkspaceId) {
throw new OpenAiChatGptAuthError(`OpenAI ChatGPT login is restricted to workspace ${forcedChatGptWorkspaceId}.`);
}
}
export async function completeOpenAiMaxDeviceCodeLogin(
deviceCode: IOpenAiMaxDeviceCode,
options: IOpenAiMaxCompleteDeviceCodeOptions = {},
): Promise<IOpenAiMaxTokenData> {
const authorizationCode = await pollOpenAiMaxDeviceCode(deviceCode, options);
const tokenData = await exchangeOpenAiMaxAuthorizationCode(authorizationCode, options);
ensureOpenAiMaxWorkspaceAllowed(tokenData, options.forcedChatGptWorkspaceId);
export async function completeOpenAiChatGptDeviceCodeLogin(
deviceCode: IOpenAiChatGptDeviceCode,
options: IOpenAiChatGptCompleteDeviceCodeOptions = {},
): Promise<IOpenAiChatGptTokenData> {
const authorizationCode = await pollOpenAiChatGptDeviceCode(deviceCode, options);
const tokenData = await exchangeOpenAiChatGptAuthorizationCode(authorizationCode, options);
ensureOpenAiChatGptWorkspaceAllowed(tokenData, options.forcedChatGptWorkspaceId);
return tokenData;
}
export async function refreshOpenAiMaxTokenData(
tokenData: IOpenAiMaxTokenData,
options: IOpenAiMaxAuthOptions = {},
): Promise<IOpenAiMaxTokenData> {
export async function refreshOpenAiChatGptTokenData(
tokenData: IOpenAiChatGptTokenData,
options: IOpenAiChatGptAuthOptions = {},
): Promise<IOpenAiChatGptTokenData> {
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 IOpenAiMaxTokenResponse;
}, 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 createOpenAiMaxProviderSettings(credentials: IOpenAiMaxAuthCredentials): {
export function createOpenAiChatGptProviderSettings(credentials: IOpenAiChatGptAuthCredentials): {
apiKey: string;
baseURL: string;
headers: Record<string, string>;
} {
const accountId = credentials.accountId ?? credentials.idTokenInfo?.chatgptAccountId;
const isFedrampAccount = credentials.idTokenInfo?.chatgptAccountIsFedramp === true;
const accountId = credentials.accountId ?? credentials.tokenInfo?.chatgptAccountId;
const isFedrampAccount = credentials.tokenInfo?.chatgptAccountIsFedramp === true;
const headers: Record<string, string> = {
originator: credentials.originator ?? OPENAI_MAX_DEFAULT_ORIGINATOR,
originator: credentials.originator ?? OPENAI_CHATGPT_DEFAULT_ORIGINATOR,
};
if (accountId) {
@@ -297,7 +306,7 @@ export function createOpenAiMaxProviderSettings(credentials: IOpenAiMaxAuthCrede
return {
apiKey: credentials.accessToken,
baseURL: credentials.baseUrl ?? OPENAI_MAX_CODEX_BASE_URL,
baseURL: credentials.baseUrl ?? OPENAI_CHATGPT_CODEX_BASE_URL,
headers,
};
}
+3 -3
View File
@@ -2,7 +2,7 @@ import * as plugins from './plugins.js';
import type { ISmartAiModelSetup, ISmartAiOptions, LanguageModelV3 } from './smartai.interfaces.js';
import { createOllamaModel } from './smartai.provider.ollama.js';
import { createAnthropicCachingMiddleware } from './smartai.middleware.anthropic.js';
import { createOpenAiMaxProviderSettings } from './smartai.auth.openai.js';
import { createOpenAiChatGptProviderSettings } from './smartai.auth.openai.js';
/**
* Returns a LanguageModelV3 for the given provider and model.
@@ -24,8 +24,8 @@ export function getModel(options: ISmartAiOptions): LanguageModelV3 {
}
case 'openai': {
const p = plugins.createOpenAI(
options.openAiMaxAuth
? createOpenAiMaxProviderSettings(options.openAiMaxAuth)
options.openAiChatGptAuth
? createOpenAiChatGptProviderSettings(options.openAiChatGptAuth)
: { apiKey: options.apiKey },
);
return p(options.model) as LanguageModelV3;
+10 -11
View File
@@ -15,7 +15,7 @@ export type TOpenAiReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'hi
export type TOpenAiTextVerbosity = 'low' | 'medium' | 'high';
export interface IOpenAiMaxIdTokenInfo {
export interface IOpenAiChatGptTokenInfo {
email?: string;
chatgptPlanType?: string;
chatgptUserId?: string;
@@ -25,41 +25,40 @@ export interface IOpenAiMaxIdTokenInfo {
rawJwt: string;
}
export interface IOpenAiMaxAuthCredentials {
export interface IOpenAiChatGptAuthCredentials {
accessToken: string;
refreshToken?: string;
idToken?: string;
accountId?: string;
idTokenInfo?: IOpenAiMaxIdTokenInfo;
tokenInfo?: IOpenAiChatGptTokenInfo;
baseUrl?: string;
originator?: string;
}
export interface IOpenAiMaxTokenData extends IOpenAiMaxAuthCredentials {
export interface IOpenAiChatGptTokenData extends IOpenAiChatGptAuthCredentials {
refreshToken: string;
idToken: string;
idTokenInfo: IOpenAiMaxIdTokenInfo;
tokenInfo: IOpenAiChatGptTokenInfo;
}
export interface IOpenAiMaxDeviceCode {
export interface IOpenAiChatGptDeviceCode {
verificationUrl: string;
userCode: string;
deviceAuthId: string;
intervalSeconds: number;
}
export interface IOpenAiMaxAuthOptions {
export interface IOpenAiChatGptAuthOptions {
issuer?: string;
clientId?: string;
fetch?: typeof fetch;
}
export interface IOpenAiMaxDeviceCodePollOptions extends IOpenAiMaxAuthOptions {
export interface IOpenAiChatGptDeviceCodePollOptions extends IOpenAiChatGptAuthOptions {
timeoutMs?: number;
sleep?: (ms: number) => Promise<void>;
}
export interface IOpenAiMaxCompleteDeviceCodeOptions extends IOpenAiMaxDeviceCodePollOptions {
export interface IOpenAiChatGptCompleteDeviceCodeOptions extends IOpenAiChatGptDeviceCodePollOptions {
forcedChatGptWorkspaceId?: string;
}
@@ -108,7 +107,7 @@ export interface ISmartAiOptions {
* OpenAI ChatGPT/Codex subscription credentials from the device-code auth flow.
* Only used when provider === 'openai'.
*/
openAiMaxAuth?: IOpenAiMaxAuthCredentials;
openAiChatGptAuth?: IOpenAiChatGptAuthCredentials;
/**
* Provider-specific AI SDK generation options.
* Pass this to generateText()/streamText() alongside the model.
+351
View File
@@ -0,0 +1,351 @@
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import {
parseOpenAiChatGptTokenInfo,
refreshOpenAiChatGptTokenData,
} from '../ts/smartai.auth.openai.js';
import type {
IOpenAiChatGptAuthOptions,
IOpenAiChatGptTokenData,
IOpenAiChatGptTokenInfo,
} from '../ts/smartai.interfaces.js';
export type TOpenAiChatGptAuthSource = 'smartai' | 'opencode' | 'codex';
export type TOpenAiChatGptAuthFileFormat = TOpenAiChatGptAuthSource | 'auto';
export interface IOpenAiChatGptAuthSourceConfig {
source: TOpenAiChatGptAuthSource;
filePath?: string;
format?: TOpenAiChatGptAuthFileFormat;
writeBack?: boolean;
}
export interface IOpenAiChatGptAuthSourceInspection {
source: TOpenAiChatGptAuthSource;
filePath: string;
exists: boolean;
usable: boolean;
expired?: boolean;
accountId?: string;
email?: string;
plan?: string;
expiresAt?: string;
error?: string;
}
export interface IInspectOpenAiChatGptAuthSourcesOptions {
sources?: Array<TOpenAiChatGptAuthSource | IOpenAiChatGptAuthSourceConfig>;
homeDir?: string;
now?: Date;
}
export interface IResolveOpenAiChatGptAuthOptions extends IInspectOpenAiChatGptAuthSourcesOptions {
refresh?: 'ifNeeded' | false;
writeBack?: Partial<Record<TOpenAiChatGptAuthSource, boolean>>;
authOptions?: IOpenAiChatGptAuthOptions;
}
export interface IResolvedOpenAiChatGptAuth {
source: TOpenAiChatGptAuthSource;
filePath: string;
tokenData: IOpenAiChatGptTokenData;
refreshed: boolean;
}
interface INormalizedOpenAiChatGptAuthSourceConfig {
source: TOpenAiChatGptAuthSource;
filePath: string;
format: TOpenAiChatGptAuthFileFormat;
writeBack?: boolean;
}
const defaultSources: TOpenAiChatGptAuthSource[] = ['smartai', 'opencode', 'codex'];
const refreshWindowMs = 5 * 60 * 1000;
export const getDefaultOpenAiChatGptAuthPath = (
source: TOpenAiChatGptAuthSource,
homeDir = os.homedir(),
): string => {
switch (source) {
case 'smartai':
return path.join(homeDir, '.git.zone', 'ide', 'openai-chatgpt-auth.json');
case 'opencode':
return path.join(homeDir, '.local', 'share', 'opencode', 'auth.json');
case 'codex':
return path.join(homeDir, '.codex', 'auth.json');
default:
throw new Error(`Unsupported OpenAI ChatGPT auth source: ${source satisfies never}`);
}
};
export const normalizeOpenAiChatGptAuth = (
input: unknown,
format: TOpenAiChatGptAuthFileFormat = 'auto',
): IOpenAiChatGptTokenData | undefined => {
if (format === 'auto') {
return normalizeOpenAiChatGptAuth(input, 'smartai')
?? normalizeOpenAiChatGptAuth(input, 'opencode')
?? normalizeOpenAiChatGptAuth(input, 'codex');
}
if (!isRecord(input)) return undefined;
if (format === 'opencode') {
return normalizeOpenCodeAuth(input);
}
return normalizeTokenObject(isRecord(input.tokens) ? input.tokens : input);
};
export const readOpenAiChatGptAuthFile = async (
filePath: string,
format: TOpenAiChatGptAuthFileFormat = 'auto',
): Promise<IOpenAiChatGptTokenData | undefined> => {
const parsed = JSON.parse(await fs.readFile(filePath, 'utf8')) as unknown;
return normalizeOpenAiChatGptAuth(parsed, format);
};
export const inspectOpenAiChatGptAuthSources = async (
options: IInspectOpenAiChatGptAuthSourcesOptions = {},
): Promise<IOpenAiChatGptAuthSourceInspection[]> => {
const now = options.now ?? new Date();
const sourceConfigs = normalizeSourceConfigs(options.sources, options.homeDir);
return Promise.all(sourceConfigs.map(async (sourceConfig) => {
try {
const tokenData = await readOpenAiChatGptAuthFile(sourceConfig.filePath!, sourceConfig.format ?? sourceConfig.source);
if (!tokenData) {
return {
source: sourceConfig.source,
filePath: sourceConfig.filePath!,
exists: true,
usable: false,
error: 'No OpenAI ChatGPT auth token found in file.',
};
}
return toInspection(sourceConfig.source, sourceConfig.filePath!, tokenData, now);
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
return {
source: sourceConfig.source,
filePath: sourceConfig.filePath!,
exists: nodeError.code !== 'ENOENT',
usable: false,
error: nodeError.code === 'ENOENT' ? undefined : nodeError.message,
};
}
}));
};
export const resolveOpenAiChatGptAuth = async (
options: IResolveOpenAiChatGptAuthOptions = {},
): Promise<IResolvedOpenAiChatGptAuth | undefined> => {
const now = options.now ?? new Date();
const sourceConfigs = normalizeSourceConfigs(options.sources, options.homeDir);
const refresh = options.refresh ?? 'ifNeeded';
for (const sourceConfig of sourceConfigs) {
let tokenData: IOpenAiChatGptTokenData | undefined;
try {
tokenData = await readOpenAiChatGptAuthFile(sourceConfig.filePath!, sourceConfig.format ?? sourceConfig.source);
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code === 'ENOENT') continue;
continue;
}
if (!tokenData) continue;
const shouldRefresh = refresh === 'ifNeeded' && shouldRefreshToken(tokenData, now);
const writeBack = sourceConfig.writeBack ?? options.writeBack?.[sourceConfig.source] ?? sourceConfig.source === 'smartai';
if (!shouldRefresh) {
return { source: sourceConfig.source, filePath: sourceConfig.filePath!, tokenData, refreshed: false };
}
if (!writeBack) {
if (!isExpired(tokenData, now)) {
return { source: sourceConfig.source, filePath: sourceConfig.filePath!, tokenData, refreshed: false };
}
continue;
}
try {
const refreshed = await refreshOpenAiChatGptTokenData(tokenData, options.authOptions ?? {});
await writeOpenAiChatGptAuthFile(sourceConfig.filePath!, refreshed, sourceConfig.format ?? sourceConfig.source);
return { source: sourceConfig.source, filePath: sourceConfig.filePath!, tokenData: refreshed, refreshed: true };
} catch {
continue;
}
}
return undefined;
};
export const writeOpenAiChatGptAuthFile = async (
filePath: string,
tokenData: IOpenAiChatGptTokenData,
format: TOpenAiChatGptAuthFileFormat = 'smartai',
): Promise<void> => {
const current = await readJsonFileIfExists(filePath);
const payload = format === 'opencode'
? toOpenCodeAuthFile(current, tokenData)
: format === 'codex'
? toCodexAuthFile(current, tokenData)
: toSmartAiAuthFile(tokenData);
await writeJsonAtomic(filePath, payload);
};
const normalizeSourceConfigs = (
sources: Array<TOpenAiChatGptAuthSource | IOpenAiChatGptAuthSourceConfig> | undefined,
homeDir = os.homedir(),
): INormalizedOpenAiChatGptAuthSourceConfig[] => {
return (sources ?? defaultSources).map((sourceInput) => {
const sourceConfig = typeof sourceInput === 'string' ? { source: sourceInput } : sourceInput;
return {
source: sourceConfig.source,
filePath: sourceConfig.filePath ?? getDefaultOpenAiChatGptAuthPath(sourceConfig.source, homeDir),
format: sourceConfig.format ?? sourceConfig.source,
writeBack: sourceConfig.writeBack,
};
});
};
const toInspection = (
source: TOpenAiChatGptAuthSource,
filePath: string,
tokenData: IOpenAiChatGptTokenData,
now: Date,
): IOpenAiChatGptAuthSourceInspection => {
const expired = isExpired(tokenData, now);
return {
source,
filePath,
exists: true,
usable: !expired,
expired,
accountId: tokenData.accountId ?? tokenData.tokenInfo.chatgptAccountId,
email: tokenData.tokenInfo.email,
plan: tokenData.tokenInfo.chatgptPlanType,
expiresAt: tokenData.tokenInfo.expiresAt,
};
};
const normalizeTokenObject = (input: Record<string, unknown>): IOpenAiChatGptTokenData | undefined => {
const accessToken = stringValue(input.accessToken) ?? stringValue(input.access_token);
const refreshToken = stringValue(input.refreshToken) ?? stringValue(input.refresh_token);
const idToken = stringValue(input.idToken) ?? stringValue(input.id_token) ?? stringValue((input.tokenInfo as Record<string, unknown> | undefined)?.rawJwt);
const accountId = stringValue(input.accountId) ?? stringValue(input.account_id);
return createTokenDataFromValues({ accessToken, refreshToken, idToken, accountId });
};
const normalizeOpenCodeAuth = (input: Record<string, unknown>): IOpenAiChatGptTokenData | undefined => {
if (!isRecord(input.openai)) return undefined;
const accessToken = stringValue(input.openai.access);
const refreshToken = stringValue(input.openai.refresh);
const accountId = stringValue(input.openai.accountId);
const expiresAt = typeof input.openai.expires === 'number' ? new Date(input.openai.expires).toISOString() : undefined;
return createTokenDataFromValues({ accessToken, refreshToken, accountId, fallbackExpiresAt: expiresAt });
};
const createTokenDataFromValues = (input: {
accessToken?: string;
refreshToken?: string;
idToken?: string;
accountId?: string;
fallbackExpiresAt?: string;
}): IOpenAiChatGptTokenData | undefined => {
if (!input.accessToken || !input.refreshToken) return undefined;
const parseToken = input.idToken ?? input.accessToken;
const tokenInfo = parseTokenInfo(parseToken, input.accountId, input.fallbackExpiresAt);
return {
accessToken: input.accessToken,
refreshToken: input.refreshToken,
idToken: input.idToken,
accountId: input.accountId ?? tokenInfo.chatgptAccountId,
tokenInfo,
};
};
const parseTokenInfo = (token: string, accountId?: string, fallbackExpiresAt?: string): IOpenAiChatGptTokenInfo => {
try {
const tokenInfo = parseOpenAiChatGptTokenInfo(token);
return {
...tokenInfo,
chatgptAccountId: tokenInfo.chatgptAccountId ?? accountId,
expiresAt: tokenInfo.expiresAt ?? fallbackExpiresAt,
};
} catch {
return {
chatgptAccountId: accountId,
chatgptAccountIsFedramp: false,
expiresAt: fallbackExpiresAt,
rawJwt: token,
};
}
};
const toSmartAiAuthFile = (tokenData: IOpenAiChatGptTokenData): Record<string, unknown> => ({
accessToken: tokenData.accessToken,
refreshToken: tokenData.refreshToken,
idToken: tokenData.idToken,
accountId: tokenData.accountId,
tokenInfo: tokenData.tokenInfo,
});
const toCodexAuthFile = (current: Record<string, unknown>, tokenData: IOpenAiChatGptTokenData): Record<string, unknown> => ({
...current,
OPENAI_API_KEY: current.OPENAI_API_KEY ?? null,
tokens: {
...(isRecord(current.tokens) ? current.tokens : {}),
id_token: tokenData.idToken,
access_token: tokenData.accessToken,
refresh_token: tokenData.refreshToken,
account_id: tokenData.accountId ?? tokenData.tokenInfo.chatgptAccountId,
},
last_refresh: new Date().toISOString(),
});
const toOpenCodeAuthFile = (current: Record<string, unknown>, tokenData: IOpenAiChatGptTokenData): Record<string, unknown> => ({
...current,
openai: {
...(isRecord(current.openai) ? current.openai : {}),
type: 'oauth',
refresh: tokenData.refreshToken,
access: tokenData.accessToken,
expires: tokenData.tokenInfo.expiresAt ? Date.parse(tokenData.tokenInfo.expiresAt) : undefined,
accountId: tokenData.accountId ?? tokenData.tokenInfo.chatgptAccountId,
},
});
const shouldRefreshToken = (tokenData: IOpenAiChatGptTokenData, now: Date): boolean => {
if (!tokenData.tokenInfo.expiresAt) return false;
return Date.parse(tokenData.tokenInfo.expiresAt) - now.getTime() < refreshWindowMs;
};
const isExpired = (tokenData: IOpenAiChatGptTokenData, now: Date): boolean => {
if (!tokenData.tokenInfo.expiresAt) return false;
return Date.parse(tokenData.tokenInfo.expiresAt) <= now.getTime();
};
const readJsonFileIfExists = async (filePath: string): Promise<Record<string, unknown>> => {
try {
const parsed = JSON.parse(await fs.readFile(filePath, 'utf8')) as unknown;
return isRecord(parsed) ? parsed : {};
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code === 'ENOENT') return {};
throw error;
}
};
const writeJsonAtomic = async (filePath: string, payload: unknown): Promise<void> => {
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
await fs.writeFile(tempPath, `${JSON.stringify(payload, undefined, 2)}\n`, { mode: 0o600 });
await fs.rename(tempPath, filePath);
};
const isRecord = (value: unknown): value is Record<string, unknown> => {
return !!value && typeof value === 'object' && !Array.isArray(value);
};
const stringValue = (value: unknown): string | undefined => {
return typeof value === 'string' && value.length > 0 ? value : undefined;
};