@pure01fx/dsh-openai-codex-auth 0.8.0 → 0.9.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.
- package/CHANGELOG.md +10 -0
- package/client.js +95 -12
- package/lib/catalog.d.ts +5 -0
- package/lib/catalog.js +19 -0
- package/lib/index.d.ts +18 -1
- package/lib/index.js +556 -127
- package/lib/native-adapter.d.ts +5 -0
- package/lib/native-adapter.js +22 -10
- package/lib/native-http.js +2 -0
- package/lib/native-websocket.js +11 -5
- package/lib/replay.d.ts +1 -0
- package/lib/replay.js +8 -2
- package/lib/response-usage.d.ts +4 -2
- package/lib/response-usage.js +26 -6
- package/lib/responses.d.ts +8 -1
- package/lib/responses.js +71 -9
- package/lib/upstream.d.ts +4 -4
- package/lib/upstream.js +4 -4
- package/lib/usage.d.ts +1 -0
- package/lib/usage.js +10 -3
- package/package.json +6 -7
package/lib/index.js
CHANGED
|
@@ -39,6 +39,10 @@ const TOKEN_REF = credentialRef('DSH_OPENAI_CODEX_TOKEN');
|
|
|
39
39
|
const USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
|
|
40
40
|
const MAX_ERROR_BODY_LENGTH = 1_024;
|
|
41
41
|
const MAX_REQUEST_BODY_LENGTH = 8_192;
|
|
42
|
+
const ACCOUNT_USAGE_REQUEST_TIMEOUT_MS = 10_000;
|
|
43
|
+
const TOKEN_REFRESH_REQUEST_TIMEOUT_MS = 30_000;
|
|
44
|
+
const ACCOUNT_REFRESH_LOCK_WAIT_MS = 35_000;
|
|
45
|
+
const CREDENTIAL_REFRESH_PERSIST_WAIT_MS = 500;
|
|
42
46
|
class DeviceCodeUnavailableError extends Error {
|
|
43
47
|
code = 'device_code_unavailable';
|
|
44
48
|
}
|
|
@@ -48,12 +52,30 @@ class CredentialNotWritableError extends Error {
|
|
|
48
52
|
}
|
|
49
53
|
class AccountNotFoundError extends Error {
|
|
50
54
|
}
|
|
55
|
+
class CredentialRefreshUncertainError extends Error {
|
|
56
|
+
constructor(cause) {
|
|
57
|
+
super('OpenAI credential refresh outcome is uncertain; sign in to this account again', { cause });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
51
60
|
const PERMANENT_REFRESH_CODES = new Set([
|
|
52
61
|
'refresh_token_expired',
|
|
53
62
|
'refresh_token_reused',
|
|
54
63
|
'refresh_token_invalidated',
|
|
55
64
|
'invalid_grant',
|
|
56
65
|
]);
|
|
66
|
+
const DEFINITIVE_OAUTH_REFRESH_CODES = new Set([
|
|
67
|
+
...PERMANENT_REFRESH_CODES,
|
|
68
|
+
'access_denied',
|
|
69
|
+
'invalid_client',
|
|
70
|
+
'invalid_request',
|
|
71
|
+
'invalid_scope',
|
|
72
|
+
'server_error',
|
|
73
|
+
'slow_down',
|
|
74
|
+
'temporarily_unavailable',
|
|
75
|
+
'temporary_failure',
|
|
76
|
+
'unauthorized_client',
|
|
77
|
+
'unsupported_grant_type',
|
|
78
|
+
]);
|
|
57
79
|
class OAuthEndpointError extends Error {
|
|
58
80
|
status;
|
|
59
81
|
oauthCode;
|
|
@@ -70,6 +92,33 @@ function base64Url(value) {
|
|
|
70
92
|
function messageOf(error) {
|
|
71
93
|
return error instanceof Error ? error.message : String(error);
|
|
72
94
|
}
|
|
95
|
+
function isTimeoutAbort(signal) {
|
|
96
|
+
return signal?.aborted === true
|
|
97
|
+
&& signal.reason instanceof DOMException
|
|
98
|
+
&& signal.reason.name === 'TimeoutError';
|
|
99
|
+
}
|
|
100
|
+
function boundedAccountUsageSignal(signal) {
|
|
101
|
+
const timeout = AbortSignal.timeout(ACCOUNT_USAGE_REQUEST_TIMEOUT_MS);
|
|
102
|
+
return signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
|
|
103
|
+
}
|
|
104
|
+
function isFileLockTimeout(error) {
|
|
105
|
+
return error instanceof Error
|
|
106
|
+
&& error.message.startsWith('atomic-write: timed out waiting for the writer lock at ');
|
|
107
|
+
}
|
|
108
|
+
function waitForSignal(promise, signal) {
|
|
109
|
+
if (signal === undefined)
|
|
110
|
+
return promise;
|
|
111
|
+
if (signal.aborted)
|
|
112
|
+
return Promise.reject(signal.reason instanceof Error ? signal.reason : new Error('Request cancelled'));
|
|
113
|
+
return new Promise((resolveWait, rejectWait) => {
|
|
114
|
+
const onAbort = () => {
|
|
115
|
+
signal.removeEventListener('abort', onAbort);
|
|
116
|
+
rejectWait(signal.reason instanceof Error ? signal.reason : new Error('Request cancelled'));
|
|
117
|
+
};
|
|
118
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
119
|
+
promise.then((value) => { signal.removeEventListener('abort', onAbort); resolveWait(value); }, (error) => { signal.removeEventListener('abort', onAbort); rejectWait(error); });
|
|
120
|
+
});
|
|
121
|
+
}
|
|
73
122
|
function throwIfCancelled(signal) {
|
|
74
123
|
if (!signal?.aborted)
|
|
75
124
|
return;
|
|
@@ -143,6 +192,12 @@ function isPermanentRefreshError(error) {
|
|
|
143
192
|
&& (error.status === 401
|
|
144
193
|
|| (error.oauthCode !== undefined && PERMANENT_REFRESH_CODES.has(error.oauthCode.toLowerCase())));
|
|
145
194
|
}
|
|
195
|
+
function isDefinitiveOAuthRefreshError(error) {
|
|
196
|
+
return error instanceof OAuthEndpointError
|
|
197
|
+
&& (error.status === 400 || error.status === 401)
|
|
198
|
+
&& error.oauthCode !== undefined
|
|
199
|
+
&& DEFINITIVE_OAUTH_REFRESH_CODES.has(error.oauthCode.toLowerCase());
|
|
200
|
+
}
|
|
146
201
|
function validateCredential(value, filename) {
|
|
147
202
|
const credential = value;
|
|
148
203
|
if (credential === null || typeof credential !== 'object'
|
|
@@ -258,7 +313,7 @@ async function tokenRequest(init, previous, signal) {
|
|
|
258
313
|
}
|
|
259
314
|
catch (error) {
|
|
260
315
|
if (signal?.aborted)
|
|
261
|
-
throw new Error('OpenAI login cancelled');
|
|
316
|
+
throw new Error(isTimeoutAbort(signal) ? 'OpenAI token request timed out' : 'OpenAI login cancelled');
|
|
262
317
|
throw error;
|
|
263
318
|
}
|
|
264
319
|
if (!response.ok) {
|
|
@@ -655,6 +710,8 @@ export class OpenAICodexAuth extends Service {
|
|
|
655
710
|
usageError;
|
|
656
711
|
usageRefresh;
|
|
657
712
|
usageGeneration = 0;
|
|
713
|
+
accountUsageRequestGeneration = 0;
|
|
714
|
+
accountUsageCredentialRefreshes = new Map();
|
|
658
715
|
directUsageSequence = 0;
|
|
659
716
|
directUsageAccountId;
|
|
660
717
|
usageHasDirectDefault = false;
|
|
@@ -755,6 +812,7 @@ export class OpenAICodexAuth extends Service {
|
|
|
755
812
|
register('/openai-codex/browser/complete', (req, res) => this.handleBrowserComplete(req, res));
|
|
756
813
|
register('/openai-codex/cancel', (req, res) => this.handleCancel(req, res));
|
|
757
814
|
register('/openai-codex/accounts/current', (req, res) => this.handleSetCurrentAccount(req, res));
|
|
815
|
+
register('/openai-codex/accounts/usage', (req, res) => this.handleAccountUsage(req, res));
|
|
758
816
|
register('/openai-codex/accounts/logout', (req, res) => this.handleAccountLogout(req, res));
|
|
759
817
|
register('/openai-codex/logout', (req, res) => this.handleLogout(req, res));
|
|
760
818
|
}
|
|
@@ -800,7 +858,7 @@ export class OpenAICodexAuth extends Service {
|
|
|
800
858
|
return;
|
|
801
859
|
this.responseUsage = {
|
|
802
860
|
accountId: observation.accountId,
|
|
803
|
-
|
|
861
|
+
metadata: observation.metadata,
|
|
804
862
|
observedAt: Date.now(),
|
|
805
863
|
};
|
|
806
864
|
}
|
|
@@ -821,8 +879,7 @@ export class OpenAICodexAuth extends Service {
|
|
|
821
879
|
const sameUsageAccount = this.usageAccountId === accountId;
|
|
822
880
|
if (!sameUsageAccount)
|
|
823
881
|
this.usageHasDirectDefault = false;
|
|
824
|
-
|
|
825
|
-
this.usageGeneration += 1;
|
|
882
|
+
this.usageGeneration += 1;
|
|
826
883
|
this.usageCache = mergeDirectUsage(sameUsageAccount ? this.usageCache : undefined, accepted);
|
|
827
884
|
this.usageAccountId = accountId;
|
|
828
885
|
this.usageError = undefined;
|
|
@@ -855,7 +912,7 @@ export class OpenAICodexAuth extends Service {
|
|
|
855
912
|
}
|
|
856
913
|
async performUsageRefresh(generation) {
|
|
857
914
|
try {
|
|
858
|
-
const credential = await
|
|
915
|
+
const credential = await this.managedCredential();
|
|
859
916
|
if (credential === undefined) {
|
|
860
917
|
if (this.usageGeneration === generation) {
|
|
861
918
|
this.usageCache = undefined;
|
|
@@ -1004,56 +1061,73 @@ export class OpenAICodexAuth extends Service {
|
|
|
1004
1061
|
const document = parsed?.document ?? { version: 2, currentAccountId: null, accounts: [] };
|
|
1005
1062
|
await this.commitDocument(this.upsertCurrentCredential(document, credential));
|
|
1006
1063
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const parsed = await readCredentialDocument(this.filename);
|
|
1010
|
-
const document = parsed?.document;
|
|
1011
|
-
const current = currentCredential(document);
|
|
1064
|
+
/** Return the current managed bearer token, refreshing and migrating it when needed. */
|
|
1065
|
+
async bearerToken(signal) {
|
|
1012
1066
|
throwIfCancelled(signal);
|
|
1013
|
-
|
|
1014
|
-
this.
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1067
|
+
while (true) {
|
|
1068
|
+
const prepared = await withFileLock(this.filename, async () => {
|
|
1069
|
+
throwIfCancelled(signal);
|
|
1070
|
+
const parsed = await readCredentialDocument(this.filename);
|
|
1071
|
+
const document = parsed?.document;
|
|
1072
|
+
const current = currentCredential(document);
|
|
1073
|
+
if (current === undefined) {
|
|
1074
|
+
this.setCredentialAccount(undefined);
|
|
1075
|
+
return undefined;
|
|
1076
|
+
}
|
|
1077
|
+
if (current.expires > Date.now() + TOKEN_REFRESH_PREEMPT_MS) {
|
|
1078
|
+
if (parsed?.migrated)
|
|
1079
|
+
await this.commitDocument(document, signal);
|
|
1080
|
+
else
|
|
1081
|
+
await this.publishCredentialToken(current.access, signal);
|
|
1082
|
+
this.setCredentialAccount(current.accountId);
|
|
1083
|
+
return { credential: current, ready: true };
|
|
1084
|
+
}
|
|
1085
|
+
return { credential: current, ready: false };
|
|
1086
|
+
});
|
|
1087
|
+
if (prepared === undefined)
|
|
1088
|
+
return undefined;
|
|
1089
|
+
if (prepared.ready)
|
|
1090
|
+
return prepared.credential.access;
|
|
1091
|
+
let refreshed;
|
|
1092
|
+
try {
|
|
1093
|
+
refreshed = await waitForSignal(this.refreshManagedAccount(prepared.credential, true), signal);
|
|
1094
|
+
}
|
|
1095
|
+
catch (error) {
|
|
1096
|
+
throwIfCancelled(signal);
|
|
1097
|
+
throw error;
|
|
1098
|
+
}
|
|
1099
|
+
if (refreshed === undefined)
|
|
1100
|
+
continue;
|
|
1101
|
+
const access = await withFileLock(this.filename, async () => {
|
|
1102
|
+
throwIfCancelled(signal);
|
|
1103
|
+
const parsed = await readCredentialDocument(this.filename);
|
|
1104
|
+
const document = parsed?.document;
|
|
1105
|
+
const current = currentCredential(document);
|
|
1106
|
+
if (current === undefined || current.accountId !== prepared.credential.accountId)
|
|
1107
|
+
return undefined;
|
|
1108
|
+
if (current.expires <= Date.now())
|
|
1109
|
+
throw new Error('OpenAI Codex access token has expired');
|
|
1033
1110
|
if (parsed?.migrated)
|
|
1034
1111
|
await this.commitDocument(document, signal);
|
|
1035
1112
|
else
|
|
1036
1113
|
await this.publishCredentialToken(current.access, signal);
|
|
1037
1114
|
this.setCredentialAccount(current.accountId);
|
|
1038
|
-
return current;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
throwIfCancelled(signal);
|
|
1043
|
-
if (next.accountId !== current.accountId) {
|
|
1044
|
-
throw new Error('OpenAI refresh changed the ChatGPT account identity');
|
|
1115
|
+
return current.access;
|
|
1116
|
+
});
|
|
1117
|
+
if (access !== undefined)
|
|
1118
|
+
return access;
|
|
1045
1119
|
}
|
|
1046
|
-
await this.commitDocument(this.upsertCurrentCredential(document, next, current.accountId), signal);
|
|
1047
|
-
this.setCredentialAccount(next.accountId);
|
|
1048
|
-
return next;
|
|
1049
1120
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1121
|
+
async managedCredential(signal) {
|
|
1122
|
+
while (true) {
|
|
1123
|
+
const access = await this.bearerToken(signal);
|
|
1124
|
+
if (access === undefined)
|
|
1125
|
+
return undefined;
|
|
1126
|
+
const current = currentCredential((await readCredentialDocument(this.filename))?.document);
|
|
1127
|
+
if (current?.access === access)
|
|
1128
|
+
return current;
|
|
1129
|
+
throwIfCancelled(signal);
|
|
1130
|
+
}
|
|
1057
1131
|
}
|
|
1058
1132
|
externalNativeCredential(accessToken) {
|
|
1059
1133
|
let accountId;
|
|
@@ -1073,23 +1147,20 @@ export class OpenAICodexAuth extends Service {
|
|
|
1073
1147
|
async resolveNativeCredential(signal) {
|
|
1074
1148
|
try {
|
|
1075
1149
|
throwIfCancelled(signal);
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
const managed = await this.resolveManagedCredentialLocked(signal);
|
|
1079
|
-
if (managed !== undefined) {
|
|
1080
|
-
throwIfCancelled(signal);
|
|
1081
|
-
return { accessToken: managed.access, accountId: managed.accountId };
|
|
1082
|
-
}
|
|
1083
|
-
const external = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
1084
|
-
throwIfCancelled(signal);
|
|
1085
|
-
if (external === undefined) {
|
|
1086
|
-
throw new LlmError('native Codex credential is not configured', 'MISSING_CREDENTIAL');
|
|
1087
|
-
}
|
|
1088
|
-
const credential = this.externalNativeCredential(external.value);
|
|
1150
|
+
const managed = await this.managedCredential(signal);
|
|
1151
|
+
if (managed !== undefined) {
|
|
1089
1152
|
throwIfCancelled(signal);
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1153
|
+
return { accessToken: managed.access, accountId: managed.accountId };
|
|
1154
|
+
}
|
|
1155
|
+
const external = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
1156
|
+
throwIfCancelled(signal);
|
|
1157
|
+
if (external === undefined) {
|
|
1158
|
+
throw new LlmError('native Codex credential is not configured', 'MISSING_CREDENTIAL');
|
|
1159
|
+
}
|
|
1160
|
+
const credential = this.externalNativeCredential(external.value);
|
|
1161
|
+
throwIfCancelled(signal);
|
|
1162
|
+
this.setCredentialAccount(credential.accountId);
|
|
1163
|
+
return credential;
|
|
1093
1164
|
}
|
|
1094
1165
|
catch (error) {
|
|
1095
1166
|
if (error instanceof LlmError)
|
|
@@ -1101,9 +1172,12 @@ export class OpenAICodexAuth extends Service {
|
|
|
1101
1172
|
}
|
|
1102
1173
|
}
|
|
1103
1174
|
nativeRecoveryError(error) {
|
|
1175
|
+
const cause = error instanceof CredentialRefreshUncertainError && error.cause !== undefined
|
|
1176
|
+
? error.cause
|
|
1177
|
+
: error;
|
|
1104
1178
|
const options = {
|
|
1105
|
-
cause
|
|
1106
|
-
...(
|
|
1179
|
+
cause,
|
|
1180
|
+
...(cause instanceof OAuthEndpointError ? { status: cause.status } : {}),
|
|
1107
1181
|
};
|
|
1108
1182
|
if (error instanceof CredentialNotWritableError || isPermanentRefreshError(error)) {
|
|
1109
1183
|
return new LlmError('native Codex managed credential cannot be refreshed', INVALID_CREDENTIAL_CODE, options);
|
|
@@ -1113,44 +1187,46 @@ export class OpenAICodexAuth extends Service {
|
|
|
1113
1187
|
async recoverNativeCredential(previous, signal) {
|
|
1114
1188
|
try {
|
|
1115
1189
|
throwIfCancelled(signal);
|
|
1116
|
-
|
|
1117
|
-
throwIfCancelled(signal);
|
|
1118
|
-
const parsed = await readCredentialDocument(this.filename);
|
|
1119
|
-
const document = parsed?.document;
|
|
1120
|
-
const current = currentCredential(document);
|
|
1190
|
+
const prepared = await withFileLock(this.filename, async () => {
|
|
1121
1191
|
throwIfCancelled(signal);
|
|
1192
|
+
const current = currentCredential((await readCredentialDocument(this.filename))?.document);
|
|
1122
1193
|
if (current === undefined) {
|
|
1123
1194
|
const external = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
1124
1195
|
throwIfCancelled(signal);
|
|
1125
1196
|
if (external === undefined)
|
|
1126
|
-
return false;
|
|
1197
|
+
return { result: false };
|
|
1127
1198
|
const credential = this.externalNativeCredential(external.value);
|
|
1128
1199
|
throwIfCancelled(signal);
|
|
1129
1200
|
if (credential.accountId !== previous.accountId)
|
|
1130
|
-
return false;
|
|
1201
|
+
return { result: false };
|
|
1131
1202
|
this.setCredentialAccount(credential.accountId);
|
|
1132
|
-
return credential.accessToken !== previous.accessToken;
|
|
1203
|
+
return { result: credential.accessToken !== previous.accessToken };
|
|
1133
1204
|
}
|
|
1134
1205
|
// A request that started under A must never recover by replaying under B.
|
|
1135
1206
|
if (current.accountId !== previous.accountId)
|
|
1136
|
-
return false;
|
|
1207
|
+
return { result: false };
|
|
1137
1208
|
if (current.access !== previous.accessToken) {
|
|
1138
1209
|
await this.publishCredentialToken(current.access, signal);
|
|
1139
1210
|
throwIfCancelled(signal);
|
|
1140
1211
|
this.setCredentialAccount(current.accountId);
|
|
1141
|
-
return true;
|
|
1212
|
+
return { result: true };
|
|
1142
1213
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1214
|
+
return { credential: current };
|
|
1215
|
+
});
|
|
1216
|
+
if (prepared.result !== undefined)
|
|
1217
|
+
return prepared.result;
|
|
1218
|
+
const refreshed = await waitForSignal(this.refreshManagedAccount(prepared.credential, true, true), signal);
|
|
1219
|
+
if (refreshed === undefined)
|
|
1220
|
+
return false;
|
|
1221
|
+
return withFileLock(this.filename, async () => {
|
|
1146
1222
|
throwIfCancelled(signal);
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
await this.
|
|
1223
|
+
const current = currentCredential((await readCredentialDocument(this.filename))?.document);
|
|
1224
|
+
if (current === undefined || current.accountId !== previous.accountId)
|
|
1225
|
+
return false;
|
|
1226
|
+
await this.publishCredentialToken(current.access, signal);
|
|
1151
1227
|
throwIfCancelled(signal);
|
|
1152
|
-
this.setCredentialAccount(
|
|
1153
|
-
return
|
|
1228
|
+
this.setCredentialAccount(current.accountId);
|
|
1229
|
+
return current.access !== previous.accessToken;
|
|
1154
1230
|
});
|
|
1155
1231
|
}
|
|
1156
1232
|
catch (error) {
|
|
@@ -1165,18 +1241,22 @@ export class OpenAICodexAuth extends Service {
|
|
|
1165
1241
|
async finishCredential(credential, signal) {
|
|
1166
1242
|
await this.assertCredentialWritable();
|
|
1167
1243
|
throwIfCancelled(signal);
|
|
1168
|
-
await
|
|
1244
|
+
await this.withAccountRefreshLock(credential.accountId, async () => {
|
|
1169
1245
|
throwIfCancelled(signal);
|
|
1170
|
-
await this.
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
this.
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1246
|
+
await withFileLock(this.filename, async () => {
|
|
1247
|
+
throwIfCancelled(signal);
|
|
1248
|
+
await this.commitCredential(credential);
|
|
1249
|
+
this.setCredentialAccount(credential.accountId);
|
|
1250
|
+
this.usageGeneration += 1;
|
|
1251
|
+
if (this.usageRefresh !== undefined)
|
|
1252
|
+
this.usageRefresh.queued = true;
|
|
1253
|
+
this.usageCache = undefined;
|
|
1254
|
+
this.usageAccountId = undefined;
|
|
1255
|
+
this.directUsageAccountId = undefined;
|
|
1256
|
+
this.usageHasDirectDefault = false;
|
|
1257
|
+
this.usageError = undefined;
|
|
1258
|
+
});
|
|
1259
|
+
await this.clearAccountRefreshJournal(credential.accountId);
|
|
1180
1260
|
});
|
|
1181
1261
|
this.lastLoginError = undefined;
|
|
1182
1262
|
}
|
|
@@ -1406,24 +1486,23 @@ export class OpenAICodexAuth extends Service {
|
|
|
1406
1486
|
this.lastLoginError = undefined;
|
|
1407
1487
|
}
|
|
1408
1488
|
async setCurrentAccount(accountId) {
|
|
1409
|
-
await
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1489
|
+
await this.withAccountRefreshLock(accountId, async () => {
|
|
1490
|
+
await this.reconcileAccountRefreshJournal(accountId);
|
|
1491
|
+
await withFileLock(this.filename, async () => {
|
|
1492
|
+
const document = (await readCredentialDocument(this.filename))?.document;
|
|
1493
|
+
if (document === undefined || !document.accounts.some(account => account.accountId === accountId)) {
|
|
1494
|
+
throw new AccountNotFoundError('OpenAI Codex account was not found');
|
|
1495
|
+
}
|
|
1496
|
+
if (document.currentAccountId !== accountId) {
|
|
1497
|
+
await this.assertCredentialWritable();
|
|
1498
|
+
await this.commitDocument({ ...document, currentAccountId: accountId });
|
|
1499
|
+
}
|
|
1500
|
+
this.resetCurrentAccountState(accountId);
|
|
1501
|
+
});
|
|
1419
1502
|
});
|
|
1420
1503
|
}
|
|
1421
|
-
async
|
|
1422
|
-
|
|
1423
|
-
await this.cancelLogin(true);
|
|
1424
|
-
let nextAccountId;
|
|
1425
|
-
let removedCurrent = false;
|
|
1426
|
-
await withFileLock(this.filename, async () => {
|
|
1504
|
+
async logoutAttempt(accountId, expectedPromotion) {
|
|
1505
|
+
return withFileLock(this.filename, async () => {
|
|
1427
1506
|
let parsed;
|
|
1428
1507
|
try {
|
|
1429
1508
|
parsed = await readCredentialDocument(this.filename);
|
|
@@ -1440,9 +1519,8 @@ export class OpenAICodexAuth extends Service {
|
|
|
1440
1519
|
catch (failure) {
|
|
1441
1520
|
return this.failAfterPublicationRollback(previous, undefined, failure);
|
|
1442
1521
|
}
|
|
1443
|
-
removedCurrent = true;
|
|
1444
1522
|
this.resetCurrentAccountState(undefined);
|
|
1445
|
-
return;
|
|
1523
|
+
return true;
|
|
1446
1524
|
}
|
|
1447
1525
|
const document = parsed?.document;
|
|
1448
1526
|
const target = accountId ?? document?.currentAccountId ?? undefined;
|
|
@@ -1450,22 +1528,54 @@ export class OpenAICodexAuth extends Service {
|
|
|
1450
1528
|
|| !document.accounts.some(account => account.accountId === target)) {
|
|
1451
1529
|
if (accountId !== undefined)
|
|
1452
1530
|
throw new AccountNotFoundError('OpenAI Codex account was not found');
|
|
1453
|
-
return;
|
|
1531
|
+
return true;
|
|
1454
1532
|
}
|
|
1455
|
-
removedCurrent = document.currentAccountId === target;
|
|
1533
|
+
const removedCurrent = document.currentAccountId === target;
|
|
1456
1534
|
const targetIndex = document.accounts.findIndex(account => account.accountId === target);
|
|
1457
1535
|
const accounts = document.accounts.filter(account => account.accountId !== target);
|
|
1458
1536
|
const currentAccountId = removedCurrent
|
|
1459
1537
|
? accounts[targetIndex]?.accountId ?? accounts[0]?.accountId ?? null
|
|
1460
1538
|
: document.currentAccountId;
|
|
1539
|
+
const promotion = removedCurrent ? currentAccountId ?? undefined : undefined;
|
|
1540
|
+
if (promotion !== expectedPromotion)
|
|
1541
|
+
return false;
|
|
1461
1542
|
if (removedCurrent)
|
|
1462
1543
|
await this.assertCredentialWritable();
|
|
1463
1544
|
await this.commitDocument({ version: 2, currentAccountId, accounts });
|
|
1464
|
-
|
|
1545
|
+
await this.clearAccountRefreshJournal(target);
|
|
1465
1546
|
if (removedCurrent)
|
|
1466
|
-
this.resetCurrentAccountState(
|
|
1547
|
+
this.resetCurrentAccountState(currentAccountId ?? undefined);
|
|
1548
|
+
return true;
|
|
1467
1549
|
});
|
|
1468
1550
|
}
|
|
1551
|
+
async logout(accountId) {
|
|
1552
|
+
if (accountId === undefined)
|
|
1553
|
+
await this.cancelLogin(true);
|
|
1554
|
+
while (true) {
|
|
1555
|
+
let target;
|
|
1556
|
+
let expectedPromotion;
|
|
1557
|
+
try {
|
|
1558
|
+
const document = (await readCredentialDocument(this.filename))?.document;
|
|
1559
|
+
target = accountId ?? document?.currentAccountId ?? undefined;
|
|
1560
|
+
if (document !== undefined && target !== undefined && document.currentAccountId === target) {
|
|
1561
|
+
const targetIndex = document.accounts.findIndex(account => account.accountId === target);
|
|
1562
|
+
const accounts = document.accounts.filter(account => account.accountId !== target);
|
|
1563
|
+
expectedPromotion = accounts[targetIndex]?.accountId ?? accounts[0]?.accountId;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
catch { }
|
|
1567
|
+
const lockIds = [target, expectedPromotion].filter((value) => value !== undefined);
|
|
1568
|
+
const done = lockIds.length === 0
|
|
1569
|
+
? await this.logoutAttempt(accountId, undefined)
|
|
1570
|
+
: await this.withAccountRefreshLocks(lockIds, async () => {
|
|
1571
|
+
if (expectedPromotion !== undefined)
|
|
1572
|
+
await this.reconcileAccountRefreshJournal(expectedPromotion);
|
|
1573
|
+
return this.logoutAttempt(accountId, expectedPromotion);
|
|
1574
|
+
});
|
|
1575
|
+
if (done)
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1469
1579
|
async status(refresh, callbackUrl) {
|
|
1470
1580
|
let document;
|
|
1471
1581
|
let credential;
|
|
@@ -1532,7 +1642,7 @@ export class OpenAICodexAuth extends Service {
|
|
|
1532
1642
|
? {} : { usage: this.usageCache },
|
|
1533
1643
|
...this.responseUsage === undefined || this.responseUsage.accountId !== credential.accountId
|
|
1534
1644
|
? {} : { responseUsage: {
|
|
1535
|
-
|
|
1645
|
+
...this.responseUsage.metadata,
|
|
1536
1646
|
observedAt: this.responseUsage.observedAt,
|
|
1537
1647
|
} },
|
|
1538
1648
|
...this.usageError === undefined ? {} : { usageError: this.usageError },
|
|
@@ -1540,16 +1650,310 @@ export class OpenAICodexAuth extends Service {
|
|
|
1540
1650
|
csrf: this.csrf,
|
|
1541
1651
|
};
|
|
1542
1652
|
}
|
|
1543
|
-
|
|
1544
|
-
const
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1653
|
+
accountRefreshJournalFilename(accountId) {
|
|
1654
|
+
const key = createHash('sha256').update(accountId).digest('hex').slice(0, 24);
|
|
1655
|
+
return `${this.filename}.account-${key}-refresh.json`;
|
|
1656
|
+
}
|
|
1657
|
+
async readAccountRefreshJournal(accountId) {
|
|
1658
|
+
const filename = this.accountRefreshJournalFilename(accountId);
|
|
1659
|
+
let value;
|
|
1660
|
+
try {
|
|
1661
|
+
value = JSON.parse(await readFile(filename, 'utf8'));
|
|
1662
|
+
}
|
|
1663
|
+
catch (error) {
|
|
1664
|
+
if (error.code === 'ENOENT')
|
|
1665
|
+
return undefined;
|
|
1666
|
+
throw error;
|
|
1667
|
+
}
|
|
1668
|
+
const journal = value;
|
|
1669
|
+
if (journal === null || typeof journal !== 'object' || journal.version !== 1
|
|
1670
|
+
|| journal.accountId !== accountId || typeof journal.startedAt !== 'number'
|
|
1671
|
+
|| !Number.isFinite(journal.startedAt)
|
|
1672
|
+
|| (journal.phase !== 'pending' && journal.phase !== 'uncertain' && journal.phase !== 'refreshed')) {
|
|
1673
|
+
throw new Error(`openai-codex-auth: invalid credential refresh journal ${filename}`);
|
|
1674
|
+
}
|
|
1675
|
+
const stored = validateCredential(journal.stored, filename);
|
|
1676
|
+
if (stored.accountId !== accountId)
|
|
1677
|
+
throw new Error(`openai-codex-auth: invalid credential refresh journal ${filename}`);
|
|
1678
|
+
if (journal.phase === 'refreshed') {
|
|
1679
|
+
const refreshed = validateCredential(journal.refreshed, filename);
|
|
1680
|
+
if (refreshed.accountId !== accountId)
|
|
1681
|
+
throw new Error(`openai-codex-auth: invalid credential refresh journal ${filename}`);
|
|
1682
|
+
return { version: 1, accountId, startedAt: journal.startedAt, phase: 'refreshed', stored, refreshed };
|
|
1683
|
+
}
|
|
1684
|
+
return { version: 1, accountId, startedAt: journal.startedAt, phase: journal.phase, stored };
|
|
1685
|
+
}
|
|
1686
|
+
writeAccountRefreshJournal(journal) {
|
|
1687
|
+
return writeFileAtomic(this.accountRefreshJournalFilename(journal.accountId), JSON.stringify(journal), { mode: 0o600 });
|
|
1688
|
+
}
|
|
1689
|
+
async clearAccountRefreshJournal(accountId) {
|
|
1690
|
+
try {
|
|
1691
|
+
await unlink(this.accountRefreshJournalFilename(accountId));
|
|
1692
|
+
}
|
|
1693
|
+
catch (error) {
|
|
1694
|
+
if (error.code !== 'ENOENT')
|
|
1695
|
+
throw error;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
async removeDeadFileLock(filename) {
|
|
1699
|
+
const lockFilename = `${filename}.lock`;
|
|
1700
|
+
let owner;
|
|
1701
|
+
try {
|
|
1702
|
+
const text = (await readFile(lockFilename, 'utf8')).trim();
|
|
1703
|
+
owner = Number(text);
|
|
1704
|
+
if (!Number.isSafeInteger(owner) || owner <= 0)
|
|
1705
|
+
return false;
|
|
1706
|
+
}
|
|
1707
|
+
catch (error) {
|
|
1708
|
+
return error.code === 'ENOENT';
|
|
1709
|
+
}
|
|
1710
|
+
try {
|
|
1711
|
+
process.kill(owner, 0);
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
catch (error) {
|
|
1715
|
+
if (error.code !== 'ESRCH')
|
|
1716
|
+
return false;
|
|
1717
|
+
try {
|
|
1718
|
+
await unlink(lockFilename);
|
|
1719
|
+
return true;
|
|
1720
|
+
}
|
|
1721
|
+
catch (unlinkError) {
|
|
1722
|
+
return unlinkError.code === 'ENOENT';
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
async withAccountRefreshLock(accountId, operation) {
|
|
1727
|
+
const filename = this.accountRefreshJournalFilename(accountId);
|
|
1728
|
+
await this.removeDeadFileLock(filename);
|
|
1729
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1730
|
+
try {
|
|
1731
|
+
return await withFileLock(filename, operation, { waitMs: ACCOUNT_REFRESH_LOCK_WAIT_MS });
|
|
1732
|
+
}
|
|
1733
|
+
catch (error) {
|
|
1734
|
+
if (!isFileLockTimeout(error) || attempt > 0 || !await this.removeDeadFileLock(filename))
|
|
1735
|
+
throw error;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
throw new Error('OpenAI credential refresh lock could not be acquired');
|
|
1739
|
+
}
|
|
1740
|
+
withAccountRefreshLocks(accountIds, operation) {
|
|
1741
|
+
const ordered = [...new Set(accountIds)].sort();
|
|
1742
|
+
const acquire = (index) => index === ordered.length
|
|
1743
|
+
? operation()
|
|
1744
|
+
: this.withAccountRefreshLock(ordered[index], () => acquire(index + 1));
|
|
1745
|
+
return acquire(0);
|
|
1746
|
+
}
|
|
1747
|
+
async reconcileAccountRefreshJournal(accountId) {
|
|
1748
|
+
const journal = await this.readAccountRefreshJournal(accountId);
|
|
1749
|
+
if (journal === undefined)
|
|
1750
|
+
return undefined;
|
|
1751
|
+
const latest = (await readCredentialDocument(this.filename))?.document.accounts
|
|
1752
|
+
.find(account => account.accountId === accountId);
|
|
1753
|
+
if (latest === undefined
|
|
1754
|
+
|| latest.access !== journal.stored.access
|
|
1755
|
+
|| latest.refresh !== journal.stored.refresh
|
|
1756
|
+
|| latest.expires !== journal.stored.expires) {
|
|
1757
|
+
await this.clearAccountRefreshJournal(accountId);
|
|
1758
|
+
return latest;
|
|
1759
|
+
}
|
|
1760
|
+
if (journal.phase !== 'refreshed')
|
|
1761
|
+
throw new CredentialRefreshUncertainError();
|
|
1762
|
+
try {
|
|
1763
|
+
const credential = await this.persistAccountUsageCredential(journal.stored, journal.refreshed);
|
|
1764
|
+
await this.clearAccountRefreshJournal(accountId);
|
|
1765
|
+
return credential;
|
|
1766
|
+
}
|
|
1767
|
+
catch (error) {
|
|
1768
|
+
if (error instanceof AccountNotFoundError)
|
|
1769
|
+
await this.clearAccountRefreshJournal(accountId);
|
|
1770
|
+
throw error;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
async refreshManagedAccount(stored, requireCurrent, force = false) {
|
|
1774
|
+
return this.withAccountRefreshLock(stored.accountId, async () => {
|
|
1775
|
+
const recovered = await this.reconcileAccountRefreshJournal(stored.accountId);
|
|
1776
|
+
if (recovered !== undefined)
|
|
1777
|
+
return recovered;
|
|
1778
|
+
const prepared = await withFileLock(this.filename, async () => {
|
|
1779
|
+
const document = (await readCredentialDocument(this.filename))?.document;
|
|
1780
|
+
const latest = document?.accounts.find(account => account.accountId === stored.accountId);
|
|
1781
|
+
if (document === undefined || latest === undefined)
|
|
1782
|
+
return undefined;
|
|
1783
|
+
if (requireCurrent && document.currentAccountId !== latest.accountId)
|
|
1784
|
+
return undefined;
|
|
1785
|
+
if (latest.access !== stored.access || latest.refresh !== stored.refresh || latest.expires !== stored.expires) {
|
|
1786
|
+
return { credential: latest, changed: true };
|
|
1787
|
+
}
|
|
1788
|
+
if ((force || latest.expires <= Date.now() + TOKEN_REFRESH_PREEMPT_MS)
|
|
1789
|
+
&& document.currentAccountId === latest.accountId)
|
|
1790
|
+
await this.assertCredentialWritable();
|
|
1791
|
+
return { credential: latest, changed: false };
|
|
1792
|
+
});
|
|
1793
|
+
if (prepared === undefined || prepared.changed
|
|
1794
|
+
|| (!force && prepared.credential.expires > Date.now() + TOKEN_REFRESH_PREEMPT_MS))
|
|
1795
|
+
return prepared?.credential;
|
|
1796
|
+
const credential = prepared.credential;
|
|
1797
|
+
const pending = {
|
|
1798
|
+
version: 1,
|
|
1799
|
+
accountId: credential.accountId,
|
|
1800
|
+
startedAt: Date.now(),
|
|
1801
|
+
phase: 'pending',
|
|
1802
|
+
stored: credential,
|
|
1803
|
+
};
|
|
1804
|
+
await this.writeAccountRefreshJournal(pending);
|
|
1805
|
+
let refreshed;
|
|
1806
|
+
try {
|
|
1807
|
+
refreshed = await refreshToken(credential, AbortSignal.timeout(TOKEN_REFRESH_REQUEST_TIMEOUT_MS));
|
|
1808
|
+
}
|
|
1809
|
+
catch (error) {
|
|
1810
|
+
if (isDefinitiveOAuthRefreshError(error)) {
|
|
1811
|
+
await this.clearAccountRefreshJournal(credential.accountId);
|
|
1812
|
+
if (!force && !isPermanentRefreshError(error) && credential.expires > Date.now())
|
|
1813
|
+
return credential;
|
|
1814
|
+
throw error;
|
|
1815
|
+
}
|
|
1816
|
+
await this.writeAccountRefreshJournal({ ...pending, phase: 'uncertain' });
|
|
1817
|
+
throw new CredentialRefreshUncertainError(error);
|
|
1818
|
+
}
|
|
1819
|
+
if (refreshed.accountId !== credential.accountId) {
|
|
1820
|
+
await this.writeAccountRefreshJournal({ ...pending, phase: 'uncertain' });
|
|
1821
|
+
throw new Error('OpenAI refresh changed the ChatGPT account identity');
|
|
1822
|
+
}
|
|
1823
|
+
await this.writeAccountRefreshJournal({ ...pending, phase: 'refreshed', refreshed });
|
|
1824
|
+
try {
|
|
1825
|
+
const persisted = await this.persistAccountUsageCredential(credential, refreshed);
|
|
1826
|
+
await this.clearAccountRefreshJournal(credential.accountId);
|
|
1827
|
+
return persisted;
|
|
1828
|
+
}
|
|
1829
|
+
catch (error) {
|
|
1830
|
+
if (error instanceof AccountNotFoundError)
|
|
1831
|
+
await this.clearAccountRefreshJournal(credential.accountId);
|
|
1832
|
+
throw error;
|
|
1833
|
+
}
|
|
1552
1834
|
});
|
|
1835
|
+
}
|
|
1836
|
+
async persistAccountUsageCredential(stored, refreshed) {
|
|
1837
|
+
await this.removeDeadFileLock(this.filename);
|
|
1838
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1839
|
+
try {
|
|
1840
|
+
return await withFileLock(this.filename, async () => {
|
|
1841
|
+
const document = (await readCredentialDocument(this.filename))?.document;
|
|
1842
|
+
const latest = document?.accounts.find(account => account.accountId === stored.accountId);
|
|
1843
|
+
if (document === undefined || latest === undefined) {
|
|
1844
|
+
throw new AccountNotFoundError('OpenAI Codex account was removed while its quota was refreshing');
|
|
1845
|
+
}
|
|
1846
|
+
if (latest.access !== stored.access || latest.refresh !== stored.refresh || latest.expires !== stored.expires) {
|
|
1847
|
+
return latest;
|
|
1848
|
+
}
|
|
1849
|
+
const nextDocument = {
|
|
1850
|
+
...document,
|
|
1851
|
+
accounts: document.accounts.map(account => account.accountId === stored.accountId ? refreshed : account),
|
|
1852
|
+
};
|
|
1853
|
+
if (document.currentAccountId === stored.accountId) {
|
|
1854
|
+
await this.assertCredentialWritable();
|
|
1855
|
+
await this.commitDocument(nextDocument);
|
|
1856
|
+
}
|
|
1857
|
+
else {
|
|
1858
|
+
await this.write(nextDocument);
|
|
1859
|
+
}
|
|
1860
|
+
return refreshed;
|
|
1861
|
+
}, { waitMs: CREDENTIAL_REFRESH_PERSIST_WAIT_MS });
|
|
1862
|
+
}
|
|
1863
|
+
catch (error) {
|
|
1864
|
+
if (!isFileLockTimeout(error) || attempt > 0 || !await this.removeDeadFileLock(this.filename))
|
|
1865
|
+
throw error;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
throw new Error('OpenAI rotated credential could not be persisted');
|
|
1869
|
+
}
|
|
1870
|
+
refreshAccountUsageCredential(stored) {
|
|
1871
|
+
const active = this.accountUsageCredentialRefreshes.get(stored.accountId);
|
|
1872
|
+
if (active !== undefined)
|
|
1873
|
+
return active;
|
|
1874
|
+
const operation = this.refreshManagedAccount(stored, false).then((credential) => {
|
|
1875
|
+
if (credential === undefined) {
|
|
1876
|
+
throw new AccountNotFoundError('OpenAI Codex account was removed while its quota was refreshing');
|
|
1877
|
+
}
|
|
1878
|
+
return credential;
|
|
1879
|
+
});
|
|
1880
|
+
let tracked;
|
|
1881
|
+
tracked = operation.finally(() => {
|
|
1882
|
+
if (this.accountUsageCredentialRefreshes.get(stored.accountId) === tracked) {
|
|
1883
|
+
this.accountUsageCredentialRefreshes.delete(stored.accountId);
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
this.accountUsageCredentialRefreshes.set(stored.accountId, tracked);
|
|
1887
|
+
return tracked;
|
|
1888
|
+
}
|
|
1889
|
+
async accountUsageCredential(stored, signal) {
|
|
1890
|
+
if (stored.expires > Date.now() + TOKEN_REFRESH_PREEMPT_MS)
|
|
1891
|
+
return stored;
|
|
1892
|
+
return waitForSignal(this.refreshAccountUsageCredential(stored), signal);
|
|
1893
|
+
}
|
|
1894
|
+
async accountUsages(signal) {
|
|
1895
|
+
const prepared = await withFileLock(this.filename, async () => {
|
|
1896
|
+
const document = (await readCredentialDocument(this.filename))?.document;
|
|
1897
|
+
if (document === undefined)
|
|
1898
|
+
return undefined;
|
|
1899
|
+
return {
|
|
1900
|
+
cacheGeneration: this.usageGeneration,
|
|
1901
|
+
requestGeneration: ++this.accountUsageRequestGeneration,
|
|
1902
|
+
tasks: document.accounts.map(stored => ({
|
|
1903
|
+
accountId: stored.accountId,
|
|
1904
|
+
credential: this.accountUsageCredential(stored, signal),
|
|
1905
|
+
})),
|
|
1906
|
+
};
|
|
1907
|
+
});
|
|
1908
|
+
if (prepared === undefined)
|
|
1909
|
+
return [];
|
|
1910
|
+
const results = await Promise.all(prepared.tasks.map(async (task) => {
|
|
1911
|
+
try {
|
|
1912
|
+
const credential = await task.credential;
|
|
1913
|
+
return {
|
|
1914
|
+
accountId: task.accountId,
|
|
1915
|
+
usage: await this.fetchUsage(credential, boundedAccountUsageSignal(signal)),
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
catch (error) {
|
|
1919
|
+
return { accountId: task.accountId, error: messageOf(error) };
|
|
1920
|
+
}
|
|
1921
|
+
}));
|
|
1922
|
+
if (!signal?.aborted) {
|
|
1923
|
+
const currentAccountId = (await readCredentialDocument(this.filename))?.document.currentAccountId;
|
|
1924
|
+
const current = results.find(row => row.accountId === currentAccountId);
|
|
1925
|
+
if (current?.usage !== undefined
|
|
1926
|
+
&& this.usageGeneration === prepared.cacheGeneration
|
|
1927
|
+
&& this.accountUsageRequestGeneration === prepared.requestGeneration) {
|
|
1928
|
+
this.usageGeneration += 1;
|
|
1929
|
+
this.usageCache = current.usage;
|
|
1930
|
+
this.usageAccountId = currentAccountId ?? undefined;
|
|
1931
|
+
this.usageHasDirectDefault = false;
|
|
1932
|
+
this.usageError = undefined;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
return results;
|
|
1936
|
+
}
|
|
1937
|
+
async fetchUsage(credential, signal) {
|
|
1938
|
+
let response;
|
|
1939
|
+
try {
|
|
1940
|
+
response = await fetch(USAGE_URL, {
|
|
1941
|
+
redirect: 'error',
|
|
1942
|
+
...signal === undefined ? {} : { signal },
|
|
1943
|
+
headers: {
|
|
1944
|
+
accept: 'application/json',
|
|
1945
|
+
authorization: `Bearer ${credential.access}`,
|
|
1946
|
+
'chatgpt-account-id': credential.accountId,
|
|
1947
|
+
'user-agent': 'dsh-openai-codex-auth/0.5.0',
|
|
1948
|
+
},
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
catch (error) {
|
|
1952
|
+
if (signal?.aborted) {
|
|
1953
|
+
throw new Error(isTimeoutAbort(signal) ? 'Codex usage request timed out' : 'Codex usage request cancelled');
|
|
1954
|
+
}
|
|
1955
|
+
throw error;
|
|
1956
|
+
}
|
|
1553
1957
|
if (!response.ok)
|
|
1554
1958
|
throw new Error(`Codex usage request failed (HTTP ${response.status})`);
|
|
1555
1959
|
return normalizeUsage(await response.json());
|
|
@@ -1602,6 +2006,31 @@ export class OpenAICodexAuth extends Service {
|
|
|
1602
2006
|
this.sendJson(res, 500, { error: messageOf(error) });
|
|
1603
2007
|
}
|
|
1604
2008
|
}
|
|
2009
|
+
async handleAccountUsage(req, res) {
|
|
2010
|
+
if (!this.trustedManagementRequest(req, res))
|
|
2011
|
+
return;
|
|
2012
|
+
if (req.method !== 'POST') {
|
|
2013
|
+
this.sendJson(res, 405, { error: 'POST only' }, { allow: 'POST' });
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
if (!this.requireCsrf(req, res))
|
|
2017
|
+
return;
|
|
2018
|
+
const abort = new AbortController();
|
|
2019
|
+
const onAborted = () => { abort.abort(new Error('Account quota request cancelled')); };
|
|
2020
|
+
const observesAbort = typeof req.once === 'function' && typeof req.off === 'function';
|
|
2021
|
+
if (observesAbort)
|
|
2022
|
+
req.once('aborted', onAborted);
|
|
2023
|
+
try {
|
|
2024
|
+
this.sendJson(res, 200, { accounts: await this.accountUsages(abort.signal) });
|
|
2025
|
+
}
|
|
2026
|
+
catch (error) {
|
|
2027
|
+
this.sendJson(res, error instanceof CredentialNotWritableError ? 409 : 500, { error: messageOf(error) });
|
|
2028
|
+
}
|
|
2029
|
+
finally {
|
|
2030
|
+
if (observesAbort)
|
|
2031
|
+
req.off('aborted', onAborted);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
1605
2034
|
async handleDeviceStart(req, res) {
|
|
1606
2035
|
if (!this.trustedManagementRequest(req, res))
|
|
1607
2036
|
return;
|