@wingman-ai/gateway 0.2.4 → 0.2.5
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/dist/providers/codex.cjs +230 -37
- package/dist/providers/codex.d.ts +2 -0
- package/dist/providers/codex.js +231 -38
- package/dist/tests/codex-provider.test.cjs +173 -0
- package/dist/tests/codex-provider.test.js +174 -1
- package/dist/webui/assets/{index-Dlyzwalc.js → index-C7EuTbnE.js} +14 -14
- package/dist/webui/index.html +1 -1
- package/package.json +10 -10
package/dist/providers/codex.cjs
CHANGED
|
@@ -34,6 +34,9 @@ const external_node_path_namespaceObject = require("node:path");
|
|
|
34
34
|
const external_logger_cjs_namespaceObject = require("../logger.cjs");
|
|
35
35
|
const CODEX_HOME_ENV = "CODEX_HOME";
|
|
36
36
|
const CODEX_AUTH_FILE = "auth.json";
|
|
37
|
+
const CODEX_REFRESH_TOKEN_URL_OVERRIDE_ENV = "CODEX_REFRESH_TOKEN_URL_OVERRIDE";
|
|
38
|
+
const DEFAULT_CODEX_REFRESH_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
39
|
+
const TOKEN_REFRESH_BUFFER_MS = 300000;
|
|
37
40
|
const DEFAULT_CODEX_INSTRUCTIONS = "You are Wingman, a coding assistant. Follow the user's request exactly and keep tool usage focused.";
|
|
38
41
|
const logger = (0, external_logger_cjs_namespaceObject.createLogger)();
|
|
39
42
|
function getCodexAuthPath() {
|
|
@@ -43,53 +46,77 @@ function getCodexAuthPath() {
|
|
|
43
46
|
}
|
|
44
47
|
function resolveCodexAuthFromFile() {
|
|
45
48
|
const authPath = getCodexAuthPath();
|
|
46
|
-
|
|
49
|
+
const root = readCodexAuthRoot(authPath);
|
|
50
|
+
if (!root) return {
|
|
51
|
+
authPath
|
|
52
|
+
};
|
|
53
|
+
const tokens = root.tokens && "object" == typeof root.tokens ? root.tokens : void 0;
|
|
54
|
+
const accessToken = firstNonEmptyString([
|
|
55
|
+
tokens?.access_token,
|
|
56
|
+
root.access_token
|
|
57
|
+
]);
|
|
58
|
+
const refreshToken = firstNonEmptyString([
|
|
59
|
+
tokens?.refresh_token,
|
|
60
|
+
root.refresh_token
|
|
61
|
+
]);
|
|
62
|
+
const idToken = firstNonEmptyString([
|
|
63
|
+
tokens?.id_token,
|
|
64
|
+
root.id_token
|
|
65
|
+
]);
|
|
66
|
+
const accountId = firstNonEmptyString([
|
|
67
|
+
tokens?.account_id,
|
|
68
|
+
root.account_id,
|
|
69
|
+
extractAccountIdFromIdToken(idToken)
|
|
70
|
+
]);
|
|
71
|
+
return {
|
|
72
|
+
accessToken,
|
|
73
|
+
refreshToken,
|
|
74
|
+
idToken,
|
|
75
|
+
accountId,
|
|
47
76
|
authPath
|
|
48
77
|
};
|
|
49
|
-
try {
|
|
50
|
-
const parsed = JSON.parse((0, external_node_fs_namespaceObject.readFileSync)(authPath, "utf-8"));
|
|
51
|
-
if (!parsed || "object" != typeof parsed) return {
|
|
52
|
-
authPath
|
|
53
|
-
};
|
|
54
|
-
const root = parsed;
|
|
55
|
-
const tokens = root.tokens && "object" == typeof root.tokens ? root.tokens : void 0;
|
|
56
|
-
const accessToken = firstNonEmptyString([
|
|
57
|
-
tokens?.access_token,
|
|
58
|
-
root.access_token
|
|
59
|
-
]);
|
|
60
|
-
const accountId = firstNonEmptyString([
|
|
61
|
-
tokens?.account_id,
|
|
62
|
-
root.account_id
|
|
63
|
-
]);
|
|
64
|
-
return {
|
|
65
|
-
accessToken,
|
|
66
|
-
accountId,
|
|
67
|
-
authPath
|
|
68
|
-
};
|
|
69
|
-
} catch {
|
|
70
|
-
return {
|
|
71
|
-
authPath
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
78
|
}
|
|
75
79
|
function createCodexFetch(options = {}) {
|
|
76
80
|
const baseFetch = options.baseFetch || globalThis.fetch.bind(globalThis);
|
|
77
81
|
return async (input, init)=>{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
let codexAuth = await maybeRefreshCodexAuth({
|
|
83
|
+
authState: resolveCodexAuthFromFile(),
|
|
84
|
+
baseFetch
|
|
85
|
+
});
|
|
86
|
+
let accessToken = codexAuth.accessToken || options.fallbackToken;
|
|
87
|
+
let accountId = codexAuth.accountId || options.fallbackAccountId;
|
|
81
88
|
if (!accessToken) throw new Error("Codex credentials missing. Run `codex login` or set CODEX_ACCESS_TOKEN.");
|
|
82
|
-
const headers = new Headers(init?.headers || {});
|
|
83
|
-
headers.delete("authorization");
|
|
84
|
-
headers.delete("x-api-key");
|
|
85
|
-
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
86
|
-
if (accountId) headers.set("ChatGPT-Account-ID", accountId);
|
|
87
89
|
const body = withCodexRequestDefaults(init?.body);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
let response = await dispatchCodexRequest({
|
|
91
|
+
input,
|
|
92
|
+
init,
|
|
93
|
+
baseFetch,
|
|
94
|
+
accessToken,
|
|
95
|
+
accountId,
|
|
91
96
|
body
|
|
92
97
|
});
|
|
98
|
+
if ((401 === response.status || 403 === response.status) && canRetryCodexRequest(body) && codexAuth.refreshToken) {
|
|
99
|
+
const refreshed = await maybeRefreshCodexAuth({
|
|
100
|
+
authState: codexAuth,
|
|
101
|
+
baseFetch,
|
|
102
|
+
force: true
|
|
103
|
+
});
|
|
104
|
+
const refreshedAccessToken = refreshed.accessToken || options.fallbackToken;
|
|
105
|
+
const refreshedAccountId = refreshed.accountId || options.fallbackAccountId;
|
|
106
|
+
if (refreshedAccessToken && refreshedAccessToken !== accessToken) {
|
|
107
|
+
codexAuth = refreshed;
|
|
108
|
+
accessToken = refreshedAccessToken;
|
|
109
|
+
accountId = refreshedAccountId;
|
|
110
|
+
response = await dispatchCodexRequest({
|
|
111
|
+
input,
|
|
112
|
+
init,
|
|
113
|
+
baseFetch,
|
|
114
|
+
accessToken,
|
|
115
|
+
accountId,
|
|
116
|
+
body
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
93
120
|
if (!response.ok) {
|
|
94
121
|
let responseBody = "";
|
|
95
122
|
try {
|
|
@@ -105,6 +132,172 @@ function createCodexFetch(options = {}) {
|
|
|
105
132
|
return response;
|
|
106
133
|
};
|
|
107
134
|
}
|
|
135
|
+
async function dispatchCodexRequest(input) {
|
|
136
|
+
const headers = new Headers(input.init?.headers || {});
|
|
137
|
+
headers.delete("authorization");
|
|
138
|
+
headers.delete("x-api-key");
|
|
139
|
+
headers.set("Authorization", `Bearer ${input.accessToken}`);
|
|
140
|
+
if (input.accountId) headers.set("ChatGPT-Account-ID", input.accountId);
|
|
141
|
+
return input.baseFetch(input.input, {
|
|
142
|
+
...input.init,
|
|
143
|
+
headers,
|
|
144
|
+
body: input.body
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function canRetryCodexRequest(body) {
|
|
148
|
+
return null == body || "string" == typeof body || body instanceof URLSearchParams;
|
|
149
|
+
}
|
|
150
|
+
async function maybeRefreshCodexAuth(input) {
|
|
151
|
+
const { authState, baseFetch, force = false } = input;
|
|
152
|
+
if (!authState.refreshToken) return authState;
|
|
153
|
+
const shouldRefresh = force || !authState.accessToken || isTokenExpiredOrExpiring(authState.accessToken);
|
|
154
|
+
if (!shouldRefresh) return authState;
|
|
155
|
+
try {
|
|
156
|
+
const refreshed = await refreshCodexAuthToken(authState, baseFetch);
|
|
157
|
+
if (refreshed) return refreshed;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
logger.warn("Failed to refresh Codex token", {
|
|
160
|
+
error: error instanceof Error ? error.message : String(error)
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return authState;
|
|
164
|
+
}
|
|
165
|
+
async function refreshCodexAuthToken(authState, baseFetch) {
|
|
166
|
+
const refreshToken = authState.refreshToken;
|
|
167
|
+
if (!refreshToken) return;
|
|
168
|
+
const clientId = extractClientIdForRefresh(authState);
|
|
169
|
+
const tokenUrl = resolveCodexRefreshTokenUrl();
|
|
170
|
+
const form = new URLSearchParams({
|
|
171
|
+
grant_type: "refresh_token",
|
|
172
|
+
refresh_token: refreshToken
|
|
173
|
+
});
|
|
174
|
+
if (clientId) form.set("client_id", clientId);
|
|
175
|
+
const response = await baseFetch(tokenUrl, {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: {
|
|
178
|
+
Accept: "application/json",
|
|
179
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
180
|
+
},
|
|
181
|
+
body: form.toString()
|
|
182
|
+
});
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
const preview = await readResponsePreview(response);
|
|
185
|
+
logger.warn("Codex token refresh failed", {
|
|
186
|
+
status: response.status,
|
|
187
|
+
statusText: response.statusText || null,
|
|
188
|
+
bodyPreview: preview || null
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const payload = await response.json();
|
|
193
|
+
const accessToken = firstNonEmptyString([
|
|
194
|
+
payload.access_token
|
|
195
|
+
]);
|
|
196
|
+
if (!accessToken) return void logger.warn("Codex token refresh failed: missing access_token");
|
|
197
|
+
const idToken = firstNonEmptyString([
|
|
198
|
+
payload.id_token,
|
|
199
|
+
authState.idToken
|
|
200
|
+
]);
|
|
201
|
+
const refreshed = {
|
|
202
|
+
accessToken,
|
|
203
|
+
refreshToken: firstNonEmptyString([
|
|
204
|
+
payload.refresh_token,
|
|
205
|
+
authState.refreshToken
|
|
206
|
+
]),
|
|
207
|
+
idToken,
|
|
208
|
+
accountId: firstNonEmptyString([
|
|
209
|
+
extractAccountIdFromIdToken(idToken),
|
|
210
|
+
authState.accountId
|
|
211
|
+
])
|
|
212
|
+
};
|
|
213
|
+
persistCodexAuthUpdate(authState.authPath, refreshed);
|
|
214
|
+
return resolveCodexAuthFromFile();
|
|
215
|
+
}
|
|
216
|
+
async function readResponsePreview(response) {
|
|
217
|
+
try {
|
|
218
|
+
const text = await response.text();
|
|
219
|
+
return text.trim().slice(0, 1200);
|
|
220
|
+
} catch {
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function persistCodexAuthUpdate(authPath, updated) {
|
|
225
|
+
const root = readCodexAuthRoot(authPath) || {};
|
|
226
|
+
const existingTokens = root.tokens && "object" == typeof root.tokens && !Array.isArray(root.tokens) ? root.tokens : {};
|
|
227
|
+
const tokens = {
|
|
228
|
+
...existingTokens,
|
|
229
|
+
access_token: updated.accessToken
|
|
230
|
+
};
|
|
231
|
+
if (updated.refreshToken) tokens.refresh_token = updated.refreshToken;
|
|
232
|
+
if (updated.idToken) tokens.id_token = updated.idToken;
|
|
233
|
+
if (updated.accountId) tokens.account_id = updated.accountId;
|
|
234
|
+
root.tokens = tokens;
|
|
235
|
+
root.last_refresh = new Date().toISOString();
|
|
236
|
+
(0, external_node_fs_namespaceObject.writeFileSync)(authPath, `${JSON.stringify(root, null, 2)}\n`, "utf-8");
|
|
237
|
+
}
|
|
238
|
+
function readCodexAuthRoot(authPath) {
|
|
239
|
+
if (!(0, external_node_fs_namespaceObject.existsSync)(authPath)) return;
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse((0, external_node_fs_namespaceObject.readFileSync)(authPath, "utf-8"));
|
|
242
|
+
if (!parsed || "object" != typeof parsed || Array.isArray(parsed)) return;
|
|
243
|
+
return parsed;
|
|
244
|
+
} catch {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function resolveCodexRefreshTokenUrl() {
|
|
249
|
+
const override = process.env[CODEX_REFRESH_TOKEN_URL_OVERRIDE_ENV];
|
|
250
|
+
if (override?.trim()) return override.trim();
|
|
251
|
+
return DEFAULT_CODEX_REFRESH_TOKEN_URL;
|
|
252
|
+
}
|
|
253
|
+
function extractClientIdForRefresh(authState) {
|
|
254
|
+
const accessTokenClaims = parseJwtPayload(authState.accessToken);
|
|
255
|
+
const accessTokenClientId = accessTokenClaims && "string" == typeof accessTokenClaims.client_id ? accessTokenClaims.client_id : void 0;
|
|
256
|
+
if (accessTokenClientId?.trim()) return accessTokenClientId.trim();
|
|
257
|
+
const idTokenClaims = parseJwtPayload(authState.idToken);
|
|
258
|
+
if (!idTokenClaims) return;
|
|
259
|
+
const aud = idTokenClaims.aud;
|
|
260
|
+
if ("string" == typeof aud && aud.trim()) return aud.trim();
|
|
261
|
+
if (Array.isArray(aud)) {
|
|
262
|
+
for (const value of aud)if ("string" == typeof value && value.trim()) return value.trim();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function isTokenExpiredOrExpiring(token) {
|
|
266
|
+
const expiryMs = extractTokenExpiryMs(token);
|
|
267
|
+
if (!expiryMs) return false;
|
|
268
|
+
return expiryMs <= Date.now() + TOKEN_REFRESH_BUFFER_MS;
|
|
269
|
+
}
|
|
270
|
+
function extractTokenExpiryMs(token) {
|
|
271
|
+
const payload = parseJwtPayload(token);
|
|
272
|
+
if (!payload || "number" != typeof payload.exp) return;
|
|
273
|
+
return 1000 * payload.exp;
|
|
274
|
+
}
|
|
275
|
+
function extractAccountIdFromIdToken(idToken) {
|
|
276
|
+
const payload = parseJwtPayload(idToken);
|
|
277
|
+
if (!payload) return;
|
|
278
|
+
const nested = payload["https://api.openai.com/auth"];
|
|
279
|
+
if (nested && "object" == typeof nested && !Array.isArray(nested)) {
|
|
280
|
+
const accountId = nested.chatgpt_account_id;
|
|
281
|
+
if ("string" == typeof accountId && accountId.trim()) return accountId.trim();
|
|
282
|
+
}
|
|
283
|
+
const direct = payload.chatgpt_account_id;
|
|
284
|
+
if ("string" == typeof direct && direct.trim()) return direct.trim();
|
|
285
|
+
}
|
|
286
|
+
function parseJwtPayload(token) {
|
|
287
|
+
if (!token) return;
|
|
288
|
+
const parts = token.split(".");
|
|
289
|
+
if (3 !== parts.length) return;
|
|
290
|
+
try {
|
|
291
|
+
const payload = parts[1];
|
|
292
|
+
const normalized = payload + "=".repeat((4 - payload.length % 4) % 4);
|
|
293
|
+
const decoded = Buffer.from(normalized, "base64url").toString("utf-8");
|
|
294
|
+
const parsed = JSON.parse(decoded);
|
|
295
|
+
if (!parsed || "object" != typeof parsed || Array.isArray(parsed)) return;
|
|
296
|
+
return parsed;
|
|
297
|
+
} catch {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
108
301
|
function withCodexRequestDefaults(body) {
|
|
109
302
|
if ("string" != typeof body || !body.trim()) return body;
|
|
110
303
|
try {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
type FetchLike = (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => ReturnType<typeof fetch>;
|
|
2
2
|
export interface CodexAuthState {
|
|
3
3
|
accessToken?: string;
|
|
4
|
+
refreshToken?: string;
|
|
5
|
+
idToken?: string;
|
|
4
6
|
accountId?: string;
|
|
5
7
|
authPath: string;
|
|
6
8
|
}
|
package/dist/providers/codex.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { createLogger } from "../logger.js";
|
|
5
5
|
const CODEX_HOME_ENV = "CODEX_HOME";
|
|
6
6
|
const CODEX_AUTH_FILE = "auth.json";
|
|
7
|
+
const CODEX_REFRESH_TOKEN_URL_OVERRIDE_ENV = "CODEX_REFRESH_TOKEN_URL_OVERRIDE";
|
|
8
|
+
const DEFAULT_CODEX_REFRESH_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
9
|
+
const TOKEN_REFRESH_BUFFER_MS = 300000;
|
|
7
10
|
const DEFAULT_CODEX_INSTRUCTIONS = "You are Wingman, a coding assistant. Follow the user's request exactly and keep tool usage focused.";
|
|
8
11
|
const logger = createLogger();
|
|
9
12
|
function getCodexAuthPath() {
|
|
@@ -13,53 +16,77 @@ function getCodexAuthPath() {
|
|
|
13
16
|
}
|
|
14
17
|
function resolveCodexAuthFromFile() {
|
|
15
18
|
const authPath = getCodexAuthPath();
|
|
16
|
-
|
|
19
|
+
const root = readCodexAuthRoot(authPath);
|
|
20
|
+
if (!root) return {
|
|
21
|
+
authPath
|
|
22
|
+
};
|
|
23
|
+
const tokens = root.tokens && "object" == typeof root.tokens ? root.tokens : void 0;
|
|
24
|
+
const accessToken = firstNonEmptyString([
|
|
25
|
+
tokens?.access_token,
|
|
26
|
+
root.access_token
|
|
27
|
+
]);
|
|
28
|
+
const refreshToken = firstNonEmptyString([
|
|
29
|
+
tokens?.refresh_token,
|
|
30
|
+
root.refresh_token
|
|
31
|
+
]);
|
|
32
|
+
const idToken = firstNonEmptyString([
|
|
33
|
+
tokens?.id_token,
|
|
34
|
+
root.id_token
|
|
35
|
+
]);
|
|
36
|
+
const accountId = firstNonEmptyString([
|
|
37
|
+
tokens?.account_id,
|
|
38
|
+
root.account_id,
|
|
39
|
+
extractAccountIdFromIdToken(idToken)
|
|
40
|
+
]);
|
|
41
|
+
return {
|
|
42
|
+
accessToken,
|
|
43
|
+
refreshToken,
|
|
44
|
+
idToken,
|
|
45
|
+
accountId,
|
|
17
46
|
authPath
|
|
18
47
|
};
|
|
19
|
-
try {
|
|
20
|
-
const parsed = JSON.parse(readFileSync(authPath, "utf-8"));
|
|
21
|
-
if (!parsed || "object" != typeof parsed) return {
|
|
22
|
-
authPath
|
|
23
|
-
};
|
|
24
|
-
const root = parsed;
|
|
25
|
-
const tokens = root.tokens && "object" == typeof root.tokens ? root.tokens : void 0;
|
|
26
|
-
const accessToken = firstNonEmptyString([
|
|
27
|
-
tokens?.access_token,
|
|
28
|
-
root.access_token
|
|
29
|
-
]);
|
|
30
|
-
const accountId = firstNonEmptyString([
|
|
31
|
-
tokens?.account_id,
|
|
32
|
-
root.account_id
|
|
33
|
-
]);
|
|
34
|
-
return {
|
|
35
|
-
accessToken,
|
|
36
|
-
accountId,
|
|
37
|
-
authPath
|
|
38
|
-
};
|
|
39
|
-
} catch {
|
|
40
|
-
return {
|
|
41
|
-
authPath
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
48
|
}
|
|
45
49
|
function createCodexFetch(options = {}) {
|
|
46
50
|
const baseFetch = options.baseFetch || globalThis.fetch.bind(globalThis);
|
|
47
51
|
return async (input, init)=>{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
let codexAuth = await maybeRefreshCodexAuth({
|
|
53
|
+
authState: resolveCodexAuthFromFile(),
|
|
54
|
+
baseFetch
|
|
55
|
+
});
|
|
56
|
+
let accessToken = codexAuth.accessToken || options.fallbackToken;
|
|
57
|
+
let accountId = codexAuth.accountId || options.fallbackAccountId;
|
|
51
58
|
if (!accessToken) throw new Error("Codex credentials missing. Run `codex login` or set CODEX_ACCESS_TOKEN.");
|
|
52
|
-
const headers = new Headers(init?.headers || {});
|
|
53
|
-
headers.delete("authorization");
|
|
54
|
-
headers.delete("x-api-key");
|
|
55
|
-
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
56
|
-
if (accountId) headers.set("ChatGPT-Account-ID", accountId);
|
|
57
59
|
const body = withCodexRequestDefaults(init?.body);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
let response = await dispatchCodexRequest({
|
|
61
|
+
input,
|
|
62
|
+
init,
|
|
63
|
+
baseFetch,
|
|
64
|
+
accessToken,
|
|
65
|
+
accountId,
|
|
61
66
|
body
|
|
62
67
|
});
|
|
68
|
+
if ((401 === response.status || 403 === response.status) && canRetryCodexRequest(body) && codexAuth.refreshToken) {
|
|
69
|
+
const refreshed = await maybeRefreshCodexAuth({
|
|
70
|
+
authState: codexAuth,
|
|
71
|
+
baseFetch,
|
|
72
|
+
force: true
|
|
73
|
+
});
|
|
74
|
+
const refreshedAccessToken = refreshed.accessToken || options.fallbackToken;
|
|
75
|
+
const refreshedAccountId = refreshed.accountId || options.fallbackAccountId;
|
|
76
|
+
if (refreshedAccessToken && refreshedAccessToken !== accessToken) {
|
|
77
|
+
codexAuth = refreshed;
|
|
78
|
+
accessToken = refreshedAccessToken;
|
|
79
|
+
accountId = refreshedAccountId;
|
|
80
|
+
response = await dispatchCodexRequest({
|
|
81
|
+
input,
|
|
82
|
+
init,
|
|
83
|
+
baseFetch,
|
|
84
|
+
accessToken,
|
|
85
|
+
accountId,
|
|
86
|
+
body
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
63
90
|
if (!response.ok) {
|
|
64
91
|
let responseBody = "";
|
|
65
92
|
try {
|
|
@@ -75,6 +102,172 @@ function createCodexFetch(options = {}) {
|
|
|
75
102
|
return response;
|
|
76
103
|
};
|
|
77
104
|
}
|
|
105
|
+
async function dispatchCodexRequest(input) {
|
|
106
|
+
const headers = new Headers(input.init?.headers || {});
|
|
107
|
+
headers.delete("authorization");
|
|
108
|
+
headers.delete("x-api-key");
|
|
109
|
+
headers.set("Authorization", `Bearer ${input.accessToken}`);
|
|
110
|
+
if (input.accountId) headers.set("ChatGPT-Account-ID", input.accountId);
|
|
111
|
+
return input.baseFetch(input.input, {
|
|
112
|
+
...input.init,
|
|
113
|
+
headers,
|
|
114
|
+
body: input.body
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function canRetryCodexRequest(body) {
|
|
118
|
+
return null == body || "string" == typeof body || body instanceof URLSearchParams;
|
|
119
|
+
}
|
|
120
|
+
async function maybeRefreshCodexAuth(input) {
|
|
121
|
+
const { authState, baseFetch, force = false } = input;
|
|
122
|
+
if (!authState.refreshToken) return authState;
|
|
123
|
+
const shouldRefresh = force || !authState.accessToken || isTokenExpiredOrExpiring(authState.accessToken);
|
|
124
|
+
if (!shouldRefresh) return authState;
|
|
125
|
+
try {
|
|
126
|
+
const refreshed = await refreshCodexAuthToken(authState, baseFetch);
|
|
127
|
+
if (refreshed) return refreshed;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
logger.warn("Failed to refresh Codex token", {
|
|
130
|
+
error: error instanceof Error ? error.message : String(error)
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return authState;
|
|
134
|
+
}
|
|
135
|
+
async function refreshCodexAuthToken(authState, baseFetch) {
|
|
136
|
+
const refreshToken = authState.refreshToken;
|
|
137
|
+
if (!refreshToken) return;
|
|
138
|
+
const clientId = extractClientIdForRefresh(authState);
|
|
139
|
+
const tokenUrl = resolveCodexRefreshTokenUrl();
|
|
140
|
+
const form = new URLSearchParams({
|
|
141
|
+
grant_type: "refresh_token",
|
|
142
|
+
refresh_token: refreshToken
|
|
143
|
+
});
|
|
144
|
+
if (clientId) form.set("client_id", clientId);
|
|
145
|
+
const response = await baseFetch(tokenUrl, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: {
|
|
148
|
+
Accept: "application/json",
|
|
149
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
150
|
+
},
|
|
151
|
+
body: form.toString()
|
|
152
|
+
});
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const preview = await readResponsePreview(response);
|
|
155
|
+
logger.warn("Codex token refresh failed", {
|
|
156
|
+
status: response.status,
|
|
157
|
+
statusText: response.statusText || null,
|
|
158
|
+
bodyPreview: preview || null
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const payload = await response.json();
|
|
163
|
+
const accessToken = firstNonEmptyString([
|
|
164
|
+
payload.access_token
|
|
165
|
+
]);
|
|
166
|
+
if (!accessToken) return void logger.warn("Codex token refresh failed: missing access_token");
|
|
167
|
+
const idToken = firstNonEmptyString([
|
|
168
|
+
payload.id_token,
|
|
169
|
+
authState.idToken
|
|
170
|
+
]);
|
|
171
|
+
const refreshed = {
|
|
172
|
+
accessToken,
|
|
173
|
+
refreshToken: firstNonEmptyString([
|
|
174
|
+
payload.refresh_token,
|
|
175
|
+
authState.refreshToken
|
|
176
|
+
]),
|
|
177
|
+
idToken,
|
|
178
|
+
accountId: firstNonEmptyString([
|
|
179
|
+
extractAccountIdFromIdToken(idToken),
|
|
180
|
+
authState.accountId
|
|
181
|
+
])
|
|
182
|
+
};
|
|
183
|
+
persistCodexAuthUpdate(authState.authPath, refreshed);
|
|
184
|
+
return resolveCodexAuthFromFile();
|
|
185
|
+
}
|
|
186
|
+
async function readResponsePreview(response) {
|
|
187
|
+
try {
|
|
188
|
+
const text = await response.text();
|
|
189
|
+
return text.trim().slice(0, 1200);
|
|
190
|
+
} catch {
|
|
191
|
+
return "";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function persistCodexAuthUpdate(authPath, updated) {
|
|
195
|
+
const root = readCodexAuthRoot(authPath) || {};
|
|
196
|
+
const existingTokens = root.tokens && "object" == typeof root.tokens && !Array.isArray(root.tokens) ? root.tokens : {};
|
|
197
|
+
const tokens = {
|
|
198
|
+
...existingTokens,
|
|
199
|
+
access_token: updated.accessToken
|
|
200
|
+
};
|
|
201
|
+
if (updated.refreshToken) tokens.refresh_token = updated.refreshToken;
|
|
202
|
+
if (updated.idToken) tokens.id_token = updated.idToken;
|
|
203
|
+
if (updated.accountId) tokens.account_id = updated.accountId;
|
|
204
|
+
root.tokens = tokens;
|
|
205
|
+
root.last_refresh = new Date().toISOString();
|
|
206
|
+
writeFileSync(authPath, `${JSON.stringify(root, null, 2)}\n`, "utf-8");
|
|
207
|
+
}
|
|
208
|
+
function readCodexAuthRoot(authPath) {
|
|
209
|
+
if (!existsSync(authPath)) return;
|
|
210
|
+
try {
|
|
211
|
+
const parsed = JSON.parse(readFileSync(authPath, "utf-8"));
|
|
212
|
+
if (!parsed || "object" != typeof parsed || Array.isArray(parsed)) return;
|
|
213
|
+
return parsed;
|
|
214
|
+
} catch {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function resolveCodexRefreshTokenUrl() {
|
|
219
|
+
const override = process.env[CODEX_REFRESH_TOKEN_URL_OVERRIDE_ENV];
|
|
220
|
+
if (override?.trim()) return override.trim();
|
|
221
|
+
return DEFAULT_CODEX_REFRESH_TOKEN_URL;
|
|
222
|
+
}
|
|
223
|
+
function extractClientIdForRefresh(authState) {
|
|
224
|
+
const accessTokenClaims = parseJwtPayload(authState.accessToken);
|
|
225
|
+
const accessTokenClientId = accessTokenClaims && "string" == typeof accessTokenClaims.client_id ? accessTokenClaims.client_id : void 0;
|
|
226
|
+
if (accessTokenClientId?.trim()) return accessTokenClientId.trim();
|
|
227
|
+
const idTokenClaims = parseJwtPayload(authState.idToken);
|
|
228
|
+
if (!idTokenClaims) return;
|
|
229
|
+
const aud = idTokenClaims.aud;
|
|
230
|
+
if ("string" == typeof aud && aud.trim()) return aud.trim();
|
|
231
|
+
if (Array.isArray(aud)) {
|
|
232
|
+
for (const value of aud)if ("string" == typeof value && value.trim()) return value.trim();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function isTokenExpiredOrExpiring(token) {
|
|
236
|
+
const expiryMs = extractTokenExpiryMs(token);
|
|
237
|
+
if (!expiryMs) return false;
|
|
238
|
+
return expiryMs <= Date.now() + TOKEN_REFRESH_BUFFER_MS;
|
|
239
|
+
}
|
|
240
|
+
function extractTokenExpiryMs(token) {
|
|
241
|
+
const payload = parseJwtPayload(token);
|
|
242
|
+
if (!payload || "number" != typeof payload.exp) return;
|
|
243
|
+
return 1000 * payload.exp;
|
|
244
|
+
}
|
|
245
|
+
function extractAccountIdFromIdToken(idToken) {
|
|
246
|
+
const payload = parseJwtPayload(idToken);
|
|
247
|
+
if (!payload) return;
|
|
248
|
+
const nested = payload["https://api.openai.com/auth"];
|
|
249
|
+
if (nested && "object" == typeof nested && !Array.isArray(nested)) {
|
|
250
|
+
const accountId = nested.chatgpt_account_id;
|
|
251
|
+
if ("string" == typeof accountId && accountId.trim()) return accountId.trim();
|
|
252
|
+
}
|
|
253
|
+
const direct = payload.chatgpt_account_id;
|
|
254
|
+
if ("string" == typeof direct && direct.trim()) return direct.trim();
|
|
255
|
+
}
|
|
256
|
+
function parseJwtPayload(token) {
|
|
257
|
+
if (!token) return;
|
|
258
|
+
const parts = token.split(".");
|
|
259
|
+
if (3 !== parts.length) return;
|
|
260
|
+
try {
|
|
261
|
+
const payload = parts[1];
|
|
262
|
+
const normalized = payload + "=".repeat((4 - payload.length % 4) % 4);
|
|
263
|
+
const decoded = Buffer.from(normalized, "base64url").toString("utf-8");
|
|
264
|
+
const parsed = JSON.parse(decoded);
|
|
265
|
+
if (!parsed || "object" != typeof parsed || Array.isArray(parsed)) return;
|
|
266
|
+
return parsed;
|
|
267
|
+
} catch {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
78
271
|
function withCodexRequestDefaults(body) {
|
|
79
272
|
if ("string" != typeof body || !body.trim()) return body;
|
|
80
273
|
try {
|