@iam-brain/opencode-codex-auth 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/dist/bin/opencode-codex-auth.d.ts +3 -0
- package/dist/bin/opencode-codex-auth.d.ts.map +1 -0
- package/dist/bin/opencode-codex-auth.js +13 -0
- package/dist/bin/opencode-codex-auth.js.map +1 -0
- package/dist/bin/opencode-openai-multi.d.ts +3 -0
- package/dist/bin/opencode-openai-multi.d.ts.map +1 -0
- package/dist/bin/opencode-openai-multi.js +13 -0
- package/dist/bin/opencode-openai-multi.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +137 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/accounts-tools.d.ts +14 -0
- package/dist/lib/accounts-tools.d.ts.map +1 -0
- package/dist/lib/accounts-tools.js +100 -0
- package/dist/lib/accounts-tools.js.map +1 -0
- package/dist/lib/claims.d.ts +20 -0
- package/dist/lib/claims.d.ts.map +1 -0
- package/dist/lib/claims.js +37 -0
- package/dist/lib/claims.js.map +1 -0
- package/dist/lib/codex-native.d.ts +74 -0
- package/dist/lib/codex-native.d.ts.map +1 -0
- package/dist/lib/codex-native.js +2404 -0
- package/dist/lib/codex-native.js.map +1 -0
- package/dist/lib/codex-quota-fetch.d.ts +11 -0
- package/dist/lib/codex-quota-fetch.d.ts.map +1 -0
- package/dist/lib/codex-quota-fetch.js +149 -0
- package/dist/lib/codex-quota-fetch.js.map +1 -0
- package/dist/lib/codex-status-storage.d.ts +5 -0
- package/dist/lib/codex-status-storage.d.ts.map +1 -0
- package/dist/lib/codex-status-storage.js +58 -0
- package/dist/lib/codex-status-storage.js.map +1 -0
- package/dist/lib/codex-status-tool.d.ts +6 -0
- package/dist/lib/codex-status-tool.d.ts.map +1 -0
- package/dist/lib/codex-status-tool.js +37 -0
- package/dist/lib/codex-status-tool.js.map +1 -0
- package/dist/lib/codex-status-ui.d.ts +7 -0
- package/dist/lib/codex-status-ui.d.ts.map +1 -0
- package/dist/lib/codex-status-ui.js +109 -0
- package/dist/lib/codex-status-ui.js.map +1 -0
- package/dist/lib/codex-status.d.ts +14 -0
- package/dist/lib/codex-status.d.ts.map +1 -0
- package/dist/lib/codex-status.js +31 -0
- package/dist/lib/codex-status.js.map +1 -0
- package/dist/lib/compat-sanitizer.d.ts +7 -0
- package/dist/lib/compat-sanitizer.d.ts.map +1 -0
- package/dist/lib/compat-sanitizer.js +101 -0
- package/dist/lib/compat-sanitizer.js.map +1 -0
- package/dist/lib/config.d.ts +94 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +495 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/fatal-errors.d.ts +17 -0
- package/dist/lib/fatal-errors.d.ts.map +1 -0
- package/dist/lib/fatal-errors.js +42 -0
- package/dist/lib/fatal-errors.js.map +1 -0
- package/dist/lib/fetch-orchestrator.d.ts +56 -0
- package/dist/lib/fetch-orchestrator.d.ts.map +1 -0
- package/dist/lib/fetch-orchestrator.js +240 -0
- package/dist/lib/fetch-orchestrator.js.map +1 -0
- package/dist/lib/identity.d.ts +10 -0
- package/dist/lib/identity.d.ts.map +1 -0
- package/dist/lib/identity.js +31 -0
- package/dist/lib/identity.js.map +1 -0
- package/dist/lib/installer-cli.d.ts +7 -0
- package/dist/lib/installer-cli.d.ts.map +1 -0
- package/dist/lib/installer-cli.js +115 -0
- package/dist/lib/installer-cli.js.map +1 -0
- package/dist/lib/logger.d.ts +11 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +25 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/model-catalog.d.ts +66 -0
- package/dist/lib/model-catalog.d.ts.map +1 -0
- package/dist/lib/model-catalog.js +532 -0
- package/dist/lib/model-catalog.js.map +1 -0
- package/dist/lib/oauth-pages.d.ts +2 -0
- package/dist/lib/oauth-pages.d.ts.map +1 -0
- package/dist/lib/oauth-pages.js +199 -0
- package/dist/lib/oauth-pages.js.map +1 -0
- package/dist/lib/opencode-install.d.ts +15 -0
- package/dist/lib/opencode-install.d.ts.map +1 -0
- package/dist/lib/opencode-install.js +53 -0
- package/dist/lib/opencode-install.js.map +1 -0
- package/dist/lib/orchestrator-agents.d.ts +29 -0
- package/dist/lib/orchestrator-agents.d.ts.map +1 -0
- package/dist/lib/orchestrator-agents.js +212 -0
- package/dist/lib/orchestrator-agents.js.map +1 -0
- package/dist/lib/paths.d.ts +7 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +18 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/personalities.d.ts +5 -0
- package/dist/lib/personalities.d.ts.map +1 -0
- package/dist/lib/personalities.js +69 -0
- package/dist/lib/personalities.js.map +1 -0
- package/dist/lib/proactive-refresh.d.ts +11 -0
- package/dist/lib/proactive-refresh.d.ts.map +1 -0
- package/dist/lib/proactive-refresh.js +110 -0
- package/dist/lib/proactive-refresh.js.map +1 -0
- package/dist/lib/quarantine.d.ts +9 -0
- package/dist/lib/quarantine.d.ts.map +1 -0
- package/dist/lib/quarantine.js +39 -0
- package/dist/lib/quarantine.js.map +1 -0
- package/dist/lib/rate-limit.d.ts +9 -0
- package/dist/lib/rate-limit.d.ts.map +1 -0
- package/dist/lib/rate-limit.js +32 -0
- package/dist/lib/rate-limit.js.map +1 -0
- package/dist/lib/refresh-queue.d.ts +30 -0
- package/dist/lib/refresh-queue.d.ts.map +1 -0
- package/dist/lib/refresh-queue.js +67 -0
- package/dist/lib/refresh-queue.js.map +1 -0
- package/dist/lib/request-snapshots.d.ts +14 -0
- package/dist/lib/request-snapshots.d.ts.map +1 -0
- package/dist/lib/request-snapshots.js +148 -0
- package/dist/lib/request-snapshots.js.map +1 -0
- package/dist/lib/rotation.d.ts +29 -0
- package/dist/lib/rotation.d.ts.map +1 -0
- package/dist/lib/rotation.js +252 -0
- package/dist/lib/rotation.js.map +1 -0
- package/dist/lib/storage.d.ts +27 -0
- package/dist/lib/storage.d.ts.map +1 -0
- package/dist/lib/storage.js +610 -0
- package/dist/lib/storage.js.map +1 -0
- package/dist/lib/toast.d.ts +2 -0
- package/dist/lib/toast.d.ts.map +1 -0
- package/dist/lib/toast.js +47 -0
- package/dist/lib/toast.js.map +1 -0
- package/dist/lib/tools-output.d.ts +6 -0
- package/dist/lib/tools-output.d.ts.map +1 -0
- package/dist/lib/tools-output.js +6 -0
- package/dist/lib/tools-output.js.map +1 -0
- package/dist/lib/types.d.ts +62 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/ui/auth-menu-runner.d.ts +19 -0
- package/dist/lib/ui/auth-menu-runner.d.ts.map +1 -0
- package/dist/lib/ui/auth-menu-runner.js +79 -0
- package/dist/lib/ui/auth-menu-runner.js.map +1 -0
- package/dist/lib/ui/auth-menu.d.ts +80 -0
- package/dist/lib/ui/auth-menu.d.ts.map +1 -0
- package/dist/lib/ui/auth-menu.js +321 -0
- package/dist/lib/ui/auth-menu.js.map +1 -0
- package/dist/lib/ui/tty/ansi.d.ts +18 -0
- package/dist/lib/ui/tty/ansi.d.ts.map +1 -0
- package/dist/lib/ui/tty/ansi.js +37 -0
- package/dist/lib/ui/tty/ansi.js.map +1 -0
- package/dist/lib/ui/tty/confirm.d.ts +7 -0
- package/dist/lib/ui/tty/confirm.d.ts.map +1 -0
- package/dist/lib/ui/tty/confirm.js +20 -0
- package/dist/lib/ui/tty/confirm.js.map +1 -0
- package/dist/lib/ui/tty/select.d.ts +17 -0
- package/dist/lib/ui/tty/select.d.ts.map +1 -0
- package/dist/lib/ui/tty/select.js +223 -0
- package/dist/lib/ui/tty/select.js.map +1 -0
- package/package.json +68 -0
- package/schemas/codex-config.schema.json +146 -0
- package/schemas/opencode.schema.json +23 -0
|
@@ -0,0 +1,2404 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
4
|
+
import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { extractAccountIdFromClaims as extractAccountIdFromClaimsBase, extractEmailFromClaims, extractPlanFromClaims, parseJwtClaims } from "./claims";
|
|
8
|
+
import { CodexStatus } from "./codex-status";
|
|
9
|
+
import { saveSnapshots } from "./codex-status-storage";
|
|
10
|
+
import { PluginFatalError, formatWaitTime, isPluginFatalError, toSyntheticErrorResponse } from "./fatal-errors";
|
|
11
|
+
import { buildIdentityKey, ensureIdentityKey, normalizeEmail, normalizePlan } from "./identity";
|
|
12
|
+
import { defaultSnapshotsPath } from "./paths";
|
|
13
|
+
import { createStickySessionState, selectAccount } from "./rotation";
|
|
14
|
+
import { ensureOpenAIOAuthDomain, getOpenAIOAuthDomain, importLegacyInstallData, listOpenAIOAuthDomains, loadAuthStorage, saveAuthStorage, setAccountCooldown, shouldOfferLegacyTransfer } from "./storage";
|
|
15
|
+
import { toolOutputForStatus } from "./codex-status-tool";
|
|
16
|
+
import { FetchOrchestrator, createFetchOrchestratorState } from "./fetch-orchestrator";
|
|
17
|
+
import { formatToastMessage } from "./toast";
|
|
18
|
+
import { runAuthMenuOnce } from "./ui/auth-menu-runner";
|
|
19
|
+
import { applyCodexCatalogToProviderModels, getCodexModelCatalog, resolveInstructionsForModel } from "./model-catalog";
|
|
20
|
+
import { CODEX_RS_COMPACT_PROMPT } from "./orchestrator-agents";
|
|
21
|
+
import { sanitizeRequestPayloadForCompat } from "./compat-sanitizer";
|
|
22
|
+
import { fetchQuotaSnapshotFromBackend } from "./codex-quota-fetch";
|
|
23
|
+
import { createRequestSnapshots } from "./request-snapshots";
|
|
24
|
+
import { CODEX_OAUTH_SUCCESS_HTML } from "./oauth-pages";
|
|
25
|
+
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
26
|
+
const ISSUER = "https://auth.openai.com";
|
|
27
|
+
const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
|
|
28
|
+
const OAUTH_PORT = 1455;
|
|
29
|
+
const OAUTH_DUMMY_KEY = "oauth_dummy_key";
|
|
30
|
+
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000;
|
|
31
|
+
const AUTH_REFRESH_FAILURE_COOLDOWN_MS = 30_000;
|
|
32
|
+
const OAUTH_CALLBACK_TIMEOUT_MS = (() => {
|
|
33
|
+
const raw = process.env.CODEX_OAUTH_CALLBACK_TIMEOUT_MS;
|
|
34
|
+
if (!raw)
|
|
35
|
+
return 10 * 60 * 1000;
|
|
36
|
+
const parsed = Number(raw);
|
|
37
|
+
return Number.isFinite(parsed) && parsed >= 60_000 ? parsed : 10 * 60 * 1000;
|
|
38
|
+
})();
|
|
39
|
+
const OAUTH_SERVER_SHUTDOWN_GRACE_MS = (() => {
|
|
40
|
+
const raw = process.env.CODEX_OAUTH_SERVER_SHUTDOWN_GRACE_MS;
|
|
41
|
+
if (!raw)
|
|
42
|
+
return 2000;
|
|
43
|
+
const parsed = Number(raw);
|
|
44
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 2000;
|
|
45
|
+
})();
|
|
46
|
+
const OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS = (() => {
|
|
47
|
+
const raw = process.env.CODEX_OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS;
|
|
48
|
+
if (!raw)
|
|
49
|
+
return 60_000;
|
|
50
|
+
const parsed = Number(raw);
|
|
51
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 60_000;
|
|
52
|
+
})();
|
|
53
|
+
const OAUTH_DEBUG_LOG_DIR = path.join(os.homedir(), ".config", "opencode", "logs", "codex-plugin");
|
|
54
|
+
const OAUTH_DEBUG_LOG_FILE = path.join(OAUTH_DEBUG_LOG_DIR, "oauth-lifecycle.log");
|
|
55
|
+
const execFileAsync = promisify(execFile);
|
|
56
|
+
const DEFAULT_PLUGIN_VERSION = "0.1.0";
|
|
57
|
+
const INTERNAL_COLLABORATION_MODE_HEADER = "x-opencode-collaboration-mode-kind";
|
|
58
|
+
const CODEX_PLAN_MODE_INSTRUCTIONS = [
|
|
59
|
+
"# Plan Mode",
|
|
60
|
+
"",
|
|
61
|
+
"You are in Plan Mode. Focus on producing a decision-complete implementation plan before making code changes.",
|
|
62
|
+
"Use non-mutating exploration first, ask focused questions only when needed, and make assumptions explicit.",
|
|
63
|
+
"If asked to execute while still in plan mode, continue planning until the active agent switches out of plan mode."
|
|
64
|
+
].join("\n");
|
|
65
|
+
const CODEX_CODE_MODE_INSTRUCTIONS = "you are now in code mode.";
|
|
66
|
+
const CODEX_EXECUTE_MODE_INSTRUCTIONS = [
|
|
67
|
+
"# Collaboration Style: Execute",
|
|
68
|
+
"You execute on a well-specified task independently and report progress.",
|
|
69
|
+
"",
|
|
70
|
+
"You do not collaborate on decisions in this mode. You execute end-to-end.",
|
|
71
|
+
"You make reasonable assumptions when the user hasn't specified something, and you proceed without asking questions."
|
|
72
|
+
].join("\n");
|
|
73
|
+
const CODEX_PAIR_PROGRAMMING_MODE_INSTRUCTIONS = [
|
|
74
|
+
"# Collaboration Style: Pair Programming",
|
|
75
|
+
"",
|
|
76
|
+
"## Build together as you go",
|
|
77
|
+
"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."
|
|
78
|
+
].join("\n");
|
|
79
|
+
const STATIC_FALLBACK_MODELS = [
|
|
80
|
+
"gpt-5.1-codex-max",
|
|
81
|
+
"gpt-5.1-codex-mini",
|
|
82
|
+
"gpt-5.2",
|
|
83
|
+
"gpt-5.2-codex",
|
|
84
|
+
"gpt-5.3-codex",
|
|
85
|
+
"gpt-5.1-codex"
|
|
86
|
+
];
|
|
87
|
+
function sleep(ms) {
|
|
88
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
89
|
+
}
|
|
90
|
+
export function browserOpenInvocationFor(url, platform = process.platform) {
|
|
91
|
+
if (platform === "darwin") {
|
|
92
|
+
return { command: "open", args: [url] };
|
|
93
|
+
}
|
|
94
|
+
if (platform === "win32") {
|
|
95
|
+
return { command: "cmd", args: ["/c", "start", "", url] };
|
|
96
|
+
}
|
|
97
|
+
return { command: "xdg-open", args: [url] };
|
|
98
|
+
}
|
|
99
|
+
export async function tryOpenUrlInBrowser(url, log) {
|
|
100
|
+
if (process.env.OPENCODE_NO_BROWSER === "1")
|
|
101
|
+
return false;
|
|
102
|
+
if (process.env.VITEST || process.env.NODE_ENV === "test")
|
|
103
|
+
return false;
|
|
104
|
+
const invocation = browserOpenInvocationFor(url);
|
|
105
|
+
emitOAuthDebug("browser_open_attempt", { command: invocation.command });
|
|
106
|
+
try {
|
|
107
|
+
await execFileAsync(invocation.command, invocation.args, { windowsHide: true, timeout: 5000 });
|
|
108
|
+
emitOAuthDebug("browser_open_success", { command: invocation.command });
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
emitOAuthDebug("browser_open_failure", {
|
|
113
|
+
command: invocation.command,
|
|
114
|
+
error: error instanceof Error ? error.message : String(error)
|
|
115
|
+
});
|
|
116
|
+
log?.warn("failed to auto-open oauth URL", {
|
|
117
|
+
command: invocation.command,
|
|
118
|
+
error: error instanceof Error ? error.message : String(error)
|
|
119
|
+
});
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function escapeHtml(value) {
|
|
124
|
+
return value
|
|
125
|
+
.replace(/&/g, "&")
|
|
126
|
+
.replace(/</g, "<")
|
|
127
|
+
.replace(/>/g, ">")
|
|
128
|
+
.replace(/"/g, """)
|
|
129
|
+
.replace(/'/g, "'");
|
|
130
|
+
}
|
|
131
|
+
async function generatePKCE() {
|
|
132
|
+
const verifier = base64UrlEncode(crypto.getRandomValues(new Uint8Array(64)));
|
|
133
|
+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
134
|
+
const challenge = base64UrlEncode(new Uint8Array(hash));
|
|
135
|
+
return { verifier, challenge };
|
|
136
|
+
}
|
|
137
|
+
function base64UrlEncode(bytes) {
|
|
138
|
+
return Buffer.from(bytes)
|
|
139
|
+
.toString("base64")
|
|
140
|
+
.replace(/\+/g, "-")
|
|
141
|
+
.replace(/\//g, "_")
|
|
142
|
+
.replace(/=+$/g, "");
|
|
143
|
+
}
|
|
144
|
+
function generateState() {
|
|
145
|
+
return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
|
|
146
|
+
}
|
|
147
|
+
export function extractAccountIdFromClaims(claims) {
|
|
148
|
+
return extractAccountIdFromClaimsBase(claims);
|
|
149
|
+
}
|
|
150
|
+
export function extractAccountId(tokens) {
|
|
151
|
+
if (!tokens)
|
|
152
|
+
return undefined;
|
|
153
|
+
if (tokens.id_token) {
|
|
154
|
+
const accountId = extractAccountIdFromClaims(parseJwtClaims(tokens.id_token));
|
|
155
|
+
if (accountId)
|
|
156
|
+
return accountId;
|
|
157
|
+
}
|
|
158
|
+
if (tokens.access_token) {
|
|
159
|
+
return extractAccountIdFromClaims(parseJwtClaims(tokens.access_token));
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
function isOAuthTokenRefreshError(value) {
|
|
164
|
+
return value instanceof Error && ("status" in value || "oauthCode" in value);
|
|
165
|
+
}
|
|
166
|
+
function buildAuthorizeUrl(redirectUri, pkce, state, originator) {
|
|
167
|
+
const query = [
|
|
168
|
+
["response_type", "code"],
|
|
169
|
+
["client_id", CLIENT_ID],
|
|
170
|
+
["redirect_uri", redirectUri],
|
|
171
|
+
["scope", "openid profile email offline_access"],
|
|
172
|
+
["code_challenge", pkce.challenge],
|
|
173
|
+
["code_challenge_method", "S256"],
|
|
174
|
+
["id_token_add_organizations", "true"],
|
|
175
|
+
["codex_cli_simplified_flow", "true"],
|
|
176
|
+
["state", state],
|
|
177
|
+
["originator", originator]
|
|
178
|
+
]
|
|
179
|
+
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
|
180
|
+
.join("&");
|
|
181
|
+
return `${ISSUER}/oauth/authorize?${query}`;
|
|
182
|
+
}
|
|
183
|
+
export const __testOnly = {
|
|
184
|
+
buildAuthorizeUrl,
|
|
185
|
+
generatePKCE,
|
|
186
|
+
buildOAuthSuccessHtml,
|
|
187
|
+
buildOAuthErrorHtml,
|
|
188
|
+
composeCodexSuccessRedirectUrl,
|
|
189
|
+
modeForRuntimeMode,
|
|
190
|
+
buildCodexUserAgent,
|
|
191
|
+
resolveRequestUserAgent,
|
|
192
|
+
resolveHookAgentName,
|
|
193
|
+
resolveCollaborationModeKind,
|
|
194
|
+
resolveSubagentHeaderValue,
|
|
195
|
+
stopOAuthServer
|
|
196
|
+
};
|
|
197
|
+
async function exchangeCodeForTokens(code, redirectUri, pkce) {
|
|
198
|
+
const response = await fetch(`${ISSUER}/oauth/token`, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
201
|
+
body: new URLSearchParams({
|
|
202
|
+
grant_type: "authorization_code",
|
|
203
|
+
code,
|
|
204
|
+
redirect_uri: redirectUri,
|
|
205
|
+
client_id: CLIENT_ID,
|
|
206
|
+
code_verifier: pkce.verifier
|
|
207
|
+
}).toString()
|
|
208
|
+
});
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
throw new Error(`Token exchange failed: ${response.status}`);
|
|
211
|
+
}
|
|
212
|
+
return (await response.json());
|
|
213
|
+
}
|
|
214
|
+
export async function refreshAccessToken(refreshToken) {
|
|
215
|
+
const response = await fetch(`${ISSUER}/oauth/token`, {
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
218
|
+
body: new URLSearchParams({
|
|
219
|
+
grant_type: "refresh_token",
|
|
220
|
+
refresh_token: refreshToken,
|
|
221
|
+
client_id: CLIENT_ID
|
|
222
|
+
}).toString()
|
|
223
|
+
});
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
let oauthCode;
|
|
226
|
+
let oauthDescription;
|
|
227
|
+
try {
|
|
228
|
+
const raw = await response.text();
|
|
229
|
+
if (raw) {
|
|
230
|
+
const payload = JSON.parse(raw);
|
|
231
|
+
if (typeof payload.error === "string")
|
|
232
|
+
oauthCode = payload.error;
|
|
233
|
+
if (typeof payload.error_description === "string") {
|
|
234
|
+
oauthDescription = payload.error_description;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// Best effort parse only.
|
|
240
|
+
}
|
|
241
|
+
const detail = oauthCode
|
|
242
|
+
? `${oauthCode}${oauthDescription ? `: ${oauthDescription}` : ""}`
|
|
243
|
+
: `status ${response.status}`;
|
|
244
|
+
const error = new Error(`Token refresh failed (${detail})`);
|
|
245
|
+
error.status = response.status;
|
|
246
|
+
error.oauthCode = oauthCode;
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
return (await response.json());
|
|
250
|
+
}
|
|
251
|
+
function getOpenAIAuthClaims(token) {
|
|
252
|
+
if (!token)
|
|
253
|
+
return {};
|
|
254
|
+
const claims = parseJwtClaims(token);
|
|
255
|
+
const authClaims = claims?.["https://api.openai.com/auth"];
|
|
256
|
+
if (!authClaims || typeof authClaims !== "object" || Array.isArray(authClaims)) {
|
|
257
|
+
return {};
|
|
258
|
+
}
|
|
259
|
+
return authClaims;
|
|
260
|
+
}
|
|
261
|
+
function getClaimString(claims, key) {
|
|
262
|
+
const value = claims[key];
|
|
263
|
+
return typeof value === "string" ? value : "";
|
|
264
|
+
}
|
|
265
|
+
function getClaimBoolean(claims, key) {
|
|
266
|
+
const value = claims[key];
|
|
267
|
+
return typeof value === "boolean" ? value : false;
|
|
268
|
+
}
|
|
269
|
+
function composeCodexSuccessRedirectUrl(tokens, options = {}) {
|
|
270
|
+
const issuer = options.issuer ?? ISSUER;
|
|
271
|
+
const port = options.port ?? OAUTH_PORT;
|
|
272
|
+
const idClaims = getOpenAIAuthClaims(tokens.id_token);
|
|
273
|
+
const accessClaims = getOpenAIAuthClaims(tokens.access_token);
|
|
274
|
+
const needsSetup = !getClaimBoolean(idClaims, "completed_platform_onboarding") &&
|
|
275
|
+
getClaimBoolean(idClaims, "is_org_owner");
|
|
276
|
+
const platformUrl = issuer === ISSUER ? "https://platform.openai.com" : "https://platform.api.openai.org";
|
|
277
|
+
const params = new URLSearchParams({
|
|
278
|
+
id_token: tokens.id_token ?? "",
|
|
279
|
+
needs_setup: String(needsSetup),
|
|
280
|
+
org_id: getClaimString(idClaims, "organization_id"),
|
|
281
|
+
project_id: getClaimString(idClaims, "project_id"),
|
|
282
|
+
plan_type: getClaimString(accessClaims, "chatgpt_plan_type"),
|
|
283
|
+
platform_url: platformUrl
|
|
284
|
+
});
|
|
285
|
+
return `http://localhost:${port}/success?${params.toString()}`;
|
|
286
|
+
}
|
|
287
|
+
function buildOAuthSuccessHtml(mode = "codex") {
|
|
288
|
+
if (mode === "codex")
|
|
289
|
+
return CODEX_OAUTH_SUCCESS_HTML;
|
|
290
|
+
return `<!doctype html>
|
|
291
|
+
<html>
|
|
292
|
+
<head>
|
|
293
|
+
<title>OpenCode - Codex Authorization Successful</title>
|
|
294
|
+
<style>
|
|
295
|
+
body {
|
|
296
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
297
|
+
display: flex;
|
|
298
|
+
justify-content: center;
|
|
299
|
+
align-items: center;
|
|
300
|
+
height: 100vh;
|
|
301
|
+
margin: 0;
|
|
302
|
+
background: #131010;
|
|
303
|
+
color: #f1ecec;
|
|
304
|
+
}
|
|
305
|
+
.container {
|
|
306
|
+
text-align: center;
|
|
307
|
+
padding: 2rem;
|
|
308
|
+
}
|
|
309
|
+
h1 {
|
|
310
|
+
color: #f1ecec;
|
|
311
|
+
margin-bottom: 1rem;
|
|
312
|
+
}
|
|
313
|
+
p {
|
|
314
|
+
color: #b7b1b1;
|
|
315
|
+
}
|
|
316
|
+
</style>
|
|
317
|
+
</head>
|
|
318
|
+
<body>
|
|
319
|
+
<div class="container">
|
|
320
|
+
<h1>Authorization Successful</h1>
|
|
321
|
+
<p>You can close this window and return to OpenCode.</p>
|
|
322
|
+
</div>
|
|
323
|
+
<script>
|
|
324
|
+
setTimeout(() => window.close(), 2000)
|
|
325
|
+
</script>
|
|
326
|
+
</body>
|
|
327
|
+
</html>`;
|
|
328
|
+
}
|
|
329
|
+
function buildOAuthErrorHtml(error) {
|
|
330
|
+
return `<!doctype html>
|
|
331
|
+
<html>
|
|
332
|
+
<head>
|
|
333
|
+
<title>Sign into Codex</title>
|
|
334
|
+
<style>
|
|
335
|
+
body {
|
|
336
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
337
|
+
display: flex;
|
|
338
|
+
justify-content: center;
|
|
339
|
+
align-items: center;
|
|
340
|
+
height: 100vh;
|
|
341
|
+
margin: 0;
|
|
342
|
+
background: #131010;
|
|
343
|
+
color: #f1ecec;
|
|
344
|
+
}
|
|
345
|
+
.container {
|
|
346
|
+
text-align: center;
|
|
347
|
+
padding: 2rem;
|
|
348
|
+
}
|
|
349
|
+
h1 {
|
|
350
|
+
color: #fc533a;
|
|
351
|
+
margin-bottom: 1rem;
|
|
352
|
+
}
|
|
353
|
+
.error {
|
|
354
|
+
color: #ff917b;
|
|
355
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
356
|
+
margin-top: 1rem;
|
|
357
|
+
padding: 1rem;
|
|
358
|
+
background: #3c140d;
|
|
359
|
+
border-radius: 0.5rem;
|
|
360
|
+
}
|
|
361
|
+
</style>
|
|
362
|
+
</head>
|
|
363
|
+
<body>
|
|
364
|
+
<div class="container">
|
|
365
|
+
<h1>Sign-in failed</h1>
|
|
366
|
+
<p>An error occurred during authorization.</p>
|
|
367
|
+
<div class="error">${escapeHtml(error)}</div>
|
|
368
|
+
</div>
|
|
369
|
+
</body>
|
|
370
|
+
</html>`;
|
|
371
|
+
}
|
|
372
|
+
let oauthServer;
|
|
373
|
+
let pendingOAuth;
|
|
374
|
+
let oauthServerCloseTimer;
|
|
375
|
+
function isOAuthDebugEnabled() {
|
|
376
|
+
return process.env.CODEX_AUTH_DEBUG === "1";
|
|
377
|
+
}
|
|
378
|
+
function emitOAuthDebug(event, meta = {}) {
|
|
379
|
+
if (!isOAuthDebugEnabled())
|
|
380
|
+
return;
|
|
381
|
+
const payload = {
|
|
382
|
+
ts: new Date().toISOString(),
|
|
383
|
+
pid: process.pid,
|
|
384
|
+
event,
|
|
385
|
+
...meta
|
|
386
|
+
};
|
|
387
|
+
const line = JSON.stringify(payload);
|
|
388
|
+
try {
|
|
389
|
+
console.error(`[codex-auth-debug] ${line}`);
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// best effort stderr logging
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
mkdirSync(OAUTH_DEBUG_LOG_DIR, { recursive: true, mode: 0o700 });
|
|
396
|
+
appendFileSync(OAUTH_DEBUG_LOG_FILE, `${line}\n`, { encoding: "utf8", mode: 0o600 });
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
// best effort file logging
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function clearOAuthServerCloseTimer() {
|
|
403
|
+
if (!oauthServerCloseTimer)
|
|
404
|
+
return;
|
|
405
|
+
clearTimeout(oauthServerCloseTimer);
|
|
406
|
+
oauthServerCloseTimer = undefined;
|
|
407
|
+
}
|
|
408
|
+
async function startOAuthServer() {
|
|
409
|
+
clearOAuthServerCloseTimer();
|
|
410
|
+
if (oauthServer) {
|
|
411
|
+
emitOAuthDebug("server_reuse", { port: OAUTH_PORT });
|
|
412
|
+
return { redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` };
|
|
413
|
+
}
|
|
414
|
+
emitOAuthDebug("server_starting", { port: OAUTH_PORT });
|
|
415
|
+
oauthServer = http.createServer((req, res) => {
|
|
416
|
+
try {
|
|
417
|
+
const base = `http://${req.headers.host ?? `localhost:${OAUTH_PORT}`}`;
|
|
418
|
+
const url = new URL(req.url ?? "/", base);
|
|
419
|
+
const sendHtml = (status, html) => {
|
|
420
|
+
res.statusCode = status;
|
|
421
|
+
res.setHeader("Content-Type", "text/html");
|
|
422
|
+
res.end(html);
|
|
423
|
+
};
|
|
424
|
+
const redirect = (location) => {
|
|
425
|
+
res.statusCode = 302;
|
|
426
|
+
res.setHeader("Location", location);
|
|
427
|
+
res.end();
|
|
428
|
+
};
|
|
429
|
+
if (url.pathname === "/auth/callback") {
|
|
430
|
+
const code = url.searchParams.get("code");
|
|
431
|
+
const state = url.searchParams.get("state");
|
|
432
|
+
const error = url.searchParams.get("error");
|
|
433
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
434
|
+
emitOAuthDebug("callback_hit", {
|
|
435
|
+
hasCode: Boolean(code),
|
|
436
|
+
hasState: Boolean(state),
|
|
437
|
+
hasError: Boolean(error)
|
|
438
|
+
});
|
|
439
|
+
if (error) {
|
|
440
|
+
const errorMsg = errorDescription || error;
|
|
441
|
+
emitOAuthDebug("callback_error", { reason: errorMsg });
|
|
442
|
+
pendingOAuth?.reject(new Error(errorMsg));
|
|
443
|
+
pendingOAuth = undefined;
|
|
444
|
+
sendHtml(200, buildOAuthErrorHtml(errorMsg));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (!code) {
|
|
448
|
+
const errorMsg = "Missing authorization code";
|
|
449
|
+
emitOAuthDebug("callback_error", { reason: errorMsg });
|
|
450
|
+
pendingOAuth?.reject(new Error(errorMsg));
|
|
451
|
+
pendingOAuth = undefined;
|
|
452
|
+
sendHtml(400, buildOAuthErrorHtml(errorMsg));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (!pendingOAuth || state !== pendingOAuth.state) {
|
|
456
|
+
const errorMsg = "Invalid state - potential CSRF attack";
|
|
457
|
+
emitOAuthDebug("callback_error", { reason: errorMsg });
|
|
458
|
+
pendingOAuth?.reject(new Error(errorMsg));
|
|
459
|
+
pendingOAuth = undefined;
|
|
460
|
+
sendHtml(400, buildOAuthErrorHtml(errorMsg));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const current = pendingOAuth;
|
|
464
|
+
pendingOAuth = undefined;
|
|
465
|
+
emitOAuthDebug("token_exchange_start", { authMode: current.authMode });
|
|
466
|
+
exchangeCodeForTokens(code, `http://localhost:${OAUTH_PORT}/auth/callback`, current.pkce)
|
|
467
|
+
.then((tokens) => {
|
|
468
|
+
current.resolve(tokens);
|
|
469
|
+
emitOAuthDebug("token_exchange_success", { authMode: current.authMode });
|
|
470
|
+
if (res.writableEnded)
|
|
471
|
+
return;
|
|
472
|
+
if (current.authMode === "codex") {
|
|
473
|
+
redirect(composeCodexSuccessRedirectUrl(tokens));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
sendHtml(200, buildOAuthSuccessHtml("native"));
|
|
477
|
+
})
|
|
478
|
+
.catch((err) => {
|
|
479
|
+
const oauthError = err instanceof Error ? err : new Error(String(err));
|
|
480
|
+
current.reject(oauthError);
|
|
481
|
+
emitOAuthDebug("token_exchange_error", { error: oauthError.message });
|
|
482
|
+
if (res.writableEnded)
|
|
483
|
+
return;
|
|
484
|
+
sendHtml(500, buildOAuthErrorHtml(oauthError.message));
|
|
485
|
+
});
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (url.pathname === "/success") {
|
|
489
|
+
emitOAuthDebug("callback_success_page");
|
|
490
|
+
sendHtml(200, buildOAuthSuccessHtml("codex"));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (url.pathname === "/cancel") {
|
|
494
|
+
emitOAuthDebug("callback_cancel");
|
|
495
|
+
pendingOAuth?.reject(new Error("Login cancelled"));
|
|
496
|
+
pendingOAuth = undefined;
|
|
497
|
+
res.statusCode = 200;
|
|
498
|
+
res.end("Login cancelled");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
res.statusCode = 404;
|
|
502
|
+
res.end("Not found");
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
res.statusCode = 500;
|
|
506
|
+
res.end(`Server error: ${error.message}`);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
try {
|
|
510
|
+
await new Promise((resolve, reject) => {
|
|
511
|
+
oauthServer?.once("error", reject);
|
|
512
|
+
oauthServer?.listen(OAUTH_PORT, () => resolve());
|
|
513
|
+
});
|
|
514
|
+
emitOAuthDebug("server_started", { port: OAUTH_PORT });
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
emitOAuthDebug("server_start_error", {
|
|
518
|
+
error: error instanceof Error ? error.message : String(error)
|
|
519
|
+
});
|
|
520
|
+
const server = oauthServer;
|
|
521
|
+
oauthServer = undefined;
|
|
522
|
+
try {
|
|
523
|
+
server?.close();
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
// best-effort cleanup
|
|
527
|
+
}
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
return { redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` };
|
|
531
|
+
}
|
|
532
|
+
function stopOAuthServer() {
|
|
533
|
+
clearOAuthServerCloseTimer();
|
|
534
|
+
emitOAuthDebug("server_stopping", { hadPendingOAuth: Boolean(pendingOAuth) });
|
|
535
|
+
oauthServer?.close();
|
|
536
|
+
oauthServer = undefined;
|
|
537
|
+
emitOAuthDebug("server_stopped");
|
|
538
|
+
}
|
|
539
|
+
function scheduleOAuthServerStop(delayMs = OAUTH_SERVER_SHUTDOWN_GRACE_MS, reason = "other") {
|
|
540
|
+
if (!oauthServer)
|
|
541
|
+
return;
|
|
542
|
+
clearOAuthServerCloseTimer();
|
|
543
|
+
emitOAuthDebug("server_stop_scheduled", { delayMs, reason });
|
|
544
|
+
oauthServerCloseTimer = setTimeout(() => {
|
|
545
|
+
oauthServerCloseTimer = undefined;
|
|
546
|
+
if (pendingOAuth)
|
|
547
|
+
return;
|
|
548
|
+
emitOAuthDebug("server_stop_timer_fired", { reason });
|
|
549
|
+
stopOAuthServer();
|
|
550
|
+
}, delayMs);
|
|
551
|
+
}
|
|
552
|
+
function waitForOAuthCallback(pkce, state, authMode) {
|
|
553
|
+
emitOAuthDebug("callback_wait_start", {
|
|
554
|
+
authMode,
|
|
555
|
+
stateTail: state.slice(-6)
|
|
556
|
+
});
|
|
557
|
+
return new Promise((resolve, reject) => {
|
|
558
|
+
let settled = false;
|
|
559
|
+
const resolveOnce = (tokens) => {
|
|
560
|
+
if (settled)
|
|
561
|
+
return;
|
|
562
|
+
settled = true;
|
|
563
|
+
clearTimeout(timeout);
|
|
564
|
+
emitOAuthDebug("callback_wait_resolved", { authMode });
|
|
565
|
+
resolve(tokens);
|
|
566
|
+
};
|
|
567
|
+
const rejectOnce = (error) => {
|
|
568
|
+
if (settled)
|
|
569
|
+
return;
|
|
570
|
+
settled = true;
|
|
571
|
+
clearTimeout(timeout);
|
|
572
|
+
emitOAuthDebug("callback_wait_rejected", { authMode, error: error.message });
|
|
573
|
+
reject(error);
|
|
574
|
+
};
|
|
575
|
+
const timeout = setTimeout(() => {
|
|
576
|
+
pendingOAuth = undefined;
|
|
577
|
+
emitOAuthDebug("callback_wait_timeout", { authMode, timeoutMs: OAUTH_CALLBACK_TIMEOUT_MS });
|
|
578
|
+
rejectOnce(new Error("OAuth callback timeout - authorization took too long"));
|
|
579
|
+
}, OAUTH_CALLBACK_TIMEOUT_MS);
|
|
580
|
+
pendingOAuth = {
|
|
581
|
+
pkce,
|
|
582
|
+
state,
|
|
583
|
+
authMode,
|
|
584
|
+
resolve: resolveOnce,
|
|
585
|
+
reject: rejectOnce
|
|
586
|
+
};
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
function modeForRuntimeMode(runtimeMode) {
|
|
590
|
+
return runtimeMode === "native" ? "native" : "codex";
|
|
591
|
+
}
|
|
592
|
+
const ACCOUNT_AUTH_TYPE_ORDER = ["native", "codex"];
|
|
593
|
+
function normalizeAccountAuthTypes(input) {
|
|
594
|
+
const source = Array.isArray(input) ? input : ["native"];
|
|
595
|
+
const seen = new Set();
|
|
596
|
+
const out = [];
|
|
597
|
+
for (const rawType of source) {
|
|
598
|
+
const type = rawType === "codex" ? "codex" : rawType === "native" ? "native" : undefined;
|
|
599
|
+
if (!type || seen.has(type))
|
|
600
|
+
continue;
|
|
601
|
+
seen.add(type);
|
|
602
|
+
out.push(type);
|
|
603
|
+
}
|
|
604
|
+
if (out.length === 0)
|
|
605
|
+
out.push("native");
|
|
606
|
+
out.sort((a, b) => ACCOUNT_AUTH_TYPE_ORDER.indexOf(a) - ACCOUNT_AUTH_TYPE_ORDER.indexOf(b));
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
function mergeAccountAuthTypes(existing, incoming) {
|
|
610
|
+
const merged = [...normalizeAccountAuthTypes(existing), ...normalizeAccountAuthTypes(incoming)];
|
|
611
|
+
return normalizeAccountAuthTypes(merged);
|
|
612
|
+
}
|
|
613
|
+
function removeAccountAuthType(existing, scope) {
|
|
614
|
+
return normalizeAccountAuthTypes(existing).filter((type) => type !== scope);
|
|
615
|
+
}
|
|
616
|
+
export function upsertAccount(openai, incoming) {
|
|
617
|
+
const normalizedEmail = normalizeEmail(incoming.email);
|
|
618
|
+
const normalizedPlan = normalizePlan(incoming.plan);
|
|
619
|
+
const normalizedAccountId = incoming.accountId?.trim();
|
|
620
|
+
const strictIdentityKey = buildIdentityKey({
|
|
621
|
+
accountId: normalizedAccountId,
|
|
622
|
+
email: normalizedEmail,
|
|
623
|
+
plan: normalizedPlan
|
|
624
|
+
});
|
|
625
|
+
const strictMatch = strictIdentityKey
|
|
626
|
+
? openai.accounts.find((existing) => {
|
|
627
|
+
const existingAccountId = existing.accountId?.trim();
|
|
628
|
+
const existingEmail = normalizeEmail(existing.email);
|
|
629
|
+
const existingPlan = normalizePlan(existing.plan);
|
|
630
|
+
return (existingAccountId === normalizedAccountId &&
|
|
631
|
+
existingEmail === normalizedEmail &&
|
|
632
|
+
existingPlan === normalizedPlan);
|
|
633
|
+
})
|
|
634
|
+
: undefined;
|
|
635
|
+
const refreshFallbackMatch = strictMatch || !incoming.refresh
|
|
636
|
+
? undefined
|
|
637
|
+
: openai.accounts.find((existing) => existing.refresh === incoming.refresh);
|
|
638
|
+
const match = strictMatch ?? refreshFallbackMatch;
|
|
639
|
+
const matchedByRefreshFallback = refreshFallbackMatch !== undefined && strictMatch === undefined;
|
|
640
|
+
const requiresInsert = matchedByRefreshFallback &&
|
|
641
|
+
strictIdentityKey !== undefined &&
|
|
642
|
+
match?.identityKey !== undefined &&
|
|
643
|
+
match.identityKey !== strictIdentityKey;
|
|
644
|
+
const target = !match || requiresInsert ? {} : match;
|
|
645
|
+
if (!match || requiresInsert) {
|
|
646
|
+
openai.accounts.push(target);
|
|
647
|
+
}
|
|
648
|
+
if (!matchedByRefreshFallback || requiresInsert) {
|
|
649
|
+
if (normalizedAccountId)
|
|
650
|
+
target.accountId = normalizedAccountId;
|
|
651
|
+
if (normalizedEmail)
|
|
652
|
+
target.email = normalizedEmail;
|
|
653
|
+
if (normalizedPlan)
|
|
654
|
+
target.plan = normalizedPlan;
|
|
655
|
+
}
|
|
656
|
+
if (incoming.enabled !== undefined)
|
|
657
|
+
target.enabled = incoming.enabled;
|
|
658
|
+
if (incoming.refresh)
|
|
659
|
+
target.refresh = incoming.refresh;
|
|
660
|
+
if (incoming.access)
|
|
661
|
+
target.access = incoming.access;
|
|
662
|
+
if (incoming.expires !== undefined)
|
|
663
|
+
target.expires = incoming.expires;
|
|
664
|
+
if (incoming.lastUsed !== undefined)
|
|
665
|
+
target.lastUsed = incoming.lastUsed;
|
|
666
|
+
target.authTypes = normalizeAccountAuthTypes(incoming.authTypes ?? match?.authTypes);
|
|
667
|
+
ensureIdentityKey(target);
|
|
668
|
+
if (!target.identityKey && strictIdentityKey)
|
|
669
|
+
target.identityKey = strictIdentityKey;
|
|
670
|
+
return target;
|
|
671
|
+
}
|
|
672
|
+
function rewriteUrl(requestInput) {
|
|
673
|
+
const parsed = requestInput instanceof URL
|
|
674
|
+
? requestInput
|
|
675
|
+
: new URL(typeof requestInput === "string" ? requestInput : requestInput.url);
|
|
676
|
+
if (parsed.pathname.includes("/v1/responses") ||
|
|
677
|
+
parsed.pathname.includes("/chat/completions")) {
|
|
678
|
+
return new URL(CODEX_API_ENDPOINT);
|
|
679
|
+
}
|
|
680
|
+
return parsed;
|
|
681
|
+
}
|
|
682
|
+
function opencodeUserAgent() {
|
|
683
|
+
return `opencode-codex-auth ( ${os.platform()} ${os.release()}; ${os.arch()} )`;
|
|
684
|
+
}
|
|
685
|
+
let cachedPluginVersion;
|
|
686
|
+
let cachedMacProductVersion;
|
|
687
|
+
let cachedTerminalUserAgentToken;
|
|
688
|
+
function isPrintableAscii(value) {
|
|
689
|
+
if (!value)
|
|
690
|
+
return false;
|
|
691
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
692
|
+
const code = value.charCodeAt(index);
|
|
693
|
+
if (code < 0x20 || code > 0x7e)
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
function sanitizeUserAgentCandidate(candidate, fallback, originator) {
|
|
699
|
+
if (isPrintableAscii(candidate))
|
|
700
|
+
return candidate;
|
|
701
|
+
const sanitized = Array.from(candidate)
|
|
702
|
+
.map((char) => {
|
|
703
|
+
const code = char.charCodeAt(0);
|
|
704
|
+
return code >= 0x20 && code <= 0x7e ? char : "_";
|
|
705
|
+
})
|
|
706
|
+
.join("");
|
|
707
|
+
if (isPrintableAscii(sanitized))
|
|
708
|
+
return sanitized;
|
|
709
|
+
if (isPrintableAscii(fallback))
|
|
710
|
+
return fallback;
|
|
711
|
+
return originator;
|
|
712
|
+
}
|
|
713
|
+
function sanitizeTerminalToken(value) {
|
|
714
|
+
return value.replace(/[^A-Za-z0-9._/-]/g, "_");
|
|
715
|
+
}
|
|
716
|
+
function nonEmptyEnv(env, key) {
|
|
717
|
+
const value = env[key]?.trim();
|
|
718
|
+
return value ? value : undefined;
|
|
719
|
+
}
|
|
720
|
+
function splitProgramAndVersion(value) {
|
|
721
|
+
const [program, version] = value.trim().split(/\s+/, 2);
|
|
722
|
+
return {
|
|
723
|
+
program: program ?? "unknown",
|
|
724
|
+
...(version ? { version } : {})
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function tmuxDisplayMessage(format) {
|
|
728
|
+
try {
|
|
729
|
+
const value = execFileSync("tmux", ["display-message", "-p", format], { encoding: "utf8" }).trim();
|
|
730
|
+
return value || undefined;
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
return undefined;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function resolveTerminalUserAgentToken(env = process.env) {
|
|
737
|
+
if (cachedTerminalUserAgentToken)
|
|
738
|
+
return cachedTerminalUserAgentToken;
|
|
739
|
+
const termProgram = nonEmptyEnv(env, "TERM_PROGRAM");
|
|
740
|
+
const termProgramVersion = nonEmptyEnv(env, "TERM_PROGRAM_VERSION");
|
|
741
|
+
const term = nonEmptyEnv(env, "TERM");
|
|
742
|
+
const hasTmux = Boolean(nonEmptyEnv(env, "TMUX") || nonEmptyEnv(env, "TMUX_PANE"));
|
|
743
|
+
if (termProgram && termProgram.toLowerCase() === "tmux" && hasTmux) {
|
|
744
|
+
const tmuxTermType = tmuxDisplayMessage("#{client_termtype}");
|
|
745
|
+
if (tmuxTermType) {
|
|
746
|
+
const { program, version } = splitProgramAndVersion(tmuxTermType);
|
|
747
|
+
cachedTerminalUserAgentToken = sanitizeTerminalToken(version ? `${program}/${version}` : program);
|
|
748
|
+
return cachedTerminalUserAgentToken;
|
|
749
|
+
}
|
|
750
|
+
const tmuxTermName = tmuxDisplayMessage("#{client_termname}");
|
|
751
|
+
if (tmuxTermName) {
|
|
752
|
+
cachedTerminalUserAgentToken = sanitizeTerminalToken(tmuxTermName);
|
|
753
|
+
return cachedTerminalUserAgentToken;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (termProgram) {
|
|
757
|
+
cachedTerminalUserAgentToken = sanitizeTerminalToken(termProgramVersion ? `${termProgram}/${termProgramVersion}` : termProgram);
|
|
758
|
+
return cachedTerminalUserAgentToken;
|
|
759
|
+
}
|
|
760
|
+
const weztermVersion = nonEmptyEnv(env, "WEZTERM_VERSION");
|
|
761
|
+
if (weztermVersion) {
|
|
762
|
+
cachedTerminalUserAgentToken = sanitizeTerminalToken(`WezTerm/${weztermVersion}`);
|
|
763
|
+
return cachedTerminalUserAgentToken;
|
|
764
|
+
}
|
|
765
|
+
if (env.ITERM_SESSION_ID || env.ITERM_PROFILE || env.ITERM_PROFILE_NAME) {
|
|
766
|
+
cachedTerminalUserAgentToken = "iTerm.app";
|
|
767
|
+
return cachedTerminalUserAgentToken;
|
|
768
|
+
}
|
|
769
|
+
if (env.TERM_SESSION_ID) {
|
|
770
|
+
cachedTerminalUserAgentToken = "Apple_Terminal";
|
|
771
|
+
return cachedTerminalUserAgentToken;
|
|
772
|
+
}
|
|
773
|
+
if (env.KITTY_WINDOW_ID || term?.includes("kitty")) {
|
|
774
|
+
cachedTerminalUserAgentToken = "kitty";
|
|
775
|
+
return cachedTerminalUserAgentToken;
|
|
776
|
+
}
|
|
777
|
+
if (env.ALACRITTY_SOCKET || term === "alacritty") {
|
|
778
|
+
cachedTerminalUserAgentToken = "Alacritty";
|
|
779
|
+
return cachedTerminalUserAgentToken;
|
|
780
|
+
}
|
|
781
|
+
const konsoleVersion = nonEmptyEnv(env, "KONSOLE_VERSION");
|
|
782
|
+
if (konsoleVersion) {
|
|
783
|
+
cachedTerminalUserAgentToken = sanitizeTerminalToken(`Konsole/${konsoleVersion}`);
|
|
784
|
+
return cachedTerminalUserAgentToken;
|
|
785
|
+
}
|
|
786
|
+
if (env.GNOME_TERMINAL_SCREEN) {
|
|
787
|
+
cachedTerminalUserAgentToken = "gnome-terminal";
|
|
788
|
+
return cachedTerminalUserAgentToken;
|
|
789
|
+
}
|
|
790
|
+
const vteVersion = nonEmptyEnv(env, "VTE_VERSION");
|
|
791
|
+
if (vteVersion) {
|
|
792
|
+
cachedTerminalUserAgentToken = sanitizeTerminalToken(`VTE/${vteVersion}`);
|
|
793
|
+
return cachedTerminalUserAgentToken;
|
|
794
|
+
}
|
|
795
|
+
if (env.WT_SESSION) {
|
|
796
|
+
cachedTerminalUserAgentToken = "WindowsTerminal";
|
|
797
|
+
return cachedTerminalUserAgentToken;
|
|
798
|
+
}
|
|
799
|
+
if (term) {
|
|
800
|
+
cachedTerminalUserAgentToken = sanitizeTerminalToken(term);
|
|
801
|
+
return cachedTerminalUserAgentToken;
|
|
802
|
+
}
|
|
803
|
+
cachedTerminalUserAgentToken = "unknown";
|
|
804
|
+
return cachedTerminalUserAgentToken;
|
|
805
|
+
}
|
|
806
|
+
function resolvePluginVersion() {
|
|
807
|
+
if (cachedPluginVersion)
|
|
808
|
+
return cachedPluginVersion;
|
|
809
|
+
const fromEnv = process.env.npm_package_version?.trim();
|
|
810
|
+
if (fromEnv) {
|
|
811
|
+
cachedPluginVersion = fromEnv;
|
|
812
|
+
return cachedPluginVersion;
|
|
813
|
+
}
|
|
814
|
+
try {
|
|
815
|
+
const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
816
|
+
const parsed = JSON.parse(raw);
|
|
817
|
+
if (typeof parsed.version === "string" && parsed.version.trim()) {
|
|
818
|
+
cachedPluginVersion = parsed.version.trim();
|
|
819
|
+
return cachedPluginVersion;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
catch {
|
|
823
|
+
// Use fallback version below.
|
|
824
|
+
}
|
|
825
|
+
cachedPluginVersion = DEFAULT_PLUGIN_VERSION;
|
|
826
|
+
return cachedPluginVersion;
|
|
827
|
+
}
|
|
828
|
+
function resolveMacProductVersion() {
|
|
829
|
+
if (cachedMacProductVersion)
|
|
830
|
+
return cachedMacProductVersion;
|
|
831
|
+
try {
|
|
832
|
+
const value = execFileSync("sw_vers", ["-productVersion"], { encoding: "utf8" }).trim();
|
|
833
|
+
cachedMacProductVersion = value || os.release();
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
cachedMacProductVersion = os.release();
|
|
837
|
+
}
|
|
838
|
+
return cachedMacProductVersion;
|
|
839
|
+
}
|
|
840
|
+
function normalizeArchitecture(architecture) {
|
|
841
|
+
if (architecture === "x64")
|
|
842
|
+
return "x86_64";
|
|
843
|
+
if (architecture === "arm64")
|
|
844
|
+
return "arm64";
|
|
845
|
+
return architecture || "unknown";
|
|
846
|
+
}
|
|
847
|
+
function resolveCodexPlatformSignature(platform = process.platform) {
|
|
848
|
+
const architecture = normalizeArchitecture(os.arch());
|
|
849
|
+
if (platform === "darwin") {
|
|
850
|
+
return `Mac OS ${resolveMacProductVersion()}; ${architecture}`;
|
|
851
|
+
}
|
|
852
|
+
if (platform === "win32") {
|
|
853
|
+
return `Windows ${os.release()}; ${architecture}`;
|
|
854
|
+
}
|
|
855
|
+
if (platform === "linux") {
|
|
856
|
+
return `Linux ${os.release()}; ${architecture}`;
|
|
857
|
+
}
|
|
858
|
+
return `${platform} ${os.release()}; ${architecture}`;
|
|
859
|
+
}
|
|
860
|
+
function buildCodexUserAgent(originator) {
|
|
861
|
+
const buildVersion = resolvePluginVersion();
|
|
862
|
+
const terminalToken = resolveTerminalUserAgentToken();
|
|
863
|
+
const prefix = `${originator}/${buildVersion} (${resolveCodexPlatformSignature()}) ${terminalToken}`;
|
|
864
|
+
return sanitizeUserAgentCandidate(prefix, prefix, originator);
|
|
865
|
+
}
|
|
866
|
+
function resolveRequestUserAgent(spoofMode, originator) {
|
|
867
|
+
if (spoofMode === "codex")
|
|
868
|
+
return buildCodexUserAgent(originator);
|
|
869
|
+
return opencodeUserAgent();
|
|
870
|
+
}
|
|
871
|
+
function resolveHookAgentName(agent) {
|
|
872
|
+
const direct = asString(agent);
|
|
873
|
+
if (direct)
|
|
874
|
+
return direct;
|
|
875
|
+
if (!isRecord(agent))
|
|
876
|
+
return undefined;
|
|
877
|
+
return asString(agent.name) ?? asString(agent.agent);
|
|
878
|
+
}
|
|
879
|
+
function normalizeAgentNameForCollaboration(agentName) {
|
|
880
|
+
return agentName.trim().toLowerCase().replace(/\s+/g, "-");
|
|
881
|
+
}
|
|
882
|
+
function tokenizeAgentName(normalizedAgentName) {
|
|
883
|
+
return normalizedAgentName
|
|
884
|
+
.split(/[-./:_]+/)
|
|
885
|
+
.map((token) => token.trim())
|
|
886
|
+
.filter((token) => token.length > 0);
|
|
887
|
+
}
|
|
888
|
+
function isPluginCollaborationAgent(normalizedAgentName) {
|
|
889
|
+
const tokens = tokenizeAgentName(normalizedAgentName);
|
|
890
|
+
if (tokens.length === 0)
|
|
891
|
+
return false;
|
|
892
|
+
if (tokens[0] !== "codex")
|
|
893
|
+
return false;
|
|
894
|
+
return tokens.some((token) => [
|
|
895
|
+
"orchestrator",
|
|
896
|
+
"default",
|
|
897
|
+
"code",
|
|
898
|
+
"plan",
|
|
899
|
+
"planner",
|
|
900
|
+
"execute",
|
|
901
|
+
"pair",
|
|
902
|
+
"pairprogramming",
|
|
903
|
+
"review",
|
|
904
|
+
"compact",
|
|
905
|
+
"compaction"
|
|
906
|
+
].includes(token));
|
|
907
|
+
}
|
|
908
|
+
function resolveCollaborationModeKindFromName(normalizedAgentName) {
|
|
909
|
+
const tokens = tokenizeAgentName(normalizedAgentName);
|
|
910
|
+
if (tokens.includes("plan") || tokens.includes("planner"))
|
|
911
|
+
return "plan";
|
|
912
|
+
if (tokens.includes("execute"))
|
|
913
|
+
return "execute";
|
|
914
|
+
if (tokens.includes("pair") || tokens.includes("pairprogramming"))
|
|
915
|
+
return "pair_programming";
|
|
916
|
+
return "code";
|
|
917
|
+
}
|
|
918
|
+
function resolveCollaborationProfile(agent) {
|
|
919
|
+
const name = resolveHookAgentName(agent);
|
|
920
|
+
if (!name)
|
|
921
|
+
return { enabled: false };
|
|
922
|
+
const normalizedAgentName = normalizeAgentNameForCollaboration(name);
|
|
923
|
+
if (!isPluginCollaborationAgent(normalizedAgentName)) {
|
|
924
|
+
return { enabled: false, normalizedAgentName };
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
enabled: true,
|
|
928
|
+
normalizedAgentName,
|
|
929
|
+
kind: resolveCollaborationModeKindFromName(normalizedAgentName)
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
function resolveCollaborationModeKind(agent) {
|
|
933
|
+
const profile = resolveCollaborationProfile(agent);
|
|
934
|
+
return profile.kind ?? "code";
|
|
935
|
+
}
|
|
936
|
+
function resolveCollaborationInstructions(kind) {
|
|
937
|
+
if (kind === "plan")
|
|
938
|
+
return CODEX_PLAN_MODE_INSTRUCTIONS;
|
|
939
|
+
if (kind === "execute")
|
|
940
|
+
return CODEX_EXECUTE_MODE_INSTRUCTIONS;
|
|
941
|
+
if (kind === "pair_programming")
|
|
942
|
+
return CODEX_PAIR_PROGRAMMING_MODE_INSTRUCTIONS;
|
|
943
|
+
return CODEX_CODE_MODE_INSTRUCTIONS;
|
|
944
|
+
}
|
|
945
|
+
function mergeInstructions(base, extra) {
|
|
946
|
+
const normalizedExtra = extra.trim();
|
|
947
|
+
if (!normalizedExtra)
|
|
948
|
+
return base?.trim() ?? "";
|
|
949
|
+
const normalizedBase = base?.trim();
|
|
950
|
+
if (!normalizedBase)
|
|
951
|
+
return normalizedExtra;
|
|
952
|
+
if (normalizedBase.includes(normalizedExtra))
|
|
953
|
+
return normalizedBase;
|
|
954
|
+
return `${normalizedBase}\n\n${normalizedExtra}`;
|
|
955
|
+
}
|
|
956
|
+
function resolveSubagentHeaderValue(agent) {
|
|
957
|
+
const profile = resolveCollaborationProfile(agent);
|
|
958
|
+
const normalized = profile.normalizedAgentName;
|
|
959
|
+
if (!profile.enabled || !normalized) {
|
|
960
|
+
return undefined;
|
|
961
|
+
}
|
|
962
|
+
const tokens = tokenizeAgentName(normalized);
|
|
963
|
+
const isCodexPrimary = tokens[0] === "codex" &&
|
|
964
|
+
(tokens.includes("orchestrator") ||
|
|
965
|
+
tokens.includes("default") ||
|
|
966
|
+
tokens.includes("code") ||
|
|
967
|
+
tokens.includes("plan") ||
|
|
968
|
+
tokens.includes("planner") ||
|
|
969
|
+
tokens.includes("execute") ||
|
|
970
|
+
tokens.includes("pair") ||
|
|
971
|
+
tokens.includes("pairprogramming"));
|
|
972
|
+
if (isCodexPrimary) {
|
|
973
|
+
return undefined;
|
|
974
|
+
}
|
|
975
|
+
if (tokens.includes("plan") || tokens.includes("planner")) {
|
|
976
|
+
return undefined;
|
|
977
|
+
}
|
|
978
|
+
if (normalized === "compaction") {
|
|
979
|
+
return "compact";
|
|
980
|
+
}
|
|
981
|
+
if (normalized.includes("review"))
|
|
982
|
+
return "review";
|
|
983
|
+
if (normalized.includes("compact") || normalized.includes("compaction"))
|
|
984
|
+
return "compact";
|
|
985
|
+
return "collab_spawn";
|
|
986
|
+
}
|
|
987
|
+
async function sessionUsesOpenAIProvider(client, sessionID) {
|
|
988
|
+
const sessionApi = client?.session;
|
|
989
|
+
if (!sessionApi || typeof sessionApi.messages !== "function")
|
|
990
|
+
return false;
|
|
991
|
+
try {
|
|
992
|
+
const response = await sessionApi.messages({ sessionID, limit: 100 });
|
|
993
|
+
const rows = isRecord(response) && Array.isArray(response.data) ? response.data : [];
|
|
994
|
+
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
|
995
|
+
const row = rows[index];
|
|
996
|
+
if (!isRecord(row) || !isRecord(row.info))
|
|
997
|
+
continue;
|
|
998
|
+
const info = row.info;
|
|
999
|
+
if (asString(info.role) !== "user")
|
|
1000
|
+
continue;
|
|
1001
|
+
const model = isRecord(info.model) ? info.model : undefined;
|
|
1002
|
+
const providerID = model
|
|
1003
|
+
? asString(model.providerID)
|
|
1004
|
+
: asString(info.providerID);
|
|
1005
|
+
if (!providerID)
|
|
1006
|
+
continue;
|
|
1007
|
+
return providerID === "openai";
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
catch {
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
function isTuiWorkerInvocation(argv) {
|
|
1016
|
+
return argv.some((entry) => /(?:^|[\\/])tui[\\/]worker\.(?:js|ts)$/i.test(entry));
|
|
1017
|
+
}
|
|
1018
|
+
function resolveCodexOriginator(spoofMode, argv = process.argv) {
|
|
1019
|
+
if (spoofMode !== "codex")
|
|
1020
|
+
return "codex_cli_rs";
|
|
1021
|
+
const normalizedArgv = argv.map((entry) => String(entry));
|
|
1022
|
+
if (isTuiWorkerInvocation(normalizedArgv))
|
|
1023
|
+
return "codex_cli_rs";
|
|
1024
|
+
return normalizedArgv.includes("run") ? "codex_exec" : "codex_cli_rs";
|
|
1025
|
+
}
|
|
1026
|
+
function formatAccountLabel(account, index) {
|
|
1027
|
+
const email = account?.email?.trim();
|
|
1028
|
+
const plan = account?.plan?.trim();
|
|
1029
|
+
const accountId = account?.accountId?.trim();
|
|
1030
|
+
const idSuffix = accountId
|
|
1031
|
+
? accountId.length > 6
|
|
1032
|
+
? accountId.slice(-6)
|
|
1033
|
+
: accountId
|
|
1034
|
+
: null;
|
|
1035
|
+
if (email && plan)
|
|
1036
|
+
return `${email} (${plan})`;
|
|
1037
|
+
if (email)
|
|
1038
|
+
return email;
|
|
1039
|
+
if (idSuffix)
|
|
1040
|
+
return `id:${idSuffix}`;
|
|
1041
|
+
return `Account ${index + 1}`;
|
|
1042
|
+
}
|
|
1043
|
+
function hasActiveCooldown(account, now) {
|
|
1044
|
+
return typeof account.cooldownUntil === "number" && Number.isFinite(account.cooldownUntil) && account.cooldownUntil > now;
|
|
1045
|
+
}
|
|
1046
|
+
function ensureAccountAuthTypes(account) {
|
|
1047
|
+
const normalized = normalizeAccountAuthTypes(account.authTypes);
|
|
1048
|
+
account.authTypes = normalized;
|
|
1049
|
+
return normalized;
|
|
1050
|
+
}
|
|
1051
|
+
function reconcileActiveIdentityKey(openai) {
|
|
1052
|
+
if (openai.activeIdentityKey &&
|
|
1053
|
+
openai.accounts.some((account) => account.identityKey === openai.activeIdentityKey && account.enabled !== false)) {
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
const fallback = openai.accounts.find((account) => account.enabled !== false && account.identityKey);
|
|
1057
|
+
openai.activeIdentityKey = fallback?.identityKey;
|
|
1058
|
+
}
|
|
1059
|
+
function findDomainAccountIndex(domain, account) {
|
|
1060
|
+
if (account.identityKey) {
|
|
1061
|
+
const byIdentity = domain.accounts.findIndex((entry) => entry.identityKey === account.identityKey);
|
|
1062
|
+
if (byIdentity >= 0)
|
|
1063
|
+
return byIdentity;
|
|
1064
|
+
}
|
|
1065
|
+
return domain.accounts.findIndex((entry) => {
|
|
1066
|
+
const sameId = (entry.accountId?.trim() ?? "") === (account.accountId?.trim() ?? "");
|
|
1067
|
+
const sameEmail = normalizeEmail(entry.email) === normalizeEmail(account.email);
|
|
1068
|
+
const samePlan = normalizePlan(entry.plan) === normalizePlan(account.plan);
|
|
1069
|
+
return sameId && sameEmail && samePlan;
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
function buildAuthMenuAccounts(input) {
|
|
1073
|
+
const now = Date.now();
|
|
1074
|
+
const rows = new Map();
|
|
1075
|
+
const mergeFromDomain = (authMode, domain) => {
|
|
1076
|
+
if (!domain)
|
|
1077
|
+
return;
|
|
1078
|
+
for (const account of domain.accounts) {
|
|
1079
|
+
const normalizedTypes = ensureAccountAuthTypes(account);
|
|
1080
|
+
const identity = account.identityKey ??
|
|
1081
|
+
buildIdentityKey({
|
|
1082
|
+
accountId: account.accountId,
|
|
1083
|
+
email: normalizeEmail(account.email),
|
|
1084
|
+
plan: normalizePlan(account.plan)
|
|
1085
|
+
}) ??
|
|
1086
|
+
`${authMode}:${account.accountId ?? account.email ?? account.plan ?? "unknown"}`;
|
|
1087
|
+
const existing = rows.get(identity);
|
|
1088
|
+
const currentStatus = hasActiveCooldown(account, now)
|
|
1089
|
+
? "rate-limited"
|
|
1090
|
+
: typeof account.expires === "number" &&
|
|
1091
|
+
Number.isFinite(account.expires) &&
|
|
1092
|
+
account.expires <= now
|
|
1093
|
+
? "expired"
|
|
1094
|
+
: "unknown";
|
|
1095
|
+
if (!existing) {
|
|
1096
|
+
const isCurrentAccount = authMode === input.activeMode && Boolean(domain.activeIdentityKey && account.identityKey === domain.activeIdentityKey);
|
|
1097
|
+
rows.set(identity, {
|
|
1098
|
+
identityKey: account.identityKey,
|
|
1099
|
+
index: rows.size,
|
|
1100
|
+
accountId: account.accountId,
|
|
1101
|
+
email: account.email,
|
|
1102
|
+
plan: account.plan,
|
|
1103
|
+
authTypes: [authMode],
|
|
1104
|
+
lastUsed: account.lastUsed,
|
|
1105
|
+
enabled: account.enabled,
|
|
1106
|
+
status: isCurrentAccount ? "active" : currentStatus,
|
|
1107
|
+
isCurrentAccount
|
|
1108
|
+
});
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
existing.authTypes = normalizeAccountAuthTypes([...(existing.authTypes ?? []), authMode]);
|
|
1112
|
+
if (typeof account.lastUsed === "number" &&
|
|
1113
|
+
(!existing.lastUsed || account.lastUsed > existing.lastUsed)) {
|
|
1114
|
+
existing.lastUsed = account.lastUsed;
|
|
1115
|
+
}
|
|
1116
|
+
if (existing.enabled === false && account.enabled !== false) {
|
|
1117
|
+
existing.enabled = true;
|
|
1118
|
+
}
|
|
1119
|
+
if (existing.status !== "rate-limited" && currentStatus === "rate-limited") {
|
|
1120
|
+
existing.status = "rate-limited";
|
|
1121
|
+
}
|
|
1122
|
+
else if (existing.status !== "rate-limited" &&
|
|
1123
|
+
existing.status !== "expired" &&
|
|
1124
|
+
currentStatus === "expired") {
|
|
1125
|
+
existing.status = "expired";
|
|
1126
|
+
}
|
|
1127
|
+
const isCurrentAccount = authMode === input.activeMode && Boolean(domain.activeIdentityKey && account.identityKey === domain.activeIdentityKey);
|
|
1128
|
+
if (isCurrentAccount) {
|
|
1129
|
+
existing.isCurrentAccount = true;
|
|
1130
|
+
existing.status = "active";
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
mergeFromDomain("native", input.native);
|
|
1135
|
+
mergeFromDomain("codex", input.codex);
|
|
1136
|
+
return Array.from(rows.values()).map((row, index) => ({ ...row, index }));
|
|
1137
|
+
}
|
|
1138
|
+
function hydrateAccountIdentityFromAccessClaims(account) {
|
|
1139
|
+
const claims = typeof account.access === "string" && account.access.length > 0
|
|
1140
|
+
? parseJwtClaims(account.access)
|
|
1141
|
+
: undefined;
|
|
1142
|
+
if (!account.accountId)
|
|
1143
|
+
account.accountId = extractAccountIdFromClaims(claims);
|
|
1144
|
+
if (!account.email)
|
|
1145
|
+
account.email = extractEmailFromClaims(claims);
|
|
1146
|
+
if (!account.plan)
|
|
1147
|
+
account.plan = extractPlanFromClaims(claims);
|
|
1148
|
+
account.email = normalizeEmail(account.email);
|
|
1149
|
+
account.plan = normalizePlan(account.plan);
|
|
1150
|
+
if (account.accountId)
|
|
1151
|
+
account.accountId = account.accountId.trim();
|
|
1152
|
+
ensureAccountAuthTypes(account);
|
|
1153
|
+
ensureIdentityKey(account);
|
|
1154
|
+
}
|
|
1155
|
+
async function selectCatalogAuthCandidate(authMode, pidOffsetEnabled) {
|
|
1156
|
+
try {
|
|
1157
|
+
const auth = await loadAuthStorage();
|
|
1158
|
+
const domain = getOpenAIOAuthDomain(auth, authMode);
|
|
1159
|
+
if (!domain) {
|
|
1160
|
+
return {};
|
|
1161
|
+
}
|
|
1162
|
+
const selected = selectAccount({
|
|
1163
|
+
accounts: domain.accounts,
|
|
1164
|
+
strategy: domain.strategy,
|
|
1165
|
+
activeIdentityKey: domain.activeIdentityKey,
|
|
1166
|
+
now: Date.now(),
|
|
1167
|
+
stickyPidOffset: pidOffsetEnabled
|
|
1168
|
+
});
|
|
1169
|
+
if (!selected?.access) {
|
|
1170
|
+
return { accountId: selected?.accountId };
|
|
1171
|
+
}
|
|
1172
|
+
if (selected.expires && selected.expires <= Date.now()) {
|
|
1173
|
+
return { accountId: selected.accountId };
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
accessToken: selected.access,
|
|
1177
|
+
accountId: selected.accountId
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
catch {
|
|
1181
|
+
return {};
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function isRecord(value) {
|
|
1185
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1186
|
+
}
|
|
1187
|
+
function asString(value) {
|
|
1188
|
+
if (typeof value !== "string")
|
|
1189
|
+
return undefined;
|
|
1190
|
+
const trimmed = value.trim();
|
|
1191
|
+
return trimmed ? trimmed : undefined;
|
|
1192
|
+
}
|
|
1193
|
+
function asStringArray(value) {
|
|
1194
|
+
if (!Array.isArray(value))
|
|
1195
|
+
return undefined;
|
|
1196
|
+
return value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
1197
|
+
}
|
|
1198
|
+
function readModelRuntimeDefaults(options) {
|
|
1199
|
+
const raw = options.codexRuntimeDefaults;
|
|
1200
|
+
if (!isRecord(raw))
|
|
1201
|
+
return {};
|
|
1202
|
+
return {
|
|
1203
|
+
applyPatchToolType: asString(raw.applyPatchToolType),
|
|
1204
|
+
defaultReasoningEffort: asString(raw.defaultReasoningEffort),
|
|
1205
|
+
supportsReasoningSummaries: typeof raw.supportsReasoningSummaries === "boolean" ? raw.supportsReasoningSummaries : undefined,
|
|
1206
|
+
defaultVerbosity: raw.defaultVerbosity === "low" || raw.defaultVerbosity === "medium" || raw.defaultVerbosity === "high"
|
|
1207
|
+
? raw.defaultVerbosity
|
|
1208
|
+
: undefined,
|
|
1209
|
+
supportsVerbosity: typeof raw.supportsVerbosity === "boolean" ? raw.supportsVerbosity : undefined
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
function mergeUnique(values) {
|
|
1213
|
+
const out = [];
|
|
1214
|
+
const seen = new Set();
|
|
1215
|
+
for (const value of values) {
|
|
1216
|
+
if (seen.has(value))
|
|
1217
|
+
continue;
|
|
1218
|
+
seen.add(value);
|
|
1219
|
+
out.push(value);
|
|
1220
|
+
}
|
|
1221
|
+
return out;
|
|
1222
|
+
}
|
|
1223
|
+
function normalizePersonalityKey(value) {
|
|
1224
|
+
const normalized = asString(value)?.toLowerCase();
|
|
1225
|
+
if (!normalized)
|
|
1226
|
+
return undefined;
|
|
1227
|
+
if (normalized.includes("/") || normalized.includes("\\") || normalized.includes("..")) {
|
|
1228
|
+
return undefined;
|
|
1229
|
+
}
|
|
1230
|
+
return normalized;
|
|
1231
|
+
}
|
|
1232
|
+
function getModelLookupCandidates(model) {
|
|
1233
|
+
const out = [];
|
|
1234
|
+
const seen = new Set();
|
|
1235
|
+
const add = (value) => {
|
|
1236
|
+
const trimmed = value?.trim();
|
|
1237
|
+
if (!trimmed)
|
|
1238
|
+
return;
|
|
1239
|
+
if (seen.has(trimmed))
|
|
1240
|
+
return;
|
|
1241
|
+
seen.add(trimmed);
|
|
1242
|
+
out.push(trimmed);
|
|
1243
|
+
};
|
|
1244
|
+
add(model.id);
|
|
1245
|
+
add(model.api?.id);
|
|
1246
|
+
add(model.id?.split("/").pop());
|
|
1247
|
+
add(model.api?.id?.split("/").pop());
|
|
1248
|
+
return out;
|
|
1249
|
+
}
|
|
1250
|
+
function getVariantLookupCandidates(input) {
|
|
1251
|
+
const out = [];
|
|
1252
|
+
const seen = new Set();
|
|
1253
|
+
const add = (value) => {
|
|
1254
|
+
const trimmed = value?.trim();
|
|
1255
|
+
if (!trimmed)
|
|
1256
|
+
return;
|
|
1257
|
+
if (seen.has(trimmed))
|
|
1258
|
+
return;
|
|
1259
|
+
seen.add(trimmed);
|
|
1260
|
+
out.push(trimmed);
|
|
1261
|
+
};
|
|
1262
|
+
if (isRecord(input.message)) {
|
|
1263
|
+
add(asString(input.message.variant));
|
|
1264
|
+
}
|
|
1265
|
+
for (const candidate of input.modelCandidates) {
|
|
1266
|
+
const slash = candidate.lastIndexOf("/");
|
|
1267
|
+
if (slash <= 0 || slash >= candidate.length - 1)
|
|
1268
|
+
continue;
|
|
1269
|
+
add(candidate.slice(slash + 1));
|
|
1270
|
+
}
|
|
1271
|
+
return out;
|
|
1272
|
+
}
|
|
1273
|
+
function resolveCaseInsensitiveEntry(entries, candidate) {
|
|
1274
|
+
if (!entries)
|
|
1275
|
+
return undefined;
|
|
1276
|
+
const direct = entries[candidate];
|
|
1277
|
+
if (direct !== undefined)
|
|
1278
|
+
return direct;
|
|
1279
|
+
const lowered = entries[candidate.toLowerCase()];
|
|
1280
|
+
if (lowered !== undefined)
|
|
1281
|
+
return lowered;
|
|
1282
|
+
const loweredCandidate = candidate.toLowerCase();
|
|
1283
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
1284
|
+
if (name.trim().toLowerCase() === loweredCandidate) {
|
|
1285
|
+
return entry;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return undefined;
|
|
1289
|
+
}
|
|
1290
|
+
function getModelPersonalityOverride(customSettings, modelCandidates, variantCandidates) {
|
|
1291
|
+
const models = customSettings?.models;
|
|
1292
|
+
if (!models)
|
|
1293
|
+
return undefined;
|
|
1294
|
+
for (const candidate of modelCandidates) {
|
|
1295
|
+
const entry = resolveCaseInsensitiveEntry(models, candidate);
|
|
1296
|
+
if (!entry)
|
|
1297
|
+
continue;
|
|
1298
|
+
for (const variantCandidate of variantCandidates) {
|
|
1299
|
+
const variantEntry = resolveCaseInsensitiveEntry(entry.variants, variantCandidate);
|
|
1300
|
+
const variantPersonality = normalizePersonalityKey(variantEntry?.options?.personality);
|
|
1301
|
+
if (variantPersonality)
|
|
1302
|
+
return variantPersonality;
|
|
1303
|
+
}
|
|
1304
|
+
const modelPersonality = normalizePersonalityKey(entry.options?.personality);
|
|
1305
|
+
if (modelPersonality)
|
|
1306
|
+
return modelPersonality;
|
|
1307
|
+
}
|
|
1308
|
+
return undefined;
|
|
1309
|
+
}
|
|
1310
|
+
function getModelThinkingSummariesOverride(customSettings, modelCandidates, variantCandidates) {
|
|
1311
|
+
const models = customSettings?.models;
|
|
1312
|
+
if (!models)
|
|
1313
|
+
return undefined;
|
|
1314
|
+
for (const candidate of modelCandidates) {
|
|
1315
|
+
const entry = resolveCaseInsensitiveEntry(models, candidate);
|
|
1316
|
+
if (!entry)
|
|
1317
|
+
continue;
|
|
1318
|
+
for (const variantCandidate of variantCandidates) {
|
|
1319
|
+
const variantEntry = resolveCaseInsensitiveEntry(entry.variants, variantCandidate);
|
|
1320
|
+
if (typeof variantEntry?.thinkingSummaries === "boolean") {
|
|
1321
|
+
return variantEntry.thinkingSummaries;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
if (typeof entry.thinkingSummaries === "boolean") {
|
|
1325
|
+
return entry.thinkingSummaries;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
return undefined;
|
|
1329
|
+
}
|
|
1330
|
+
function resolvePersonalityForModel(input) {
|
|
1331
|
+
const modelOverride = getModelPersonalityOverride(input.customSettings, input.modelCandidates, input.variantCandidates);
|
|
1332
|
+
if (modelOverride)
|
|
1333
|
+
return modelOverride;
|
|
1334
|
+
const globalOverride = normalizePersonalityKey(input.customSettings?.options?.personality);
|
|
1335
|
+
if (globalOverride)
|
|
1336
|
+
return globalOverride;
|
|
1337
|
+
return normalizePersonalityKey(input.fallback);
|
|
1338
|
+
}
|
|
1339
|
+
function applyCodexRuntimeDefaultsToParams(input) {
|
|
1340
|
+
const options = input.output.options;
|
|
1341
|
+
const modelOptions = input.modelOptions;
|
|
1342
|
+
const defaults = readModelRuntimeDefaults(modelOptions);
|
|
1343
|
+
const codexInstructions = asString(modelOptions.codexInstructions);
|
|
1344
|
+
if (codexInstructions && asString(options.instructions) === undefined) {
|
|
1345
|
+
options.instructions = codexInstructions;
|
|
1346
|
+
}
|
|
1347
|
+
if (asString(options.reasoningEffort) === undefined && defaults.defaultReasoningEffort) {
|
|
1348
|
+
options.reasoningEffort = defaults.defaultReasoningEffort;
|
|
1349
|
+
}
|
|
1350
|
+
const reasoningEffort = asString(options.reasoningEffort);
|
|
1351
|
+
const hasReasoning = reasoningEffort !== undefined && reasoningEffort !== "none";
|
|
1352
|
+
const currentReasoningSummary = asString(options.reasoningSummary);
|
|
1353
|
+
if (currentReasoningSummary === undefined) {
|
|
1354
|
+
if (input.thinkingSummariesOverride === false) {
|
|
1355
|
+
options.reasoningSummary = "none";
|
|
1356
|
+
}
|
|
1357
|
+
else if (hasReasoning &&
|
|
1358
|
+
(defaults.supportsReasoningSummaries === true || input.thinkingSummariesOverride === true)) {
|
|
1359
|
+
options.reasoningSummary = "auto";
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
if (asString(options.textVerbosity) === undefined &&
|
|
1363
|
+
defaults.defaultVerbosity &&
|
|
1364
|
+
(defaults.supportsVerbosity ?? true)) {
|
|
1365
|
+
options.textVerbosity = defaults.defaultVerbosity;
|
|
1366
|
+
}
|
|
1367
|
+
if (asString(options.applyPatchToolType) === undefined && defaults.applyPatchToolType) {
|
|
1368
|
+
options.applyPatchToolType = defaults.applyPatchToolType;
|
|
1369
|
+
}
|
|
1370
|
+
if (typeof options.parallelToolCalls !== "boolean" && input.modelToolCallCapable !== undefined) {
|
|
1371
|
+
options.parallelToolCalls = input.modelToolCallCapable;
|
|
1372
|
+
}
|
|
1373
|
+
const shouldIncludeReasoning = hasReasoning &&
|
|
1374
|
+
((asString(options.reasoningSummary) !== undefined && asString(options.reasoningSummary) !== "none") ||
|
|
1375
|
+
defaults.supportsReasoningSummaries === true);
|
|
1376
|
+
if (shouldIncludeReasoning) {
|
|
1377
|
+
const include = asStringArray(options.include) ?? [];
|
|
1378
|
+
options.include = mergeUnique([...include, "reasoning.encrypted_content"]);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
function resolvePromptCacheKey(options) {
|
|
1382
|
+
return asString(options.promptCacheKey);
|
|
1383
|
+
}
|
|
1384
|
+
async function sanitizeOutboundRequestIfNeeded(request, enabled) {
|
|
1385
|
+
if (!enabled)
|
|
1386
|
+
return { request, changed: false };
|
|
1387
|
+
const method = request.method.toUpperCase();
|
|
1388
|
+
if (method !== "POST")
|
|
1389
|
+
return { request, changed: false };
|
|
1390
|
+
let payload;
|
|
1391
|
+
try {
|
|
1392
|
+
const raw = await request.clone().text();
|
|
1393
|
+
if (!raw)
|
|
1394
|
+
return { request, changed: false };
|
|
1395
|
+
payload = JSON.parse(raw);
|
|
1396
|
+
}
|
|
1397
|
+
catch {
|
|
1398
|
+
return { request, changed: false };
|
|
1399
|
+
}
|
|
1400
|
+
if (!isRecord(payload))
|
|
1401
|
+
return { request, changed: false };
|
|
1402
|
+
const sanitized = sanitizeRequestPayloadForCompat(payload);
|
|
1403
|
+
if (!sanitized.changed)
|
|
1404
|
+
return { request, changed: false };
|
|
1405
|
+
const headers = new Headers(request.headers);
|
|
1406
|
+
headers.set("content-type", "application/json");
|
|
1407
|
+
const sanitizedRequest = new Request(request.url, {
|
|
1408
|
+
method: request.method,
|
|
1409
|
+
headers,
|
|
1410
|
+
body: JSON.stringify(sanitized.payload),
|
|
1411
|
+
redirect: request.redirect
|
|
1412
|
+
});
|
|
1413
|
+
return { request: sanitizedRequest, changed: true };
|
|
1414
|
+
}
|
|
1415
|
+
export async function CodexAuthPlugin(input, opts = {}) {
|
|
1416
|
+
opts.log?.debug("codex-native init");
|
|
1417
|
+
const spoofMode = opts.spoofMode === "codex" ||
|
|
1418
|
+
opts.spoofMode === "strict"
|
|
1419
|
+
? "codex"
|
|
1420
|
+
: "native";
|
|
1421
|
+
const runtimeMode = opts.mode === "collab" || opts.mode === "codex" || opts.mode === "native"
|
|
1422
|
+
? opts.mode
|
|
1423
|
+
: spoofMode === "codex"
|
|
1424
|
+
? "codex"
|
|
1425
|
+
: "native";
|
|
1426
|
+
const collabModeEnabled = runtimeMode === "collab";
|
|
1427
|
+
const authMode = modeForRuntimeMode(runtimeMode);
|
|
1428
|
+
const resolveCatalogHeaders = () => {
|
|
1429
|
+
const originator = resolveCodexOriginator(spoofMode);
|
|
1430
|
+
return {
|
|
1431
|
+
originator,
|
|
1432
|
+
userAgent: resolveRequestUserAgent(spoofMode, originator),
|
|
1433
|
+
...(spoofMode === "native" ? { openaiBeta: "responses=experimental" } : {})
|
|
1434
|
+
};
|
|
1435
|
+
};
|
|
1436
|
+
const requestSnapshots = createRequestSnapshots({
|
|
1437
|
+
enabled: opts.headerSnapshots === true,
|
|
1438
|
+
log: opts.log
|
|
1439
|
+
});
|
|
1440
|
+
const showToast = async (message, variant = "info", quietMode = false) => {
|
|
1441
|
+
if (quietMode)
|
|
1442
|
+
return;
|
|
1443
|
+
const tui = input.client?.tui;
|
|
1444
|
+
if (!tui || typeof tui.showToast !== "function")
|
|
1445
|
+
return;
|
|
1446
|
+
try {
|
|
1447
|
+
await tui.showToast({ body: { message: formatToastMessage(message), variant } });
|
|
1448
|
+
}
|
|
1449
|
+
catch (error) {
|
|
1450
|
+
opts.log?.debug("toast failed", {
|
|
1451
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
const refreshQuotaSnapshotsForAuthMenu = async () => {
|
|
1456
|
+
const auth = await loadAuthStorage();
|
|
1457
|
+
const snapshotUpdates = {};
|
|
1458
|
+
for (const { mode, domain } of listOpenAIOAuthDomains(auth)) {
|
|
1459
|
+
for (let index = 0; index < domain.accounts.length; index += 1) {
|
|
1460
|
+
const account = domain.accounts[index];
|
|
1461
|
+
if (!account || account.enabled === false)
|
|
1462
|
+
continue;
|
|
1463
|
+
hydrateAccountIdentityFromAccessClaims(account);
|
|
1464
|
+
let accessToken = typeof account.access === "string" && account.access.length > 0 ? account.access : undefined;
|
|
1465
|
+
const now = Date.now();
|
|
1466
|
+
const expired = typeof account.expires === "number" &&
|
|
1467
|
+
Number.isFinite(account.expires) &&
|
|
1468
|
+
account.expires <= now;
|
|
1469
|
+
if ((!accessToken || expired) && account.refresh) {
|
|
1470
|
+
try {
|
|
1471
|
+
await saveAuthStorage(undefined, async (authFile) => {
|
|
1472
|
+
const current = ensureOpenAIOAuthDomain(authFile, mode);
|
|
1473
|
+
const target = current.accounts[index];
|
|
1474
|
+
if (!target || target.enabled === false || !target.refresh)
|
|
1475
|
+
return authFile;
|
|
1476
|
+
const tokens = await refreshAccessToken(target.refresh);
|
|
1477
|
+
const refreshedAt = Date.now();
|
|
1478
|
+
const claims = parseJwtClaims(tokens.id_token ?? tokens.access_token);
|
|
1479
|
+
target.refresh = tokens.refresh_token;
|
|
1480
|
+
target.access = tokens.access_token;
|
|
1481
|
+
target.expires = refreshedAt + (tokens.expires_in ?? 3600) * 1000;
|
|
1482
|
+
target.accountId = extractAccountId(tokens) || target.accountId;
|
|
1483
|
+
target.email = extractEmailFromClaims(claims) || target.email;
|
|
1484
|
+
target.plan = extractPlanFromClaims(claims) || target.plan;
|
|
1485
|
+
target.lastUsed = refreshedAt;
|
|
1486
|
+
hydrateAccountIdentityFromAccessClaims(target);
|
|
1487
|
+
account.refresh = target.refresh;
|
|
1488
|
+
account.access = target.access;
|
|
1489
|
+
account.expires = target.expires;
|
|
1490
|
+
account.accountId = target.accountId;
|
|
1491
|
+
account.email = target.email;
|
|
1492
|
+
account.plan = target.plan;
|
|
1493
|
+
account.identityKey = target.identityKey;
|
|
1494
|
+
accessToken = target.access;
|
|
1495
|
+
return authFile;
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
catch (error) {
|
|
1499
|
+
opts.log?.debug("quota check refresh failed", {
|
|
1500
|
+
index,
|
|
1501
|
+
mode,
|
|
1502
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
if (!accessToken)
|
|
1507
|
+
continue;
|
|
1508
|
+
if (!account.identityKey) {
|
|
1509
|
+
hydrateAccountIdentityFromAccessClaims(account);
|
|
1510
|
+
}
|
|
1511
|
+
if (!account.identityKey)
|
|
1512
|
+
continue;
|
|
1513
|
+
const snapshot = await fetchQuotaSnapshotFromBackend({
|
|
1514
|
+
accessToken,
|
|
1515
|
+
accountId: account.accountId,
|
|
1516
|
+
now: Date.now(),
|
|
1517
|
+
modelFamily: "gpt-5.3-codex",
|
|
1518
|
+
userAgent: resolveRequestUserAgent(spoofMode, resolveCodexOriginator(spoofMode)),
|
|
1519
|
+
log: opts.log
|
|
1520
|
+
});
|
|
1521
|
+
if (!snapshot)
|
|
1522
|
+
continue;
|
|
1523
|
+
snapshotUpdates[account.identityKey] = snapshot;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
if (Object.keys(snapshotUpdates).length === 0)
|
|
1527
|
+
return;
|
|
1528
|
+
await saveSnapshots(defaultSnapshotsPath(), (current) => ({
|
|
1529
|
+
...current,
|
|
1530
|
+
...snapshotUpdates
|
|
1531
|
+
}));
|
|
1532
|
+
};
|
|
1533
|
+
const runInteractiveAuthMenu = async (options) => {
|
|
1534
|
+
while (true) {
|
|
1535
|
+
const auth = await loadAuthStorage();
|
|
1536
|
+
const nativeDomain = getOpenAIOAuthDomain(auth, "native");
|
|
1537
|
+
const codexDomain = getOpenAIOAuthDomain(auth, "codex");
|
|
1538
|
+
const menuAccounts = buildAuthMenuAccounts({
|
|
1539
|
+
native: nativeDomain,
|
|
1540
|
+
codex: codexDomain,
|
|
1541
|
+
activeMode: authMode
|
|
1542
|
+
});
|
|
1543
|
+
const allowTransfer = await shouldOfferLegacyTransfer();
|
|
1544
|
+
const result = await runAuthMenuOnce({
|
|
1545
|
+
accounts: menuAccounts,
|
|
1546
|
+
allowTransfer,
|
|
1547
|
+
input: process.stdin,
|
|
1548
|
+
output: process.stdout,
|
|
1549
|
+
handlers: {
|
|
1550
|
+
onCheckQuotas: async () => {
|
|
1551
|
+
await refreshQuotaSnapshotsForAuthMenu();
|
|
1552
|
+
const report = await toolOutputForStatus();
|
|
1553
|
+
process.stdout.write(`\n${report}\n\n`);
|
|
1554
|
+
},
|
|
1555
|
+
onConfigureModels: async () => {
|
|
1556
|
+
process.stdout.write("\nConfigure provider models in opencode.json and runtime flags in codex-config.json.\n\n");
|
|
1557
|
+
},
|
|
1558
|
+
onTransfer: async () => {
|
|
1559
|
+
const transfer = await importLegacyInstallData();
|
|
1560
|
+
let total = transfer.imported;
|
|
1561
|
+
let hydrated = 0;
|
|
1562
|
+
let refreshed = 0;
|
|
1563
|
+
await saveAuthStorage(undefined, async (authFile) => {
|
|
1564
|
+
for (const mode of ["native", "codex"]) {
|
|
1565
|
+
const domain = getOpenAIOAuthDomain(authFile, mode);
|
|
1566
|
+
if (!domain)
|
|
1567
|
+
continue;
|
|
1568
|
+
for (const account of domain.accounts) {
|
|
1569
|
+
const hadIdentity = Boolean(buildIdentityKey(account));
|
|
1570
|
+
hydrateAccountIdentityFromAccessClaims(account);
|
|
1571
|
+
const hasIdentityAfterClaims = Boolean(buildIdentityKey(account));
|
|
1572
|
+
if (!hadIdentity && hasIdentityAfterClaims)
|
|
1573
|
+
hydrated += 1;
|
|
1574
|
+
if (hasIdentityAfterClaims || account.enabled === false || !account.refresh) {
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
try {
|
|
1578
|
+
const tokens = await refreshAccessToken(account.refresh);
|
|
1579
|
+
refreshed += 1;
|
|
1580
|
+
const now = Date.now();
|
|
1581
|
+
const claims = parseJwtClaims(tokens.id_token ?? tokens.access_token);
|
|
1582
|
+
account.refresh = tokens.refresh_token;
|
|
1583
|
+
account.access = tokens.access_token;
|
|
1584
|
+
account.expires = now + (tokens.expires_in ?? 3600) * 1000;
|
|
1585
|
+
account.accountId = extractAccountId(tokens) || account.accountId;
|
|
1586
|
+
account.email = extractEmailFromClaims(claims) || account.email;
|
|
1587
|
+
account.plan = extractPlanFromClaims(claims) || account.plan;
|
|
1588
|
+
account.lastUsed = now;
|
|
1589
|
+
hydrateAccountIdentityFromAccessClaims(account);
|
|
1590
|
+
if (!hadIdentity && buildIdentityKey(account))
|
|
1591
|
+
hydrated += 1;
|
|
1592
|
+
}
|
|
1593
|
+
catch {
|
|
1594
|
+
// best effort per-account hydration
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
return authFile;
|
|
1599
|
+
});
|
|
1600
|
+
process.stdout.write(`\nTransfer complete: imported ${total} account(s). Hydrated ${hydrated} account(s)` +
|
|
1601
|
+
`${refreshed > 0 ? `, refreshed ${refreshed} token(s)` : ""}.\n\n`);
|
|
1602
|
+
},
|
|
1603
|
+
onDeleteAll: async (scope) => {
|
|
1604
|
+
await saveAuthStorage(undefined, (authFile) => {
|
|
1605
|
+
const targets = scope === "both" ? ["native", "codex"] : [scope];
|
|
1606
|
+
for (const targetMode of targets) {
|
|
1607
|
+
const domain = ensureOpenAIOAuthDomain(authFile, targetMode);
|
|
1608
|
+
domain.accounts = [];
|
|
1609
|
+
domain.activeIdentityKey = undefined;
|
|
1610
|
+
}
|
|
1611
|
+
return authFile;
|
|
1612
|
+
});
|
|
1613
|
+
const deletedLabel = scope === "both"
|
|
1614
|
+
? "Deleted all OpenAI accounts."
|
|
1615
|
+
: `Deleted ${scope === "native" ? "Native" : "Codex"} auth from all accounts.`;
|
|
1616
|
+
process.stdout.write(`\n${deletedLabel}\n\n`);
|
|
1617
|
+
},
|
|
1618
|
+
onToggleAccount: async (account) => {
|
|
1619
|
+
await saveAuthStorage(undefined, (authFile) => {
|
|
1620
|
+
const authTypes = account.authTypes && account.authTypes.length > 0
|
|
1621
|
+
? [...account.authTypes]
|
|
1622
|
+
: ["native"];
|
|
1623
|
+
for (const mode of authTypes) {
|
|
1624
|
+
const domain = getOpenAIOAuthDomain(authFile, mode);
|
|
1625
|
+
if (!domain)
|
|
1626
|
+
continue;
|
|
1627
|
+
const idx = findDomainAccountIndex(domain, account);
|
|
1628
|
+
if (idx < 0)
|
|
1629
|
+
continue;
|
|
1630
|
+
const target = domain.accounts[idx];
|
|
1631
|
+
if (!target)
|
|
1632
|
+
continue;
|
|
1633
|
+
target.enabled = target.enabled === false;
|
|
1634
|
+
reconcileActiveIdentityKey(domain);
|
|
1635
|
+
}
|
|
1636
|
+
return authFile;
|
|
1637
|
+
});
|
|
1638
|
+
process.stdout.write("\nUpdated account status.\n\n");
|
|
1639
|
+
},
|
|
1640
|
+
onRefreshAccount: async (account) => {
|
|
1641
|
+
let refreshed = false;
|
|
1642
|
+
try {
|
|
1643
|
+
await saveAuthStorage(undefined, async (authFile) => {
|
|
1644
|
+
const preferred = [
|
|
1645
|
+
authMode,
|
|
1646
|
+
...(account.authTypes ?? []).filter((mode) => mode !== authMode)
|
|
1647
|
+
];
|
|
1648
|
+
for (const mode of preferred) {
|
|
1649
|
+
const domain = getOpenAIOAuthDomain(authFile, mode);
|
|
1650
|
+
if (!domain)
|
|
1651
|
+
continue;
|
|
1652
|
+
const idx = findDomainAccountIndex(domain, account);
|
|
1653
|
+
if (idx < 0)
|
|
1654
|
+
continue;
|
|
1655
|
+
const target = domain.accounts[idx];
|
|
1656
|
+
if (!target || target.enabled === false || !target.refresh)
|
|
1657
|
+
continue;
|
|
1658
|
+
const tokens = await refreshAccessToken(target.refresh);
|
|
1659
|
+
const now = Date.now();
|
|
1660
|
+
const claims = parseJwtClaims(tokens.id_token ?? tokens.access_token);
|
|
1661
|
+
target.refresh = tokens.refresh_token;
|
|
1662
|
+
target.access = tokens.access_token;
|
|
1663
|
+
target.expires = now + (tokens.expires_in ?? 3600) * 1000;
|
|
1664
|
+
target.accountId = extractAccountId(tokens) || target.accountId;
|
|
1665
|
+
target.email = extractEmailFromClaims(claims) || target.email;
|
|
1666
|
+
target.plan = extractPlanFromClaims(claims) || target.plan;
|
|
1667
|
+
target.lastUsed = now;
|
|
1668
|
+
ensureAccountAuthTypes(target);
|
|
1669
|
+
ensureIdentityKey(target);
|
|
1670
|
+
if (target.identityKey)
|
|
1671
|
+
domain.activeIdentityKey = target.identityKey;
|
|
1672
|
+
refreshed = true;
|
|
1673
|
+
break;
|
|
1674
|
+
}
|
|
1675
|
+
return authFile;
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
catch {
|
|
1679
|
+
refreshed = false;
|
|
1680
|
+
}
|
|
1681
|
+
process.stdout.write(refreshed
|
|
1682
|
+
? "\nAccount refreshed successfully.\n\n"
|
|
1683
|
+
: "\nAccount refresh failed. Run login to reauthenticate.\n\n");
|
|
1684
|
+
},
|
|
1685
|
+
onDeleteAccount: async (account, scope) => {
|
|
1686
|
+
await saveAuthStorage(undefined, (authFile) => {
|
|
1687
|
+
const targets = scope === "both" ? ["native", "codex"] : [scope];
|
|
1688
|
+
for (const mode of targets) {
|
|
1689
|
+
const domain = getOpenAIOAuthDomain(authFile, mode);
|
|
1690
|
+
if (!domain)
|
|
1691
|
+
continue;
|
|
1692
|
+
const idx = findDomainAccountIndex(domain, account);
|
|
1693
|
+
if (idx < 0)
|
|
1694
|
+
continue;
|
|
1695
|
+
domain.accounts.splice(idx, 1);
|
|
1696
|
+
reconcileActiveIdentityKey(domain);
|
|
1697
|
+
}
|
|
1698
|
+
return authFile;
|
|
1699
|
+
});
|
|
1700
|
+
const deletedLabel = scope === "both"
|
|
1701
|
+
? "Deleted account."
|
|
1702
|
+
: `Deleted ${scope === "native" ? "Native" : "Codex"} auth from account.`;
|
|
1703
|
+
process.stdout.write(`\n${deletedLabel}\n\n`);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1707
|
+
if (result === "add")
|
|
1708
|
+
return "add";
|
|
1709
|
+
if (result === "exit") {
|
|
1710
|
+
if (options.allowExit)
|
|
1711
|
+
return "exit";
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
return {
|
|
1717
|
+
auth: {
|
|
1718
|
+
provider: "openai",
|
|
1719
|
+
async loader(getAuth, provider) {
|
|
1720
|
+
const auth = await getAuth();
|
|
1721
|
+
let hasOAuth = auth.type === "oauth";
|
|
1722
|
+
if (!hasOAuth) {
|
|
1723
|
+
try {
|
|
1724
|
+
const stored = await loadAuthStorage();
|
|
1725
|
+
hasOAuth = stored.openai?.type === "oauth";
|
|
1726
|
+
}
|
|
1727
|
+
catch {
|
|
1728
|
+
hasOAuth = false;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
if (!hasOAuth)
|
|
1732
|
+
return {};
|
|
1733
|
+
const catalogAuth = await selectCatalogAuthCandidate(authMode, opts.pidOffsetEnabled === true);
|
|
1734
|
+
const catalogModels = await getCodexModelCatalog({
|
|
1735
|
+
accessToken: catalogAuth.accessToken,
|
|
1736
|
+
accountId: catalogAuth.accountId,
|
|
1737
|
+
...resolveCatalogHeaders(),
|
|
1738
|
+
onEvent: (event) => opts.log?.debug("codex model catalog", event)
|
|
1739
|
+
});
|
|
1740
|
+
applyCodexCatalogToProviderModels({
|
|
1741
|
+
providerModels: provider.models,
|
|
1742
|
+
catalogModels,
|
|
1743
|
+
fallbackModels: STATIC_FALLBACK_MODELS,
|
|
1744
|
+
personality: opts.personality
|
|
1745
|
+
});
|
|
1746
|
+
const orchestratorState = createFetchOrchestratorState();
|
|
1747
|
+
const stickySessionState = createStickySessionState();
|
|
1748
|
+
return {
|
|
1749
|
+
apiKey: OAUTH_DUMMY_KEY,
|
|
1750
|
+
async fetch(requestInput, init) {
|
|
1751
|
+
const baseRequest = new Request(requestInput, init);
|
|
1752
|
+
const outbound = new Request(rewriteUrl(baseRequest), baseRequest);
|
|
1753
|
+
const inboundOriginator = outbound.headers.get("originator")?.trim();
|
|
1754
|
+
const outboundOriginator = inboundOriginator === "codex_exec" || inboundOriginator === "codex_cli_rs"
|
|
1755
|
+
? inboundOriginator
|
|
1756
|
+
: resolveCodexOriginator(spoofMode);
|
|
1757
|
+
outbound.headers.set("originator", outboundOriginator);
|
|
1758
|
+
outbound.headers.set("user-agent", resolveRequestUserAgent(spoofMode, outboundOriginator));
|
|
1759
|
+
const collaborationModeKind = outbound.headers.get(INTERNAL_COLLABORATION_MODE_HEADER);
|
|
1760
|
+
if (collaborationModeKind) {
|
|
1761
|
+
outbound.headers.delete(INTERNAL_COLLABORATION_MODE_HEADER);
|
|
1762
|
+
}
|
|
1763
|
+
let selectedIdentityKey;
|
|
1764
|
+
await requestSnapshots.captureRequest("before-auth", outbound, {
|
|
1765
|
+
spoofMode,
|
|
1766
|
+
...(collaborationModeKind ? { collaborationModeKind } : {})
|
|
1767
|
+
});
|
|
1768
|
+
const orchestrator = new FetchOrchestrator({
|
|
1769
|
+
acquireAuth: async (context) => {
|
|
1770
|
+
let access;
|
|
1771
|
+
let accountId;
|
|
1772
|
+
let identityKey;
|
|
1773
|
+
let accountLabel;
|
|
1774
|
+
let email;
|
|
1775
|
+
let plan;
|
|
1776
|
+
try {
|
|
1777
|
+
await saveAuthStorage(undefined, async (authFile) => {
|
|
1778
|
+
const now = Date.now();
|
|
1779
|
+
const openai = authFile.openai;
|
|
1780
|
+
if (!openai || openai.type !== "oauth") {
|
|
1781
|
+
throw new PluginFatalError({
|
|
1782
|
+
message: "Not authenticated with OpenAI. Run `opencode auth login`.",
|
|
1783
|
+
status: 401,
|
|
1784
|
+
type: "oauth_not_configured",
|
|
1785
|
+
param: "auth"
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
const domain = ensureOpenAIOAuthDomain(authFile, authMode);
|
|
1789
|
+
if (domain.accounts.length === 0) {
|
|
1790
|
+
throw new PluginFatalError({
|
|
1791
|
+
message: `No OpenAI ${authMode} accounts configured. Run \`opencode auth login\`.`,
|
|
1792
|
+
status: 401,
|
|
1793
|
+
type: "no_accounts_configured",
|
|
1794
|
+
param: "accounts"
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
const enabled = domain.accounts.filter((account) => account.enabled !== false);
|
|
1798
|
+
if (enabled.length === 0) {
|
|
1799
|
+
throw new PluginFatalError({
|
|
1800
|
+
message: `No enabled OpenAI ${authMode} accounts available. Enable an account or run \`opencode auth login\`.`,
|
|
1801
|
+
status: 403,
|
|
1802
|
+
type: "no_enabled_accounts",
|
|
1803
|
+
param: "accounts"
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
const attempted = new Set();
|
|
1807
|
+
let sawInvalidGrant = false;
|
|
1808
|
+
let sawRefreshFailure = false;
|
|
1809
|
+
let sawMissingRefresh = false;
|
|
1810
|
+
opts.log?.debug("rotation begin", {
|
|
1811
|
+
strategy: domain.strategy ?? "sticky",
|
|
1812
|
+
activeIdentityKey: domain.activeIdentityKey,
|
|
1813
|
+
totalAccounts: domain.accounts.length,
|
|
1814
|
+
enabledAccounts: enabled.length,
|
|
1815
|
+
mode: authMode,
|
|
1816
|
+
sessionKey: context?.sessionKey ?? null
|
|
1817
|
+
});
|
|
1818
|
+
while (attempted.size < domain.accounts.length) {
|
|
1819
|
+
const selected = selectAccount({
|
|
1820
|
+
accounts: domain.accounts,
|
|
1821
|
+
strategy: domain.strategy,
|
|
1822
|
+
activeIdentityKey: domain.activeIdentityKey,
|
|
1823
|
+
now,
|
|
1824
|
+
stickyPidOffset: opts.pidOffsetEnabled === true,
|
|
1825
|
+
stickySessionKey: context?.sessionKey,
|
|
1826
|
+
stickySessionState,
|
|
1827
|
+
onDebug: (event) => {
|
|
1828
|
+
opts.log?.debug("rotation decision", event);
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
if (!selected) {
|
|
1832
|
+
opts.log?.debug("rotation stop: no selectable account", {
|
|
1833
|
+
attempted: attempted.size,
|
|
1834
|
+
totalAccounts: domain.accounts.length
|
|
1835
|
+
});
|
|
1836
|
+
break;
|
|
1837
|
+
}
|
|
1838
|
+
const selectedIndex = domain.accounts.findIndex((account) => account === selected);
|
|
1839
|
+
const attemptKey = selected.identityKey ??
|
|
1840
|
+
selected.refresh ??
|
|
1841
|
+
(selectedIndex >= 0 ? `idx:${selectedIndex}` : `idx:${attempted.size}`);
|
|
1842
|
+
if (attempted.has(attemptKey)) {
|
|
1843
|
+
opts.log?.debug("rotation stop: duplicate attempt key", {
|
|
1844
|
+
attemptKey,
|
|
1845
|
+
selectedIdentityKey: selected.identityKey,
|
|
1846
|
+
selectedIndex
|
|
1847
|
+
});
|
|
1848
|
+
break;
|
|
1849
|
+
}
|
|
1850
|
+
attempted.add(attemptKey);
|
|
1851
|
+
opts.log?.debug("rotation candidate selected", {
|
|
1852
|
+
attemptKey,
|
|
1853
|
+
selectedIdentityKey: selected.identityKey,
|
|
1854
|
+
selectedIndex,
|
|
1855
|
+
selectedEnabled: selected.enabled !== false,
|
|
1856
|
+
selectedCooldownUntil: selected.cooldownUntil ?? null,
|
|
1857
|
+
selectedExpires: selected.expires ?? null
|
|
1858
|
+
});
|
|
1859
|
+
accountLabel = formatAccountLabel(selected, selectedIndex >= 0 ? selectedIndex : 0);
|
|
1860
|
+
email = selected.email;
|
|
1861
|
+
plan = selected.plan;
|
|
1862
|
+
if (selected.access && selected.expires && selected.expires > now) {
|
|
1863
|
+
selected.lastUsed = now;
|
|
1864
|
+
access = selected.access;
|
|
1865
|
+
accountId = selected.accountId;
|
|
1866
|
+
identityKey = selected.identityKey;
|
|
1867
|
+
if (selected.identityKey)
|
|
1868
|
+
domain.activeIdentityKey = selected.identityKey;
|
|
1869
|
+
return authFile;
|
|
1870
|
+
}
|
|
1871
|
+
if (!selected.refresh) {
|
|
1872
|
+
sawMissingRefresh = true;
|
|
1873
|
+
selected.cooldownUntil = now + AUTH_REFRESH_FAILURE_COOLDOWN_MS;
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
let tokens;
|
|
1877
|
+
try {
|
|
1878
|
+
tokens = await refreshAccessToken(selected.refresh);
|
|
1879
|
+
}
|
|
1880
|
+
catch (error) {
|
|
1881
|
+
if (isOAuthTokenRefreshError(error) &&
|
|
1882
|
+
error.oauthCode?.toLowerCase() === "invalid_grant") {
|
|
1883
|
+
sawInvalidGrant = true;
|
|
1884
|
+
selected.enabled = false;
|
|
1885
|
+
delete selected.cooldownUntil;
|
|
1886
|
+
delete selected.refreshLeaseUntil;
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
sawRefreshFailure = true;
|
|
1890
|
+
selected.cooldownUntil = now + AUTH_REFRESH_FAILURE_COOLDOWN_MS;
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
const expires = now + (tokens.expires_in ?? 3600) * 1000;
|
|
1894
|
+
const refreshedAccountId = extractAccountId(tokens) || selected.accountId;
|
|
1895
|
+
const claims = parseJwtClaims(tokens.id_token ?? tokens.access_token);
|
|
1896
|
+
selected.refresh = tokens.refresh_token;
|
|
1897
|
+
selected.access = tokens.access_token;
|
|
1898
|
+
selected.expires = expires;
|
|
1899
|
+
selected.accountId = refreshedAccountId;
|
|
1900
|
+
if (claims?.email)
|
|
1901
|
+
selected.email = normalizeEmail(claims.email);
|
|
1902
|
+
if (claims?.plan)
|
|
1903
|
+
selected.plan = normalizePlan(claims.plan);
|
|
1904
|
+
ensureIdentityKey(selected);
|
|
1905
|
+
selected.lastUsed = now;
|
|
1906
|
+
accountLabel = formatAccountLabel(selected, selectedIndex >= 0 ? selectedIndex : 0);
|
|
1907
|
+
email = selected.email;
|
|
1908
|
+
plan = selected.plan;
|
|
1909
|
+
identityKey = selected.identityKey;
|
|
1910
|
+
if (selected.identityKey)
|
|
1911
|
+
domain.activeIdentityKey = selected.identityKey;
|
|
1912
|
+
access = selected.access;
|
|
1913
|
+
accountId = selected.accountId;
|
|
1914
|
+
return authFile;
|
|
1915
|
+
}
|
|
1916
|
+
const enabledAfterAttempts = domain.accounts.filter((account) => account.enabled !== false);
|
|
1917
|
+
if (enabledAfterAttempts.length === 0 && sawInvalidGrant) {
|
|
1918
|
+
throw new PluginFatalError({
|
|
1919
|
+
message: "All enabled OpenAI refresh tokens were rejected (invalid_grant). Run `opencode auth login` to reauthenticate.",
|
|
1920
|
+
status: 401,
|
|
1921
|
+
type: "refresh_invalid_grant",
|
|
1922
|
+
param: "auth"
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
const nextAvailableAt = enabledAfterAttempts.reduce((current, account) => {
|
|
1926
|
+
const cooldownUntil = account.cooldownUntil;
|
|
1927
|
+
if (typeof cooldownUntil !== "number" || cooldownUntil <= now)
|
|
1928
|
+
return current;
|
|
1929
|
+
if (current === undefined || cooldownUntil < current)
|
|
1930
|
+
return cooldownUntil;
|
|
1931
|
+
return current;
|
|
1932
|
+
}, undefined);
|
|
1933
|
+
if (nextAvailableAt !== undefined) {
|
|
1934
|
+
const waitMs = Math.max(0, nextAvailableAt - now);
|
|
1935
|
+
throw new PluginFatalError({
|
|
1936
|
+
message: `All enabled OpenAI accounts are cooling down. Try again in ${formatWaitTime(waitMs)} or run \`opencode auth login\`.`,
|
|
1937
|
+
status: 429,
|
|
1938
|
+
type: "all_accounts_cooling_down",
|
|
1939
|
+
param: "accounts"
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
if (sawInvalidGrant) {
|
|
1943
|
+
throw new PluginFatalError({
|
|
1944
|
+
message: "OpenAI refresh token was rejected (invalid_grant). Run `opencode auth login` to reauthenticate this account.",
|
|
1945
|
+
status: 401,
|
|
1946
|
+
type: "refresh_invalid_grant",
|
|
1947
|
+
param: "auth"
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
if (sawMissingRefresh) {
|
|
1951
|
+
throw new PluginFatalError({
|
|
1952
|
+
message: "Selected OpenAI account is missing a refresh token. Run `opencode auth login` to reauthenticate.",
|
|
1953
|
+
status: 401,
|
|
1954
|
+
type: "missing_refresh_token",
|
|
1955
|
+
param: "accounts"
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
if (sawRefreshFailure) {
|
|
1959
|
+
throw new PluginFatalError({
|
|
1960
|
+
message: "Failed to refresh OpenAI access token. Run `opencode auth login` and try again.",
|
|
1961
|
+
status: 401,
|
|
1962
|
+
type: "refresh_failed",
|
|
1963
|
+
param: "auth"
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
throw new PluginFatalError({
|
|
1967
|
+
message: `No enabled OpenAI ${authMode} accounts available. Enable an account or run \`opencode auth login\`.`,
|
|
1968
|
+
status: 403,
|
|
1969
|
+
type: "no_enabled_accounts",
|
|
1970
|
+
param: "accounts"
|
|
1971
|
+
});
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
catch (error) {
|
|
1975
|
+
if (isPluginFatalError(error))
|
|
1976
|
+
throw error;
|
|
1977
|
+
throw new PluginFatalError({
|
|
1978
|
+
message: "Unable to access OpenAI auth storage. Check plugin configuration and run `opencode auth login` if needed.",
|
|
1979
|
+
status: 500,
|
|
1980
|
+
type: "auth_storage_error",
|
|
1981
|
+
param: "auth"
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
if (!access) {
|
|
1985
|
+
throw new PluginFatalError({
|
|
1986
|
+
message: "No valid OpenAI access token available. Run `opencode auth login`.",
|
|
1987
|
+
status: 401,
|
|
1988
|
+
type: "no_valid_access_token",
|
|
1989
|
+
param: "auth"
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
void getCodexModelCatalog({
|
|
1993
|
+
accessToken: access,
|
|
1994
|
+
accountId,
|
|
1995
|
+
...resolveCatalogHeaders(),
|
|
1996
|
+
onEvent: (event) => opts.log?.debug("codex model catalog", event)
|
|
1997
|
+
}).catch(() => { });
|
|
1998
|
+
selectedIdentityKey = identityKey;
|
|
1999
|
+
return { access, accountId, identityKey, accountLabel, email, plan };
|
|
2000
|
+
},
|
|
2001
|
+
setCooldown: async (idKey, cooldownUntil) => {
|
|
2002
|
+
await setAccountCooldown(undefined, idKey, cooldownUntil, authMode);
|
|
2003
|
+
},
|
|
2004
|
+
quietMode: opts.quietMode === true,
|
|
2005
|
+
state: orchestratorState,
|
|
2006
|
+
showToast,
|
|
2007
|
+
onAttemptRequest: async ({ attempt, maxAttempts, request, auth, sessionKey }) => {
|
|
2008
|
+
await requestSnapshots.captureRequest("outbound-attempt", request, {
|
|
2009
|
+
attempt: attempt + 1,
|
|
2010
|
+
maxAttempts,
|
|
2011
|
+
sessionKey,
|
|
2012
|
+
identityKey: auth.identityKey,
|
|
2013
|
+
accountLabel: auth.accountLabel,
|
|
2014
|
+
...(collaborationModeKind ? { collaborationModeKind } : {})
|
|
2015
|
+
});
|
|
2016
|
+
},
|
|
2017
|
+
onAttemptResponse: async ({ attempt, maxAttempts, response, auth, sessionKey }) => {
|
|
2018
|
+
await requestSnapshots.captureResponse("outbound-response", response, {
|
|
2019
|
+
attempt: attempt + 1,
|
|
2020
|
+
maxAttempts,
|
|
2021
|
+
sessionKey,
|
|
2022
|
+
identityKey: auth.identityKey,
|
|
2023
|
+
accountLabel: auth.accountLabel,
|
|
2024
|
+
...(collaborationModeKind ? { collaborationModeKind } : {})
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
});
|
|
2028
|
+
const sanitizedOutbound = await sanitizeOutboundRequestIfNeeded(outbound, opts.compatInputSanitizer === true);
|
|
2029
|
+
if (sanitizedOutbound.changed) {
|
|
2030
|
+
opts.log?.debug("compat input sanitizer applied", { mode: spoofMode });
|
|
2031
|
+
}
|
|
2032
|
+
await requestSnapshots.captureRequest("after-sanitize", sanitizedOutbound.request, {
|
|
2033
|
+
spoofMode,
|
|
2034
|
+
sanitized: sanitizedOutbound.changed,
|
|
2035
|
+
...(collaborationModeKind ? { collaborationModeKind } : {})
|
|
2036
|
+
});
|
|
2037
|
+
let response;
|
|
2038
|
+
try {
|
|
2039
|
+
response = await orchestrator.execute(sanitizedOutbound.request);
|
|
2040
|
+
}
|
|
2041
|
+
catch (error) {
|
|
2042
|
+
if (isPluginFatalError(error)) {
|
|
2043
|
+
opts.log?.debug("fatal auth/error response", {
|
|
2044
|
+
type: error.type,
|
|
2045
|
+
status: error.status
|
|
2046
|
+
});
|
|
2047
|
+
return toSyntheticErrorResponse(error);
|
|
2048
|
+
}
|
|
2049
|
+
opts.log?.debug("unexpected fetch failure", {
|
|
2050
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2051
|
+
});
|
|
2052
|
+
return toSyntheticErrorResponse(new PluginFatalError({
|
|
2053
|
+
message: "OpenAI request failed unexpectedly. Retry once, and if it persists run `opencode auth login`.",
|
|
2054
|
+
status: 502,
|
|
2055
|
+
type: "plugin_fetch_failed",
|
|
2056
|
+
param: "request"
|
|
2057
|
+
}));
|
|
2058
|
+
}
|
|
2059
|
+
if (selectedIdentityKey) {
|
|
2060
|
+
const headers = {};
|
|
2061
|
+
response.headers.forEach((value, key) => {
|
|
2062
|
+
headers[key.toLowerCase()] = value;
|
|
2063
|
+
});
|
|
2064
|
+
const status = new CodexStatus();
|
|
2065
|
+
const snapshot = status.parseFromHeaders({
|
|
2066
|
+
now: Date.now(),
|
|
2067
|
+
modelFamily: "codex",
|
|
2068
|
+
headers
|
|
2069
|
+
});
|
|
2070
|
+
if (snapshot.limits.length > 0) {
|
|
2071
|
+
void saveSnapshots(defaultSnapshotsPath(), (current) => ({
|
|
2072
|
+
...current,
|
|
2073
|
+
[selectedIdentityKey]: snapshot
|
|
2074
|
+
})).catch(() => { });
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
return response;
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
},
|
|
2081
|
+
methods: [
|
|
2082
|
+
{
|
|
2083
|
+
label: "ChatGPT Pro/Plus (browser)",
|
|
2084
|
+
type: "oauth",
|
|
2085
|
+
authorize: async (inputs) => {
|
|
2086
|
+
const toOAuthSuccess = (tokens) => ({
|
|
2087
|
+
type: "success",
|
|
2088
|
+
refresh: tokens.refresh_token,
|
|
2089
|
+
access: tokens.access_token,
|
|
2090
|
+
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
|
2091
|
+
accountId: extractAccountId(tokens)
|
|
2092
|
+
});
|
|
2093
|
+
const runSingleBrowserOAuthInline = async () => {
|
|
2094
|
+
const { redirectUri } = await startOAuthServer();
|
|
2095
|
+
const pkce = await generatePKCE();
|
|
2096
|
+
const state = generateState();
|
|
2097
|
+
const authUrl = buildAuthorizeUrl(redirectUri, pkce, state, "codex_cli_rs");
|
|
2098
|
+
const callbackPromise = waitForOAuthCallback(pkce, state, authMode);
|
|
2099
|
+
void tryOpenUrlInBrowser(authUrl, opts.log);
|
|
2100
|
+
process.stdout.write(`\nGo to: ${authUrl}\n`);
|
|
2101
|
+
process.stdout.write("Complete authorization in your browser. This window will close automatically.\n");
|
|
2102
|
+
let authFailed = false;
|
|
2103
|
+
try {
|
|
2104
|
+
const tokens = await callbackPromise;
|
|
2105
|
+
await persistOAuthTokens(tokens);
|
|
2106
|
+
process.stdout.write("\nAccount added.\n\n");
|
|
2107
|
+
return tokens;
|
|
2108
|
+
}
|
|
2109
|
+
catch (error) {
|
|
2110
|
+
authFailed = true;
|
|
2111
|
+
const reason = error instanceof Error ? error.message : "Authorization failed";
|
|
2112
|
+
process.stdout.write(`\nAuthorization failed: ${reason}\n\n`);
|
|
2113
|
+
return null;
|
|
2114
|
+
}
|
|
2115
|
+
finally {
|
|
2116
|
+
pendingOAuth = undefined;
|
|
2117
|
+
scheduleOAuthServerStop(authFailed ? OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS : OAUTH_SERVER_SHUTDOWN_GRACE_MS, authFailed ? "error" : "success");
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
const runInteractiveBrowserAuthLoop = async () => {
|
|
2121
|
+
let lastAddedTokens;
|
|
2122
|
+
while (true) {
|
|
2123
|
+
const menuResult = await runInteractiveAuthMenu({ allowExit: true });
|
|
2124
|
+
if (menuResult === "exit") {
|
|
2125
|
+
if (!lastAddedTokens) {
|
|
2126
|
+
return {
|
|
2127
|
+
url: "",
|
|
2128
|
+
method: "auto",
|
|
2129
|
+
instructions: "Login cancelled.",
|
|
2130
|
+
callback: async () => ({ type: "failed" })
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
const latest = lastAddedTokens;
|
|
2134
|
+
return {
|
|
2135
|
+
url: "",
|
|
2136
|
+
method: "auto",
|
|
2137
|
+
instructions: "",
|
|
2138
|
+
callback: async () => toOAuthSuccess(latest)
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
const tokens = await runSingleBrowserOAuthInline();
|
|
2142
|
+
if (tokens) {
|
|
2143
|
+
lastAddedTokens = tokens;
|
|
2144
|
+
continue;
|
|
2145
|
+
}
|
|
2146
|
+
return {
|
|
2147
|
+
url: "",
|
|
2148
|
+
method: "auto",
|
|
2149
|
+
instructions: "Authorization failed.",
|
|
2150
|
+
callback: async () => ({ type: "failed" })
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
};
|
|
2154
|
+
if (inputs &&
|
|
2155
|
+
process.env.OPENCODE_NO_BROWSER !== "1" &&
|
|
2156
|
+
process.stdin.isTTY &&
|
|
2157
|
+
process.stdout.isTTY) {
|
|
2158
|
+
return runInteractiveBrowserAuthLoop();
|
|
2159
|
+
}
|
|
2160
|
+
const { redirectUri } = await startOAuthServer();
|
|
2161
|
+
const pkce = await generatePKCE();
|
|
2162
|
+
const state = generateState();
|
|
2163
|
+
const authUrl = buildAuthorizeUrl(redirectUri, pkce, state, "codex_cli_rs");
|
|
2164
|
+
const callbackPromise = waitForOAuthCallback(pkce, state, authMode);
|
|
2165
|
+
void tryOpenUrlInBrowser(authUrl, opts.log);
|
|
2166
|
+
return {
|
|
2167
|
+
url: authUrl,
|
|
2168
|
+
instructions: "Complete authorization in your browser. If you close the tab early, cancel (Ctrl+C) and retry.",
|
|
2169
|
+
method: "auto",
|
|
2170
|
+
callback: async () => {
|
|
2171
|
+
let authFailed = false;
|
|
2172
|
+
try {
|
|
2173
|
+
const tokens = await callbackPromise;
|
|
2174
|
+
await persistOAuthTokens(tokens);
|
|
2175
|
+
return toOAuthSuccess(tokens);
|
|
2176
|
+
}
|
|
2177
|
+
catch {
|
|
2178
|
+
authFailed = true;
|
|
2179
|
+
return { type: "failed" };
|
|
2180
|
+
}
|
|
2181
|
+
finally {
|
|
2182
|
+
pendingOAuth = undefined;
|
|
2183
|
+
scheduleOAuthServerStop(authFailed ? OAUTH_SERVER_SHUTDOWN_ERROR_GRACE_MS : OAUTH_SERVER_SHUTDOWN_GRACE_MS, authFailed ? "error" : "success");
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
},
|
|
2189
|
+
{
|
|
2190
|
+
label: "ChatGPT Pro/Plus (headless)",
|
|
2191
|
+
type: "oauth",
|
|
2192
|
+
authorize: async () => {
|
|
2193
|
+
const deviceResponse = await fetch(`${ISSUER}/api/accounts/deviceauth/usercode`, {
|
|
2194
|
+
method: "POST",
|
|
2195
|
+
headers: {
|
|
2196
|
+
"Content-Type": "application/json",
|
|
2197
|
+
"User-Agent": resolveRequestUserAgent(spoofMode, resolveCodexOriginator(spoofMode))
|
|
2198
|
+
},
|
|
2199
|
+
body: JSON.stringify({ client_id: CLIENT_ID })
|
|
2200
|
+
});
|
|
2201
|
+
if (!deviceResponse.ok) {
|
|
2202
|
+
throw new Error("Failed to initiate device authorization");
|
|
2203
|
+
}
|
|
2204
|
+
const deviceData = (await deviceResponse.json());
|
|
2205
|
+
const interval = Math.max(parseInt(deviceData.interval) || 5, 1) * 1000;
|
|
2206
|
+
return {
|
|
2207
|
+
url: `${ISSUER}/codex/device`,
|
|
2208
|
+
instructions: `Enter code: ${deviceData.user_code}`,
|
|
2209
|
+
method: "auto",
|
|
2210
|
+
async callback() {
|
|
2211
|
+
while (true) {
|
|
2212
|
+
const response = await fetch(`${ISSUER}/api/accounts/deviceauth/token`, {
|
|
2213
|
+
method: "POST",
|
|
2214
|
+
headers: {
|
|
2215
|
+
"Content-Type": "application/json",
|
|
2216
|
+
"User-Agent": resolveRequestUserAgent(spoofMode, resolveCodexOriginator(spoofMode))
|
|
2217
|
+
},
|
|
2218
|
+
body: JSON.stringify({
|
|
2219
|
+
device_auth_id: deviceData.device_auth_id,
|
|
2220
|
+
user_code: deviceData.user_code
|
|
2221
|
+
})
|
|
2222
|
+
});
|
|
2223
|
+
if (response.ok) {
|
|
2224
|
+
const data = (await response.json());
|
|
2225
|
+
const tokenResponse = await fetch(`${ISSUER}/oauth/token`, {
|
|
2226
|
+
method: "POST",
|
|
2227
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2228
|
+
body: new URLSearchParams({
|
|
2229
|
+
grant_type: "authorization_code",
|
|
2230
|
+
code: data.authorization_code,
|
|
2231
|
+
redirect_uri: `${ISSUER}/deviceauth/callback`,
|
|
2232
|
+
client_id: CLIENT_ID,
|
|
2233
|
+
code_verifier: data.code_verifier
|
|
2234
|
+
}).toString()
|
|
2235
|
+
});
|
|
2236
|
+
if (!tokenResponse.ok) {
|
|
2237
|
+
throw new Error(`Token exchange failed: ${tokenResponse.status}`);
|
|
2238
|
+
}
|
|
2239
|
+
const tokens = (await tokenResponse.json());
|
|
2240
|
+
await persistOAuthTokens(tokens);
|
|
2241
|
+
return {
|
|
2242
|
+
type: "success",
|
|
2243
|
+
refresh: tokens.refresh_token,
|
|
2244
|
+
access: tokens.access_token,
|
|
2245
|
+
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
|
2246
|
+
accountId: extractAccountId(tokens)
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
if (response.status !== 403 && response.status !== 404) {
|
|
2250
|
+
return { type: "failed" };
|
|
2251
|
+
}
|
|
2252
|
+
await sleep(interval + OAUTH_POLLING_SAFETY_MARGIN_MS);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
},
|
|
2258
|
+
{
|
|
2259
|
+
label: "Manually enter API Key",
|
|
2260
|
+
type: "api"
|
|
2261
|
+
}
|
|
2262
|
+
]
|
|
2263
|
+
},
|
|
2264
|
+
"chat.message": async (hookInput, output) => {
|
|
2265
|
+
const directProviderID = hookInput.model?.providerID;
|
|
2266
|
+
const isOpenAI = directProviderID === "openai"
|
|
2267
|
+
|| (directProviderID === undefined
|
|
2268
|
+
&& (await sessionUsesOpenAIProvider(input.client, hookInput.sessionID)));
|
|
2269
|
+
if (!isOpenAI)
|
|
2270
|
+
return;
|
|
2271
|
+
for (const part of output.parts) {
|
|
2272
|
+
const partRecord = part;
|
|
2273
|
+
if (asString(partRecord.type) !== "subtask")
|
|
2274
|
+
continue;
|
|
2275
|
+
if ((asString(partRecord.command) ?? "").trim().toLowerCase() !== "review")
|
|
2276
|
+
continue;
|
|
2277
|
+
partRecord.agent = "Codex Review";
|
|
2278
|
+
}
|
|
2279
|
+
},
|
|
2280
|
+
"chat.params": async (hookInput, output) => {
|
|
2281
|
+
if (hookInput.model.providerID !== "openai")
|
|
2282
|
+
return;
|
|
2283
|
+
const initialReasoningEffort = asString(output.options.reasoningEffort);
|
|
2284
|
+
const collaborationProfile = collabModeEnabled
|
|
2285
|
+
? resolveCollaborationProfile(hookInput.agent)
|
|
2286
|
+
: { enabled: false };
|
|
2287
|
+
const modelOptions = isRecord(hookInput.model.options) ? hookInput.model.options : {};
|
|
2288
|
+
const modelCandidates = getModelLookupCandidates({
|
|
2289
|
+
id: hookInput.model.id,
|
|
2290
|
+
api: { id: hookInput.model.api?.id }
|
|
2291
|
+
});
|
|
2292
|
+
const variantCandidates = getVariantLookupCandidates({
|
|
2293
|
+
message: hookInput.message,
|
|
2294
|
+
modelCandidates
|
|
2295
|
+
});
|
|
2296
|
+
const effectivePersonality = resolvePersonalityForModel({
|
|
2297
|
+
customSettings: opts.customSettings,
|
|
2298
|
+
modelCandidates,
|
|
2299
|
+
variantCandidates,
|
|
2300
|
+
fallback: opts.personality
|
|
2301
|
+
});
|
|
2302
|
+
const modelThinkingSummariesOverride = getModelThinkingSummariesOverride(opts.customSettings, modelCandidates, variantCandidates);
|
|
2303
|
+
if (asString(output.options.instructions) === undefined && isRecord(modelOptions.codexCatalogModel)) {
|
|
2304
|
+
const rendered = resolveInstructionsForModel(modelOptions.codexCatalogModel, effectivePersonality);
|
|
2305
|
+
if (rendered) {
|
|
2306
|
+
modelOptions.codexInstructions = rendered;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
applyCodexRuntimeDefaultsToParams({
|
|
2310
|
+
modelOptions,
|
|
2311
|
+
modelToolCallCapable: hookInput.model.capabilities?.toolcall,
|
|
2312
|
+
thinkingSummariesOverride: modelThinkingSummariesOverride ?? opts.customSettings?.thinkingSummaries,
|
|
2313
|
+
output
|
|
2314
|
+
});
|
|
2315
|
+
if (collabModeEnabled && collaborationProfile.enabled && collaborationProfile.kind) {
|
|
2316
|
+
const collaborationModeKind = collaborationProfile.kind;
|
|
2317
|
+
const collaborationInstructions = resolveCollaborationInstructions(collaborationModeKind);
|
|
2318
|
+
const mergedInstructions = mergeInstructions(asString(output.options.instructions), collaborationInstructions);
|
|
2319
|
+
if (mergedInstructions) {
|
|
2320
|
+
output.options.instructions = mergedInstructions;
|
|
2321
|
+
}
|
|
2322
|
+
if (initialReasoningEffort === undefined) {
|
|
2323
|
+
if (collaborationModeKind === "plan" || collaborationModeKind === "pair_programming") {
|
|
2324
|
+
output.options.reasoningEffort = "medium";
|
|
2325
|
+
}
|
|
2326
|
+
else if (collaborationModeKind === "execute") {
|
|
2327
|
+
output.options.reasoningEffort = "high";
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
},
|
|
2332
|
+
"chat.headers": async (hookInput, output) => {
|
|
2333
|
+
if (hookInput.model.providerID !== "openai")
|
|
2334
|
+
return;
|
|
2335
|
+
const collaborationProfile = collabModeEnabled
|
|
2336
|
+
? resolveCollaborationProfile(hookInput.agent)
|
|
2337
|
+
: { enabled: false };
|
|
2338
|
+
const collaborationModeKind = collaborationProfile.enabled ? collaborationProfile.kind : undefined;
|
|
2339
|
+
const originator = resolveCodexOriginator(spoofMode);
|
|
2340
|
+
output.headers.originator = originator;
|
|
2341
|
+
output.headers["User-Agent"] = resolveRequestUserAgent(spoofMode, originator);
|
|
2342
|
+
const modelOptions = isRecord(hookInput.model.options) ? hookInput.model.options : {};
|
|
2343
|
+
const promptCacheKey = resolvePromptCacheKey(modelOptions);
|
|
2344
|
+
if (spoofMode === "native") {
|
|
2345
|
+
output.headers["OpenAI-Beta"] = "responses=experimental";
|
|
2346
|
+
if (promptCacheKey) {
|
|
2347
|
+
output.headers.session_id = promptCacheKey;
|
|
2348
|
+
output.headers.conversation_id = promptCacheKey;
|
|
2349
|
+
}
|
|
2350
|
+
else {
|
|
2351
|
+
delete output.headers.session_id;
|
|
2352
|
+
delete output.headers.conversation_id;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
else {
|
|
2356
|
+
output.headers.session_id = promptCacheKey ?? hookInput.sessionID;
|
|
2357
|
+
delete output.headers["OpenAI-Beta"];
|
|
2358
|
+
delete output.headers.conversation_id;
|
|
2359
|
+
const subagentHeader = collaborationProfile.enabled ? resolveSubagentHeaderValue(hookInput.agent) : undefined;
|
|
2360
|
+
if (subagentHeader) {
|
|
2361
|
+
output.headers["x-openai-subagent"] = subagentHeader;
|
|
2362
|
+
}
|
|
2363
|
+
else {
|
|
2364
|
+
delete output.headers["x-openai-subagent"];
|
|
2365
|
+
}
|
|
2366
|
+
if (collaborationModeKind) {
|
|
2367
|
+
output.headers[INTERNAL_COLLABORATION_MODE_HEADER] = collaborationModeKind;
|
|
2368
|
+
}
|
|
2369
|
+
else {
|
|
2370
|
+
delete output.headers[INTERNAL_COLLABORATION_MODE_HEADER];
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
},
|
|
2374
|
+
"experimental.session.compacting": async (hookInput, output) => {
|
|
2375
|
+
if (await sessionUsesOpenAIProvider(input.client, hookInput.sessionID)) {
|
|
2376
|
+
output.prompt = CODEX_RS_COMPACT_PROMPT;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
async function persistOAuthTokens(tokens) {
|
|
2381
|
+
const now = Date.now();
|
|
2382
|
+
const expires = now + (tokens.expires_in ?? 3600) * 1000;
|
|
2383
|
+
const claims = parseJwtClaims(tokens.id_token ?? tokens.access_token);
|
|
2384
|
+
const account = {
|
|
2385
|
+
enabled: true,
|
|
2386
|
+
refresh: tokens.refresh_token,
|
|
2387
|
+
access: tokens.access_token,
|
|
2388
|
+
expires,
|
|
2389
|
+
accountId: extractAccountId(tokens),
|
|
2390
|
+
email: extractEmailFromClaims(claims),
|
|
2391
|
+
plan: extractPlanFromClaims(claims),
|
|
2392
|
+
lastUsed: now
|
|
2393
|
+
};
|
|
2394
|
+
await saveAuthStorage(undefined, async (authFile) => {
|
|
2395
|
+
const domain = ensureOpenAIOAuthDomain(authFile, authMode);
|
|
2396
|
+
const stored = upsertAccount(domain, { ...account, authTypes: [authMode] });
|
|
2397
|
+
if (stored.identityKey) {
|
|
2398
|
+
domain.activeIdentityKey = stored.identityKey;
|
|
2399
|
+
}
|
|
2400
|
+
return authFile;
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
//# sourceMappingURL=codex-native.js.map
|