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