@iam-brain/opencode-codex-auth 1.2.3 → 1.2.5
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 +15 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +41 -18
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts-tools.d.ts.map +1 -1
- package/dist/lib/accounts-tools.js +114 -29
- package/dist/lib/accounts-tools.js.map +1 -1
- package/dist/lib/cache-io.d.ts.map +1 -1
- package/dist/lib/cache-io.js +8 -2
- package/dist/lib/cache-io.js.map +1 -1
- package/dist/lib/cache-lock.d.ts +1 -0
- package/dist/lib/cache-lock.d.ts.map +1 -1
- package/dist/lib/cache-lock.js +12 -2
- package/dist/lib/cache-lock.js.map +1 -1
- package/dist/lib/codex-native/accounts.d.ts.map +1 -1
- package/dist/lib/codex-native/accounts.js +18 -12
- package/dist/lib/codex-native/accounts.js.map +1 -1
- package/dist/lib/codex-native/acquire-auth.d.ts +1 -1
- package/dist/lib/codex-native/acquire-auth.d.ts.map +1 -1
- package/dist/lib/codex-native/acquire-auth.js +317 -204
- package/dist/lib/codex-native/acquire-auth.js.map +1 -1
- package/dist/lib/codex-native/auth-menu-flow.d.ts.map +1 -1
- package/dist/lib/codex-native/auth-menu-flow.js +27 -12
- package/dist/lib/codex-native/auth-menu-flow.js.map +1 -1
- package/dist/lib/codex-native/auth-menu-quotas.d.ts.map +1 -1
- package/dist/lib/codex-native/auth-menu-quotas.js +11 -4
- package/dist/lib/codex-native/auth-menu-quotas.js.map +1 -1
- package/dist/lib/codex-native/browser.d.ts +2 -0
- package/dist/lib/codex-native/browser.d.ts.map +1 -1
- package/dist/lib/codex-native/browser.js +3 -3
- package/dist/lib/codex-native/browser.js.map +1 -1
- package/dist/lib/codex-native/catalog-auth.d.ts.map +1 -1
- package/dist/lib/codex-native/catalog-auth.js +4 -2
- package/dist/lib/codex-native/catalog-auth.js.map +1 -1
- package/dist/lib/codex-native/chat-hooks.d.ts.map +1 -1
- package/dist/lib/codex-native/chat-hooks.js +0 -8
- package/dist/lib/codex-native/chat-hooks.js.map +1 -1
- package/dist/lib/codex-native/client-identity.d.ts.map +1 -1
- package/dist/lib/codex-native/client-identity.js +13 -5
- package/dist/lib/codex-native/client-identity.js.map +1 -1
- package/dist/lib/codex-native/collaboration.d.ts +1 -1
- package/dist/lib/codex-native/collaboration.d.ts.map +1 -1
- package/dist/lib/codex-native/collaboration.js +9 -116
- package/dist/lib/codex-native/collaboration.js.map +1 -1
- package/dist/lib/codex-native/oauth-auth-methods.d.ts +1 -0
- package/dist/lib/codex-native/oauth-auth-methods.d.ts.map +1 -1
- package/dist/lib/codex-native/oauth-auth-methods.js +36 -6
- package/dist/lib/codex-native/oauth-auth-methods.js.map +1 -1
- package/dist/lib/codex-native/oauth-persistence.d.ts.map +1 -1
- package/dist/lib/codex-native/oauth-persistence.js +6 -0
- package/dist/lib/codex-native/oauth-persistence.js.map +1 -1
- package/dist/lib/codex-native/oauth-server.d.ts +1 -0
- package/dist/lib/codex-native/oauth-server.d.ts.map +1 -1
- package/dist/lib/codex-native/oauth-server.js +114 -43
- package/dist/lib/codex-native/oauth-server.js.map +1 -1
- package/dist/lib/codex-native/openai-loader-fetch.d.ts.map +1 -1
- package/dist/lib/codex-native/openai-loader-fetch.js +122 -33
- package/dist/lib/codex-native/openai-loader-fetch.js.map +1 -1
- package/dist/lib/codex-native/originator.d.ts.map +1 -1
- package/dist/lib/codex-native/originator.js +18 -1
- package/dist/lib/codex-native/originator.js.map +1 -1
- package/dist/lib/codex-native/request-routing.d.ts.map +1 -1
- package/dist/lib/codex-native/request-routing.js +9 -0
- package/dist/lib/codex-native/request-routing.js.map +1 -1
- package/dist/lib/codex-native/request-transform.d.ts.map +1 -1
- package/dist/lib/codex-native/request-transform.js +39 -8
- package/dist/lib/codex-native/request-transform.js.map +1 -1
- package/dist/lib/codex-native/session-affinity-state.d.ts +1 -1
- package/dist/lib/codex-native/session-affinity-state.d.ts.map +1 -1
- package/dist/lib/codex-native/session-affinity-state.js +1 -0
- package/dist/lib/codex-native/session-affinity-state.js.map +1 -1
- package/dist/lib/codex-native.js.map +1 -1
- package/dist/lib/codex-prompts-cache.d.ts.map +1 -1
- package/dist/lib/codex-prompts-cache.js.map +1 -1
- package/dist/lib/codex-quota-fetch.d.ts.map +1 -1
- package/dist/lib/codex-quota-fetch.js +12 -9
- package/dist/lib/codex-quota-fetch.js.map +1 -1
- package/dist/lib/codex-status-storage.d.ts.map +1 -1
- package/dist/lib/codex-status-storage.js.map +1 -1
- package/dist/lib/codex-status.d.ts.map +1 -1
- package/dist/lib/codex-status.js +28 -3
- package/dist/lib/codex-status.js.map +1 -1
- package/dist/lib/config.d.ts +3 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +41 -21
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/fetch-orchestrator.d.ts +2 -1
- package/dist/lib/fetch-orchestrator.d.ts.map +1 -1
- package/dist/lib/fetch-orchestrator.js +81 -11
- package/dist/lib/fetch-orchestrator.js.map +1 -1
- package/dist/lib/identity.d.ts +6 -0
- package/dist/lib/identity.d.ts.map +1 -1
- package/dist/lib/identity.js +25 -4
- package/dist/lib/identity.js.map +1 -1
- package/dist/lib/opencode-install.d.ts.map +1 -1
- package/dist/lib/opencode-install.js +5 -6
- package/dist/lib/opencode-install.js.map +1 -1
- package/dist/lib/orchestrator-agent.d.ts.map +1 -1
- package/dist/lib/orchestrator-agent.js +2 -1
- package/dist/lib/orchestrator-agent.js.map +1 -1
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/paths.js +24 -3
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/proactive-refresh.d.ts.map +1 -1
- package/dist/lib/proactive-refresh.js +50 -13
- package/dist/lib/proactive-refresh.js.map +1 -1
- package/dist/lib/refresh-queue.d.ts.map +1 -1
- package/dist/lib/refresh-queue.js +1 -0
- package/dist/lib/refresh-queue.js.map +1 -1
- package/dist/lib/remote-cache-fetch.d.ts.map +1 -1
- package/dist/lib/remote-cache-fetch.js +5 -1
- package/dist/lib/remote-cache-fetch.js.map +1 -1
- package/dist/lib/request-snapshots.d.ts.map +1 -1
- package/dist/lib/request-snapshots.js +46 -10
- package/dist/lib/request-snapshots.js.map +1 -1
- package/dist/lib/rotation.d.ts.map +1 -1
- package/dist/lib/rotation.js +3 -2
- package/dist/lib/rotation.js.map +1 -1
- package/dist/lib/session-affinity.d.ts.map +1 -1
- package/dist/lib/session-affinity.js +35 -20
- package/dist/lib/session-affinity.js.map +1 -1
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +110 -45
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/ui/auth-menu.d.ts +3 -2
- package/dist/lib/ui/auth-menu.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.js +1 -1
- package/dist/lib/ui/auth-menu.js.map +1 -1
- package/package.json +20 -8
|
@@ -1,22 +1,60 @@
|
|
|
1
1
|
import { PluginFatalError, formatWaitTime, isPluginFatalError } from "../fatal-errors.js";
|
|
2
2
|
import { ensureIdentityKey, normalizeEmail, normalizePlan } from "../identity.js";
|
|
3
3
|
import { createStickySessionState, selectAccount } from "../rotation.js";
|
|
4
|
-
import { ensureOpenAIOAuthDomain, saveAuthStorage } from "../storage.js";
|
|
4
|
+
import { ensureOpenAIOAuthDomain, loadAuthStorage, saveAuthStorage } from "../storage.js";
|
|
5
5
|
import { parseJwtClaims } from "../claims.js";
|
|
6
6
|
import { formatAccountLabel } from "./accounts.js";
|
|
7
7
|
import { extractAccountId, refreshAccessToken } from "./oauth-utils.js";
|
|
8
8
|
const AUTH_REFRESH_FAILURE_COOLDOWN_MS = 30_000;
|
|
9
9
|
const AUTH_REFRESH_LEASE_MS = 30_000;
|
|
10
|
-
const LAST_USED_WRITE_INTERVAL_MS =
|
|
10
|
+
const LAST_USED_WRITE_INTERVAL_MS = 60_000;
|
|
11
11
|
function isOAuthTokenRefreshError(value) {
|
|
12
12
|
return value instanceof Error && ("status" in value || "oauthCode" in value);
|
|
13
13
|
}
|
|
14
|
+
const TERMINAL_REFRESH_ERROR_CODES = new Set([
|
|
15
|
+
"invalid_grant",
|
|
16
|
+
"invalid_refresh_token",
|
|
17
|
+
"refresh_token_revoked",
|
|
18
|
+
"token_revoked"
|
|
19
|
+
]);
|
|
20
|
+
function isTerminalRefreshCredentialError(error) {
|
|
21
|
+
if (isOAuthTokenRefreshError(error)) {
|
|
22
|
+
const oauthCode = typeof error.oauthCode === "string" ? error.oauthCode.trim().toLowerCase() : undefined;
|
|
23
|
+
if (oauthCode && TERMINAL_REFRESH_ERROR_CODES.has(oauthCode)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (error instanceof Error) {
|
|
28
|
+
const message = error.message.trim().toLowerCase();
|
|
29
|
+
if (message.includes("invalid_grant"))
|
|
30
|
+
return true;
|
|
31
|
+
if (message.includes("refresh token") && (message.includes("invalid") || message.includes("expired"))) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (message.includes("refresh token") && message.includes("revoked")) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
14
40
|
export function createAcquireOpenAIAuthInputDefaults() {
|
|
15
41
|
return {
|
|
16
42
|
stickySessionState: createStickySessionState(),
|
|
17
43
|
hybridSessionState: createStickySessionState()
|
|
18
44
|
};
|
|
19
45
|
}
|
|
46
|
+
function buildAttemptKeyForCandidate(account, index) {
|
|
47
|
+
const identityKey = account.identityKey?.trim();
|
|
48
|
+
if (identityKey)
|
|
49
|
+
return identityKey;
|
|
50
|
+
const accountId = account.accountId?.trim();
|
|
51
|
+
const email = normalizeEmail(account.email);
|
|
52
|
+
const plan = normalizePlan(account.plan);
|
|
53
|
+
if (accountId && email && plan) {
|
|
54
|
+
return `${accountId}|${email}|${plan}`;
|
|
55
|
+
}
|
|
56
|
+
return `idx:${index}`;
|
|
57
|
+
}
|
|
20
58
|
export async function acquireOpenAIAuth(input) {
|
|
21
59
|
let access;
|
|
22
60
|
let accountId;
|
|
@@ -33,67 +71,74 @@ export async function acquireOpenAIAuth(input) {
|
|
|
33
71
|
let rotationLogged = false;
|
|
34
72
|
let lastSelectionTrace;
|
|
35
73
|
try {
|
|
36
|
-
if (input.isSubagentRequest && input.context?.sessionKey) {
|
|
37
|
-
input.seenSessionKeys.delete(input.context.sessionKey);
|
|
38
|
-
input.stickySessionState.bySessionKey.delete(input.context.sessionKey);
|
|
39
|
-
input.hybridSessionState.bySessionKey.delete(input.context.sessionKey);
|
|
40
|
-
}
|
|
41
74
|
while (true) {
|
|
42
75
|
let refreshClaim;
|
|
43
76
|
let shouldStop = false;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
77
|
+
let shouldPersistSessionAffinityState = false;
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const authSnapshot = await loadAuthStorage(undefined, { lockReads: false });
|
|
80
|
+
const openai = authSnapshot.openai;
|
|
81
|
+
if (!openai || openai.type !== "oauth") {
|
|
82
|
+
throw new PluginFatalError({
|
|
83
|
+
message: "Not authenticated with OpenAI. Run `opencode auth login`.",
|
|
84
|
+
status: 401,
|
|
85
|
+
type: "oauth_not_configured",
|
|
86
|
+
param: "auth"
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const domain = ensureOpenAIOAuthDomain(authSnapshot, input.authMode);
|
|
90
|
+
totalAccounts = domain.accounts.length;
|
|
91
|
+
if (domain.accounts.length === 0) {
|
|
92
|
+
throw new PluginFatalError({
|
|
93
|
+
message: `No OpenAI ${input.authMode} accounts configured. Run \`opencode auth login\`.`,
|
|
94
|
+
status: 401,
|
|
95
|
+
type: "no_accounts_configured",
|
|
96
|
+
param: "accounts"
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const enabled = domain.accounts.filter((account) => account.enabled !== false);
|
|
100
|
+
if (enabled.length === 0) {
|
|
101
|
+
throw new PluginFatalError({
|
|
102
|
+
message: `No enabled OpenAI ${input.authMode} accounts available. Enable an account or run \`opencode auth login\`.`,
|
|
103
|
+
status: 403,
|
|
104
|
+
type: "no_enabled_accounts",
|
|
105
|
+
param: "accounts"
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const rotationStrategy = input.configuredRotationStrategy ?? domain.strategy ?? "sticky";
|
|
109
|
+
if (!rotationLogged) {
|
|
110
|
+
input.log?.debug("rotation begin", {
|
|
111
|
+
strategy: rotationStrategy,
|
|
112
|
+
activeIdentityKey: domain.activeIdentityKey,
|
|
113
|
+
totalAccounts: domain.accounts.length,
|
|
114
|
+
enabledAccounts: enabled.length,
|
|
115
|
+
mode: input.authMode,
|
|
116
|
+
sessionKey: input.context?.sessionKey ?? null
|
|
117
|
+
});
|
|
118
|
+
rotationLogged = true;
|
|
119
|
+
}
|
|
120
|
+
const selectableEntries = domain.accounts
|
|
121
|
+
.map((account, index) => ({
|
|
122
|
+
account,
|
|
123
|
+
index,
|
|
124
|
+
attemptKey: buildAttemptKeyForCandidate(account, index)
|
|
125
|
+
}))
|
|
126
|
+
.filter((entry) => !attempted.has(entry.attemptKey));
|
|
127
|
+
if (selectableEntries.length === 0) {
|
|
128
|
+
input.log?.debug("rotation stop: exhausted candidate set", {
|
|
129
|
+
attempted: attempted.size,
|
|
130
|
+
totalAccounts: domain.accounts.length
|
|
131
|
+
});
|
|
132
|
+
shouldStop = true;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
90
135
|
const sessionState = rotationStrategy === "sticky"
|
|
91
136
|
? input.stickySessionState
|
|
92
137
|
: rotationStrategy === "hybrid"
|
|
93
138
|
? input.hybridSessionState
|
|
94
139
|
: undefined;
|
|
95
140
|
const selected = selectAccount({
|
|
96
|
-
accounts:
|
|
141
|
+
accounts: selectableEntries.map((entry) => entry.account),
|
|
97
142
|
strategy: rotationStrategy,
|
|
98
143
|
activeIdentityKey: domain.activeIdentityKey,
|
|
99
144
|
now,
|
|
@@ -123,77 +168,140 @@ export async function acquireOpenAIAuth(input) {
|
|
|
123
168
|
totalAccounts: domain.accounts.length
|
|
124
169
|
});
|
|
125
170
|
shouldStop = true;
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const selectedIndex = domain.accounts.findIndex((account) => account === selected);
|
|
129
|
-
const attemptKey = selected.identityKey ??
|
|
130
|
-
selected.refresh ??
|
|
131
|
-
(selectedIndex >= 0 ? `idx:${selectedIndex}` : `idx:${attempted.size}`);
|
|
132
|
-
if (attempted.has(attemptKey)) {
|
|
133
|
-
input.log?.debug("rotation stop: duplicate attempt key", {
|
|
134
|
-
attemptKey,
|
|
135
|
-
selectedIdentityKey: selected.identityKey,
|
|
136
|
-
selectedIndex
|
|
137
|
-
});
|
|
138
|
-
shouldStop = true;
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
attempted.add(attemptKey);
|
|
142
|
-
if (!input.isSubagentRequest && input.context?.sessionKey && sessionState) {
|
|
143
|
-
input.persistSessionAffinityState();
|
|
144
|
-
}
|
|
145
|
-
input.log?.debug("rotation candidate selected", {
|
|
146
|
-
attemptKey,
|
|
147
|
-
selectedIdentityKey: selected.identityKey,
|
|
148
|
-
selectedIndex,
|
|
149
|
-
selectedEnabled: selected.enabled !== false,
|
|
150
|
-
selectedCooldownUntil: selected.cooldownUntil ?? null,
|
|
151
|
-
selectedExpires: selected.expires ?? null
|
|
152
|
-
});
|
|
153
|
-
if (lastSelectionTrace) {
|
|
154
|
-
lastSelectionTrace = {
|
|
155
|
-
...lastSelectionTrace,
|
|
156
|
-
attemptedCount: attempted.size,
|
|
157
|
-
...(selected.identityKey ? { selectedIdentityKey: selected.identityKey } : null),
|
|
158
|
-
...(selectedIndex >= 0 ? { selectedIndex } : null),
|
|
159
|
-
attemptKey
|
|
160
|
-
};
|
|
161
171
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
else {
|
|
173
|
+
const selectedEntry = selectableEntries.find((entry) => entry.account === selected);
|
|
174
|
+
if (!selectedEntry) {
|
|
175
|
+
shouldStop = true;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const { index: selectedIndex, attemptKey } = selectedEntry;
|
|
179
|
+
if (attempted.has(attemptKey)) {
|
|
180
|
+
input.log?.debug("rotation skip: duplicate attempt key", {
|
|
181
|
+
attemptKey,
|
|
182
|
+
selectedIdentityKey: selected.identityKey,
|
|
183
|
+
selectedIndex
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
attempted.add(attemptKey);
|
|
188
|
+
if (!input.isSubagentRequest && input.context?.sessionKey && sessionState) {
|
|
189
|
+
shouldPersistSessionAffinityState = true;
|
|
190
|
+
}
|
|
191
|
+
input.log?.debug("rotation candidate selected", {
|
|
192
|
+
attemptKey,
|
|
193
|
+
selectedIdentityKey: selected.identityKey,
|
|
194
|
+
selectedIndex,
|
|
195
|
+
selectedEnabled: selected.enabled !== false,
|
|
196
|
+
selectedCooldownUntil: selected.cooldownUntil ?? null,
|
|
197
|
+
selectedExpires: selected.expires ?? null
|
|
198
|
+
});
|
|
199
|
+
if (lastSelectionTrace) {
|
|
200
|
+
lastSelectionTrace = {
|
|
201
|
+
...lastSelectionTrace,
|
|
202
|
+
attemptedCount: attempted.size,
|
|
203
|
+
...(selected.identityKey ? { selectedIdentityKey: selected.identityKey } : null),
|
|
204
|
+
...(selectedIndex >= 0 ? { selectedIndex } : null),
|
|
205
|
+
attemptKey
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
accountLabel = formatAccountLabel(selected, selectedIndex >= 0 ? selectedIndex : 0);
|
|
209
|
+
email = selected.email;
|
|
210
|
+
plan = selected.plan;
|
|
211
|
+
const selectedIdentityKey = ensureIdentityKey(selected).identityKey;
|
|
212
|
+
if (!selectedIdentityKey) {
|
|
213
|
+
sawMissingIdentity = true;
|
|
214
|
+
}
|
|
215
|
+
else if (selected.access && selected.expires && selected.expires > now) {
|
|
216
|
+
access = selected.access;
|
|
217
|
+
accountId = selected.accountId;
|
|
218
|
+
identityKey = selectedIdentityKey;
|
|
219
|
+
const selectionStrategy = lastSelectionTrace?.strategy ?? input.configuredRotationStrategy ?? domain.strategy;
|
|
220
|
+
if (selectionStrategy === "hybrid" || selectionStrategy === "round_robin") {
|
|
221
|
+
await saveAuthStorage(undefined, (authFile) => {
|
|
222
|
+
const currentDomain = ensureOpenAIOAuthDomain(authFile, input.authMode);
|
|
223
|
+
const currentByIdentity = selectedIdentityKey
|
|
224
|
+
? currentDomain.accounts.find((account) => account.identityKey === selectedIdentityKey)
|
|
225
|
+
: undefined;
|
|
226
|
+
const current = currentByIdentity ?? currentDomain.accounts[selectedIndex];
|
|
227
|
+
if (!current)
|
|
228
|
+
return;
|
|
229
|
+
const currentIndex = currentDomain.accounts.findIndex((account) => account === current);
|
|
230
|
+
const currentAttemptKey = buildAttemptKeyForCandidate(current, currentIndex >= 0 ? currentIndex : selectedIndex);
|
|
231
|
+
if (currentAttemptKey !== attemptKey || current.enabled === false) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (!current.identityKey && selectedIdentityKey) {
|
|
235
|
+
current.identityKey = selectedIdentityKey;
|
|
236
|
+
}
|
|
237
|
+
if (selectionStrategy === "round_robin" && current.identityKey) {
|
|
238
|
+
if (currentDomain.activeIdentityKey !== current.identityKey) {
|
|
239
|
+
currentDomain.activeIdentityKey = current.identityKey;
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const currentNow = Date.now();
|
|
244
|
+
const previousLastUsed = typeof current.lastUsed === "number" ? current.lastUsed : undefined;
|
|
245
|
+
if (previousLastUsed === undefined || currentNow - previousLastUsed >= LAST_USED_WRITE_INTERVAL_MS) {
|
|
246
|
+
current.lastUsed = currentNow;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else if (!selected.refresh) {
|
|
252
|
+
sawMissingRefresh = true;
|
|
253
|
+
await saveAuthStorage(undefined, (authFile) => {
|
|
254
|
+
const currentDomain = ensureOpenAIOAuthDomain(authFile, input.authMode);
|
|
255
|
+
const currentByIdentity = selectedIdentityKey
|
|
256
|
+
? currentDomain.accounts.find((account) => account.identityKey === selectedIdentityKey)
|
|
257
|
+
: undefined;
|
|
258
|
+
const current = currentByIdentity ?? currentDomain.accounts[selectedIndex];
|
|
259
|
+
if (!current)
|
|
260
|
+
return;
|
|
261
|
+
const currentIndex = currentDomain.accounts.findIndex((account) => account === current);
|
|
262
|
+
const currentAttemptKey = buildAttemptKeyForCandidate(current, currentIndex >= 0 ? currentIndex : selectedIndex);
|
|
263
|
+
if (currentAttemptKey !== attemptKey || current.enabled === false || current.refresh) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
current.cooldownUntil = now + AUTH_REFRESH_FAILURE_COOLDOWN_MS;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
const leaseUntil = now + AUTH_REFRESH_LEASE_MS;
|
|
271
|
+
await saveAuthStorage(undefined, (authFile) => {
|
|
272
|
+
const currentDomain = ensureOpenAIOAuthDomain(authFile, input.authMode);
|
|
273
|
+
const currentByIdentity = currentDomain.accounts.find((account) => account.identityKey === selectedIdentityKey);
|
|
274
|
+
const current = currentByIdentity ?? currentDomain.accounts[selectedIndex];
|
|
275
|
+
if (!current)
|
|
276
|
+
return;
|
|
277
|
+
const currentIndex = currentDomain.accounts.findIndex((account) => account === current);
|
|
278
|
+
const currentAttemptKey = buildAttemptKeyForCandidate(current, currentIndex >= 0 ? currentIndex : selectedIndex);
|
|
279
|
+
if (currentAttemptKey !== attemptKey)
|
|
280
|
+
return;
|
|
281
|
+
if (current.enabled === false ||
|
|
282
|
+
!current.refresh ||
|
|
283
|
+
current.refresh !== selected.refresh ||
|
|
284
|
+
(typeof current.refreshLeaseUntil === "number" && current.refreshLeaseUntil > now)) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
current.refreshLeaseUntil = leaseUntil;
|
|
288
|
+
refreshClaim = {
|
|
289
|
+
identityKey: current.identityKey ?? selectedIdentityKey,
|
|
290
|
+
refreshToken: current.refresh,
|
|
291
|
+
leaseUntil,
|
|
292
|
+
selectedIndex: currentIndex >= 0 ? currentIndex : selectedIndex
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
169
297
|
}
|
|
170
|
-
access = selected.access;
|
|
171
|
-
accountId = selected.accountId;
|
|
172
|
-
identityKey = selected.identityKey;
|
|
173
|
-
if (selected.identityKey)
|
|
174
|
-
domain.activeIdentityKey = selected.identityKey;
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
if (!selected.refresh) {
|
|
178
|
-
sawMissingRefresh = true;
|
|
179
|
-
selected.cooldownUntil = now + AUTH_REFRESH_FAILURE_COOLDOWN_MS;
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
if (!selected.identityKey) {
|
|
183
|
-
sawMissingIdentity = true;
|
|
184
|
-
return;
|
|
185
298
|
}
|
|
186
|
-
|
|
187
|
-
selected.refreshLeaseUntil = leaseUntil;
|
|
188
|
-
refreshClaim = {
|
|
189
|
-
identityKey: selected.identityKey,
|
|
190
|
-
refreshToken: selected.refresh,
|
|
191
|
-
leaseUntil,
|
|
192
|
-
selectedIndex: selectedIndex >= 0 ? selectedIndex : 0
|
|
193
|
-
};
|
|
194
|
-
});
|
|
299
|
+
}
|
|
195
300
|
if (access)
|
|
196
301
|
break;
|
|
302
|
+
if (shouldPersistSessionAffinityState) {
|
|
303
|
+
await input.persistSessionAffinityState();
|
|
304
|
+
}
|
|
197
305
|
if (!refreshClaim) {
|
|
198
306
|
if (shouldStop || (totalAccounts > 0 && attempted.size >= totalAccounts)) {
|
|
199
307
|
break;
|
|
@@ -201,21 +309,24 @@ export async function acquireOpenAIAuth(input) {
|
|
|
201
309
|
continue;
|
|
202
310
|
}
|
|
203
311
|
try {
|
|
204
|
-
const
|
|
312
|
+
const activeRefreshClaim = refreshClaim;
|
|
313
|
+
const tokens = await refreshAccessToken(activeRefreshClaim.refreshToken);
|
|
205
314
|
const refreshedExpires = Date.now() + (tokens.expires_in ?? 3600) * 1000;
|
|
206
315
|
const refreshedAccountId = extractAccountId(tokens);
|
|
207
316
|
const claims = parseJwtClaims(tokens.id_token ?? tokens.access_token);
|
|
208
317
|
await saveAuthStorage(undefined, (authFile) => {
|
|
209
318
|
const domain = ensureOpenAIOAuthDomain(authFile, input.authMode);
|
|
210
|
-
const selected =
|
|
319
|
+
const selected = activeRefreshClaim.identityKey
|
|
320
|
+
? domain.accounts.find((account) => account.identityKey === activeRefreshClaim.identityKey)
|
|
321
|
+
: domain.accounts[activeRefreshClaim.selectedIndex];
|
|
211
322
|
if (!selected)
|
|
212
323
|
return;
|
|
213
324
|
const now = Date.now();
|
|
214
325
|
if (typeof selected.refreshLeaseUntil !== "number" ||
|
|
215
|
-
selected.refreshLeaseUntil !==
|
|
326
|
+
selected.refreshLeaseUntil !== activeRefreshClaim.leaseUntil ||
|
|
216
327
|
selected.refreshLeaseUntil <= now ||
|
|
217
|
-
selected.refresh !==
|
|
218
|
-
if (selected.refreshLeaseUntil ===
|
|
328
|
+
selected.refresh !== activeRefreshClaim.refreshToken) {
|
|
329
|
+
if (selected.refreshLeaseUntil === activeRefreshClaim.leaseUntil) {
|
|
219
330
|
delete selected.refreshLeaseUntil;
|
|
220
331
|
}
|
|
221
332
|
return;
|
|
@@ -241,7 +352,7 @@ export async function acquireOpenAIAuth(input) {
|
|
|
241
352
|
delete selected.cooldownUntil;
|
|
242
353
|
if (selected.identityKey)
|
|
243
354
|
domain.activeIdentityKey = selected.identityKey;
|
|
244
|
-
accountLabel = formatAccountLabel(selected,
|
|
355
|
+
accountLabel = formatAccountLabel(selected, activeRefreshClaim.selectedIndex);
|
|
245
356
|
email = selected.email;
|
|
246
357
|
plan = selected.plan;
|
|
247
358
|
access = selected.access;
|
|
@@ -250,20 +361,23 @@ export async function acquireOpenAIAuth(input) {
|
|
|
250
361
|
});
|
|
251
362
|
}
|
|
252
363
|
catch (error) {
|
|
253
|
-
const invalidGrant =
|
|
364
|
+
const invalidGrant = isTerminalRefreshCredentialError(error);
|
|
254
365
|
if (invalidGrant) {
|
|
255
366
|
sawInvalidGrant = true;
|
|
256
367
|
}
|
|
257
368
|
else {
|
|
258
369
|
sawRefreshFailure = true;
|
|
259
370
|
}
|
|
371
|
+
const activeRefreshClaim = refreshClaim;
|
|
260
372
|
await saveAuthStorage(undefined, (authFile) => {
|
|
261
373
|
const domain = ensureOpenAIOAuthDomain(authFile, input.authMode);
|
|
262
|
-
const selected =
|
|
374
|
+
const selected = activeRefreshClaim.identityKey
|
|
375
|
+
? domain.accounts.find((account) => account.identityKey === activeRefreshClaim.identityKey)
|
|
376
|
+
: domain.accounts[activeRefreshClaim.selectedIndex];
|
|
263
377
|
if (!selected)
|
|
264
378
|
return;
|
|
265
|
-
if (selected.refreshLeaseUntil !==
|
|
266
|
-
selected.refresh !==
|
|
379
|
+
if (selected.refreshLeaseUntil !== activeRefreshClaim.leaseUntil ||
|
|
380
|
+
selected.refresh !== activeRefreshClaim.refreshToken) {
|
|
267
381
|
return;
|
|
268
382
|
}
|
|
269
383
|
delete selected.refreshLeaseUntil;
|
|
@@ -284,84 +398,83 @@ export async function acquireOpenAIAuth(input) {
|
|
|
284
398
|
}
|
|
285
399
|
}
|
|
286
400
|
if (!access) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
return current;
|
|
314
|
-
if (current === undefined || cooldownUntil < current)
|
|
315
|
-
return cooldownUntil;
|
|
401
|
+
const now = Date.now();
|
|
402
|
+
const authSnapshot = await loadAuthStorage(undefined, { lockReads: false });
|
|
403
|
+
const openai = authSnapshot.openai;
|
|
404
|
+
if (!openai || openai.type !== "oauth") {
|
|
405
|
+
throw new PluginFatalError({
|
|
406
|
+
message: "Not authenticated with OpenAI. Run `opencode auth login`.",
|
|
407
|
+
status: 401,
|
|
408
|
+
type: "oauth_not_configured",
|
|
409
|
+
param: "auth"
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const domain = ensureOpenAIOAuthDomain(authSnapshot, input.authMode);
|
|
413
|
+
const enabledAfterAttempts = domain.accounts.filter((account) => account.enabled !== false);
|
|
414
|
+
if (enabledAfterAttempts.length === 0 && sawInvalidGrant) {
|
|
415
|
+
throw new PluginFatalError({
|
|
416
|
+
message: "All enabled OpenAI refresh tokens were rejected (invalid_grant). Run `opencode auth login` to reauthenticate.",
|
|
417
|
+
status: 401,
|
|
418
|
+
type: "refresh_invalid_grant",
|
|
419
|
+
param: "auth"
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
const nextAvailableAt = enabledAfterAttempts.reduce((current, account) => {
|
|
423
|
+
const cooldownUntil = typeof account.refreshLeaseUntil === "number" && account.refreshLeaseUntil > now
|
|
424
|
+
? account.refreshLeaseUntil
|
|
425
|
+
: account.cooldownUntil;
|
|
426
|
+
if (typeof cooldownUntil !== "number" || cooldownUntil <= now)
|
|
316
427
|
return current;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
type: "all_accounts_cooling_down",
|
|
324
|
-
param: "accounts"
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
if (sawInvalidGrant) {
|
|
328
|
-
throw new PluginFatalError({
|
|
329
|
-
message: "OpenAI refresh token was rejected (invalid_grant). Run `opencode auth login` to reauthenticate this account.",
|
|
330
|
-
status: 401,
|
|
331
|
-
type: "refresh_invalid_grant",
|
|
332
|
-
param: "auth"
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
if (sawMissingRefresh) {
|
|
336
|
-
throw new PluginFatalError({
|
|
337
|
-
message: "Selected OpenAI account is missing a refresh token. Run `opencode auth login` to reauthenticate.",
|
|
338
|
-
status: 401,
|
|
339
|
-
type: "missing_refresh_token",
|
|
340
|
-
param: "accounts"
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
if (sawMissingIdentity) {
|
|
344
|
-
throw new PluginFatalError({
|
|
345
|
-
message: "Selected OpenAI account is missing identity metadata. Run `opencode auth login` to reauthenticate.",
|
|
346
|
-
status: 401,
|
|
347
|
-
type: "missing_account_identity",
|
|
348
|
-
param: "accounts"
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
if (sawRefreshFailure) {
|
|
352
|
-
throw new PluginFatalError({
|
|
353
|
-
message: "Failed to refresh OpenAI access token. Run `opencode auth login` and try again.",
|
|
354
|
-
status: 401,
|
|
355
|
-
type: "refresh_failed",
|
|
356
|
-
param: "auth"
|
|
357
|
-
});
|
|
358
|
-
}
|
|
428
|
+
if (current === undefined || cooldownUntil < current)
|
|
429
|
+
return cooldownUntil;
|
|
430
|
+
return current;
|
|
431
|
+
}, undefined);
|
|
432
|
+
if (nextAvailableAt !== undefined) {
|
|
433
|
+
const waitMs = Math.max(0, nextAvailableAt - now);
|
|
359
434
|
throw new PluginFatalError({
|
|
360
|
-
message: `
|
|
361
|
-
status:
|
|
362
|
-
type: "
|
|
435
|
+
message: `All enabled OpenAI accounts are cooling down. Try again in ${formatWaitTime(waitMs)} or run \`opencode auth login\`.`,
|
|
436
|
+
status: 429,
|
|
437
|
+
type: "all_accounts_cooling_down",
|
|
363
438
|
param: "accounts"
|
|
364
439
|
});
|
|
440
|
+
}
|
|
441
|
+
if (sawInvalidGrant) {
|
|
442
|
+
throw new PluginFatalError({
|
|
443
|
+
message: "OpenAI refresh token was rejected (invalid_grant). Run `opencode auth login` to reauthenticate this account.",
|
|
444
|
+
status: 401,
|
|
445
|
+
type: "refresh_invalid_grant",
|
|
446
|
+
param: "auth"
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (sawMissingRefresh) {
|
|
450
|
+
throw new PluginFatalError({
|
|
451
|
+
message: "Selected OpenAI account is missing a refresh token. Run `opencode auth login` to reauthenticate.",
|
|
452
|
+
status: 401,
|
|
453
|
+
type: "missing_refresh_token",
|
|
454
|
+
param: "accounts"
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
if (sawMissingIdentity) {
|
|
458
|
+
throw new PluginFatalError({
|
|
459
|
+
message: "Selected OpenAI account is missing identity metadata. Run `opencode auth login` to reauthenticate.",
|
|
460
|
+
status: 401,
|
|
461
|
+
type: "missing_account_identity",
|
|
462
|
+
param: "accounts"
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (sawRefreshFailure) {
|
|
466
|
+
throw new PluginFatalError({
|
|
467
|
+
message: "Failed to refresh OpenAI access token. Run `opencode auth login` and try again.",
|
|
468
|
+
status: 401,
|
|
469
|
+
type: "refresh_failed",
|
|
470
|
+
param: "auth"
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
throw new PluginFatalError({
|
|
474
|
+
message: `No enabled OpenAI ${input.authMode} accounts available. Enable an account or run \`opencode auth login\`.`,
|
|
475
|
+
status: 403,
|
|
476
|
+
type: "no_enabled_accounts",
|
|
477
|
+
param: "accounts"
|
|
365
478
|
});
|
|
366
479
|
}
|
|
367
480
|
}
|