opencode-codex-multi-account 0.1.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.
Files changed (3) hide show
  1. package/README.md +33 -0
  2. package/dist/index.js +3233 -0
  3. package/package.json +66 -0
package/dist/index.js ADDED
@@ -0,0 +1,3233 @@
1
+ // src/index.ts
2
+ import { tool } from "@opencode-ai/plugin";
3
+
4
+ // ../multi-account-core/src/account-manager.ts
5
+ import { randomUUID } from "node:crypto";
6
+
7
+ // ../multi-account-core/src/claims.ts
8
+ import { promises as fs2 } from "node:fs";
9
+ import { randomBytes as randomBytes2 } from "node:crypto";
10
+ import { dirname as dirname2, join as join3 } from "node:path";
11
+
12
+ // ../multi-account-core/src/utils.ts
13
+ import { join as join2 } from "node:path";
14
+ import { homedir as homedir2 } from "node:os";
15
+
16
+ // ../multi-account-core/src/config.ts
17
+ import { promises as fs } from "node:fs";
18
+ import { randomBytes } from "node:crypto";
19
+ import { dirname, join } from "node:path";
20
+ import { homedir } from "node:os";
21
+ import * as v2 from "valibot";
22
+
23
+ // ../multi-account-core/src/types.ts
24
+ import * as v from "valibot";
25
+ var OAuthCredentialsSchema = v.object({
26
+ type: v.literal("oauth"),
27
+ refresh: v.string(),
28
+ access: v.string(),
29
+ expires: v.number()
30
+ });
31
+ var UsageLimitEntrySchema = v.object({
32
+ utilization: v.number(),
33
+ resets_at: v.nullable(v.string())
34
+ });
35
+ var UsageLimitsSchema = v.object({
36
+ five_hour: v.optional(v.nullable(UsageLimitEntrySchema), null),
37
+ seven_day: v.optional(v.nullable(UsageLimitEntrySchema), null),
38
+ seven_day_sonnet: v.optional(v.nullable(UsageLimitEntrySchema), null)
39
+ });
40
+ var CredentialRefreshPatchSchema = v.object({
41
+ accessToken: v.string(),
42
+ expiresAt: v.number(),
43
+ refreshToken: v.optional(v.string()),
44
+ uuid: v.optional(v.string()),
45
+ accountId: v.optional(v.string()),
46
+ email: v.optional(v.string())
47
+ });
48
+ var StoredAccountSchema = v.object({
49
+ uuid: v.optional(v.string()),
50
+ accountId: v.optional(v.string()),
51
+ label: v.optional(v.string()),
52
+ email: v.optional(v.string()),
53
+ planTier: v.optional(v.string(), ""),
54
+ refreshToken: v.string(),
55
+ accessToken: v.optional(v.string()),
56
+ expiresAt: v.optional(v.number()),
57
+ addedAt: v.number(),
58
+ lastUsed: v.number(),
59
+ enabled: v.optional(v.boolean(), true),
60
+ rateLimitResetAt: v.optional(v.number()),
61
+ cachedUsage: v.optional(UsageLimitsSchema),
62
+ cachedUsageAt: v.optional(v.number()),
63
+ consecutiveAuthFailures: v.optional(v.number(), 0),
64
+ isAuthDisabled: v.optional(v.boolean(), false),
65
+ authDisabledReason: v.optional(v.string())
66
+ });
67
+ var AccountStorageSchema = v.object({
68
+ version: v.literal(1),
69
+ accounts: v.optional(v.array(StoredAccountSchema), []),
70
+ activeAccountUuid: v.optional(v.string())
71
+ });
72
+ var AccountSelectionStrategySchema = v.picklist(["sticky", "round-robin", "hybrid"]);
73
+ var PluginConfigSchema = v.object({
74
+ account_selection_strategy: v.optional(AccountSelectionStrategySchema, "sticky"),
75
+ cross_process_claims: v.optional(v.boolean(), true),
76
+ soft_quota_threshold_percent: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(100)), 100),
77
+ rate_limit_min_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
78
+ default_retry_after_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 6e4),
79
+ max_consecutive_auth_failures: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 3),
80
+ token_failure_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
81
+ proactive_refresh: v.optional(v.boolean(), true),
82
+ proactive_refresh_buffer_seconds: v.optional(v.pipe(v.number(), v.minValue(60)), 1800),
83
+ proactive_refresh_interval_seconds: v.optional(v.pipe(v.number(), v.minValue(30)), 300),
84
+ quiet_mode: v.optional(v.boolean(), false),
85
+ debug: v.optional(v.boolean(), false)
86
+ });
87
+
88
+ // ../multi-account-core/src/config.ts
89
+ var DEFAULT_CONFIG_FILENAME = "multiauth-config.json";
90
+ var DEFAULT_CONFIG = v2.parse(PluginConfigSchema, {});
91
+ var configFilename = DEFAULT_CONFIG_FILENAME;
92
+ var cachedConfig = null;
93
+ var externalConfigGetter = null;
94
+ function getConfigDir() {
95
+ return process.env.OPENCODE_CONFIG_DIR || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
96
+ }
97
+ function getConfigPath() {
98
+ return join(getConfigDir(), configFilename);
99
+ }
100
+ function parseConfig(raw) {
101
+ const result = v2.safeParse(PluginConfigSchema, raw);
102
+ return result.success ? result.output : DEFAULT_CONFIG;
103
+ }
104
+ function initCoreConfig(filename) {
105
+ configFilename = filename || DEFAULT_CONFIG_FILENAME;
106
+ cachedConfig = null;
107
+ }
108
+ async function loadConfig() {
109
+ if (cachedConfig) return cachedConfig;
110
+ const path = getConfigPath();
111
+ try {
112
+ const content = await fs.readFile(path, "utf-8");
113
+ cachedConfig = parseConfig(JSON.parse(content));
114
+ } catch {
115
+ cachedConfig = DEFAULT_CONFIG;
116
+ }
117
+ return cachedConfig;
118
+ }
119
+ function getConfig() {
120
+ if (cachedConfig) return cachedConfig;
121
+ if (externalConfigGetter && externalConfigGetter !== getConfig) {
122
+ try {
123
+ return parseConfig(externalConfigGetter());
124
+ } catch {
125
+ return DEFAULT_CONFIG;
126
+ }
127
+ }
128
+ return DEFAULT_CONFIG;
129
+ }
130
+ function setConfigGetter(getter) {
131
+ if (getter === getConfig) {
132
+ return;
133
+ }
134
+ externalConfigGetter = getter;
135
+ }
136
+ async function updateConfigField(key, value) {
137
+ const path = getConfigPath();
138
+ let existing = {};
139
+ try {
140
+ const content2 = await fs.readFile(path, "utf-8");
141
+ existing = JSON.parse(content2);
142
+ } catch {
143
+ }
144
+ existing[key] = value;
145
+ await fs.mkdir(dirname(path), { recursive: true });
146
+ const content = `${JSON.stringify(existing, null, 2)}
147
+ `;
148
+ const tempPath = `${path}.${randomBytes(8).toString("hex")}.tmp`;
149
+ try {
150
+ await fs.writeFile(tempPath, content, "utf-8");
151
+ await fs.rename(tempPath, path);
152
+ } catch (error) {
153
+ try {
154
+ await fs.unlink(tempPath);
155
+ } catch {
156
+ }
157
+ throw error;
158
+ }
159
+ cachedConfig = null;
160
+ await loadConfig();
161
+ }
162
+
163
+ // ../multi-account-core/src/utils.ts
164
+ function getConfigDir2() {
165
+ return process.env.OPENCODE_CONFIG_DIR || join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "opencode");
166
+ }
167
+ function getErrorCode(error) {
168
+ if (typeof error !== "object" || error === null || !("code" in error)) {
169
+ return void 0;
170
+ }
171
+ const code = error.code;
172
+ return typeof code === "string" ? code : void 0;
173
+ }
174
+ function formatWaitTime(ms) {
175
+ const totalSeconds = Math.ceil(ms / 1e3);
176
+ if (totalSeconds < 60) return `${totalSeconds}s`;
177
+ const days = Math.floor(totalSeconds / 86400);
178
+ const hours = Math.floor(totalSeconds % 86400 / 3600);
179
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
180
+ const seconds = totalSeconds % 60;
181
+ const parts = [];
182
+ if (days > 0) parts.push(`${days}d`);
183
+ if (hours > 0) parts.push(`${hours}h`);
184
+ if (minutes > 0) parts.push(`${minutes}m`);
185
+ if (seconds > 0 && days === 0) parts.push(`${seconds}s`);
186
+ return parts.join(" ") || "0s";
187
+ }
188
+ function getAccountLabel(account) {
189
+ if (account.label) return account.label;
190
+ if (account.email) return account.email;
191
+ if (account.uuid) return `Account (${account.uuid.slice(0, 8)})`;
192
+ return `Account ${account.index + 1}`;
193
+ }
194
+ function sleep(ms) {
195
+ return new Promise((resolve) => setTimeout(resolve, ms));
196
+ }
197
+ async function showToast(client, message, variant) {
198
+ if (getConfig().quiet_mode) return;
199
+ try {
200
+ await client.tui.showToast({ body: { message, variant } });
201
+ } catch {
202
+ }
203
+ }
204
+ function debugLog(client, message, extra) {
205
+ if (!getConfig().debug) return;
206
+ client.app.log({
207
+ body: { service: "claude-multiauth", level: "debug", message, extra }
208
+ }).catch(() => {
209
+ });
210
+ }
211
+ function createMinimalClient() {
212
+ return {
213
+ auth: {
214
+ set: async () => {
215
+ }
216
+ },
217
+ tui: {
218
+ showToast: async () => {
219
+ }
220
+ },
221
+ app: {
222
+ log: async () => {
223
+ }
224
+ }
225
+ };
226
+ }
227
+
228
+ // ../multi-account-core/src/claims.ts
229
+ var CLAIMS_FILENAME = "multiauth-claims.json";
230
+ var CLAIM_EXPIRY_MS = 6e4;
231
+ function getClaimsPath() {
232
+ return join3(getConfigDir2(), CLAIMS_FILENAME);
233
+ }
234
+ function isClaimShape(value) {
235
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
236
+ const claim = value;
237
+ return typeof claim.pid === "number" && Number.isInteger(claim.pid) && claim.pid > 0 && typeof claim.at === "number" && Number.isFinite(claim.at);
238
+ }
239
+ function parseClaims(raw) {
240
+ let parsed;
241
+ try {
242
+ parsed = JSON.parse(raw);
243
+ } catch {
244
+ return {};
245
+ }
246
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
247
+ return {};
248
+ }
249
+ const claims = {};
250
+ for (const [accountId, claim] of Object.entries(parsed)) {
251
+ if (isClaimShape(claim)) {
252
+ claims[accountId] = claim;
253
+ }
254
+ }
255
+ return claims;
256
+ }
257
+ function isProcessAlive(pid) {
258
+ try {
259
+ process.kill(pid, 0);
260
+ return true;
261
+ } catch {
262
+ return false;
263
+ }
264
+ }
265
+ function cleanClaims(claims, now) {
266
+ const cleaned = {};
267
+ let changed = false;
268
+ for (const [accountId, claim] of Object.entries(claims)) {
269
+ const expiredByTime = now - claim.at > CLAIM_EXPIRY_MS;
270
+ const zombieClaim = !isProcessAlive(claim.pid);
271
+ if (expiredByTime || zombieClaim) {
272
+ changed = true;
273
+ continue;
274
+ }
275
+ cleaned[accountId] = claim;
276
+ }
277
+ return { cleaned, changed };
278
+ }
279
+ async function writeClaimsFile(claims) {
280
+ const path = getClaimsPath();
281
+ const tempPath = `${path}.${randomBytes2(6).toString("hex")}.tmp`;
282
+ await fs2.mkdir(dirname2(path), { recursive: true });
283
+ try {
284
+ await fs2.writeFile(tempPath, JSON.stringify(claims, null, 2), { encoding: "utf-8", mode: 384 });
285
+ await fs2.rename(tempPath, path);
286
+ } catch (error) {
287
+ try {
288
+ await fs2.unlink(tempPath);
289
+ } catch {
290
+ }
291
+ throw error;
292
+ }
293
+ }
294
+ async function readClaims() {
295
+ try {
296
+ const data = await fs2.readFile(getClaimsPath(), "utf-8");
297
+ const parsed = parseClaims(data);
298
+ const now = Date.now();
299
+ const { cleaned, changed } = cleanClaims(parsed, now);
300
+ if (changed) {
301
+ try {
302
+ await writeClaimsFile(cleaned);
303
+ } catch {
304
+ }
305
+ }
306
+ return cleaned;
307
+ } catch {
308
+ return {};
309
+ }
310
+ }
311
+ async function writeClaim(accountId) {
312
+ const now = Date.now();
313
+ const claims = await readClaims();
314
+ const { cleaned } = cleanClaims(claims, now);
315
+ cleaned[accountId] = { pid: process.pid, at: now };
316
+ try {
317
+ await writeClaimsFile(cleaned);
318
+ } catch {
319
+ }
320
+ }
321
+ function isClaimedByOther(claims, accountId) {
322
+ if (!accountId) return false;
323
+ const claim = claims[accountId];
324
+ if (!claim) return false;
325
+ if (Date.now() - claim.at > CLAIM_EXPIRY_MS) return false;
326
+ if (!isProcessAlive(claim.pid)) return false;
327
+ return claim.pid !== process.pid;
328
+ }
329
+
330
+ // ../multi-account-core/src/account-manager.ts
331
+ var STARTUP_REFRESH_CONCURRENCY = 3;
332
+ var RECENT_429_COOLDOWN_MS = 3e4;
333
+ var HYBRID_SWITCH_MARGIN = 40;
334
+ function createAccountManagerForProvider(dependencies) {
335
+ const {
336
+ providerAuthId,
337
+ isTokenExpired: isTokenExpired2,
338
+ refreshToken: refreshToken2
339
+ } = dependencies;
340
+ return class AccountManager2 {
341
+ constructor(store) {
342
+ this.store = store;
343
+ }
344
+ cached = [];
345
+ activeAccountUuid;
346
+ client = null;
347
+ runtimeFactory = null;
348
+ roundRobinCursor = 0;
349
+ last429Map = /* @__PURE__ */ new Map();
350
+ static async create(store, currentAuth, client) {
351
+ const manager = new AccountManager2(store);
352
+ await manager.initialize(currentAuth, client);
353
+ return manager;
354
+ }
355
+ async initialize(currentAuth, client) {
356
+ if (client) this.client = client;
357
+ const storage = await this.store.load();
358
+ if (storage.accounts.length > 0) {
359
+ this.cached = storage.accounts.map((account, index) => this.toManagedAccount(account, index));
360
+ this.activeAccountUuid = storage.activeAccountUuid;
361
+ if (!this.getActiveAccount() && this.cached.length > 0) {
362
+ this.activeAccountUuid = this.cached[0].uuid;
363
+ }
364
+ return;
365
+ }
366
+ if (currentAuth.refresh) {
367
+ const newAccount = this.createNewAccount(currentAuth, Date.now());
368
+ await this.store.addAccount(newAccount);
369
+ await this.store.setActiveUuid(newAccount.uuid);
370
+ this.cached = [this.toManagedAccount(newAccount, 0)];
371
+ this.activeAccountUuid = newAccount.uuid;
372
+ }
373
+ }
374
+ async refresh() {
375
+ const storage = await this.store.load();
376
+ this.cached = storage.accounts.map((account, index) => this.toManagedAccount(account, index));
377
+ if (storage.activeAccountUuid) {
378
+ this.activeAccountUuid = storage.activeAccountUuid;
379
+ }
380
+ }
381
+ toManagedAccount(storedAccount, index) {
382
+ return {
383
+ index,
384
+ uuid: storedAccount.uuid,
385
+ accountId: storedAccount.accountId,
386
+ label: storedAccount.label,
387
+ email: storedAccount.email,
388
+ planTier: storedAccount.planTier,
389
+ refreshToken: storedAccount.refreshToken,
390
+ accessToken: storedAccount.accessToken,
391
+ expiresAt: storedAccount.expiresAt,
392
+ addedAt: storedAccount.addedAt,
393
+ lastUsed: storedAccount.lastUsed,
394
+ enabled: storedAccount.enabled,
395
+ rateLimitResetAt: storedAccount.rateLimitResetAt,
396
+ cachedUsage: storedAccount.cachedUsage,
397
+ cachedUsageAt: storedAccount.cachedUsageAt,
398
+ consecutiveAuthFailures: storedAccount.consecutiveAuthFailures,
399
+ isAuthDisabled: storedAccount.isAuthDisabled,
400
+ authDisabledReason: storedAccount.authDisabledReason,
401
+ last429At: storedAccount.uuid ? this.last429Map.get(storedAccount.uuid) : void 0
402
+ };
403
+ }
404
+ createNewAccount(auth, now) {
405
+ return {
406
+ uuid: randomUUID(),
407
+ refreshToken: auth.refresh,
408
+ accessToken: auth.access,
409
+ expiresAt: auth.expires,
410
+ addedAt: now,
411
+ lastUsed: now,
412
+ enabled: true,
413
+ planTier: "",
414
+ consecutiveAuthFailures: 0,
415
+ isAuthDisabled: false
416
+ };
417
+ }
418
+ getAccountCount() {
419
+ return this.getEligibleAccounts().length;
420
+ }
421
+ getAccounts() {
422
+ return [...this.cached];
423
+ }
424
+ getActiveAccount() {
425
+ if (this.activeAccountUuid) {
426
+ return this.cached.find((account) => account.uuid === this.activeAccountUuid) ?? null;
427
+ }
428
+ return this.cached[0] ?? null;
429
+ }
430
+ setClient(client) {
431
+ this.client = client;
432
+ }
433
+ setRuntimeFactory(factory) {
434
+ this.runtimeFactory = factory;
435
+ }
436
+ getEligibleAccounts() {
437
+ return this.cached.filter((account) => account.uuid && account.enabled && !account.isAuthDisabled);
438
+ }
439
+ exceedsSoftQuota(account) {
440
+ const threshold = getConfig().soft_quota_threshold_percent;
441
+ if (threshold >= 100) return false;
442
+ const usage = account.cachedUsage;
443
+ if (!usage) return false;
444
+ const tiers = [usage.five_hour, usage.seven_day];
445
+ return tiers.some((tier) => tier != null && tier.utilization >= threshold);
446
+ }
447
+ hasAnyUsableAccount() {
448
+ return this.getEligibleAccounts().length > 0;
449
+ }
450
+ isRateLimited(account) {
451
+ if (account.rateLimitResetAt && Date.now() < account.rateLimitResetAt) {
452
+ return true;
453
+ }
454
+ return this.isUsageExhausted(account);
455
+ }
456
+ isUsageExhausted(account) {
457
+ const usage = account.cachedUsage;
458
+ if (!usage) return false;
459
+ const now = Date.now();
460
+ const tiers = [usage.five_hour, usage.seven_day];
461
+ return tiers.some(
462
+ (tier) => tier != null && tier.utilization >= 100 && tier.resets_at != null && Date.parse(tier.resets_at) > now
463
+ );
464
+ }
465
+ clearExpiredRateLimits() {
466
+ const now = Date.now();
467
+ for (const account of this.cached) {
468
+ if (account.rateLimitResetAt && now >= account.rateLimitResetAt) {
469
+ account.rateLimitResetAt = void 0;
470
+ }
471
+ }
472
+ }
473
+ getMinWaitTime() {
474
+ const eligible = this.getEligibleAccounts();
475
+ const available = eligible.filter((account) => !this.isRateLimited(account));
476
+ if (available.length > 0) return 0;
477
+ const now = Date.now();
478
+ const waits = [];
479
+ for (const account of eligible) {
480
+ if (account.rateLimitResetAt) {
481
+ const ms = account.rateLimitResetAt - now;
482
+ if (ms > 0) waits.push(ms);
483
+ }
484
+ const usageResetMs = this.getUsageResetMs(account);
485
+ if (usageResetMs !== null && usageResetMs > 0) {
486
+ waits.push(usageResetMs);
487
+ }
488
+ }
489
+ return waits.length > 0 ? Math.min(...waits) : 0;
490
+ }
491
+ getUsageResetMs(account) {
492
+ const usage = account.cachedUsage;
493
+ if (!usage) return null;
494
+ const now = Date.now();
495
+ const candidates = [];
496
+ const tiers = [usage.five_hour, usage.seven_day];
497
+ for (const tier of tiers) {
498
+ if (tier != null && tier.utilization >= 100 && tier.resets_at != null) {
499
+ const ms = Date.parse(tier.resets_at) - now;
500
+ if (ms > 0) candidates.push(ms);
501
+ }
502
+ }
503
+ return candidates.length > 0 ? Math.min(...candidates) : null;
504
+ }
505
+ async selectAccount() {
506
+ await this.refresh();
507
+ this.clearExpiredRateLimits();
508
+ const eligible = this.getEligibleAccounts();
509
+ if (eligible.length === 0) return null;
510
+ const config = getConfig();
511
+ const claims = config.cross_process_claims ? await readClaims() : {};
512
+ const strategy = config.account_selection_strategy;
513
+ let selected;
514
+ switch (strategy) {
515
+ case "round-robin":
516
+ selected = this.selectRoundRobin(eligible, claims);
517
+ break;
518
+ case "hybrid":
519
+ selected = this.selectHybrid(eligible, claims);
520
+ break;
521
+ case "sticky":
522
+ default:
523
+ selected = this.selectSticky(eligible, claims);
524
+ break;
525
+ }
526
+ if (selected?.uuid) {
527
+ this.activeAccountUuid = selected.uuid;
528
+ this.store.setActiveUuid(selected.uuid).catch(() => {
529
+ });
530
+ }
531
+ if (config.cross_process_claims && selected?.uuid) {
532
+ writeClaim(selected.uuid).catch(() => {
533
+ });
534
+ }
535
+ return selected;
536
+ }
537
+ isUsable(account) {
538
+ return !this.isRateLimited(account) && !this.isInRecentCooldown(account) && !this.exceedsSoftQuota(account);
539
+ }
540
+ isInRecentCooldown(account) {
541
+ if (!account.last429At) return false;
542
+ return Date.now() - account.last429At < RECENT_429_COOLDOWN_MS;
543
+ }
544
+ fallbackNotRateLimited(eligible) {
545
+ const account = eligible.find((candidate) => !this.isRateLimited(candidate));
546
+ if (account) {
547
+ this.activateAccount(account);
548
+ return account;
549
+ }
550
+ return null;
551
+ }
552
+ selectSticky(eligible, claims) {
553
+ const current = this.getActiveAccount();
554
+ if (current?.enabled && !current.isAuthDisabled && this.isUsable(current)) {
555
+ this.activateAccount(current);
556
+ return current;
557
+ }
558
+ const unclaimed = eligible.find(
559
+ (account) => this.isUsable(account) && !isClaimedByOther(claims, account.uuid)
560
+ );
561
+ if (unclaimed) {
562
+ this.activateAccount(unclaimed);
563
+ return unclaimed;
564
+ }
565
+ const available = eligible.find((account) => this.isUsable(account));
566
+ if (available) {
567
+ this.activateAccount(available);
568
+ return available;
569
+ }
570
+ return this.fallbackNotRateLimited(eligible);
571
+ }
572
+ selectRoundRobin(eligible, claims) {
573
+ for (let i = 0; i < eligible.length; i++) {
574
+ const index = (this.roundRobinCursor + i) % eligible.length;
575
+ const account = eligible[index];
576
+ if (this.isUsable(account) && !isClaimedByOther(claims, account.uuid)) {
577
+ this.roundRobinCursor = (index + 1) % eligible.length;
578
+ this.activateAccount(account);
579
+ return account;
580
+ }
581
+ }
582
+ for (let i = 0; i < eligible.length; i++) {
583
+ const index = (this.roundRobinCursor + i) % eligible.length;
584
+ const account = eligible[index];
585
+ if (this.isUsable(account)) {
586
+ this.roundRobinCursor = (index + 1) % eligible.length;
587
+ this.activateAccount(account);
588
+ return account;
589
+ }
590
+ }
591
+ return this.fallbackNotRateLimited(eligible);
592
+ }
593
+ selectHybrid(eligible, claims) {
594
+ const usable = eligible.filter((account) => this.isUsable(account));
595
+ const pool = usable.length > 0 ? usable : eligible.filter((account) => !this.isRateLimited(account));
596
+ if (pool.length === 0) return null;
597
+ const activeUuid = this.activeAccountUuid;
598
+ let best = pool[0];
599
+ let bestScore = this.calculateHybridScore(best, best.uuid === activeUuid, claims);
600
+ for (let i = 1; i < pool.length; i++) {
601
+ const account = pool[i];
602
+ const score = this.calculateHybridScore(account, account.uuid === activeUuid, claims);
603
+ if (score > bestScore) {
604
+ best = account;
605
+ bestScore = score;
606
+ }
607
+ }
608
+ const current = pool.find((account) => account.uuid === activeUuid);
609
+ if (current && current !== best) {
610
+ const currentScore = this.calculateHybridScore(current, true, claims);
611
+ const bestWithoutStickiness = this.calculateHybridScore(best, false, claims);
612
+ if (bestWithoutStickiness <= currentScore + HYBRID_SWITCH_MARGIN) {
613
+ this.activateAccount(current);
614
+ return current;
615
+ }
616
+ }
617
+ this.activateAccount(best);
618
+ return best;
619
+ }
620
+ calculateHybridScore(account, isActive, claims) {
621
+ const maxUtilization = Math.min(100, Math.max(0, this.getMaxUtilization(account)));
622
+ const usageScore = (100 - maxUtilization) / 100 * 450;
623
+ const maxFailures = Math.max(1, getConfig().max_consecutive_auth_failures);
624
+ const healthScore = Math.max(0, (maxFailures - account.consecutiveAuthFailures) / maxFailures * 250);
625
+ const secondsSinceUsed = (Date.now() - account.lastUsed) / 1e3;
626
+ const freshnessScore = Math.min(secondsSinceUsed, 900) / 900 * 60;
627
+ const stickinessBonus = isActive ? 120 : 0;
628
+ const claimPenalty = isClaimedByOther(claims, account.uuid) ? -200 : 0;
629
+ return usageScore + healthScore + freshnessScore + stickinessBonus + claimPenalty;
630
+ }
631
+ getMaxUtilization(account) {
632
+ const usage = account.cachedUsage;
633
+ if (!usage) return 65;
634
+ const tiers = [usage.five_hour, usage.seven_day];
635
+ const utilizations = tiers.filter((tier) => tier != null).map((tier) => tier.utilization);
636
+ return utilizations.length > 0 ? Math.max(...utilizations) : 65;
637
+ }
638
+ activateAccount(account) {
639
+ this.activeAccountUuid = account.uuid;
640
+ account.lastUsed = Date.now();
641
+ }
642
+ async markRateLimited(uuid, backoffMs) {
643
+ const effectiveBackoff = backoffMs ?? getConfig().rate_limit_min_backoff_ms;
644
+ this.last429Map.set(uuid, Date.now());
645
+ await this.store.mutateAccount(uuid, (account) => {
646
+ account.rateLimitResetAt = Date.now() + effectiveBackoff;
647
+ });
648
+ }
649
+ async markRevoked(uuid) {
650
+ await this.store.mutateAccount(uuid, (account) => {
651
+ account.isAuthDisabled = true;
652
+ account.authDisabledReason = "OAuth token revoked (403)";
653
+ account.accessToken = void 0;
654
+ account.expiresAt = void 0;
655
+ });
656
+ this.runtimeFactory?.invalidate(uuid);
657
+ }
658
+ async markSuccess(uuid) {
659
+ this.last429Map.delete(uuid);
660
+ await this.store.mutateAccount(uuid, (account) => {
661
+ account.rateLimitResetAt = void 0;
662
+ account.consecutiveAuthFailures = 0;
663
+ account.lastUsed = Date.now();
664
+ });
665
+ }
666
+ syncToOpenCode(account) {
667
+ if (!this.client || !account.accessToken || !account.expiresAt) return;
668
+ this.client.auth.set({
669
+ path: { id: providerAuthId },
670
+ body: {
671
+ type: "oauth",
672
+ refresh: account.refreshToken,
673
+ access: account.accessToken,
674
+ expires: account.expiresAt
675
+ }
676
+ }).catch(() => {
677
+ });
678
+ }
679
+ async markAuthFailure(uuid, result) {
680
+ await this.store.mutateStorage((storage) => {
681
+ const account = storage.accounts.find((entry) => entry.uuid === uuid);
682
+ if (!account) return;
683
+ if (!result.ok && result.permanent) {
684
+ account.isAuthDisabled = true;
685
+ account.authDisabledReason = "Token permanently rejected (400/401/403)";
686
+ return;
687
+ }
688
+ account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
689
+ const maxFailures = getConfig().max_consecutive_auth_failures;
690
+ const usableCount = storage.accounts.filter(
691
+ (entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== uuid
692
+ ).length;
693
+ if (account.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
694
+ account.isAuthDisabled = true;
695
+ account.authDisabledReason = `${maxFailures} consecutive auth failures`;
696
+ }
697
+ });
698
+ }
699
+ async applyUsageCache(uuid, usage) {
700
+ await this.store.mutateAccount(uuid, (account) => {
701
+ account.cachedUsage = usage;
702
+ account.cachedUsageAt = Date.now();
703
+ });
704
+ }
705
+ async applyProfileCache(uuid, profile) {
706
+ await this.store.mutateAccount(uuid, (account) => {
707
+ account.email = profile.email ?? account.email;
708
+ account.planTier = profile.planTier;
709
+ });
710
+ }
711
+ async ensureValidToken(uuid, client) {
712
+ const credentials = await this.store.readCredentials(uuid);
713
+ if (!credentials) return { ok: false, permanent: true };
714
+ if (credentials.accessToken && credentials.expiresAt && !isTokenExpired2(credentials)) {
715
+ return {
716
+ ok: true,
717
+ patch: { accessToken: credentials.accessToken, expiresAt: credentials.expiresAt }
718
+ };
719
+ }
720
+ const result = await refreshToken2(credentials.refreshToken, uuid, client);
721
+ if (!result.ok) return result;
722
+ const updated = await this.store.mutateAccount(uuid, (account) => {
723
+ account.accessToken = result.patch.accessToken;
724
+ account.expiresAt = result.patch.expiresAt;
725
+ if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
726
+ if (result.patch.uuid && result.patch.uuid !== uuid) account.uuid = result.patch.uuid;
727
+ if (result.patch.accountId) account.accountId = result.patch.accountId;
728
+ if (result.patch.email) account.email = result.patch.email;
729
+ account.consecutiveAuthFailures = 0;
730
+ account.isAuthDisabled = false;
731
+ account.authDisabledReason = void 0;
732
+ });
733
+ if (result.patch.uuid && result.patch.uuid !== uuid && this.activeAccountUuid === uuid) {
734
+ this.activeAccountUuid = result.patch.uuid;
735
+ this.store.setActiveUuid(result.patch.uuid).catch(() => {
736
+ });
737
+ }
738
+ if (updated && (uuid === this.activeAccountUuid || updated.uuid === this.activeAccountUuid)) {
739
+ this.syncToOpenCode(updated);
740
+ }
741
+ return result;
742
+ }
743
+ async validateNonActiveTokens(client) {
744
+ await this.refresh();
745
+ const activeUuid = this.activeAccountUuid;
746
+ const eligible = this.cached.filter(
747
+ (account) => account.enabled && !account.isAuthDisabled && account.uuid && account.uuid !== activeUuid
748
+ );
749
+ for (let i = 0; i < eligible.length; i += STARTUP_REFRESH_CONCURRENCY) {
750
+ const batch = eligible.slice(i, i + STARTUP_REFRESH_CONCURRENCY);
751
+ await Promise.all(
752
+ batch.map(async (account) => {
753
+ if (!account.uuid || !isTokenExpired2(account)) return;
754
+ const result = await this.ensureValidToken(account.uuid, client);
755
+ if (!result.ok) {
756
+ await this.markAuthFailure(account.uuid, result);
757
+ }
758
+ })
759
+ );
760
+ }
761
+ }
762
+ async removeAccount(index) {
763
+ const account = this.cached[index];
764
+ if (!account?.uuid) return false;
765
+ const removed = await this.store.removeAccount(account.uuid);
766
+ if (removed) {
767
+ await this.refresh();
768
+ }
769
+ return removed;
770
+ }
771
+ async clearAllAccounts() {
772
+ await this.store.clear();
773
+ this.cached = [];
774
+ this.activeAccountUuid = void 0;
775
+ }
776
+ async addAccount(auth) {
777
+ if (!auth.refresh) return;
778
+ const existing = this.cached.find((account) => account.refreshToken === auth.refresh);
779
+ if (existing) return;
780
+ const newAccount = this.createNewAccount(auth, Date.now());
781
+ await this.store.addAccount(newAccount);
782
+ this.activeAccountUuid = newAccount.uuid;
783
+ await this.store.setActiveUuid(newAccount.uuid);
784
+ await this.refresh();
785
+ }
786
+ async toggleEnabled(uuid) {
787
+ await this.store.mutateAccount(uuid, (account) => {
788
+ account.enabled = !(account.enabled ?? true);
789
+ if (account.enabled) {
790
+ account.isAuthDisabled = false;
791
+ account.authDisabledReason = void 0;
792
+ account.consecutiveAuthFailures = 0;
793
+ }
794
+ });
795
+ }
796
+ async replaceAccountCredentials(uuid, auth) {
797
+ const updated = await this.store.mutateAccount(uuid, (account) => {
798
+ account.refreshToken = auth.refresh;
799
+ account.accessToken = auth.access;
800
+ account.expiresAt = auth.expires;
801
+ account.lastUsed = Date.now();
802
+ account.enabled = true;
803
+ account.isAuthDisabled = false;
804
+ account.authDisabledReason = void 0;
805
+ account.consecutiveAuthFailures = 0;
806
+ account.rateLimitResetAt = void 0;
807
+ });
808
+ this.runtimeFactory?.invalidate(uuid);
809
+ if (updated && uuid === this.activeAccountUuid) {
810
+ this.syncToOpenCode(updated);
811
+ }
812
+ }
813
+ async retryAuth(uuid, client) {
814
+ await this.store.mutateAccount(uuid, (account) => {
815
+ account.consecutiveAuthFailures = 0;
816
+ account.isAuthDisabled = false;
817
+ account.authDisabledReason = void 0;
818
+ });
819
+ this.runtimeFactory?.invalidate(uuid);
820
+ const credentials = await this.store.readCredentials(uuid);
821
+ if (!credentials) return { ok: false, permanent: true };
822
+ const result = await refreshToken2(credentials.refreshToken, uuid, client);
823
+ if (result.ok) {
824
+ const updated = await this.store.mutateAccount(uuid, (account) => {
825
+ account.accessToken = result.patch.accessToken;
826
+ account.expiresAt = result.patch.expiresAt;
827
+ if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
828
+ if (result.patch.uuid) account.uuid = result.patch.uuid;
829
+ if (result.patch.accountId) account.accountId = result.patch.accountId;
830
+ if (result.patch.email) account.email = result.patch.email;
831
+ account.enabled = true;
832
+ account.consecutiveAuthFailures = 0;
833
+ });
834
+ this.runtimeFactory?.invalidate(uuid);
835
+ if (result.patch.uuid) {
836
+ this.runtimeFactory?.invalidate(result.patch.uuid);
837
+ }
838
+ const nextUuid = result.patch.uuid ?? uuid;
839
+ if (this.activeAccountUuid === uuid && result.patch.uuid && result.patch.uuid !== uuid) {
840
+ this.activeAccountUuid = result.patch.uuid;
841
+ await this.store.setActiveUuid(result.patch.uuid);
842
+ }
843
+ if (updated && (uuid === this.activeAccountUuid || nextUuid === this.activeAccountUuid)) {
844
+ const freshCredentials = await this.store.readCredentials(nextUuid);
845
+ if (freshCredentials) {
846
+ this.syncToOpenCode({
847
+ refreshToken: freshCredentials.refreshToken,
848
+ accessToken: freshCredentials.accessToken,
849
+ expiresAt: freshCredentials.expiresAt
850
+ });
851
+ }
852
+ }
853
+ } else {
854
+ await this.markAuthFailure(uuid, result);
855
+ this.runtimeFactory?.invalidate(uuid);
856
+ }
857
+ return result;
858
+ }
859
+ };
860
+ }
861
+
862
+ // ../multi-account-core/src/account-store.ts
863
+ import { promises as fs4 } from "node:fs";
864
+ import { randomBytes as randomBytes3 } from "node:crypto";
865
+ import { dirname as dirname4, join as join5 } from "node:path";
866
+ import lockfile from "proper-lockfile";
867
+ import * as v4 from "valibot";
868
+
869
+ // ../multi-account-core/src/storage.ts
870
+ import { promises as fs3 } from "node:fs";
871
+ import { dirname as dirname3, join as join4 } from "node:path";
872
+ import * as v3 from "valibot";
873
+
874
+ // ../multi-account-core/src/constants.ts
875
+ var DEFAULT_ACCOUNTS_FILENAME = "multiauth-accounts.json";
876
+ var ACCOUNTS_FILENAME = DEFAULT_ACCOUNTS_FILENAME;
877
+ function setAccountsFilename(filename) {
878
+ if (!filename) {
879
+ ACCOUNTS_FILENAME = DEFAULT_ACCOUNTS_FILENAME;
880
+ return;
881
+ }
882
+ ACCOUNTS_FILENAME = filename;
883
+ }
884
+
885
+ // ../multi-account-core/src/storage.ts
886
+ function getStoragePath() {
887
+ return join4(getConfigDir2(), ACCOUNTS_FILENAME);
888
+ }
889
+ async function backupCorruptFile(targetPath, content) {
890
+ const backupPath = `${targetPath}.corrupt.${Date.now()}.bak`;
891
+ await fs3.mkdir(dirname3(backupPath), { recursive: true });
892
+ await fs3.writeFile(backupPath, content, "utf-8");
893
+ }
894
+ async function readStorageFromDisk(targetPath, backupOnCorrupt) {
895
+ let content;
896
+ try {
897
+ content = await fs3.readFile(targetPath, "utf-8");
898
+ } catch (error) {
899
+ if (getErrorCode(error) === "ENOENT") {
900
+ return null;
901
+ }
902
+ throw error;
903
+ }
904
+ let parsed;
905
+ try {
906
+ parsed = JSON.parse(content);
907
+ } catch {
908
+ if (backupOnCorrupt) {
909
+ try {
910
+ await backupCorruptFile(targetPath, content);
911
+ } catch {
912
+ }
913
+ }
914
+ return null;
915
+ }
916
+ const validation = v3.safeParse(AccountStorageSchema, parsed);
917
+ if (!validation.success) {
918
+ if (backupOnCorrupt) {
919
+ try {
920
+ await backupCorruptFile(targetPath, content);
921
+ } catch {
922
+ }
923
+ }
924
+ return null;
925
+ }
926
+ return validation.output;
927
+ }
928
+ function deduplicateAccounts(accounts) {
929
+ const deduplicated = [];
930
+ const indexByUuid = /* @__PURE__ */ new Map();
931
+ for (const account of accounts) {
932
+ if (!account.uuid) {
933
+ deduplicated.push(account);
934
+ continue;
935
+ }
936
+ const existingIndex = indexByUuid.get(account.uuid);
937
+ if (existingIndex === void 0) {
938
+ indexByUuid.set(account.uuid, deduplicated.length);
939
+ deduplicated.push(account);
940
+ continue;
941
+ }
942
+ const existingAccount = deduplicated[existingIndex];
943
+ if (!existingAccount || account.lastUsed >= existingAccount.lastUsed) {
944
+ deduplicated[existingIndex] = account;
945
+ }
946
+ }
947
+ return deduplicated;
948
+ }
949
+ async function loadAccounts() {
950
+ const storagePath = getStoragePath();
951
+ const storage = await readStorageFromDisk(storagePath, true);
952
+ if (!storage) {
953
+ return null;
954
+ }
955
+ return {
956
+ ...storage,
957
+ accounts: deduplicateAccounts(storage.accounts || [])
958
+ };
959
+ }
960
+
961
+ // ../multi-account-core/src/account-store.ts
962
+ var FILE_MODE = 384;
963
+ var LOCK_OPTIONS = {
964
+ stale: 1e4,
965
+ retries: { retries: 10, minTimeout: 50, maxTimeout: 2e3, factor: 2 }
966
+ };
967
+ function getStoragePath2() {
968
+ return join5(getConfigDir2(), ACCOUNTS_FILENAME);
969
+ }
970
+ function createEmptyStorage() {
971
+ return { version: 1, accounts: [] };
972
+ }
973
+ function buildTempPath(targetPath) {
974
+ return `${targetPath}.${randomBytes3(8).toString("hex")}.tmp`;
975
+ }
976
+ async function writeAtomicText(targetPath, content) {
977
+ await fs4.mkdir(dirname4(targetPath), { recursive: true });
978
+ const tempPath = buildTempPath(targetPath);
979
+ try {
980
+ await fs4.writeFile(tempPath, content, { encoding: "utf-8", mode: FILE_MODE });
981
+ await fs4.chmod(tempPath, FILE_MODE);
982
+ await fs4.rename(tempPath, targetPath);
983
+ await fs4.chmod(targetPath, FILE_MODE);
984
+ } catch (error) {
985
+ try {
986
+ await fs4.unlink(tempPath);
987
+ } catch {
988
+ }
989
+ throw error;
990
+ }
991
+ }
992
+ async function writeStorageAtomic(targetPath, storage) {
993
+ const validation = v4.safeParse(AccountStorageSchema, storage);
994
+ if (!validation.success) {
995
+ throw new Error("Invalid account storage payload");
996
+ }
997
+ await writeAtomicText(targetPath, `${JSON.stringify(validation.output, null, 2)}
998
+ `);
999
+ }
1000
+ async function ensureStorageFileExists(targetPath) {
1001
+ await fs4.mkdir(dirname4(targetPath), { recursive: true });
1002
+ const emptyContent = `${JSON.stringify(createEmptyStorage(), null, 2)}
1003
+ `;
1004
+ try {
1005
+ await fs4.writeFile(targetPath, emptyContent, { flag: "wx", mode: FILE_MODE });
1006
+ } catch (error) {
1007
+ if (getErrorCode(error) !== "EEXIST") throw error;
1008
+ }
1009
+ }
1010
+ async function withFileLock(fn) {
1011
+ const storagePath = getStoragePath2();
1012
+ await ensureStorageFileExists(storagePath);
1013
+ let release = null;
1014
+ try {
1015
+ release = await lockfile.lock(storagePath, LOCK_OPTIONS);
1016
+ return await fn(storagePath);
1017
+ } finally {
1018
+ if (release) {
1019
+ try {
1020
+ await release();
1021
+ } catch {
1022
+ }
1023
+ }
1024
+ }
1025
+ }
1026
+ var AccountStore = class {
1027
+ async load() {
1028
+ const storage = await loadAccounts();
1029
+ return storage ?? createEmptyStorage();
1030
+ }
1031
+ async readCredentials(uuid) {
1032
+ const storagePath = getStoragePath2();
1033
+ const storage = await readStorageFromDisk(storagePath, false);
1034
+ if (!storage) return null;
1035
+ const account = storage.accounts.find((a) => a.uuid === uuid);
1036
+ if (!account) return null;
1037
+ return {
1038
+ refreshToken: account.refreshToken,
1039
+ accessToken: account.accessToken,
1040
+ expiresAt: account.expiresAt,
1041
+ accountId: account.accountId
1042
+ };
1043
+ }
1044
+ async mutateAccount(uuid, fn) {
1045
+ return await withFileLock(async (storagePath) => {
1046
+ const current = await readStorageFromDisk(storagePath, false);
1047
+ if (!current) return null;
1048
+ const account = current.accounts.find((a) => a.uuid === uuid);
1049
+ if (!account) return null;
1050
+ fn(account);
1051
+ await writeStorageAtomic(storagePath, current);
1052
+ return { ...account };
1053
+ });
1054
+ }
1055
+ async mutateStorage(fn) {
1056
+ await withFileLock(async (storagePath) => {
1057
+ const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
1058
+ fn(current);
1059
+ await writeStorageAtomic(storagePath, current);
1060
+ });
1061
+ }
1062
+ async addAccount(account) {
1063
+ await withFileLock(async (storagePath) => {
1064
+ const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
1065
+ const exists = current.accounts.some(
1066
+ (a) => a.uuid === account.uuid || a.refreshToken === account.refreshToken
1067
+ );
1068
+ if (exists) return;
1069
+ current.accounts.push(account);
1070
+ await writeStorageAtomic(storagePath, current);
1071
+ });
1072
+ }
1073
+ async removeAccount(uuid) {
1074
+ return await withFileLock(async (storagePath) => {
1075
+ const current = await readStorageFromDisk(storagePath, false);
1076
+ if (!current) return false;
1077
+ const initialLength = current.accounts.length;
1078
+ current.accounts = current.accounts.filter((a) => a.uuid !== uuid);
1079
+ if (current.accounts.length === initialLength) return false;
1080
+ if (current.activeAccountUuid === uuid) {
1081
+ current.activeAccountUuid = current.accounts[0]?.uuid;
1082
+ }
1083
+ await writeStorageAtomic(storagePath, current);
1084
+ return true;
1085
+ });
1086
+ }
1087
+ async setActiveUuid(uuid) {
1088
+ await this.mutateStorage((storage) => {
1089
+ storage.activeAccountUuid = uuid;
1090
+ });
1091
+ }
1092
+ async clear() {
1093
+ await withFileLock(async (storagePath) => {
1094
+ await writeStorageAtomic(storagePath, createEmptyStorage());
1095
+ });
1096
+ }
1097
+ };
1098
+
1099
+ // ../multi-account-core/src/executor.ts
1100
+ var MIN_MAX_RETRIES = 6;
1101
+ var RETRIES_PER_ACCOUNT = 3;
1102
+ var MAX_SERVER_RETRIES_PER_ATTEMPT = 2;
1103
+ var MAX_RESOLVE_ATTEMPTS = 10;
1104
+ var SERVER_RETRY_BASE_MS = 1e3;
1105
+ var SERVER_RETRY_MAX_MS = 4e3;
1106
+ var PERMANENT_AUTH_FAILURE_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
1107
+ function createExecutorForProvider(providerName, dependencies) {
1108
+ const {
1109
+ handleRateLimitResponse: handleRateLimitResponse2,
1110
+ formatWaitTime: formatWaitTime2,
1111
+ sleep: sleep2,
1112
+ showToast: showToast2,
1113
+ getAccountLabel: getAccountLabel2
1114
+ } = dependencies;
1115
+ async function executeWithAccountRotation2(manager, runtimeFactory, client, input, init) {
1116
+ const maxRetries = Math.max(MIN_MAX_RETRIES, manager.getAccountCount() * RETRIES_PER_ACCOUNT);
1117
+ let retries = 0;
1118
+ let previousAccountUuid;
1119
+ while (true) {
1120
+ if (++retries > maxRetries) {
1121
+ throw new Error(
1122
+ `Exhausted ${maxRetries} retries across all accounts. All attempts failed due to auth errors, rate limits, or token issues.`
1123
+ );
1124
+ }
1125
+ await manager.refresh();
1126
+ const account = await resolveAccount(manager, client);
1127
+ const accountUuid = account.uuid;
1128
+ if (!accountUuid) continue;
1129
+ if (previousAccountUuid && accountUuid !== previousAccountUuid && manager.getAccountCount() > 1) {
1130
+ void showToast2(client, `Switched to ${getAccountLabel2(account)}`, "info");
1131
+ }
1132
+ previousAccountUuid = accountUuid;
1133
+ let runtime;
1134
+ let response;
1135
+ try {
1136
+ runtime = await runtimeFactory.getRuntime(accountUuid);
1137
+ response = await runtime.fetch(input, init);
1138
+ } catch (error) {
1139
+ if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1140
+ continue;
1141
+ }
1142
+ void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1143
+ continue;
1144
+ }
1145
+ if (response.status >= 500) {
1146
+ let serverResponse = response;
1147
+ let networkErrorDuringServerRetry = false;
1148
+ let authFailureDuringServerRetry = false;
1149
+ for (let attempt = 0; attempt < MAX_SERVER_RETRIES_PER_ATTEMPT; attempt++) {
1150
+ const backoff = Math.min(SERVER_RETRY_BASE_MS * 2 ** attempt, SERVER_RETRY_MAX_MS);
1151
+ const jitteredBackoff = backoff * (0.5 + Math.random() * 0.5);
1152
+ await sleep2(jitteredBackoff);
1153
+ try {
1154
+ serverResponse = await runtime.fetch(input, init);
1155
+ } catch (error) {
1156
+ if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1157
+ authFailureDuringServerRetry = true;
1158
+ break;
1159
+ }
1160
+ networkErrorDuringServerRetry = true;
1161
+ void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1162
+ break;
1163
+ }
1164
+ if (serverResponse.status < 500) break;
1165
+ }
1166
+ if (authFailureDuringServerRetry) {
1167
+ continue;
1168
+ }
1169
+ if (networkErrorDuringServerRetry || serverResponse.status >= 500) {
1170
+ continue;
1171
+ }
1172
+ response = serverResponse;
1173
+ }
1174
+ if (response.status === 401) {
1175
+ runtimeFactory.invalidate(accountUuid);
1176
+ try {
1177
+ const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
1178
+ const retryResponse = await retryRuntime.fetch(input, init);
1179
+ if (retryResponse.status !== 401) {
1180
+ await manager.markSuccess(accountUuid);
1181
+ return retryResponse;
1182
+ }
1183
+ } catch (error) {
1184
+ if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1185
+ continue;
1186
+ }
1187
+ continue;
1188
+ }
1189
+ await manager.markAuthFailure(accountUuid, { ok: false, permanent: false });
1190
+ await manager.refresh();
1191
+ if (!manager.hasAnyUsableAccount()) {
1192
+ void showToast2(client, "All accounts have auth failures.", "error");
1193
+ throw new Error(
1194
+ `All ${providerName} accounts have authentication failures. Re-authenticate with \`opencode auth login\`.`
1195
+ );
1196
+ }
1197
+ void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
1198
+ continue;
1199
+ }
1200
+ if (response.status === 403) {
1201
+ const revoked = await isRevokedTokenResponse(response);
1202
+ if (revoked) {
1203
+ await manager.markRevoked(accountUuid);
1204
+ await manager.refresh();
1205
+ void showToast2(
1206
+ client,
1207
+ `${getAccountLabel2(account)} disabled: OAuth token revoked.`,
1208
+ "error"
1209
+ );
1210
+ if (!manager.hasAnyUsableAccount()) {
1211
+ throw new Error(
1212
+ `All ${providerName} accounts have been revoked or disabled. Re-authenticate with \`opencode auth login\`.`
1213
+ );
1214
+ }
1215
+ continue;
1216
+ }
1217
+ }
1218
+ if (response.status === 429) {
1219
+ await handleRateLimitResponse2(manager, client, account, response);
1220
+ continue;
1221
+ }
1222
+ await manager.markSuccess(accountUuid);
1223
+ return response;
1224
+ }
1225
+ }
1226
+ async function handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error) {
1227
+ const refreshFailureStatus = getRefreshFailureStatus(error);
1228
+ if (refreshFailureStatus === void 0) return false;
1229
+ if (!account.uuid) return false;
1230
+ const accountUuid = account.uuid;
1231
+ runtimeFactory.invalidate(accountUuid);
1232
+ await manager.markAuthFailure(accountUuid, {
1233
+ ok: false,
1234
+ permanent: PERMANENT_AUTH_FAILURE_STATUSES.has(refreshFailureStatus)
1235
+ });
1236
+ await manager.refresh();
1237
+ if (!manager.hasAnyUsableAccount()) {
1238
+ void showToast2(client, "All accounts have auth failures.", "error");
1239
+ throw new Error(
1240
+ `All ${providerName} accounts have authentication failures. Re-authenticate with \`opencode auth login\`.`
1241
+ );
1242
+ }
1243
+ void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
1244
+ return true;
1245
+ }
1246
+ async function resolveAccount(manager, client) {
1247
+ let attempts = 0;
1248
+ while (true) {
1249
+ if (++attempts > MAX_RESOLVE_ATTEMPTS) {
1250
+ throw new Error(
1251
+ `Failed to resolve an available account after ${MAX_RESOLVE_ATTEMPTS} attempts. All accounts may be rate-limited or disabled.`
1252
+ );
1253
+ }
1254
+ const account = await manager.selectAccount();
1255
+ if (account) return account;
1256
+ if (!manager.hasAnyUsableAccount()) {
1257
+ throw new Error(
1258
+ `All ${providerName} accounts are disabled. Re-authenticate with \`opencode auth login\`.`
1259
+ );
1260
+ }
1261
+ const waitMs = manager.getMinWaitTime();
1262
+ if (waitMs <= 0) {
1263
+ throw new Error(
1264
+ `All ${providerName} accounts are rate-limited. Add more accounts with \`opencode auth login\` or wait.`
1265
+ );
1266
+ }
1267
+ await showToast2(
1268
+ client,
1269
+ `All ${manager.getAccountCount()} account(s) rate-limited. Waiting ${formatWaitTime2(waitMs)}...`,
1270
+ "warning"
1271
+ );
1272
+ await sleep2(waitMs);
1273
+ }
1274
+ }
1275
+ return {
1276
+ executeWithAccountRotation: executeWithAccountRotation2
1277
+ };
1278
+ }
1279
+ function getRefreshFailureStatus(error) {
1280
+ if (!(error instanceof Error)) return void 0;
1281
+ const matched = error.message.match(/Token refresh failed:\s*(\d{3})/);
1282
+ if (!matched) return void 0;
1283
+ const status = Number(matched[1]);
1284
+ return Number.isFinite(status) ? status : void 0;
1285
+ }
1286
+ async function isRevokedTokenResponse(response) {
1287
+ try {
1288
+ const cloned = response.clone();
1289
+ const body = await cloned.text();
1290
+ return body.includes("revoked");
1291
+ } catch {
1292
+ return false;
1293
+ }
1294
+ }
1295
+
1296
+ // ../multi-account-core/src/proactive-refresh.ts
1297
+ var INITIAL_DELAY_MS = 5e3;
1298
+ function createProactiveRefreshQueueForProvider(dependencies) {
1299
+ const {
1300
+ getConfig: getConfig2,
1301
+ refreshToken: refreshToken2,
1302
+ isTokenExpired: isTokenExpired2,
1303
+ debugLog: debugLog2
1304
+ } = dependencies;
1305
+ return class ProactiveRefreshQueue {
1306
+ constructor(client, store, onInvalidate) {
1307
+ this.client = client;
1308
+ this.store = store;
1309
+ this.onInvalidate = onInvalidate;
1310
+ }
1311
+ timeoutHandle = null;
1312
+ runToken = 0;
1313
+ inFlight = null;
1314
+ start() {
1315
+ const config = getConfig2();
1316
+ if (!config.proactive_refresh) return;
1317
+ this.runToken++;
1318
+ this.scheduleNext(this.runToken, INITIAL_DELAY_MS);
1319
+ debugLog2(this.client, "Proactive refresh started", {
1320
+ intervalSeconds: config.proactive_refresh_interval_seconds,
1321
+ bufferSeconds: config.proactive_refresh_buffer_seconds
1322
+ });
1323
+ }
1324
+ async stop() {
1325
+ this.runToken++;
1326
+ if (this.timeoutHandle) {
1327
+ clearTimeout(this.timeoutHandle);
1328
+ this.timeoutHandle = null;
1329
+ }
1330
+ if (this.inFlight) {
1331
+ await this.inFlight;
1332
+ this.inFlight = null;
1333
+ }
1334
+ debugLog2(this.client, "Proactive refresh stopped");
1335
+ }
1336
+ scheduleNext(token, delayMs) {
1337
+ this.timeoutHandle = setTimeout(() => {
1338
+ if (token !== this.runToken) return;
1339
+ this.inFlight = this.runCheck(token).finally(() => {
1340
+ this.inFlight = null;
1341
+ });
1342
+ }, delayMs);
1343
+ }
1344
+ needsProactiveRefresh(account) {
1345
+ if (!account.accessToken || !account.expiresAt) return false;
1346
+ if (isTokenExpired2(account)) return false;
1347
+ const bufferMs = getConfig2().proactive_refresh_buffer_seconds * 1e3;
1348
+ return account.expiresAt <= Date.now() + bufferMs;
1349
+ }
1350
+ async runCheck(token) {
1351
+ try {
1352
+ const stored = await this.store.load();
1353
+ if (token !== this.runToken) return;
1354
+ const candidates = stored.accounts.filter(
1355
+ (a) => a.enabled !== false && !a.isAuthDisabled && a.uuid && this.needsProactiveRefresh(a)
1356
+ );
1357
+ if (candidates.length === 0) return;
1358
+ debugLog2(this.client, `Proactive refresh: ${candidates.length} account(s) approaching expiry`);
1359
+ for (const account of candidates) {
1360
+ if (token !== this.runToken) return;
1361
+ const credentials = await this.store.readCredentials(account.uuid);
1362
+ if (!credentials || !this.needsProactiveRefresh(credentials)) continue;
1363
+ const result = await refreshToken2(credentials.refreshToken, account.uuid, this.client);
1364
+ if (result.ok) {
1365
+ await this.store.mutateAccount(account.uuid, (target) => {
1366
+ target.accessToken = result.patch.accessToken;
1367
+ target.expiresAt = result.patch.expiresAt;
1368
+ if (result.patch.refreshToken) target.refreshToken = result.patch.refreshToken;
1369
+ if (result.patch.uuid) target.uuid = result.patch.uuid;
1370
+ if (result.patch.email) target.email = result.patch.email;
1371
+ if (result.patch.accountId) target.accountId = result.patch.accountId;
1372
+ target.consecutiveAuthFailures = 0;
1373
+ target.isAuthDisabled = false;
1374
+ target.authDisabledReason = void 0;
1375
+ });
1376
+ this.onInvalidate?.(account.uuid);
1377
+ } else {
1378
+ await this.persistFailure(account, result.permanent);
1379
+ }
1380
+ }
1381
+ } catch (error) {
1382
+ debugLog2(this.client, `Proactive refresh check error: ${error}`);
1383
+ } finally {
1384
+ if (token === this.runToken) {
1385
+ const intervalMs = getConfig2().proactive_refresh_interval_seconds * 1e3;
1386
+ this.scheduleNext(token, intervalMs);
1387
+ }
1388
+ }
1389
+ }
1390
+ async persistFailure(account, permanent) {
1391
+ try {
1392
+ await this.store.mutateAccount(account.uuid, (target) => {
1393
+ if (permanent) {
1394
+ target.isAuthDisabled = true;
1395
+ target.authDisabledReason = "Token permanently rejected (proactive refresh)";
1396
+ } else {
1397
+ target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
1398
+ const maxFailures = getConfig2().max_consecutive_auth_failures;
1399
+ if (target.consecutiveAuthFailures >= maxFailures) {
1400
+ target.isAuthDisabled = true;
1401
+ target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
1402
+ }
1403
+ }
1404
+ });
1405
+ } catch {
1406
+ debugLog2(this.client, `Failed to persist auth failure for ${account.uuid}`);
1407
+ }
1408
+ }
1409
+ };
1410
+ }
1411
+
1412
+ // ../multi-account-core/src/rate-limit.ts
1413
+ var USAGE_FETCH_COOLDOWN_MS = 3e4;
1414
+ function createRateLimitHandlers(dependencies) {
1415
+ const {
1416
+ fetchUsage: fetchUsage2,
1417
+ getConfig: getConfig2,
1418
+ formatWaitTime: formatWaitTime2,
1419
+ getAccountLabel: getAccountLabel2,
1420
+ showToast: showToast2
1421
+ } = dependencies;
1422
+ function retryAfterMsFromResponse2(response) {
1423
+ const retryAfterMs = response.headers.get("retry-after-ms");
1424
+ if (retryAfterMs) {
1425
+ const parsed = parseInt(retryAfterMs, 10);
1426
+ if (!isNaN(parsed) && parsed > 0) return parsed;
1427
+ }
1428
+ const retryAfter = response.headers.get("retry-after");
1429
+ if (retryAfter) {
1430
+ const parsed = parseInt(retryAfter, 10);
1431
+ if (!isNaN(parsed) && parsed > 0) return parsed * 1e3;
1432
+ }
1433
+ return getConfig2().default_retry_after_ms;
1434
+ }
1435
+ function getResetMsFromUsage2(account) {
1436
+ const usage = account.cachedUsage;
1437
+ if (!usage) return null;
1438
+ const now = Date.now();
1439
+ const candidates = [];
1440
+ if (usage.five_hour?.resets_at) {
1441
+ const ms = Date.parse(usage.five_hour.resets_at) - now;
1442
+ if (ms > 0) candidates.push(ms);
1443
+ }
1444
+ if (usage.seven_day?.resets_at) {
1445
+ const ms = Date.parse(usage.seven_day.resets_at) - now;
1446
+ if (ms > 0) candidates.push(ms);
1447
+ }
1448
+ return candidates.length > 0 ? Math.min(...candidates) : null;
1449
+ }
1450
+ async function fetchUsageLimits2(accessToken, accountId) {
1451
+ if (!accessToken) return null;
1452
+ try {
1453
+ const result = await fetchUsage2(accessToken, accountId);
1454
+ return result.ok ? result.data : null;
1455
+ } catch {
1456
+ return null;
1457
+ }
1458
+ }
1459
+ async function handleRateLimitResponse2(manager, client, account, response) {
1460
+ if (!account.uuid) return;
1461
+ const resetMs = getResetMsFromUsage2(account) ?? retryAfterMsFromResponse2(response);
1462
+ await manager.markRateLimited(account.uuid, resetMs);
1463
+ const shouldFetchUsage = account.accessToken && (!account.cachedUsageAt || Date.now() - account.cachedUsageAt > USAGE_FETCH_COOLDOWN_MS);
1464
+ if (shouldFetchUsage) {
1465
+ const usage = await fetchUsageLimits2(account.accessToken, account.accountId);
1466
+ if (usage) {
1467
+ await manager.applyUsageCache(account.uuid, usage);
1468
+ }
1469
+ }
1470
+ if (manager.getAccountCount() > 1) {
1471
+ void showToast2(
1472
+ client,
1473
+ `${getAccountLabel2(account)} rate-limited (resets in ${formatWaitTime2(resetMs)}). Switching...`,
1474
+ "warning"
1475
+ );
1476
+ }
1477
+ }
1478
+ return {
1479
+ retryAfterMsFromResponse: retryAfterMsFromResponse2,
1480
+ getResetMsFromUsage: getResetMsFromUsage2,
1481
+ fetchUsageLimits: fetchUsageLimits2,
1482
+ handleRateLimitResponse: handleRateLimitResponse2
1483
+ };
1484
+ }
1485
+
1486
+ // ../multi-account-core/src/auth-migration.ts
1487
+ import { promises as fs5 } from "node:fs";
1488
+ import { join as join6 } from "node:path";
1489
+ var AUTH_JSON_FILENAME = "auth.json";
1490
+ function isValidOAuthCredential(value) {
1491
+ if (typeof value !== "object" || value === null) return false;
1492
+ const candidate = value;
1493
+ return candidate.type === "oauth" && typeof candidate.refresh === "string" && candidate.refresh.length > 0;
1494
+ }
1495
+ function resolveAuthJsonPath() {
1496
+ return join6(getConfigDir2(), AUTH_JSON_FILENAME);
1497
+ }
1498
+ async function readAuthJson() {
1499
+ const authPath = resolveAuthJsonPath();
1500
+ let content;
1501
+ try {
1502
+ content = await fs5.readFile(authPath, "utf-8");
1503
+ } catch {
1504
+ return null;
1505
+ }
1506
+ try {
1507
+ const parsed = JSON.parse(content);
1508
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1509
+ return null;
1510
+ }
1511
+ return parsed;
1512
+ } catch {
1513
+ return null;
1514
+ }
1515
+ }
1516
+ async function migrateFromAuthJson(providerKey, store) {
1517
+ const storage = await store.load();
1518
+ const hasExistingAccounts = storage.accounts.length > 0;
1519
+ if (hasExistingAccounts) return false;
1520
+ const authData = await readAuthJson();
1521
+ if (!authData) return false;
1522
+ const providerCredential = authData[providerKey];
1523
+ if (!isValidOAuthCredential(providerCredential)) return false;
1524
+ const now = Date.now();
1525
+ const newAccount = {
1526
+ uuid: crypto.randomUUID(),
1527
+ refreshToken: providerCredential.refresh,
1528
+ accessToken: providerCredential.access,
1529
+ expiresAt: providerCredential.expires,
1530
+ addedAt: now,
1531
+ lastUsed: now,
1532
+ enabled: true,
1533
+ planTier: "",
1534
+ consecutiveAuthFailures: 0,
1535
+ isAuthDisabled: false
1536
+ };
1537
+ await store.addAccount(newAccount);
1538
+ await store.setActiveUuid(newAccount.uuid);
1539
+ return true;
1540
+ }
1541
+
1542
+ // ../multi-account-core/src/ui/ansi.ts
1543
+ var ANSI = {
1544
+ hide: "\x1B[?25l",
1545
+ show: "\x1B[?25h",
1546
+ up: (n = 1) => `\x1B[${n}A`,
1547
+ down: (n = 1) => `\x1B[${n}B`,
1548
+ clearLine: "\x1B[2K",
1549
+ cyan: "\x1B[36m",
1550
+ green: "\x1B[32m",
1551
+ red: "\x1B[31m",
1552
+ yellow: "\x1B[33m",
1553
+ dim: "\x1B[2m",
1554
+ bold: "\x1B[1m",
1555
+ reset: "\x1B[0m"
1556
+ };
1557
+ function parseKey(data) {
1558
+ const s = data.toString();
1559
+ if (s === "\x1B[A" || s === "\x1BOA") return "up";
1560
+ if (s === "\x1B[B" || s === "\x1BOB") return "down";
1561
+ if (s === "\r" || s === "\n") return "enter";
1562
+ if (s === "") return "escape";
1563
+ if (s === "\x1B") return "escape-start";
1564
+ return null;
1565
+ }
1566
+ function isTTY() {
1567
+ return Boolean(process.stdin.isTTY);
1568
+ }
1569
+
1570
+ // ../multi-account-core/src/ui/select.ts
1571
+ var ESCAPE_TIMEOUT_MS = 50;
1572
+ var COLOR_MAP = {
1573
+ red: ANSI.red,
1574
+ green: ANSI.green,
1575
+ yellow: ANSI.yellow,
1576
+ cyan: ANSI.cyan
1577
+ };
1578
+ async function select(items, options) {
1579
+ if (!isTTY()) {
1580
+ throw new Error("Interactive select requires a TTY terminal");
1581
+ }
1582
+ const enabledItems = items.filter((i) => !i.disabled && !i.separator);
1583
+ if (enabledItems.length === 0) {
1584
+ throw new Error("All items disabled");
1585
+ }
1586
+ if (enabledItems.length === 1) {
1587
+ return enabledItems[0].value;
1588
+ }
1589
+ const { message, subtitle } = options;
1590
+ const { stdin, stdout } = process;
1591
+ let cursor = items.findIndex((i) => !i.disabled && !i.separator);
1592
+ if (cursor === -1) cursor = 0;
1593
+ let escapeTimeout = null;
1594
+ let isCleanedUp = false;
1595
+ let isFirstRender = true;
1596
+ const getTotalLines = () => {
1597
+ const subtitleLines = subtitle ? 3 : 0;
1598
+ return 1 + subtitleLines + items.length + 1 + 1;
1599
+ };
1600
+ const renderItemLabel = (item, isSelected) => {
1601
+ const colorCode = item.color ? COLOR_MAP[item.color] ?? "" : "";
1602
+ if (item.disabled) {
1603
+ return `${ANSI.dim}${item.label} (unavailable)${ANSI.reset}`;
1604
+ }
1605
+ const hintSuffix = item.hint ? ` ${ANSI.dim}${item.hint}${ANSI.reset}` : "";
1606
+ if (isSelected) {
1607
+ const label = colorCode ? `${colorCode}${item.label}${ANSI.reset}` : item.label;
1608
+ return `${label}${hintSuffix}`;
1609
+ }
1610
+ const dimLabel = colorCode ? `${ANSI.dim}${colorCode}${item.label}${ANSI.reset}` : `${ANSI.dim}${item.label}${ANSI.reset}`;
1611
+ return `${dimLabel}${hintSuffix}`;
1612
+ };
1613
+ const render = () => {
1614
+ const totalLines = getTotalLines();
1615
+ if (!isFirstRender) {
1616
+ stdout.write(ANSI.up(totalLines) + "\r");
1617
+ }
1618
+ isFirstRender = false;
1619
+ stdout.write(`${ANSI.clearLine}${ANSI.dim}\u250C ${ANSI.reset}${message}
1620
+ `);
1621
+ if (subtitle) {
1622
+ stdout.write(`${ANSI.clearLine}${ANSI.dim}\u2502${ANSI.reset}
1623
+ `);
1624
+ stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u25C6${ANSI.reset} ${subtitle}
1625
+ `);
1626
+ stdout.write(`${ANSI.clearLine}
1627
+ `);
1628
+ }
1629
+ for (let i = 0; i < items.length; i++) {
1630
+ const item = items[i];
1631
+ if (!item) continue;
1632
+ if (item.separator) {
1633
+ stdout.write(`${ANSI.clearLine}${ANSI.dim}\u2502${ANSI.reset}
1634
+ `);
1635
+ continue;
1636
+ }
1637
+ const isSelected = i === cursor;
1638
+ const labelText = renderItemLabel(item, isSelected);
1639
+ const bullet = isSelected ? `${ANSI.green}\u25CF${ANSI.reset}` : `${ANSI.dim}\u25CB${ANSI.reset}`;
1640
+ stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2502${ANSI.reset} ${bullet} ${labelText}
1641
+ `);
1642
+ }
1643
+ stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2502${ANSI.reset} ${ANSI.dim}\u2191/\u2193 to select \u2022 Enter: confirm${ANSI.reset}
1644
+ `);
1645
+ stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2514${ANSI.reset}
1646
+ `);
1647
+ };
1648
+ return new Promise((resolve) => {
1649
+ const wasRaw = stdin.isRaw ?? false;
1650
+ const cleanup = () => {
1651
+ if (isCleanedUp) return;
1652
+ isCleanedUp = true;
1653
+ if (escapeTimeout) {
1654
+ clearTimeout(escapeTimeout);
1655
+ escapeTimeout = null;
1656
+ }
1657
+ try {
1658
+ stdin.removeListener("data", onKey);
1659
+ stdin.setRawMode(wasRaw);
1660
+ stdin.pause();
1661
+ stdout.write(ANSI.show);
1662
+ } catch {
1663
+ }
1664
+ process.removeListener("SIGINT", onSignal);
1665
+ process.removeListener("SIGTERM", onSignal);
1666
+ };
1667
+ const onSignal = () => {
1668
+ cleanup();
1669
+ resolve(null);
1670
+ };
1671
+ const finishWithValue = (value) => {
1672
+ cleanup();
1673
+ resolve(value);
1674
+ };
1675
+ const findNextSelectable = (from, direction) => {
1676
+ if (items.length === 0) return from;
1677
+ let next = from;
1678
+ do {
1679
+ next = (next + direction + items.length) % items.length;
1680
+ } while (items[next]?.disabled || items[next]?.separator);
1681
+ return next;
1682
+ };
1683
+ const onKey = (data) => {
1684
+ if (escapeTimeout) {
1685
+ clearTimeout(escapeTimeout);
1686
+ escapeTimeout = null;
1687
+ }
1688
+ const action = parseKey(data);
1689
+ switch (action) {
1690
+ case "up":
1691
+ cursor = findNextSelectable(cursor, -1);
1692
+ render();
1693
+ return;
1694
+ case "down":
1695
+ cursor = findNextSelectable(cursor, 1);
1696
+ render();
1697
+ return;
1698
+ case "enter":
1699
+ finishWithValue(items[cursor]?.value ?? null);
1700
+ return;
1701
+ case "escape":
1702
+ finishWithValue(null);
1703
+ return;
1704
+ case "escape-start":
1705
+ escapeTimeout = setTimeout(() => {
1706
+ finishWithValue(null);
1707
+ }, ESCAPE_TIMEOUT_MS);
1708
+ return;
1709
+ default:
1710
+ return;
1711
+ }
1712
+ };
1713
+ process.once("SIGINT", onSignal);
1714
+ process.once("SIGTERM", onSignal);
1715
+ try {
1716
+ stdin.setRawMode(true);
1717
+ } catch {
1718
+ cleanup();
1719
+ resolve(null);
1720
+ return;
1721
+ }
1722
+ stdin.resume();
1723
+ stdout.write(ANSI.hide);
1724
+ render();
1725
+ stdin.on("data", onKey);
1726
+ });
1727
+ }
1728
+
1729
+ // ../multi-account-core/src/ui/confirm.ts
1730
+ async function confirm(message, defaultYes = false) {
1731
+ const items = defaultYes ? [
1732
+ { label: "Yes", value: true },
1733
+ { label: "No", value: false }
1734
+ ] : [
1735
+ { label: "No", value: false },
1736
+ { label: "Yes", value: true }
1737
+ ];
1738
+ const result = await select(items, { message });
1739
+ return result ?? false;
1740
+ }
1741
+
1742
+ // ../oauth-adapters/src/openai.ts
1743
+ var ISSUER = "https://auth.openai.com";
1744
+ var openAIOAuthAdapter = {
1745
+ id: "openai",
1746
+ authProviderId: "openai",
1747
+ modelDisplayName: "ChatGPT",
1748
+ statusToolName: "chatgpt_multiauth_status",
1749
+ authMethodLabel: "ChatGPT Plus/Pro (Multi-Auth)",
1750
+ serviceLogName: "chatgpt-multiauth",
1751
+ oauthClientId: "app_EMoamEEZ73f0CkXaXp7hrann",
1752
+ tokenEndpoint: `${ISSUER}/oauth/token`,
1753
+ usageEndpoint: "",
1754
+ profileEndpoint: "",
1755
+ oauthBetaHeader: "",
1756
+ requestBetaHeader: "",
1757
+ cliUserAgent: "opencode/1.1.53",
1758
+ toolPrefix: "mcp_",
1759
+ accountStorageFilename: "openai-multi-account-accounts.json",
1760
+ transform: {
1761
+ rewriteOpenCodeBranding: false,
1762
+ addToolPrefix: false,
1763
+ stripToolPrefixInResponse: false,
1764
+ enableMessagesBetaQuery: false
1765
+ },
1766
+ planLabels: {
1767
+ pro: "ChatGPT Pro",
1768
+ plus: "ChatGPT Plus",
1769
+ go: "ChatGPT Go",
1770
+ free: "Free"
1771
+ },
1772
+ supported: true
1773
+ };
1774
+
1775
+ // src/constants.ts
1776
+ var OPENAI_OAUTH_ADAPTER = openAIOAuthAdapter;
1777
+ var OPENAI_CLIENT_ID = OPENAI_OAUTH_ADAPTER.oauthClientId;
1778
+ var OPENAI_TOKEN_ENDPOINT = OPENAI_OAUTH_ADAPTER.tokenEndpoint;
1779
+ var OPENAI_USAGE_ENDPOINT = OPENAI_OAUTH_ADAPTER.usageEndpoint;
1780
+ var OPENAI_PROFILE_ENDPOINT = OPENAI_OAUTH_ADAPTER.profileEndpoint;
1781
+ var CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
1782
+ var CODEX_USAGE_ENDPOINT = "https://chatgpt.com/backend-api/wham/usage";
1783
+ var OAUTH_ISSUER = "https://auth.openai.com";
1784
+ var OAUTH_PORT = 1455;
1785
+ var OPENAI_BETA_HEADER = OPENAI_OAUTH_ADAPTER.requestBetaHeader;
1786
+ var OPENAI_CLI_USER_AGENT = OPENAI_OAUTH_ADAPTER.cliUserAgent;
1787
+ var TOOL_PREFIX = OPENAI_OAUTH_ADAPTER.toolPrefix;
1788
+ var ACCOUNTS_FILENAME2 = OPENAI_OAUTH_ADAPTER.accountStorageFilename;
1789
+ var PLAN_LABELS = OPENAI_OAUTH_ADAPTER.planLabels;
1790
+ var TOKEN_EXPIRY_BUFFER_MS = 6e4;
1791
+ var TOKEN_REFRESH_TIMEOUT_MS = 3e4;
1792
+
1793
+ // src/oauth.ts
1794
+ import * as v6 from "valibot";
1795
+
1796
+ // src/types.ts
1797
+ import * as v5 from "valibot";
1798
+ var OAuthCredentialsSchema2 = v5.object({
1799
+ type: v5.literal("oauth"),
1800
+ refresh: v5.string(),
1801
+ access: v5.string(),
1802
+ expires: v5.number()
1803
+ });
1804
+ var UsageLimitEntrySchema2 = v5.object({
1805
+ utilization: v5.number(),
1806
+ resets_at: v5.nullable(v5.string())
1807
+ });
1808
+ var UsageLimitsSchema2 = v5.object({
1809
+ five_hour: v5.optional(v5.nullable(UsageLimitEntrySchema2), null),
1810
+ seven_day: v5.optional(v5.nullable(UsageLimitEntrySchema2), null),
1811
+ seven_day_sonnet: v5.optional(v5.nullable(UsageLimitEntrySchema2), null)
1812
+ });
1813
+ var CredentialRefreshPatchSchema2 = v5.object({
1814
+ accessToken: v5.string(),
1815
+ expiresAt: v5.number(),
1816
+ refreshToken: v5.optional(v5.string()),
1817
+ uuid: v5.optional(v5.string()),
1818
+ accountId: v5.optional(v5.string()),
1819
+ email: v5.optional(v5.string())
1820
+ });
1821
+ var StoredAccountSchema2 = v5.object({
1822
+ uuid: v5.optional(v5.string()),
1823
+ accountId: v5.optional(v5.string()),
1824
+ label: v5.optional(v5.string()),
1825
+ email: v5.optional(v5.string()),
1826
+ planTier: v5.optional(v5.string(), ""),
1827
+ refreshToken: v5.string(),
1828
+ accessToken: v5.optional(v5.string()),
1829
+ expiresAt: v5.optional(v5.number()),
1830
+ addedAt: v5.number(),
1831
+ lastUsed: v5.number(),
1832
+ enabled: v5.optional(v5.boolean(), true),
1833
+ rateLimitResetAt: v5.optional(v5.number()),
1834
+ cachedUsage: v5.optional(UsageLimitsSchema2),
1835
+ cachedUsageAt: v5.optional(v5.number()),
1836
+ consecutiveAuthFailures: v5.optional(v5.number(), 0),
1837
+ isAuthDisabled: v5.optional(v5.boolean(), false),
1838
+ authDisabledReason: v5.optional(v5.string())
1839
+ });
1840
+ var AccountStorageSchema2 = v5.object({
1841
+ version: v5.literal(1),
1842
+ accounts: v5.optional(v5.array(StoredAccountSchema2), []),
1843
+ activeAccountUuid: v5.optional(v5.string())
1844
+ });
1845
+ var TokenResponseSchema = v5.object({
1846
+ id_token: v5.optional(v5.string()),
1847
+ access_token: v5.string(),
1848
+ refresh_token: v5.optional(v5.string()),
1849
+ expires_in: v5.number()
1850
+ });
1851
+ var AccountSelectionStrategySchema2 = v5.picklist(["sticky", "round-robin", "hybrid"]);
1852
+ var PluginConfigSchema2 = v5.object({
1853
+ /** sticky: same account until failure, round-robin: rotate every request, hybrid: health+usage scoring */
1854
+ account_selection_strategy: v5.optional(AccountSelectionStrategySchema2, "sticky"),
1855
+ /** Use cross-process claim file to distribute parallel sessions across accounts */
1856
+ cross_process_claims: v5.optional(v5.boolean(), true),
1857
+ /** Skip account when any usage tier utilization >= this % (100 = disabled) */
1858
+ soft_quota_threshold_percent: v5.optional(v5.pipe(v5.number(), v5.minValue(0), v5.maxValue(100)), 100),
1859
+ /** Minimum backoff after rate limit (ms) */
1860
+ rate_limit_min_backoff_ms: v5.optional(v5.pipe(v5.number(), v5.minValue(0)), 3e4),
1861
+ /** Default retry-after when header is missing (ms) */
1862
+ default_retry_after_ms: v5.optional(v5.pipe(v5.number(), v5.minValue(0)), 6e4),
1863
+ /** Consecutive auth failures before disabling account */
1864
+ max_consecutive_auth_failures: v5.optional(v5.pipe(v5.number(), v5.integer(), v5.minValue(1)), 3),
1865
+ /** Backoff after token refresh failure (ms) */
1866
+ token_failure_backoff_ms: v5.optional(v5.pipe(v5.number(), v5.minValue(0)), 3e4),
1867
+ /** Enable proactive background token refresh */
1868
+ proactive_refresh: v5.optional(v5.boolean(), true),
1869
+ /** Seconds before expiry to trigger proactive refresh (default 30 min) */
1870
+ proactive_refresh_buffer_seconds: v5.optional(v5.pipe(v5.number(), v5.minValue(60)), 1800),
1871
+ /** Interval between background refresh checks in seconds (default 5 min) */
1872
+ proactive_refresh_interval_seconds: v5.optional(v5.pipe(v5.number(), v5.minValue(30)), 300),
1873
+ /** Suppress toast notifications */
1874
+ quiet_mode: v5.optional(v5.boolean(), false),
1875
+ /** Enable debug logging */
1876
+ debug: v5.optional(v5.boolean(), false)
1877
+ });
1878
+
1879
+ // src/oauth.ts
1880
+ var OAUTH_CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
1881
+ var oauthServer = null;
1882
+ var resolveOAuthQuery = null;
1883
+ var rejectOAuthQuery = null;
1884
+ function getBunRuntime() {
1885
+ const maybeBun = globalThis.Bun;
1886
+ if (!maybeBun || typeof maybeBun.serve !== "function") {
1887
+ throw new Error("Browser OAuth requires Bun runtime");
1888
+ }
1889
+ return maybeBun;
1890
+ }
1891
+ function getRedirectUri(port = OAUTH_PORT) {
1892
+ return `http://localhost:${port}/auth/callback`;
1893
+ }
1894
+ function renderHtml(title, body) {
1895
+ return `<!doctype html>
1896
+ <html>
1897
+ <head>
1898
+ <meta charset="utf-8" />
1899
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1900
+ <title>${title}</title>
1901
+ <style>
1902
+ :root { color-scheme: dark; }
1903
+ body {
1904
+ margin: 0;
1905
+ min-height: 100vh;
1906
+ display: grid;
1907
+ place-items: center;
1908
+ background: #0f1115;
1909
+ color: #f5f7ff;
1910
+ font: 16px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1911
+ }
1912
+ main {
1913
+ width: min(520px, calc(100vw - 32px));
1914
+ border: 1px solid #2a3040;
1915
+ border-radius: 12px;
1916
+ padding: 24px;
1917
+ background: #171b25;
1918
+ }
1919
+ h1 { margin: 0 0 8px 0; font-size: 22px; }
1920
+ p { margin: 0; color: #c4cadb; }
1921
+ </style>
1922
+ </head>
1923
+ <body>
1924
+ <main>
1925
+ <h1>${title}</h1>
1926
+ <p>${body}</p>
1927
+ </main>
1928
+ </body>
1929
+ </html>`;
1930
+ }
1931
+ function renderSuccessHtml() {
1932
+ return renderHtml("Authentication Complete", "You can close this tab and return to OpenCode.");
1933
+ }
1934
+ function renderErrorHtml(message) {
1935
+ return renderHtml("Authentication Failed", message);
1936
+ }
1937
+ function completeOAuthQuery(query) {
1938
+ if (resolveOAuthQuery) {
1939
+ resolveOAuthQuery(query);
1940
+ }
1941
+ resolveOAuthQuery = null;
1942
+ rejectOAuthQuery = null;
1943
+ }
1944
+ function failOAuthQuery(reason) {
1945
+ if (rejectOAuthQuery) {
1946
+ rejectOAuthQuery(reason);
1947
+ }
1948
+ resolveOAuthQuery = null;
1949
+ rejectOAuthQuery = null;
1950
+ }
1951
+ function tokenEndpoint() {
1952
+ return `${OAUTH_ISSUER}/oauth/token`;
1953
+ }
1954
+ function parseTokenResponse(json) {
1955
+ return v6.parse(TokenResponseSchema, json);
1956
+ }
1957
+ async function postTokenForm(body) {
1958
+ const response = await fetch(tokenEndpoint(), {
1959
+ method: "POST",
1960
+ headers: {
1961
+ "Content-Type": "application/x-www-form-urlencoded"
1962
+ },
1963
+ body
1964
+ });
1965
+ const payload = await response.json().catch(() => ({}));
1966
+ if (!response.ok) {
1967
+ throw new Error(`Token request failed: ${response.status}`);
1968
+ }
1969
+ return parseTokenResponse(payload);
1970
+ }
1971
+ async function generatePKCE() {
1972
+ const verifier = generateRandomString(64);
1973
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
1974
+ const challenge = base64UrlEncode(digest);
1975
+ return { verifier, challenge };
1976
+ }
1977
+ function generateRandomBytes(length) {
1978
+ const bytes = new Uint8Array(length);
1979
+ crypto.getRandomValues(bytes);
1980
+ return bytes;
1981
+ }
1982
+ function generateRandomString(length) {
1983
+ const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
1984
+ const bytes = generateRandomBytes(length);
1985
+ let out = "";
1986
+ for (const value of bytes) {
1987
+ out += charset[value % charset.length];
1988
+ }
1989
+ return out;
1990
+ }
1991
+ function base64UrlEncode(buffer) {
1992
+ const bytes = new Uint8Array(buffer);
1993
+ let binary = "";
1994
+ for (const byte of bytes) {
1995
+ binary += String.fromCharCode(byte);
1996
+ }
1997
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
1998
+ }
1999
+ function generateState() {
2000
+ const bytes = generateRandomBytes(32);
2001
+ const buffer = new ArrayBuffer(bytes.byteLength);
2002
+ new Uint8Array(buffer).set(bytes);
2003
+ return base64UrlEncode(buffer);
2004
+ }
2005
+ async function exchangeCodeForTokens(code, redirectUri, pkce) {
2006
+ return postTokenForm(new URLSearchParams({
2007
+ grant_type: "authorization_code",
2008
+ code,
2009
+ redirect_uri: redirectUri,
2010
+ client_id: OPENAI_CLIENT_ID,
2011
+ code_verifier: pkce.verifier
2012
+ }));
2013
+ }
2014
+ function base64UrlDecode(value) {
2015
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
2016
+ const padded = normalized + "=".repeat((4 - (normalized.length % 4 || 4)) % 4);
2017
+ return atob(padded);
2018
+ }
2019
+ function parseJwtClaims(token) {
2020
+ try {
2021
+ const parts = token.split(".");
2022
+ if (parts.length < 2 || !parts[1]) return void 0;
2023
+ const json = base64UrlDecode(parts[1]);
2024
+ return JSON.parse(json);
2025
+ } catch {
2026
+ return void 0;
2027
+ }
2028
+ }
2029
+ function findAccountId(claims) {
2030
+ if (!claims) return void 0;
2031
+ if (claims.chatgpt_account_id) return claims.chatgpt_account_id;
2032
+ if (claims["https://api.openai.com/auth"]?.chatgpt_account_id) {
2033
+ return claims["https://api.openai.com/auth"]?.chatgpt_account_id;
2034
+ }
2035
+ if (Array.isArray(claims.organizations) && claims.organizations[0]?.id) {
2036
+ return claims.organizations[0].id;
2037
+ }
2038
+ return void 0;
2039
+ }
2040
+ function extractAccountId(tokens) {
2041
+ const fromIdToken = findAccountId(parseJwtClaims(tokens.id_token ?? ""));
2042
+ if (fromIdToken) return fromIdToken;
2043
+ return findAccountId(parseJwtClaims(tokens.access_token));
2044
+ }
2045
+ async function startOAuthServer() {
2046
+ if (oauthServer) {
2047
+ return { port: OAUTH_PORT, redirectUri: getRedirectUri() };
2048
+ }
2049
+ const bun = getBunRuntime();
2050
+ oauthServer = bun.serve({
2051
+ port: OAUTH_PORT,
2052
+ fetch(request) {
2053
+ const url = new URL(request.url);
2054
+ if (url.pathname === "/cancel") {
2055
+ failOAuthQuery(new Error("Authentication cancelled by user"));
2056
+ return new Response(renderErrorHtml("Authentication was cancelled."), {
2057
+ status: 200,
2058
+ headers: { "Content-Type": "text/html; charset=utf-8" }
2059
+ });
2060
+ }
2061
+ if (url.pathname !== "/auth/callback") {
2062
+ return new Response("Not Found", { status: 404 });
2063
+ }
2064
+ const error = url.searchParams.get("error") ?? void 0;
2065
+ const errorDescription = url.searchParams.get("error_description") ?? void 0;
2066
+ const code = url.searchParams.get("code") ?? void 0;
2067
+ const state = url.searchParams.get("state") ?? void 0;
2068
+ if (error) {
2069
+ failOAuthQuery(new Error(errorDescription ?? error));
2070
+ return new Response(renderErrorHtml(errorDescription ?? error), {
2071
+ status: 400,
2072
+ headers: { "Content-Type": "text/html; charset=utf-8" }
2073
+ });
2074
+ }
2075
+ if (!code) {
2076
+ failOAuthQuery(new Error("Missing authorization code"));
2077
+ return new Response(renderErrorHtml("Missing authorization code."), {
2078
+ status: 400,
2079
+ headers: { "Content-Type": "text/html; charset=utf-8" }
2080
+ });
2081
+ }
2082
+ completeOAuthQuery({ code, state });
2083
+ return new Response(renderSuccessHtml(), {
2084
+ status: 200,
2085
+ headers: { "Content-Type": "text/html; charset=utf-8" }
2086
+ });
2087
+ }
2088
+ });
2089
+ return { port: OAUTH_PORT, redirectUri: getRedirectUri() };
2090
+ }
2091
+ function stopOAuthServer() {
2092
+ if (oauthServer) {
2093
+ oauthServer.stop(true);
2094
+ }
2095
+ oauthServer = null;
2096
+ failOAuthQuery(new Error("OAuth server stopped"));
2097
+ }
2098
+ function waitForOAuthCallback(pkce, state) {
2099
+ return new Promise(async (resolve, reject) => {
2100
+ try {
2101
+ const { redirectUri } = await startOAuthServer();
2102
+ if (resolveOAuthQuery || rejectOAuthQuery) {
2103
+ reject(new Error("OAuth callback wait already active"));
2104
+ return;
2105
+ }
2106
+ const timeout = setTimeout(() => {
2107
+ failOAuthQuery(new Error("OAuth callback timed out"));
2108
+ }, OAUTH_CALLBACK_TIMEOUT_MS);
2109
+ resolveOAuthQuery = async (query) => {
2110
+ clearTimeout(timeout);
2111
+ try {
2112
+ if (!query.code) {
2113
+ reject(new Error("Missing OAuth authorization code"));
2114
+ return;
2115
+ }
2116
+ if (!query.state || query.state !== state) {
2117
+ reject(new Error("OAuth state mismatch"));
2118
+ return;
2119
+ }
2120
+ const tokens = await exchangeCodeForTokens(query.code, redirectUri, pkce);
2121
+ resolve(tokens);
2122
+ } catch (error) {
2123
+ reject(error);
2124
+ } finally {
2125
+ resolveOAuthQuery = null;
2126
+ rejectOAuthQuery = null;
2127
+ }
2128
+ };
2129
+ rejectOAuthQuery = (reason) => {
2130
+ clearTimeout(timeout);
2131
+ reject(reason);
2132
+ };
2133
+ } catch (error) {
2134
+ reject(error);
2135
+ }
2136
+ });
2137
+ }
2138
+ function buildAuthorizeUrl(redirectUri, pkce, state) {
2139
+ const url = new URL(`${OAUTH_ISSUER}/oauth/authorize`);
2140
+ url.searchParams.set("response_type", "code");
2141
+ url.searchParams.set("client_id", OPENAI_CLIENT_ID);
2142
+ url.searchParams.set("redirect_uri", redirectUri);
2143
+ url.searchParams.set("scope", "openid profile email offline_access");
2144
+ url.searchParams.set("code_challenge", pkce.challenge);
2145
+ url.searchParams.set("code_challenge_method", "S256");
2146
+ url.searchParams.set("id_token_add_organizations", "true");
2147
+ url.searchParams.set("codex_cli_simplified_flow", "true");
2148
+ url.searchParams.set("state", state);
2149
+ url.searchParams.set("originator", "opencode");
2150
+ return url.toString();
2151
+ }
2152
+
2153
+ // src/token.ts
2154
+ import * as v7 from "valibot";
2155
+ var PERMANENT_FAILURE_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
2156
+ var refreshMutexByAccountId = /* @__PURE__ */ new Map();
2157
+ function isTokenExpired(account) {
2158
+ if (!account.accessToken || !account.expiresAt) return true;
2159
+ return account.expiresAt <= Date.now() + TOKEN_EXPIRY_BUFFER_MS;
2160
+ }
2161
+ async function refreshToken(currentRefreshToken, accountId, client) {
2162
+ if (!currentRefreshToken) return { ok: false, permanent: true };
2163
+ const inFlightRefresh = refreshMutexByAccountId.get(accountId);
2164
+ if (inFlightRefresh) return inFlightRefresh;
2165
+ const refreshPromise = (async () => {
2166
+ const controller = new AbortController();
2167
+ const timeout = setTimeout(() => controller.abort(), TOKEN_REFRESH_TIMEOUT_MS);
2168
+ try {
2169
+ const startTime = Date.now();
2170
+ const response = await fetch(OPENAI_TOKEN_ENDPOINT, {
2171
+ method: "POST",
2172
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2173
+ body: new URLSearchParams({
2174
+ grant_type: "refresh_token",
2175
+ refresh_token: currentRefreshToken,
2176
+ client_id: OPENAI_CLIENT_ID
2177
+ }),
2178
+ signal: controller.signal
2179
+ });
2180
+ if (!response.ok) {
2181
+ const isPermanent = PERMANENT_FAILURE_STATUSES.has(response.status);
2182
+ await client.app.log({
2183
+ body: {
2184
+ service: OPENAI_OAUTH_ADAPTER.serviceLogName,
2185
+ level: isPermanent ? "error" : "warn",
2186
+ message: `Token refresh failed: ${response.status}${isPermanent ? " (permanent)" : ""}`,
2187
+ extra: { accountId }
2188
+ }
2189
+ }).catch(() => {
2190
+ });
2191
+ return { ok: false, permanent: isPermanent, status: response.status };
2192
+ }
2193
+ const json = v7.parse(TokenResponseSchema, await response.json());
2194
+ const patch = {
2195
+ accessToken: json.access_token,
2196
+ expiresAt: startTime + json.expires_in * 1e3,
2197
+ refreshToken: json.refresh_token,
2198
+ accountId: extractAccountId(json)
2199
+ };
2200
+ return { ok: true, patch };
2201
+ } catch (error) {
2202
+ await client.app.log({
2203
+ body: {
2204
+ service: OPENAI_OAUTH_ADAPTER.serviceLogName,
2205
+ level: "warn",
2206
+ message: `Token refresh network error: ${error instanceof Error ? error.message : String(error)}`,
2207
+ extra: { accountId }
2208
+ }
2209
+ }).catch(() => {
2210
+ });
2211
+ return { ok: false, permanent: false };
2212
+ } finally {
2213
+ clearTimeout(timeout);
2214
+ refreshMutexByAccountId.delete(accountId);
2215
+ }
2216
+ })();
2217
+ refreshMutexByAccountId.set(accountId, refreshPromise);
2218
+ return refreshPromise;
2219
+ }
2220
+
2221
+ // src/account-manager.ts
2222
+ var AccountManager = createAccountManagerForProvider({
2223
+ providerAuthId: "openai",
2224
+ isTokenExpired,
2225
+ refreshToken
2226
+ });
2227
+
2228
+ // src/config.ts
2229
+ initCoreConfig("codex-multiauth.json");
2230
+
2231
+ // src/utils.ts
2232
+ setConfigGetter(getConfig);
2233
+
2234
+ // src/usage.ts
2235
+ import * as v8 from "valibot";
2236
+ var OPENAI_AUTH_CLAIM = "https://api.openai.com/auth";
2237
+ var OPENAI_PROFILE_CLAIM = "https://api.openai.com/profile";
2238
+ var OpenAIAuthClaimSchema = v8.object({
2239
+ chatgpt_plan_type: v8.optional(v8.string()),
2240
+ chatgpt_account_id: v8.optional(v8.string()),
2241
+ chatgpt_user_id: v8.optional(v8.string())
2242
+ });
2243
+ var OpenAIProfileClaimSchema = v8.object({
2244
+ email: v8.optional(v8.string())
2245
+ });
2246
+ var WhamRateLimitWindowSchema = v8.object({
2247
+ used_percent: v8.number(),
2248
+ reset_after_seconds: v8.number()
2249
+ });
2250
+ var WhamUsageResponseSchema = v8.object({
2251
+ plan_type: v8.optional(v8.nullable(v8.string())),
2252
+ rate_limit: v8.optional(v8.nullable(v8.object({
2253
+ primary_window: v8.optional(v8.nullable(WhamRateLimitWindowSchema)),
2254
+ secondary_window: v8.optional(v8.nullable(WhamRateLimitWindowSchema))
2255
+ }))),
2256
+ credits: v8.optional(v8.nullable(v8.object({
2257
+ balance: v8.optional(v8.nullable(v8.string())),
2258
+ unlimited: v8.optional(v8.nullable(v8.boolean()))
2259
+ })))
2260
+ });
2261
+ function secondsToISOResetTime(resetAfterSeconds) {
2262
+ return new Date(Date.now() + resetAfterSeconds * 1e3).toISOString();
2263
+ }
2264
+ async function fetchUsage(accessToken, accountId) {
2265
+ try {
2266
+ const headers = {
2267
+ Authorization: `Bearer ${accessToken}`,
2268
+ "User-Agent": OPENAI_CLI_USER_AGENT
2269
+ };
2270
+ if (accountId) {
2271
+ headers["ChatGPT-Account-Id"] = accountId;
2272
+ }
2273
+ const response = await fetch(CODEX_USAGE_ENDPOINT, {
2274
+ method: "GET",
2275
+ headers
2276
+ });
2277
+ if (!response.ok) {
2278
+ return { ok: false, reason: `HTTP ${response.status} ${response.statusText}` };
2279
+ }
2280
+ const parsed = v8.safeParse(WhamUsageResponseSchema, await response.json());
2281
+ if (!parsed.success) {
2282
+ return { ok: false, reason: `Invalid response: ${parsed.issues[0]?.message ?? "unknown"}` };
2283
+ }
2284
+ const wham = parsed.output;
2285
+ const primaryWindow = wham.rate_limit?.primary_window;
2286
+ const secondaryWindow = wham.rate_limit?.secondary_window;
2287
+ const usage = {
2288
+ five_hour: primaryWindow ? {
2289
+ utilization: primaryWindow.used_percent,
2290
+ resets_at: secondsToISOResetTime(primaryWindow.reset_after_seconds)
2291
+ } : null,
2292
+ seven_day: secondaryWindow ? {
2293
+ utilization: secondaryWindow.used_percent,
2294
+ resets_at: secondsToISOResetTime(secondaryWindow.reset_after_seconds)
2295
+ } : null,
2296
+ seven_day_sonnet: null
2297
+ };
2298
+ const validated = v8.safeParse(UsageLimitsSchema2, usage);
2299
+ if (!validated.success) {
2300
+ return { ok: false, reason: `Mapping error: ${validated.issues[0]?.message ?? "unknown"}` };
2301
+ }
2302
+ return { ok: true, data: validated.output, planType: wham.plan_type ?? void 0 };
2303
+ } catch (error) {
2304
+ const message = error instanceof Error ? error.message : "Unknown error";
2305
+ return { ok: false, reason: message };
2306
+ }
2307
+ }
2308
+ function derivePlanTier(planType) {
2309
+ const normalized = planType.toLowerCase().trim();
2310
+ return normalized || "free";
2311
+ }
2312
+ function fetchProfile(accessToken) {
2313
+ try {
2314
+ const claims = parseJwtClaims(accessToken);
2315
+ if (!claims || typeof claims !== "object") {
2316
+ return { ok: true, data: { planTier: "free" } };
2317
+ }
2318
+ const record = claims;
2319
+ const profileClaim = record[OPENAI_PROFILE_CLAIM];
2320
+ const profileParsed = v8.safeParse(OpenAIProfileClaimSchema, profileClaim ?? {});
2321
+ const email = profileParsed.success ? profileParsed.output.email : void 0;
2322
+ const authClaim = record[OPENAI_AUTH_CLAIM];
2323
+ const authParsed = v8.safeParse(OpenAIAuthClaimSchema, authClaim ?? {});
2324
+ const planType = authParsed.success ? authParsed.output.chatgpt_plan_type ?? "" : "";
2325
+ const planTier = derivePlanTier(planType);
2326
+ return {
2327
+ ok: true,
2328
+ data: { email, planTier }
2329
+ };
2330
+ } catch (error) {
2331
+ const message = error instanceof Error ? error.message : "Unknown error";
2332
+ return { ok: false, reason: message };
2333
+ }
2334
+ }
2335
+ function formatTimeRemaining(resetAt) {
2336
+ if (!resetAt) return "unknown";
2337
+ const diffMs = new Date(resetAt).getTime() - Date.now();
2338
+ if (diffMs <= 0) return "0m";
2339
+ return formatWaitTime(diffMs);
2340
+ }
2341
+ function getUsageSummary(account) {
2342
+ if (!account.cachedUsage) return "no usage data";
2343
+ const parsed = v8.safeParse(UsageLimitsSchema2, account.cachedUsage);
2344
+ if (!parsed.success) return "no usage data";
2345
+ const parts = [];
2346
+ const { five_hour, seven_day } = parsed.output;
2347
+ if (five_hour) {
2348
+ const reset = five_hour.resets_at ? ` (resets ${formatTimeRemaining(five_hour.resets_at)})` : "";
2349
+ parts.push(`5h: ${five_hour.utilization.toFixed(0)}%${reset}`);
2350
+ }
2351
+ if (seven_day) {
2352
+ const reset = seven_day.resets_at ? ` (resets ${formatTimeRemaining(seven_day.resets_at)})` : "";
2353
+ parts.push(`7d: ${seven_day.utilization.toFixed(0)}%${reset}`);
2354
+ }
2355
+ return parts.length > 0 ? parts.join(", ") : "no usage data";
2356
+ }
2357
+ function getPlanLabel(account) {
2358
+ if (!account.planTier || account.planTier === "free") return "";
2359
+ return PLAN_LABELS[account.planTier] ?? account.planTier.charAt(0).toUpperCase() + account.planTier.slice(1);
2360
+ }
2361
+
2362
+ // src/rate-limit.ts
2363
+ var {
2364
+ fetchUsageLimits,
2365
+ getResetMsFromUsage,
2366
+ handleRateLimitResponse,
2367
+ retryAfterMsFromResponse
2368
+ } = createRateLimitHandlers({
2369
+ fetchUsage,
2370
+ getConfig,
2371
+ formatWaitTime,
2372
+ getAccountLabel,
2373
+ showToast
2374
+ });
2375
+
2376
+ // src/executor.ts
2377
+ var { executeWithAccountRotation } = createExecutorForProvider("Codex", {
2378
+ handleRateLimitResponse: async (manager, client, account, response) => handleRateLimitResponse(
2379
+ manager,
2380
+ client,
2381
+ account,
2382
+ response
2383
+ ),
2384
+ formatWaitTime,
2385
+ sleep,
2386
+ showToast,
2387
+ getAccountLabel
2388
+ });
2389
+
2390
+ // src/auth-handler.ts
2391
+ import * as v9 from "valibot";
2392
+
2393
+ // src/ui/auth-menu.ts
2394
+ function formatRelativeTime(timestamp) {
2395
+ if (!timestamp) return "never";
2396
+ const days = Math.floor((Date.now() - timestamp) / 864e5);
2397
+ if (days === 0) return "today";
2398
+ if (days === 1) return "yesterday";
2399
+ if (days < 7) return `${days}d ago`;
2400
+ if (days < 30) return `${Math.floor(days / 7)}w ago`;
2401
+ return new Date(timestamp).toLocaleDateString();
2402
+ }
2403
+ function formatDate(timestamp) {
2404
+ if (!timestamp) return "unknown";
2405
+ return new Date(timestamp).toLocaleDateString();
2406
+ }
2407
+ function getAccountStatus(account) {
2408
+ if (account.isAuthDisabled) return "auth-disabled";
2409
+ if (!account.enabled) return "disabled";
2410
+ if (account.rateLimitResetAt && account.rateLimitResetAt > Date.now()) return "rate-limited";
2411
+ return "active";
2412
+ }
2413
+ var STATUS_BADGE = {
2414
+ "active": `${ANSI.green}[active]${ANSI.reset}`,
2415
+ "rate-limited": `${ANSI.yellow}[rate-limited]${ANSI.reset}`,
2416
+ "auth-disabled": `${ANSI.red}[auth-disabled]${ANSI.reset}`,
2417
+ "disabled": `${ANSI.red}[disabled]${ANSI.reset}`
2418
+ };
2419
+ function buildAccountMenuItem(account) {
2420
+ const label = getAccountLabel(account);
2421
+ const status = getAccountStatus(account);
2422
+ const badge = STATUS_BADGE[status];
2423
+ const fullLabel = `${label} ${badge}`;
2424
+ return {
2425
+ label: fullLabel,
2426
+ hint: account.lastUsed ? `used ${formatRelativeTime(account.lastUsed)}` : "",
2427
+ value: account,
2428
+ disabled: false
2429
+ };
2430
+ }
2431
+ async function showAuthMenu(accounts) {
2432
+ const items = [
2433
+ { label: "Add new account", value: { type: "add" }, color: "green" },
2434
+ { label: "Check quotas", value: { type: "check-quotas" }, color: "cyan" },
2435
+ { label: "Manage accounts", value: { type: "manage" } },
2436
+ { label: "Load balancing", value: { type: "load-balancing" } },
2437
+ { label: "", value: { type: "cancel" }, separator: true },
2438
+ { label: "Delete all accounts", value: { type: "delete-all" }, color: "red" }
2439
+ ];
2440
+ while (true) {
2441
+ const subtitle = `${accounts.length} account(s) registered`;
2442
+ const result = await select(items, {
2443
+ message: "Claude Multi-Auth",
2444
+ subtitle
2445
+ });
2446
+ if (!result) return { type: "cancel" };
2447
+ if (result.type === "delete-all") {
2448
+ const confirmed = await confirm("Delete ALL accounts? This cannot be undone.");
2449
+ if (!confirmed) continue;
2450
+ }
2451
+ return result;
2452
+ }
2453
+ }
2454
+ async function showManageAccounts(accounts) {
2455
+ const items = [
2456
+ { label: "Back", value: null },
2457
+ { label: "", value: null, separator: true },
2458
+ ...accounts.map(buildAccountMenuItem)
2459
+ ];
2460
+ const selected = await select(items, {
2461
+ message: "Manage Accounts",
2462
+ subtitle: "Select an account to manage"
2463
+ });
2464
+ if (!selected) return { action: "back" };
2465
+ return showAccountDetails(selected);
2466
+ }
2467
+ async function showAccountDetails(account) {
2468
+ const label = getAccountLabel(account);
2469
+ const status = getAccountStatus(account);
2470
+ const badge = STATUS_BADGE[status];
2471
+ console.log("");
2472
+ console.log(`${ANSI.bold}Account: ${label} ${badge}${ANSI.reset}`);
2473
+ console.log(`${ANSI.dim}Added: ${formatDate(account.addedAt)}${ANSI.reset}`);
2474
+ console.log(`${ANSI.dim}Last used: ${formatRelativeTime(account.lastUsed)}${ANSI.reset}`);
2475
+ if (account.isAuthDisabled) {
2476
+ console.log(`${ANSI.red}Auth disabled: ${account.authDisabledReason ?? "unknown"}${ANSI.reset}`);
2477
+ }
2478
+ console.log("");
2479
+ while (true) {
2480
+ const toggleLabel = account.enabled ? "Disable account" : "Enable account";
2481
+ const toggleColor = account.enabled ? "yellow" : "green";
2482
+ const items = [
2483
+ { label: "Back", value: "back" }
2484
+ ];
2485
+ items.push({ label: toggleLabel, value: "toggle", color: toggleColor });
2486
+ items.push({ label: "Re-authenticate", value: "retry-auth", color: "cyan" });
2487
+ items.push({ label: "Delete this account", value: "delete", color: "red" });
2488
+ const result = await select(items, {
2489
+ message: "Account options",
2490
+ subtitle: label
2491
+ });
2492
+ if (result === "delete") {
2493
+ const confirmed = await confirm(`Delete ${label}?`);
2494
+ if (!confirmed) continue;
2495
+ }
2496
+ return { action: result ?? "cancel", account };
2497
+ }
2498
+ }
2499
+ function getUsageColor(utilization) {
2500
+ if (utilization >= 90) return ANSI.red;
2501
+ if (utilization >= 60) return ANSI.yellow;
2502
+ return ANSI.green;
2503
+ }
2504
+ function createProgressBar(utilization, width = 20) {
2505
+ const filled = Math.round(utilization / 100 * width);
2506
+ const empty = width - filled;
2507
+ const color = getUsageColor(utilization);
2508
+ return `${color}${"\u2588".repeat(filled)}${ANSI.reset}${"\u2591".repeat(empty)} ${color}${Math.round(utilization)}% used${ANSI.reset}`;
2509
+ }
2510
+ function formatResetTime(resetAt) {
2511
+ if (!resetAt) return "";
2512
+ const date = new Date(resetAt);
2513
+ const now = /* @__PURE__ */ new Date();
2514
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
2515
+ const timeStr = date.toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit", hour12: true });
2516
+ const isSameDay = date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate();
2517
+ if (isSameDay) {
2518
+ return ` (resets ${timeStr}, ${tz})`;
2519
+ }
2520
+ const dateStr = date.toLocaleDateString(void 0, { month: "short", day: "numeric" });
2521
+ return ` (resets ${dateStr} ${timeStr}, ${tz})`;
2522
+ }
2523
+ function printUsageEntry(name, entry, isLast) {
2524
+ const connector = isLast ? "\u2514\u2500" : "\u251C\u2500";
2525
+ if (!entry) {
2526
+ console.log(` ${connector} ${name.padEnd(16)} no data`);
2527
+ return;
2528
+ }
2529
+ const bar = createProgressBar(entry.utilization);
2530
+ const reset = formatResetTime(entry.resets_at);
2531
+ console.log(` ${connector} ${name.padEnd(16)} ${bar}${reset}`);
2532
+ }
2533
+ function printQuotaReport(account, usage) {
2534
+ const label = getAccountLabel(account);
2535
+ const status = getAccountStatus(account);
2536
+ const badge = STATUS_BADGE[status];
2537
+ const planLabel = getPlanLabel(account) || "Free";
2538
+ console.log(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);
2539
+ console.log(` ${label} ${badge}`);
2540
+ console.log(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);
2541
+ if (account.email) {
2542
+ console.log(` \u{1F4E7} ${account.email}`);
2543
+ }
2544
+ console.log(` \u{1F4CB} ${planLabel}`);
2545
+ console.log(`
2546
+ \u2514\u2500 Codex Quota`);
2547
+ printUsageEntry("Current session", usage.five_hour, false);
2548
+ printUsageEntry("Current week", usage.seven_day, true);
2549
+ console.log("");
2550
+ }
2551
+ var STRATEGY_DESCRIPTIONS = {
2552
+ "sticky": "Same account until rate-limited",
2553
+ "round-robin": "Rotate every request",
2554
+ "hybrid": "Score-based (usage + health)"
2555
+ };
2556
+ async function showStrategySelect(current) {
2557
+ const strategies = ["sticky", "round-robin", "hybrid"];
2558
+ const items = strategies.map((s) => ({
2559
+ label: `${s}${s === current ? " (current)" : ""}`,
2560
+ hint: STRATEGY_DESCRIPTIONS[s],
2561
+ value: s
2562
+ }));
2563
+ return select(items, {
2564
+ message: "Load Balancing Strategy",
2565
+ subtitle: `Current: ${current}`
2566
+ });
2567
+ }
2568
+ async function showMethodSelect() {
2569
+ const items = [
2570
+ { label: "ChatGPT Pro/Plus (browser)", value: "browser", color: "green" },
2571
+ { label: "ChatGPT Pro/Plus (headless)", value: "headless" }
2572
+ ];
2573
+ return select(items, {
2574
+ message: "Login Method",
2575
+ subtitle: "Select authentication method"
2576
+ });
2577
+ }
2578
+ function printQuotaError(account, error) {
2579
+ const label = getAccountLabel(account);
2580
+ console.log(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);
2581
+ console.log(` ${label}`);
2582
+ if (account.email) {
2583
+ console.log(` \u{1F4E7} ${account.email}`);
2584
+ }
2585
+ console.log(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);
2586
+ console.log(` ${ANSI.red}Error: ${error}${ANSI.reset}
2587
+ `);
2588
+ }
2589
+
2590
+ // src/account-store.ts
2591
+ setAccountsFilename(ACCOUNTS_FILENAME2);
2592
+
2593
+ // src/auth-handler.ts
2594
+ import { randomUUID as randomUUID2 } from "node:crypto";
2595
+ var DeviceUserCodeResponseSchema = v9.object({
2596
+ device_code: v9.string(),
2597
+ user_code: v9.string(),
2598
+ expires_in: v9.number(),
2599
+ interval: v9.optional(v9.number())
2600
+ });
2601
+ function makeFailedFlowResult(message) {
2602
+ return {
2603
+ url: "",
2604
+ instructions: message,
2605
+ method: "auto",
2606
+ callback: async () => ({ type: "failed" })
2607
+ };
2608
+ }
2609
+ function normalizeMethod(method) {
2610
+ return method === "headless" ? "headless" : "browser";
2611
+ }
2612
+ function toCallbackResponse(tokens) {
2613
+ if (!tokens.refresh_token) {
2614
+ return { type: "failed" };
2615
+ }
2616
+ return {
2617
+ type: "success",
2618
+ refresh: tokens.refresh_token,
2619
+ access: tokens.access_token,
2620
+ expires: Date.now() + (tokens.expires_in ?? 3600) * 1e3,
2621
+ accountId: extractAccountId(tokens)
2622
+ };
2623
+ }
2624
+ async function pollDeviceAuthToken(startResult) {
2625
+ const expiresAt = Date.now() + startResult.expires_in * 1e3;
2626
+ let intervalMs = Math.max(1, startResult.interval ?? 5) * 1e3;
2627
+ while (Date.now() < expiresAt) {
2628
+ const response = await fetch(`${OAUTH_ISSUER}/api/accounts/deviceauth/token`, {
2629
+ method: "POST",
2630
+ headers: {
2631
+ "Content-Type": "application/json"
2632
+ },
2633
+ body: JSON.stringify({
2634
+ client_id: OPENAI_CLIENT_ID,
2635
+ device_code: startResult.device_code
2636
+ })
2637
+ });
2638
+ if (response.ok) {
2639
+ return v9.parse(TokenResponseSchema, await response.json());
2640
+ }
2641
+ const payload = await response.json().catch(() => ({}));
2642
+ const error = typeof payload.error === "string" ? payload.error : "";
2643
+ if (error === "authorization_pending") {
2644
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
2645
+ continue;
2646
+ }
2647
+ if (error === "slow_down") {
2648
+ intervalMs += 5e3;
2649
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
2650
+ continue;
2651
+ }
2652
+ if (error === "expired_token") {
2653
+ throw new Error("Device code expired. Start authentication again.");
2654
+ }
2655
+ if (error === "access_denied") {
2656
+ throw new Error("Device authorization denied by user.");
2657
+ }
2658
+ throw new Error(`Device token polling failed: ${response.status}`);
2659
+ }
2660
+ throw new Error("Device authorization timed out");
2661
+ }
2662
+ async function startBrowserAuth() {
2663
+ const { redirectUri } = await startOAuthServer();
2664
+ const pkce = await generatePKCE();
2665
+ const state = generateState();
2666
+ const authUrl = buildAuthorizeUrl(redirectUri, pkce, state);
2667
+ return {
2668
+ url: authUrl,
2669
+ instructions: "Complete authorization in your browser.",
2670
+ method: "auto",
2671
+ callback: async () => {
2672
+ try {
2673
+ const tokens = await waitForOAuthCallback(pkce, state);
2674
+ return toCallbackResponse(tokens);
2675
+ } finally {
2676
+ stopOAuthServer();
2677
+ }
2678
+ }
2679
+ };
2680
+ }
2681
+ async function startDeviceAuth() {
2682
+ const response = await fetch(`${OAUTH_ISSUER}/api/accounts/deviceauth/usercode`, {
2683
+ method: "POST",
2684
+ headers: {
2685
+ "Content-Type": "application/json"
2686
+ },
2687
+ body: JSON.stringify({ client_id: OPENAI_CLIENT_ID })
2688
+ });
2689
+ if (!response.ok) {
2690
+ throw new Error(`Failed to start device authorization: ${response.status}`);
2691
+ }
2692
+ const startResult = v9.parse(DeviceUserCodeResponseSchema, await response.json());
2693
+ return {
2694
+ url: `${OAUTH_ISSUER}/codex/device`,
2695
+ instructions: `Enter code: ${startResult.user_code}`,
2696
+ method: "auto",
2697
+ callback: async () => {
2698
+ const tokens = await pollDeviceAuthToken(startResult);
2699
+ return toCallbackResponse(tokens);
2700
+ }
2701
+ };
2702
+ }
2703
+ async function startFlow(method) {
2704
+ if (normalizeMethod(method) === "headless") {
2705
+ return startDeviceAuth();
2706
+ }
2707
+ return startBrowserAuth();
2708
+ }
2709
+ function wrapCallbackWithAccountReplace(result, manager, targetAccount) {
2710
+ const originalCallback = result.callback;
2711
+ return {
2712
+ ...result,
2713
+ callback: async function() {
2714
+ const code = arguments.length > 0 ? arguments[0] : void 0;
2715
+ const callbackResult = await originalCallback(code);
2716
+ if (callbackResult?.type === "success" && callbackResult.refresh) {
2717
+ const auth = {
2718
+ type: "oauth",
2719
+ refresh: callbackResult.refresh,
2720
+ access: callbackResult.access,
2721
+ expires: callbackResult.expires
2722
+ };
2723
+ if (targetAccount.uuid) {
2724
+ await manager.replaceAccountCredentials(targetAccount.uuid, auth);
2725
+ }
2726
+ const label = getAccountLabel(targetAccount);
2727
+ console.log(`
2728
+ \u2705 ${label} re-authenticated successfully.
2729
+ `);
2730
+ }
2731
+ return callbackResult;
2732
+ }
2733
+ };
2734
+ }
2735
+ function wrapCallbackWithManagerSync(result, manager) {
2736
+ const originalCallback = result.callback;
2737
+ return {
2738
+ ...result,
2739
+ callback: async function() {
2740
+ const code = arguments.length > 0 ? arguments[0] : void 0;
2741
+ const callbackResult = await originalCallback(code);
2742
+ if (callbackResult?.type === "success" && callbackResult.refresh) {
2743
+ const auth = {
2744
+ type: "oauth",
2745
+ refresh: callbackResult.refresh,
2746
+ access: callbackResult.access,
2747
+ expires: callbackResult.expires
2748
+ };
2749
+ if (manager) {
2750
+ const countBefore = manager.getAccounts().length;
2751
+ await manager.addAccount(auth);
2752
+ const countAfter = manager.getAccounts().length;
2753
+ if (countAfter > countBefore) {
2754
+ console.log(`
2755
+ \u2705 Account added to multi-auth pool (${countAfter} total).
2756
+ `);
2757
+ } else {
2758
+ console.log(`
2759
+ \u2139\uFE0F Account already exists in multi-auth pool (${countAfter} total).
2760
+ `);
2761
+ }
2762
+ } else {
2763
+ await persistFallback(auth, callbackResult.accountId);
2764
+ console.log("\n\u2705 Account saved.\n");
2765
+ }
2766
+ }
2767
+ return callbackResult;
2768
+ }
2769
+ };
2770
+ }
2771
+ async function persistFallback(auth, accountId) {
2772
+ try {
2773
+ const store = new AccountStore();
2774
+ const now = Date.now();
2775
+ const account = {
2776
+ uuid: randomUUID2(),
2777
+ accountId,
2778
+ refreshToken: auth.refresh,
2779
+ accessToken: auth.access,
2780
+ expiresAt: auth.expires,
2781
+ addedAt: now,
2782
+ lastUsed: now,
2783
+ enabled: true,
2784
+ planTier: "",
2785
+ consecutiveAuthFailures: 0,
2786
+ isAuthDisabled: false
2787
+ };
2788
+ await store.addAccount(account);
2789
+ await store.setActiveUuid(account.uuid);
2790
+ } catch {
2791
+ }
2792
+ }
2793
+ async function selectMethodAndStartFlow() {
2794
+ if (!isTTY()) {
2795
+ const flow2 = await startFlow("browser");
2796
+ return { flow: flow2, method: "browser" };
2797
+ }
2798
+ const selected = await showMethodSelect();
2799
+ if (!selected) return null;
2800
+ const flow = await startFlow(selected);
2801
+ return { flow, method: selected };
2802
+ }
2803
+ async function handleAuthorize(manager, inputs, client) {
2804
+ if (!inputs || !isTTY()) {
2805
+ return wrapCallbackWithManagerSync(await startFlow("browser"), manager);
2806
+ }
2807
+ const effectiveManager = manager ?? await loadManagerFromDisk(client);
2808
+ if (!effectiveManager || effectiveManager.getAccounts().length === 0) {
2809
+ const result = await selectMethodAndStartFlow();
2810
+ if (!result) return makeFailedFlowResult("Authentication cancelled");
2811
+ return wrapCallbackWithManagerSync(result.flow, manager);
2812
+ }
2813
+ return runAccountManagementMenu(effectiveManager, client);
2814
+ }
2815
+ async function loadManagerFromDisk(client) {
2816
+ const store = new AccountStore();
2817
+ const stored = await store.load();
2818
+ if (stored.accounts.length === 0) return null;
2819
+ const emptyAuth = { type: "oauth", refresh: "", access: "", expires: 0 };
2820
+ const mgr = await AccountManager.create(store, emptyAuth, client);
2821
+ return mgr;
2822
+ }
2823
+ async function runAccountManagementMenu(manager, client) {
2824
+ while (true) {
2825
+ const allAccounts = manager.getAccounts();
2826
+ const menuAction = await showAuthMenu(allAccounts);
2827
+ switch (menuAction.type) {
2828
+ case "add": {
2829
+ const result = await selectMethodAndStartFlow();
2830
+ if (!result) continue;
2831
+ return wrapCallbackWithManagerSync(result.flow, manager);
2832
+ }
2833
+ case "check-quotas":
2834
+ await handleCheckQuotas(manager, client);
2835
+ continue;
2836
+ case "manage": {
2837
+ const result = await showManageAccounts(allAccounts);
2838
+ if (result.action === "back" || result.action === "cancel") continue;
2839
+ const manageResult = await handleManageAction(manager, result.action, result.account, client);
2840
+ if (manageResult.triggerOAuth) {
2841
+ const methodResult = await selectMethodAndStartFlow();
2842
+ if (!methodResult) continue;
2843
+ return wrapCallbackWithAccountReplace(methodResult.flow, manager, manageResult.account);
2844
+ }
2845
+ continue;
2846
+ }
2847
+ case "load-balancing":
2848
+ await handleLoadBalancing();
2849
+ continue;
2850
+ case "delete-all": {
2851
+ await manager.clearAllAccounts();
2852
+ console.log("\nAll accounts deleted.\n");
2853
+ const result = await selectMethodAndStartFlow();
2854
+ if (!result) return makeFailedFlowResult("Authentication cancelled");
2855
+ return wrapCallbackWithManagerSync(result.flow, manager);
2856
+ }
2857
+ case "cancel":
2858
+ return makeFailedFlowResult("Authentication cancelled");
2859
+ }
2860
+ }
2861
+ }
2862
+ async function handleCheckQuotas(manager, client) {
2863
+ await manager.refresh();
2864
+ const accounts = manager.getAccounts();
2865
+ const effectiveClient = client ?? createMinimalClient();
2866
+ if (client) manager.setClient(client);
2867
+ console.log(`
2868
+ \u{1F4CA} Checking quotas for ${accounts.length} account(s)...
2869
+ `);
2870
+ for (const account of accounts) {
2871
+ if (account.isAuthDisabled || !account.accessToken || isTokenExpired(account)) {
2872
+ if (!account.uuid) {
2873
+ printQuotaError(account, "Missing account UUID");
2874
+ continue;
2875
+ }
2876
+ const result = await manager.ensureValidToken(account.uuid, effectiveClient);
2877
+ if (!result.ok) {
2878
+ printQuotaError(account, account.isAuthDisabled ? `${account.authDisabledReason ?? "Auth disabled"} (refresh failed)` : "Failed to refresh token");
2879
+ continue;
2880
+ }
2881
+ await manager.refresh();
2882
+ }
2883
+ const freshAccounts = manager.getAccounts();
2884
+ const freshAccount = freshAccounts.find((candidate) => candidate.uuid === account.uuid);
2885
+ if (!freshAccount?.accessToken) {
2886
+ printQuotaError(account, "No access token available");
2887
+ continue;
2888
+ }
2889
+ const usageResult = await fetchUsage(freshAccount.accessToken, freshAccount.accountId);
2890
+ if (!usageResult.ok) {
2891
+ printQuotaError(freshAccount, `Failed to fetch usage: ${usageResult.reason}`);
2892
+ continue;
2893
+ }
2894
+ if (freshAccount.uuid) {
2895
+ await manager.applyUsageCache(freshAccount.uuid, usageResult.data);
2896
+ }
2897
+ const profileResult = fetchProfile(freshAccount.accessToken);
2898
+ let email = freshAccount.email;
2899
+ let planTier = freshAccount.planTier ?? "";
2900
+ if (profileResult.ok) {
2901
+ email = profileResult.data.email ?? email;
2902
+ planTier = profileResult.data.planTier;
2903
+ }
2904
+ if ((!planTier || planTier === "free") && usageResult.planType) {
2905
+ planTier = derivePlanTier(usageResult.planType);
2906
+ }
2907
+ const profileData = { email, planTier };
2908
+ if (freshAccount.uuid) {
2909
+ await manager.applyProfileCache(freshAccount.uuid, profileData);
2910
+ }
2911
+ const reportAccount = { ...freshAccount, email, planTier };
2912
+ printQuotaReport(reportAccount, usageResult.data);
2913
+ }
2914
+ }
2915
+ async function handleLoadBalancing() {
2916
+ const current = getConfig().account_selection_strategy;
2917
+ const selected = await showStrategySelect(current);
2918
+ if (!selected || selected === current) return;
2919
+ await updateConfigField("account_selection_strategy", selected);
2920
+ console.log(`
2921
+ Load balancing strategy changed: ${current} \u2192 ${selected}
2922
+ `);
2923
+ }
2924
+ async function handleManageAction(manager, action, account, client) {
2925
+ if (!account) return { triggerOAuth: false };
2926
+ const label = getAccountLabel(account);
2927
+ switch (action) {
2928
+ case "toggle":
2929
+ if (!account.uuid) break;
2930
+ await manager.toggleEnabled(account.uuid);
2931
+ await manager.refresh();
2932
+ {
2933
+ const updated = manager.getAccounts().find((candidate) => candidate.uuid === account.uuid);
2934
+ console.log(`
2935
+ ${label} ${updated?.enabled ? "enabled" : "disabled"}.
2936
+ `);
2937
+ }
2938
+ break;
2939
+ case "delete":
2940
+ if (!account.uuid) break;
2941
+ {
2942
+ const removed = await manager.removeAccount(account.index);
2943
+ console.log(removed ? "\nAccount deleted.\n" : "\nFailed to delete account.\n");
2944
+ }
2945
+ break;
2946
+ case "retry-auth": {
2947
+ if (!account.uuid) break;
2948
+ const effectiveClient = client ?? createMinimalClient();
2949
+ console.log(`
2950
+ Retrying authentication for ${label}...
2951
+ `);
2952
+ const result = await manager.retryAuth(account.uuid, effectiveClient);
2953
+ if (result.ok) {
2954
+ console.log(`\u2705 ${label} re-authenticated successfully.
2955
+ `);
2956
+ } else {
2957
+ console.log("Token refresh failed \u2014 starting OAuth flow...\n");
2958
+ return { triggerOAuth: true, account };
2959
+ }
2960
+ break;
2961
+ }
2962
+ }
2963
+ return { triggerOAuth: false };
2964
+ }
2965
+
2966
+ // src/proactive-refresh.ts
2967
+ var ProactiveRefreshQueue = createProactiveRefreshQueueForProvider({
2968
+ getConfig,
2969
+ isTokenExpired,
2970
+ refreshToken,
2971
+ debugLog
2972
+ });
2973
+
2974
+ // src/request-transform.ts
2975
+ function mergeHeaders(base, incoming) {
2976
+ if (!incoming) return;
2977
+ if (incoming instanceof Headers) {
2978
+ incoming.forEach((value, key) => base.set(key, value));
2979
+ return;
2980
+ }
2981
+ if (Array.isArray(incoming)) {
2982
+ for (const [key, value] of incoming) {
2983
+ if (value !== void 0) base.set(key, String(value));
2984
+ }
2985
+ return;
2986
+ }
2987
+ for (const [key, value] of Object.entries(incoming)) {
2988
+ if (value !== void 0) base.set(key, String(value));
2989
+ }
2990
+ }
2991
+ function buildRequestHeaders(input, init, accessToken, accountId) {
2992
+ const headers = new Headers();
2993
+ if (input instanceof Request) {
2994
+ input.headers.forEach((value, key) => headers.set(key, value));
2995
+ }
2996
+ mergeHeaders(headers, init?.headers);
2997
+ headers.delete("authorization");
2998
+ headers.delete("Authorization");
2999
+ headers.set("authorization", `Bearer ${accessToken}`);
3000
+ headers.set("originator", "opencode");
3001
+ headers.set("User-Agent", OPENAI_CLI_USER_AGENT);
3002
+ if (accountId) {
3003
+ headers.set("ChatGPT-Account-Id", accountId);
3004
+ } else {
3005
+ headers.delete("ChatGPT-Account-Id");
3006
+ }
3007
+ headers.delete("x-api-key");
3008
+ return headers;
3009
+ }
3010
+ function asUrl(input) {
3011
+ try {
3012
+ if (typeof input === "string" || input instanceof URL) {
3013
+ return new URL(input.toString());
3014
+ }
3015
+ if (input instanceof Request) {
3016
+ return new URL(input.url);
3017
+ }
3018
+ return null;
3019
+ } catch {
3020
+ return null;
3021
+ }
3022
+ }
3023
+ function transformRequestUrl(input) {
3024
+ const url = asUrl(input);
3025
+ if (!url) return input;
3026
+ const shouldRewrite = url.pathname.includes("/v1/responses") || url.pathname.includes("/chat/completions");
3027
+ if (!shouldRewrite) return input;
3028
+ const rewritten = new URL(CODEX_API_ENDPOINT);
3029
+ rewritten.search = url.search;
3030
+ if (input instanceof Request) {
3031
+ return new Request(rewritten.toString(), input);
3032
+ }
3033
+ return rewritten;
3034
+ }
3035
+
3036
+ // src/runtime-factory.ts
3037
+ var AccountRuntimeFactory = class {
3038
+ constructor(store, client) {
3039
+ this.store = store;
3040
+ this.client = client;
3041
+ }
3042
+ runtimes = /* @__PURE__ */ new Map();
3043
+ initLocks = /* @__PURE__ */ new Map();
3044
+ async getRuntime(uuid) {
3045
+ const cached = this.runtimes.get(uuid);
3046
+ if (cached) return cached;
3047
+ const existing = this.initLocks.get(uuid);
3048
+ if (existing) return existing;
3049
+ const initPromise = this.createRuntime(uuid);
3050
+ this.initLocks.set(uuid, initPromise);
3051
+ try {
3052
+ const runtime = await initPromise;
3053
+ this.runtimes.set(uuid, runtime);
3054
+ return runtime;
3055
+ } finally {
3056
+ this.initLocks.delete(uuid);
3057
+ }
3058
+ }
3059
+ invalidate(uuid) {
3060
+ this.runtimes.delete(uuid);
3061
+ }
3062
+ invalidateAll() {
3063
+ this.runtimes.clear();
3064
+ }
3065
+ async createRuntime(uuid) {
3066
+ const fetchWithAccount = async (input, init) => {
3067
+ const storage = await this.store.load();
3068
+ const storedAccount = storage.accounts.find((account) => account.uuid === uuid);
3069
+ if (!storedAccount) {
3070
+ throw new Error(`No credentials found for account ${uuid}`);
3071
+ }
3072
+ let accessToken = storedAccount.accessToken;
3073
+ let expiresAt = storedAccount.expiresAt;
3074
+ let accountId = storedAccount.accountId;
3075
+ if (!accessToken || !expiresAt || isTokenExpired({ accessToken, expiresAt })) {
3076
+ const refreshed = await refreshToken(storedAccount.refreshToken, uuid, this.client);
3077
+ if (!refreshed.ok) {
3078
+ if (typeof refreshed.status === "number") {
3079
+ throw new Error(`Token refresh failed: ${refreshed.status}`);
3080
+ }
3081
+ throw new Error("Token refresh failed");
3082
+ }
3083
+ accessToken = refreshed.patch.accessToken;
3084
+ expiresAt = refreshed.patch.expiresAt;
3085
+ accountId = refreshed.patch.accountId ?? accountId;
3086
+ await this.store.mutateAccount(uuid, (account) => {
3087
+ account.accessToken = refreshed.patch.accessToken;
3088
+ account.expiresAt = refreshed.patch.expiresAt;
3089
+ if (refreshed.patch.refreshToken) account.refreshToken = refreshed.patch.refreshToken;
3090
+ if (refreshed.patch.accountId) account.accountId = refreshed.patch.accountId;
3091
+ if (refreshed.patch.email) account.email = refreshed.patch.email;
3092
+ account.consecutiveAuthFailures = 0;
3093
+ account.isAuthDisabled = false;
3094
+ account.authDisabledReason = void 0;
3095
+ });
3096
+ }
3097
+ if (!accessToken) {
3098
+ throw new Error(`No access token available for account ${uuid}`);
3099
+ }
3100
+ const transformedInput = transformRequestUrl(input);
3101
+ const headers = buildRequestHeaders(transformedInput, init, accessToken, accountId);
3102
+ return fetch(transformedInput, {
3103
+ ...init,
3104
+ headers
3105
+ });
3106
+ };
3107
+ debugLog(this.client, `Runtime created for account ${uuid.slice(0, 8)}`);
3108
+ return { fetch: fetchWithAccount };
3109
+ }
3110
+ };
3111
+
3112
+ // src/index.ts
3113
+ var CodexMultiAuthPlugin = async (ctx) => {
3114
+ const { client } = ctx;
3115
+ await loadConfig();
3116
+ const store = new AccountStore();
3117
+ let manager = null;
3118
+ let runtimeFactory = null;
3119
+ let refreshQueue = null;
3120
+ return {
3121
+ tool: {
3122
+ [OPENAI_OAUTH_ADAPTER.statusToolName]: tool({
3123
+ description: "Show status of all multi-auth accounts including rate limits and usage.",
3124
+ args: {},
3125
+ async execute(_args, _context) {
3126
+ if (!manager) {
3127
+ return "Multi-auth not initialized. No OAuth accounts detected.";
3128
+ }
3129
+ const accounts = manager.getAccounts();
3130
+ if (accounts.length === 0) {
3131
+ return "No accounts configured. Run `opencode auth login` to add an account.";
3132
+ }
3133
+ const lines = [
3134
+ `## Codex Multi-Auth Status (${accounts.length} accounts)
3135
+ `
3136
+ ];
3137
+ for (const account of accounts) {
3138
+ const isActive = account.uuid === manager.getActiveAccount()?.uuid;
3139
+ const marker = isActive ? " **[ACTIVE]**" : "";
3140
+ const label = getAccountLabel(account);
3141
+ const usage = getUsageSummary(account);
3142
+ const statusParts = [];
3143
+ if (account.isAuthDisabled) statusParts.push(`AUTH DISABLED: ${account.authDisabledReason}`);
3144
+ else if (!account.enabled) statusParts.push("disabled");
3145
+ else statusParts.push("enabled");
3146
+ if (account.rateLimitResetAt && account.rateLimitResetAt > Date.now()) {
3147
+ const remaining = formatWaitTime(account.rateLimitResetAt - Date.now());
3148
+ statusParts.push(`RATE LIMITED (resets in ${remaining})`);
3149
+ }
3150
+ lines.push(
3151
+ `- **${label}**${marker}: ${statusParts.join(" | ")} | ${usage}`
3152
+ );
3153
+ }
3154
+ return lines.join("\n");
3155
+ }
3156
+ })
3157
+ },
3158
+ auth: {
3159
+ provider: OPENAI_OAUTH_ADAPTER.authProviderId,
3160
+ methods: [
3161
+ {
3162
+ label: OPENAI_OAUTH_ADAPTER.authMethodLabel,
3163
+ type: "oauth",
3164
+ async authorize() {
3165
+ const inputs = arguments.length > 0 ? arguments[0] : void 0;
3166
+ return handleAuthorize(manager, inputs, client);
3167
+ }
3168
+ },
3169
+ { type: "api", label: "Manually enter API Key" }
3170
+ ],
3171
+ async loader(getAuth, provider) {
3172
+ const auth = await getAuth();
3173
+ if (auth.type !== "oauth") {
3174
+ return { apiKey: "", fetch };
3175
+ }
3176
+ for (const model of Object.values(provider.models ?? {})) {
3177
+ if (model) {
3178
+ model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } };
3179
+ }
3180
+ }
3181
+ const credentials = auth;
3182
+ await migrateFromAuthJson("openai", store);
3183
+ manager = await AccountManager.create(store, credentials, client);
3184
+ runtimeFactory = new AccountRuntimeFactory(store, client);
3185
+ manager.setRuntimeFactory(runtimeFactory);
3186
+ if (manager.getAccountCount() > 0) {
3187
+ const activeAccount = manager.getActiveAccount();
3188
+ const activeLabel = activeAccount ? getAccountLabel(activeAccount) : "none";
3189
+ void showToast(
3190
+ client,
3191
+ `Multi-Auth: ${manager.getAccountCount()} account(s) loaded. Active: ${activeLabel}`,
3192
+ "info"
3193
+ );
3194
+ await manager.validateNonActiveTokens(client);
3195
+ const disabledCount = manager.getAccounts().filter((a) => a.isAuthDisabled).length;
3196
+ if (disabledCount > 0) {
3197
+ void showToast(
3198
+ client,
3199
+ `${disabledCount} account(s) have auth failures.`,
3200
+ "warning"
3201
+ );
3202
+ }
3203
+ if (refreshQueue) {
3204
+ await refreshQueue.stop();
3205
+ }
3206
+ refreshQueue = new ProactiveRefreshQueue(
3207
+ client,
3208
+ store,
3209
+ (uuid) => runtimeFactory?.invalidate(uuid)
3210
+ );
3211
+ refreshQueue.start();
3212
+ }
3213
+ return {
3214
+ apiKey: "CODEX_OAUTH",
3215
+ async fetch(input, init) {
3216
+ if (!manager || !runtimeFactory) {
3217
+ return fetch(input, init);
3218
+ }
3219
+ if (manager.getAccountCount() === 0) {
3220
+ throw new Error(
3221
+ "No Codex accounts configured. Run `opencode auth login` to add an account."
3222
+ );
3223
+ }
3224
+ return executeWithAccountRotation(manager, runtimeFactory, client, input, init);
3225
+ }
3226
+ };
3227
+ }
3228
+ }
3229
+ };
3230
+ };
3231
+ export {
3232
+ CodexMultiAuthPlugin
3233
+ };