praxis-agent 0.43.1 → 0.45.0

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,770 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createHash, randomBytes } from 'node:crypto';
3
+ import { createServer } from 'node:http';
4
+ import { promisify } from 'node:util';
5
+ export const CODEX_OAUTH = Object.freeze({
6
+ clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',
7
+ authorizeUrl: 'https://auth.openai.com/oauth/authorize',
8
+ tokenUrl: 'https://auth.openai.com/oauth/token',
9
+ redirectUri: 'http://localhost:1455/auth/callback',
10
+ scope: 'openid profile email offline_access',
11
+ deviceCodeUrl: 'https://auth.openai.com/api/accounts/deviceauth/usercode',
12
+ deviceTokenUrl: 'https://auth.openai.com/api/accounts/deviceauth/token',
13
+ deviceVerificationUrl: 'https://auth.openai.com/codex/device',
14
+ deviceRedirectUri: 'https://auth.openai.com/deviceauth/callback',
15
+ });
16
+ export class CodexOAuthError extends Error {
17
+ code;
18
+ constructor(code, message) {
19
+ super(message);
20
+ this.name = 'CodexOAuthError';
21
+ this.code = code;
22
+ }
23
+ }
24
+ const MAX_RESPONSE_BYTES = 64 * 1024;
25
+ const MAX_JWT_BYTES = 64 * 1024;
26
+ const MAX_ACCOUNT_ID_BYTES = 512;
27
+ const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
28
+ const DEFAULT_AUTHORIZATION_TIMEOUT_MS = 5 * 60_000;
29
+ const DEFAULT_DEVICE_TIMEOUT_MS = 15 * 60_000;
30
+ const DEFAULT_BROWSER_BRIDGE_TIMEOUT_MS = 30_000;
31
+ const REFRESH_WINDOW_MS = 5 * 60_000;
32
+ function credentialKey(profileId) {
33
+ return { providerId: 'openai-codex', profileId };
34
+ }
35
+ function fail(code, message) {
36
+ throw new CodexOAuthError(code, message);
37
+ }
38
+ function cancellation() {
39
+ return new CodexOAuthError('authorization_cancelled', 'Codex OAuth authorization was cancelled');
40
+ }
41
+ function base64Url(value) {
42
+ return Buffer.from(value).toString('base64url');
43
+ }
44
+ function randomValue(size, implementation) {
45
+ const value = implementation(size);
46
+ if (!(value instanceof Uint8Array) || value.byteLength !== size) {
47
+ fail('invalid_token', 'Codex OAuth secure random generation failed');
48
+ }
49
+ return base64Url(value);
50
+ }
51
+ function expiryTime(now, expiresIn) {
52
+ if (!Number.isSafeInteger(now) ||
53
+ now < 0 ||
54
+ !Number.isSafeInteger(expiresIn) ||
55
+ expiresIn <= 0) {
56
+ fail('invalid_token', 'Codex OAuth token response was invalid');
57
+ }
58
+ const expiresAt = now + expiresIn * 1000;
59
+ if (!Number.isSafeInteger(expiresAt) || expiresAt <= now) {
60
+ fail('invalid_token', 'Codex OAuth token response was invalid');
61
+ }
62
+ return expiresAt;
63
+ }
64
+ function validAccountId(value) {
65
+ return (typeof value === 'string' &&
66
+ value.length > 0 &&
67
+ value === value.trim() &&
68
+ Buffer.byteLength(value) <= MAX_ACCOUNT_ID_BYTES);
69
+ }
70
+ function accountIdFromAccessToken(accessToken) {
71
+ try {
72
+ if (Buffer.byteLength(accessToken) > MAX_JWT_BYTES ||
73
+ !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/u.test(accessToken)) {
74
+ throw new Error('invalid JWT');
75
+ }
76
+ const payloadPart = accessToken.split('.')[1];
77
+ if (payloadPart === undefined ||
78
+ Buffer.byteLength(payloadPart) > MAX_JWT_BYTES) {
79
+ throw new Error('invalid JWT payload');
80
+ }
81
+ const decoded = Buffer.from(payloadPart, 'base64url');
82
+ if (decoded.byteLength > MAX_JWT_BYTES)
83
+ throw new Error('JWT payload is too large');
84
+ const payload = JSON.parse(decoded.toString('utf8'));
85
+ if (typeof payload !== 'object' ||
86
+ payload === null ||
87
+ Array.isArray(payload)) {
88
+ throw new Error('invalid JWT payload');
89
+ }
90
+ const auth = payload['https://api.openai.com/auth'];
91
+ if (typeof auth !== 'object' || auth === null || Array.isArray(auth))
92
+ throw new Error('missing auth claim');
93
+ const accountId = auth.chatgpt_account_id;
94
+ if (!validAccountId(accountId)) {
95
+ throw new Error('invalid account claim');
96
+ }
97
+ return accountId;
98
+ }
99
+ catch {
100
+ fail('invalid_account_claim', 'Codex OAuth access token has no valid account identity');
101
+ }
102
+ }
103
+ async function cancelBody(response) {
104
+ try {
105
+ await response.body?.cancel();
106
+ }
107
+ catch {
108
+ // Preserve the sanitized OAuth failure.
109
+ }
110
+ }
111
+ async function readBoundedJson(response, code, options) {
112
+ const declared = response.headers.get('content-length');
113
+ if (declared !== null) {
114
+ const size = Number(declared);
115
+ if (Number.isFinite(size) && size > MAX_RESPONSE_BYTES) {
116
+ await cancelBody(response);
117
+ fail(code, 'Codex OAuth response is too large');
118
+ }
119
+ }
120
+ const reader = response.body?.getReader();
121
+ if (!reader)
122
+ fail(code, 'Codex OAuth response was malformed');
123
+ const chunks = [];
124
+ let bytes = 0;
125
+ let ended = false;
126
+ let timedOut = false;
127
+ const cancelReader = () => {
128
+ void reader.cancel().catch(() => undefined);
129
+ };
130
+ const onAbort = () => cancelReader();
131
+ options.signal?.addEventListener('abort', onAbort, { once: true });
132
+ const timer = setTimeout(() => {
133
+ timedOut = true;
134
+ cancelReader();
135
+ }, options.timeoutMs);
136
+ try {
137
+ while (true) {
138
+ const result = await reader.read();
139
+ if (result.done) {
140
+ ended = true;
141
+ break;
142
+ }
143
+ bytes += result.value.byteLength;
144
+ if (bytes > MAX_RESPONSE_BYTES) {
145
+ fail(code, 'Codex OAuth response is too large');
146
+ }
147
+ chunks.push(result.value);
148
+ }
149
+ if (options.signal?.aborted)
150
+ throw cancellation();
151
+ if (timedOut)
152
+ fail(code, 'Codex OAuth response timed out');
153
+ }
154
+ catch (error) {
155
+ if (error instanceof CodexOAuthError)
156
+ throw error;
157
+ if (options.signal?.aborted)
158
+ throw cancellation();
159
+ if (timedOut)
160
+ fail(code, 'Codex OAuth response timed out');
161
+ fail(code, 'Codex OAuth response was malformed');
162
+ }
163
+ finally {
164
+ clearTimeout(timer);
165
+ options.signal?.removeEventListener('abort', onAbort);
166
+ if (!ended) {
167
+ try {
168
+ await reader.cancel();
169
+ }
170
+ catch {
171
+ // Preserve the sanitized OAuth failure.
172
+ }
173
+ }
174
+ reader.releaseLock();
175
+ }
176
+ try {
177
+ const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
178
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
179
+ throw new Error('not an object');
180
+ return value;
181
+ }
182
+ catch {
183
+ fail(code, 'Codex OAuth response was malformed');
184
+ }
185
+ }
186
+ async function fetchWithTimeout(fetchImplementation, input, init, options) {
187
+ if (options.signal?.aborted)
188
+ throw cancellation();
189
+ const controller = new AbortController();
190
+ let timedOut = false;
191
+ const onAbort = () => controller.abort();
192
+ options.signal?.addEventListener('abort', onAbort, { once: true });
193
+ const timer = setTimeout(() => {
194
+ timedOut = true;
195
+ controller.abort();
196
+ }, options.timeoutMs);
197
+ try {
198
+ return await fetchImplementation(input, {
199
+ ...init,
200
+ signal: controller.signal,
201
+ });
202
+ }
203
+ catch {
204
+ if (options.signal?.aborted)
205
+ throw cancellation();
206
+ throw new CodexOAuthError(options.code, timedOut ? `${options.message} timed out` : options.message);
207
+ }
208
+ finally {
209
+ clearTimeout(timer);
210
+ options.signal?.removeEventListener('abort', onAbort);
211
+ }
212
+ }
213
+ async function requestToken(body, options) {
214
+ const response = await fetchWithTimeout(options.fetchImplementation, CODEX_OAUTH.tokenUrl, {
215
+ method: 'POST',
216
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
217
+ body,
218
+ }, {
219
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
220
+ timeoutMs: options.timeoutMs,
221
+ code: options.code,
222
+ message: 'Codex OAuth token request failed',
223
+ });
224
+ if (!response.ok) {
225
+ await cancelBody(response);
226
+ fail(options.code, 'Codex OAuth token request failed');
227
+ }
228
+ const value = await readBoundedJson(response, options.code, {
229
+ timeoutMs: options.timeoutMs,
230
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
231
+ });
232
+ const accessToken = value.access_token;
233
+ const refreshToken = value.refresh_token;
234
+ const expiresIn = value.expires_in;
235
+ if (typeof accessToken !== 'string' ||
236
+ accessToken.length === 0 ||
237
+ typeof refreshToken !== 'string' ||
238
+ refreshToken.length === 0 ||
239
+ !Number.isSafeInteger(expiresIn) ||
240
+ expiresIn <= 0) {
241
+ fail(options.code, 'Codex OAuth token response was invalid');
242
+ }
243
+ return {
244
+ accessToken,
245
+ refreshToken,
246
+ expiresIn: expiresIn,
247
+ accountId: accountIdFromAccessToken(accessToken),
248
+ };
249
+ }
250
+ export function validateAuthorizationCallback(value, expectedState, redirectUri = CODEX_OAUTH.redirectUri) {
251
+ let actual;
252
+ try {
253
+ actual = new URL(value);
254
+ }
255
+ catch {
256
+ fail('invalid_callback', 'Codex OAuth callback URL is invalid');
257
+ }
258
+ const expected = new URL(redirectUri);
259
+ if (actual.origin !== expected.origin ||
260
+ actual.pathname !== expected.pathname) {
261
+ fail('invalid_callback', 'Codex OAuth callback does not match the local redirect');
262
+ }
263
+ const states = actual.searchParams.getAll('state');
264
+ if (states.length !== 1 || states[0] !== expectedState) {
265
+ fail('invalid_state', 'Codex OAuth callback state did not match');
266
+ }
267
+ const codes = actual.searchParams.getAll('code');
268
+ const errors = actual.searchParams.getAll('error');
269
+ if (codes.length > 1 ||
270
+ errors.length > 1 ||
271
+ (codes.length === 1) === (errors.length === 1)) {
272
+ fail('invalid_callback', 'Codex OAuth callback parameters are invalid');
273
+ }
274
+ if (codes.length === 1 && codes[0]?.length === 0)
275
+ fail('invalid_callback', 'Codex OAuth callback did not contain a code');
276
+ if (errors.length === 1 && errors[0]?.length === 0)
277
+ fail('invalid_callback', 'Codex OAuth callback error was invalid');
278
+ return actual;
279
+ }
280
+ export function createAuthorizationCallbackValidator(expectedState, redirectUri = CODEX_OAUTH.redirectUri) {
281
+ let consumed = false;
282
+ return (value) => {
283
+ if (consumed)
284
+ fail('invalid_state', 'Codex OAuth callback state was already used');
285
+ const result = validateAuthorizationCallback(value, expectedState, redirectUri);
286
+ consumed = true;
287
+ return result;
288
+ };
289
+ }
290
+ async function listenForAuthorizationRedirect(authorizationUrl, random, timeoutMs) {
291
+ const path = `/authorize/${randomValue(32, random)}`;
292
+ let consumed = false;
293
+ const lifetime = {};
294
+ let closeFlight;
295
+ const server = createServer((request, response) => {
296
+ const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
297
+ const headers = {
298
+ 'cache-control': 'no-store',
299
+ pragma: 'no-cache',
300
+ 'referrer-policy': 'no-referrer',
301
+ };
302
+ if (request.method !== 'GET' ||
303
+ requestUrl.pathname !== path ||
304
+ requestUrl.search !== '') {
305
+ response.writeHead(404, headers);
306
+ response.end('Not found');
307
+ return;
308
+ }
309
+ if (consumed) {
310
+ response.writeHead(410, headers);
311
+ response.end('Gone');
312
+ return;
313
+ }
314
+ consumed = true;
315
+ response.writeHead(302, {
316
+ ...headers,
317
+ location: authorizationUrl.toString(),
318
+ });
319
+ response.once('finish', () => void close());
320
+ response.end();
321
+ });
322
+ const close = () => {
323
+ if (closeFlight)
324
+ return closeFlight;
325
+ if (lifetime.timer)
326
+ clearTimeout(lifetime.timer);
327
+ closeFlight = server.listening
328
+ ? new Promise((resolve) => {
329
+ server.close(() => resolve());
330
+ server.closeAllConnections();
331
+ })
332
+ : Promise.resolve();
333
+ return closeFlight;
334
+ };
335
+ try {
336
+ await new Promise((resolve, reject) => {
337
+ const onError = (error) => {
338
+ server.off('listening', onListening);
339
+ reject(error);
340
+ };
341
+ const onListening = () => {
342
+ server.off('error', onError);
343
+ resolve();
344
+ };
345
+ server.once('error', onError);
346
+ server.once('listening', onListening);
347
+ server.listen(0, '127.0.0.1');
348
+ });
349
+ }
350
+ catch (error) {
351
+ await close();
352
+ throw error;
353
+ }
354
+ const address = server.address();
355
+ if (!address || typeof address === 'string') {
356
+ await close();
357
+ throw new Error('Codex OAuth browser bridge failed');
358
+ }
359
+ server.on('error', () => void close());
360
+ server.unref();
361
+ lifetime.timer = setTimeout(() => void close(), timeoutMs);
362
+ lifetime.timer.unref();
363
+ return {
364
+ browserUrl: new URL(`http://127.0.0.1:${address.port}${path}`),
365
+ close,
366
+ };
367
+ }
368
+ export async function openCodexAuthorizationUrl(authorizationUrl, options = {}) {
369
+ let bridge;
370
+ try {
371
+ bridge = await listenForAuthorizationRedirect(authorizationUrl, options.randomBytes ?? randomBytes, options.bridgeTimeoutMs ?? DEFAULT_BROWSER_BRIDGE_TIMEOUT_MS);
372
+ const platform = options.platform ?? process.platform;
373
+ const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
374
+ const args = platform === 'win32'
375
+ ? ['/c', 'start', '', bridge.browserUrl.toString()]
376
+ : [bridge.browserUrl.toString()];
377
+ await (options.runCommand ??
378
+ (async (executable, commandArguments) => {
379
+ await promisify(execFile)(executable, [...commandArguments], {
380
+ timeout: 10_000,
381
+ });
382
+ }))(command, args);
383
+ }
384
+ catch {
385
+ await bridge?.close();
386
+ }
387
+ }
388
+ async function listenForCallback(port) {
389
+ let resolveCallback = () => undefined;
390
+ let rejectCallback = () => undefined;
391
+ const callback = new Promise((resolve, reject) => {
392
+ resolveCallback = resolve;
393
+ rejectCallback = reject;
394
+ });
395
+ let callbackOrigin = 'http://localhost';
396
+ const server = createServer((request, response) => {
397
+ const url = new URL(request.url ?? '/', 'http://localhost');
398
+ if (url.pathname !== '/auth/callback') {
399
+ response.writeHead(404);
400
+ response.end('Not found');
401
+ return;
402
+ }
403
+ response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
404
+ response.end('<html><body>Praxis authorization received. You may close this tab.</body></html>');
405
+ resolveCallback(`${callbackOrigin}${url.pathname}${url.search}`);
406
+ });
407
+ server.once('error', rejectCallback);
408
+ try {
409
+ await new Promise((resolve, reject) => {
410
+ server.once('listening', resolve);
411
+ server.once('error', reject);
412
+ server.listen(port, '127.0.0.1');
413
+ });
414
+ }
415
+ catch {
416
+ fail('authorization_timeout', 'Codex OAuth callback server failed');
417
+ }
418
+ const address = server.address();
419
+ if (!address || typeof address === 'string') {
420
+ await closeCallbackServer(server);
421
+ fail('authorization_timeout', 'Codex OAuth callback server failed');
422
+ }
423
+ callbackOrigin = `http://localhost:${address.port}`;
424
+ return {
425
+ server,
426
+ redirectUri: `${callbackOrigin}/auth/callback`,
427
+ wait: () => callback,
428
+ };
429
+ }
430
+ async function closeCallbackServer(server) {
431
+ if (!server.listening)
432
+ return;
433
+ await new Promise((resolve) => server.close(() => resolve()));
434
+ }
435
+ async function waitForAuthorization(input, timeoutMs, signal) {
436
+ if (signal?.aborted)
437
+ throw cancellation();
438
+ let timer;
439
+ let onAbort;
440
+ try {
441
+ return await new Promise((resolve, reject) => {
442
+ timer = setTimeout(() => reject(new CodexOAuthError('authorization_timeout', 'Codex OAuth authorization timed out')), timeoutMs);
443
+ onAbort = () => reject(cancellation());
444
+ signal?.addEventListener('abort', onAbort, { once: true });
445
+ input.then(resolve, reject);
446
+ });
447
+ }
448
+ finally {
449
+ if (timer)
450
+ clearTimeout(timer);
451
+ if (onAbort)
452
+ signal?.removeEventListener('abort', onAbort);
453
+ }
454
+ }
455
+ async function defaultSleep(milliseconds, signal) {
456
+ if (signal?.aborted)
457
+ throw cancellation();
458
+ await new Promise((resolve, reject) => {
459
+ const onAbort = () => {
460
+ clearTimeout(timer);
461
+ signal?.removeEventListener('abort', onAbort);
462
+ reject(cancellation());
463
+ };
464
+ const timer = setTimeout(() => {
465
+ signal?.removeEventListener('abort', onAbort);
466
+ resolve();
467
+ }, milliseconds);
468
+ signal?.addEventListener('abort', onAbort, { once: true });
469
+ });
470
+ }
471
+ async function persistOAuthCredential(vault, profileId, input) {
472
+ const stored = await vault.modify(credentialKey(profileId), () => input);
473
+ if (!stored || stored.type !== 'oauth') {
474
+ fail('invalid_token', 'Codex OAuth credential could not be stored');
475
+ }
476
+ return stored;
477
+ }
478
+ async function browserLogin(options) {
479
+ const random = options.randomBytes ?? randomBytes;
480
+ const verifier = randomValue(32, random);
481
+ const challenge = base64Url(createHash('sha256').update(verifier).digest());
482
+ const state = randomValue(32, random);
483
+ const callback = await listenForCallback(options.callbackPort ?? 1455);
484
+ try {
485
+ const authorizationUrl = new URL(CODEX_OAUTH.authorizeUrl);
486
+ authorizationUrl.search = new URLSearchParams({
487
+ response_type: 'code',
488
+ client_id: CODEX_OAUTH.clientId,
489
+ redirect_uri: callback.redirectUri,
490
+ scope: CODEX_OAUTH.scope,
491
+ code_challenge: challenge,
492
+ code_challenge_method: 'S256',
493
+ state,
494
+ id_token_add_organizations: 'true',
495
+ codex_cli_simplified_flow: 'true',
496
+ originator: 'praxis',
497
+ }).toString();
498
+ options.write?.(`Visit this URL to authorize Praxis:\n ${authorizationUrl}\n`);
499
+ if (!options.noBrowser) {
500
+ await (options.openBrowser ?? openCodexAuthorizationUrl)(authorizationUrl);
501
+ }
502
+ const candidates = [callback.wait()];
503
+ if (options.readRedirectUrl)
504
+ candidates.push(options.readRedirectUrl());
505
+ const redirect = await waitForAuthorization(Promise.race(candidates), options.authorizationTimeoutMs ?? DEFAULT_AUTHORIZATION_TIMEOUT_MS, options.signal);
506
+ const parsed = createAuthorizationCallbackValidator(state, callback.redirectUri)(redirect);
507
+ if (parsed.searchParams.has('error')) {
508
+ fail('invalid_callback', 'Codex OAuth authorization was denied');
509
+ }
510
+ const authorizationCode = parsed.searchParams.get('code');
511
+ if (!authorizationCode)
512
+ fail('invalid_callback', 'Codex OAuth callback did not contain a code');
513
+ const token = await requestToken(new URLSearchParams({
514
+ grant_type: 'authorization_code',
515
+ client_id: CODEX_OAUTH.clientId,
516
+ code: authorizationCode,
517
+ redirect_uri: callback.redirectUri,
518
+ code_verifier: verifier,
519
+ }), {
520
+ fetchImplementation: options.fetchImplementation ?? fetch,
521
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
522
+ timeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
523
+ code: 'invalid_token',
524
+ });
525
+ const now = (options.now ?? Date.now)();
526
+ return persistOAuthCredential(options.vault, options.profileId, {
527
+ type: 'oauth',
528
+ accessToken: token.accessToken,
529
+ refreshToken: token.refreshToken,
530
+ expiresAt: expiryTime(now, token.expiresIn),
531
+ accountId: token.accountId,
532
+ });
533
+ }
534
+ catch (error) {
535
+ if (error instanceof CodexOAuthError)
536
+ throw error;
537
+ if (options.signal?.aborted)
538
+ throw cancellation();
539
+ fail('authorization_timeout', 'Codex OAuth authorization failed');
540
+ }
541
+ finally {
542
+ await closeCallbackServer(callback.server);
543
+ }
544
+ }
545
+ async function deviceRequest(url, body, options) {
546
+ return fetchWithTimeout(options.fetchImplementation ?? fetch, url, {
547
+ method: 'POST',
548
+ headers: { 'content-type': 'application/json' },
549
+ body: JSON.stringify(body),
550
+ }, {
551
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
552
+ timeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
553
+ code: 'device_failure',
554
+ message: 'Codex device authorization failed',
555
+ });
556
+ }
557
+ async function deviceLogin(options) {
558
+ const now = options.now ?? Date.now;
559
+ const userCodeResponse = await deviceRequest(CODEX_OAUTH.deviceCodeUrl, { client_id: CODEX_OAUTH.clientId }, options);
560
+ if (!userCodeResponse.ok) {
561
+ await cancelBody(userCodeResponse);
562
+ fail('device_failure', 'Codex device authorization failed');
563
+ }
564
+ const initial = await readBoundedJson(userCodeResponse, 'device_failure', {
565
+ timeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
566
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
567
+ });
568
+ const userCode = initial.user_code;
569
+ const deviceAuthId = initial.device_auth_id;
570
+ if (typeof userCode !== 'string' ||
571
+ userCode.length === 0 ||
572
+ typeof deviceAuthId !== 'string' ||
573
+ deviceAuthId.length === 0) {
574
+ fail('device_failure', 'Codex device authorization response was invalid');
575
+ }
576
+ options.write?.(`Visit ${CODEX_OAUTH.deviceVerificationUrl} and enter code ${userCode}\n`);
577
+ let interval = typeof initial.interval === 'number' &&
578
+ Number.isFinite(initial.interval) &&
579
+ initial.interval > 0
580
+ ? Math.min(initial.interval * 1000, 60_000)
581
+ : 5000;
582
+ const startedAt = now();
583
+ const timeoutMs = options.deviceTimeoutMs ?? DEFAULT_DEVICE_TIMEOUT_MS;
584
+ const sleep = options.sleep ?? defaultSleep;
585
+ let authorizationCode;
586
+ let verifier;
587
+ while (authorizationCode === undefined || verifier === undefined) {
588
+ if (options.signal?.aborted)
589
+ throw cancellation();
590
+ if (now() - startedAt >= timeoutMs) {
591
+ fail('device_timeout', 'Codex device authorization timed out');
592
+ }
593
+ try {
594
+ await sleep(interval, options.signal);
595
+ }
596
+ catch (error) {
597
+ if (error instanceof CodexOAuthError)
598
+ throw error;
599
+ if (options.signal?.aborted)
600
+ throw cancellation();
601
+ fail('device_failure', 'Codex device authorization failed');
602
+ }
603
+ if (options.signal?.aborted)
604
+ throw cancellation();
605
+ if (now() - startedAt >= timeoutMs) {
606
+ fail('device_timeout', 'Codex device authorization timed out');
607
+ }
608
+ const poll = await deviceRequest(CODEX_OAUTH.deviceTokenUrl, { device_auth_id: deviceAuthId, user_code: userCode }, options);
609
+ if (poll.status === 403 || poll.status === 404) {
610
+ await cancelBody(poll);
611
+ continue;
612
+ }
613
+ const value = await readBoundedJson(poll, 'device_failure', {
614
+ timeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
615
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
616
+ });
617
+ const error = value.error;
618
+ if (error === 'authorization_pending')
619
+ continue;
620
+ if (error === 'slow_down') {
621
+ interval = Math.min(interval + 5000, 60_000);
622
+ continue;
623
+ }
624
+ if (!poll.ok) {
625
+ fail('device_failure', 'Codex device authorization failed');
626
+ }
627
+ if (typeof value.authorization_code !== 'string' ||
628
+ value.authorization_code.length === 0 ||
629
+ typeof value.code_verifier !== 'string' ||
630
+ value.code_verifier.length === 0) {
631
+ fail('device_failure', 'Codex device authorization response was invalid');
632
+ }
633
+ authorizationCode = value.authorization_code;
634
+ verifier = value.code_verifier;
635
+ }
636
+ const token = await requestToken(new URLSearchParams({
637
+ grant_type: 'authorization_code',
638
+ client_id: CODEX_OAUTH.clientId,
639
+ code: authorizationCode,
640
+ redirect_uri: CODEX_OAUTH.deviceRedirectUri,
641
+ code_verifier: verifier,
642
+ }), {
643
+ fetchImplementation: options.fetchImplementation ?? fetch,
644
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
645
+ timeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
646
+ code: 'invalid_token',
647
+ });
648
+ return persistOAuthCredential(options.vault, options.profileId, {
649
+ type: 'oauth',
650
+ accessToken: token.accessToken,
651
+ refreshToken: token.refreshToken,
652
+ expiresAt: expiryTime(now(), token.expiresIn),
653
+ accountId: token.accountId,
654
+ });
655
+ }
656
+ export function loginWithCodexOAuth(options) {
657
+ return browserLogin(options);
658
+ }
659
+ export function deviceLoginWithCodexOAuth(options) {
660
+ return deviceLogin(options);
661
+ }
662
+ function requireOAuthCredential(value) {
663
+ if (!value ||
664
+ value.type !== 'oauth' ||
665
+ value.accessToken.length === 0 ||
666
+ value.refreshToken.length === 0 ||
667
+ !Number.isFinite(value.expiresAt) ||
668
+ value.expiresAt < 0) {
669
+ fail('missing_credential', 'Codex OAuth credential is unavailable');
670
+ }
671
+ return value;
672
+ }
673
+ function accessFromCredential(credential) {
674
+ if (!validAccountId(credential.accountId)) {
675
+ fail('invalid_account_claim', 'Codex OAuth credential has no account identity');
676
+ }
677
+ return {
678
+ accessToken: credential.accessToken,
679
+ accountId: credential.accountId,
680
+ expiresAt: credential.expiresAt,
681
+ };
682
+ }
683
+ export class CodexOAuthCredentialManager {
684
+ vault;
685
+ profileId;
686
+ options;
687
+ refreshFlight;
688
+ constructor(vault, profileId, options = {}) {
689
+ this.vault = vault;
690
+ this.profileId = profileId;
691
+ this.options = options;
692
+ }
693
+ async access(options = {}) {
694
+ if (options.signal?.aborted)
695
+ throw cancellation();
696
+ const current = requireOAuthCredential(await this.vault.read(credentialKey(this.profileId)));
697
+ const now = (this.options.now ?? Date.now)();
698
+ const needsRefresh = current.expiresAt <= now + REFRESH_WINDOW_MS ||
699
+ current.accessToken === options.forceAfter;
700
+ if (!needsRefresh)
701
+ return accessFromCredential(current);
702
+ if (!this.refreshFlight) {
703
+ const flight = this.refresh(options).finally(() => {
704
+ if (this.refreshFlight === flight)
705
+ this.refreshFlight = undefined;
706
+ });
707
+ this.refreshFlight = flight;
708
+ }
709
+ return this.refreshFlight;
710
+ }
711
+ async refresh(accessOptions) {
712
+ let access;
713
+ try {
714
+ await this.vault.modify(credentialKey(this.profileId), async (stored) => {
715
+ const current = requireOAuthCredential(stored);
716
+ const now = (this.options.now ?? Date.now)();
717
+ const stillNeedsRefresh = current.expiresAt <= now + REFRESH_WINDOW_MS ||
718
+ current.accessToken === accessOptions.forceAfter;
719
+ if (!stillNeedsRefresh) {
720
+ access = accessFromCredential(current);
721
+ return {
722
+ type: 'oauth',
723
+ accessToken: current.accessToken,
724
+ refreshToken: current.refreshToken,
725
+ expiresAt: current.expiresAt,
726
+ ...(current.accountId === undefined
727
+ ? {}
728
+ : { accountId: current.accountId }),
729
+ };
730
+ }
731
+ const token = await requestToken(new URLSearchParams({
732
+ grant_type: 'refresh_token',
733
+ client_id: CODEX_OAUTH.clientId,
734
+ refresh_token: current.refreshToken,
735
+ }), {
736
+ fetchImplementation: this.options.fetchImplementation ?? fetch,
737
+ ...(accessOptions.signal === undefined
738
+ ? {}
739
+ : { signal: accessOptions.signal }),
740
+ timeoutMs: this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
741
+ code: 'refresh_failure',
742
+ });
743
+ if (token.refreshToken === current.refreshToken) {
744
+ fail('refresh_failure', 'Codex OAuth refresh did not rotate the refresh token');
745
+ }
746
+ if (!current.accountId || token.accountId !== current.accountId) {
747
+ fail('refresh_failure', 'Codex OAuth account identity changed during refresh');
748
+ }
749
+ const next = {
750
+ type: 'oauth',
751
+ accessToken: token.accessToken,
752
+ refreshToken: token.refreshToken,
753
+ expiresAt: expiryTime(now, token.expiresIn),
754
+ accountId: token.accountId,
755
+ };
756
+ access = accessFromCredential(next);
757
+ return next;
758
+ });
759
+ if (!access)
760
+ fail('refresh_failure', 'Codex OAuth refresh failed');
761
+ return access;
762
+ }
763
+ catch (error) {
764
+ if (error instanceof CodexOAuthError)
765
+ throw error;
766
+ fail('refresh_failure', 'Codex OAuth refresh failed');
767
+ }
768
+ }
769
+ }
770
+ //# sourceMappingURL=codex-oauth.js.map