@iam-brain/opencode-codex-auth 0.3.1 → 0.3.2

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.
Files changed (90) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/lib/codex-native/accounts.d.ts +21 -0
  4. package/dist/lib/codex-native/accounts.d.ts.map +1 -0
  5. package/dist/lib/codex-native/accounts.js +203 -0
  6. package/dist/lib/codex-native/accounts.js.map +1 -0
  7. package/dist/lib/codex-native/acquire-auth.d.ts +22 -0
  8. package/dist/lib/codex-native/acquire-auth.d.ts.map +1 -0
  9. package/dist/lib/codex-native/acquire-auth.js +338 -0
  10. package/dist/lib/codex-native/acquire-auth.js.map +1 -0
  11. package/dist/lib/codex-native/auth-menu-flow.d.ts +9 -0
  12. package/dist/lib/codex-native/auth-menu-flow.d.ts.map +1 -0
  13. package/dist/lib/codex-native/auth-menu-flow.js +192 -0
  14. package/dist/lib/codex-native/auth-menu-flow.js.map +1 -0
  15. package/dist/lib/codex-native/auth-menu-quotas.d.ts +9 -0
  16. package/dist/lib/codex-native/auth-menu-quotas.d.ts.map +1 -0
  17. package/dist/lib/codex-native/auth-menu-quotas.js +111 -0
  18. package/dist/lib/codex-native/auth-menu-quotas.js.map +1 -0
  19. package/dist/lib/codex-native/catalog-sync.d.ts +28 -0
  20. package/dist/lib/codex-native/catalog-sync.d.ts.map +1 -0
  21. package/dist/lib/codex-native/catalog-sync.js +36 -0
  22. package/dist/lib/codex-native/catalog-sync.js.map +1 -0
  23. package/dist/lib/codex-native/chat-hooks.d.ts +76 -0
  24. package/dist/lib/codex-native/chat-hooks.d.ts.map +1 -0
  25. package/dist/lib/codex-native/chat-hooks.js +136 -0
  26. package/dist/lib/codex-native/chat-hooks.js.map +1 -0
  27. package/dist/lib/codex-native/oauth-auth-methods.d.ts +45 -0
  28. package/dist/lib/codex-native/oauth-auth-methods.d.ts.map +1 -0
  29. package/dist/lib/codex-native/oauth-auth-methods.js +171 -0
  30. package/dist/lib/codex-native/oauth-auth-methods.js.map +1 -0
  31. package/dist/lib/codex-native/oauth-persistence.d.ts +4 -0
  32. package/dist/lib/codex-native/oauth-persistence.d.ts.map +1 -0
  33. package/dist/lib/codex-native/oauth-persistence.js +28 -0
  34. package/dist/lib/codex-native/oauth-persistence.js.map +1 -0
  35. package/dist/lib/codex-native/oauth-server.d.ts.map +1 -1
  36. package/dist/lib/codex-native/oauth-server.js +31 -1
  37. package/dist/lib/codex-native/oauth-server.js.map +1 -1
  38. package/dist/lib/codex-native/oauth-utils.d.ts +51 -0
  39. package/dist/lib/codex-native/oauth-utils.d.ts.map +1 -0
  40. package/dist/lib/codex-native/oauth-utils.js +268 -0
  41. package/dist/lib/codex-native/oauth-utils.js.map +1 -0
  42. package/dist/lib/codex-native/openai-loader-fetch.d.ts +36 -0
  43. package/dist/lib/codex-native/openai-loader-fetch.d.ts.map +1 -0
  44. package/dist/lib/codex-native/openai-loader-fetch.js +191 -0
  45. package/dist/lib/codex-native/openai-loader-fetch.js.map +1 -0
  46. package/dist/lib/codex-native/rate-limit-snapshots.d.ts +2 -0
  47. package/dist/lib/codex-native/rate-limit-snapshots.d.ts.map +1 -0
  48. package/dist/lib/codex-native/rate-limit-snapshots.js +24 -0
  49. package/dist/lib/codex-native/rate-limit-snapshots.js.map +1 -0
  50. package/dist/lib/codex-native/request-routing.d.ts +3 -0
  51. package/dist/lib/codex-native/request-routing.d.ts.map +1 -0
  52. package/dist/lib/codex-native/request-routing.js +41 -0
  53. package/dist/lib/codex-native/request-routing.js.map +1 -0
  54. package/dist/lib/codex-native/request-transform-pipeline.d.ts +19 -0
  55. package/dist/lib/codex-native/request-transform-pipeline.d.ts.map +1 -0
  56. package/dist/lib/codex-native/request-transform-pipeline.js +24 -0
  57. package/dist/lib/codex-native/request-transform-pipeline.js.map +1 -0
  58. package/dist/lib/codex-native/request-transform.d.ts +8 -4
  59. package/dist/lib/codex-native/request-transform.d.ts.map +1 -1
  60. package/dist/lib/codex-native/request-transform.js +103 -33
  61. package/dist/lib/codex-native/request-transform.js.map +1 -1
  62. package/dist/lib/codex-native/session-affinity-state.d.ts +15 -0
  63. package/dist/lib/codex-native/session-affinity-state.d.ts.map +1 -0
  64. package/dist/lib/codex-native/session-affinity-state.js +49 -0
  65. package/dist/lib/codex-native/session-affinity-state.js.map +1 -0
  66. package/dist/lib/codex-native/session-messages.d.ts +8 -0
  67. package/dist/lib/codex-native/session-messages.d.ts.map +1 -0
  68. package/dist/lib/codex-native/session-messages.js +55 -0
  69. package/dist/lib/codex-native/session-messages.js.map +1 -0
  70. package/dist/lib/codex-native.d.ts +6 -30
  71. package/dist/lib/codex-native.d.ts.map +1 -1
  72. package/dist/lib/codex-native.js +113 -1641
  73. package/dist/lib/codex-native.js.map +1 -1
  74. package/dist/lib/config.d.ts +11 -11
  75. package/dist/lib/config.d.ts.map +1 -1
  76. package/dist/lib/config.js +109 -152
  77. package/dist/lib/config.js.map +1 -1
  78. package/dist/lib/model-catalog.d.ts +1 -0
  79. package/dist/lib/model-catalog.d.ts.map +1 -1
  80. package/dist/lib/model-catalog.js +166 -8
  81. package/dist/lib/model-catalog.js.map +1 -1
  82. package/dist/lib/request-snapshots.d.ts +2 -0
  83. package/dist/lib/request-snapshots.d.ts.map +1 -1
  84. package/dist/lib/request-snapshots.js +48 -1
  85. package/dist/lib/request-snapshots.js.map +1 -1
  86. package/dist/lib/rotation.d.ts.map +1 -1
  87. package/dist/lib/rotation.js +3 -0
  88. package/dist/lib/rotation.js.map +1 -1
  89. package/package.json +3 -2
  90. package/schemas/codex-config.schema.json +12 -43
@@ -1,64 +1,22 @@
1
- import { extractAccountIdFromClaims as extractAccountIdFromClaimsBase, extractEmailFromClaims, extractPlanFromClaims, parseJwtClaims } from "./claims";
2
- import { CodexStatus } from "./codex-status";
3
- import { loadSnapshots, saveSnapshots } from "./codex-status-storage";
4
- import { PluginFatalError, formatWaitTime, isPluginFatalError, toSyntheticErrorResponse } from "./fatal-errors";
5
- import { buildIdentityKey, ensureIdentityKey, normalizeEmail, normalizePlan, synchronizeIdentityKey } from "./identity";
6
- import { defaultSessionAffinityPath, defaultSnapshotsPath } from "./paths";
7
- import { createStickySessionState, selectAccount } from "./rotation";
8
- import { ensureOpenAIOAuthDomain, getOpenAIOAuthDomain, importLegacyInstallData, listOpenAIOAuthDomains, loadAuthStorage, saveAuthStorage, setAccountCooldown, shouldOfferLegacyTransfer } from "./storage";
9
- import { toolOutputForStatus } from "./codex-status-tool";
10
- import { FetchOrchestrator, createFetchOrchestratorState } from "./fetch-orchestrator";
1
+ import { loadAuthStorage, setAccountCooldown } from "./storage";
11
2
  import { formatToastMessage } from "./toast";
12
- import { runAuthMenuOnce } from "./ui/auth-menu-runner";
13
- import { shouldUseColor } from "./ui/tty/ansi";
14
- import { applyCodexCatalogToProviderModels, getCodexModelCatalog, getRuntimeDefaultsForModel, resolveInstructionsForModel } from "./model-catalog";
15
- import { fetchQuotaSnapshotFromBackend } from "./codex-quota-fetch";
16
3
  import { createRequestSnapshots } from "./request-snapshots";
17
- import { CODEX_OAUTH_SUCCESS_HTML } from "./oauth-pages";
18
- import { applyCatalogInstructionOverrideToRequest, applyCodexRuntimeDefaultsToParams, findCatalogModelForCandidates, getModelLookupCandidates, getModelThinkingSummariesOverride, getVariantLookupCandidates, remapDeveloperMessagesToUserOnRequest, resolvePersonalityForModel, sanitizeOutboundRequestIfNeeded } from "./codex-native/request-transform";
19
- import { createSessionExistsFn, loadSessionAffinity, pruneSessionAffinitySnapshot, readSessionAffinitySnapshot, saveSessionAffinity, writeSessionAffinitySnapshot } from "./session-affinity";
20
4
  import { resolveCodexOriginator } from "./codex-native/originator";
21
5
  import { tryOpenUrlInBrowser as openUrlInBrowser } from "./codex-native/browser";
22
- import { selectCatalogAuthCandidate } from "./codex-native/catalog-auth";
23
6
  import { buildCodexUserAgent, refreshCodexClientVersionFromGitHub, resolveCodexClientVersion, resolveRequestUserAgent } from "./codex-native/client-identity";
24
7
  import { createOAuthServerController } from "./codex-native/oauth-server";
8
+ import { buildAuthorizeUrl, buildOAuthErrorHtml, buildOAuthSuccessHtml, composeCodexSuccessRedirectUrl, exchangeCodeForTokens, generatePKCE, OAUTH_CALLBACK_ORIGIN, OAUTH_CALLBACK_PATH, OAUTH_CALLBACK_TIMEOUT_MS, OAUTH_CALLBACK_URI, OAUTH_DUMMY_KEY, OAUTH_LOOPBACK_HOST, OAUTH_PORT, OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS, OAUTH_SERVER_SHUTDOWN_GRACE_MS } from "./codex-native/oauth-utils";
9
+ import { refreshQuotaSnapshotsForAuthMenu as refreshQuotaSnapshotsForAuthMenuBase } from "./codex-native/auth-menu-quotas";
10
+ import { persistOAuthTokensForMode } from "./codex-native/oauth-persistence";
11
+ import { createBrowserOAuthAuthorize, createHeadlessOAuthAuthorize } from "./codex-native/oauth-auth-methods";
12
+ import { runInteractiveAuthMenu as runInteractiveAuthMenuBase } from "./codex-native/auth-menu-flow";
13
+ import { handleChatHeadersHook, handleChatMessageHook, handleChatParamsHook, handleSessionCompactingHook, handleTextCompleteHook } from "./codex-native/chat-hooks";
14
+ import { createSessionAffinityRuntimeState } from "./codex-native/session-affinity-state";
15
+ import { initializeCatalogSync } from "./codex-native/catalog-sync";
16
+ import { createOpenAIFetchHandler } from "./codex-native/openai-loader-fetch";
25
17
  export { browserOpenInvocationFor } from "./codex-native/browser";
26
- const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
27
- const ISSUER = "https://auth.openai.com";
28
- const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
29
- const OAUTH_PORT = 1455;
30
- const OAUTH_LOOPBACK_HOST = "127.0.0.1";
31
- const OAUTH_CALLBACK_ORIGIN = `http://${OAUTH_LOOPBACK_HOST}:${OAUTH_PORT}`;
32
- const OAUTH_CALLBACK_PATH = "/auth/callback";
33
- const OAUTH_CALLBACK_URI = `${OAUTH_CALLBACK_ORIGIN}${OAUTH_CALLBACK_PATH}`;
34
- const OAUTH_DUMMY_KEY = "oauth_dummy_key";
35
- const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000;
36
- const AUTH_REFRESH_FAILURE_COOLDOWN_MS = 30_000;
37
- const OAUTH_CALLBACK_TIMEOUT_MS = (() => {
38
- const raw = process.env.CODEX_OAUTH_CALLBACK_TIMEOUT_MS;
39
- if (!raw)
40
- return 10 * 60 * 1000;
41
- const parsed = Number(raw);
42
- return Number.isFinite(parsed) && parsed >= 60_000 ? parsed : 10 * 60 * 1000;
43
- })();
44
- const OAUTH_SERVER_SHUTDOWN_GRACE_MS = (() => {
45
- const raw = process.env.CODEX_OAUTH_SERVER_SHUTDOWN_GRACE_MS;
46
- if (!raw)
47
- return 2000;
48
- const parsed = Number(raw);
49
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : 2000;
50
- })();
51
- const OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS = (() => {
52
- const raw = process.env.CODEX_OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS;
53
- if (!raw)
54
- return 60_000;
55
- const parsed = Number(raw);
56
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : 60_000;
57
- })();
58
- const OPENAI_OUTBOUND_HOST_ALLOWLIST = new Set(["api.openai.com", "auth.openai.com", "chat.openai.com", "chatgpt.com"]);
59
- const AUTH_MENU_QUOTA_SNAPSHOT_TTL_MS = 60_000;
60
- const AUTH_MENU_QUOTA_FAILURE_COOLDOWN_MS = 30_000;
61
- const AUTH_MENU_QUOTA_FETCH_TIMEOUT_MS = 5000;
18
+ export { upsertAccount } from "./codex-native/accounts";
19
+ export { extractAccountId, extractAccountIdFromClaims, refreshAccessToken } from "./codex-native/oauth-utils";
62
20
  const INTERNAL_COLLABORATION_MODE_HEADER = "x-opencode-collaboration-mode-kind";
63
21
  const SESSION_AFFINITY_MISSING_GRACE_MS = 15 * 60 * 1000;
64
22
  const STATIC_FALLBACK_MODELS = [
@@ -80,9 +38,6 @@ Include:
80
38
  Be concise, structured, and focused on helping the next LLM seamlessly continue the work.
81
39
  `;
82
40
  const CODEX_RS_COMPACT_SUMMARY_PREFIX = "Another language model started to solve this problem and produced a summary of its thinking process. You also have access to the state of the tools that were used by that language model. Use this to build on the work that has already been done and avoid duplicating work. Here is the summary produced by the other language model, use the information in this summary to assist with your own analysis:";
83
- function sleep(ms) {
84
- return new Promise((resolve) => setTimeout(resolve, ms));
85
- }
86
41
  export async function tryOpenUrlInBrowser(url, log) {
87
42
  return openUrlInBrowser({
88
43
  url,
@@ -90,62 +45,6 @@ export async function tryOpenUrlInBrowser(url, log) {
90
45
  onEvent: (event, meta) => oauthServerController.emitDebug(event, meta ?? {})
91
46
  });
92
47
  }
93
- function escapeHtml(value) {
94
- return value
95
- .replace(/&/g, "&")
96
- .replace(/</g, "&lt;")
97
- .replace(/>/g, "&gt;")
98
- .replace(/"/g, "&quot;")
99
- .replace(/'/g, "&#39;");
100
- }
101
- async function generatePKCE() {
102
- const verifier = base64UrlEncode(crypto.getRandomValues(new Uint8Array(64)));
103
- const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
104
- const challenge = base64UrlEncode(new Uint8Array(hash));
105
- return { verifier, challenge };
106
- }
107
- function base64UrlEncode(bytes) {
108
- return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
109
- }
110
- function generateState() {
111
- return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
112
- }
113
- export function extractAccountIdFromClaims(claims) {
114
- return extractAccountIdFromClaimsBase(claims);
115
- }
116
- export function extractAccountId(tokens) {
117
- if (!tokens)
118
- return undefined;
119
- if (tokens.id_token) {
120
- const accountId = extractAccountIdFromClaims(parseJwtClaims(tokens.id_token));
121
- if (accountId)
122
- return accountId;
123
- }
124
- if (tokens.access_token) {
125
- return extractAccountIdFromClaims(parseJwtClaims(tokens.access_token));
126
- }
127
- return undefined;
128
- }
129
- function isOAuthTokenRefreshError(value) {
130
- return value instanceof Error && ("status" in value || "oauthCode" in value);
131
- }
132
- function buildAuthorizeUrl(redirectUri, pkce, state, originator) {
133
- const query = [
134
- ["response_type", "code"],
135
- ["client_id", CLIENT_ID],
136
- ["redirect_uri", redirectUri],
137
- ["scope", "openid profile email offline_access"],
138
- ["code_challenge", pkce.challenge],
139
- ["code_challenge_method", "S256"],
140
- ["id_token_add_organizations", "true"],
141
- ["codex_cli_simplified_flow", "true"],
142
- ["state", state],
143
- ["originator", originator]
144
- ]
145
- .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
146
- .join("&");
147
- return `${ISSUER}/oauth/authorize?${query}`;
148
- }
149
48
  export const __testOnly = {
150
49
  buildAuthorizeUrl,
151
50
  generatePKCE,
@@ -160,179 +59,6 @@ export const __testOnly = {
160
59
  isOAuthDebugEnabled,
161
60
  stopOAuthServer
162
61
  };
163
- async function exchangeCodeForTokens(code, redirectUri, pkce) {
164
- const response = await fetch(`${ISSUER}/oauth/token`, {
165
- method: "POST",
166
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
167
- body: new URLSearchParams({
168
- grant_type: "authorization_code",
169
- code,
170
- redirect_uri: redirectUri,
171
- client_id: CLIENT_ID,
172
- code_verifier: pkce.verifier
173
- }).toString()
174
- });
175
- if (!response.ok) {
176
- throw new Error(`Token exchange failed: ${response.status}`);
177
- }
178
- return (await response.json());
179
- }
180
- export async function refreshAccessToken(refreshToken) {
181
- const response = await fetch(`${ISSUER}/oauth/token`, {
182
- method: "POST",
183
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
184
- body: new URLSearchParams({
185
- grant_type: "refresh_token",
186
- refresh_token: refreshToken,
187
- client_id: CLIENT_ID
188
- }).toString()
189
- });
190
- if (!response.ok) {
191
- let oauthCode;
192
- let oauthDescription;
193
- try {
194
- const raw = await response.text();
195
- if (raw) {
196
- const payload = JSON.parse(raw);
197
- if (typeof payload.error === "string")
198
- oauthCode = payload.error;
199
- if (typeof payload.error_description === "string") {
200
- oauthDescription = payload.error_description;
201
- }
202
- }
203
- }
204
- catch {
205
- // Best effort parse only.
206
- }
207
- const detail = oauthCode
208
- ? `${oauthCode}${oauthDescription ? `: ${oauthDescription}` : ""}`
209
- : `status ${response.status}`;
210
- const error = new Error(`Token refresh failed (${detail})`);
211
- error.status = response.status;
212
- error.oauthCode = oauthCode;
213
- throw error;
214
- }
215
- return (await response.json());
216
- }
217
- function getOpenAIAuthClaims(token) {
218
- if (!token)
219
- return {};
220
- const claims = parseJwtClaims(token);
221
- const authClaims = claims?.["https://api.openai.com/auth"];
222
- if (!authClaims || typeof authClaims !== "object" || Array.isArray(authClaims)) {
223
- return {};
224
- }
225
- return authClaims;
226
- }
227
- function getClaimString(claims, key) {
228
- const value = claims[key];
229
- return typeof value === "string" ? value : "";
230
- }
231
- function getClaimBoolean(claims, key) {
232
- const value = claims[key];
233
- return typeof value === "boolean" ? value : false;
234
- }
235
- function composeCodexSuccessRedirectUrl(tokens, options = {}) {
236
- const issuer = options.issuer ?? ISSUER;
237
- const port = options.port ?? OAUTH_PORT;
238
- const idClaims = getOpenAIAuthClaims(tokens.id_token);
239
- const accessClaims = getOpenAIAuthClaims(tokens.access_token);
240
- const needsSetup = !getClaimBoolean(idClaims, "completed_platform_onboarding") && getClaimBoolean(idClaims, "is_org_owner");
241
- const platformUrl = issuer === ISSUER ? "https://platform.openai.com" : "https://platform.api.openai.org";
242
- const params = new URLSearchParams({
243
- needs_setup: String(needsSetup),
244
- org_id: getClaimString(idClaims, "organization_id"),
245
- project_id: getClaimString(idClaims, "project_id"),
246
- plan_type: getClaimString(accessClaims, "chatgpt_plan_type"),
247
- platform_url: platformUrl
248
- });
249
- return `http://localhost:${port}/success?${params.toString()}`;
250
- }
251
- function buildOAuthSuccessHtml(mode = "codex") {
252
- if (mode === "codex")
253
- return CODEX_OAUTH_SUCCESS_HTML;
254
- return `<!doctype html>
255
- <html>
256
- <head>
257
- <title>OpenCode - Codex Authorization Successful</title>
258
- <style>
259
- body {
260
- font-family: system-ui, -apple-system, sans-serif;
261
- display: flex;
262
- justify-content: center;
263
- align-items: center;
264
- height: 100vh;
265
- margin: 0;
266
- background: #131010;
267
- color: #f1ecec;
268
- }
269
- .container {
270
- text-align: center;
271
- padding: 2rem;
272
- }
273
- h1 {
274
- color: #f1ecec;
275
- margin-bottom: 1rem;
276
- }
277
- p {
278
- color: #b7b1b1;
279
- }
280
- </style>
281
- </head>
282
- <body>
283
- <div class="container">
284
- <h1>Authorization Successful</h1>
285
- <p>You can close this window and return to OpenCode.</p>
286
- </div>
287
- <script>
288
- setTimeout(() => window.close(), 2000)
289
- </script>
290
- </body>
291
- </html>`;
292
- }
293
- function buildOAuthErrorHtml(error) {
294
- return `<!doctype html>
295
- <html>
296
- <head>
297
- <title>Sign into Codex</title>
298
- <style>
299
- body {
300
- font-family: system-ui, -apple-system, sans-serif;
301
- display: flex;
302
- justify-content: center;
303
- align-items: center;
304
- height: 100vh;
305
- margin: 0;
306
- background: #131010;
307
- color: #f1ecec;
308
- }
309
- .container {
310
- text-align: center;
311
- padding: 2rem;
312
- }
313
- h1 {
314
- color: #fc533a;
315
- margin-bottom: 1rem;
316
- }
317
- .error {
318
- color: #ff917b;
319
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
320
- margin-top: 1rem;
321
- padding: 1rem;
322
- background: #3c140d;
323
- border-radius: 0.5rem;
324
- }
325
- </style>
326
- </head>
327
- <body>
328
- <div class="container">
329
- <h1>Sign-in failed</h1>
330
- <p>An error occurred during authorization.</p>
331
- <div class="error">${escapeHtml(error)}</div>
332
- </div>
333
- </body>
334
- </html>`;
335
- }
336
62
  const oauthServerController = createOAuthServerController({
337
63
  port: OAUTH_PORT,
338
64
  loopbackHost: OAUTH_LOOPBACK_HOST,
@@ -363,297 +89,6 @@ function waitForOAuthCallback(pkce, state, authMode) {
363
89
  function modeForRuntimeMode(runtimeMode) {
364
90
  return runtimeMode === "native" ? "native" : "codex";
365
91
  }
366
- const ACCOUNT_AUTH_TYPE_ORDER = ["native", "codex"];
367
- function normalizeAccountAuthTypes(input) {
368
- const source = Array.isArray(input) ? input : ["native"];
369
- const seen = new Set();
370
- const out = [];
371
- for (const rawType of source) {
372
- const type = rawType === "codex" ? "codex" : rawType === "native" ? "native" : undefined;
373
- if (!type || seen.has(type))
374
- continue;
375
- seen.add(type);
376
- out.push(type);
377
- }
378
- if (out.length === 0)
379
- out.push("native");
380
- out.sort((a, b) => ACCOUNT_AUTH_TYPE_ORDER.indexOf(a) - ACCOUNT_AUTH_TYPE_ORDER.indexOf(b));
381
- return out;
382
- }
383
- function mergeAccountAuthTypes(existing, incoming) {
384
- const merged = [...normalizeAccountAuthTypes(existing), ...normalizeAccountAuthTypes(incoming)];
385
- return normalizeAccountAuthTypes(merged);
386
- }
387
- function removeAccountAuthType(existing, scope) {
388
- return normalizeAccountAuthTypes(existing).filter((type) => type !== scope);
389
- }
390
- export function upsertAccount(openai, incoming) {
391
- const normalizedEmail = normalizeEmail(incoming.email);
392
- const normalizedPlan = normalizePlan(incoming.plan);
393
- const normalizedAccountId = incoming.accountId?.trim();
394
- const strictIdentityKey = buildIdentityKey({
395
- accountId: normalizedAccountId,
396
- email: normalizedEmail,
397
- plan: normalizedPlan
398
- });
399
- const strictMatch = strictIdentityKey
400
- ? openai.accounts.find((existing) => {
401
- const existingAccountId = existing.accountId?.trim();
402
- const existingEmail = normalizeEmail(existing.email);
403
- const existingPlan = normalizePlan(existing.plan);
404
- return (existingAccountId === normalizedAccountId &&
405
- existingEmail === normalizedEmail &&
406
- existingPlan === normalizedPlan);
407
- })
408
- : undefined;
409
- const refreshFallbackMatch = strictMatch || !incoming.refresh
410
- ? undefined
411
- : openai.accounts.find((existing) => existing.refresh === incoming.refresh);
412
- const match = strictMatch ?? refreshFallbackMatch;
413
- const matchedByRefreshFallback = refreshFallbackMatch !== undefined && strictMatch === undefined;
414
- const requiresInsert = matchedByRefreshFallback &&
415
- strictIdentityKey !== undefined &&
416
- match?.identityKey !== undefined &&
417
- match.identityKey !== strictIdentityKey;
418
- const target = !match || requiresInsert ? {} : match;
419
- if (!match || requiresInsert) {
420
- openai.accounts.push(target);
421
- }
422
- if (!matchedByRefreshFallback || requiresInsert) {
423
- if (normalizedAccountId)
424
- target.accountId = normalizedAccountId;
425
- if (normalizedEmail)
426
- target.email = normalizedEmail;
427
- if (normalizedPlan)
428
- target.plan = normalizedPlan;
429
- }
430
- if (incoming.enabled !== undefined)
431
- target.enabled = incoming.enabled;
432
- if (incoming.refresh)
433
- target.refresh = incoming.refresh;
434
- if (incoming.access)
435
- target.access = incoming.access;
436
- if (incoming.expires !== undefined)
437
- target.expires = incoming.expires;
438
- if (incoming.lastUsed !== undefined)
439
- target.lastUsed = incoming.lastUsed;
440
- target.authTypes = normalizeAccountAuthTypes(incoming.authTypes ?? match?.authTypes);
441
- synchronizeIdentityKey(target);
442
- if (!target.identityKey && strictIdentityKey)
443
- target.identityKey = strictIdentityKey;
444
- return target;
445
- }
446
- function rewriteUrl(requestInput) {
447
- const parsed = requestInput instanceof URL
448
- ? requestInput
449
- : new URL(typeof requestInput === "string" ? requestInput : requestInput.url);
450
- if (parsed.pathname.includes("/v1/responses") || parsed.pathname.includes("/chat/completions")) {
451
- return new URL(CODEX_API_ENDPOINT);
452
- }
453
- return parsed;
454
- }
455
- function isAllowedOpenAIOutboundHost(hostname) {
456
- const normalized = hostname.trim().toLowerCase();
457
- if (!normalized)
458
- return false;
459
- if (OPENAI_OUTBOUND_HOST_ALLOWLIST.has(normalized))
460
- return true;
461
- return normalized.endsWith(".openai.com") || normalized.endsWith(".chatgpt.com");
462
- }
463
- function assertAllowedOutboundUrl(url) {
464
- const protocol = url.protocol.trim().toLowerCase();
465
- if (protocol !== "https:") {
466
- throw new PluginFatalError({
467
- message: `Blocked outbound request with unsupported protocol "${protocol || "unknown"}". ` +
468
- "This plugin only proxies HTTPS requests to OpenAI/ChatGPT backends.",
469
- status: 400,
470
- type: "disallowed_outbound_protocol",
471
- param: "request"
472
- });
473
- }
474
- if (isAllowedOpenAIOutboundHost(url.hostname))
475
- return;
476
- throw new PluginFatalError({
477
- message: `Blocked outbound request to "${url.hostname}". ` + "This plugin only proxies OpenAI/ChatGPT backend traffic.",
478
- status: 400,
479
- type: "disallowed_outbound_host",
480
- param: "request"
481
- });
482
- }
483
- async function sessionUsesOpenAIProvider(client, sessionID) {
484
- const rows = await readSessionMessageRows(client, sessionID);
485
- for (let index = rows.length - 1; index >= 0; index -= 1) {
486
- const row = rows[index];
487
- if (!isRecord(row) || !isRecord(row.info))
488
- continue;
489
- const info = row.info;
490
- if (asString(info.role) !== "user")
491
- continue;
492
- const providerID = getMessageProviderID(info);
493
- if (!providerID)
494
- continue;
495
- return providerID === "openai";
496
- }
497
- return false;
498
- }
499
- function getMessageProviderID(info) {
500
- const model = isRecord(info.model) ? info.model : undefined;
501
- return model ? asString(model.providerID) : asString(info.providerID);
502
- }
503
- async function readSessionMessageRows(client, sessionID) {
504
- const sessionApi = client?.session;
505
- if (!sessionApi || typeof sessionApi.messages !== "function")
506
- return [];
507
- try {
508
- const response = await sessionApi.messages({ sessionID, limit: 100 });
509
- return isRecord(response) && Array.isArray(response.data) ? response.data : [];
510
- }
511
- catch {
512
- return [];
513
- }
514
- }
515
- async function readSessionMessageInfo(client, sessionID, messageID) {
516
- const rows = await readSessionMessageRows(client, sessionID);
517
- for (let index = rows.length - 1; index >= 0; index -= 1) {
518
- const row = rows[index];
519
- if (!isRecord(row) || !isRecord(row.info))
520
- continue;
521
- const info = row.info;
522
- if (asString(info.id) !== messageID)
523
- continue;
524
- return info;
525
- }
526
- return undefined;
527
- }
528
- function formatAccountLabel(account, index) {
529
- const email = account?.email?.trim();
530
- const plan = account?.plan?.trim();
531
- const accountId = account?.accountId?.trim();
532
- const idSuffix = accountId ? (accountId.length > 6 ? accountId.slice(-6) : accountId) : null;
533
- if (email && plan)
534
- return `${email} (${plan})`;
535
- if (email)
536
- return email;
537
- if (idSuffix)
538
- return `id:${idSuffix}`;
539
- return `Account ${index + 1}`;
540
- }
541
- function hasActiveCooldown(account, now) {
542
- return (typeof account.cooldownUntil === "number" && Number.isFinite(account.cooldownUntil) && account.cooldownUntil > now);
543
- }
544
- function ensureAccountAuthTypes(account) {
545
- const normalized = normalizeAccountAuthTypes(account.authTypes);
546
- account.authTypes = normalized;
547
- return normalized;
548
- }
549
- function reconcileActiveIdentityKey(openai) {
550
- if (openai.activeIdentityKey &&
551
- openai.accounts.some((account) => account.identityKey === openai.activeIdentityKey && account.enabled !== false)) {
552
- return;
553
- }
554
- const fallback = openai.accounts.find((account) => account.enabled !== false && account.identityKey);
555
- openai.activeIdentityKey = fallback?.identityKey;
556
- }
557
- function findDomainAccountIndex(domain, account) {
558
- if (account.identityKey) {
559
- const byIdentity = domain.accounts.findIndex((entry) => entry.identityKey === account.identityKey);
560
- if (byIdentity >= 0)
561
- return byIdentity;
562
- }
563
- return domain.accounts.findIndex((entry) => {
564
- const sameId = (entry.accountId?.trim() ?? "") === (account.accountId?.trim() ?? "");
565
- const sameEmail = normalizeEmail(entry.email) === normalizeEmail(account.email);
566
- const samePlan = normalizePlan(entry.plan) === normalizePlan(account.plan);
567
- return sameId && sameEmail && samePlan;
568
- });
569
- }
570
- function buildAuthMenuAccounts(input) {
571
- const now = Date.now();
572
- const rows = new Map();
573
- const mergeFromDomain = (authMode, domain) => {
574
- if (!domain)
575
- return;
576
- for (const account of domain.accounts) {
577
- const normalizedTypes = ensureAccountAuthTypes(account);
578
- const identity = account.identityKey ??
579
- buildIdentityKey({
580
- accountId: account.accountId,
581
- email: normalizeEmail(account.email),
582
- plan: normalizePlan(account.plan)
583
- }) ??
584
- `${authMode}:${account.accountId ?? account.email ?? account.plan ?? "unknown"}`;
585
- const existing = rows.get(identity);
586
- const currentStatus = hasActiveCooldown(account, now)
587
- ? "rate-limited"
588
- : typeof account.expires === "number" && Number.isFinite(account.expires) && account.expires <= now
589
- ? "expired"
590
- : "unknown";
591
- if (!existing) {
592
- const isCurrentAccount = authMode === input.activeMode &&
593
- Boolean(domain.activeIdentityKey && account.identityKey === domain.activeIdentityKey);
594
- rows.set(identity, {
595
- identityKey: account.identityKey,
596
- index: rows.size,
597
- accountId: account.accountId,
598
- email: account.email,
599
- plan: account.plan,
600
- authTypes: [authMode],
601
- lastUsed: account.lastUsed,
602
- enabled: account.enabled,
603
- status: isCurrentAccount ? "active" : currentStatus,
604
- isCurrentAccount
605
- });
606
- continue;
607
- }
608
- existing.authTypes = normalizeAccountAuthTypes([...(existing.authTypes ?? []), authMode]);
609
- if (typeof account.lastUsed === "number" && (!existing.lastUsed || account.lastUsed > existing.lastUsed)) {
610
- existing.lastUsed = account.lastUsed;
611
- }
612
- if (existing.enabled === false && account.enabled !== false) {
613
- existing.enabled = true;
614
- }
615
- if (existing.status !== "rate-limited" && currentStatus === "rate-limited") {
616
- existing.status = "rate-limited";
617
- }
618
- else if (existing.status !== "rate-limited" && existing.status !== "expired" && currentStatus === "expired") {
619
- existing.status = "expired";
620
- }
621
- const isCurrentAccount = authMode === input.activeMode &&
622
- Boolean(domain.activeIdentityKey && account.identityKey === domain.activeIdentityKey);
623
- if (isCurrentAccount) {
624
- existing.isCurrentAccount = true;
625
- existing.status = "active";
626
- }
627
- }
628
- };
629
- mergeFromDomain("native", input.native);
630
- mergeFromDomain("codex", input.codex);
631
- return Array.from(rows.values()).map((row, index) => ({ ...row, index }));
632
- }
633
- function hydrateAccountIdentityFromAccessClaims(account) {
634
- const claims = typeof account.access === "string" && account.access.length > 0 ? parseJwtClaims(account.access) : undefined;
635
- if (!account.accountId)
636
- account.accountId = extractAccountIdFromClaims(claims);
637
- if (!account.email)
638
- account.email = extractEmailFromClaims(claims);
639
- if (!account.plan)
640
- account.plan = extractPlanFromClaims(claims);
641
- account.email = normalizeEmail(account.email);
642
- account.plan = normalizePlan(account.plan);
643
- if (account.accountId)
644
- account.accountId = account.accountId.trim();
645
- ensureAccountAuthTypes(account);
646
- synchronizeIdentityKey(account);
647
- }
648
- function isRecord(value) {
649
- return typeof value === "object" && value !== null && !Array.isArray(value);
650
- }
651
- function asString(value) {
652
- if (typeof value !== "string")
653
- return undefined;
654
- const trimmed = value.trim();
655
- return trimmed ? trimmed : undefined;
656
- }
657
92
  export async function CodexAuthPlugin(input, opts = {}) {
658
93
  opts.log?.debug("codex-native init");
659
94
  const codexCompactionSummaryPrefixSessions = new Set();
@@ -698,286 +133,21 @@ export async function CodexAuthPlugin(input, opts = {}) {
698
133
  }
699
134
  };
700
135
  const refreshQuotaSnapshotsForAuthMenu = async () => {
701
- const auth = await loadAuthStorage();
702
- const snapshotPath = defaultSnapshotsPath();
703
- const existingSnapshots = await loadSnapshots(snapshotPath).catch(() => ({}));
704
- const snapshotUpdates = {};
705
- for (const { mode, domain } of listOpenAIOAuthDomains(auth)) {
706
- for (let index = 0; index < domain.accounts.length; index += 1) {
707
- const account = domain.accounts[index];
708
- if (!account || account.enabled === false)
709
- continue;
710
- hydrateAccountIdentityFromAccessClaims(account);
711
- const identityKey = account.identityKey;
712
- const now = Date.now();
713
- if (identityKey) {
714
- const cooldownUntil = quotaFetchCooldownByIdentity.get(identityKey);
715
- if (typeof cooldownUntil === "number" && cooldownUntil > now)
716
- continue;
717
- const existing = existingSnapshots[identityKey];
718
- if (existing &&
719
- typeof existing.updatedAt === "number" &&
720
- Number.isFinite(existing.updatedAt) &&
721
- now - existing.updatedAt < AUTH_MENU_QUOTA_SNAPSHOT_TTL_MS) {
722
- continue;
723
- }
724
- }
725
- let accessToken = typeof account.access === "string" && account.access.length > 0 ? account.access : undefined;
726
- const expired = typeof account.expires === "number" && Number.isFinite(account.expires) && account.expires <= now;
727
- if ((!accessToken || expired) && account.refresh) {
728
- try {
729
- await saveAuthStorage(undefined, async (authFile) => {
730
- const current = ensureOpenAIOAuthDomain(authFile, mode);
731
- const target = current.accounts[index];
732
- if (!target || target.enabled === false || !target.refresh)
733
- return authFile;
734
- const tokens = await refreshAccessToken(target.refresh);
735
- const refreshedAt = Date.now();
736
- const claims = parseJwtClaims(tokens.id_token ?? tokens.access_token);
737
- target.refresh = tokens.refresh_token;
738
- target.access = tokens.access_token;
739
- target.expires = refreshedAt + (tokens.expires_in ?? 3600) * 1000;
740
- target.accountId = extractAccountId(tokens) || target.accountId;
741
- target.email = extractEmailFromClaims(claims) || target.email;
742
- target.plan = extractPlanFromClaims(claims) || target.plan;
743
- target.lastUsed = refreshedAt;
744
- hydrateAccountIdentityFromAccessClaims(target);
745
- account.refresh = target.refresh;
746
- account.access = target.access;
747
- account.expires = target.expires;
748
- account.accountId = target.accountId;
749
- account.email = target.email;
750
- account.plan = target.plan;
751
- account.identityKey = target.identityKey;
752
- accessToken = target.access;
753
- return authFile;
754
- });
755
- }
756
- catch (error) {
757
- if (identityKey) {
758
- quotaFetchCooldownByIdentity.set(identityKey, Date.now() + AUTH_MENU_QUOTA_FAILURE_COOLDOWN_MS);
759
- }
760
- opts.log?.debug("quota check refresh failed", {
761
- index,
762
- mode,
763
- error: error instanceof Error ? error.message : String(error)
764
- });
765
- }
766
- }
767
- if (!accessToken)
768
- continue;
769
- if (!account.identityKey) {
770
- hydrateAccountIdentityFromAccessClaims(account);
771
- }
772
- if (!account.identityKey)
773
- continue;
774
- const snapshot = await fetchQuotaSnapshotFromBackend({
775
- accessToken,
776
- accountId: account.accountId,
777
- now: Date.now(),
778
- modelFamily: "gpt-5.3-codex",
779
- userAgent: resolveRequestUserAgent(spoofMode, resolveCodexOriginator(spoofMode)),
780
- log: opts.log,
781
- timeoutMs: AUTH_MENU_QUOTA_FETCH_TIMEOUT_MS
782
- });
783
- if (!snapshot) {
784
- quotaFetchCooldownByIdentity.set(account.identityKey, Date.now() + AUTH_MENU_QUOTA_FAILURE_COOLDOWN_MS);
785
- continue;
786
- }
787
- quotaFetchCooldownByIdentity.delete(account.identityKey);
788
- snapshotUpdates[account.identityKey] = snapshot;
789
- }
790
- }
791
- if (Object.keys(snapshotUpdates).length === 0)
792
- return;
793
- await saveSnapshots(snapshotPath, (current) => ({
794
- ...current,
795
- ...snapshotUpdates
796
- }));
136
+ await refreshQuotaSnapshotsForAuthMenuBase({
137
+ spoofMode,
138
+ log: opts.log,
139
+ cooldownByIdentity: quotaFetchCooldownByIdentity
140
+ });
797
141
  };
798
142
  const runInteractiveAuthMenu = async (options) => {
799
- while (true) {
800
- const auth = await loadAuthStorage();
801
- const nativeDomain = getOpenAIOAuthDomain(auth, "native");
802
- const codexDomain = getOpenAIOAuthDomain(auth, "codex");
803
- const menuAccounts = buildAuthMenuAccounts({
804
- native: nativeDomain,
805
- codex: codexDomain,
806
- activeMode: authMode
807
- });
808
- const allowTransfer = await shouldOfferLegacyTransfer();
809
- const result = await runAuthMenuOnce({
810
- accounts: menuAccounts,
811
- allowTransfer,
812
- input: process.stdin,
813
- output: process.stdout,
814
- handlers: {
815
- onCheckQuotas: async () => {
816
- await refreshQuotaSnapshotsForAuthMenu();
817
- const report = await toolOutputForStatus(undefined, undefined, {
818
- style: "menu",
819
- useColor: shouldUseColor()
820
- });
821
- process.stdout.write(`\n${report}\n\n`);
822
- },
823
- onConfigureModels: async () => {
824
- process.stdout.write("\nConfigure provider models in opencode.json and runtime flags in codex-config.json.\n\n");
825
- },
826
- onTransfer: async () => {
827
- const transfer = await importLegacyInstallData();
828
- let total = transfer.imported;
829
- let hydrated = 0;
830
- let refreshed = 0;
831
- await saveAuthStorage(undefined, async (authFile) => {
832
- for (const mode of ["native", "codex"]) {
833
- const domain = getOpenAIOAuthDomain(authFile, mode);
834
- if (!domain)
835
- continue;
836
- for (const account of domain.accounts) {
837
- const hadIdentity = Boolean(buildIdentityKey(account));
838
- hydrateAccountIdentityFromAccessClaims(account);
839
- const hasIdentityAfterClaims = Boolean(buildIdentityKey(account));
840
- if (!hadIdentity && hasIdentityAfterClaims)
841
- hydrated += 1;
842
- if (hasIdentityAfterClaims || account.enabled === false || !account.refresh) {
843
- continue;
844
- }
845
- try {
846
- const tokens = await refreshAccessToken(account.refresh);
847
- refreshed += 1;
848
- const now = Date.now();
849
- const claims = parseJwtClaims(tokens.id_token ?? tokens.access_token);
850
- account.refresh = tokens.refresh_token;
851
- account.access = tokens.access_token;
852
- account.expires = now + (tokens.expires_in ?? 3600) * 1000;
853
- account.accountId = extractAccountId(tokens) || account.accountId;
854
- account.email = extractEmailFromClaims(claims) || account.email;
855
- account.plan = extractPlanFromClaims(claims) || account.plan;
856
- account.lastUsed = now;
857
- hydrateAccountIdentityFromAccessClaims(account);
858
- if (!hadIdentity && buildIdentityKey(account))
859
- hydrated += 1;
860
- }
861
- catch {
862
- // best effort per-account hydration
863
- }
864
- }
865
- }
866
- return authFile;
867
- });
868
- process.stdout.write(`\nTransfer complete: imported ${total} account(s). Hydrated ${hydrated} account(s)` +
869
- `${refreshed > 0 ? `, refreshed ${refreshed} token(s)` : ""}.\n\n`);
870
- },
871
- onDeleteAll: async (scope) => {
872
- await saveAuthStorage(undefined, (authFile) => {
873
- const targets = scope === "both" ? ["native", "codex"] : [scope];
874
- for (const targetMode of targets) {
875
- const domain = ensureOpenAIOAuthDomain(authFile, targetMode);
876
- domain.accounts = [];
877
- domain.activeIdentityKey = undefined;
878
- }
879
- return authFile;
880
- });
881
- const deletedLabel = scope === "both"
882
- ? "Deleted all OpenAI accounts."
883
- : `Deleted ${scope === "native" ? "Native" : "Codex"} auth from all accounts.`;
884
- process.stdout.write(`\n${deletedLabel}\n\n`);
885
- },
886
- onToggleAccount: async (account) => {
887
- await saveAuthStorage(undefined, (authFile) => {
888
- const authTypes = account.authTypes && account.authTypes.length > 0 ? [...account.authTypes] : ["native"];
889
- for (const mode of authTypes) {
890
- const domain = getOpenAIOAuthDomain(authFile, mode);
891
- if (!domain)
892
- continue;
893
- const idx = findDomainAccountIndex(domain, account);
894
- if (idx < 0)
895
- continue;
896
- const target = domain.accounts[idx];
897
- if (!target)
898
- continue;
899
- target.enabled = target.enabled === false;
900
- reconcileActiveIdentityKey(domain);
901
- }
902
- return authFile;
903
- });
904
- process.stdout.write("\nUpdated account status.\n\n");
905
- },
906
- onRefreshAccount: async (account) => {
907
- let refreshed = false;
908
- try {
909
- await saveAuthStorage(undefined, async (authFile) => {
910
- const preferred = [
911
- authMode,
912
- ...(account.authTypes ?? []).filter((mode) => mode !== authMode)
913
- ];
914
- for (const mode of preferred) {
915
- const domain = getOpenAIOAuthDomain(authFile, mode);
916
- if (!domain)
917
- continue;
918
- const idx = findDomainAccountIndex(domain, account);
919
- if (idx < 0)
920
- continue;
921
- const target = domain.accounts[idx];
922
- if (!target || target.enabled === false || !target.refresh)
923
- continue;
924
- const tokens = await refreshAccessToken(target.refresh);
925
- const now = Date.now();
926
- const claims = parseJwtClaims(tokens.id_token ?? tokens.access_token);
927
- target.refresh = tokens.refresh_token;
928
- target.access = tokens.access_token;
929
- target.expires = now + (tokens.expires_in ?? 3600) * 1000;
930
- target.accountId = extractAccountId(tokens) || target.accountId;
931
- target.email = extractEmailFromClaims(claims) || target.email;
932
- target.plan = extractPlanFromClaims(claims) || target.plan;
933
- target.lastUsed = now;
934
- ensureAccountAuthTypes(target);
935
- ensureIdentityKey(target);
936
- if (target.identityKey)
937
- domain.activeIdentityKey = target.identityKey;
938
- refreshed = true;
939
- break;
940
- }
941
- return authFile;
942
- });
943
- }
944
- catch {
945
- refreshed = false;
946
- }
947
- process.stdout.write(refreshed
948
- ? "\nAccount refreshed successfully.\n\n"
949
- : "\nAccount refresh failed. Run login to reauthenticate.\n\n");
950
- },
951
- onDeleteAccount: async (account, scope) => {
952
- await saveAuthStorage(undefined, (authFile) => {
953
- const targets = scope === "both" ? ["native", "codex"] : [scope];
954
- for (const mode of targets) {
955
- const domain = getOpenAIOAuthDomain(authFile, mode);
956
- if (!domain)
957
- continue;
958
- const idx = findDomainAccountIndex(domain, account);
959
- if (idx < 0)
960
- continue;
961
- domain.accounts.splice(idx, 1);
962
- reconcileActiveIdentityKey(domain);
963
- }
964
- return authFile;
965
- });
966
- const deletedLabel = scope === "both"
967
- ? "Deleted account."
968
- : `Deleted ${scope === "native" ? "Native" : "Codex"} auth from account.`;
969
- process.stdout.write(`\n${deletedLabel}\n\n`);
970
- }
971
- }
972
- });
973
- if (result === "add")
974
- return "add";
975
- if (result === "exit") {
976
- if (options.allowExit)
977
- return "exit";
978
- continue;
979
- }
980
- }
143
+ return runInteractiveAuthMenuBase({
144
+ authMode,
145
+ allowExit: options.allowExit,
146
+ refreshQuotaSnapshotsForAuthMenu
147
+ });
148
+ };
149
+ const persistOAuthTokens = async (tokens) => {
150
+ await persistOAuthTokensForMode(tokens, authMode);
981
151
  };
982
152
  return {
983
153
  auth: {
@@ -996,676 +166,80 @@ export async function CodexAuthPlugin(input, opts = {}) {
996
166
  }
997
167
  if (!hasOAuth)
998
168
  return {};
999
- const sessionAffinityPath = defaultSessionAffinityPath();
1000
- const loadedSessionAffinity = await loadSessionAffinity(sessionAffinityPath).catch(() => ({
1001
- version: 1
1002
- }));
1003
- const initialSessionAffinity = readSessionAffinitySnapshot(loadedSessionAffinity, authMode);
1004
- const sessionExists = createSessionExistsFn(process.env);
1005
- await pruneSessionAffinitySnapshot(initialSessionAffinity, sessionExists, {
169
+ const { orchestratorState, stickySessionState, hybridSessionState, persistSessionAffinityState } = await createSessionAffinityRuntimeState({
170
+ authMode,
171
+ env: process.env,
1006
172
  missingGraceMs: SESSION_AFFINITY_MISSING_GRACE_MS
1007
- }).catch(() => 0);
1008
- const orchestratorState = createFetchOrchestratorState();
1009
- orchestratorState.seenSessionKeys = initialSessionAffinity.seenSessionKeys;
1010
- const stickySessionState = createStickySessionState();
1011
- stickySessionState.bySessionKey = initialSessionAffinity.stickyBySessionKey;
1012
- const hybridSessionState = createStickySessionState();
1013
- hybridSessionState.bySessionKey = initialSessionAffinity.hybridBySessionKey;
1014
- let sessionAffinityPersistQueue = Promise.resolve();
1015
- const persistSessionAffinityState = () => {
1016
- sessionAffinityPersistQueue = sessionAffinityPersistQueue
1017
- .then(async () => {
1018
- await pruneSessionAffinitySnapshot({
1019
- seenSessionKeys: orchestratorState.seenSessionKeys,
1020
- stickyBySessionKey: stickySessionState.bySessionKey,
1021
- hybridBySessionKey: hybridSessionState.bySessionKey
1022
- }, sessionExists, {
1023
- missingGraceMs: SESSION_AFFINITY_MISSING_GRACE_MS
1024
- });
1025
- await saveSessionAffinity(async (current) => writeSessionAffinitySnapshot(current, authMode, {
1026
- seenSessionKeys: orchestratorState.seenSessionKeys,
1027
- stickyBySessionKey: stickySessionState.bySessionKey,
1028
- hybridBySessionKey: hybridSessionState.bySessionKey
1029
- }), sessionAffinityPath);
1030
- })
1031
- .catch(() => {
1032
- // best-effort persistence
1033
- });
1034
- };
1035
- const catalogAuth = await selectCatalogAuthCandidate(authMode, opts.pidOffsetEnabled === true, opts.rotationStrategy);
1036
- const catalogModels = await getCodexModelCatalog({
1037
- accessToken: catalogAuth.accessToken,
1038
- accountId: catalogAuth.accountId,
1039
- ...resolveCatalogHeaders(),
1040
- onEvent: (event) => opts.log?.debug("codex model catalog", event)
1041
173
  });
1042
- const applyCatalogModels = (models) => {
1043
- if (models) {
174
+ const syncCatalogFromAuth = await initializeCatalogSync({
175
+ authMode,
176
+ pidOffsetEnabled: opts.pidOffsetEnabled === true,
177
+ rotationStrategy: opts.rotationStrategy,
178
+ resolveCatalogHeaders,
179
+ providerModels: provider.models,
180
+ fallbackModels: STATIC_FALLBACK_MODELS,
181
+ personality: opts.personality,
182
+ log: opts.log,
183
+ getLastCatalogModels: () => lastCatalogModels,
184
+ setLastCatalogModels: (models) => {
1044
185
  lastCatalogModels = models;
1045
186
  }
1046
- applyCodexCatalogToProviderModels({
1047
- providerModels: provider.models,
1048
- catalogModels: models ?? lastCatalogModels,
1049
- fallbackModels: STATIC_FALLBACK_MODELS,
1050
- personality: opts.personality
1051
- });
1052
- };
1053
- applyCatalogModels(catalogModels);
1054
- const syncCatalogFromAuth = async (auth) => {
1055
- if (!auth.accessToken)
1056
- return undefined;
1057
- const refreshedCatalog = await getCodexModelCatalog({
1058
- accessToken: auth.accessToken,
1059
- accountId: auth.accountId,
1060
- ...resolveCatalogHeaders(),
1061
- onEvent: (event) => opts.log?.debug("codex model catalog", event)
1062
- });
1063
- applyCatalogModels(refreshedCatalog);
1064
- return refreshedCatalog;
1065
- };
187
+ });
188
+ const fetch = createOpenAIFetchHandler({
189
+ authMode,
190
+ spoofMode,
191
+ remapDeveloperMessagesToUserEnabled,
192
+ behaviorSettings: opts.behaviorSettings,
193
+ personality: opts.personality,
194
+ log: opts.log,
195
+ quietMode: opts.quietMode === true,
196
+ pidOffsetEnabled: opts.pidOffsetEnabled === true,
197
+ configuredRotationStrategy: opts.rotationStrategy,
198
+ headerTransformDebug: opts.headerTransformDebug === true,
199
+ compatInputSanitizerEnabled: opts.compatInputSanitizer === true,
200
+ internalCollaborationModeHeader: INTERNAL_COLLABORATION_MODE_HEADER,
201
+ requestSnapshots,
202
+ sessionAffinityState: {
203
+ orchestratorState,
204
+ stickySessionState,
205
+ hybridSessionState,
206
+ persistSessionAffinityState
207
+ },
208
+ getCatalogModels: () => lastCatalogModels,
209
+ syncCatalogFromAuth,
210
+ setCooldown: async (idKey, cooldownUntil) => {
211
+ await setAccountCooldown(undefined, idKey, cooldownUntil, authMode);
212
+ },
213
+ showToast
214
+ });
1066
215
  return {
1067
216
  apiKey: OAUTH_DUMMY_KEY,
1068
- async fetch(requestInput, init) {
1069
- const baseRequest = new Request(requestInput, init);
1070
- if (opts.headerTransformDebug === true) {
1071
- await requestSnapshots.captureRequest("before-header-transform", baseRequest, {
1072
- spoofMode
1073
- });
1074
- }
1075
- let outbound = new Request(rewriteUrl(baseRequest), baseRequest);
1076
- const inboundOriginator = outbound.headers.get("originator")?.trim();
1077
- const outboundOriginator = inboundOriginator === "opencode" ||
1078
- inboundOriginator === "codex_exec" ||
1079
- inboundOriginator === "codex_cli_rs"
1080
- ? inboundOriginator
1081
- : resolveCodexOriginator(spoofMode);
1082
- outbound.headers.set("originator", outboundOriginator);
1083
- const inboundUserAgent = outbound.headers.get("user-agent")?.trim();
1084
- if (spoofMode === "native" && inboundUserAgent) {
1085
- outbound.headers.set("user-agent", inboundUserAgent);
1086
- }
1087
- else {
1088
- outbound.headers.set("user-agent", resolveRequestUserAgent(spoofMode, outboundOriginator));
1089
- }
1090
- if (outbound.headers.has(INTERNAL_COLLABORATION_MODE_HEADER)) {
1091
- outbound.headers.delete(INTERNAL_COLLABORATION_MODE_HEADER);
1092
- }
1093
- const instructionOverride = await applyCatalogInstructionOverrideToRequest({
1094
- request: outbound,
1095
- enabled: spoofMode === "codex",
1096
- catalogModels: lastCatalogModels,
1097
- customSettings: opts.customSettings,
1098
- fallbackPersonality: opts.personality
1099
- });
1100
- const developerRoleRemap = await remapDeveloperMessagesToUserOnRequest({
1101
- request: instructionOverride.request,
1102
- enabled: remapDeveloperMessagesToUserEnabled
1103
- });
1104
- outbound = developerRoleRemap.request;
1105
- const subagentHeader = outbound.headers.get("x-openai-subagent")?.trim();
1106
- const isSubagentRequest = Boolean(subagentHeader);
1107
- if (opts.headerTransformDebug === true) {
1108
- await requestSnapshots.captureRequest("after-header-transform", outbound, {
1109
- spoofMode,
1110
- instructionsOverridden: instructionOverride.changed,
1111
- instructionOverrideReason: instructionOverride.reason,
1112
- developerMessagesRemapped: developerRoleRemap.changed,
1113
- developerMessageRemapReason: developerRoleRemap.reason,
1114
- developerMessageRemapCount: developerRoleRemap.remappedCount,
1115
- developerMessagePreservedCount: developerRoleRemap.preservedCount,
1116
- ...(isSubagentRequest ? { subagent: subagentHeader } : {})
1117
- });
1118
- }
1119
- let selectedIdentityKey;
1120
- await requestSnapshots.captureRequest("before-auth", outbound, { spoofMode });
1121
- const orchestrator = new FetchOrchestrator({
1122
- acquireAuth: async (context) => {
1123
- let access;
1124
- let accountId;
1125
- let identityKey;
1126
- let accountLabel;
1127
- let email;
1128
- let plan;
1129
- try {
1130
- if (isSubagentRequest && context?.sessionKey) {
1131
- orchestratorState.seenSessionKeys.delete(context.sessionKey);
1132
- stickySessionState.bySessionKey.delete(context.sessionKey);
1133
- hybridSessionState.bySessionKey.delete(context.sessionKey);
1134
- }
1135
- await saveAuthStorage(undefined, async (authFile) => {
1136
- const now = Date.now();
1137
- const openai = authFile.openai;
1138
- if (!openai || openai.type !== "oauth") {
1139
- throw new PluginFatalError({
1140
- message: "Not authenticated with OpenAI. Run `opencode auth login`.",
1141
- status: 401,
1142
- type: "oauth_not_configured",
1143
- param: "auth"
1144
- });
1145
- }
1146
- const domain = ensureOpenAIOAuthDomain(authFile, authMode);
1147
- if (domain.accounts.length === 0) {
1148
- throw new PluginFatalError({
1149
- message: `No OpenAI ${authMode} accounts configured. Run \`opencode auth login\`.`,
1150
- status: 401,
1151
- type: "no_accounts_configured",
1152
- param: "accounts"
1153
- });
1154
- }
1155
- const enabled = domain.accounts.filter((account) => account.enabled !== false);
1156
- if (enabled.length === 0) {
1157
- throw new PluginFatalError({
1158
- message: `No enabled OpenAI ${authMode} accounts available. Enable an account or run \`opencode auth login\`.`,
1159
- status: 403,
1160
- type: "no_enabled_accounts",
1161
- param: "accounts"
1162
- });
1163
- }
1164
- const attempted = new Set();
1165
- let sawInvalidGrant = false;
1166
- let sawRefreshFailure = false;
1167
- let sawMissingRefresh = false;
1168
- const rotationStrategy = opts.rotationStrategy ?? domain.strategy ?? "sticky";
1169
- opts.log?.debug("rotation begin", {
1170
- strategy: rotationStrategy,
1171
- activeIdentityKey: domain.activeIdentityKey,
1172
- totalAccounts: domain.accounts.length,
1173
- enabledAccounts: enabled.length,
1174
- mode: authMode,
1175
- sessionKey: context?.sessionKey ?? null
1176
- });
1177
- while (attempted.size < domain.accounts.length) {
1178
- const sessionState = rotationStrategy === "sticky"
1179
- ? stickySessionState
1180
- : rotationStrategy === "hybrid"
1181
- ? hybridSessionState
1182
- : undefined;
1183
- const selected = selectAccount({
1184
- accounts: domain.accounts,
1185
- strategy: rotationStrategy,
1186
- activeIdentityKey: domain.activeIdentityKey,
1187
- now,
1188
- stickyPidOffset: opts.pidOffsetEnabled === true,
1189
- stickySessionKey: isSubagentRequest ? undefined : context?.sessionKey,
1190
- stickySessionState: sessionState,
1191
- onDebug: (event) => {
1192
- opts.log?.debug("rotation decision", event);
1193
- }
1194
- });
1195
- if (!selected) {
1196
- opts.log?.debug("rotation stop: no selectable account", {
1197
- attempted: attempted.size,
1198
- totalAccounts: domain.accounts.length
1199
- });
1200
- break;
1201
- }
1202
- const selectedIndex = domain.accounts.findIndex((account) => account === selected);
1203
- const attemptKey = selected.identityKey ??
1204
- selected.refresh ??
1205
- (selectedIndex >= 0 ? `idx:${selectedIndex}` : `idx:${attempted.size}`);
1206
- if (attempted.has(attemptKey)) {
1207
- opts.log?.debug("rotation stop: duplicate attempt key", {
1208
- attemptKey,
1209
- selectedIdentityKey: selected.identityKey,
1210
- selectedIndex
1211
- });
1212
- break;
1213
- }
1214
- attempted.add(attemptKey);
1215
- if (!isSubagentRequest && context?.sessionKey && sessionState) {
1216
- persistSessionAffinityState();
1217
- }
1218
- opts.log?.debug("rotation candidate selected", {
1219
- attemptKey,
1220
- selectedIdentityKey: selected.identityKey,
1221
- selectedIndex,
1222
- selectedEnabled: selected.enabled !== false,
1223
- selectedCooldownUntil: selected.cooldownUntil ?? null,
1224
- selectedExpires: selected.expires ?? null
1225
- });
1226
- accountLabel = formatAccountLabel(selected, selectedIndex >= 0 ? selectedIndex : 0);
1227
- email = selected.email;
1228
- plan = selected.plan;
1229
- if (selected.access && selected.expires && selected.expires > now) {
1230
- selected.lastUsed = now;
1231
- access = selected.access;
1232
- accountId = selected.accountId;
1233
- identityKey = selected.identityKey;
1234
- if (selected.identityKey)
1235
- domain.activeIdentityKey = selected.identityKey;
1236
- return authFile;
1237
- }
1238
- if (!selected.refresh) {
1239
- sawMissingRefresh = true;
1240
- selected.cooldownUntil = now + AUTH_REFRESH_FAILURE_COOLDOWN_MS;
1241
- continue;
1242
- }
1243
- let tokens;
1244
- try {
1245
- tokens = await refreshAccessToken(selected.refresh);
1246
- }
1247
- catch (error) {
1248
- if (isOAuthTokenRefreshError(error) && error.oauthCode?.toLowerCase() === "invalid_grant") {
1249
- sawInvalidGrant = true;
1250
- selected.enabled = false;
1251
- delete selected.cooldownUntil;
1252
- delete selected.refreshLeaseUntil;
1253
- continue;
1254
- }
1255
- sawRefreshFailure = true;
1256
- selected.cooldownUntil = now + AUTH_REFRESH_FAILURE_COOLDOWN_MS;
1257
- continue;
1258
- }
1259
- const expires = now + (tokens.expires_in ?? 3600) * 1000;
1260
- const refreshedAccountId = extractAccountId(tokens) || selected.accountId;
1261
- const claims = parseJwtClaims(tokens.id_token ?? tokens.access_token);
1262
- selected.refresh = tokens.refresh_token;
1263
- selected.access = tokens.access_token;
1264
- selected.expires = expires;
1265
- selected.accountId = refreshedAccountId;
1266
- if (claims?.email)
1267
- selected.email = normalizeEmail(claims.email);
1268
- if (claims?.plan)
1269
- selected.plan = normalizePlan(claims.plan);
1270
- ensureIdentityKey(selected);
1271
- selected.lastUsed = now;
1272
- accountLabel = formatAccountLabel(selected, selectedIndex >= 0 ? selectedIndex : 0);
1273
- email = selected.email;
1274
- plan = selected.plan;
1275
- identityKey = selected.identityKey;
1276
- if (selected.identityKey)
1277
- domain.activeIdentityKey = selected.identityKey;
1278
- access = selected.access;
1279
- accountId = selected.accountId;
1280
- return authFile;
1281
- }
1282
- const enabledAfterAttempts = domain.accounts.filter((account) => account.enabled !== false);
1283
- if (enabledAfterAttempts.length === 0 && sawInvalidGrant) {
1284
- throw new PluginFatalError({
1285
- message: "All enabled OpenAI refresh tokens were rejected (invalid_grant). Run `opencode auth login` to reauthenticate.",
1286
- status: 401,
1287
- type: "refresh_invalid_grant",
1288
- param: "auth"
1289
- });
1290
- }
1291
- const nextAvailableAt = enabledAfterAttempts.reduce((current, account) => {
1292
- const cooldownUntil = account.cooldownUntil;
1293
- if (typeof cooldownUntil !== "number" || cooldownUntil <= now)
1294
- return current;
1295
- if (current === undefined || cooldownUntil < current)
1296
- return cooldownUntil;
1297
- return current;
1298
- }, undefined);
1299
- if (nextAvailableAt !== undefined) {
1300
- const waitMs = Math.max(0, nextAvailableAt - now);
1301
- throw new PluginFatalError({
1302
- message: `All enabled OpenAI accounts are cooling down. Try again in ${formatWaitTime(waitMs)} or run \`opencode auth login\`.`,
1303
- status: 429,
1304
- type: "all_accounts_cooling_down",
1305
- param: "accounts"
1306
- });
1307
- }
1308
- if (sawInvalidGrant) {
1309
- throw new PluginFatalError({
1310
- message: "OpenAI refresh token was rejected (invalid_grant). Run `opencode auth login` to reauthenticate this account.",
1311
- status: 401,
1312
- type: "refresh_invalid_grant",
1313
- param: "auth"
1314
- });
1315
- }
1316
- if (sawMissingRefresh) {
1317
- throw new PluginFatalError({
1318
- message: "Selected OpenAI account is missing a refresh token. Run `opencode auth login` to reauthenticate.",
1319
- status: 401,
1320
- type: "missing_refresh_token",
1321
- param: "accounts"
1322
- });
1323
- }
1324
- if (sawRefreshFailure) {
1325
- throw new PluginFatalError({
1326
- message: "Failed to refresh OpenAI access token. Run `opencode auth login` and try again.",
1327
- status: 401,
1328
- type: "refresh_failed",
1329
- param: "auth"
1330
- });
1331
- }
1332
- throw new PluginFatalError({
1333
- message: `No enabled OpenAI ${authMode} accounts available. Enable an account or run \`opencode auth login\`.`,
1334
- status: 403,
1335
- type: "no_enabled_accounts",
1336
- param: "accounts"
1337
- });
1338
- });
1339
- }
1340
- catch (error) {
1341
- if (isPluginFatalError(error))
1342
- throw error;
1343
- throw new PluginFatalError({
1344
- message: "Unable to access OpenAI auth storage. Check plugin configuration and run `opencode auth login` if needed.",
1345
- status: 500,
1346
- type: "auth_storage_error",
1347
- param: "auth"
1348
- });
1349
- }
1350
- if (!access) {
1351
- throw new PluginFatalError({
1352
- message: "No valid OpenAI access token available. Run `opencode auth login`.",
1353
- status: 401,
1354
- type: "no_valid_access_token",
1355
- param: "auth"
1356
- });
1357
- }
1358
- if (spoofMode === "codex") {
1359
- const shouldAwaitCatalog = !lastCatalogModels || lastCatalogModels.length === 0;
1360
- if (shouldAwaitCatalog) {
1361
- try {
1362
- await syncCatalogFromAuth({ accessToken: access, accountId });
1363
- }
1364
- catch {
1365
- // best-effort catalog load; request can still proceed
1366
- }
1367
- }
1368
- else {
1369
- void syncCatalogFromAuth({ accessToken: access, accountId }).catch(() => { });
1370
- }
1371
- }
1372
- else {
1373
- void syncCatalogFromAuth({ accessToken: access, accountId }).catch(() => { });
1374
- }
1375
- selectedIdentityKey = identityKey;
1376
- return { access, accountId, identityKey, accountLabel, email, plan };
1377
- },
1378
- setCooldown: async (idKey, cooldownUntil) => {
1379
- await setAccountCooldown(undefined, idKey, cooldownUntil, authMode);
1380
- },
1381
- quietMode: opts.quietMode === true,
1382
- state: orchestratorState,
1383
- onSessionObserved: ({ event, sessionKey }) => {
1384
- if (isSubagentRequest) {
1385
- orchestratorState.seenSessionKeys.delete(sessionKey);
1386
- stickySessionState.bySessionKey.delete(sessionKey);
1387
- hybridSessionState.bySessionKey.delete(sessionKey);
1388
- return;
1389
- }
1390
- if (event === "new" || event === "resume" || event === "switch") {
1391
- persistSessionAffinityState();
1392
- }
1393
- },
1394
- showToast,
1395
- onAttemptRequest: async ({ attempt, maxAttempts, request, auth, sessionKey }) => {
1396
- const instructionOverride = await applyCatalogInstructionOverrideToRequest({
1397
- request,
1398
- enabled: spoofMode === "codex",
1399
- catalogModels: lastCatalogModels,
1400
- customSettings: opts.customSettings,
1401
- fallbackPersonality: opts.personality
1402
- });
1403
- const developerRoleRemap = await remapDeveloperMessagesToUserOnRequest({
1404
- request: instructionOverride.request,
1405
- enabled: remapDeveloperMessagesToUserEnabled
1406
- });
1407
- await requestSnapshots.captureRequest("outbound-attempt", developerRoleRemap.request, {
1408
- attempt: attempt + 1,
1409
- maxAttempts,
1410
- sessionKey,
1411
- identityKey: auth.identityKey,
1412
- accountLabel: auth.accountLabel,
1413
- instructionsOverridden: instructionOverride.changed,
1414
- instructionOverrideReason: instructionOverride.reason,
1415
- developerMessagesRemapped: developerRoleRemap.changed,
1416
- developerMessageRemapReason: developerRoleRemap.reason,
1417
- developerMessageRemapCount: developerRoleRemap.remappedCount,
1418
- developerMessagePreservedCount: developerRoleRemap.preservedCount
1419
- });
1420
- return developerRoleRemap.request;
1421
- },
1422
- onAttemptResponse: async ({ attempt, maxAttempts, response, auth, sessionKey }) => {
1423
- await requestSnapshots.captureResponse("outbound-response", response, {
1424
- attempt: attempt + 1,
1425
- maxAttempts,
1426
- sessionKey,
1427
- identityKey: auth.identityKey,
1428
- accountLabel: auth.accountLabel
1429
- });
1430
- }
1431
- });
1432
- const sanitizedOutbound = await sanitizeOutboundRequestIfNeeded(outbound, opts.compatInputSanitizer === true);
1433
- if (sanitizedOutbound.changed) {
1434
- opts.log?.debug("compat input sanitizer applied", { mode: spoofMode });
1435
- }
1436
- await requestSnapshots.captureRequest("after-sanitize", sanitizedOutbound.request, {
1437
- spoofMode,
1438
- sanitized: sanitizedOutbound.changed
1439
- });
1440
- try {
1441
- assertAllowedOutboundUrl(new URL(sanitizedOutbound.request.url));
1442
- }
1443
- catch (error) {
1444
- if (isPluginFatalError(error)) {
1445
- return toSyntheticErrorResponse(error);
1446
- }
1447
- return toSyntheticErrorResponse(new PluginFatalError({
1448
- message: "Outbound request validation failed before sending to OpenAI backend.",
1449
- status: 400,
1450
- type: "disallowed_outbound_request",
1451
- param: "request"
1452
- }));
1453
- }
1454
- let response;
1455
- try {
1456
- response = await orchestrator.execute(sanitizedOutbound.request);
1457
- }
1458
- catch (error) {
1459
- if (isPluginFatalError(error)) {
1460
- opts.log?.debug("fatal auth/error response", {
1461
- type: error.type,
1462
- status: error.status
1463
- });
1464
- return toSyntheticErrorResponse(error);
1465
- }
1466
- opts.log?.debug("unexpected fetch failure", {
1467
- error: error instanceof Error ? error.message : String(error)
1468
- });
1469
- return toSyntheticErrorResponse(new PluginFatalError({
1470
- message: "OpenAI request failed unexpectedly. Retry once, and if it persists run `opencode auth login`.",
1471
- status: 502,
1472
- type: "plugin_fetch_failed",
1473
- param: "request"
1474
- }));
1475
- }
1476
- if (selectedIdentityKey) {
1477
- const headers = {};
1478
- response.headers.forEach((value, key) => {
1479
- headers[key.toLowerCase()] = value;
1480
- });
1481
- const status = new CodexStatus();
1482
- const snapshot = status.parseFromHeaders({
1483
- now: Date.now(),
1484
- modelFamily: "codex",
1485
- headers
1486
- });
1487
- if (snapshot.limits.length > 0) {
1488
- void saveSnapshots(defaultSnapshotsPath(), (current) => ({
1489
- ...current,
1490
- [selectedIdentityKey]: snapshot
1491
- })).catch(() => { });
1492
- }
1493
- }
1494
- return response;
1495
- }
217
+ fetch
1496
218
  };
1497
219
  },
1498
220
  methods: [
1499
221
  {
1500
222
  label: "ChatGPT Pro/Plus (browser)",
1501
223
  type: "oauth",
1502
- authorize: async (inputs) => {
1503
- const toOAuthSuccess = (tokens) => ({
1504
- type: "success",
1505
- refresh: tokens.refresh_token,
1506
- access: tokens.access_token,
1507
- expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
1508
- accountId: extractAccountId(tokens)
1509
- });
1510
- const runSingleBrowserOAuthInline = async () => {
1511
- const { redirectUri } = await startOAuthServer();
1512
- const pkce = await generatePKCE();
1513
- const state = generateState();
1514
- const authUrl = buildAuthorizeUrl(redirectUri, pkce, state, spoofMode === "codex" ? "codex_cli_rs" : "opencode");
1515
- const callbackPromise = waitForOAuthCallback(pkce, state, authMode);
1516
- void tryOpenUrlInBrowser(authUrl, opts.log);
1517
- process.stdout.write(`\nGo to: ${authUrl}\n`);
1518
- process.stdout.write("Complete authorization in your browser. This window will close automatically.\n");
1519
- let authFailed = false;
1520
- try {
1521
- const tokens = await callbackPromise;
1522
- await persistOAuthTokens(tokens);
1523
- process.stdout.write("\nAccount added.\n\n");
1524
- return tokens;
1525
- }
1526
- catch (error) {
1527
- authFailed = true;
1528
- const reason = error instanceof Error ? error.message : "Authorization failed";
1529
- process.stdout.write(`\nAuthorization failed: ${reason}\n\n`);
1530
- return null;
1531
- }
1532
- finally {
1533
- scheduleOAuthServerStop(authFailed ? OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS : OAUTH_SERVER_SHUTDOWN_GRACE_MS, authFailed ? "error" : "success");
1534
- }
1535
- };
1536
- const runInteractiveBrowserAuthLoop = async () => {
1537
- let lastAddedTokens;
1538
- while (true) {
1539
- const menuResult = await runInteractiveAuthMenu({ allowExit: true });
1540
- if (menuResult === "exit") {
1541
- if (!lastAddedTokens) {
1542
- return {
1543
- url: "",
1544
- method: "auto",
1545
- instructions: "Login cancelled.",
1546
- callback: async () => ({ type: "failed" })
1547
- };
1548
- }
1549
- const latest = lastAddedTokens;
1550
- return {
1551
- url: "",
1552
- method: "auto",
1553
- instructions: "",
1554
- callback: async () => toOAuthSuccess(latest)
1555
- };
1556
- }
1557
- const tokens = await runSingleBrowserOAuthInline();
1558
- if (tokens) {
1559
- lastAddedTokens = tokens;
1560
- continue;
1561
- }
1562
- return {
1563
- url: "",
1564
- method: "auto",
1565
- instructions: "Authorization failed.",
1566
- callback: async () => ({ type: "failed" })
1567
- };
1568
- }
1569
- };
1570
- if (inputs && process.env.OPENCODE_NO_BROWSER !== "1" && process.stdin.isTTY && process.stdout.isTTY) {
1571
- return runInteractiveBrowserAuthLoop();
1572
- }
1573
- const { redirectUri } = await startOAuthServer();
1574
- const pkce = await generatePKCE();
1575
- const state = generateState();
1576
- const authUrl = buildAuthorizeUrl(redirectUri, pkce, state, spoofMode === "codex" ? "codex_cli_rs" : "opencode");
1577
- const callbackPromise = waitForOAuthCallback(pkce, state, authMode);
1578
- void tryOpenUrlInBrowser(authUrl, opts.log);
1579
- return {
1580
- url: authUrl,
1581
- instructions: "Complete authorization in your browser. If you close the tab early, cancel (Ctrl+C) and retry.",
1582
- method: "auto",
1583
- callback: async () => {
1584
- let authFailed = false;
1585
- try {
1586
- const tokens = await callbackPromise;
1587
- await persistOAuthTokens(tokens);
1588
- return toOAuthSuccess(tokens);
1589
- }
1590
- catch {
1591
- authFailed = true;
1592
- return { type: "failed" };
1593
- }
1594
- finally {
1595
- scheduleOAuthServerStop(authFailed ? OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS : OAUTH_SERVER_SHUTDOWN_GRACE_MS, authFailed ? "error" : "success");
1596
- }
1597
- }
1598
- };
1599
- }
224
+ authorize: createBrowserOAuthAuthorize({
225
+ authMode,
226
+ spoofMode,
227
+ runInteractiveAuthMenu,
228
+ startOAuthServer,
229
+ waitForOAuthCallback,
230
+ scheduleOAuthServerStop,
231
+ persistOAuthTokens,
232
+ openAuthUrl: (url) => {
233
+ void tryOpenUrlInBrowser(url, opts.log);
234
+ },
235
+ shutdownGraceMs: OAUTH_SERVER_SHUTDOWN_GRACE_MS,
236
+ shutdownErrorGraceMs: OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS
237
+ })
1600
238
  },
1601
239
  {
1602
240
  label: "ChatGPT Pro/Plus (headless)",
1603
241
  type: "oauth",
1604
- authorize: async () => {
1605
- const deviceResponse = await fetch(`${ISSUER}/api/accounts/deviceauth/usercode`, {
1606
- method: "POST",
1607
- headers: {
1608
- "Content-Type": "application/json",
1609
- "User-Agent": resolveRequestUserAgent(spoofMode, resolveCodexOriginator(spoofMode))
1610
- },
1611
- body: JSON.stringify({ client_id: CLIENT_ID })
1612
- });
1613
- if (!deviceResponse.ok) {
1614
- throw new Error("Failed to initiate device authorization");
1615
- }
1616
- const deviceData = (await deviceResponse.json());
1617
- const interval = Math.max(parseInt(deviceData.interval) || 5, 1) * 1000;
1618
- return {
1619
- url: `${ISSUER}/codex/device`,
1620
- instructions: `Enter code: ${deviceData.user_code}`,
1621
- method: "auto",
1622
- async callback() {
1623
- while (true) {
1624
- const response = await fetch(`${ISSUER}/api/accounts/deviceauth/token`, {
1625
- method: "POST",
1626
- headers: {
1627
- "Content-Type": "application/json",
1628
- "User-Agent": resolveRequestUserAgent(spoofMode, resolveCodexOriginator(spoofMode))
1629
- },
1630
- body: JSON.stringify({
1631
- device_auth_id: deviceData.device_auth_id,
1632
- user_code: deviceData.user_code
1633
- })
1634
- });
1635
- if (response.ok) {
1636
- const data = (await response.json());
1637
- const tokenResponse = await fetch(`${ISSUER}/oauth/token`, {
1638
- method: "POST",
1639
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1640
- body: new URLSearchParams({
1641
- grant_type: "authorization_code",
1642
- code: data.authorization_code,
1643
- redirect_uri: `${ISSUER}/deviceauth/callback`,
1644
- client_id: CLIENT_ID,
1645
- code_verifier: data.code_verifier
1646
- }).toString()
1647
- });
1648
- if (!tokenResponse.ok) {
1649
- throw new Error(`Token exchange failed: ${tokenResponse.status}`);
1650
- }
1651
- const tokens = (await tokenResponse.json());
1652
- await persistOAuthTokens(tokens);
1653
- return {
1654
- type: "success",
1655
- refresh: tokens.refresh_token,
1656
- access: tokens.access_token,
1657
- expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
1658
- accountId: extractAccountId(tokens)
1659
- };
1660
- }
1661
- if (response.status !== 403 && response.status !== 404) {
1662
- return { type: "failed" };
1663
- }
1664
- await sleep(interval + OAUTH_POLLING_SAFETY_MARGIN_MS);
1665
- }
1666
- }
1667
- };
1668
- }
242
+ authorize: createHeadlessOAuthAuthorize({ spoofMode, persistOAuthTokens })
1669
243
  },
1670
244
  {
1671
245
  label: "Manually enter API Key",
@@ -1674,148 +248,46 @@ export async function CodexAuthPlugin(input, opts = {}) {
1674
248
  ]
1675
249
  },
1676
250
  "chat.message": async (hookInput, output) => {
1677
- const directProviderID = hookInput.model?.providerID;
1678
- const isOpenAI = directProviderID === "openai" ||
1679
- (directProviderID === undefined && (await sessionUsesOpenAIProvider(input.client, hookInput.sessionID)));
1680
- if (!isOpenAI)
1681
- return;
1682
- for (const part of output.parts) {
1683
- const partRecord = part;
1684
- if (asString(partRecord.type) !== "subtask")
1685
- continue;
1686
- if ((asString(partRecord.command) ?? "").trim().toLowerCase() !== "review")
1687
- continue;
1688
- partRecord.agent = "Codex Review";
1689
- }
251
+ await handleChatMessageHook({ hookInput, output, client: input.client });
1690
252
  },
1691
253
  "chat.params": async (hookInput, output) => {
1692
- if (hookInput.model.providerID !== "openai")
1693
- return;
1694
- const modelOptions = isRecord(hookInput.model.options) ? hookInput.model.options : {};
1695
- const modelCandidates = getModelLookupCandidates({
1696
- id: hookInput.model.id,
1697
- api: { id: hookInput.model.api?.id }
1698
- });
1699
- const variantCandidates = getVariantLookupCandidates({
1700
- message: hookInput.message,
1701
- modelCandidates
1702
- });
1703
- const catalogModelFallback = findCatalogModelForCandidates(lastCatalogModels, modelCandidates);
1704
- const effectivePersonality = resolvePersonalityForModel({
1705
- customSettings: opts.customSettings,
1706
- modelCandidates,
1707
- variantCandidates,
1708
- fallback: opts.personality
1709
- });
1710
- const modelThinkingSummariesOverride = getModelThinkingSummariesOverride(opts.customSettings, modelCandidates, variantCandidates);
1711
- if (isRecord(modelOptions.codexCatalogModel)) {
1712
- const rendered = resolveInstructionsForModel(modelOptions.codexCatalogModel, effectivePersonality);
1713
- if (rendered) {
1714
- modelOptions.codexInstructions = rendered;
1715
- }
1716
- else {
1717
- delete modelOptions.codexInstructions;
1718
- }
1719
- }
1720
- else if (catalogModelFallback) {
1721
- modelOptions.codexCatalogModel = catalogModelFallback;
1722
- const rendered = resolveInstructionsForModel(catalogModelFallback, effectivePersonality);
1723
- if (rendered) {
1724
- modelOptions.codexInstructions = rendered;
1725
- }
1726
- else {
1727
- delete modelOptions.codexInstructions;
1728
- }
1729
- const defaults = getRuntimeDefaultsForModel(catalogModelFallback);
1730
- if (defaults) {
1731
- modelOptions.codexRuntimeDefaults = defaults;
1732
- }
1733
- }
1734
- else if (asString(modelOptions.codexInstructions) === undefined) {
1735
- const directModelInstructions = asString(hookInput.model.instructions);
1736
- if (directModelInstructions) {
1737
- modelOptions.codexInstructions = directModelInstructions;
1738
- }
1739
- }
1740
- applyCodexRuntimeDefaultsToParams({
1741
- modelOptions,
1742
- modelToolCallCapable: hookInput.model.capabilities?.toolcall,
1743
- thinkingSummariesOverride: modelThinkingSummariesOverride ?? opts.customSettings?.thinkingSummaries,
1744
- preferCodexInstructions: spoofMode === "codex",
1745
- output
254
+ await handleChatParamsHook({
255
+ hookInput,
256
+ output,
257
+ lastCatalogModels,
258
+ behaviorSettings: opts.behaviorSettings,
259
+ fallbackPersonality: opts.personality,
260
+ spoofMode
1746
261
  });
1747
262
  },
1748
263
  "chat.headers": async (hookInput, output) => {
1749
- if (hookInput.model.providerID !== "openai")
1750
- return;
1751
- const originator = resolveCodexOriginator(spoofMode);
1752
- output.headers.originator = originator;
1753
- output.headers["User-Agent"] = resolveRequestUserAgent(spoofMode, originator);
1754
- if (spoofMode === "native") {
1755
- output.headers.session_id = hookInput.sessionID;
1756
- delete output.headers["OpenAI-Beta"];
1757
- delete output.headers.conversation_id;
1758
- }
1759
- else {
1760
- output.headers.session_id = hookInput.sessionID;
1761
- delete output.headers["OpenAI-Beta"];
1762
- delete output.headers.conversation_id;
1763
- delete output.headers["x-openai-subagent"];
1764
- delete output.headers[INTERNAL_COLLABORATION_MODE_HEADER];
1765
- }
264
+ await handleChatHeadersHook({
265
+ hookInput,
266
+ output,
267
+ spoofMode,
268
+ internalCollaborationModeHeader: INTERNAL_COLLABORATION_MODE_HEADER
269
+ });
1766
270
  },
1767
271
  "experimental.session.compacting": async (hookInput, output) => {
1768
- if (!codexCompactionOverrideEnabled)
1769
- return;
1770
- if (await sessionUsesOpenAIProvider(input.client, hookInput.sessionID)) {
1771
- output.prompt = CODEX_RS_COMPACT_PROMPT;
1772
- codexCompactionSummaryPrefixSessions.add(hookInput.sessionID);
1773
- }
272
+ await handleSessionCompactingHook({
273
+ enabled: codexCompactionOverrideEnabled,
274
+ hookInput,
275
+ output,
276
+ client: input.client,
277
+ summaryPrefixSessions: codexCompactionSummaryPrefixSessions,
278
+ compactPrompt: CODEX_RS_COMPACT_PROMPT
279
+ });
1774
280
  },
1775
281
  "experimental.text.complete": async (hookInput, output) => {
1776
- if (!codexCompactionOverrideEnabled)
1777
- return;
1778
- if (!codexCompactionSummaryPrefixSessions.has(hookInput.sessionID))
1779
- return;
1780
- const info = await readSessionMessageInfo(input.client, hookInput.sessionID, hookInput.messageID);
1781
- codexCompactionSummaryPrefixSessions.delete(hookInput.sessionID);
1782
- if (!info)
1783
- return;
1784
- if (asString(info.role) !== "assistant")
1785
- return;
1786
- if (asString(info.agent) !== "compaction")
1787
- return;
1788
- if (info.summary !== true)
1789
- return;
1790
- if (getMessageProviderID(info) !== "openai")
1791
- return;
1792
- if (output.text.startsWith(CODEX_RS_COMPACT_SUMMARY_PREFIX))
1793
- return;
1794
- output.text = `${CODEX_RS_COMPACT_SUMMARY_PREFIX}\n${output.text.trimStart()}`;
282
+ await handleTextCompleteHook({
283
+ enabled: codexCompactionOverrideEnabled,
284
+ hookInput,
285
+ output,
286
+ client: input.client,
287
+ summaryPrefixSessions: codexCompactionSummaryPrefixSessions,
288
+ compactSummaryPrefix: CODEX_RS_COMPACT_SUMMARY_PREFIX
289
+ });
1795
290
  }
1796
291
  };
1797
- async function persistOAuthTokens(tokens) {
1798
- const now = Date.now();
1799
- const expires = now + (tokens.expires_in ?? 3600) * 1000;
1800
- const claims = parseJwtClaims(tokens.id_token ?? tokens.access_token);
1801
- const account = {
1802
- enabled: true,
1803
- refresh: tokens.refresh_token,
1804
- access: tokens.access_token,
1805
- expires,
1806
- accountId: extractAccountId(tokens),
1807
- email: extractEmailFromClaims(claims),
1808
- plan: extractPlanFromClaims(claims),
1809
- lastUsed: now
1810
- };
1811
- await saveAuthStorage(undefined, async (authFile) => {
1812
- const domain = ensureOpenAIOAuthDomain(authFile, authMode);
1813
- const stored = upsertAccount(domain, { ...account, authTypes: [authMode] });
1814
- if (stored.identityKey) {
1815
- domain.activeIdentityKey = stored.identityKey;
1816
- }
1817
- return authFile;
1818
- });
1819
- }
1820
292
  }
1821
293
  //# sourceMappingURL=codex-native.js.map