@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.
@@ -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
- if (!(0, external_node_fs_namespaceObject.existsSync)(authPath)) return {
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
- const codexAuth = resolveCodexAuthFromFile();
79
- const accessToken = codexAuth.accessToken || options.fallbackToken;
80
- const accountId = codexAuth.accountId || options.fallbackAccountId;
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
- const response = await baseFetch(input, {
89
- ...init,
90
- headers,
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
  }
@@ -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
- if (!existsSync(authPath)) return {
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
- const codexAuth = resolveCodexAuthFromFile();
49
- const accessToken = codexAuth.accessToken || options.fallbackToken;
50
- const accountId = codexAuth.accountId || options.fallbackAccountId;
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
- const response = await baseFetch(input, {
59
- ...init,
60
- headers,
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 {