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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +4 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +45 -6
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/accounts-tools.d.ts.map +1 -1
  6. package/dist/lib/accounts-tools.js +88 -4
  7. package/dist/lib/accounts-tools.js.map +1 -1
  8. package/dist/lib/auth-refresh.d.ts +2 -0
  9. package/dist/lib/auth-refresh.d.ts.map +1 -0
  10. package/dist/lib/auth-refresh.js +2 -0
  11. package/dist/lib/auth-refresh.js.map +1 -0
  12. package/dist/lib/cache-io.d.ts +24 -0
  13. package/dist/lib/cache-io.d.ts.map +1 -0
  14. package/dist/lib/cache-io.js +95 -0
  15. package/dist/lib/cache-io.js.map +1 -0
  16. package/dist/lib/cache-lock.d.ts +7 -0
  17. package/dist/lib/cache-lock.d.ts.map +1 -0
  18. package/dist/lib/cache-lock.js +37 -0
  19. package/dist/lib/cache-lock.js.map +1 -0
  20. package/dist/lib/claims.d.ts.map +1 -1
  21. package/dist/lib/claims.js +4 -1
  22. package/dist/lib/claims.js.map +1 -1
  23. package/dist/lib/codex-cache-layout.d.ts +15 -0
  24. package/dist/lib/codex-cache-layout.d.ts.map +1 -0
  25. package/dist/lib/codex-cache-layout.js +52 -0
  26. package/dist/lib/codex-cache-layout.js.map +1 -0
  27. package/dist/lib/codex-native/accounts.d.ts +21 -0
  28. package/dist/lib/codex-native/accounts.d.ts.map +1 -0
  29. package/dist/lib/codex-native/accounts.js +203 -0
  30. package/dist/lib/codex-native/accounts.js.map +1 -0
  31. package/dist/lib/codex-native/acquire-auth.d.ts +22 -0
  32. package/dist/lib/codex-native/acquire-auth.d.ts.map +1 -0
  33. package/dist/lib/codex-native/acquire-auth.js +389 -0
  34. package/dist/lib/codex-native/acquire-auth.js.map +1 -0
  35. package/dist/lib/codex-native/auth-menu-flow.d.ts +9 -0
  36. package/dist/lib/codex-native/auth-menu-flow.d.ts.map +1 -0
  37. package/dist/lib/codex-native/auth-menu-flow.js +304 -0
  38. package/dist/lib/codex-native/auth-menu-flow.js.map +1 -0
  39. package/dist/lib/codex-native/auth-menu-quotas.d.ts +9 -0
  40. package/dist/lib/codex-native/auth-menu-quotas.d.ts.map +1 -0
  41. package/dist/lib/codex-native/auth-menu-quotas.js +213 -0
  42. package/dist/lib/codex-native/auth-menu-quotas.js.map +1 -0
  43. package/dist/lib/codex-native/catalog-auth.d.ts.map +1 -1
  44. package/dist/lib/codex-native/catalog-auth.js +4 -1
  45. package/dist/lib/codex-native/catalog-auth.js.map +1 -1
  46. package/dist/lib/codex-native/catalog-sync.d.ts +28 -0
  47. package/dist/lib/codex-native/catalog-sync.d.ts.map +1 -0
  48. package/dist/lib/codex-native/catalog-sync.js +36 -0
  49. package/dist/lib/codex-native/catalog-sync.js.map +1 -0
  50. package/dist/lib/codex-native/chat-hooks.d.ts +85 -0
  51. package/dist/lib/codex-native/chat-hooks.d.ts.map +1 -0
  52. package/dist/lib/codex-native/chat-hooks.js +175 -0
  53. package/dist/lib/codex-native/chat-hooks.js.map +1 -0
  54. package/dist/lib/codex-native/client-identity.d.ts.map +1 -1
  55. package/dist/lib/codex-native/client-identity.js +55 -40
  56. package/dist/lib/codex-native/client-identity.js.map +1 -1
  57. package/dist/lib/codex-native/collaboration.d.ts +25 -0
  58. package/dist/lib/codex-native/collaboration.d.ts.map +1 -0
  59. package/dist/lib/codex-native/collaboration.js +306 -0
  60. package/dist/lib/codex-native/collaboration.js.map +1 -0
  61. package/dist/lib/codex-native/oauth-auth-methods.d.ts +45 -0
  62. package/dist/lib/codex-native/oauth-auth-methods.d.ts.map +1 -0
  63. package/dist/lib/codex-native/oauth-auth-methods.js +182 -0
  64. package/dist/lib/codex-native/oauth-auth-methods.js.map +1 -0
  65. package/dist/lib/codex-native/oauth-persistence.d.ts +4 -0
  66. package/dist/lib/codex-native/oauth-persistence.d.ts.map +1 -0
  67. package/dist/lib/codex-native/oauth-persistence.js +28 -0
  68. package/dist/lib/codex-native/oauth-persistence.js.map +1 -0
  69. package/dist/lib/codex-native/oauth-server.d.ts.map +1 -1
  70. package/dist/lib/codex-native/oauth-server.js +63 -5
  71. package/dist/lib/codex-native/oauth-server.js.map +1 -1
  72. package/dist/lib/codex-native/oauth-utils.d.ts +51 -0
  73. package/dist/lib/codex-native/oauth-utils.d.ts.map +1 -0
  74. package/dist/lib/codex-native/oauth-utils.js +265 -0
  75. package/dist/lib/codex-native/oauth-utils.js.map +1 -0
  76. package/dist/lib/codex-native/openai-loader-fetch.d.ts +39 -0
  77. package/dist/lib/codex-native/openai-loader-fetch.d.ts.map +1 -0
  78. package/dist/lib/codex-native/openai-loader-fetch.js +337 -0
  79. package/dist/lib/codex-native/openai-loader-fetch.js.map +1 -0
  80. package/dist/lib/codex-native/rate-limit-snapshots.d.ts +2 -0
  81. package/dist/lib/codex-native/rate-limit-snapshots.d.ts.map +1 -0
  82. package/dist/lib/codex-native/rate-limit-snapshots.js +28 -0
  83. package/dist/lib/codex-native/rate-limit-snapshots.js.map +1 -0
  84. package/dist/lib/codex-native/request-routing.d.ts +3 -0
  85. package/dist/lib/codex-native/request-routing.d.ts.map +1 -0
  86. package/dist/lib/codex-native/request-routing.js +41 -0
  87. package/dist/lib/codex-native/request-routing.js.map +1 -0
  88. package/dist/lib/codex-native/request-transform-pipeline.d.ts +20 -0
  89. package/dist/lib/codex-native/request-transform-pipeline.d.ts.map +1 -0
  90. package/dist/lib/codex-native/request-transform-pipeline.js +25 -0
  91. package/dist/lib/codex-native/request-transform-pipeline.js.map +1 -0
  92. package/dist/lib/codex-native/request-transform.d.ts +28 -4
  93. package/dist/lib/codex-native/request-transform.d.ts.map +1 -1
  94. package/dist/lib/codex-native/request-transform.js +347 -41
  95. package/dist/lib/codex-native/request-transform.js.map +1 -1
  96. package/dist/lib/codex-native/session-affinity-state.d.ts +17 -0
  97. package/dist/lib/codex-native/session-affinity-state.d.ts.map +1 -0
  98. package/dist/lib/codex-native/session-affinity-state.js +56 -0
  99. package/dist/lib/codex-native/session-affinity-state.js.map +1 -0
  100. package/dist/lib/codex-native/session-messages.d.ts +8 -0
  101. package/dist/lib/codex-native/session-messages.d.ts.map +1 -0
  102. package/dist/lib/codex-native/session-messages.js +58 -0
  103. package/dist/lib/codex-native/session-messages.js.map +1 -0
  104. package/dist/lib/codex-native.d.ts +12 -30
  105. package/dist/lib/codex-native.d.ts.map +1 -1
  106. package/dist/lib/codex-native.js +141 -1644
  107. package/dist/lib/codex-native.js.map +1 -1
  108. package/dist/lib/codex-prompts-cache.d.ts +17 -0
  109. package/dist/lib/codex-prompts-cache.d.ts.map +1 -0
  110. package/dist/lib/codex-prompts-cache.js +184 -0
  111. package/dist/lib/codex-prompts-cache.js.map +1 -0
  112. package/dist/lib/codex-status-storage.d.ts.map +1 -1
  113. package/dist/lib/codex-status-storage.js +8 -43
  114. package/dist/lib/codex-status-storage.js.map +1 -1
  115. package/dist/lib/codex-status-tool.js +1 -1
  116. package/dist/lib/codex-status-tool.js.map +1 -1
  117. package/dist/lib/codex-status-ui.d.ts.map +1 -1
  118. package/dist/lib/codex-status-ui.js +14 -8
  119. package/dist/lib/codex-status-ui.js.map +1 -1
  120. package/dist/lib/codex-status.d.ts.map +1 -1
  121. package/dist/lib/codex-status.js +28 -2
  122. package/dist/lib/codex-status.js.map +1 -1
  123. package/dist/lib/compat-sanitizer.d.ts.map +1 -1
  124. package/dist/lib/compat-sanitizer.js +5 -4
  125. package/dist/lib/compat-sanitizer.js.map +1 -1
  126. package/dist/lib/config-dir-gitignore.d.ts +3 -0
  127. package/dist/lib/config-dir-gitignore.d.ts.map +1 -0
  128. package/dist/lib/config-dir-gitignore.js +45 -0
  129. package/dist/lib/config-dir-gitignore.js.map +1 -0
  130. package/dist/lib/config.d.ts +30 -11
  131. package/dist/lib/config.d.ts.map +1 -1
  132. package/dist/lib/config.js +359 -153
  133. package/dist/lib/config.js.map +1 -1
  134. package/dist/lib/fetch-orchestrator.d.ts +19 -0
  135. package/dist/lib/fetch-orchestrator.d.ts.map +1 -1
  136. package/dist/lib/fetch-orchestrator.js +33 -6
  137. package/dist/lib/fetch-orchestrator.js.map +1 -1
  138. package/dist/lib/installer-cli.d.ts.map +1 -1
  139. package/dist/lib/installer-cli.js +14 -1
  140. package/dist/lib/installer-cli.js.map +1 -1
  141. package/dist/lib/logger.d.ts.map +1 -1
  142. package/dist/lib/logger.js +4 -1
  143. package/dist/lib/logger.js.map +1 -1
  144. package/dist/lib/model-catalog.d.ts +1 -0
  145. package/dist/lib/model-catalog.d.ts.map +1 -1
  146. package/dist/lib/model-catalog.js +198 -103
  147. package/dist/lib/model-catalog.js.map +1 -1
  148. package/dist/lib/opencode-install.d.ts.map +1 -1
  149. package/dist/lib/opencode-install.js +30 -6
  150. package/dist/lib/opencode-install.js.map +1 -1
  151. package/dist/lib/orchestrator-agent.d.ts +31 -0
  152. package/dist/lib/orchestrator-agent.d.ts.map +1 -0
  153. package/dist/lib/orchestrator-agent.js +268 -0
  154. package/dist/lib/orchestrator-agent.js.map +1 -0
  155. package/dist/lib/paths.d.ts +1 -1
  156. package/dist/lib/paths.d.ts.map +1 -1
  157. package/dist/lib/paths.js +1 -1
  158. package/dist/lib/paths.js.map +1 -1
  159. package/dist/lib/persona-tool-cli.d.ts.map +1 -1
  160. package/dist/lib/persona-tool-cli.js +4 -1
  161. package/dist/lib/persona-tool-cli.js.map +1 -1
  162. package/dist/lib/personalities.d.ts.map +1 -1
  163. package/dist/lib/personalities.js +9 -2
  164. package/dist/lib/personalities.js.map +1 -1
  165. package/dist/lib/personality-command.d.ts.map +1 -1
  166. package/dist/lib/personality-command.js +4 -1
  167. package/dist/lib/personality-command.js.map +1 -1
  168. package/dist/lib/personality-create.d.ts.map +1 -1
  169. package/dist/lib/personality-create.js +5 -1
  170. package/dist/lib/personality-create.js.map +1 -1
  171. package/dist/lib/personality-skill.d.ts.map +1 -1
  172. package/dist/lib/personality-skill.js +4 -1
  173. package/dist/lib/personality-skill.js.map +1 -1
  174. package/dist/lib/prompt-cache-key.d.ts +7 -0
  175. package/dist/lib/prompt-cache-key.d.ts.map +1 -0
  176. package/dist/lib/prompt-cache-key.js +17 -0
  177. package/dist/lib/prompt-cache-key.js.map +1 -0
  178. package/dist/lib/quarantine.d.ts.map +1 -1
  179. package/dist/lib/quarantine.js +22 -5
  180. package/dist/lib/quarantine.js.map +1 -1
  181. package/dist/lib/quota-threshold-alerts.d.ts +31 -0
  182. package/dist/lib/quota-threshold-alerts.d.ts.map +1 -0
  183. package/dist/lib/quota-threshold-alerts.js +97 -0
  184. package/dist/lib/quota-threshold-alerts.js.map +1 -0
  185. package/dist/lib/refresh-queue.d.ts.map +1 -1
  186. package/dist/lib/refresh-queue.js +9 -2
  187. package/dist/lib/refresh-queue.js.map +1 -1
  188. package/dist/lib/remote-cache-fetch.d.ts +28 -0
  189. package/dist/lib/remote-cache-fetch.d.ts.map +1 -0
  190. package/dist/lib/remote-cache-fetch.js +67 -0
  191. package/dist/lib/remote-cache-fetch.js.map +1 -0
  192. package/dist/lib/request-snapshots.d.ts +3 -0
  193. package/dist/lib/request-snapshots.d.ts.map +1 -1
  194. package/dist/lib/request-snapshots.js +88 -13
  195. package/dist/lib/request-snapshots.js.map +1 -1
  196. package/dist/lib/rotation.d.ts +4 -0
  197. package/dist/lib/rotation.d.ts.map +1 -1
  198. package/dist/lib/rotation.js +57 -10
  199. package/dist/lib/rotation.js.map +1 -1
  200. package/dist/lib/session-affinity.d.ts.map +1 -1
  201. package/dist/lib/session-affinity.js +22 -36
  202. package/dist/lib/session-affinity.js.map +1 -1
  203. package/dist/lib/storage.d.ts +8 -5
  204. package/dist/lib/storage.d.ts.map +1 -1
  205. package/dist/lib/storage.js +50 -39
  206. package/dist/lib/storage.js.map +1 -1
  207. package/dist/lib/ui/tty/select.d.ts.map +1 -1
  208. package/dist/lib/ui/tty/select.js +8 -2
  209. package/dist/lib/ui/tty/select.js.map +1 -1
  210. package/dist/lib/util.d.ts +2 -0
  211. package/dist/lib/util.d.ts.map +1 -0
  212. package/dist/lib/util.js +4 -0
  213. package/dist/lib/util.js.map +1 -0
  214. package/package.json +8 -5
  215. package/schemas/codex-config.schema.json +29 -43
@@ -1,65 +1,25 @@
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 process from "node:process";
2
+ import { loadAuthStorage, setAccountCooldown } from "./storage";
11
3
  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
4
  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
5
  import { resolveCodexOriginator } from "./codex-native/originator";
21
6
  import { tryOpenUrlInBrowser as openUrlInBrowser } from "./codex-native/browser";
22
- import { selectCatalogAuthCandidate } from "./codex-native/catalog-auth";
23
7
  import { buildCodexUserAgent, refreshCodexClientVersionFromGitHub, resolveCodexClientVersion, resolveRequestUserAgent } from "./codex-native/client-identity";
24
8
  import { createOAuthServerController } from "./codex-native/oauth-server";
9
+ 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";
10
+ import { refreshQuotaSnapshotsForAuthMenu as refreshQuotaSnapshotsForAuthMenuBase } from "./codex-native/auth-menu-quotas";
11
+ import { persistOAuthTokensForMode } from "./codex-native/oauth-persistence";
12
+ import { createBrowserOAuthAuthorize, createHeadlessOAuthAuthorize } from "./codex-native/oauth-auth-methods";
13
+ import { runInteractiveAuthMenu as runInteractiveAuthMenuBase } from "./codex-native/auth-menu-flow";
14
+ import { handleChatHeadersHook, handleChatMessageHook, handleChatParamsHook, handleSessionCompactingHook, handleTextCompleteHook } from "./codex-native/chat-hooks";
15
+ import { createSessionAffinityRuntimeState } from "./codex-native/session-affinity-state";
16
+ import { initializeCatalogSync } from "./codex-native/catalog-sync";
17
+ import { createOpenAIFetchHandler } from "./codex-native/openai-loader-fetch";
25
18
  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;
19
+ export { upsertAccount } from "./codex-native/accounts";
20
+ export { extractAccountId, extractAccountIdFromClaims, refreshAccessToken } from "./codex-native/oauth-utils";
62
21
  const INTERNAL_COLLABORATION_MODE_HEADER = "x-opencode-collaboration-mode-kind";
22
+ const INTERNAL_COLLABORATION_AGENT_HEADER = "x-opencode-collaboration-agent-kind";
63
23
  const SESSION_AFFINITY_MISSING_GRACE_MS = 15 * 60 * 1000;
64
24
  const STATIC_FALLBACK_MODELS = [
65
25
  "gpt-5.1-codex-max",
@@ -80,9 +40,6 @@ Include:
80
40
  Be concise, structured, and focused on helping the next LLM seamlessly continue the work.
81
41
  `;
82
42
  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
43
  export async function tryOpenUrlInBrowser(url, log) {
87
44
  return openUrlInBrowser({
88
45
  url,
@@ -90,62 +47,6 @@ export async function tryOpenUrlInBrowser(url, log) {
90
47
  onEvent: (event, meta) => oauthServerController.emitDebug(event, meta ?? {})
91
48
  });
92
49
  }
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
50
  export const __testOnly = {
150
51
  buildAuthorizeUrl,
151
52
  generatePKCE,
@@ -160,179 +61,6 @@ export const __testOnly = {
160
61
  isOAuthDebugEnabled,
161
62
  stopOAuthServer
162
63
  };
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
64
  const oauthServerController = createOAuthServerController({
337
65
  port: OAUTH_PORT,
338
66
  loopbackHost: OAUTH_LOOPBACK_HOST,
@@ -363,297 +91,6 @@ function waitForOAuthCallback(pkce, state, authMode) {
363
91
  function modeForRuntimeMode(runtimeMode) {
364
92
  return runtimeMode === "native" ? "native" : "codex";
365
93
  }
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
94
  export async function CodexAuthPlugin(input, opts = {}) {
658
95
  opts.log?.debug("codex-native init");
659
96
  const codexCompactionSummaryPrefixSessions = new Set();
@@ -664,7 +101,16 @@ export async function CodexAuthPlugin(input, opts = {}) {
664
101
  const authMode = modeForRuntimeMode(runtimeMode);
665
102
  const remapDeveloperMessagesToUserEnabled = spoofMode === "codex" && opts.remapDeveloperMessagesToUser !== false;
666
103
  const codexCompactionOverrideEnabled = opts.codexCompactionOverride !== undefined ? opts.codexCompactionOverride : runtimeMode === "codex";
667
- void refreshCodexClientVersionFromGitHub(opts.log).catch(() => { });
104
+ const collaborationProfileEnabled = typeof opts.collaborationProfileEnabled === "boolean" ? opts.collaborationProfileEnabled : runtimeMode === "codex";
105
+ const orchestratorSubagentsEnabled = typeof opts.orchestratorSubagentsEnabled === "boolean"
106
+ ? opts.orchestratorSubagentsEnabled
107
+ : collaborationProfileEnabled;
108
+ const collaborationToolProfile = opts.collaborationToolProfile === "codex" ? "codex" : "opencode";
109
+ void refreshCodexClientVersionFromGitHub(opts.log).catch((error) => {
110
+ if (error instanceof Error) {
111
+ // best-effort background refresh
112
+ }
113
+ });
668
114
  const resolveCatalogHeaders = () => {
669
115
  const originator = resolveCodexOriginator(spoofMode);
670
116
  const codexClientVersion = resolveCodexClientVersion();
@@ -678,6 +124,7 @@ export async function CodexAuthPlugin(input, opts = {}) {
678
124
  };
679
125
  const requestSnapshots = createRequestSnapshots({
680
126
  enabled: opts.headerSnapshots === true || opts.headerTransformDebug === true,
127
+ captureBodies: opts.headerSnapshotBodies === true,
681
128
  log: opts.log
682
129
  });
683
130
  let lastCatalogModels;
@@ -698,286 +145,21 @@ export async function CodexAuthPlugin(input, opts = {}) {
698
145
  }
699
146
  };
700
147
  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
- }));
148
+ await refreshQuotaSnapshotsForAuthMenuBase({
149
+ spoofMode,
150
+ log: opts.log,
151
+ cooldownByIdentity: quotaFetchCooldownByIdentity
152
+ });
797
153
  };
798
154
  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
- }
155
+ return runInteractiveAuthMenuBase({
156
+ authMode,
157
+ allowExit: options.allowExit,
158
+ refreshQuotaSnapshotsForAuthMenu
159
+ });
160
+ };
161
+ const persistOAuthTokens = async (tokens) => {
162
+ await persistOAuthTokensForMode(tokens, authMode);
981
163
  };
982
164
  return {
983
165
  auth: {
@@ -990,682 +172,93 @@ export async function CodexAuthPlugin(input, opts = {}) {
990
172
  const stored = await loadAuthStorage();
991
173
  hasOAuth = stored.openai?.type === "oauth";
992
174
  }
993
- catch {
175
+ catch (error) {
176
+ if (error instanceof Error) {
177
+ // treat storage read issues as missing oauth
178
+ }
994
179
  hasOAuth = false;
995
180
  }
996
181
  }
997
182
  if (!hasOAuth)
998
183
  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, {
1006
- 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)
184
+ const { orchestratorState, stickySessionState, hybridSessionState, persistSessionAffinityState } = await createSessionAffinityRuntimeState({
185
+ authMode,
186
+ env: process.env,
187
+ missingGraceMs: SESSION_AFFINITY_MISSING_GRACE_MS,
188
+ log: opts.log
1041
189
  });
1042
- const applyCatalogModels = (models) => {
1043
- if (models) {
190
+ const syncCatalogFromAuth = await initializeCatalogSync({
191
+ authMode,
192
+ pidOffsetEnabled: opts.pidOffsetEnabled === true,
193
+ rotationStrategy: opts.rotationStrategy,
194
+ resolveCatalogHeaders,
195
+ providerModels: provider.models,
196
+ fallbackModels: STATIC_FALLBACK_MODELS,
197
+ personality: opts.personality,
198
+ log: opts.log,
199
+ getLastCatalogModels: () => lastCatalogModels,
200
+ setLastCatalogModels: (models) => {
1044
201
  lastCatalogModels = models;
1045
202
  }
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
- };
203
+ });
204
+ const fetch = createOpenAIFetchHandler({
205
+ authMode,
206
+ spoofMode,
207
+ promptCacheKeyStrategy: opts.promptCacheKeyStrategy,
208
+ projectPath: typeof input.worktree === "string" && input.worktree.trim() ? input.worktree : process.cwd(),
209
+ remapDeveloperMessagesToUserEnabled,
210
+ behaviorSettings: opts.behaviorSettings,
211
+ personality: opts.personality,
212
+ log: opts.log,
213
+ quietMode: opts.quietMode === true,
214
+ pidOffsetEnabled: opts.pidOffsetEnabled === true,
215
+ configuredRotationStrategy: opts.rotationStrategy,
216
+ headerTransformDebug: opts.headerTransformDebug === true,
217
+ compatInputSanitizerEnabled: opts.compatInputSanitizer === true,
218
+ internalCollaborationModeHeader: INTERNAL_COLLABORATION_MODE_HEADER,
219
+ internalCollaborationAgentHeader: INTERNAL_COLLABORATION_AGENT_HEADER,
220
+ requestSnapshots,
221
+ sessionAffinityState: {
222
+ orchestratorState,
223
+ stickySessionState,
224
+ hybridSessionState,
225
+ persistSessionAffinityState
226
+ },
227
+ getCatalogModels: () => lastCatalogModels,
228
+ syncCatalogFromAuth,
229
+ setCooldown: async (idKey, cooldownUntil) => {
230
+ await setAccountCooldown(undefined, idKey, cooldownUntil, authMode);
231
+ },
232
+ showToast
233
+ });
1066
234
  return {
1067
235
  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
- }
236
+ fetch
1496
237
  };
1497
238
  },
1498
239
  methods: [
1499
240
  {
1500
241
  label: "ChatGPT Pro/Plus (browser)",
1501
242
  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
- }
243
+ authorize: createBrowserOAuthAuthorize({
244
+ authMode,
245
+ spoofMode,
246
+ runInteractiveAuthMenu,
247
+ startOAuthServer,
248
+ waitForOAuthCallback,
249
+ scheduleOAuthServerStop,
250
+ persistOAuthTokens,
251
+ openAuthUrl: (url) => {
252
+ void tryOpenUrlInBrowser(url, opts.log);
253
+ },
254
+ shutdownGraceMs: OAUTH_SERVER_SHUTDOWN_GRACE_MS,
255
+ shutdownErrorGraceMs: OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS
256
+ })
1600
257
  },
1601
258
  {
1602
259
  label: "ChatGPT Pro/Plus (headless)",
1603
260
  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
- }
261
+ authorize: createHeadlessOAuthAuthorize({ spoofMode, persistOAuthTokens })
1669
262
  },
1670
263
  {
1671
264
  label: "Manually enter API Key",
@@ -1674,148 +267,52 @@ export async function CodexAuthPlugin(input, opts = {}) {
1674
267
  ]
1675
268
  },
1676
269
  "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
- }
270
+ await handleChatMessageHook({ hookInput, output, client: input.client });
1690
271
  },
1691
272
  "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
273
+ await handleChatParamsHook({
274
+ hookInput,
275
+ output,
276
+ lastCatalogModels,
277
+ behaviorSettings: opts.behaviorSettings,
278
+ fallbackPersonality: opts.personality,
279
+ spoofMode,
280
+ collaborationProfileEnabled,
281
+ orchestratorSubagentsEnabled,
282
+ collaborationToolProfile
1746
283
  });
1747
284
  },
1748
285
  "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
- }
286
+ await handleChatHeadersHook({
287
+ hookInput,
288
+ output,
289
+ spoofMode,
290
+ internalCollaborationModeHeader: INTERNAL_COLLABORATION_MODE_HEADER,
291
+ internalCollaborationAgentHeader: INTERNAL_COLLABORATION_AGENT_HEADER,
292
+ collaborationProfileEnabled,
293
+ orchestratorSubagentsEnabled
294
+ });
1766
295
  },
1767
296
  "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
- }
297
+ await handleSessionCompactingHook({
298
+ enabled: codexCompactionOverrideEnabled,
299
+ hookInput,
300
+ output,
301
+ client: input.client,
302
+ summaryPrefixSessions: codexCompactionSummaryPrefixSessions,
303
+ compactPrompt: CODEX_RS_COMPACT_PROMPT
304
+ });
1774
305
  },
1775
306
  "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()}`;
307
+ await handleTextCompleteHook({
308
+ enabled: codexCompactionOverrideEnabled,
309
+ hookInput,
310
+ output,
311
+ client: input.client,
312
+ summaryPrefixSessions: codexCompactionSummaryPrefixSessions,
313
+ compactSummaryPrefix: CODEX_RS_COMPACT_SUMMARY_PREFIX
314
+ });
1795
315
  }
1796
316
  };
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
317
  }
1821
318
  //# sourceMappingURL=codex-native.js.map