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