@pure01fx/dsh-openai-codex-auth 0.5.0 → 0.6.1
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.
- package/CHANGELOG.md +50 -0
- package/README.md +33 -5
- package/client.js +186 -16
- package/cordis.patch.yml +2 -6
- package/lib/catalog.d.ts +77 -0
- package/lib/catalog.js +383 -0
- package/lib/endpoint.d.ts +2 -0
- package/lib/endpoint.js +38 -0
- package/lib/index.d.ts +63 -17
- package/lib/index.js +535 -130
- package/lib/native-adapter.d.ts +43 -0
- package/lib/native-adapter.js +246 -0
- package/lib/native-http.d.ts +61 -0
- package/lib/native-http.js +602 -0
- package/lib/native-websocket-session.d.ts +14 -0
- package/lib/native-websocket-session.js +109 -0
- package/lib/native-websocket-socket.d.ts +28 -0
- package/lib/native-websocket-socket.js +215 -0
- package/lib/native-websocket.d.ts +46 -0
- package/lib/native-websocket.js +628 -0
- package/lib/rate-limits.d.ts +35 -0
- package/lib/rate-limits.js +189 -0
- package/lib/replay.d.ts +49 -0
- package/lib/replay.js +240 -0
- package/lib/response-usage.d.ts +14 -0
- package/lib/response-usage.js +35 -0
- package/lib/responses.d.ts +126 -0
- package/lib/responses.js +572 -0
- package/lib/sse.d.ts +13 -0
- package/lib/sse.js +81 -0
- package/lib/upstream.d.ts +6 -0
- package/lib/upstream.js +6 -0
- package/lib/usage.d.ts +29 -0
- package/lib/usage.js +160 -0
- package/package.json +50 -15
package/lib/index.js
CHANGED
|
@@ -8,6 +8,15 @@ import { createServer } from 'node:http';
|
|
|
8
8
|
import { createHash, randomBytes } from 'node:crypto';
|
|
9
9
|
import { readFile, unlink } from 'node:fs/promises';
|
|
10
10
|
import { join, resolve } from 'node:path';
|
|
11
|
+
import { INVALID_CREDENTIAL_CODE, LlmError } from '@deepseek-ai/dsh-llm';
|
|
12
|
+
import { CODEX_PROVIDER, NATIVE_CODEX_PROVIDER, NativeCodexAdapter, } from './native-adapter.js';
|
|
13
|
+
export { CODEX_PROVIDER, NATIVE_CODEX_PROVIDER } from './native-adapter.js';
|
|
14
|
+
import { NativeCodexCatalog } from './catalog.js';
|
|
15
|
+
import { NativeCodexHttpTransport } from './native-http.js';
|
|
16
|
+
import { NativeCodexWebSocketTransport } from './native-websocket.js';
|
|
17
|
+
import { mergeDirectUsage, normalizeUsage } from './usage.js';
|
|
18
|
+
export { normalizeUsage } from './usage.js';
|
|
19
|
+
export { CODEX_CLIENT_VERSION, TRACKED_CODEX_COMMIT, TRACKED_CODEX_RELEASE, TRACKED_CODEX_REPOSITORY, } from './upstream.js';
|
|
11
20
|
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
12
21
|
const AUTH_BASE_URL = 'https://auth.openai.com';
|
|
13
22
|
const AUTHORIZE_URL = `${AUTH_BASE_URL}/oauth/authorize`;
|
|
@@ -24,6 +33,7 @@ const BROWSER_LOGIN_TIMEOUT_MS = 10 * 60_000;
|
|
|
24
33
|
const DEFAULT_DEVICE_INTERVAL_SECONDS = 5;
|
|
25
34
|
const MIN_DEVICE_INTERVAL_MS = 1_000;
|
|
26
35
|
const SLOW_DOWN_INCREMENT_MS = 5_000;
|
|
36
|
+
const TOKEN_REFRESH_PREEMPT_MS = 5 * 60_000;
|
|
27
37
|
const DEFAULT_FILENAME = 'openai-codex-auth.json';
|
|
28
38
|
const TOKEN_REF = credentialRef('DSH_OPENAI_CODEX_TOKEN');
|
|
29
39
|
const USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
|
|
@@ -36,28 +46,99 @@ class LoginConflictError extends Error {
|
|
|
36
46
|
}
|
|
37
47
|
class CredentialNotWritableError extends Error {
|
|
38
48
|
}
|
|
49
|
+
const PERMANENT_REFRESH_CODES = new Set([
|
|
50
|
+
'refresh_token_expired',
|
|
51
|
+
'refresh_token_reused',
|
|
52
|
+
'refresh_token_invalidated',
|
|
53
|
+
'invalid_grant',
|
|
54
|
+
]);
|
|
55
|
+
class OAuthEndpointError extends Error {
|
|
56
|
+
status;
|
|
57
|
+
oauthCode;
|
|
58
|
+
constructor(message, status, oauthCode) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.status = status;
|
|
61
|
+
this.oauthCode = oauthCode;
|
|
62
|
+
this.name = 'OAuthEndpointError';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
39
65
|
function base64Url(value) {
|
|
40
66
|
return value.toString('base64url');
|
|
41
67
|
}
|
|
42
68
|
function messageOf(error) {
|
|
43
69
|
return error instanceof Error ? error.message : String(error);
|
|
44
70
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
71
|
+
function throwIfCancelled(signal) {
|
|
72
|
+
if (!signal?.aborted)
|
|
73
|
+
return;
|
|
74
|
+
if (signal.reason instanceof LlmError)
|
|
75
|
+
throw signal.reason;
|
|
76
|
+
throw new Error('OpenAI login cancelled');
|
|
77
|
+
}
|
|
78
|
+
function jwtPayload(token, label) {
|
|
79
|
+
const parts = token.split('.');
|
|
47
80
|
if (parts.length !== 3)
|
|
48
|
-
throw new Error(
|
|
49
|
-
|
|
50
|
-
|
|
81
|
+
throw new Error(`OpenAI returned an invalid ${label}`);
|
|
82
|
+
try {
|
|
83
|
+
const value = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
84
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
85
|
+
throw new Error();
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw new Error(`OpenAI returned an invalid ${label}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function chatGptAccountId(token, label) {
|
|
93
|
+
const auth = jwtPayload(token, label)['https://api.openai.com/auth'];
|
|
94
|
+
const id = auth !== null && typeof auth === 'object'
|
|
95
|
+
? auth.chatgpt_account_id
|
|
96
|
+
: undefined;
|
|
51
97
|
if (typeof id !== 'string' || id.length === 0)
|
|
52
|
-
throw new Error(
|
|
98
|
+
throw new Error(`OpenAI ${label} has no ChatGPT account id`);
|
|
53
99
|
return id;
|
|
54
100
|
}
|
|
101
|
+
function accessTokenExpiry(access) {
|
|
102
|
+
const exp = jwtPayload(access, 'access token').exp;
|
|
103
|
+
return typeof exp === 'number' && Number.isFinite(exp) && exp > 0 ? exp * 1_000 : undefined;
|
|
104
|
+
}
|
|
105
|
+
function parseTokenResponse(value, previous) {
|
|
106
|
+
if (value === null || typeof value.access_token !== 'string' || value.access_token.length === 0) {
|
|
107
|
+
throw new Error('OpenAI token response has no access token');
|
|
108
|
+
}
|
|
109
|
+
const refresh = value.refresh_token === undefined ? previous?.refresh : value.refresh_token;
|
|
110
|
+
if (typeof refresh !== 'string' || refresh.length === 0) {
|
|
111
|
+
throw new Error('OpenAI token response has no refresh token');
|
|
112
|
+
}
|
|
113
|
+
const expires = typeof value.expires_in === 'number' && Number.isFinite(value.expires_in) && value.expires_in > 0
|
|
114
|
+
? Date.now() + value.expires_in * 1_000
|
|
115
|
+
: accessTokenExpiry(value.access_token);
|
|
116
|
+
if (expires === undefined)
|
|
117
|
+
throw new Error('OpenAI token response has no usable expiry');
|
|
118
|
+
const idToken = value.id_token;
|
|
119
|
+
if (idToken !== undefined && (typeof idToken !== 'string' || idToken.length === 0)) {
|
|
120
|
+
throw new Error('OpenAI token response has an invalid ID token');
|
|
121
|
+
}
|
|
122
|
+
const resolvedAccountId = idToken === undefined
|
|
123
|
+
? previous?.accountId
|
|
124
|
+
: chatGptAccountId(idToken, 'ID token');
|
|
125
|
+
if (resolvedAccountId === undefined)
|
|
126
|
+
throw new Error('OpenAI token response has no ID token');
|
|
127
|
+
return { access: value.access_token, refresh, expires, accountId: resolvedAccountId };
|
|
128
|
+
}
|
|
129
|
+
function isPermanentRefreshError(error) {
|
|
130
|
+
return error instanceof OAuthEndpointError
|
|
131
|
+
&& (error.status === 401
|
|
132
|
+
|| (error.oauthCode !== undefined && PERMANENT_REFRESH_CODES.has(error.oauthCode.toLowerCase())));
|
|
133
|
+
}
|
|
55
134
|
function parseCredential(text, filename) {
|
|
56
135
|
const value = JSON.parse(text);
|
|
57
136
|
const credential = value.credential;
|
|
58
137
|
if (value.version !== 1 || credential === undefined
|
|
59
|
-
|| typeof credential.access !== 'string' ||
|
|
60
|
-
|| typeof credential.
|
|
138
|
+
|| typeof credential.access !== 'string' || credential.access.length === 0
|
|
139
|
+
|| typeof credential.refresh !== 'string' || credential.refresh.length === 0
|
|
140
|
+
|| typeof credential.expires !== 'number' || !Number.isFinite(credential.expires)
|
|
141
|
+
|| typeof credential.accountId !== 'string' || credential.accountId.length === 0) {
|
|
61
142
|
throw new Error(`openai-codex-auth: invalid credential document ${filename}`);
|
|
62
143
|
}
|
|
63
144
|
return credential;
|
|
@@ -119,14 +200,11 @@ function parseManualAuthorizationInput(input) {
|
|
|
119
200
|
}
|
|
120
201
|
return { code: text };
|
|
121
202
|
}
|
|
122
|
-
async function tokenRequest(
|
|
203
|
+
async function tokenRequest(init, previous, signal) {
|
|
123
204
|
let response;
|
|
124
205
|
try {
|
|
125
206
|
response = await fetch(TOKEN_URL, {
|
|
126
|
-
method: 'POST',
|
|
127
|
-
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
128
|
-
body,
|
|
129
|
-
...signal === undefined ? {} : { signal },
|
|
207
|
+
method: 'POST', redirect: 'error', ...init, ...signal === undefined ? {} : { signal },
|
|
130
208
|
});
|
|
131
209
|
}
|
|
132
210
|
catch (error) {
|
|
@@ -136,65 +214,25 @@ async function tokenRequest(body, signal) {
|
|
|
136
214
|
}
|
|
137
215
|
if (!response.ok) {
|
|
138
216
|
const text = await responseText(response);
|
|
139
|
-
throw new
|
|
217
|
+
throw new OAuthEndpointError(`OpenAI token request failed (HTTP ${response.status})${text ? `: ${text}` : ''}`, response.status, errorCodeFromText(text));
|
|
140
218
|
}
|
|
141
|
-
|
|
142
|
-
if (value === null || typeof value.access_token !== 'string' || typeof value.refresh_token !== 'string'
|
|
143
|
-
|| typeof value.expires_in !== 'number')
|
|
144
|
-
throw new Error('OpenAI token response is incomplete');
|
|
145
|
-
return {
|
|
146
|
-
access: value.access_token,
|
|
147
|
-
refresh: value.refresh_token,
|
|
148
|
-
expires: Date.now() + value.expires_in * 1000,
|
|
149
|
-
accountId: accountId(value.access_token),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
function optionalNumber(value) {
|
|
153
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
219
|
+
return parseTokenResponse(await response.json(), previous);
|
|
154
220
|
}
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (usedPercent === undefined)
|
|
161
|
-
return undefined;
|
|
162
|
-
const windowSeconds = optionalNumber(row.limit_window_seconds ?? row.windowDurationSecs);
|
|
163
|
-
const resetAt = optionalNumber(row.reset_at ?? row.resetsAt);
|
|
164
|
-
return {
|
|
165
|
-
usedPercent: Math.max(0, Math.min(100, usedPercent)),
|
|
166
|
-
...windowSeconds === undefined ? {} : { windowSeconds },
|
|
167
|
-
...resetAt === undefined ? {} : { resetAt },
|
|
168
|
-
};
|
|
221
|
+
function exchangeToken(body, signal) {
|
|
222
|
+
return tokenRequest({
|
|
223
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
224
|
+
body,
|
|
225
|
+
}, undefined, signal);
|
|
169
226
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
? root.rate_limit_reset_credits
|
|
180
|
-
: undefined;
|
|
181
|
-
const planType = typeof root.plan_type === 'string'
|
|
182
|
-
? root.plan_type
|
|
183
|
-
: typeof root.planType === 'string' ? root.planType : undefined;
|
|
184
|
-
const primary = usageWindow(limits.primary_window ?? limits.primary);
|
|
185
|
-
const secondary = usageWindow(limits.secondary_window ?? limits.secondary);
|
|
186
|
-
const limitReached = typeof limits.limit_reached === 'boolean'
|
|
187
|
-
? limits.limit_reached
|
|
188
|
-
: typeof limits.limitReached === 'boolean' ? limits.limitReached : undefined;
|
|
189
|
-
const resetCredits = optionalNumber(credits?.available_count ?? credits?.availableCount);
|
|
190
|
-
return {
|
|
191
|
-
...planType === undefined ? {} : { planType },
|
|
192
|
-
...primary === undefined ? {} : { primary },
|
|
193
|
-
...secondary === undefined ? {} : { secondary },
|
|
194
|
-
...limitReached === undefined ? {} : { limitReached },
|
|
195
|
-
...resetCredits === undefined ? {} : { resetCredits },
|
|
196
|
-
fetchedAt: Date.now(),
|
|
197
|
-
};
|
|
227
|
+
function refreshToken(current, signal) {
|
|
228
|
+
return tokenRequest({
|
|
229
|
+
headers: { 'content-type': 'application/json' },
|
|
230
|
+
body: JSON.stringify({
|
|
231
|
+
client_id: CLIENT_ID,
|
|
232
|
+
grant_type: 'refresh_token',
|
|
233
|
+
refresh_token: current.refresh,
|
|
234
|
+
}),
|
|
235
|
+
}, current, signal);
|
|
198
236
|
}
|
|
199
237
|
function header(request, name) {
|
|
200
238
|
const value = request.headers[name];
|
|
@@ -361,6 +399,7 @@ async function startDeviceAuthorization(signal) {
|
|
|
361
399
|
try {
|
|
362
400
|
response = await fetch(DEVICE_USER_CODE_URL, {
|
|
363
401
|
method: 'POST',
|
|
402
|
+
redirect: 'error',
|
|
364
403
|
headers: { 'content-type': 'application/json' },
|
|
365
404
|
body: JSON.stringify({ client_id: CLIENT_ID }),
|
|
366
405
|
...signal === undefined ? {} : { signal },
|
|
@@ -407,6 +446,7 @@ function errorCodeFromText(text) {
|
|
|
407
446
|
const code = error.code;
|
|
408
447
|
return typeof code === 'string' ? code : undefined;
|
|
409
448
|
}
|
|
449
|
+
return typeof value.code === 'string' ? value.code : undefined;
|
|
410
450
|
}
|
|
411
451
|
catch {
|
|
412
452
|
return undefined;
|
|
@@ -467,6 +507,7 @@ async function pollDeviceAuthorization(device, signal) {
|
|
|
467
507
|
try {
|
|
468
508
|
response = await fetch(DEVICE_TOKEN_URL, {
|
|
469
509
|
method: 'POST',
|
|
510
|
+
redirect: 'error',
|
|
470
511
|
headers: { 'content-type': 'application/json' },
|
|
471
512
|
body: JSON.stringify({ device_auth_id: device.deviceAuthId, user_code: device.userCode }),
|
|
472
513
|
...signal === undefined ? {} : { signal },
|
|
@@ -491,7 +532,47 @@ async function pollDeviceAuthorization(device, signal) {
|
|
|
491
532
|
}
|
|
492
533
|
throw new Error('OpenAI device-code login timed out');
|
|
493
534
|
}
|
|
535
|
+
function resolveCodexRouteStatus(config, llm) {
|
|
536
|
+
let providers = [];
|
|
537
|
+
try {
|
|
538
|
+
providers = llm?.listProviders() ?? [];
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
// The auth/status surface must remain available while the LLM tree is settling.
|
|
542
|
+
}
|
|
543
|
+
const production = providers.find(provider => provider.id === CODEX_PROVIDER);
|
|
544
|
+
const compatibilityActive = providers.some(provider => provider.id === NATIVE_CODEX_PROVIDER);
|
|
545
|
+
if (config.nativeAdapter) {
|
|
546
|
+
return {
|
|
547
|
+
provider: CODEX_PROVIDER,
|
|
548
|
+
owner: 'native',
|
|
549
|
+
active: production !== undefined,
|
|
550
|
+
...production === undefined ? {} : { registeredName: production.name },
|
|
551
|
+
transport: config.nativeWebSocket ? 'websocket-v2' : 'http-sse',
|
|
552
|
+
compatibilityRoute: {
|
|
553
|
+
configured: config.nativeCompatibilityRoute,
|
|
554
|
+
active: compatibilityActive,
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
if (production !== undefined) {
|
|
559
|
+
return {
|
|
560
|
+
provider: CODEX_PROVIDER,
|
|
561
|
+
owner: 'external',
|
|
562
|
+
active: true,
|
|
563
|
+
registeredName: production.name,
|
|
564
|
+
compatibilityRoute: { configured: false, active: compatibilityActive },
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
provider: CODEX_PROVIDER,
|
|
569
|
+
owner: 'unregistered',
|
|
570
|
+
active: false,
|
|
571
|
+
compatibilityRoute: { configured: false, active: compatibilityActive },
|
|
572
|
+
};
|
|
573
|
+
}
|
|
494
574
|
export const internals = {
|
|
575
|
+
parseTokenResponse,
|
|
495
576
|
parseAuthority,
|
|
496
577
|
isLoopbackHostname,
|
|
497
578
|
isTrustedHost,
|
|
@@ -501,17 +582,31 @@ export const internals = {
|
|
|
501
582
|
startDeviceAuthorization,
|
|
502
583
|
parseDevicePollResponse,
|
|
503
584
|
pollDeviceAuthorization,
|
|
585
|
+
resolveCodexRouteStatus,
|
|
504
586
|
};
|
|
505
587
|
/** DSH service providing device-code/browser login, logout, and automatically refreshed bearer tokens. */
|
|
506
588
|
export class OpenAICodexAuth extends Service {
|
|
507
|
-
static Config = z.object({
|
|
589
|
+
static Config = z.object({
|
|
590
|
+
path: z.string(),
|
|
591
|
+
dshHome: z.string(),
|
|
592
|
+
nativeAdapter: z.boolean().default(true),
|
|
593
|
+
nativeCompatibilityRoute: z.boolean().default(false),
|
|
594
|
+
nativeWebSocket: z.boolean().default(true),
|
|
595
|
+
});
|
|
508
596
|
static inject = ['credentials', 'webServer', 'webRuntime'];
|
|
509
597
|
filename;
|
|
598
|
+
routeConfig;
|
|
510
599
|
csrf = base64Url(randomBytes(24));
|
|
511
600
|
usageCache;
|
|
601
|
+
usageAccountId;
|
|
602
|
+
responseUsage;
|
|
603
|
+
credentialAccountId;
|
|
512
604
|
usageError;
|
|
513
605
|
usageRefresh;
|
|
514
606
|
usageGeneration = 0;
|
|
607
|
+
directUsageSequence = 0;
|
|
608
|
+
directUsageAccountId;
|
|
609
|
+
usageHasDirectDefault = false;
|
|
515
610
|
codexTurns = new Map();
|
|
516
611
|
loginFlow;
|
|
517
612
|
startingDevice;
|
|
@@ -520,11 +615,50 @@ export class OpenAICodexAuth extends Service {
|
|
|
520
615
|
constructor(ctx, config) {
|
|
521
616
|
super(ctx, 'openaiCodexAuth');
|
|
522
617
|
this.filename = resolve(config.path ?? join(resolveDshHome(config.dshHome), DEFAULT_FILENAME));
|
|
618
|
+
this.routeConfig = {
|
|
619
|
+
nativeAdapter: config.nativeAdapter ?? true,
|
|
620
|
+
nativeCompatibilityRoute: config.nativeCompatibilityRoute ?? false,
|
|
621
|
+
nativeWebSocket: config.nativeWebSocket ?? true,
|
|
622
|
+
};
|
|
623
|
+
if (this.routeConfig.nativeAdapter) {
|
|
624
|
+
const catalog = new NativeCodexCatalog({
|
|
625
|
+
resolveCredential: signal => this.resolveNativeCredential(signal),
|
|
626
|
+
warn: message => { ctx.logger.warn(message); },
|
|
627
|
+
});
|
|
628
|
+
const transportOptions = {
|
|
629
|
+
resolveCredential: signal => this.resolveNativeCredential(signal),
|
|
630
|
+
recoverCredential: (previous, signal) => this.recoverNativeCredential(previous, signal),
|
|
631
|
+
readImage: async (attachment, signal) => {
|
|
632
|
+
const store = ctx.get('attachments');
|
|
633
|
+
if (store === undefined) {
|
|
634
|
+
throw new LlmError('native Codex image input requires the attachment service', 'UNSUPPORTED');
|
|
635
|
+
}
|
|
636
|
+
return store.readImage(attachment, signal);
|
|
637
|
+
},
|
|
638
|
+
onRateLimits: observation => {
|
|
639
|
+
this.acceptRateLimits(observation.accountId, observation.updates);
|
|
640
|
+
},
|
|
641
|
+
onResponseUsage: observation => {
|
|
642
|
+
this.acceptResponseUsage(observation);
|
|
643
|
+
},
|
|
644
|
+
warn: message => { ctx.logger.warn(message); },
|
|
645
|
+
};
|
|
646
|
+
const transport = this.routeConfig.nativeWebSocket
|
|
647
|
+
? new NativeCodexWebSocketTransport(transportOptions)
|
|
648
|
+
: new NativeCodexHttpTransport(transportOptions);
|
|
649
|
+
if (transport instanceof NativeCodexWebSocketTransport) {
|
|
650
|
+
ctx.effect(() => () => { transport.dispose(); }, 'openai-codex-auth: dispose native WebSockets');
|
|
651
|
+
}
|
|
652
|
+
ctx.inject(['llm'], (llmCtx) => {
|
|
653
|
+
llmCtx.llm.registerAdapter([
|
|
654
|
+
CODEX_PROVIDER,
|
|
655
|
+
...(this.routeConfig.nativeCompatibilityRoute ? [NATIVE_CODEX_PROVIDER] : []),
|
|
656
|
+
], new NativeCodexAdapter(catalog, transport));
|
|
657
|
+
});
|
|
658
|
+
}
|
|
523
659
|
ctx.effect(async () => {
|
|
524
660
|
try {
|
|
525
|
-
|
|
526
|
-
if (token !== undefined)
|
|
527
|
-
await this.storeCredentialToken(token);
|
|
661
|
+
await this.bearerToken();
|
|
528
662
|
}
|
|
529
663
|
catch (error) {
|
|
530
664
|
this.lastLoginError = messageOf(error);
|
|
@@ -534,12 +668,16 @@ export class OpenAICodexAuth extends Service {
|
|
|
534
668
|
}, 'openai-codex-auth: bootstrap credential');
|
|
535
669
|
ctx.on('agent/request', async ({ agent, turn }, next) => {
|
|
536
670
|
const request = await next();
|
|
537
|
-
if (request.provider ===
|
|
671
|
+
if (request.provider === CODEX_PROVIDER || request.provider === NATIVE_CODEX_PROVIDER) {
|
|
538
672
|
this.markCodexTurn(String(agent.id), turn);
|
|
673
|
+
}
|
|
539
674
|
return request;
|
|
540
675
|
}, { global: true, prepend: true });
|
|
541
676
|
ctx.on('session/event', (session, event) => {
|
|
542
|
-
if (event.type !== 'turn/end'
|
|
677
|
+
if (event.type !== 'turn/end')
|
|
678
|
+
return;
|
|
679
|
+
const turn = this.consumeCodexTurn(String(session.id), event.data.turn);
|
|
680
|
+
if (turn === undefined || turn.receivedDirectUsage)
|
|
543
681
|
return;
|
|
544
682
|
void this.refreshUsage(true);
|
|
545
683
|
}, { global: true });
|
|
@@ -549,7 +687,6 @@ export class OpenAICodexAuth extends Service {
|
|
|
549
687
|
ctx.effect(() => {
|
|
550
688
|
const timer = setInterval(() => {
|
|
551
689
|
void this.bearerToken()
|
|
552
|
-
.then(token => token === undefined ? undefined : this.storeCredentialToken(token))
|
|
553
690
|
.catch((error) => { this.usageError = messageOf(error); });
|
|
554
691
|
}, 60_000);
|
|
555
692
|
return () => { clearInterval(timer); };
|
|
@@ -596,18 +733,72 @@ export class OpenAICodexAuth extends Service {
|
|
|
596
733
|
};
|
|
597
734
|
}, 'openai-codex-auth: Web routes');
|
|
598
735
|
}
|
|
736
|
+
setCredentialAccount(accountId) {
|
|
737
|
+
if (this.credentialAccountId !== accountId) {
|
|
738
|
+
this.directUsageAccountId = undefined;
|
|
739
|
+
this.usageHasDirectDefault = false;
|
|
740
|
+
this.responseUsage = undefined;
|
|
741
|
+
}
|
|
742
|
+
this.credentialAccountId = accountId;
|
|
743
|
+
}
|
|
744
|
+
acceptResponseUsage(observation) {
|
|
745
|
+
if (this.credentialAccountId !== undefined
|
|
746
|
+
&& observation.accountId !== this.credentialAccountId)
|
|
747
|
+
return;
|
|
748
|
+
this.responseUsage = {
|
|
749
|
+
accountId: observation.accountId,
|
|
750
|
+
amount: observation.metadata.amount,
|
|
751
|
+
observedAt: Date.now(),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
acceptRateLimits(accountId, updates) {
|
|
755
|
+
if (this.credentialAccountId !== undefined && accountId !== this.credentialAccountId)
|
|
756
|
+
return;
|
|
757
|
+
const accepted = updates.slice(0, 32);
|
|
758
|
+
if (accepted.length === 0)
|
|
759
|
+
return;
|
|
760
|
+
const defaultUpdate = accepted.find(candidate => candidate.limitId === 'codex');
|
|
761
|
+
const hasDefaultQuota = defaultUpdate !== undefined
|
|
762
|
+
&& (defaultUpdate.primary !== undefined || defaultUpdate.secondary !== undefined);
|
|
763
|
+
const hasData = accepted.some(update => update.primary !== undefined
|
|
764
|
+
|| update.secondary !== undefined || update.limitReached !== undefined
|
|
765
|
+
|| update.planType !== undefined || update.credits !== undefined);
|
|
766
|
+
if (!hasData)
|
|
767
|
+
return;
|
|
768
|
+
const sameUsageAccount = this.usageAccountId === accountId;
|
|
769
|
+
if (!sameUsageAccount)
|
|
770
|
+
this.usageHasDirectDefault = false;
|
|
771
|
+
if (hasDefaultQuota)
|
|
772
|
+
this.usageGeneration += 1;
|
|
773
|
+
this.usageCache = mergeDirectUsage(sameUsageAccount ? this.usageCache : undefined, accepted);
|
|
774
|
+
this.usageAccountId = accountId;
|
|
775
|
+
this.usageError = undefined;
|
|
776
|
+
if (hasDefaultQuota) {
|
|
777
|
+
this.directUsageSequence += 1;
|
|
778
|
+
this.directUsageAccountId = accountId;
|
|
779
|
+
this.usageHasDirectDefault = true;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
599
782
|
markCodexTurn(sessionId, turn) {
|
|
600
|
-
const turns = this.codexTurns.get(sessionId) ?? new
|
|
601
|
-
turns.
|
|
783
|
+
const turns = this.codexTurns.get(sessionId) ?? new Map();
|
|
784
|
+
turns.set(turn, this.directUsageSequence);
|
|
602
785
|
this.codexTurns.set(sessionId, turns);
|
|
603
786
|
}
|
|
604
787
|
consumeCodexTurn(sessionId, turn) {
|
|
605
788
|
const turns = this.codexTurns.get(sessionId);
|
|
606
|
-
|
|
607
|
-
|
|
789
|
+
const directUsageAtStart = turns?.get(turn);
|
|
790
|
+
if (turns === undefined || directUsageAtStart === undefined)
|
|
791
|
+
return undefined;
|
|
792
|
+
turns.delete(turn);
|
|
608
793
|
if (turns.size === 0)
|
|
609
794
|
this.codexTurns.delete(sessionId);
|
|
610
|
-
return
|
|
795
|
+
return {
|
|
796
|
+
receivedDirectUsage: this.directUsageSequence > directUsageAtStart
|
|
797
|
+
&& this.usageHasDirectDefault
|
|
798
|
+
&& this.directUsageAccountId !== undefined
|
|
799
|
+
&& this.directUsageAccountId === this.credentialAccountId
|
|
800
|
+
&& this.directUsageAccountId === this.usageAccountId,
|
|
801
|
+
};
|
|
611
802
|
}
|
|
612
803
|
async performUsageRefresh(generation) {
|
|
613
804
|
try {
|
|
@@ -615,6 +806,8 @@ export class OpenAICodexAuth extends Service {
|
|
|
615
806
|
if (credential === undefined) {
|
|
616
807
|
if (this.usageGeneration === generation) {
|
|
617
808
|
this.usageCache = undefined;
|
|
809
|
+
this.usageAccountId = undefined;
|
|
810
|
+
this.usageHasDirectDefault = false;
|
|
618
811
|
this.usageError = undefined;
|
|
619
812
|
}
|
|
620
813
|
return;
|
|
@@ -622,6 +815,8 @@ export class OpenAICodexAuth extends Service {
|
|
|
622
815
|
const usage = await this.fetchUsage(credential);
|
|
623
816
|
if (this.usageGeneration === generation) {
|
|
624
817
|
this.usageCache = usage;
|
|
818
|
+
this.usageAccountId = credential.accountId;
|
|
819
|
+
this.usageHasDirectDefault = false;
|
|
625
820
|
this.usageError = undefined;
|
|
626
821
|
}
|
|
627
822
|
}
|
|
@@ -640,16 +835,20 @@ export class OpenAICodexAuth extends Service {
|
|
|
640
835
|
const cycle = {
|
|
641
836
|
promise: Promise.resolve(),
|
|
642
837
|
queued: false,
|
|
838
|
+
generation: undefined,
|
|
643
839
|
};
|
|
644
840
|
this.usageRefresh = cycle;
|
|
645
841
|
cycle.promise = (async () => {
|
|
646
842
|
try {
|
|
647
843
|
do {
|
|
648
844
|
cycle.queued = false;
|
|
649
|
-
|
|
845
|
+
cycle.generation = this.usageGeneration;
|
|
846
|
+
await this.performUsageRefresh(cycle.generation);
|
|
847
|
+
cycle.generation = undefined;
|
|
650
848
|
} while (cycle.queued);
|
|
651
849
|
}
|
|
652
850
|
finally {
|
|
851
|
+
cycle.generation = undefined;
|
|
653
852
|
if (this.usageRefresh === cycle)
|
|
654
853
|
this.usageRefresh = undefined;
|
|
655
854
|
}
|
|
@@ -662,52 +861,230 @@ export class OpenAICodexAuth extends Service {
|
|
|
662
861
|
throw new CredentialNotWritableError(`DSH_OPENAI_CODEX_TOKEN is supplied by read-only source ${info.source ?? 'unknown'}; remove that override before logging in`);
|
|
663
862
|
}
|
|
664
863
|
}
|
|
665
|
-
async
|
|
666
|
-
await this.
|
|
864
|
+
async publishCredentialToken(token, signal) {
|
|
865
|
+
const resolved = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
866
|
+
throwIfCancelled(signal);
|
|
867
|
+
if (resolved?.value === token)
|
|
868
|
+
return;
|
|
869
|
+
const info = await this.ctx.credentials.describe(TOKEN_REF);
|
|
870
|
+
throwIfCancelled(signal);
|
|
871
|
+
if (!info.writable) {
|
|
872
|
+
throw new CredentialNotWritableError(`DSH_OPENAI_CODEX_TOKEN is supplied by read-only source ${info.source ?? 'unknown'} and does not match the managed OpenAI credential`);
|
|
873
|
+
}
|
|
667
874
|
await this.ctx.credentials.set(TOKEN_REF, token);
|
|
668
875
|
}
|
|
669
|
-
|
|
876
|
+
async restorePublishedCredential(previous) {
|
|
877
|
+
if (previous === undefined)
|
|
878
|
+
await this.ctx.credentials.unset(TOKEN_REF);
|
|
879
|
+
else
|
|
880
|
+
await this.ctx.credentials.set(TOKEN_REF, previous.value);
|
|
881
|
+
}
|
|
882
|
+
publicationChangedError(failure) {
|
|
883
|
+
return new AggregateError([failure, new Error('DSH_OPENAI_CODEX_TOKEN changed concurrently; rollback was skipped')], 'OpenAI credential publication failed after its DSH credential authority changed');
|
|
884
|
+
}
|
|
885
|
+
async failAfterPublicationRollback(previous, expectedCurrent, failure) {
|
|
886
|
+
let current;
|
|
887
|
+
try {
|
|
888
|
+
current = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
889
|
+
}
|
|
890
|
+
catch (resolveError) {
|
|
891
|
+
throw new AggregateError([failure, resolveError], 'OpenAI credential publication failed and rollback safety could not be verified');
|
|
892
|
+
}
|
|
893
|
+
if (current?.value !== expectedCurrent)
|
|
894
|
+
throw this.publicationChangedError(failure);
|
|
895
|
+
try {
|
|
896
|
+
await this.restorePublishedCredential(previous);
|
|
897
|
+
}
|
|
898
|
+
catch (rollbackError) {
|
|
899
|
+
throw new AggregateError([failure, rollbackError], 'OpenAI credential publication failed and its prior DSH credential could not be restored');
|
|
900
|
+
}
|
|
901
|
+
throw failure;
|
|
902
|
+
}
|
|
903
|
+
async commitCredential(credential) {
|
|
904
|
+
const previous = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
905
|
+
try {
|
|
906
|
+
await this.ctx.credentials.set(TOKEN_REF, credential.access);
|
|
907
|
+
}
|
|
908
|
+
catch (error) {
|
|
909
|
+
let current;
|
|
910
|
+
try {
|
|
911
|
+
current = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
return this.failAfterPublicationRollback(previous, credential.access, error);
|
|
915
|
+
}
|
|
916
|
+
if (current?.value === credential.access) {
|
|
917
|
+
return this.failAfterPublicationRollback(previous, credential.access, error);
|
|
918
|
+
}
|
|
919
|
+
if (current?.value !== previous?.value)
|
|
920
|
+
throw this.publicationChangedError(error);
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
try {
|
|
924
|
+
await this.write(credential);
|
|
925
|
+
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
return this.failAfterPublicationRollback(previous, credential.access, error);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
async resolveManagedCredentialLocked(signal) {
|
|
931
|
+
throwIfCancelled(signal);
|
|
932
|
+
const current = await readCredential(this.filename);
|
|
933
|
+
throwIfCancelled(signal);
|
|
934
|
+
if (current === undefined) {
|
|
935
|
+
this.setCredentialAccount(undefined);
|
|
936
|
+
return undefined;
|
|
937
|
+
}
|
|
938
|
+
if (current.expires > Date.now() + TOKEN_REFRESH_PREEMPT_MS) {
|
|
939
|
+
await this.publishCredentialToken(current.access, signal);
|
|
940
|
+
this.setCredentialAccount(current.accountId);
|
|
941
|
+
return current;
|
|
942
|
+
}
|
|
943
|
+
await this.assertCredentialWritable();
|
|
944
|
+
let next;
|
|
945
|
+
try {
|
|
946
|
+
next = await refreshToken(current, signal);
|
|
947
|
+
}
|
|
948
|
+
catch (error) {
|
|
949
|
+
throwIfCancelled(signal);
|
|
950
|
+
if (!isPermanentRefreshError(error) && current.expires > Date.now()) {
|
|
951
|
+
await this.publishCredentialToken(current.access, signal);
|
|
952
|
+
this.setCredentialAccount(current.accountId);
|
|
953
|
+
return current;
|
|
954
|
+
}
|
|
955
|
+
throw error;
|
|
956
|
+
}
|
|
957
|
+
throwIfCancelled(signal);
|
|
958
|
+
await this.commitCredential(next);
|
|
959
|
+
this.setCredentialAccount(next.accountId);
|
|
960
|
+
return next;
|
|
961
|
+
}
|
|
962
|
+
/** Return a valid managed bearer token, refreshing and persisting it when near expiry. */
|
|
670
963
|
async bearerToken(signal) {
|
|
964
|
+
throwIfCancelled(signal);
|
|
671
965
|
return withFileLock(this.filename, async () => {
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
return undefined;
|
|
675
|
-
if (current.expires > Date.now() + 60_000)
|
|
676
|
-
return current.access;
|
|
677
|
-
await this.assertCredentialWritable();
|
|
678
|
-
const next = await tokenRequest(new URLSearchParams({
|
|
679
|
-
grant_type: 'refresh_token', refresh_token: current.refresh, client_id: CLIENT_ID,
|
|
680
|
-
}), signal);
|
|
681
|
-
if (signal?.aborted)
|
|
682
|
-
throw new Error('OpenAI login cancelled');
|
|
683
|
-
await this.write(next);
|
|
684
|
-
if (signal?.aborted)
|
|
685
|
-
throw new Error('OpenAI login cancelled');
|
|
686
|
-
await this.ctx.credentials.set(TOKEN_REF, next.access);
|
|
687
|
-
return next.access;
|
|
966
|
+
const credential = await this.resolveManagedCredentialLocked(signal);
|
|
967
|
+
return credential?.access;
|
|
688
968
|
});
|
|
689
969
|
}
|
|
970
|
+
externalNativeCredential(accessToken) {
|
|
971
|
+
let accountId;
|
|
972
|
+
let expires;
|
|
973
|
+
try {
|
|
974
|
+
accountId = chatGptAccountId(accessToken, 'access token');
|
|
975
|
+
expires = accessTokenExpiry(accessToken);
|
|
976
|
+
}
|
|
977
|
+
catch (error) {
|
|
978
|
+
throw new LlmError('native Codex credential is not a valid ChatGPT access token', INVALID_CREDENTIAL_CODE, { cause: error });
|
|
979
|
+
}
|
|
980
|
+
if (expires !== undefined && expires <= Date.now()) {
|
|
981
|
+
throw new LlmError('native Codex ChatGPT access token has expired', INVALID_CREDENTIAL_CODE);
|
|
982
|
+
}
|
|
983
|
+
return { accessToken, accountId };
|
|
984
|
+
}
|
|
985
|
+
async resolveNativeCredential(signal) {
|
|
986
|
+
try {
|
|
987
|
+
throwIfCancelled(signal);
|
|
988
|
+
return await withFileLock(this.filename, async () => {
|
|
989
|
+
throwIfCancelled(signal);
|
|
990
|
+
const managed = await this.resolveManagedCredentialLocked(signal);
|
|
991
|
+
if (managed !== undefined) {
|
|
992
|
+
throwIfCancelled(signal);
|
|
993
|
+
return { accessToken: managed.access, accountId: managed.accountId };
|
|
994
|
+
}
|
|
995
|
+
const external = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
996
|
+
throwIfCancelled(signal);
|
|
997
|
+
if (external === undefined) {
|
|
998
|
+
throw new LlmError('native Codex credential is not configured', 'MISSING_CREDENTIAL');
|
|
999
|
+
}
|
|
1000
|
+
const credential = this.externalNativeCredential(external.value);
|
|
1001
|
+
throwIfCancelled(signal);
|
|
1002
|
+
this.setCredentialAccount(credential.accountId);
|
|
1003
|
+
return credential;
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
catch (error) {
|
|
1007
|
+
if (error instanceof LlmError)
|
|
1008
|
+
throw error;
|
|
1009
|
+
if (signal?.aborted) {
|
|
1010
|
+
throw new LlmError('native Codex credential resolution was aborted', 'ABORTED', { cause: error });
|
|
1011
|
+
}
|
|
1012
|
+
throw new LlmError('native Codex managed credential could not be resolved', INVALID_CREDENTIAL_CODE, { cause: error });
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
nativeRecoveryError(error) {
|
|
1016
|
+
const options = {
|
|
1017
|
+
cause: error,
|
|
1018
|
+
...(error instanceof OAuthEndpointError ? { status: error.status } : {}),
|
|
1019
|
+
};
|
|
1020
|
+
if (error instanceof CredentialNotWritableError || isPermanentRefreshError(error)) {
|
|
1021
|
+
return new LlmError('native Codex managed credential cannot be refreshed', INVALID_CREDENTIAL_CODE, options);
|
|
1022
|
+
}
|
|
1023
|
+
return new LlmError('native Codex managed credential recovery failed', 'AUTH', options);
|
|
1024
|
+
}
|
|
1025
|
+
async recoverNativeCredential(previous, signal) {
|
|
1026
|
+
try {
|
|
1027
|
+
throwIfCancelled(signal);
|
|
1028
|
+
return await withFileLock(this.filename, async () => {
|
|
1029
|
+
throwIfCancelled(signal);
|
|
1030
|
+
const current = await readCredential(this.filename);
|
|
1031
|
+
throwIfCancelled(signal);
|
|
1032
|
+
if (current === undefined) {
|
|
1033
|
+
const external = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
1034
|
+
throwIfCancelled(signal);
|
|
1035
|
+
if (external === undefined)
|
|
1036
|
+
return false;
|
|
1037
|
+
const credential = this.externalNativeCredential(external.value);
|
|
1038
|
+
throwIfCancelled(signal);
|
|
1039
|
+
this.setCredentialAccount(credential.accountId);
|
|
1040
|
+
return credential.accessToken !== previous.accessToken
|
|
1041
|
+
|| credential.accountId !== previous.accountId;
|
|
1042
|
+
}
|
|
1043
|
+
if (current.access !== previous.accessToken || current.accountId !== previous.accountId) {
|
|
1044
|
+
await this.publishCredentialToken(current.access, signal);
|
|
1045
|
+
throwIfCancelled(signal);
|
|
1046
|
+
this.setCredentialAccount(current.accountId);
|
|
1047
|
+
return true;
|
|
1048
|
+
}
|
|
1049
|
+
await this.assertCredentialWritable();
|
|
1050
|
+
throwIfCancelled(signal);
|
|
1051
|
+
const next = await refreshToken(current, signal);
|
|
1052
|
+
throwIfCancelled(signal);
|
|
1053
|
+
await this.commitCredential(next);
|
|
1054
|
+
throwIfCancelled(signal);
|
|
1055
|
+
this.setCredentialAccount(next.accountId);
|
|
1056
|
+
return next.access !== previous.accessToken || next.accountId !== previous.accountId;
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
catch (error) {
|
|
1060
|
+
if (error instanceof LlmError)
|
|
1061
|
+
throw error;
|
|
1062
|
+
if (signal?.aborted) {
|
|
1063
|
+
throw new LlmError('native Codex credential recovery was aborted', 'ABORTED');
|
|
1064
|
+
}
|
|
1065
|
+
throw this.nativeRecoveryError(error);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
690
1068
|
async finishCredential(credential, signal) {
|
|
691
1069
|
await this.assertCredentialWritable();
|
|
692
|
-
|
|
693
|
-
throw new Error('OpenAI login cancelled');
|
|
1070
|
+
throwIfCancelled(signal);
|
|
694
1071
|
await withFileLock(this.filename, async () => {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1072
|
+
throwIfCancelled(signal);
|
|
1073
|
+
await this.commitCredential(credential);
|
|
1074
|
+
this.setCredentialAccount(credential.accountId);
|
|
1075
|
+
this.usageGeneration += 1;
|
|
1076
|
+
if (this.usageRefresh !== undefined)
|
|
1077
|
+
this.usageRefresh.queued = true;
|
|
1078
|
+
this.usageCache = undefined;
|
|
1079
|
+
this.usageAccountId = undefined;
|
|
1080
|
+
this.directUsageAccountId = undefined;
|
|
1081
|
+
this.usageHasDirectDefault = false;
|
|
1082
|
+
this.usageError = undefined;
|
|
698
1083
|
});
|
|
699
|
-
this.usageGeneration += 1;
|
|
700
|
-
if (this.usageRefresh !== undefined)
|
|
701
|
-
this.usageRefresh.queued = true;
|
|
702
|
-
this.usageCache = undefined;
|
|
703
|
-
this.usageError = undefined;
|
|
704
|
-
if (signal.aborted)
|
|
705
|
-
throw new Error('OpenAI login cancelled');
|
|
706
|
-
await this.ctx.credentials.set(TOKEN_REF, credential.access);
|
|
707
1084
|
this.lastLoginError = undefined;
|
|
708
1085
|
}
|
|
709
1086
|
async finishAuthorizationCode(code, verifier, redirectUri, signal) {
|
|
710
|
-
const credential = await
|
|
1087
|
+
const credential = await exchangeToken(new URLSearchParams({
|
|
711
1088
|
grant_type: 'authorization_code', client_id: CLIENT_ID, code,
|
|
712
1089
|
code_verifier: verifier, redirect_uri: redirectUri,
|
|
713
1090
|
}), signal);
|
|
@@ -857,7 +1234,7 @@ export class OpenAICodexAuth extends Service {
|
|
|
857
1234
|
const url = new URL(AUTHORIZE_URL);
|
|
858
1235
|
for (const [key, value] of Object.entries({
|
|
859
1236
|
response_type: 'code', client_id: CLIENT_ID, redirect_uri: redirectUri,
|
|
860
|
-
scope: 'openid profile email offline_access', code_challenge: challenge,
|
|
1237
|
+
scope: 'openid profile email offline_access api.connectors.read api.connectors.invoke', code_challenge: challenge,
|
|
861
1238
|
code_challenge_method: 'S256', state, id_token_add_organizations: 'true',
|
|
862
1239
|
codex_cli_simplified_flow: 'true', originator: 'deepseek-harness',
|
|
863
1240
|
}))
|
|
@@ -922,30 +1299,44 @@ export class OpenAICodexAuth extends Service {
|
|
|
922
1299
|
async logout() {
|
|
923
1300
|
await this.cancelLogin(true);
|
|
924
1301
|
await withFileLock(this.filename, async () => {
|
|
1302
|
+
const previous = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
1303
|
+
try {
|
|
1304
|
+
await this.ctx.credentials.unset(TOKEN_REF);
|
|
1305
|
+
}
|
|
1306
|
+
catch (error) {
|
|
1307
|
+
let current;
|
|
1308
|
+
try {
|
|
1309
|
+
current = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
1310
|
+
}
|
|
1311
|
+
catch {
|
|
1312
|
+
return this.failAfterPublicationRollback(previous, undefined, error);
|
|
1313
|
+
}
|
|
1314
|
+
if (current === undefined) {
|
|
1315
|
+
return this.failAfterPublicationRollback(previous, undefined, error);
|
|
1316
|
+
}
|
|
1317
|
+
if (current.value !== previous?.value)
|
|
1318
|
+
throw this.publicationChangedError(error);
|
|
1319
|
+
throw error;
|
|
1320
|
+
}
|
|
925
1321
|
try {
|
|
926
1322
|
await unlink(this.filename);
|
|
927
1323
|
}
|
|
928
1324
|
catch (error) {
|
|
929
|
-
if (error.code !== 'ENOENT')
|
|
930
|
-
|
|
1325
|
+
if (error.code !== 'ENOENT') {
|
|
1326
|
+
return this.failAfterPublicationRollback(previous, undefined, error);
|
|
1327
|
+
}
|
|
931
1328
|
}
|
|
932
1329
|
});
|
|
933
|
-
|
|
934
|
-
try {
|
|
935
|
-
await this.ctx.credentials.unset(TOKEN_REF);
|
|
936
|
-
}
|
|
937
|
-
catch (error) {
|
|
938
|
-
unsetError = error;
|
|
939
|
-
}
|
|
1330
|
+
this.setCredentialAccount(undefined);
|
|
940
1331
|
this.usageGeneration += 1;
|
|
941
1332
|
if (this.usageRefresh !== undefined)
|
|
942
1333
|
this.usageRefresh.queued = false;
|
|
943
1334
|
this.usageCache = undefined;
|
|
1335
|
+
this.usageAccountId = undefined;
|
|
1336
|
+
this.directUsageAccountId = undefined;
|
|
1337
|
+
this.usageHasDirectDefault = false;
|
|
944
1338
|
this.usageError = undefined;
|
|
945
1339
|
this.lastLoginError = undefined;
|
|
946
|
-
if (unsetError !== undefined) {
|
|
947
|
-
throw new Error(`Local OpenAI credential was removed, but DSH_OPENAI_CODEX_TOKEN could not be unset: ${messageOf(unsetError)}`);
|
|
948
|
-
}
|
|
949
1340
|
}
|
|
950
1341
|
async status(refresh, callbackUrl) {
|
|
951
1342
|
let credential;
|
|
@@ -966,7 +1357,13 @@ export class OpenAICodexAuth extends Service {
|
|
|
966
1357
|
}
|
|
967
1358
|
if (refresh)
|
|
968
1359
|
await this.refreshUsage(true);
|
|
969
|
-
else if (this.usageRefresh !== undefined
|
|
1360
|
+
else if (this.usageRefresh !== undefined
|
|
1361
|
+
&& !(this.usageRefresh.generation !== undefined
|
|
1362
|
+
&& this.usageRefresh.generation !== this.usageGeneration
|
|
1363
|
+
&& this.usageCache?.source === 'response'
|
|
1364
|
+
&& this.usageHasDirectDefault
|
|
1365
|
+
&& this.usageAccountId === credential.accountId
|
|
1366
|
+
&& this.directUsageAccountId === credential.accountId))
|
|
970
1367
|
await this.usageRefresh.promise;
|
|
971
1368
|
}
|
|
972
1369
|
const flow = this.loginFlow;
|
|
@@ -980,6 +1377,7 @@ export class OpenAICodexAuth extends Service {
|
|
|
980
1377
|
? { authorizationUrl: flow.url, probeUrl: flow.probeUrl, expiresAt: flow.expiresAt }
|
|
981
1378
|
: undefined;
|
|
982
1379
|
return {
|
|
1380
|
+
route: resolveCodexRouteStatus(this.routeConfig, this.ctx.get('llm')),
|
|
983
1381
|
loggedIn: credential !== undefined,
|
|
984
1382
|
loginPending: startingMethod !== undefined || flow !== undefined,
|
|
985
1383
|
...startingMethod !== undefined ? { loginMethod: startingMethod } : flow === undefined ? {} : { loginMethod: flow.kind },
|
|
@@ -991,7 +1389,13 @@ export class OpenAICodexAuth extends Service {
|
|
|
991
1389
|
...credential === undefined ? {} : {
|
|
992
1390
|
accountId: credential.accountId,
|
|
993
1391
|
expiresAt: credential.expires,
|
|
994
|
-
...this.usageCache === undefined
|
|
1392
|
+
...this.usageCache === undefined || this.usageAccountId !== credential.accountId
|
|
1393
|
+
? {} : { usage: this.usageCache },
|
|
1394
|
+
...this.responseUsage === undefined || this.responseUsage.accountId !== credential.accountId
|
|
1395
|
+
? {} : { responseUsage: {
|
|
1396
|
+
amount: this.responseUsage.amount,
|
|
1397
|
+
observedAt: this.responseUsage.observedAt,
|
|
1398
|
+
} },
|
|
995
1399
|
...this.usageError === undefined ? {} : { usageError: this.usageError },
|
|
996
1400
|
},
|
|
997
1401
|
csrf: this.csrf,
|
|
@@ -1002,6 +1406,7 @@ export class OpenAICodexAuth extends Service {
|
|
|
1002
1406
|
if (access === undefined)
|
|
1003
1407
|
throw new Error('OpenAI login is missing');
|
|
1004
1408
|
const response = await fetch(USAGE_URL, {
|
|
1409
|
+
redirect: 'error',
|
|
1005
1410
|
headers: {
|
|
1006
1411
|
accept: 'application/json',
|
|
1007
1412
|
authorization: `Bearer ${access}`,
|