opencode-openai-codex-multi-auth 4.5.6 → 4.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +281 -41
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts.d.ts +8 -0
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +179 -20
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/auth/auth.d.ts +5 -0
- package/dist/lib/auth/auth.d.ts.map +1 -1
- package/dist/lib/auth/auth.js +16 -0
- package/dist/lib/auth/auth.js.map +1 -1
- package/dist/lib/cli.d.ts +4 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +20 -0
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/codex-status.d.ts +34 -0
- package/dist/lib/codex-status.d.ts.map +1 -0
- package/dist/lib/codex-status.js +125 -0
- package/dist/lib/codex-status.js.map +1 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +4 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/formatting.d.ts +9 -0
- package/dist/lib/formatting.d.ts.map +1 -0
- package/dist/lib/formatting.js +71 -0
- package/dist/lib/formatting.js.map +1 -0
- package/dist/lib/oauth-success.html +1 -1
- package/dist/lib/storage-scope.d.ts +5 -0
- package/dist/lib/storage-scope.d.ts.map +1 -0
- package/dist/lib/storage-scope.js +11 -0
- package/dist/lib/storage-scope.js.map +1 -0
- package/dist/lib/storage.d.ts +39 -1
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +495 -116
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/package.json +8 -4
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAQ,KAAK,MAAM,EAAoB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAQ,KAAK,MAAM,EAAoB,MAAM,qBAAqB,CAAC;AA+H1E;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,EAAE,MA+sC9B,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* @repository https://github.com/numman-ali/opencode-openai-codex-auth
|
|
23
23
|
*/
|
|
24
24
|
import { tool } from "@opencode-ai/plugin";
|
|
25
|
-
import { createAuthorizationFlow, exchangeAuthorizationCode,
|
|
25
|
+
import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInputForFlow, REDIRECT_URI, } from "./lib/auth/auth.js";
|
|
26
26
|
import { openBrowserUrl } from "./lib/auth/browser.js";
|
|
27
27
|
import { startLocalOAuthServer } from "./lib/auth/server.js";
|
|
28
28
|
import { getAccountSelectionStrategy, getCodexMode, getDefaultRetryAfterMs, getMaxBackoffMs, getMaxCacheFirstWaitSeconds, getPidOffsetEnabled, getQuietMode, getRateLimitDedupWindowMs, getRateLimitStateResetMs, getRateLimitToastDebounceMs, getRequestJitterMaxMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getSchedulingMode, getSwitchOnFirstRateLimit, getTokenRefreshSkewMs, loadPluginConfig, } from "./lib/config.js";
|
|
@@ -30,14 +30,17 @@ import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, HTTP_STATUS, LOG_STAGES, PL
|
|
|
30
30
|
import { logRequest, logDebug } from "./lib/logger.js";
|
|
31
31
|
import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, rewriteUrlForCodex, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
|
|
32
32
|
import { AccountManager, extractAccountEmail, extractAccountId, extractAccountPlan, formatAccountLabel, formatWaitTime, isOAuthAuth, needsIdentityHydration, sanitizeEmail, } from "./lib/accounts.js";
|
|
33
|
-
import { promptAddAnotherAccount, promptLoginMode, promptManageAccounts, promptOAuthCallbackValue, } from "./lib/cli.js";
|
|
33
|
+
import { promptAddAnotherAccount, promptLoginMode, promptManageAccounts, promptOAuthCallbackValue, promptRepairAccounts, } from "./lib/cli.js";
|
|
34
34
|
import { withTerminalModeRestored } from "./lib/terminal.js";
|
|
35
|
-
import {
|
|
35
|
+
import { configureStorageForCurrentCwd, configureStorageForPluginConfig, } from "./lib/storage-scope.js";
|
|
36
|
+
import { getStoragePath, getStorageScope, autoQuarantineCorruptAccountsFile, inspectAccountsFile, loadAccounts, quarantineAccounts, quarantineCorruptFile, replaceAccountsFile, saveAccounts, toggleAccountEnabled, writeQuarantineFile, } from "./lib/storage.js";
|
|
36
37
|
import { findAccountMatchIndex } from "./lib/account-matching.js";
|
|
37
38
|
import { getModelFamily, MODEL_FAMILIES } from "./lib/prompts/codex.js";
|
|
38
39
|
import { getHealthTracker, getTokenTracker } from "./lib/rotation.js";
|
|
39
40
|
import { RateLimitTracker, decideRateLimitAction, parseRateLimitReason } from "./lib/rate-limit.js";
|
|
41
|
+
import { codexStatus } from "./lib/codex-status.js";
|
|
40
42
|
import { ProactiveRefreshQueue, createRefreshScheduler, } from "./lib/refresh-queue.js";
|
|
43
|
+
import { formatToastMessage } from "./lib/formatting.js";
|
|
41
44
|
const RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS = 5_000;
|
|
42
45
|
const AUTH_FAILURE_COOLDOWN_MS = 60_000;
|
|
43
46
|
const MAX_ACCOUNTS = 10;
|
|
@@ -83,22 +86,26 @@ function parseRetryAfterMs(headers) {
|
|
|
83
86
|
export const OpenAIAuthPlugin = async ({ client }) => {
|
|
84
87
|
let cachedAccountManager = null;
|
|
85
88
|
let proactiveRefreshScheduler = null;
|
|
89
|
+
configureStorageForPluginConfig(loadPluginConfig(), process.cwd());
|
|
86
90
|
const showToast = async (message, variant = "info", quietMode = false) => {
|
|
87
91
|
if (quietMode)
|
|
88
92
|
return;
|
|
89
93
|
try {
|
|
90
|
-
await client.tui.showToast({ body: { message, variant } });
|
|
94
|
+
await client.tui.showToast({ body: { message: formatToastMessage(message), variant } });
|
|
91
95
|
}
|
|
92
96
|
catch {
|
|
93
97
|
// ignore (non-TUI contexts)
|
|
94
98
|
}
|
|
95
99
|
};
|
|
96
|
-
const buildManualOAuthFlow = (pkce, url, onSuccess) => ({
|
|
100
|
+
const buildManualOAuthFlow = (pkce, expectedState, url, onSuccess) => ({
|
|
97
101
|
url,
|
|
98
102
|
method: "code",
|
|
99
103
|
instructions: AUTH_LABELS.INSTRUCTIONS_MANUAL,
|
|
100
104
|
callback: async (input) => {
|
|
101
|
-
const parsed =
|
|
105
|
+
const parsed = parseAuthorizationInputForFlow(input, expectedState);
|
|
106
|
+
if (parsed.stateStatus === "mismatch") {
|
|
107
|
+
return { type: "failed" };
|
|
108
|
+
}
|
|
102
109
|
if (!parsed.code) {
|
|
103
110
|
return { type: "failed" };
|
|
104
111
|
}
|
|
@@ -181,9 +188,16 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
181
188
|
if (!isOAuthAuth(auth)) {
|
|
182
189
|
return {};
|
|
183
190
|
}
|
|
191
|
+
const pluginConfig = loadPluginConfig();
|
|
192
|
+
configureStorageForPluginConfig(pluginConfig, process.cwd());
|
|
193
|
+
const quietMode = getQuietMode(pluginConfig);
|
|
184
194
|
const accountManager = await AccountManager.loadFromDisk(auth);
|
|
185
195
|
cachedAccountManager = accountManager;
|
|
186
196
|
if (accountManager.getAccountCount() === 0) {
|
|
197
|
+
const quarantinePath = await autoQuarantineCorruptAccountsFile();
|
|
198
|
+
if (quarantinePath) {
|
|
199
|
+
await showToast("Accounts file was corrupted and has been quarantined. Run `opencode auth login`.", "warning", quietMode);
|
|
200
|
+
}
|
|
187
201
|
logDebug(`[${PLUGIN_NAME}] No OAuth accounts available (run opencode auth login)`);
|
|
188
202
|
return {};
|
|
189
203
|
}
|
|
@@ -193,11 +207,9 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
193
207
|
global: providerConfig?.options || {},
|
|
194
208
|
models: providerConfig?.models || {},
|
|
195
209
|
};
|
|
196
|
-
const pluginConfig = loadPluginConfig();
|
|
197
210
|
const codexMode = getCodexMode(pluginConfig);
|
|
198
211
|
const accountSelectionStrategy = getAccountSelectionStrategy(pluginConfig);
|
|
199
212
|
const pidOffsetEnabled = getPidOffsetEnabled(pluginConfig);
|
|
200
|
-
const quietMode = getQuietMode(pluginConfig);
|
|
201
213
|
const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
|
|
202
214
|
const proactiveRefreshEnabled = (() => {
|
|
203
215
|
const rawConfig = pluginConfig;
|
|
@@ -236,6 +248,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
236
248
|
getTasks: () => {
|
|
237
249
|
const tasks = [];
|
|
238
250
|
for (const account of accountManager.getAccountsSnapshot()) {
|
|
251
|
+
if (account.enabled === false)
|
|
252
|
+
continue;
|
|
239
253
|
if (!Number.isFinite(account.expires))
|
|
240
254
|
continue;
|
|
241
255
|
tasks.push({
|
|
@@ -243,7 +257,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
243
257
|
expires: account.expires ?? 0,
|
|
244
258
|
refresh: async () => {
|
|
245
259
|
const live = accountManager.getAccountByIndex(account.index);
|
|
246
|
-
if (!live)
|
|
260
|
+
if (!live || live.enabled === false)
|
|
247
261
|
return { type: "failed" };
|
|
248
262
|
const refreshed = await accountManager.refreshAccountWithFallback(live);
|
|
249
263
|
if (refreshed.type !== "success")
|
|
@@ -327,8 +341,36 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
327
341
|
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
328
342
|
});
|
|
329
343
|
let allRateLimitedRetries = 0;
|
|
344
|
+
let autoRepairAttempted = false;
|
|
330
345
|
while (true) {
|
|
331
346
|
const accountCount = accountManager.getAccountCount();
|
|
347
|
+
if (!autoRepairAttempted && accountCount === 0) {
|
|
348
|
+
const legacyAccounts = accountManager.getLegacyAccounts();
|
|
349
|
+
if (legacyAccounts.length > 0) {
|
|
350
|
+
autoRepairAttempted = true;
|
|
351
|
+
const repair = await accountManager.repairLegacyAccounts();
|
|
352
|
+
const snapshot = accountManager.getStorageSnapshot();
|
|
353
|
+
let quarantinePath = null;
|
|
354
|
+
if (repair.quarantined.length > 0) {
|
|
355
|
+
const quarantinedTokens = new Set(repair.quarantined.map((account) => account.refreshToken));
|
|
356
|
+
const quarantineEntries = snapshot.accounts.filter((account) => quarantinedTokens.has(account.refreshToken));
|
|
357
|
+
const quarantineResult = await quarantineAccounts(snapshot, quarantineEntries, "legacy-auto-repair-failed");
|
|
358
|
+
quarantinePath = quarantineResult.quarantinePath;
|
|
359
|
+
accountManager.removeAccountsByRefreshToken(quarantinedTokens);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
await replaceAccountsFile(snapshot);
|
|
363
|
+
}
|
|
364
|
+
if (repair.quarantined.length > 0 && quarantinePath) {
|
|
365
|
+
logDebug(`[${PLUGIN_NAME}] Auto-repair quarantined: ${quarantinePath}`);
|
|
366
|
+
await showToast(`Auto-repair failed for ${repair.quarantined.length} account(s).`, "warning", quietMode);
|
|
367
|
+
}
|
|
368
|
+
else if (repair.repaired.length > 0) {
|
|
369
|
+
await showToast(`Auto-repaired ${repair.repaired.length} account(s).`, "success", quietMode);
|
|
370
|
+
}
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
332
374
|
const attempted = new Set();
|
|
333
375
|
while (attempted.size < Math.max(1, accountCount)) {
|
|
334
376
|
const account = accountManager.getCurrentOrNextForFamily(modelFamily, model, accountSelectionStrategy, usePidOffset);
|
|
@@ -407,6 +449,14 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
407
449
|
let res;
|
|
408
450
|
try {
|
|
409
451
|
res = await fetch(url, { ...requestInit, headers });
|
|
452
|
+
// Update Codex rate limit snapshot from response headers
|
|
453
|
+
const codexHeaders = {};
|
|
454
|
+
res.headers.forEach((val, key) => {
|
|
455
|
+
if (key.toLowerCase().startsWith("x-codex-")) {
|
|
456
|
+
codexHeaders[key] = val;
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
codexStatus.updateFromHeaders(account, codexHeaders);
|
|
410
460
|
}
|
|
411
461
|
catch (err) {
|
|
412
462
|
if (tokenConsumed) {
|
|
@@ -497,12 +547,35 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
497
547
|
await sleep(waitMs);
|
|
498
548
|
continue;
|
|
499
549
|
}
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
550
|
+
// Build detailed inline error message
|
|
551
|
+
const now = Date.now();
|
|
552
|
+
const accounts = accountManager.getAccountsSnapshot();
|
|
553
|
+
const statusLines = [
|
|
554
|
+
`All ${accountManager.getAccountCount()} account(s) are currently unavailable.`,
|
|
555
|
+
`Next reset in approximately ${formatWaitTime(waitMs)}.`,
|
|
556
|
+
"",
|
|
557
|
+
"Account Status:",
|
|
558
|
+
];
|
|
559
|
+
accounts.forEach((acc, idx) => {
|
|
560
|
+
if (acc.enabled === false)
|
|
561
|
+
return;
|
|
562
|
+
const label = formatAccountLabel(acc, idx);
|
|
563
|
+
const rateLimited = acc.rateLimitResetTimes &&
|
|
564
|
+
Object.values(acc.rateLimitResetTimes).some((t) => typeof t === "number" && t > now);
|
|
565
|
+
const coolingDown = typeof acc.coolingDownUntil === "number" && acc.coolingDownUntil > now;
|
|
566
|
+
let status = "ok";
|
|
567
|
+
if (rateLimited)
|
|
568
|
+
status = "rate-limited";
|
|
569
|
+
else if (coolingDown)
|
|
570
|
+
status = "cooldown";
|
|
571
|
+
statusLines.push(`- ${label} [${status}]`);
|
|
572
|
+
const codexLines = codexStatus.renderStatus(acc);
|
|
573
|
+
if (codexLines.length > 0 && !codexLines[0]?.includes("No Codex status")) {
|
|
574
|
+
statusLines.push(...codexLines.map((l) => " " + l.trim()));
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
statusLines.push("", "Run `opencode auth login` to add more accounts.");
|
|
578
|
+
return new Response(JSON.stringify({ error: { message: statusLines.join("\n") } }), {
|
|
506
579
|
status: 429,
|
|
507
580
|
headers: { "content-type": "application/json; charset=utf-8" },
|
|
508
581
|
});
|
|
@@ -528,8 +601,75 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
528
601
|
*/
|
|
529
602
|
authorize: async (inputs) => {
|
|
530
603
|
const pluginConfig = loadPluginConfig();
|
|
604
|
+
configureStorageForPluginConfig(pluginConfig, process.cwd());
|
|
531
605
|
const quietMode = getQuietMode(pluginConfig);
|
|
532
606
|
const isCliFlow = Boolean(inputs);
|
|
607
|
+
const notifyRepairResult = async (message) => {
|
|
608
|
+
if (isCliFlow) {
|
|
609
|
+
console.log(`\n${message}\n`);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
await showToast(message, "info", quietMode);
|
|
613
|
+
};
|
|
614
|
+
const maybeRepairAccounts = async () => {
|
|
615
|
+
const inspection = await inspectAccountsFile();
|
|
616
|
+
if (inspection.status === "missing" || inspection.status === "ok") {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
const corruptCount = inspection.status === "corrupt-file"
|
|
620
|
+
? 1
|
|
621
|
+
: inspection.corruptEntries.length;
|
|
622
|
+
const legacyCount = inspection.status === "needs-repair" ? inspection.legacyEntries.length : 0;
|
|
623
|
+
const shouldRepair = await promptRepairAccounts({
|
|
624
|
+
legacyCount,
|
|
625
|
+
corruptCount,
|
|
626
|
+
});
|
|
627
|
+
if (!shouldRepair)
|
|
628
|
+
return null;
|
|
629
|
+
const quarantinePaths = [];
|
|
630
|
+
if (inspection.status === "corrupt-file") {
|
|
631
|
+
const quarantinePath = await quarantineCorruptFile();
|
|
632
|
+
if (quarantinePath) {
|
|
633
|
+
quarantinePaths.push(quarantinePath);
|
|
634
|
+
logDebug(`[${PLUGIN_NAME}] Accounts file quarantined: ${quarantinePath}`);
|
|
635
|
+
await notifyRepairResult("Accounts file was corrupted and quarantined.");
|
|
636
|
+
}
|
|
637
|
+
return await loadAccounts();
|
|
638
|
+
}
|
|
639
|
+
if (inspection.corruptEntries.length > 0) {
|
|
640
|
+
const quarantinePath = await writeQuarantineFile(inspection.corruptEntries, "corrupt-entry");
|
|
641
|
+
quarantinePaths.push(quarantinePath);
|
|
642
|
+
}
|
|
643
|
+
const storage = await loadAccounts();
|
|
644
|
+
if (!storage) {
|
|
645
|
+
await notifyRepairResult("Repair skipped: no valid accounts found.");
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
const manager = new AccountManager(undefined, storage);
|
|
649
|
+
const repair = await manager.repairLegacyAccounts();
|
|
650
|
+
const snapshot = manager.getStorageSnapshot();
|
|
651
|
+
let updatedStorage = snapshot;
|
|
652
|
+
if (repair.quarantined.length > 0) {
|
|
653
|
+
const quarantinedTokens = new Set(repair.quarantined.map((account) => account.refreshToken));
|
|
654
|
+
const quarantineEntries = snapshot.accounts.filter((account) => quarantinedTokens.has(account.refreshToken));
|
|
655
|
+
const quarantineResult = await quarantineAccounts(snapshot, quarantineEntries, "legacy-repair-failed");
|
|
656
|
+
updatedStorage = quarantineResult.storage;
|
|
657
|
+
quarantinePaths.push(quarantineResult.quarantinePath);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
await replaceAccountsFile(snapshot);
|
|
661
|
+
}
|
|
662
|
+
const summaryParts = [
|
|
663
|
+
`Repaired ${repair.repaired.length}`,
|
|
664
|
+
`quarantined ${repair.quarantined.length}`,
|
|
665
|
+
];
|
|
666
|
+
if (quarantinePaths.length > 0) {
|
|
667
|
+
logDebug(`[${PLUGIN_NAME}] Repair quarantine paths: ${quarantinePaths.join(", ")}`);
|
|
668
|
+
}
|
|
669
|
+
await notifyRepairResult(`Account repair complete. ${summaryParts.join(", ")}.`);
|
|
670
|
+
return updatedStorage;
|
|
671
|
+
};
|
|
672
|
+
const repairedStorage = await maybeRepairAccounts();
|
|
533
673
|
// CLI flow (`opencode auth login`) passes inputs; TUI does not.
|
|
534
674
|
if (isCliFlow) {
|
|
535
675
|
debugAuth("[OAuthAuthorize] Starting OAuth flow in CLI mode");
|
|
@@ -541,8 +681,15 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
541
681
|
const { pkce, state, url } = await createAuthorizationFlow();
|
|
542
682
|
console.log("\nOAuth URL:\n" + url + "\n");
|
|
543
683
|
if (noBrowser) {
|
|
544
|
-
const callbackInput = await promptOAuthCallbackValue("Paste the redirect URL (or just the code
|
|
545
|
-
const parsed =
|
|
684
|
+
const callbackInput = await promptOAuthCallbackValue("Paste the full redirect URL (recommended). You can also paste code#state or just the code: ");
|
|
685
|
+
const parsed = parseAuthorizationInputForFlow(callbackInput, state);
|
|
686
|
+
if (parsed.stateStatus === "mismatch") {
|
|
687
|
+
console.log("\nOAuth state mismatch. Paste the redirect URL from this login session (the one shown above).\n");
|
|
688
|
+
return { type: "failed" };
|
|
689
|
+
}
|
|
690
|
+
if (parsed.stateStatus === "missing") {
|
|
691
|
+
console.log("\nWarning: redirect state not provided. For best security, paste the full redirect URL.\n");
|
|
692
|
+
}
|
|
546
693
|
if (!parsed.code)
|
|
547
694
|
return { type: "failed" };
|
|
548
695
|
return await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
|
|
@@ -557,8 +704,15 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
557
704
|
openBrowserUrl(url);
|
|
558
705
|
if (!serverInfo || !serverInfo.ready) {
|
|
559
706
|
serverInfo?.close();
|
|
560
|
-
const callbackInput = await promptOAuthCallbackValue("Paste the redirect URL (or just the code
|
|
561
|
-
const parsed =
|
|
707
|
+
const callbackInput = await promptOAuthCallbackValue("Paste the full redirect URL (recommended). You can also paste code#state or just the code: ");
|
|
708
|
+
const parsed = parseAuthorizationInputForFlow(callbackInput, state);
|
|
709
|
+
if (parsed.stateStatus === "mismatch") {
|
|
710
|
+
console.log("\nOAuth state mismatch. Paste the redirect URL from this login session (the one shown above).\n");
|
|
711
|
+
return { type: "failed" };
|
|
712
|
+
}
|
|
713
|
+
if (parsed.stateStatus === "missing") {
|
|
714
|
+
console.log("\nWarning: redirect state not provided. For best security, paste the full redirect URL.\n");
|
|
715
|
+
}
|
|
562
716
|
if (!parsed.code)
|
|
563
717
|
return { type: "failed" };
|
|
564
718
|
return await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
|
|
@@ -571,7 +725,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
571
725
|
};
|
|
572
726
|
const authenticated = [];
|
|
573
727
|
let startFresh = true;
|
|
574
|
-
let existingStorage = await loadAccounts();
|
|
728
|
+
let existingStorage = repairedStorage ?? (await loadAccounts());
|
|
575
729
|
if (existingStorage && existingStorage.accounts.length > 0) {
|
|
576
730
|
const needsHydration = needsIdentityHydration(existingStorage.accounts);
|
|
577
731
|
if (needsHydration) {
|
|
@@ -609,7 +763,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
609
763
|
break;
|
|
610
764
|
const toggled = toggleAccountEnabled(updatedStorage, toggleIndex);
|
|
611
765
|
if (toggled) {
|
|
612
|
-
await saveAccounts(toggled);
|
|
766
|
+
await saveAccounts(toggled, { preserveRefreshTokens: true });
|
|
613
767
|
updatedStorage = toggled;
|
|
614
768
|
}
|
|
615
769
|
}
|
|
@@ -631,7 +785,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
631
785
|
accounts: [],
|
|
632
786
|
activeIndex: 0,
|
|
633
787
|
activeIndexByFamily: {},
|
|
634
|
-
});
|
|
788
|
+
}, { replace: true });
|
|
635
789
|
}
|
|
636
790
|
while (authenticated.length < MAX_ACCOUNTS) {
|
|
637
791
|
console.log(`\n=== OpenAI OAuth (Account ${authenticated.length + 1}) ===`);
|
|
@@ -684,7 +838,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
684
838
|
process.env.SSH_TTY ||
|
|
685
839
|
process.env.OPENCODE_HEADLESS);
|
|
686
840
|
const useManualFlow = isHeadless || process.env.OPENCODE_NO_BROWSER === "1";
|
|
687
|
-
const existingStorage = await loadAccounts();
|
|
841
|
+
const existingStorage = repairedStorage ?? (await loadAccounts());
|
|
688
842
|
const existingCount = existingStorage?.accounts.length ?? 0;
|
|
689
843
|
const { pkce, state, url } = await createAuthorizationFlow();
|
|
690
844
|
let serverInfo = null;
|
|
@@ -730,10 +884,14 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
730
884
|
serverInfo?.close();
|
|
731
885
|
return {
|
|
732
886
|
url,
|
|
733
|
-
instructions: "Visit the URL above, complete OAuth, then paste
|
|
887
|
+
instructions: "Visit the URL above, complete OAuth, then paste the full redirect URL (recommended) or the authorization code.",
|
|
734
888
|
method: "code",
|
|
735
889
|
callback: async (input) => {
|
|
736
|
-
const parsed =
|
|
890
|
+
const parsed = parseAuthorizationInputForFlow(input, state);
|
|
891
|
+
if (parsed.stateStatus === "mismatch") {
|
|
892
|
+
await showToast("OAuth state mismatch. Paste the redirect URL from this login session.", "error", quietMode);
|
|
893
|
+
return { type: "failed" };
|
|
894
|
+
}
|
|
737
895
|
if (!parsed.code)
|
|
738
896
|
return { type: "failed" };
|
|
739
897
|
const tokens = await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
|
|
@@ -759,8 +917,8 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
759
917
|
label: AUTH_LABELS.OAUTH_MANUAL,
|
|
760
918
|
type: "oauth",
|
|
761
919
|
authorize: async () => {
|
|
762
|
-
const { pkce, url } = await createAuthorizationFlow();
|
|
763
|
-
return buildManualOAuthFlow(pkce, url, async (tokens) => {
|
|
920
|
+
const { pkce, state, url } = await createAuthorizationFlow();
|
|
921
|
+
return buildManualOAuthFlow(pkce, state, url, async (tokens) => {
|
|
764
922
|
await persistAccount(tokens);
|
|
765
923
|
});
|
|
766
924
|
},
|
|
@@ -776,27 +934,39 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
776
934
|
description: "List all configured OpenAI OAuth accounts.",
|
|
777
935
|
args: {},
|
|
778
936
|
async execute() {
|
|
937
|
+
configureStorageForCurrentCwd();
|
|
779
938
|
const storage = await loadAccounts();
|
|
780
|
-
const
|
|
939
|
+
const { scope, storagePath } = getStorageScope();
|
|
940
|
+
const scopeLabel = scope === "project" ? "project" : "global";
|
|
781
941
|
if (!storage || storage.accounts.length === 0) {
|
|
782
942
|
return [
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
`
|
|
943
|
+
`OpenAI Codex Status`,
|
|
944
|
+
``,
|
|
945
|
+
` Scope: ${scopeLabel}`,
|
|
946
|
+
` Accounts: 0`,
|
|
947
|
+
``,
|
|
948
|
+
`Add accounts:`,
|
|
949
|
+
` opencode auth login`,
|
|
950
|
+
``,
|
|
951
|
+
`Storage: ${storagePath}`,
|
|
789
952
|
].join("\n");
|
|
790
953
|
}
|
|
791
954
|
const activeIndex = typeof storage.activeIndex === "number" && Number.isFinite(storage.activeIndex)
|
|
792
955
|
? storage.activeIndex
|
|
793
956
|
: 0;
|
|
794
957
|
const now = Date.now();
|
|
958
|
+
const enabledCount = storage.accounts.filter((a) => a.enabled !== false).length;
|
|
959
|
+
const rateLimitedCount = storage.accounts.filter((a) => a.rateLimitResetTimes &&
|
|
960
|
+
Object.values(a.rateLimitResetTimes).some((t) => typeof t === "number" && t > now)).length;
|
|
795
961
|
const lines = [
|
|
796
|
-
`OpenAI
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
962
|
+
`OpenAI Codex Status`,
|
|
963
|
+
``,
|
|
964
|
+
` Scope: ${scopeLabel}`,
|
|
965
|
+
` Accounts: ${enabledCount}/${storage.accounts.length} enabled`,
|
|
966
|
+
...(rateLimitedCount > 0 ? [` Rate-limited: ${rateLimitedCount}`] : []),
|
|
967
|
+
``,
|
|
968
|
+
` # Label Status`,
|
|
969
|
+
`--- ----------------------------------------- ---------------------`,
|
|
800
970
|
];
|
|
801
971
|
storage.accounts.forEach((account, index) => {
|
|
802
972
|
const label = formatAccountLabel(account, index);
|
|
@@ -813,9 +983,47 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
813
983
|
account.coolingDownUntil > now) {
|
|
814
984
|
statuses.push("cooldown");
|
|
815
985
|
}
|
|
816
|
-
lines.push(`${String(index + 1).padEnd(3)} ${label.padEnd(
|
|
986
|
+
lines.push(`${String(index + 1).padEnd(3)} ${label.padEnd(41)} ${statuses.length > 0 ? statuses.join(", ") : "ok"}`);
|
|
987
|
+
// Add Codex status details
|
|
988
|
+
const codexLines = codexStatus.renderStatus(account);
|
|
989
|
+
lines.push(...codexLines);
|
|
990
|
+
lines.push(""); // Spacer between accounts
|
|
991
|
+
});
|
|
992
|
+
lines.push(`Storage: ${storagePath}`);
|
|
993
|
+
return lines.join("\n");
|
|
994
|
+
},
|
|
995
|
+
}),
|
|
996
|
+
"status-codex": tool({
|
|
997
|
+
description: "Show a compact inline status of all OpenAI Codex accounts.",
|
|
998
|
+
args: {},
|
|
999
|
+
async execute() {
|
|
1000
|
+
configureStorageForCurrentCwd();
|
|
1001
|
+
const storage = await loadAccounts();
|
|
1002
|
+
if (!storage || storage.accounts.length === 0) {
|
|
1003
|
+
return "No OpenAI accounts configured. Run `opencode auth login`.";
|
|
1004
|
+
}
|
|
1005
|
+
const activeIndex = storage.activeIndex ?? 0;
|
|
1006
|
+
const now = Date.now();
|
|
1007
|
+
const lines = ["OpenAI Codex Accounts Status:"];
|
|
1008
|
+
storage.accounts.forEach((account, index) => {
|
|
1009
|
+
const label = formatAccountLabel(account, index);
|
|
1010
|
+
const rateLimited = account.rateLimitResetTimes &&
|
|
1011
|
+
Object.values(account.rateLimitResetTimes).some((t) => typeof t === "number" && t > now);
|
|
1012
|
+
const coolingDown = typeof account.coolingDownUntil === "number" && account.coolingDownUntil > now;
|
|
1013
|
+
const disabled = account.enabled === false;
|
|
1014
|
+
let status = "ok";
|
|
1015
|
+
if (disabled)
|
|
1016
|
+
status = "disabled";
|
|
1017
|
+
else if (rateLimited)
|
|
1018
|
+
status = "rate-limited";
|
|
1019
|
+
else if (coolingDown)
|
|
1020
|
+
status = "cooldown";
|
|
1021
|
+
lines.push(`${index === activeIndex ? "*" : "-"} ${label} [${status}]`);
|
|
1022
|
+
const codexLines = codexStatus.renderStatus(account);
|
|
1023
|
+
if (codexLines.length > 0 && !codexLines[0]?.includes("No Codex status")) {
|
|
1024
|
+
lines.push(...codexLines);
|
|
1025
|
+
}
|
|
817
1026
|
});
|
|
818
|
-
lines.push("", `Storage: ${storePath}`);
|
|
819
1027
|
return lines.join("\n");
|
|
820
1028
|
},
|
|
821
1029
|
}),
|
|
@@ -825,6 +1033,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
825
1033
|
index: tool.schema.number().describe("Account number (1-based)"),
|
|
826
1034
|
},
|
|
827
1035
|
async execute({ index }) {
|
|
1036
|
+
configureStorageForCurrentCwd();
|
|
828
1037
|
const storage = await loadAccounts();
|
|
829
1038
|
if (!storage || storage.accounts.length === 0) {
|
|
830
1039
|
return "No OpenAI accounts configured. Run: opencode auth login";
|
|
@@ -843,7 +1052,7 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
843
1052
|
account.lastUsed = Date.now();
|
|
844
1053
|
account.lastSwitchReason = "rotation";
|
|
845
1054
|
}
|
|
846
|
-
await saveAccounts(storage);
|
|
1055
|
+
await saveAccounts(storage, { preserveRefreshTokens: true });
|
|
847
1056
|
if (cachedAccountManager) {
|
|
848
1057
|
cachedAccountManager.setActiveIndex(targetIndex);
|
|
849
1058
|
await cachedAccountManager.saveToDisk();
|
|
@@ -851,6 +1060,37 @@ export const OpenAIAuthPlugin = async ({ client }) => {
|
|
|
851
1060
|
return `Switched to ${formatAccountLabel(account, targetIndex)}`;
|
|
852
1061
|
},
|
|
853
1062
|
}),
|
|
1063
|
+
"openai-accounts-toggle": tool({
|
|
1064
|
+
description: "Enable or disable an OpenAI account by index (1-based).",
|
|
1065
|
+
args: {
|
|
1066
|
+
index: tool.schema.number().describe("Account number (1-based)"),
|
|
1067
|
+
},
|
|
1068
|
+
async execute({ index }) {
|
|
1069
|
+
configureStorageForCurrentCwd();
|
|
1070
|
+
const storage = await loadAccounts();
|
|
1071
|
+
if (!storage || storage.accounts.length === 0) {
|
|
1072
|
+
return "No OpenAI accounts configured. Run: opencode auth login";
|
|
1073
|
+
}
|
|
1074
|
+
const targetIndex = Math.floor((index ?? 0) - 1);
|
|
1075
|
+
if (targetIndex < 0 || targetIndex >= storage.accounts.length) {
|
|
1076
|
+
return `Invalid account number: ${index}\nValid range: 1-${storage.accounts.length}`;
|
|
1077
|
+
}
|
|
1078
|
+
const updated = toggleAccountEnabled(storage, targetIndex);
|
|
1079
|
+
if (!updated) {
|
|
1080
|
+
return `Failed to toggle account number: ${index}`;
|
|
1081
|
+
}
|
|
1082
|
+
await saveAccounts(updated, { preserveRefreshTokens: true });
|
|
1083
|
+
const account = updated.accounts[targetIndex];
|
|
1084
|
+
if (cachedAccountManager) {
|
|
1085
|
+
const live = cachedAccountManager.getAccountByIndex(targetIndex);
|
|
1086
|
+
if (live)
|
|
1087
|
+
live.enabled = account?.enabled !== false;
|
|
1088
|
+
}
|
|
1089
|
+
const enabled = account?.enabled !== false;
|
|
1090
|
+
const verb = enabled ? "Enabled" : "Disabled";
|
|
1091
|
+
return `${verb} ${formatAccountLabel(account, targetIndex)} (${targetIndex + 1}/${updated.accounts.length})`;
|
|
1092
|
+
},
|
|
1093
|
+
}),
|
|
854
1094
|
},
|
|
855
1095
|
};
|
|
856
1096
|
};
|