@iam-brain/opencode-codex-auth 1.2.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -83
- 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 +112 -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 +6 -1
- package/dist/lib/cache-io.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 +318 -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/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 +11 -4
- 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.map +1 -1
- package/dist/lib/codex-native/oauth-auth-methods.js +25 -6
- package/dist/lib/codex-native/oauth-auth-methods.js.map +1 -1
- package/dist/lib/codex-native/oauth-server-debug.d.ts +10 -0
- package/dist/lib/codex-native/oauth-server-debug.d.ts.map +1 -0
- package/dist/lib/codex-native/oauth-server-debug.js +92 -0
- package/dist/lib/codex-native/oauth-server-debug.js.map +1 -0
- package/dist/lib/codex-native/oauth-server-network.d.ts +5 -0
- package/dist/lib/codex-native/oauth-server-network.d.ts.map +1 -0
- package/dist/lib/codex-native/oauth-server-network.js +39 -0
- package/dist/lib/codex-native/oauth-server-network.js.map +1 -0
- package/dist/lib/codex-native/oauth-server-types.d.ts +24 -0
- package/dist/lib/codex-native/oauth-server-types.d.ts.map +1 -0
- package/dist/lib/codex-native/oauth-server-types.js +2 -0
- package/dist/lib/codex-native/oauth-server-types.js.map +1 -0
- package/dist/lib/codex-native/oauth-server.d.ts +2 -16
- package/dist/lib/codex-native/oauth-server.d.ts.map +1 -1
- package/dist/lib/codex-native/oauth-server.js +63 -118
- package/dist/lib/codex-native/oauth-server.js.map +1 -1
- package/dist/lib/codex-native/openai-loader-fetch-quota.d.ts +18 -0
- package/dist/lib/codex-native/openai-loader-fetch-quota.d.ts.map +1 -0
- package/dist/lib/codex-native/openai-loader-fetch-quota.js +71 -0
- package/dist/lib/codex-native/openai-loader-fetch-quota.js.map +1 -0
- package/dist/lib/codex-native/openai-loader-fetch-state.d.ts +27 -0
- package/dist/lib/codex-native/openai-loader-fetch-state.d.ts.map +1 -0
- package/dist/lib/codex-native/openai-loader-fetch-state.js +91 -0
- package/dist/lib/codex-native/openai-loader-fetch-state.js.map +1 -0
- package/dist/lib/codex-native/openai-loader-fetch.d.ts.map +1 -1
- package/dist/lib/codex-native/openai-loader-fetch.js +49 -131
- 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-transform-instructions.d.ts +16 -0
- package/dist/lib/codex-native/request-transform-instructions.d.ts.map +1 -0
- package/dist/lib/codex-native/request-transform-instructions.js +114 -0
- package/dist/lib/codex-native/request-transform-instructions.js.map +1 -0
- package/dist/lib/codex-native/request-transform-model.d.ts +39 -0
- package/dist/lib/codex-native/request-transform-model.d.ts.map +1 -0
- package/dist/lib/codex-native/request-transform-model.js +270 -0
- package/dist/lib/codex-native/request-transform-model.js.map +1 -0
- package/dist/lib/codex-native/request-transform-payload-helpers.d.ts +26 -0
- package/dist/lib/codex-native/request-transform-payload-helpers.d.ts.map +1 -0
- package/dist/lib/codex-native/request-transform-payload-helpers.js +232 -0
- package/dist/lib/codex-native/request-transform-payload-helpers.js.map +1 -0
- package/dist/lib/codex-native/request-transform-payload.d.ts +53 -0
- package/dist/lib/codex-native/request-transform-payload.d.ts.map +1 -0
- package/dist/lib/codex-native/request-transform-payload.js +214 -0
- package/dist/lib/codex-native/request-transform-payload.js.map +1 -0
- package/dist/lib/codex-native/request-transform-shared.d.ts +8 -0
- package/dist/lib/codex-native/request-transform-shared.d.ts.map +1 -0
- package/dist/lib/codex-native/request-transform-shared.js +49 -0
- package/dist/lib/codex-native/request-transform-shared.js.map +1 -0
- package/dist/lib/codex-native/request-transform.d.ts +3 -122
- package/dist/lib/codex-native/request-transform.d.ts.map +1 -1
- package/dist/lib/codex-native/request-transform.js +3 -831
- package/dist/lib/codex-native/request-transform.js.map +1 -1
- package/dist/lib/codex-native/session-affinity-state.d.ts +14 -1
- package/dist/lib/codex-native/session-affinity-state.d.ts.map +1 -1
- package/dist/lib/codex-native/session-affinity-state.js +21 -8
- 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 +13 -10
- 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/io.d.ts +16 -0
- package/dist/lib/config/io.d.ts.map +1 -0
- package/dist/lib/config/io.js +64 -0
- package/dist/lib/config/io.js.map +1 -0
- package/dist/lib/config/parse.d.ts +21 -0
- package/dist/lib/config/parse.d.ts.map +1 -0
- package/dist/lib/config/parse.js +347 -0
- package/dist/lib/config/parse.js.map +1 -0
- package/dist/lib/config/resolve.d.ts +27 -0
- package/dist/lib/config/resolve.d.ts.map +1 -0
- package/dist/lib/config/resolve.js +152 -0
- package/dist/lib/config/resolve.js.map +1 -0
- package/dist/lib/config/types.d.ts +72 -0
- package/dist/lib/config/types.d.ts.map +1 -0
- package/dist/lib/config/types.js +151 -0
- package/dist/lib/config/types.js.map +1 -0
- package/dist/lib/config/validation.d.ts +6 -0
- package/dist/lib/config/validation.d.ts.map +1 -0
- package/dist/lib/config/validation.js +160 -0
- package/dist/lib/config/validation.js.map +1 -0
- package/dist/lib/config.d.ts +5 -111
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +5 -835
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/fetch-orchestrator-helpers.d.ts +13 -0
- package/dist/lib/fetch-orchestrator-helpers.d.ts.map +1 -0
- package/dist/lib/fetch-orchestrator-helpers.js +63 -0
- package/dist/lib/fetch-orchestrator-helpers.js.map +1 -0
- package/dist/lib/fetch-orchestrator-types.d.ts +71 -0
- package/dist/lib/fetch-orchestrator-types.d.ts.map +1 -0
- package/dist/lib/fetch-orchestrator-types.js +11 -0
- package/dist/lib/fetch-orchestrator-types.js.map +1 -0
- package/dist/lib/fetch-orchestrator.d.ts +3 -69
- package/dist/lib/fetch-orchestrator.d.ts.map +1 -1
- package/dist/lib/fetch-orchestrator.js +78 -57
- 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/model-catalog/cache-helpers.d.ts +23 -0
- package/dist/lib/model-catalog/cache-helpers.d.ts.map +1 -0
- package/dist/lib/model-catalog/cache-helpers.js +210 -0
- package/dist/lib/model-catalog/cache-helpers.js.map +1 -0
- package/dist/lib/model-catalog/catalog-fetch.d.ts +3 -0
- package/dist/lib/model-catalog/catalog-fetch.d.ts.map +1 -0
- package/dist/lib/model-catalog/catalog-fetch.js +159 -0
- package/dist/lib/model-catalog/catalog-fetch.js.map +1 -0
- package/dist/lib/model-catalog/provider.d.ts +6 -0
- package/dist/lib/model-catalog/provider.d.ts.map +1 -0
- package/dist/lib/model-catalog/provider.js +254 -0
- package/dist/lib/model-catalog/provider.js.map +1 -0
- package/dist/lib/model-catalog/shared.d.ts +95 -0
- package/dist/lib/model-catalog/shared.d.ts.map +1 -0
- package/dist/lib/model-catalog/shared.js +154 -0
- package/dist/lib/model-catalog/shared.js.map +1 -0
- package/dist/lib/model-catalog.d.ts +3 -68
- package/dist/lib/model-catalog.d.ts.map +1 -1
- package/dist/lib/model-catalog.js +3 -767
- package/dist/lib/model-catalog.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 +8 -2
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/proactive-refresh.d.ts.map +1 -1
- package/dist/lib/proactive-refresh.js +48 -13
- package/dist/lib/proactive-refresh.js.map +1 -1
- package/dist/lib/quarantine.js.map +1 -1
- package/dist/lib/quota-threshold-alerts.d.ts.map +1 -1
- package/dist/lib/quota-threshold-alerts.js +3 -1
- package/dist/lib/quota-threshold-alerts.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/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/domain-state.d.ts +23 -0
- package/dist/lib/storage/domain-state.d.ts.map +1 -0
- package/dist/lib/storage/domain-state.js +275 -0
- package/dist/lib/storage/domain-state.js.map +1 -0
- package/dist/lib/storage/migration.d.ts +13 -0
- package/dist/lib/storage/migration.d.ts.map +1 -0
- package/dist/lib/storage/migration.js +225 -0
- package/dist/lib/storage/migration.js.map +1 -0
- package/dist/lib/storage.d.ts +2 -9
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +44 -470
- 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 +28 -15
|
@@ -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,141 @@ 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 ||
|
|
246
|
+
currentNow - previousLastUsed >= LAST_USED_WRITE_INTERVAL_MS) {
|
|
247
|
+
current.lastUsed = currentNow;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else if (!selected.refresh) {
|
|
253
|
+
sawMissingRefresh = true;
|
|
254
|
+
await saveAuthStorage(undefined, (authFile) => {
|
|
255
|
+
const currentDomain = ensureOpenAIOAuthDomain(authFile, input.authMode);
|
|
256
|
+
const currentByIdentity = selectedIdentityKey
|
|
257
|
+
? currentDomain.accounts.find((account) => account.identityKey === selectedIdentityKey)
|
|
258
|
+
: undefined;
|
|
259
|
+
const current = currentByIdentity ?? currentDomain.accounts[selectedIndex];
|
|
260
|
+
if (!current)
|
|
261
|
+
return;
|
|
262
|
+
const currentIndex = currentDomain.accounts.findIndex((account) => account === current);
|
|
263
|
+
const currentAttemptKey = buildAttemptKeyForCandidate(current, currentIndex >= 0 ? currentIndex : selectedIndex);
|
|
264
|
+
if (currentAttemptKey !== attemptKey || current.enabled === false || current.refresh) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
current.cooldownUntil = now + AUTH_REFRESH_FAILURE_COOLDOWN_MS;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
const leaseUntil = now + AUTH_REFRESH_LEASE_MS;
|
|
272
|
+
await saveAuthStorage(undefined, (authFile) => {
|
|
273
|
+
const currentDomain = ensureOpenAIOAuthDomain(authFile, input.authMode);
|
|
274
|
+
const currentByIdentity = currentDomain.accounts.find((account) => account.identityKey === selectedIdentityKey);
|
|
275
|
+
const current = currentByIdentity ?? currentDomain.accounts[selectedIndex];
|
|
276
|
+
if (!current)
|
|
277
|
+
return;
|
|
278
|
+
const currentIndex = currentDomain.accounts.findIndex((account) => account === current);
|
|
279
|
+
const currentAttemptKey = buildAttemptKeyForCandidate(current, currentIndex >= 0 ? currentIndex : selectedIndex);
|
|
280
|
+
if (currentAttemptKey !== attemptKey)
|
|
281
|
+
return;
|
|
282
|
+
if (current.enabled === false ||
|
|
283
|
+
!current.refresh ||
|
|
284
|
+
current.refresh !== selected.refresh ||
|
|
285
|
+
(typeof current.refreshLeaseUntil === "number" && current.refreshLeaseUntil > now)) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
current.refreshLeaseUntil = leaseUntil;
|
|
289
|
+
refreshClaim = {
|
|
290
|
+
identityKey: current.identityKey ?? selectedIdentityKey,
|
|
291
|
+
refreshToken: current.refresh,
|
|
292
|
+
leaseUntil,
|
|
293
|
+
selectedIndex: currentIndex >= 0 ? currentIndex : selectedIndex
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
169
298
|
}
|
|
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
299
|
}
|
|
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
|
-
});
|
|
300
|
+
}
|
|
195
301
|
if (access)
|
|
196
302
|
break;
|
|
303
|
+
if (shouldPersistSessionAffinityState) {
|
|
304
|
+
await input.persistSessionAffinityState();
|
|
305
|
+
}
|
|
197
306
|
if (!refreshClaim) {
|
|
198
307
|
if (shouldStop || (totalAccounts > 0 && attempted.size >= totalAccounts)) {
|
|
199
308
|
break;
|
|
@@ -201,21 +310,24 @@ export async function acquireOpenAIAuth(input) {
|
|
|
201
310
|
continue;
|
|
202
311
|
}
|
|
203
312
|
try {
|
|
204
|
-
const
|
|
313
|
+
const activeRefreshClaim = refreshClaim;
|
|
314
|
+
const tokens = await refreshAccessToken(activeRefreshClaim.refreshToken);
|
|
205
315
|
const refreshedExpires = Date.now() + (tokens.expires_in ?? 3600) * 1000;
|
|
206
316
|
const refreshedAccountId = extractAccountId(tokens);
|
|
207
317
|
const claims = parseJwtClaims(tokens.id_token ?? tokens.access_token);
|
|
208
318
|
await saveAuthStorage(undefined, (authFile) => {
|
|
209
319
|
const domain = ensureOpenAIOAuthDomain(authFile, input.authMode);
|
|
210
|
-
const selected =
|
|
320
|
+
const selected = activeRefreshClaim.identityKey
|
|
321
|
+
? domain.accounts.find((account) => account.identityKey === activeRefreshClaim.identityKey)
|
|
322
|
+
: domain.accounts[activeRefreshClaim.selectedIndex];
|
|
211
323
|
if (!selected)
|
|
212
324
|
return;
|
|
213
325
|
const now = Date.now();
|
|
214
326
|
if (typeof selected.refreshLeaseUntil !== "number" ||
|
|
215
|
-
selected.refreshLeaseUntil !==
|
|
327
|
+
selected.refreshLeaseUntil !== activeRefreshClaim.leaseUntil ||
|
|
216
328
|
selected.refreshLeaseUntil <= now ||
|
|
217
|
-
selected.refresh !==
|
|
218
|
-
if (selected.refreshLeaseUntil ===
|
|
329
|
+
selected.refresh !== activeRefreshClaim.refreshToken) {
|
|
330
|
+
if (selected.refreshLeaseUntil === activeRefreshClaim.leaseUntil) {
|
|
219
331
|
delete selected.refreshLeaseUntil;
|
|
220
332
|
}
|
|
221
333
|
return;
|
|
@@ -241,7 +353,7 @@ export async function acquireOpenAIAuth(input) {
|
|
|
241
353
|
delete selected.cooldownUntil;
|
|
242
354
|
if (selected.identityKey)
|
|
243
355
|
domain.activeIdentityKey = selected.identityKey;
|
|
244
|
-
accountLabel = formatAccountLabel(selected,
|
|
356
|
+
accountLabel = formatAccountLabel(selected, activeRefreshClaim.selectedIndex);
|
|
245
357
|
email = selected.email;
|
|
246
358
|
plan = selected.plan;
|
|
247
359
|
access = selected.access;
|
|
@@ -250,20 +362,23 @@ export async function acquireOpenAIAuth(input) {
|
|
|
250
362
|
});
|
|
251
363
|
}
|
|
252
364
|
catch (error) {
|
|
253
|
-
const invalidGrant =
|
|
365
|
+
const invalidGrant = isTerminalRefreshCredentialError(error);
|
|
254
366
|
if (invalidGrant) {
|
|
255
367
|
sawInvalidGrant = true;
|
|
256
368
|
}
|
|
257
369
|
else {
|
|
258
370
|
sawRefreshFailure = true;
|
|
259
371
|
}
|
|
372
|
+
const activeRefreshClaim = refreshClaim;
|
|
260
373
|
await saveAuthStorage(undefined, (authFile) => {
|
|
261
374
|
const domain = ensureOpenAIOAuthDomain(authFile, input.authMode);
|
|
262
|
-
const selected =
|
|
375
|
+
const selected = activeRefreshClaim.identityKey
|
|
376
|
+
? domain.accounts.find((account) => account.identityKey === activeRefreshClaim.identityKey)
|
|
377
|
+
: domain.accounts[activeRefreshClaim.selectedIndex];
|
|
263
378
|
if (!selected)
|
|
264
379
|
return;
|
|
265
|
-
if (selected.refreshLeaseUntil !==
|
|
266
|
-
selected.refresh !==
|
|
380
|
+
if (selected.refreshLeaseUntil !== activeRefreshClaim.leaseUntil ||
|
|
381
|
+
selected.refresh !== activeRefreshClaim.refreshToken) {
|
|
267
382
|
return;
|
|
268
383
|
}
|
|
269
384
|
delete selected.refreshLeaseUntil;
|
|
@@ -284,84 +399,83 @@ export async function acquireOpenAIAuth(input) {
|
|
|
284
399
|
}
|
|
285
400
|
}
|
|
286
401
|
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;
|
|
402
|
+
const now = Date.now();
|
|
403
|
+
const authSnapshot = await loadAuthStorage(undefined, { lockReads: false });
|
|
404
|
+
const openai = authSnapshot.openai;
|
|
405
|
+
if (!openai || openai.type !== "oauth") {
|
|
406
|
+
throw new PluginFatalError({
|
|
407
|
+
message: "Not authenticated with OpenAI. Run `opencode auth login`.",
|
|
408
|
+
status: 401,
|
|
409
|
+
type: "oauth_not_configured",
|
|
410
|
+
param: "auth"
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
const domain = ensureOpenAIOAuthDomain(authSnapshot, input.authMode);
|
|
414
|
+
const enabledAfterAttempts = domain.accounts.filter((account) => account.enabled !== false);
|
|
415
|
+
if (enabledAfterAttempts.length === 0 && sawInvalidGrant) {
|
|
416
|
+
throw new PluginFatalError({
|
|
417
|
+
message: "All enabled OpenAI refresh tokens were rejected (invalid_grant). Run `opencode auth login` to reauthenticate.",
|
|
418
|
+
status: 401,
|
|
419
|
+
type: "refresh_invalid_grant",
|
|
420
|
+
param: "auth"
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
const nextAvailableAt = enabledAfterAttempts.reduce((current, account) => {
|
|
424
|
+
const cooldownUntil = typeof account.refreshLeaseUntil === "number" && account.refreshLeaseUntil > now
|
|
425
|
+
? account.refreshLeaseUntil
|
|
426
|
+
: account.cooldownUntil;
|
|
427
|
+
if (typeof cooldownUntil !== "number" || cooldownUntil <= now)
|
|
316
428
|
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
|
-
}
|
|
429
|
+
if (current === undefined || cooldownUntil < current)
|
|
430
|
+
return cooldownUntil;
|
|
431
|
+
return current;
|
|
432
|
+
}, undefined);
|
|
433
|
+
if (nextAvailableAt !== undefined) {
|
|
434
|
+
const waitMs = Math.max(0, nextAvailableAt - now);
|
|
359
435
|
throw new PluginFatalError({
|
|
360
|
-
message: `
|
|
361
|
-
status:
|
|
362
|
-
type: "
|
|
436
|
+
message: `All enabled OpenAI accounts are cooling down. Try again in ${formatWaitTime(waitMs)} or run \`opencode auth login\`.`,
|
|
437
|
+
status: 429,
|
|
438
|
+
type: "all_accounts_cooling_down",
|
|
363
439
|
param: "accounts"
|
|
364
440
|
});
|
|
441
|
+
}
|
|
442
|
+
if (sawInvalidGrant) {
|
|
443
|
+
throw new PluginFatalError({
|
|
444
|
+
message: "OpenAI refresh token was rejected (invalid_grant). Run `opencode auth login` to reauthenticate this account.",
|
|
445
|
+
status: 401,
|
|
446
|
+
type: "refresh_invalid_grant",
|
|
447
|
+
param: "auth"
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
if (sawMissingRefresh) {
|
|
451
|
+
throw new PluginFatalError({
|
|
452
|
+
message: "Selected OpenAI account is missing a refresh token. Run `opencode auth login` to reauthenticate.",
|
|
453
|
+
status: 401,
|
|
454
|
+
type: "missing_refresh_token",
|
|
455
|
+
param: "accounts"
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (sawMissingIdentity) {
|
|
459
|
+
throw new PluginFatalError({
|
|
460
|
+
message: "Selected OpenAI account is missing identity metadata. Run `opencode auth login` to reauthenticate.",
|
|
461
|
+
status: 401,
|
|
462
|
+
type: "missing_account_identity",
|
|
463
|
+
param: "accounts"
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
if (sawRefreshFailure) {
|
|
467
|
+
throw new PluginFatalError({
|
|
468
|
+
message: "Failed to refresh OpenAI access token. Run `opencode auth login` and try again.",
|
|
469
|
+
status: 401,
|
|
470
|
+
type: "refresh_failed",
|
|
471
|
+
param: "auth"
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
throw new PluginFatalError({
|
|
475
|
+
message: `No enabled OpenAI ${input.authMode} accounts available. Enable an account or run \`opencode auth login\`.`,
|
|
476
|
+
status: 403,
|
|
477
|
+
type: "no_enabled_accounts",
|
|
478
|
+
param: "accounts"
|
|
365
479
|
});
|
|
366
480
|
}
|
|
367
481
|
}
|