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