@ai-sdk/harness-codex 1.0.109 → 1.0.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,484 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import {
4
+ chmod,
5
+ mkdir,
6
+ readFile,
7
+ realpath,
8
+ rename,
9
+ writeFile,
10
+ } from 'node:fs/promises';
11
+ import { homedir } from 'node:os';
12
+ import { dirname, join, resolve } from 'node:path';
13
+ import type {
14
+ HarnessV1RequestTransformation,
15
+ HarnessV1RequestTransformationSources,
16
+ } from '@ai-sdk/harness';
17
+ import {
18
+ getJwtExpiresAt,
19
+ isAccessTokenExpiringSoon,
20
+ isHarnessAuthenticationEnvironment,
21
+ isLinux,
22
+ isMacOS,
23
+ isWindows,
24
+ readLinuxSecretServicePassword,
25
+ readMacOSKeychainPassword,
26
+ readWindowsCredentialManagerPassword,
27
+ refreshOAuthAccessToken,
28
+ shouldResolveNativeSubscription,
29
+ } from '@ai-sdk/harness/utils';
30
+ import { isRecord, safeParseJSON } from '@ai-sdk/provider-utils';
31
+ import {
32
+ createCodexRequestTransformations,
33
+ resolveCodexEnv,
34
+ type CodexAuthenticationMode,
35
+ type CodexResolvedAuthenticationMode,
36
+ } from './codex-auth';
37
+
38
+ const CHATGPT_CODEX_BASE_URL = 'https://chatgpt.com/backend-api/codex';
39
+ const OPENAI_TOKEN_URL = 'https://auth.openai.com/oauth/token';
40
+ const OPENAI_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
41
+
42
+ export type CodexResolvedAuthentication = {
43
+ readonly environment: Record<string, string>;
44
+ readonly requestHeaders?: Readonly<Record<string, string>>;
45
+ };
46
+
47
+ export function createCodexSubscriptionRequestTransformations({
48
+ requestHeaders,
49
+ ...sources
50
+ }: HarnessV1RequestTransformationSources<CodexResolvedAuthenticationMode> & {
51
+ requestHeaders?: Readonly<Record<string, string>>;
52
+ }): HarnessV1RequestTransformation[] {
53
+ const transformations = createCodexRequestTransformations(sources);
54
+ if (requestHeaders == null) return transformations;
55
+
56
+ return transformations.map(transformation => ({
57
+ ...transformation,
58
+ transform: {
59
+ ...transformation.transform,
60
+ headers: {
61
+ ...transformation.transform.headers,
62
+ ...requestHeaders,
63
+ },
64
+ },
65
+ }));
66
+ }
67
+
68
+ type CodexAuthCredentialsStoreMode = 'file' | 'keyring' | 'auto' | 'ephemeral';
69
+
70
+ type CodexKeyring = {
71
+ read(options: {
72
+ service: string;
73
+ account: string;
74
+ }): Promise<string | undefined>;
75
+ write(options: {
76
+ service: string;
77
+ account: string;
78
+ value: string;
79
+ }): Promise<void>;
80
+ };
81
+
82
+ type CodexAuthFile = {
83
+ readonly auth_mode?: unknown;
84
+ readonly tokens?: {
85
+ readonly access_token?: unknown;
86
+ readonly refresh_token?: unknown;
87
+ readonly account_id?: unknown;
88
+ };
89
+ readonly [key: string]: unknown;
90
+ };
91
+
92
+ export async function resolveCodexAuthentication({
93
+ auth,
94
+ processEnv = process.env,
95
+ authCredentialsStoreMode,
96
+ readSubscription = readCodexSubscription,
97
+ }: {
98
+ auth: CodexAuthenticationMode | undefined;
99
+ processEnv?: Record<string, string | undefined>;
100
+ authCredentialsStoreMode?: unknown;
101
+ readSubscription?: (options?: {
102
+ authCredentialsStoreMode?: unknown;
103
+ }) => Promise<CodexResolvedAuthentication | undefined>;
104
+ }): Promise<CodexResolvedAuthentication> {
105
+ const environment = resolveCodexEnv(auth, processEnv);
106
+ if (isHarnessAuthenticationEnvironment(auth)) {
107
+ return { environment };
108
+ }
109
+ if (
110
+ !shouldResolveNativeSubscription({
111
+ auth,
112
+ env: environment,
113
+ hasDirectCredential: environment.CODEX_API_KEY != null,
114
+ })
115
+ ) {
116
+ return { environment };
117
+ }
118
+
119
+ return (
120
+ (await readSubscription({ authCredentialsStoreMode })) ?? { environment }
121
+ );
122
+ }
123
+
124
+ export async function readCodexSubscription({
125
+ env = process.env,
126
+ homeDirectory = homedir(),
127
+ authCredentialsStoreMode,
128
+ platform = process.platform,
129
+ keyring,
130
+ fetch,
131
+ }: {
132
+ env?: Record<string, string | undefined>;
133
+ homeDirectory?: string;
134
+ authCredentialsStoreMode?: unknown;
135
+ platform?: NodeJS.Platform;
136
+ keyring?: CodexKeyring;
137
+ fetch?: typeof globalThis.fetch;
138
+ } = {}): Promise<CodexResolvedAuthentication | undefined> {
139
+ const codexHome = resolve(env.CODEX_HOME ?? join(homeDirectory, '.codex'));
140
+ const authPath = join(codexHome, 'auth.json');
141
+ const storageMode =
142
+ toCodexAuthCredentialsStoreMode(authCredentialsStoreMode) ??
143
+ (await readCodexAuthCredentialsStoreMode({ codexHome })) ??
144
+ 'file';
145
+ const stored = await readCodexAuthStore({
146
+ codexHome,
147
+ authPath,
148
+ storageMode,
149
+ keyring: keyring ?? createCodexKeyring({ platform }),
150
+ });
151
+ if (stored == null) return undefined;
152
+
153
+ const credential = await toCodexCredential(stored.value);
154
+ if (credential == null) return undefined;
155
+
156
+ let accessToken = credential.accessToken;
157
+ if (isAccessTokenExpiringSoon({ expiresAt: credential.expiresAt })) {
158
+ const refreshed = await refreshOAuthAccessToken({
159
+ tokenUrl: OPENAI_TOKEN_URL,
160
+ clientId: OPENAI_CLIENT_ID,
161
+ refreshToken: credential.refreshToken,
162
+ requestFormat: 'json',
163
+ ...(fetch == null ? {} : { fetch }),
164
+ });
165
+ accessToken = refreshed.accessToken;
166
+ await stored.write({
167
+ ...stored.value,
168
+ last_refresh: new Date().toISOString(),
169
+ tokens: {
170
+ ...stored.value.tokens,
171
+ access_token: refreshed.accessToken,
172
+ refresh_token: refreshed.refreshToken ?? credential.refreshToken,
173
+ },
174
+ });
175
+ }
176
+
177
+ return {
178
+ environment: {
179
+ CODEX_API_KEY: accessToken,
180
+ OPENAI_BASE_URL: CHATGPT_CODEX_BASE_URL,
181
+ },
182
+ ...(credential.accountId == null
183
+ ? {}
184
+ : {
185
+ requestHeaders: {
186
+ 'ChatGPT-Account-ID': credential.accountId,
187
+ },
188
+ }),
189
+ };
190
+ }
191
+
192
+ async function readCodexAuthStore({
193
+ codexHome,
194
+ authPath,
195
+ storageMode,
196
+ keyring,
197
+ }: {
198
+ codexHome: string;
199
+ authPath: string;
200
+ storageMode: CodexAuthCredentialsStoreMode;
201
+ keyring: CodexKeyring | undefined;
202
+ }): Promise<
203
+ | {
204
+ value: CodexAuthFile;
205
+ write(value: CodexAuthFile): Promise<void>;
206
+ }
207
+ | undefined
208
+ > {
209
+ if (storageMode === 'ephemeral') return undefined;
210
+
211
+ const readFileStore = async () => {
212
+ const value = await readCodexAuthFile(authPath);
213
+ return value == null
214
+ ? undefined
215
+ : {
216
+ value,
217
+ write: (updated: CodexAuthFile) =>
218
+ writeCodexAuthFile({ authPath, value: updated }),
219
+ };
220
+ };
221
+ const readKeyringStore = async () => {
222
+ if (keyring == null) return undefined;
223
+ const canonicalHome = await realpath(codexHome).catch(() => codexHome);
224
+ const account = `cli|${createHash('sha256')
225
+ .update(canonicalHome)
226
+ .digest('hex')
227
+ .slice(0, 16)}`;
228
+ const text = await keyring.read({ service: 'Codex Auth', account });
229
+ if (text == null) return undefined;
230
+ const value = await parseCodexAuthFile(text);
231
+ return value == null
232
+ ? undefined
233
+ : {
234
+ value,
235
+ write: (updated: CodexAuthFile) =>
236
+ keyring.write({
237
+ service: 'Codex Auth',
238
+ account,
239
+ value: JSON.stringify(updated),
240
+ }),
241
+ };
242
+ };
243
+
244
+ if (storageMode === 'file') return readFileStore();
245
+ if (storageMode === 'keyring') return readKeyringStore();
246
+ return (await readKeyringStore()) ?? readFileStore();
247
+ }
248
+
249
+ async function readCodexAuthCredentialsStoreMode({
250
+ codexHome,
251
+ }: {
252
+ codexHome: string;
253
+ }): Promise<CodexAuthCredentialsStoreMode | undefined> {
254
+ const text = await readFile(join(codexHome, 'config.toml'), 'utf8').catch(
255
+ () => undefined,
256
+ );
257
+ if (text == null) return undefined;
258
+ for (const line of text.split(/\r?\n/)) {
259
+ if (line.trimStart().startsWith('[')) return undefined;
260
+ const match =
261
+ /^\s*cli_auth_credentials_store\s*=\s*["'](file|keyring|auto|ephemeral)["']\s*(?:#.*)?$/.exec(
262
+ line,
263
+ );
264
+ if (match != null) {
265
+ return toCodexAuthCredentialsStoreMode(match[1]);
266
+ }
267
+ }
268
+ return undefined;
269
+ }
270
+
271
+ function toCodexAuthCredentialsStoreMode(
272
+ value: unknown,
273
+ ): CodexAuthCredentialsStoreMode | undefined {
274
+ return value === 'file' ||
275
+ value === 'keyring' ||
276
+ value === 'auto' ||
277
+ value === 'ephemeral'
278
+ ? value
279
+ : undefined;
280
+ }
281
+
282
+ async function readCodexAuthFile(
283
+ authPath: string,
284
+ ): Promise<CodexAuthFile | undefined> {
285
+ const text = await readFile(authPath, 'utf8').catch(() => undefined);
286
+ return text == null ? undefined : parseCodexAuthFile(text);
287
+ }
288
+
289
+ async function parseCodexAuthFile(
290
+ text: string,
291
+ ): Promise<CodexAuthFile | undefined> {
292
+ const parsed = await safeParseJSON({ text });
293
+ return parsed.success && isRecord(parsed.value)
294
+ ? (parsed.value as CodexAuthFile)
295
+ : undefined;
296
+ }
297
+
298
+ async function toCodexCredential(value: CodexAuthFile): Promise<
299
+ | {
300
+ accessToken: string;
301
+ refreshToken: string;
302
+ expiresAt: number;
303
+ accountId?: string;
304
+ }
305
+ | undefined
306
+ > {
307
+ if (value.auth_mode !== 'chatgpt' || !isRecord(value.tokens)) {
308
+ return undefined;
309
+ }
310
+ const accessToken = value.tokens.access_token;
311
+ const refreshToken = value.tokens.refresh_token;
312
+ if (typeof accessToken !== 'string' || typeof refreshToken !== 'string') {
313
+ return undefined;
314
+ }
315
+ const expiresAt = await getJwtExpiresAt({ token: accessToken });
316
+ if (expiresAt == null) return undefined;
317
+ const accountId = value.tokens.account_id;
318
+ return {
319
+ accessToken,
320
+ refreshToken,
321
+ expiresAt,
322
+ ...(typeof accountId === 'string' ? { accountId } : {}),
323
+ };
324
+ }
325
+
326
+ async function writeCodexAuthFile({
327
+ authPath,
328
+ value,
329
+ }: {
330
+ authPath: string;
331
+ value: CodexAuthFile;
332
+ }): Promise<void> {
333
+ await mkdir(dirname(authPath), { recursive: true });
334
+ const temporaryPath = `${authPath}.${process.pid}.tmp`;
335
+ await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, {
336
+ mode: 0o600,
337
+ });
338
+ await chmod(temporaryPath, 0o600);
339
+ await rename(temporaryPath, authPath);
340
+ }
341
+
342
+ function createCodexKeyring({
343
+ platform,
344
+ }: {
345
+ platform: NodeJS.Platform;
346
+ }): CodexKeyring | undefined {
347
+ if (isMacOS(platform)) {
348
+ return {
349
+ async read({ service, account }) {
350
+ return readMacOSKeychainPassword({ service, account });
351
+ },
352
+ async write({ service, account, value }) {
353
+ const command = `add-generic-password -U -s ${shellQuoteForSecurity(service)} -a ${shellQuoteForSecurity(account)} -X ${Buffer.from(value, 'utf8').toString('hex')}\n`;
354
+ await runCommandWithInput({
355
+ command: '/usr/bin/security',
356
+ args: ['-i'],
357
+ input: command,
358
+ });
359
+ },
360
+ };
361
+ }
362
+ if (isLinux(platform)) {
363
+ return {
364
+ async read({ service, account }) {
365
+ return readLinuxSecretServicePassword({
366
+ attributes: {
367
+ service,
368
+ username: account,
369
+ target: 'default',
370
+ },
371
+ });
372
+ },
373
+ async write({ service, account, value }) {
374
+ await runCommandWithInput({
375
+ command: 'secret-tool',
376
+ args: [
377
+ 'store',
378
+ '--label',
379
+ `${account}@${service}:default`,
380
+ 'service',
381
+ service,
382
+ 'username',
383
+ account,
384
+ 'target',
385
+ 'default',
386
+ 'application',
387
+ 'rust-keyring',
388
+ ],
389
+ input: value,
390
+ });
391
+ },
392
+ };
393
+ }
394
+ if (isWindows(platform)) {
395
+ return createWindowsCodexKeyring();
396
+ }
397
+ return undefined;
398
+ }
399
+
400
+ function shellQuoteForSecurity(value: string): string {
401
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
402
+ }
403
+
404
+ async function runCommandWithInput({
405
+ command,
406
+ args,
407
+ input,
408
+ env,
409
+ }: {
410
+ command: string;
411
+ args: string[];
412
+ input: string;
413
+ env?: NodeJS.ProcessEnv;
414
+ }): Promise<void> {
415
+ await new Promise<void>((resolvePromise, reject) => {
416
+ const child = spawn(command, args, {
417
+ stdio: ['pipe', 'ignore', 'pipe'],
418
+ ...(env == null ? {} : { env }),
419
+ });
420
+ let stderr = '';
421
+ child.stderr.setEncoding('utf8');
422
+ child.stderr.on('data', chunk => {
423
+ stderr += chunk;
424
+ });
425
+ child.once('error', reject);
426
+ child.once('close', code => {
427
+ if (code === 0) resolvePromise();
428
+ else reject(new Error(stderr || `${command} exited with code ${code}.`));
429
+ });
430
+ child.stdin.end(input);
431
+ });
432
+ }
433
+
434
+ function createWindowsCodexKeyring(): CodexKeyring {
435
+ const source = `
436
+ using System;
437
+ using System.Runtime.InteropServices;
438
+ using System.Text;
439
+ public static class CodexCredentialManager {
440
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
441
+ public struct Credential {
442
+ public UInt32 Flags; public UInt32 Type; public string TargetName;
443
+ public string Comment; public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
444
+ public UInt32 CredentialBlobSize; public IntPtr CredentialBlob;
445
+ public UInt32 Persist; public UInt32 AttributeCount; public IntPtr Attributes;
446
+ public string TargetAlias; public string UserName;
447
+ }
448
+ [DllImport("advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
449
+ static extern bool CredWrite(ref Credential credential, UInt32 flags);
450
+ public static void Write(string target, string userName, string value) {
451
+ byte[] bytes = Encoding.Unicode.GetBytes(value);
452
+ IntPtr blob = Marshal.AllocHGlobal(bytes.Length);
453
+ try {
454
+ Marshal.Copy(bytes, 0, blob, bytes.Length);
455
+ Credential credential = new Credential {
456
+ Type = 1, TargetName = target, CredentialBlobSize = (UInt32)bytes.Length,
457
+ CredentialBlob = blob, Persist = 3, UserName = userName
458
+ };
459
+ if (!CredWrite(ref credential, 0)) throw new System.ComponentModel.Win32Exception();
460
+ } finally { Marshal.FreeHGlobal(blob); }
461
+ }
462
+ }`;
463
+ return {
464
+ async read({ service, account }) {
465
+ return readWindowsCredentialManagerPassword({
466
+ targetName: `${account}.${service}`,
467
+ });
468
+ },
469
+ async write({ service, account, value }) {
470
+ const script = `Add-Type -TypeDefinition $env:AI_SDK_CODEX_CREDENTIAL_SOURCE; $value = [Console]::In.ReadToEnd(); [CodexCredentialManager]::Write($env:AI_SDK_CODEX_CREDENTIAL_TARGET, $env:AI_SDK_CODEX_CREDENTIAL_USER, $value)`;
471
+ await runCommandWithInput({
472
+ command: 'powershell.exe',
473
+ args: ['-NoProfile', '-NonInteractive', '-Command', script],
474
+ input: value,
475
+ env: {
476
+ ...process.env,
477
+ AI_SDK_CODEX_CREDENTIAL_SOURCE: source,
478
+ AI_SDK_CODEX_CREDENTIAL_TARGET: `${account}.${service}`,
479
+ AI_SDK_CODEX_CREDENTIAL_USER: account,
480
+ },
481
+ });
482
+ },
483
+ };
484
+ }