300 lines
10 KiB
TypeScript
300 lines
10 KiB
TypeScript
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 { IOpenAiChatGptTokenData } from '../ts/index.js';
|
|
import {
|
|
inspectOpenAiChatGptAuthSources,
|
|
normalizeOpenAiChatGptAuth,
|
|
resolveOpenAiChatGptAuth,
|
|
writeOpenAiChatGptAuthFile,
|
|
} from '../ts_openai_chatgpt_auth/index.js';
|
|
|
|
interface IMockFetchRequest {
|
|
url: string;
|
|
init?: RequestInit;
|
|
}
|
|
|
|
function createJwt(payload: Record<string, unknown>): string {
|
|
const encode = (value: Record<string, unknown>) => Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
return `${encode({ alg: 'none', typ: 'JWT' })}.${encode(payload)}.sig`;
|
|
}
|
|
|
|
function createTokenData(accountId = 'workspace-1', expiresAtSeconds = 4_102_444_800): IOpenAiChatGptTokenData {
|
|
const accessToken = createJwt({
|
|
email: 'user@example.com',
|
|
exp: expiresAtSeconds,
|
|
'https://api.openai.com/auth': {
|
|
chatgpt_plan_type: 'pro',
|
|
chatgpt_user_id: 'user-1',
|
|
chatgpt_account_id: accountId,
|
|
chatgpt_account_is_fedramp: false,
|
|
},
|
|
});
|
|
const tokenInfo = smartai.parseOpenAiChatGptTokenInfo(accessToken);
|
|
return {
|
|
accessToken,
|
|
refreshToken: 'refresh-token',
|
|
accountId,
|
|
tokenInfo,
|
|
};
|
|
}
|
|
|
|
function jsonResponse(body: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
function getHeader(init: RequestInit | undefined, name: string): string | null {
|
|
return new Headers(init?.headers).get(name);
|
|
}
|
|
|
|
tap.test('requestOpenAiChatGptDeviceCode requests a user code', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const requests: IMockFetchRequest[] = [];
|
|
|
|
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
requests.push({ url: String(input), init });
|
|
return jsonResponse({
|
|
device_auth_id: 'device-1',
|
|
usercode: 'ABCD-EFGH',
|
|
interval: '2',
|
|
});
|
|
};
|
|
|
|
try {
|
|
const deviceCode = await smartai.requestOpenAiChatGptDeviceCode({
|
|
issuer: 'https://auth.example.test',
|
|
clientId: 'client-1',
|
|
});
|
|
|
|
expect(deviceCode).toEqual({
|
|
verificationUrl: 'https://auth.example.test/codex/device',
|
|
userCode: 'ABCD-EFGH',
|
|
deviceAuthId: 'device-1',
|
|
intervalSeconds: 2,
|
|
});
|
|
expect(requests[0].url).toEqual('https://auth.example.test/api/accounts/deviceauth/usercode');
|
|
expect(JSON.parse(String(requests[0].init?.body))).toEqual({ client_id: 'client-1' });
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
tap.test('completeOpenAiChatGptDeviceCodeLogin polls and exchanges OAuth tokens', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const requests: IMockFetchRequest[] = [];
|
|
const tokenData = createTokenData('workspace-1');
|
|
const responses = [
|
|
jsonResponse({}, 403),
|
|
jsonResponse({
|
|
authorization_code: 'auth-code',
|
|
code_challenge: 'challenge',
|
|
code_verifier: 'verifier',
|
|
}),
|
|
jsonResponse({
|
|
access_token: tokenData.accessToken,
|
|
refresh_token: tokenData.refreshToken,
|
|
}),
|
|
];
|
|
|
|
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
requests.push({ url: String(input), init });
|
|
const response = responses.shift();
|
|
if (!response) throw new Error('Unexpected fetch call');
|
|
return response;
|
|
};
|
|
|
|
try {
|
|
const result = await smartai.completeOpenAiChatGptDeviceCodeLogin({
|
|
verificationUrl: 'https://auth.example.test/codex/device',
|
|
userCode: 'ABCD-EFGH',
|
|
deviceAuthId: 'device-1',
|
|
intervalSeconds: 1,
|
|
}, {
|
|
issuer: 'https://auth.example.test',
|
|
clientId: 'client-1',
|
|
forcedChatGptWorkspaceId: 'workspace-1',
|
|
sleep: async () => undefined,
|
|
});
|
|
|
|
expect(result.accessToken).toEqual(tokenData.accessToken);
|
|
expect(result.refreshToken).toEqual('refresh-token');
|
|
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',
|
|
user_code: 'ABCD-EFGH',
|
|
});
|
|
const tokenExchangeBody = new URLSearchParams(String(requests[2].init?.body));
|
|
expect(tokenExchangeBody.get('grant_type')).toEqual('authorization_code');
|
|
expect(tokenExchangeBody.get('code')).toEqual('auth-code');
|
|
expect(tokenExchangeBody.get('redirect_uri')).toEqual('https://auth.example.test/deviceauth/callback');
|
|
expect(tokenExchangeBody.get('client_id')).toEqual('client-1');
|
|
expect(tokenExchangeBody.get('code_verifier')).toEqual('verifier');
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
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: refreshedToken });
|
|
};
|
|
|
|
try {
|
|
const result = await smartai.refreshOpenAiChatGptTokenData(tokenData, {
|
|
issuer: 'https://auth.example.test',
|
|
clientId: 'client-1',
|
|
});
|
|
|
|
expect(result.accessToken).toEqual(refreshedToken);
|
|
expect(result.refreshToken).toEqual('refresh-token');
|
|
expect(JSON.parse(String(requests[0].init?.body))).toEqual({
|
|
client_id: 'client-1',
|
|
grant_type: 'refresh_token',
|
|
refresh_token: 'refresh-token',
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
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',
|
|
openAiChatGptAuth: tokenData,
|
|
});
|
|
|
|
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
capturedRequest = { url: String(input), init };
|
|
return jsonResponse({
|
|
id: 'resp-1',
|
|
created_at: 1,
|
|
model: 'gpt-5.5',
|
|
output: [{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
id: 'msg-1',
|
|
content: [{ type: 'output_text', text: 'ok', annotations: [] }],
|
|
}],
|
|
usage: {
|
|
input_tokens: 1,
|
|
output_tokens: 1,
|
|
},
|
|
});
|
|
};
|
|
|
|
try {
|
|
await smartai.generateText({
|
|
model,
|
|
system: 'system prompt',
|
|
prompt: 'hello',
|
|
});
|
|
|
|
expect(capturedRequest?.url).toEqual('https://chatgpt.com/backend-api/codex/responses');
|
|
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');
|
|
const capturedBody = JSON.parse(String(capturedRequest?.init?.body));
|
|
expect(capturedBody.instructions).toEqual('system prompt');
|
|
expect(capturedBody.input).toEqual([
|
|
{ role: 'user', content: [{ type: 'input_text', text: 'hello' }] },
|
|
]);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
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();
|