opencode-codex-multi-account 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +28 -4
- package/dist/index.js +154 -1952
- package/dist/index.js.map +1 -0
- package/package.json +3 -3
- package/dist/account-manager.d.ts +0 -2
- package/dist/account-store.d.ts +0 -2
- package/dist/auth-handler.d.ts +0 -20
- package/dist/claims.d.ts +0 -1
- package/dist/config.d.ts +0 -2
- package/dist/constants.d.ts +0 -32
- package/dist/executor.d.ts +0 -2
- package/dist/oauth.d.ts +0 -37
- package/dist/proactive-refresh.d.ts +0 -2
- package/dist/rate-limit.d.ts +0 -2
- package/dist/request-transform.d.ts +0 -2
- package/dist/runtime-factory.d.ts +0 -18
- package/dist/storage.d.ts +0 -1
- package/dist/token.d.ts +0 -4
- package/dist/types.d.ts +0 -216
- package/dist/ui/ansi.d.ts +0 -1
- package/dist/ui/auth-menu.d.ts +0 -28
- package/dist/ui/confirm.d.ts +0 -1
- package/dist/ui/select.d.ts +0 -1
- package/dist/usage.d.ts +0 -25
- package/dist/utils.d.ts +0 -1
package/dist/index.js
CHANGED
|
@@ -1,1860 +1,12 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
import { migrateFromAuthJson } from "opencode-multi-account-core";
|
|
3
4
|
|
|
4
|
-
//
|
|
5
|
-
import {
|
|
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
|
-
var TokenRefreshError = class _TokenRefreshError extends Error {
|
|
88
|
-
status;
|
|
89
|
-
permanent;
|
|
90
|
-
constructor(permanent, status) {
|
|
91
|
-
super(status === void 0 ? "Token refresh failed" : `Token refresh failed: ${status}`);
|
|
92
|
-
this.name = "TokenRefreshError";
|
|
93
|
-
this.status = status;
|
|
94
|
-
this.permanent = permanent;
|
|
95
|
-
Object.setPrototypeOf(this, _TokenRefreshError.prototype);
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
function isTokenRefreshError(error) {
|
|
99
|
-
if (error instanceof TokenRefreshError) return true;
|
|
100
|
-
if (!(error instanceof Error)) return false;
|
|
101
|
-
const candidate = error;
|
|
102
|
-
return candidate.name === "TokenRefreshError" && typeof candidate.permanent === "boolean" && (candidate.status === void 0 || typeof candidate.status === "number");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ../multi-account-core/src/config.ts
|
|
106
|
-
var DEFAULT_CONFIG_FILENAME = "multiauth-config.json";
|
|
107
|
-
var DEFAULT_CONFIG = v2.parse(PluginConfigSchema, {});
|
|
108
|
-
var configFilename = DEFAULT_CONFIG_FILENAME;
|
|
109
|
-
var cachedConfig = null;
|
|
110
|
-
var externalConfigGetter = null;
|
|
111
|
-
function getConfigDir() {
|
|
112
|
-
return process.env.OPENCODE_CONFIG_DIR || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
|
|
113
|
-
}
|
|
114
|
-
function getConfigPath() {
|
|
115
|
-
return join(getConfigDir(), configFilename);
|
|
116
|
-
}
|
|
117
|
-
function parseConfig(raw) {
|
|
118
|
-
const result = v2.safeParse(PluginConfigSchema, raw);
|
|
119
|
-
return result.success ? result.output : DEFAULT_CONFIG;
|
|
120
|
-
}
|
|
121
|
-
function initCoreConfig(filename) {
|
|
122
|
-
configFilename = filename || DEFAULT_CONFIG_FILENAME;
|
|
123
|
-
cachedConfig = null;
|
|
124
|
-
}
|
|
125
|
-
async function loadConfig() {
|
|
126
|
-
if (cachedConfig) return cachedConfig;
|
|
127
|
-
const path = getConfigPath();
|
|
128
|
-
try {
|
|
129
|
-
const content = await fs.readFile(path, "utf-8");
|
|
130
|
-
cachedConfig = parseConfig(JSON.parse(content));
|
|
131
|
-
} catch {
|
|
132
|
-
cachedConfig = DEFAULT_CONFIG;
|
|
133
|
-
}
|
|
134
|
-
return cachedConfig;
|
|
135
|
-
}
|
|
136
|
-
function getConfig() {
|
|
137
|
-
if (cachedConfig) return cachedConfig;
|
|
138
|
-
if (externalConfigGetter && externalConfigGetter !== getConfig) {
|
|
139
|
-
try {
|
|
140
|
-
return parseConfig(externalConfigGetter());
|
|
141
|
-
} catch {
|
|
142
|
-
return DEFAULT_CONFIG;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return DEFAULT_CONFIG;
|
|
146
|
-
}
|
|
147
|
-
function setConfigGetter(getter) {
|
|
148
|
-
if (getter === getConfig) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
externalConfigGetter = getter;
|
|
152
|
-
}
|
|
153
|
-
async function updateConfigField(key, value) {
|
|
154
|
-
const path = getConfigPath();
|
|
155
|
-
let existing = {};
|
|
156
|
-
try {
|
|
157
|
-
const content2 = await fs.readFile(path, "utf-8");
|
|
158
|
-
existing = JSON.parse(content2);
|
|
159
|
-
} catch {
|
|
160
|
-
}
|
|
161
|
-
existing[key] = value;
|
|
162
|
-
await fs.mkdir(dirname(path), { recursive: true });
|
|
163
|
-
const content = `${JSON.stringify(existing, null, 2)}
|
|
164
|
-
`;
|
|
165
|
-
const tempPath = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
166
|
-
try {
|
|
167
|
-
await fs.writeFile(tempPath, content, "utf-8");
|
|
168
|
-
await fs.rename(tempPath, path);
|
|
169
|
-
} catch (error) {
|
|
170
|
-
try {
|
|
171
|
-
await fs.unlink(tempPath);
|
|
172
|
-
} catch {
|
|
173
|
-
}
|
|
174
|
-
throw error;
|
|
175
|
-
}
|
|
176
|
-
cachedConfig = null;
|
|
177
|
-
await loadConfig();
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ../multi-account-core/src/utils.ts
|
|
181
|
-
function getConfigDir2() {
|
|
182
|
-
return process.env.OPENCODE_CONFIG_DIR || join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "opencode");
|
|
183
|
-
}
|
|
184
|
-
function getErrorCode(error) {
|
|
185
|
-
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
186
|
-
return void 0;
|
|
187
|
-
}
|
|
188
|
-
const code = error.code;
|
|
189
|
-
return typeof code === "string" ? code : void 0;
|
|
190
|
-
}
|
|
191
|
-
function formatWaitTime(ms) {
|
|
192
|
-
const totalSeconds = Math.ceil(ms / 1e3);
|
|
193
|
-
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
194
|
-
const days = Math.floor(totalSeconds / 86400);
|
|
195
|
-
const hours = Math.floor(totalSeconds % 86400 / 3600);
|
|
196
|
-
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
197
|
-
const seconds = totalSeconds % 60;
|
|
198
|
-
const parts = [];
|
|
199
|
-
if (days > 0) parts.push(`${days}d`);
|
|
200
|
-
if (hours > 0) parts.push(`${hours}h`);
|
|
201
|
-
if (minutes > 0) parts.push(`${minutes}m`);
|
|
202
|
-
if (seconds > 0 && days === 0) parts.push(`${seconds}s`);
|
|
203
|
-
return parts.join(" ") || "0s";
|
|
204
|
-
}
|
|
205
|
-
function getAccountLabel(account) {
|
|
206
|
-
if (account.label) return account.label;
|
|
207
|
-
if (account.email) return account.email;
|
|
208
|
-
if (account.uuid) return `Account (${account.uuid.slice(0, 8)})`;
|
|
209
|
-
return `Account ${account.index + 1}`;
|
|
210
|
-
}
|
|
211
|
-
function sleep(ms) {
|
|
212
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
213
|
-
}
|
|
214
|
-
async function showToast(client, message, variant) {
|
|
215
|
-
if (getConfig().quiet_mode) return;
|
|
216
|
-
try {
|
|
217
|
-
await client.tui.showToast({ body: { message, variant } });
|
|
218
|
-
} catch {
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
function debugLog(client, message, extra) {
|
|
222
|
-
if (!getConfig().debug) return;
|
|
223
|
-
client.app.log({
|
|
224
|
-
body: { service: "claude-multiauth", level: "debug", message, extra }
|
|
225
|
-
}).catch(() => {
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
function createMinimalClient() {
|
|
229
|
-
return {
|
|
230
|
-
auth: {
|
|
231
|
-
set: async () => {
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
tui: {
|
|
235
|
-
showToast: async () => {
|
|
236
|
-
}
|
|
237
|
-
},
|
|
238
|
-
app: {
|
|
239
|
-
log: async () => {
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
function getClearedOAuthBody() {
|
|
245
|
-
return {
|
|
246
|
-
type: "oauth",
|
|
247
|
-
refresh: "",
|
|
248
|
-
access: "",
|
|
249
|
-
expires: 0
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// ../multi-account-core/src/claims.ts
|
|
254
|
-
var CLAIMS_FILENAME = "multiauth-claims.json";
|
|
255
|
-
var CLAIM_EXPIRY_MS = 6e4;
|
|
256
|
-
function getClaimsPath() {
|
|
257
|
-
return join3(getConfigDir2(), CLAIMS_FILENAME);
|
|
258
|
-
}
|
|
259
|
-
function isClaimShape(value) {
|
|
260
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
261
|
-
const claim = value;
|
|
262
|
-
return typeof claim.pid === "number" && Number.isInteger(claim.pid) && claim.pid > 0 && typeof claim.at === "number" && Number.isFinite(claim.at);
|
|
263
|
-
}
|
|
264
|
-
function parseClaims(raw) {
|
|
265
|
-
let parsed;
|
|
266
|
-
try {
|
|
267
|
-
parsed = JSON.parse(raw);
|
|
268
|
-
} catch {
|
|
269
|
-
return {};
|
|
270
|
-
}
|
|
271
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
272
|
-
return {};
|
|
273
|
-
}
|
|
274
|
-
const claims = {};
|
|
275
|
-
for (const [accountId, claim] of Object.entries(parsed)) {
|
|
276
|
-
if (isClaimShape(claim)) {
|
|
277
|
-
claims[accountId] = claim;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return claims;
|
|
281
|
-
}
|
|
282
|
-
function isProcessAlive(pid) {
|
|
283
|
-
try {
|
|
284
|
-
process.kill(pid, 0);
|
|
285
|
-
return true;
|
|
286
|
-
} catch {
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
function cleanClaims(claims, now) {
|
|
291
|
-
const cleaned = {};
|
|
292
|
-
let changed = false;
|
|
293
|
-
for (const [accountId, claim] of Object.entries(claims)) {
|
|
294
|
-
const expiredByTime = now - claim.at > CLAIM_EXPIRY_MS;
|
|
295
|
-
const zombieClaim = !isProcessAlive(claim.pid);
|
|
296
|
-
if (expiredByTime || zombieClaim) {
|
|
297
|
-
changed = true;
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
cleaned[accountId] = claim;
|
|
301
|
-
}
|
|
302
|
-
return { cleaned, changed };
|
|
303
|
-
}
|
|
304
|
-
async function writeClaimsFile(claims) {
|
|
305
|
-
const path = getClaimsPath();
|
|
306
|
-
const tempPath = `${path}.${randomBytes2(6).toString("hex")}.tmp`;
|
|
307
|
-
await fs2.mkdir(dirname2(path), { recursive: true });
|
|
308
|
-
try {
|
|
309
|
-
await fs2.writeFile(tempPath, JSON.stringify(claims, null, 2), { encoding: "utf-8", mode: 384 });
|
|
310
|
-
await fs2.rename(tempPath, path);
|
|
311
|
-
} catch (error) {
|
|
312
|
-
try {
|
|
313
|
-
await fs2.unlink(tempPath);
|
|
314
|
-
} catch {
|
|
315
|
-
}
|
|
316
|
-
throw error;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
async function readClaims() {
|
|
320
|
-
try {
|
|
321
|
-
const data = await fs2.readFile(getClaimsPath(), "utf-8");
|
|
322
|
-
const parsed = parseClaims(data);
|
|
323
|
-
const now = Date.now();
|
|
324
|
-
const { cleaned, changed } = cleanClaims(parsed, now);
|
|
325
|
-
if (changed) {
|
|
326
|
-
try {
|
|
327
|
-
await writeClaimsFile(cleaned);
|
|
328
|
-
} catch {
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return cleaned;
|
|
332
|
-
} catch {
|
|
333
|
-
return {};
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
async function writeClaim(accountId) {
|
|
337
|
-
const now = Date.now();
|
|
338
|
-
const claims = await readClaims();
|
|
339
|
-
const { cleaned } = cleanClaims(claims, now);
|
|
340
|
-
cleaned[accountId] = { pid: process.pid, at: now };
|
|
341
|
-
try {
|
|
342
|
-
await writeClaimsFile(cleaned);
|
|
343
|
-
} catch {
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
function isClaimedByOther(claims, accountId) {
|
|
347
|
-
if (!accountId) return false;
|
|
348
|
-
const claim = claims[accountId];
|
|
349
|
-
if (!claim) return false;
|
|
350
|
-
if (Date.now() - claim.at > CLAIM_EXPIRY_MS) return false;
|
|
351
|
-
if (!isProcessAlive(claim.pid)) return false;
|
|
352
|
-
return claim.pid !== process.pid;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// ../multi-account-core/src/account-manager.ts
|
|
356
|
-
var STARTUP_REFRESH_CONCURRENCY = 3;
|
|
357
|
-
var RECENT_429_COOLDOWN_MS = 3e4;
|
|
358
|
-
var HYBRID_SWITCH_MARGIN = 40;
|
|
359
|
-
function createAccountManagerForProvider(dependencies) {
|
|
360
|
-
const {
|
|
361
|
-
providerAuthId,
|
|
362
|
-
isTokenExpired: isTokenExpired2,
|
|
363
|
-
refreshToken: refreshToken2
|
|
364
|
-
} = dependencies;
|
|
365
|
-
return class AccountManager2 {
|
|
366
|
-
constructor(store) {
|
|
367
|
-
this.store = store;
|
|
368
|
-
}
|
|
369
|
-
cached = [];
|
|
370
|
-
activeAccountUuid;
|
|
371
|
-
client = null;
|
|
372
|
-
runtimeFactory = null;
|
|
373
|
-
roundRobinCursor = 0;
|
|
374
|
-
last429Map = /* @__PURE__ */ new Map();
|
|
375
|
-
static async create(store, currentAuth, client) {
|
|
376
|
-
const manager = new AccountManager2(store);
|
|
377
|
-
await manager.initialize(currentAuth, client);
|
|
378
|
-
return manager;
|
|
379
|
-
}
|
|
380
|
-
async initialize(currentAuth, client) {
|
|
381
|
-
if (client) this.client = client;
|
|
382
|
-
const storage = await this.store.load();
|
|
383
|
-
if (storage.accounts.length > 0) {
|
|
384
|
-
this.cached = storage.accounts.map((account, index) => this.toManagedAccount(account, index));
|
|
385
|
-
this.activeAccountUuid = storage.activeAccountUuid;
|
|
386
|
-
if (!this.getActiveAccount() && this.cached.length > 0) {
|
|
387
|
-
this.activeAccountUuid = this.cached[0].uuid;
|
|
388
|
-
}
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (currentAuth.refresh) {
|
|
392
|
-
const newAccount = this.createNewAccount(currentAuth, Date.now());
|
|
393
|
-
await this.store.addAccount(newAccount);
|
|
394
|
-
await this.store.setActiveUuid(newAccount.uuid);
|
|
395
|
-
this.cached = [this.toManagedAccount(newAccount, 0)];
|
|
396
|
-
this.activeAccountUuid = newAccount.uuid;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
async refresh() {
|
|
400
|
-
const storage = await this.store.load();
|
|
401
|
-
this.cached = storage.accounts.map((account, index) => this.toManagedAccount(account, index));
|
|
402
|
-
if (storage.activeAccountUuid) {
|
|
403
|
-
this.activeAccountUuid = storage.activeAccountUuid;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
toManagedAccount(storedAccount, index) {
|
|
407
|
-
return {
|
|
408
|
-
index,
|
|
409
|
-
uuid: storedAccount.uuid,
|
|
410
|
-
accountId: storedAccount.accountId,
|
|
411
|
-
label: storedAccount.label,
|
|
412
|
-
email: storedAccount.email,
|
|
413
|
-
planTier: storedAccount.planTier,
|
|
414
|
-
refreshToken: storedAccount.refreshToken,
|
|
415
|
-
accessToken: storedAccount.accessToken,
|
|
416
|
-
expiresAt: storedAccount.expiresAt,
|
|
417
|
-
addedAt: storedAccount.addedAt,
|
|
418
|
-
lastUsed: storedAccount.lastUsed,
|
|
419
|
-
enabled: storedAccount.enabled,
|
|
420
|
-
rateLimitResetAt: storedAccount.rateLimitResetAt,
|
|
421
|
-
cachedUsage: storedAccount.cachedUsage,
|
|
422
|
-
cachedUsageAt: storedAccount.cachedUsageAt,
|
|
423
|
-
consecutiveAuthFailures: storedAccount.consecutiveAuthFailures,
|
|
424
|
-
isAuthDisabled: storedAccount.isAuthDisabled,
|
|
425
|
-
authDisabledReason: storedAccount.authDisabledReason,
|
|
426
|
-
last429At: storedAccount.uuid ? this.last429Map.get(storedAccount.uuid) : void 0
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
createNewAccount(auth, now) {
|
|
430
|
-
return {
|
|
431
|
-
uuid: randomUUID(),
|
|
432
|
-
refreshToken: auth.refresh,
|
|
433
|
-
accessToken: auth.access,
|
|
434
|
-
expiresAt: auth.expires,
|
|
435
|
-
addedAt: now,
|
|
436
|
-
lastUsed: now,
|
|
437
|
-
enabled: true,
|
|
438
|
-
planTier: "",
|
|
439
|
-
consecutiveAuthFailures: 0,
|
|
440
|
-
isAuthDisabled: false
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
getAccountCount() {
|
|
444
|
-
return this.getEligibleAccounts().length;
|
|
445
|
-
}
|
|
446
|
-
getAccounts() {
|
|
447
|
-
return [...this.cached];
|
|
448
|
-
}
|
|
449
|
-
getActiveAccount() {
|
|
450
|
-
if (this.activeAccountUuid) {
|
|
451
|
-
return this.cached.find((account) => account.uuid === this.activeAccountUuid) ?? null;
|
|
452
|
-
}
|
|
453
|
-
return this.cached[0] ?? null;
|
|
454
|
-
}
|
|
455
|
-
setClient(client) {
|
|
456
|
-
this.client = client;
|
|
457
|
-
}
|
|
458
|
-
setRuntimeFactory(factory) {
|
|
459
|
-
this.runtimeFactory = factory;
|
|
460
|
-
}
|
|
461
|
-
getEligibleAccounts() {
|
|
462
|
-
return this.cached.filter((account) => account.uuid && account.enabled && !account.isAuthDisabled);
|
|
463
|
-
}
|
|
464
|
-
exceedsSoftQuota(account) {
|
|
465
|
-
const threshold = getConfig().soft_quota_threshold_percent;
|
|
466
|
-
if (threshold >= 100) return false;
|
|
467
|
-
const usage = account.cachedUsage;
|
|
468
|
-
if (!usage) return false;
|
|
469
|
-
const tiers = [usage.five_hour, usage.seven_day];
|
|
470
|
-
return tiers.some((tier) => tier != null && tier.utilization >= threshold);
|
|
471
|
-
}
|
|
472
|
-
hasAnyUsableAccount() {
|
|
473
|
-
return this.getEligibleAccounts().length > 0;
|
|
474
|
-
}
|
|
475
|
-
isRateLimited(account) {
|
|
476
|
-
if (account.rateLimitResetAt && Date.now() < account.rateLimitResetAt) {
|
|
477
|
-
return true;
|
|
478
|
-
}
|
|
479
|
-
return this.isUsageExhausted(account);
|
|
480
|
-
}
|
|
481
|
-
isUsageExhausted(account) {
|
|
482
|
-
const usage = account.cachedUsage;
|
|
483
|
-
if (!usage) return false;
|
|
484
|
-
const now = Date.now();
|
|
485
|
-
const tiers = [usage.five_hour, usage.seven_day];
|
|
486
|
-
return tiers.some(
|
|
487
|
-
(tier) => tier != null && tier.utilization >= 100 && tier.resets_at != null && Date.parse(tier.resets_at) > now
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
clearExpiredRateLimits() {
|
|
491
|
-
const now = Date.now();
|
|
492
|
-
for (const account of this.cached) {
|
|
493
|
-
if (account.rateLimitResetAt && now >= account.rateLimitResetAt) {
|
|
494
|
-
account.rateLimitResetAt = void 0;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
getMinWaitTime() {
|
|
499
|
-
const eligible = this.getEligibleAccounts();
|
|
500
|
-
const available = eligible.filter((account) => !this.isRateLimited(account));
|
|
501
|
-
if (available.length > 0) return 0;
|
|
502
|
-
const now = Date.now();
|
|
503
|
-
const waits = [];
|
|
504
|
-
for (const account of eligible) {
|
|
505
|
-
if (account.rateLimitResetAt) {
|
|
506
|
-
const ms = account.rateLimitResetAt - now;
|
|
507
|
-
if (ms > 0) waits.push(ms);
|
|
508
|
-
}
|
|
509
|
-
const usageResetMs = this.getUsageResetMs(account);
|
|
510
|
-
if (usageResetMs !== null && usageResetMs > 0) {
|
|
511
|
-
waits.push(usageResetMs);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
return waits.length > 0 ? Math.min(...waits) : 0;
|
|
515
|
-
}
|
|
516
|
-
getUsageResetMs(account) {
|
|
517
|
-
const usage = account.cachedUsage;
|
|
518
|
-
if (!usage) return null;
|
|
519
|
-
const now = Date.now();
|
|
520
|
-
const candidates = [];
|
|
521
|
-
const tiers = [usage.five_hour, usage.seven_day];
|
|
522
|
-
for (const tier of tiers) {
|
|
523
|
-
if (tier != null && tier.utilization >= 100 && tier.resets_at != null) {
|
|
524
|
-
const ms = Date.parse(tier.resets_at) - now;
|
|
525
|
-
if (ms > 0) candidates.push(ms);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
return candidates.length > 0 ? Math.min(...candidates) : null;
|
|
529
|
-
}
|
|
530
|
-
async selectAccount() {
|
|
531
|
-
await this.refresh();
|
|
532
|
-
this.clearExpiredRateLimits();
|
|
533
|
-
const eligible = this.getEligibleAccounts();
|
|
534
|
-
if (eligible.length === 0) return null;
|
|
535
|
-
const config = getConfig();
|
|
536
|
-
const claims = config.cross_process_claims ? await readClaims() : {};
|
|
537
|
-
const strategy = config.account_selection_strategy;
|
|
538
|
-
let selected;
|
|
539
|
-
switch (strategy) {
|
|
540
|
-
case "round-robin":
|
|
541
|
-
selected = this.selectRoundRobin(eligible, claims);
|
|
542
|
-
break;
|
|
543
|
-
case "hybrid":
|
|
544
|
-
selected = this.selectHybrid(eligible, claims);
|
|
545
|
-
break;
|
|
546
|
-
case "sticky":
|
|
547
|
-
default:
|
|
548
|
-
selected = this.selectSticky(eligible, claims);
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
if (selected?.uuid) {
|
|
552
|
-
this.activeAccountUuid = selected.uuid;
|
|
553
|
-
this.store.setActiveUuid(selected.uuid).catch(() => {
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
if (config.cross_process_claims && selected?.uuid) {
|
|
557
|
-
writeClaim(selected.uuid).catch(() => {
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
return selected;
|
|
561
|
-
}
|
|
562
|
-
isUsable(account) {
|
|
563
|
-
return !this.isRateLimited(account) && !this.isInRecentCooldown(account) && !this.exceedsSoftQuota(account);
|
|
564
|
-
}
|
|
565
|
-
isInRecentCooldown(account) {
|
|
566
|
-
if (!account.last429At) return false;
|
|
567
|
-
return Date.now() - account.last429At < RECENT_429_COOLDOWN_MS;
|
|
568
|
-
}
|
|
569
|
-
fallbackNotRateLimited(eligible) {
|
|
570
|
-
const account = eligible.find((candidate) => !this.isRateLimited(candidate));
|
|
571
|
-
if (account) {
|
|
572
|
-
this.activateAccount(account);
|
|
573
|
-
return account;
|
|
574
|
-
}
|
|
575
|
-
return null;
|
|
576
|
-
}
|
|
577
|
-
selectSticky(eligible, claims) {
|
|
578
|
-
const current = this.getActiveAccount();
|
|
579
|
-
if (current?.enabled && !current.isAuthDisabled && this.isUsable(current)) {
|
|
580
|
-
this.activateAccount(current);
|
|
581
|
-
return current;
|
|
582
|
-
}
|
|
583
|
-
const unclaimed = eligible.find(
|
|
584
|
-
(account) => this.isUsable(account) && !isClaimedByOther(claims, account.uuid)
|
|
585
|
-
);
|
|
586
|
-
if (unclaimed) {
|
|
587
|
-
this.activateAccount(unclaimed);
|
|
588
|
-
return unclaimed;
|
|
589
|
-
}
|
|
590
|
-
const available = eligible.find((account) => this.isUsable(account));
|
|
591
|
-
if (available) {
|
|
592
|
-
this.activateAccount(available);
|
|
593
|
-
return available;
|
|
594
|
-
}
|
|
595
|
-
return this.fallbackNotRateLimited(eligible);
|
|
596
|
-
}
|
|
597
|
-
selectRoundRobin(eligible, claims) {
|
|
598
|
-
for (let i = 0; i < eligible.length; i++) {
|
|
599
|
-
const index = (this.roundRobinCursor + i) % eligible.length;
|
|
600
|
-
const account = eligible[index];
|
|
601
|
-
if (this.isUsable(account) && !isClaimedByOther(claims, account.uuid)) {
|
|
602
|
-
this.roundRobinCursor = (index + 1) % eligible.length;
|
|
603
|
-
this.activateAccount(account);
|
|
604
|
-
return account;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
for (let i = 0; i < eligible.length; i++) {
|
|
608
|
-
const index = (this.roundRobinCursor + i) % eligible.length;
|
|
609
|
-
const account = eligible[index];
|
|
610
|
-
if (this.isUsable(account)) {
|
|
611
|
-
this.roundRobinCursor = (index + 1) % eligible.length;
|
|
612
|
-
this.activateAccount(account);
|
|
613
|
-
return account;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
return this.fallbackNotRateLimited(eligible);
|
|
617
|
-
}
|
|
618
|
-
selectHybrid(eligible, claims) {
|
|
619
|
-
const usable = eligible.filter((account) => this.isUsable(account));
|
|
620
|
-
const pool = usable.length > 0 ? usable : eligible.filter((account) => !this.isRateLimited(account));
|
|
621
|
-
if (pool.length === 0) return null;
|
|
622
|
-
const activeUuid = this.activeAccountUuid;
|
|
623
|
-
let best = pool[0];
|
|
624
|
-
let bestScore = this.calculateHybridScore(best, best.uuid === activeUuid, claims);
|
|
625
|
-
for (let i = 1; i < pool.length; i++) {
|
|
626
|
-
const account = pool[i];
|
|
627
|
-
const score = this.calculateHybridScore(account, account.uuid === activeUuid, claims);
|
|
628
|
-
if (score > bestScore) {
|
|
629
|
-
best = account;
|
|
630
|
-
bestScore = score;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
const current = pool.find((account) => account.uuid === activeUuid);
|
|
634
|
-
if (current && current !== best) {
|
|
635
|
-
const currentScore = this.calculateHybridScore(current, true, claims);
|
|
636
|
-
const bestWithoutStickiness = this.calculateHybridScore(best, false, claims);
|
|
637
|
-
if (bestWithoutStickiness <= currentScore + HYBRID_SWITCH_MARGIN) {
|
|
638
|
-
this.activateAccount(current);
|
|
639
|
-
return current;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
this.activateAccount(best);
|
|
643
|
-
return best;
|
|
644
|
-
}
|
|
645
|
-
calculateHybridScore(account, isActive, claims) {
|
|
646
|
-
const maxUtilization = Math.min(100, Math.max(0, this.getMaxUtilization(account)));
|
|
647
|
-
const usageScore = (100 - maxUtilization) / 100 * 450;
|
|
648
|
-
const maxFailures = Math.max(1, getConfig().max_consecutive_auth_failures);
|
|
649
|
-
const healthScore = Math.max(0, (maxFailures - account.consecutiveAuthFailures) / maxFailures * 250);
|
|
650
|
-
const secondsSinceUsed = (Date.now() - account.lastUsed) / 1e3;
|
|
651
|
-
const freshnessScore = Math.min(secondsSinceUsed, 900) / 900 * 60;
|
|
652
|
-
const stickinessBonus = isActive ? 120 : 0;
|
|
653
|
-
const claimPenalty = isClaimedByOther(claims, account.uuid) ? -200 : 0;
|
|
654
|
-
return usageScore + healthScore + freshnessScore + stickinessBonus + claimPenalty;
|
|
655
|
-
}
|
|
656
|
-
getMaxUtilization(account) {
|
|
657
|
-
const usage = account.cachedUsage;
|
|
658
|
-
if (!usage) return 65;
|
|
659
|
-
const tiers = [usage.five_hour, usage.seven_day];
|
|
660
|
-
const utilizations = tiers.filter((tier) => tier != null).map((tier) => tier.utilization);
|
|
661
|
-
return utilizations.length > 0 ? Math.max(...utilizations) : 65;
|
|
662
|
-
}
|
|
663
|
-
activateAccount(account) {
|
|
664
|
-
this.activeAccountUuid = account.uuid;
|
|
665
|
-
account.lastUsed = Date.now();
|
|
666
|
-
}
|
|
667
|
-
async markRateLimited(uuid, backoffMs) {
|
|
668
|
-
const effectiveBackoff = backoffMs ?? getConfig().rate_limit_min_backoff_ms;
|
|
669
|
-
this.last429Map.set(uuid, Date.now());
|
|
670
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
671
|
-
account.rateLimitResetAt = Date.now() + effectiveBackoff;
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
async markRevoked(uuid) {
|
|
675
|
-
await this.removeAccountByUuid(uuid);
|
|
676
|
-
}
|
|
677
|
-
async markSuccess(uuid) {
|
|
678
|
-
this.last429Map.delete(uuid);
|
|
679
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
680
|
-
account.rateLimitResetAt = void 0;
|
|
681
|
-
account.consecutiveAuthFailures = 0;
|
|
682
|
-
account.lastUsed = Date.now();
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
syncToOpenCode(account) {
|
|
686
|
-
if (!this.client || !account.accessToken || !account.expiresAt) return;
|
|
687
|
-
this.client.auth.set({
|
|
688
|
-
path: { id: providerAuthId },
|
|
689
|
-
body: {
|
|
690
|
-
type: "oauth",
|
|
691
|
-
refresh: account.refreshToken,
|
|
692
|
-
access: account.accessToken,
|
|
693
|
-
expires: account.expiresAt
|
|
694
|
-
}
|
|
695
|
-
}).catch(() => {
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
async clearOpenCodeAuthIfNoAccountsRemain() {
|
|
699
|
-
if (!this.client) return;
|
|
700
|
-
const storage = await this.store.load();
|
|
701
|
-
if (storage.accounts.length > 0) return;
|
|
702
|
-
await this.client.auth.set({
|
|
703
|
-
path: { id: providerAuthId },
|
|
704
|
-
body: getClearedOAuthBody()
|
|
705
|
-
}).catch(() => {
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
async removeAccountByUuid(uuid) {
|
|
709
|
-
const removed = await this.store.removeAccount(uuid);
|
|
710
|
-
if (!removed) return;
|
|
711
|
-
this.last429Map.delete(uuid);
|
|
712
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
713
|
-
await this.refresh();
|
|
714
|
-
await this.clearOpenCodeAuthIfNoAccountsRemain();
|
|
715
|
-
}
|
|
716
|
-
async markAuthFailure(uuid, result) {
|
|
717
|
-
if (!result.ok && result.permanent) {
|
|
718
|
-
await this.removeAccountByUuid(uuid);
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
await this.store.mutateStorage((storage) => {
|
|
722
|
-
const account = storage.accounts.find((entry) => entry.uuid === uuid);
|
|
723
|
-
if (!account) return;
|
|
724
|
-
account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
|
|
725
|
-
const maxFailures = getConfig().max_consecutive_auth_failures;
|
|
726
|
-
const usableCount = storage.accounts.filter(
|
|
727
|
-
(entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== uuid
|
|
728
|
-
).length;
|
|
729
|
-
if (account.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
|
|
730
|
-
account.isAuthDisabled = true;
|
|
731
|
-
account.authDisabledReason = `${maxFailures} consecutive auth failures`;
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
async applyUsageCache(uuid, usage) {
|
|
736
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
737
|
-
const now = Date.now();
|
|
738
|
-
const exhaustedTierResetTimes = [usage.five_hour, usage.seven_day].flatMap((tier) => {
|
|
739
|
-
if (tier == null || tier.utilization < 100 || tier.resets_at == null) {
|
|
740
|
-
return [];
|
|
741
|
-
}
|
|
742
|
-
return [Date.parse(tier.resets_at)];
|
|
743
|
-
}).filter((resetAt) => Number.isFinite(resetAt) && resetAt > now);
|
|
744
|
-
account.cachedUsage = usage;
|
|
745
|
-
account.cachedUsageAt = Date.now();
|
|
746
|
-
account.rateLimitResetAt = exhaustedTierResetTimes.length > 0 ? Math.min(...exhaustedTierResetTimes) : void 0;
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
async applyProfileCache(uuid, profile) {
|
|
750
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
751
|
-
account.email = profile.email ?? account.email;
|
|
752
|
-
account.planTier = profile.planTier;
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
async ensureValidToken(uuid, client) {
|
|
756
|
-
const credentials = await this.store.readCredentials(uuid);
|
|
757
|
-
if (!credentials) return { ok: false, permanent: true };
|
|
758
|
-
if (credentials.accessToken && credentials.expiresAt && !isTokenExpired2(credentials)) {
|
|
759
|
-
return {
|
|
760
|
-
ok: true,
|
|
761
|
-
patch: { accessToken: credentials.accessToken, expiresAt: credentials.expiresAt }
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
const result = await refreshToken2(credentials.refreshToken, uuid, client);
|
|
765
|
-
if (!result.ok) return result;
|
|
766
|
-
const updated = await this.store.mutateAccount(uuid, (account) => {
|
|
767
|
-
account.accessToken = result.patch.accessToken;
|
|
768
|
-
account.expiresAt = result.patch.expiresAt;
|
|
769
|
-
if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
|
|
770
|
-
if (result.patch.uuid && result.patch.uuid !== uuid) account.uuid = result.patch.uuid;
|
|
771
|
-
if (result.patch.accountId) account.accountId = result.patch.accountId;
|
|
772
|
-
if (result.patch.email) account.email = result.patch.email;
|
|
773
|
-
account.consecutiveAuthFailures = 0;
|
|
774
|
-
account.isAuthDisabled = false;
|
|
775
|
-
account.authDisabledReason = void 0;
|
|
776
|
-
});
|
|
777
|
-
if (result.patch.uuid && result.patch.uuid !== uuid && this.activeAccountUuid === uuid) {
|
|
778
|
-
this.activeAccountUuid = result.patch.uuid;
|
|
779
|
-
this.store.setActiveUuid(result.patch.uuid).catch(() => {
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
if (updated && (uuid === this.activeAccountUuid || updated.uuid === this.activeAccountUuid)) {
|
|
783
|
-
this.syncToOpenCode(updated);
|
|
784
|
-
}
|
|
785
|
-
return result;
|
|
786
|
-
}
|
|
787
|
-
async validateNonActiveTokens(client) {
|
|
788
|
-
await this.refresh();
|
|
789
|
-
const activeUuid = this.activeAccountUuid;
|
|
790
|
-
const eligible = this.cached.filter(
|
|
791
|
-
(account) => account.enabled && !account.isAuthDisabled && account.uuid && account.uuid !== activeUuid
|
|
792
|
-
);
|
|
793
|
-
for (let i = 0; i < eligible.length; i += STARTUP_REFRESH_CONCURRENCY) {
|
|
794
|
-
const batch = eligible.slice(i, i + STARTUP_REFRESH_CONCURRENCY);
|
|
795
|
-
await Promise.all(
|
|
796
|
-
batch.map(async (account) => {
|
|
797
|
-
if (!account.uuid || !isTokenExpired2(account)) return;
|
|
798
|
-
const result = await this.ensureValidToken(account.uuid, client);
|
|
799
|
-
if (!result.ok) {
|
|
800
|
-
await this.markAuthFailure(account.uuid, result);
|
|
801
|
-
}
|
|
802
|
-
})
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
async removeAccount(index) {
|
|
807
|
-
const account = this.cached[index];
|
|
808
|
-
if (!account?.uuid) return false;
|
|
809
|
-
const removed = await this.store.removeAccount(account.uuid);
|
|
810
|
-
if (removed) {
|
|
811
|
-
await this.refresh();
|
|
812
|
-
}
|
|
813
|
-
return removed;
|
|
814
|
-
}
|
|
815
|
-
async clearAllAccounts() {
|
|
816
|
-
await this.store.clear();
|
|
817
|
-
this.cached = [];
|
|
818
|
-
this.activeAccountUuid = void 0;
|
|
819
|
-
}
|
|
820
|
-
async addAccount(auth, email) {
|
|
821
|
-
if (!auth.refresh) return;
|
|
822
|
-
const existingByToken = this.cached.find((account) => account.refreshToken === auth.refresh);
|
|
823
|
-
if (existingByToken) return;
|
|
824
|
-
if (email) {
|
|
825
|
-
const existingByEmail = this.cached.find(
|
|
826
|
-
(account) => account.email && account.email === email
|
|
827
|
-
);
|
|
828
|
-
if (existingByEmail?.uuid) {
|
|
829
|
-
await this.replaceAccountCredentials(existingByEmail.uuid, auth);
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
const newAccount = this.createNewAccount(auth, Date.now());
|
|
834
|
-
if (email) newAccount.email = email;
|
|
835
|
-
await this.store.addAccount(newAccount);
|
|
836
|
-
this.activeAccountUuid = newAccount.uuid;
|
|
837
|
-
await this.store.setActiveUuid(newAccount.uuid);
|
|
838
|
-
await this.refresh();
|
|
839
|
-
}
|
|
840
|
-
async toggleEnabled(uuid) {
|
|
841
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
842
|
-
account.enabled = !(account.enabled ?? true);
|
|
843
|
-
if (account.enabled) {
|
|
844
|
-
account.isAuthDisabled = false;
|
|
845
|
-
account.authDisabledReason = void 0;
|
|
846
|
-
account.consecutiveAuthFailures = 0;
|
|
847
|
-
}
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
async replaceAccountCredentials(uuid, auth) {
|
|
851
|
-
const updated = await this.store.mutateAccount(uuid, (account) => {
|
|
852
|
-
account.refreshToken = auth.refresh;
|
|
853
|
-
account.accessToken = auth.access;
|
|
854
|
-
account.expiresAt = auth.expires;
|
|
855
|
-
account.lastUsed = Date.now();
|
|
856
|
-
account.enabled = true;
|
|
857
|
-
account.isAuthDisabled = false;
|
|
858
|
-
account.authDisabledReason = void 0;
|
|
859
|
-
account.consecutiveAuthFailures = 0;
|
|
860
|
-
account.rateLimitResetAt = void 0;
|
|
861
|
-
});
|
|
862
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
863
|
-
if (updated && uuid === this.activeAccountUuid) {
|
|
864
|
-
this.syncToOpenCode(updated);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
async retryAuth(uuid, client) {
|
|
868
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
869
|
-
account.consecutiveAuthFailures = 0;
|
|
870
|
-
account.isAuthDisabled = false;
|
|
871
|
-
account.authDisabledReason = void 0;
|
|
872
|
-
});
|
|
873
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
874
|
-
const credentials = await this.store.readCredentials(uuid);
|
|
875
|
-
if (!credentials) return { ok: false, permanent: true };
|
|
876
|
-
const result = await refreshToken2(credentials.refreshToken, uuid, client);
|
|
877
|
-
if (result.ok) {
|
|
878
|
-
const updated = await this.store.mutateAccount(uuid, (account) => {
|
|
879
|
-
account.accessToken = result.patch.accessToken;
|
|
880
|
-
account.expiresAt = result.patch.expiresAt;
|
|
881
|
-
if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
|
|
882
|
-
if (result.patch.uuid) account.uuid = result.patch.uuid;
|
|
883
|
-
if (result.patch.accountId) account.accountId = result.patch.accountId;
|
|
884
|
-
if (result.patch.email) account.email = result.patch.email;
|
|
885
|
-
account.enabled = true;
|
|
886
|
-
account.consecutiveAuthFailures = 0;
|
|
887
|
-
});
|
|
888
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
889
|
-
if (result.patch.uuid) {
|
|
890
|
-
this.runtimeFactory?.invalidate(result.patch.uuid);
|
|
891
|
-
}
|
|
892
|
-
const nextUuid = result.patch.uuid ?? uuid;
|
|
893
|
-
if (this.activeAccountUuid === uuid && result.patch.uuid && result.patch.uuid !== uuid) {
|
|
894
|
-
this.activeAccountUuid = result.patch.uuid;
|
|
895
|
-
await this.store.setActiveUuid(result.patch.uuid);
|
|
896
|
-
}
|
|
897
|
-
if (updated && (uuid === this.activeAccountUuid || nextUuid === this.activeAccountUuid)) {
|
|
898
|
-
const freshCredentials = await this.store.readCredentials(nextUuid);
|
|
899
|
-
if (freshCredentials) {
|
|
900
|
-
this.syncToOpenCode({
|
|
901
|
-
refreshToken: freshCredentials.refreshToken,
|
|
902
|
-
accessToken: freshCredentials.accessToken,
|
|
903
|
-
expiresAt: freshCredentials.expiresAt
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
} else {
|
|
908
|
-
await this.markAuthFailure(uuid, result);
|
|
909
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
910
|
-
}
|
|
911
|
-
return result;
|
|
912
|
-
}
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// ../multi-account-core/src/account-store.ts
|
|
917
|
-
import { promises as fs4 } from "node:fs";
|
|
918
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
919
|
-
import { dirname as dirname4, join as join5 } from "node:path";
|
|
920
|
-
import lockfile from "proper-lockfile";
|
|
921
|
-
import * as v4 from "valibot";
|
|
922
|
-
|
|
923
|
-
// ../multi-account-core/src/storage.ts
|
|
924
|
-
import { promises as fs3 } from "node:fs";
|
|
925
|
-
import { dirname as dirname3, join as join4 } from "node:path";
|
|
926
|
-
import * as v3 from "valibot";
|
|
927
|
-
|
|
928
|
-
// ../multi-account-core/src/constants.ts
|
|
929
|
-
var DEFAULT_ACCOUNTS_FILENAME = "multiauth-accounts.json";
|
|
930
|
-
var ACCOUNTS_FILENAME = DEFAULT_ACCOUNTS_FILENAME;
|
|
931
|
-
function setAccountsFilename(filename) {
|
|
932
|
-
if (!filename) {
|
|
933
|
-
ACCOUNTS_FILENAME = DEFAULT_ACCOUNTS_FILENAME;
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
ACCOUNTS_FILENAME = filename;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// ../multi-account-core/src/storage.ts
|
|
940
|
-
function getStoragePath() {
|
|
941
|
-
return join4(getConfigDir2(), ACCOUNTS_FILENAME);
|
|
942
|
-
}
|
|
943
|
-
async function backupCorruptFile(targetPath, content) {
|
|
944
|
-
const backupPath = `${targetPath}.corrupt.${Date.now()}.bak`;
|
|
945
|
-
await fs3.mkdir(dirname3(backupPath), { recursive: true });
|
|
946
|
-
await fs3.writeFile(backupPath, content, "utf-8");
|
|
947
|
-
}
|
|
948
|
-
async function readStorageFromDisk(targetPath, backupOnCorrupt) {
|
|
949
|
-
let content;
|
|
950
|
-
try {
|
|
951
|
-
content = await fs3.readFile(targetPath, "utf-8");
|
|
952
|
-
} catch (error) {
|
|
953
|
-
if (getErrorCode(error) === "ENOENT") {
|
|
954
|
-
return null;
|
|
955
|
-
}
|
|
956
|
-
throw error;
|
|
957
|
-
}
|
|
958
|
-
let parsed;
|
|
959
|
-
try {
|
|
960
|
-
parsed = JSON.parse(content);
|
|
961
|
-
} catch {
|
|
962
|
-
if (backupOnCorrupt) {
|
|
963
|
-
try {
|
|
964
|
-
await backupCorruptFile(targetPath, content);
|
|
965
|
-
} catch {
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
return null;
|
|
969
|
-
}
|
|
970
|
-
const validation = v3.safeParse(AccountStorageSchema, parsed);
|
|
971
|
-
if (!validation.success) {
|
|
972
|
-
if (backupOnCorrupt) {
|
|
973
|
-
try {
|
|
974
|
-
await backupCorruptFile(targetPath, content);
|
|
975
|
-
} catch {
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
return null;
|
|
979
|
-
}
|
|
980
|
-
return validation.output;
|
|
981
|
-
}
|
|
982
|
-
function deduplicateAccounts(accounts) {
|
|
983
|
-
const deduplicated = [];
|
|
984
|
-
const indexByUuid = /* @__PURE__ */ new Map();
|
|
985
|
-
for (const account of accounts) {
|
|
986
|
-
if (!account.uuid) {
|
|
987
|
-
deduplicated.push(account);
|
|
988
|
-
continue;
|
|
989
|
-
}
|
|
990
|
-
const existingIndex = indexByUuid.get(account.uuid);
|
|
991
|
-
if (existingIndex === void 0) {
|
|
992
|
-
indexByUuid.set(account.uuid, deduplicated.length);
|
|
993
|
-
deduplicated.push(account);
|
|
994
|
-
continue;
|
|
995
|
-
}
|
|
996
|
-
const existingAccount = deduplicated[existingIndex];
|
|
997
|
-
if (!existingAccount || account.lastUsed >= existingAccount.lastUsed) {
|
|
998
|
-
deduplicated[existingIndex] = account;
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
return deduplicated;
|
|
1002
|
-
}
|
|
1003
|
-
async function loadAccounts() {
|
|
1004
|
-
const storagePath = getStoragePath();
|
|
1005
|
-
const storage = await readStorageFromDisk(storagePath, true);
|
|
1006
|
-
if (!storage) {
|
|
1007
|
-
return null;
|
|
1008
|
-
}
|
|
1009
|
-
return {
|
|
1010
|
-
...storage,
|
|
1011
|
-
accounts: deduplicateAccounts(storage.accounts || [])
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
// ../multi-account-core/src/account-store.ts
|
|
1016
|
-
var FILE_MODE = 384;
|
|
1017
|
-
var LOCK_OPTIONS = {
|
|
1018
|
-
stale: 1e4,
|
|
1019
|
-
retries: { retries: 10, minTimeout: 50, maxTimeout: 2e3, factor: 2 }
|
|
1020
|
-
};
|
|
1021
|
-
function getStoragePath2() {
|
|
1022
|
-
return join5(getConfigDir2(), ACCOUNTS_FILENAME);
|
|
1023
|
-
}
|
|
1024
|
-
function createEmptyStorage() {
|
|
1025
|
-
return { version: 1, accounts: [] };
|
|
1026
|
-
}
|
|
1027
|
-
function buildTempPath(targetPath) {
|
|
1028
|
-
return `${targetPath}.${randomBytes3(8).toString("hex")}.tmp`;
|
|
1029
|
-
}
|
|
1030
|
-
async function writeAtomicText(targetPath, content) {
|
|
1031
|
-
await fs4.mkdir(dirname4(targetPath), { recursive: true });
|
|
1032
|
-
const tempPath = buildTempPath(targetPath);
|
|
1033
|
-
try {
|
|
1034
|
-
await fs4.writeFile(tempPath, content, { encoding: "utf-8", mode: FILE_MODE });
|
|
1035
|
-
await fs4.chmod(tempPath, FILE_MODE);
|
|
1036
|
-
await fs4.rename(tempPath, targetPath);
|
|
1037
|
-
await fs4.chmod(targetPath, FILE_MODE);
|
|
1038
|
-
} catch (error) {
|
|
1039
|
-
try {
|
|
1040
|
-
await fs4.unlink(tempPath);
|
|
1041
|
-
} catch {
|
|
1042
|
-
}
|
|
1043
|
-
throw error;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
async function writeStorageAtomic(targetPath, storage) {
|
|
1047
|
-
const validation = v4.safeParse(AccountStorageSchema, storage);
|
|
1048
|
-
if (!validation.success) {
|
|
1049
|
-
throw new Error("Invalid account storage payload");
|
|
1050
|
-
}
|
|
1051
|
-
await writeAtomicText(targetPath, `${JSON.stringify(validation.output, null, 2)}
|
|
1052
|
-
`);
|
|
1053
|
-
}
|
|
1054
|
-
async function ensureStorageFileExists(targetPath) {
|
|
1055
|
-
await fs4.mkdir(dirname4(targetPath), { recursive: true });
|
|
1056
|
-
const emptyContent = `${JSON.stringify(createEmptyStorage(), null, 2)}
|
|
1057
|
-
`;
|
|
1058
|
-
try {
|
|
1059
|
-
await fs4.writeFile(targetPath, emptyContent, { flag: "wx", mode: FILE_MODE });
|
|
1060
|
-
} catch (error) {
|
|
1061
|
-
if (getErrorCode(error) !== "EEXIST") throw error;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
async function withFileLock(fn) {
|
|
1065
|
-
const storagePath = getStoragePath2();
|
|
1066
|
-
await ensureStorageFileExists(storagePath);
|
|
1067
|
-
let release = null;
|
|
1068
|
-
try {
|
|
1069
|
-
release = await lockfile.lock(storagePath, LOCK_OPTIONS);
|
|
1070
|
-
return await fn(storagePath);
|
|
1071
|
-
} finally {
|
|
1072
|
-
if (release) {
|
|
1073
|
-
try {
|
|
1074
|
-
await release();
|
|
1075
|
-
} catch {
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
var AccountStore = class {
|
|
1081
|
-
async load() {
|
|
1082
|
-
const storage = await loadAccounts();
|
|
1083
|
-
return storage ?? createEmptyStorage();
|
|
1084
|
-
}
|
|
1085
|
-
async readCredentials(uuid) {
|
|
1086
|
-
const storagePath = getStoragePath2();
|
|
1087
|
-
const storage = await readStorageFromDisk(storagePath, false);
|
|
1088
|
-
if (!storage) return null;
|
|
1089
|
-
const account = storage.accounts.find((a) => a.uuid === uuid);
|
|
1090
|
-
if (!account) return null;
|
|
1091
|
-
return {
|
|
1092
|
-
refreshToken: account.refreshToken,
|
|
1093
|
-
accessToken: account.accessToken,
|
|
1094
|
-
expiresAt: account.expiresAt,
|
|
1095
|
-
accountId: account.accountId
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
|
-
async mutateAccount(uuid, fn) {
|
|
1099
|
-
return await withFileLock(async (storagePath) => {
|
|
1100
|
-
const current = await readStorageFromDisk(storagePath, false);
|
|
1101
|
-
if (!current) return null;
|
|
1102
|
-
const account = current.accounts.find((a) => a.uuid === uuid);
|
|
1103
|
-
if (!account) return null;
|
|
1104
|
-
fn(account);
|
|
1105
|
-
await writeStorageAtomic(storagePath, current);
|
|
1106
|
-
return { ...account };
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
async mutateStorage(fn) {
|
|
1110
|
-
await withFileLock(async (storagePath) => {
|
|
1111
|
-
const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
|
|
1112
|
-
fn(current);
|
|
1113
|
-
await writeStorageAtomic(storagePath, current);
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
async addAccount(account) {
|
|
1117
|
-
await withFileLock(async (storagePath) => {
|
|
1118
|
-
const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
|
|
1119
|
-
const exists = current.accounts.some(
|
|
1120
|
-
(a) => a.uuid === account.uuid || a.refreshToken === account.refreshToken
|
|
1121
|
-
);
|
|
1122
|
-
if (exists) return;
|
|
1123
|
-
current.accounts.push(account);
|
|
1124
|
-
await writeStorageAtomic(storagePath, current);
|
|
1125
|
-
});
|
|
1126
|
-
}
|
|
1127
|
-
async removeAccount(uuid) {
|
|
1128
|
-
return await withFileLock(async (storagePath) => {
|
|
1129
|
-
const current = await readStorageFromDisk(storagePath, false);
|
|
1130
|
-
if (!current) return false;
|
|
1131
|
-
const initialLength = current.accounts.length;
|
|
1132
|
-
current.accounts = current.accounts.filter((a) => a.uuid !== uuid);
|
|
1133
|
-
if (current.accounts.length === initialLength) return false;
|
|
1134
|
-
if (current.activeAccountUuid === uuid) {
|
|
1135
|
-
current.activeAccountUuid = current.accounts[0]?.uuid;
|
|
1136
|
-
}
|
|
1137
|
-
await writeStorageAtomic(storagePath, current);
|
|
1138
|
-
return true;
|
|
1139
|
-
});
|
|
1140
|
-
}
|
|
1141
|
-
async setActiveUuid(uuid) {
|
|
1142
|
-
await this.mutateStorage((storage) => {
|
|
1143
|
-
storage.activeAccountUuid = uuid;
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
|
-
async clear() {
|
|
1147
|
-
await withFileLock(async (storagePath) => {
|
|
1148
|
-
await writeStorageAtomic(storagePath, createEmptyStorage());
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
};
|
|
1152
|
-
|
|
1153
|
-
// ../multi-account-core/src/executor.ts
|
|
1154
|
-
var MIN_MAX_RETRIES = 6;
|
|
1155
|
-
var RETRIES_PER_ACCOUNT = 3;
|
|
1156
|
-
var MAX_SERVER_RETRIES_PER_ATTEMPT = 2;
|
|
1157
|
-
var MAX_RESOLVE_ATTEMPTS = 10;
|
|
1158
|
-
var SERVER_RETRY_BASE_MS = 1e3;
|
|
1159
|
-
var SERVER_RETRY_MAX_MS = 4e3;
|
|
1160
|
-
function isAbortError(error) {
|
|
1161
|
-
return error instanceof Error && error.name === "AbortError";
|
|
1162
|
-
}
|
|
1163
|
-
function createExecutorForProvider(providerName, dependencies) {
|
|
1164
|
-
const {
|
|
1165
|
-
handleRateLimitResponse: handleRateLimitResponse2,
|
|
1166
|
-
formatWaitTime: formatWaitTime2,
|
|
1167
|
-
sleep: sleep2,
|
|
1168
|
-
showToast: showToast2,
|
|
1169
|
-
getAccountLabel: getAccountLabel2
|
|
1170
|
-
} = dependencies;
|
|
1171
|
-
async function executeWithAccountRotation2(manager, runtimeFactory, client, input, init) {
|
|
1172
|
-
const maxRetries = Math.max(MIN_MAX_RETRIES, manager.getAccountCount() * RETRIES_PER_ACCOUNT);
|
|
1173
|
-
let previousAccountUuid;
|
|
1174
|
-
async function retryServerErrors(account, runtime) {
|
|
1175
|
-
for (let attempt = 0; attempt < MAX_SERVER_RETRIES_PER_ATTEMPT; attempt++) {
|
|
1176
|
-
const backoff = Math.min(SERVER_RETRY_BASE_MS * 2 ** attempt, SERVER_RETRY_MAX_MS);
|
|
1177
|
-
const jitteredBackoff = backoff * (0.5 + Math.random() * 0.5);
|
|
1178
|
-
await sleep2(jitteredBackoff);
|
|
1179
|
-
let retryResponse;
|
|
1180
|
-
try {
|
|
1181
|
-
retryResponse = await runtime.fetch(input, init);
|
|
1182
|
-
} catch (error) {
|
|
1183
|
-
if (isAbortError(error)) throw error;
|
|
1184
|
-
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1185
|
-
return null;
|
|
1186
|
-
}
|
|
1187
|
-
void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
|
|
1188
|
-
return null;
|
|
1189
|
-
}
|
|
1190
|
-
if (retryResponse.status < 500) return retryResponse;
|
|
1191
|
-
}
|
|
1192
|
-
return null;
|
|
1193
|
-
}
|
|
1194
|
-
const dispatchResponseStatus = async (account, accountUuid, runtime, response, allow401Retry, from401RefreshRetry) => {
|
|
1195
|
-
if (response.status >= 500) {
|
|
1196
|
-
const recovered = await retryServerErrors(account, runtime);
|
|
1197
|
-
if (recovered === null) {
|
|
1198
|
-
return { type: "retryOuter" };
|
|
1199
|
-
}
|
|
1200
|
-
response = recovered;
|
|
1201
|
-
}
|
|
1202
|
-
if (response.status === 401) {
|
|
1203
|
-
if (allow401Retry) {
|
|
1204
|
-
runtimeFactory.invalidate(accountUuid);
|
|
1205
|
-
try {
|
|
1206
|
-
const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
|
|
1207
|
-
const retryResponse = await retryRuntime.fetch(input, init);
|
|
1208
|
-
return dispatchResponseStatus(account, accountUuid, retryRuntime, retryResponse, false, true);
|
|
1209
|
-
} catch (error) {
|
|
1210
|
-
if (isAbortError(error)) throw error;
|
|
1211
|
-
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1212
|
-
return { type: "retryOuter" };
|
|
1213
|
-
}
|
|
1214
|
-
return { type: "retryOuter" };
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
await manager.markAuthFailure(accountUuid, { ok: false, permanent: false });
|
|
1218
|
-
await manager.refresh();
|
|
1219
|
-
if (!manager.hasAnyUsableAccount()) {
|
|
1220
|
-
void showToast2(client, "All accounts have auth failures.", "error");
|
|
1221
|
-
throw new Error(
|
|
1222
|
-
`All ${providerName} accounts have authentication failures. Re-authenticate with \`opencode auth login\`.`
|
|
1223
|
-
);
|
|
1224
|
-
}
|
|
1225
|
-
void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
|
|
1226
|
-
return { type: "retryOuter" };
|
|
1227
|
-
}
|
|
1228
|
-
if (response.status === 403) {
|
|
1229
|
-
const revoked = await isRevokedTokenResponse(response);
|
|
1230
|
-
if (revoked) {
|
|
1231
|
-
await manager.markRevoked(accountUuid);
|
|
1232
|
-
await manager.refresh();
|
|
1233
|
-
void showToast2(
|
|
1234
|
-
client,
|
|
1235
|
-
`${getAccountLabel2(account)} disabled: OAuth token revoked.`,
|
|
1236
|
-
"error"
|
|
1237
|
-
);
|
|
1238
|
-
if (!manager.hasAnyUsableAccount()) {
|
|
1239
|
-
throw new Error(
|
|
1240
|
-
`All ${providerName} accounts have been revoked or disabled. Re-authenticate with \`opencode auth login\`.`
|
|
1241
|
-
);
|
|
1242
|
-
}
|
|
1243
|
-
return { type: "retryOuter" };
|
|
1244
|
-
}
|
|
1245
|
-
if (from401RefreshRetry) {
|
|
1246
|
-
return { type: "handled", response };
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
if (response.status === 429) {
|
|
1250
|
-
await handleRateLimitResponse2(manager, client, account, response);
|
|
1251
|
-
return { type: "handled" };
|
|
1252
|
-
}
|
|
1253
|
-
return { type: "success", response };
|
|
1254
|
-
};
|
|
1255
|
-
for (let retries = 1; retries <= maxRetries; retries++) {
|
|
1256
|
-
await manager.refresh();
|
|
1257
|
-
const account = await resolveAccount(manager, client);
|
|
1258
|
-
const accountUuid = account.uuid;
|
|
1259
|
-
if (!accountUuid) continue;
|
|
1260
|
-
if (previousAccountUuid && accountUuid !== previousAccountUuid && manager.getAccountCount() > 1) {
|
|
1261
|
-
void showToast2(client, `Switched to ${getAccountLabel2(account)}`, "info");
|
|
1262
|
-
}
|
|
1263
|
-
previousAccountUuid = accountUuid;
|
|
1264
|
-
let runtime;
|
|
1265
|
-
let response;
|
|
1266
|
-
try {
|
|
1267
|
-
runtime = await runtimeFactory.getRuntime(accountUuid);
|
|
1268
|
-
response = await runtime.fetch(input, init);
|
|
1269
|
-
} catch (error) {
|
|
1270
|
-
if (isAbortError(error)) throw error;
|
|
1271
|
-
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1272
|
-
continue;
|
|
1273
|
-
}
|
|
1274
|
-
void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
|
|
1275
|
-
continue;
|
|
1276
|
-
}
|
|
1277
|
-
const transition = await dispatchResponseStatus(account, accountUuid, runtime, response, true, false);
|
|
1278
|
-
if (transition.type === "retryOuter" || transition.type === "handled") {
|
|
1279
|
-
if (transition.type === "handled" && transition.response) {
|
|
1280
|
-
return transition.response;
|
|
1281
|
-
}
|
|
1282
|
-
continue;
|
|
1283
|
-
}
|
|
1284
|
-
await manager.markSuccess(accountUuid);
|
|
1285
|
-
return transition.response;
|
|
1286
|
-
}
|
|
1287
|
-
throw new Error(
|
|
1288
|
-
`Exhausted ${maxRetries} retries across all accounts. All attempts failed due to auth errors, rate limits, or token issues.`
|
|
1289
|
-
);
|
|
1290
|
-
}
|
|
1291
|
-
async function handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error) {
|
|
1292
|
-
if (!isTokenRefreshError(error)) return false;
|
|
1293
|
-
if (!account.uuid) return false;
|
|
1294
|
-
const accountUuid = account.uuid;
|
|
1295
|
-
runtimeFactory.invalidate(accountUuid);
|
|
1296
|
-
await manager.markAuthFailure(accountUuid, {
|
|
1297
|
-
ok: false,
|
|
1298
|
-
permanent: error.permanent
|
|
1299
|
-
});
|
|
1300
|
-
await manager.refresh();
|
|
1301
|
-
if (!manager.hasAnyUsableAccount()) {
|
|
1302
|
-
void showToast2(client, "All accounts have auth failures.", "error");
|
|
1303
|
-
throw new Error(
|
|
1304
|
-
`All ${providerName} accounts have authentication failures. Re-authenticate with \`opencode auth login\`.`
|
|
1305
|
-
);
|
|
1306
|
-
}
|
|
1307
|
-
void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
|
|
1308
|
-
return true;
|
|
1309
|
-
}
|
|
1310
|
-
async function resolveAccount(manager, client) {
|
|
1311
|
-
let attempts = 0;
|
|
1312
|
-
while (true) {
|
|
1313
|
-
if (++attempts > MAX_RESOLVE_ATTEMPTS) {
|
|
1314
|
-
throw new Error(
|
|
1315
|
-
`Failed to resolve an available account after ${MAX_RESOLVE_ATTEMPTS} attempts. All accounts may be rate-limited or disabled.`
|
|
1316
|
-
);
|
|
1317
|
-
}
|
|
1318
|
-
const account = await manager.selectAccount();
|
|
1319
|
-
if (account) return account;
|
|
1320
|
-
if (!manager.hasAnyUsableAccount()) {
|
|
1321
|
-
throw new Error(
|
|
1322
|
-
`All ${providerName} accounts are disabled. Re-authenticate with \`opencode auth login\`.`
|
|
1323
|
-
);
|
|
1324
|
-
}
|
|
1325
|
-
const waitMs = manager.getMinWaitTime();
|
|
1326
|
-
if (waitMs <= 0) {
|
|
1327
|
-
throw new Error(
|
|
1328
|
-
`All ${providerName} accounts are rate-limited. Add more accounts with \`opencode auth login\` or wait.`
|
|
1329
|
-
);
|
|
1330
|
-
}
|
|
1331
|
-
await showToast2(
|
|
1332
|
-
client,
|
|
1333
|
-
`All ${manager.getAccountCount()} account(s) rate-limited. Waiting ${formatWaitTime2(waitMs)}...`,
|
|
1334
|
-
"warning"
|
|
1335
|
-
);
|
|
1336
|
-
await sleep2(waitMs);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
return {
|
|
1340
|
-
executeWithAccountRotation: executeWithAccountRotation2
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1343
|
-
async function isRevokedTokenResponse(response) {
|
|
1344
|
-
try {
|
|
1345
|
-
const cloned = response.clone();
|
|
1346
|
-
const body = await cloned.text();
|
|
1347
|
-
return body.includes("revoked");
|
|
1348
|
-
} catch {
|
|
1349
|
-
return false;
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
// ../multi-account-core/src/proactive-refresh.ts
|
|
1354
|
-
var INITIAL_DELAY_MS = 5e3;
|
|
1355
|
-
function createProactiveRefreshQueueForProvider(dependencies) {
|
|
1356
|
-
const {
|
|
1357
|
-
providerAuthId,
|
|
1358
|
-
getConfig: getConfig2,
|
|
1359
|
-
refreshToken: refreshToken2,
|
|
1360
|
-
isTokenExpired: isTokenExpired2,
|
|
1361
|
-
debugLog: debugLog2
|
|
1362
|
-
} = dependencies;
|
|
1363
|
-
return class ProactiveRefreshQueue {
|
|
1364
|
-
constructor(client, store, onInvalidate) {
|
|
1365
|
-
this.client = client;
|
|
1366
|
-
this.store = store;
|
|
1367
|
-
this.onInvalidate = onInvalidate;
|
|
1368
|
-
}
|
|
1369
|
-
timeoutHandle = null;
|
|
1370
|
-
runToken = 0;
|
|
1371
|
-
inFlight = null;
|
|
1372
|
-
start() {
|
|
1373
|
-
const config = getConfig2();
|
|
1374
|
-
if (!config.proactive_refresh) return;
|
|
1375
|
-
this.runToken++;
|
|
1376
|
-
if (this.timeoutHandle) {
|
|
1377
|
-
clearTimeout(this.timeoutHandle);
|
|
1378
|
-
this.timeoutHandle = null;
|
|
1379
|
-
}
|
|
1380
|
-
this.scheduleNext(this.runToken, INITIAL_DELAY_MS);
|
|
1381
|
-
debugLog2(this.client, "Proactive refresh started", {
|
|
1382
|
-
intervalSeconds: config.proactive_refresh_interval_seconds,
|
|
1383
|
-
bufferSeconds: config.proactive_refresh_buffer_seconds
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
|
-
async stop() {
|
|
1387
|
-
this.runToken++;
|
|
1388
|
-
if (this.timeoutHandle) {
|
|
1389
|
-
clearTimeout(this.timeoutHandle);
|
|
1390
|
-
this.timeoutHandle = null;
|
|
1391
|
-
}
|
|
1392
|
-
if (this.inFlight) {
|
|
1393
|
-
await this.inFlight;
|
|
1394
|
-
this.inFlight = null;
|
|
1395
|
-
}
|
|
1396
|
-
debugLog2(this.client, "Proactive refresh stopped");
|
|
1397
|
-
}
|
|
1398
|
-
scheduleNext(token, delayMs) {
|
|
1399
|
-
this.timeoutHandle = setTimeout(() => {
|
|
1400
|
-
if (token !== this.runToken) return;
|
|
1401
|
-
this.inFlight = this.runCheck(token).finally(() => {
|
|
1402
|
-
this.inFlight = null;
|
|
1403
|
-
});
|
|
1404
|
-
}, delayMs);
|
|
1405
|
-
}
|
|
1406
|
-
needsProactiveRefresh(account) {
|
|
1407
|
-
if (!account.accessToken || !account.expiresAt) return false;
|
|
1408
|
-
if (isTokenExpired2(account)) return false;
|
|
1409
|
-
const bufferMs = getConfig2().proactive_refresh_buffer_seconds * 1e3;
|
|
1410
|
-
return account.expiresAt <= Date.now() + bufferMs;
|
|
1411
|
-
}
|
|
1412
|
-
async runCheck(token) {
|
|
1413
|
-
try {
|
|
1414
|
-
const stored = await this.store.load();
|
|
1415
|
-
if (token !== this.runToken) return;
|
|
1416
|
-
const candidates = stored.accounts.filter(
|
|
1417
|
-
(a) => a.enabled !== false && !a.isAuthDisabled && a.uuid && this.needsProactiveRefresh(a)
|
|
1418
|
-
);
|
|
1419
|
-
if (candidates.length === 0) return;
|
|
1420
|
-
debugLog2(this.client, `Proactive refresh: ${candidates.length} account(s) approaching expiry`);
|
|
1421
|
-
for (const account of candidates) {
|
|
1422
|
-
if (token !== this.runToken) return;
|
|
1423
|
-
const credentials = await this.store.readCredentials(account.uuid);
|
|
1424
|
-
if (!credentials || !this.needsProactiveRefresh(credentials)) continue;
|
|
1425
|
-
const result = await refreshToken2(credentials.refreshToken, account.uuid, this.client);
|
|
1426
|
-
if (result.ok) {
|
|
1427
|
-
await this.store.mutateAccount(account.uuid, (target) => {
|
|
1428
|
-
target.accessToken = result.patch.accessToken;
|
|
1429
|
-
target.expiresAt = result.patch.expiresAt;
|
|
1430
|
-
if (result.patch.refreshToken) target.refreshToken = result.patch.refreshToken;
|
|
1431
|
-
if (result.patch.uuid) target.uuid = result.patch.uuid;
|
|
1432
|
-
if (result.patch.email) target.email = result.patch.email;
|
|
1433
|
-
if (result.patch.accountId) target.accountId = result.patch.accountId;
|
|
1434
|
-
target.consecutiveAuthFailures = 0;
|
|
1435
|
-
target.isAuthDisabled = false;
|
|
1436
|
-
target.authDisabledReason = void 0;
|
|
1437
|
-
});
|
|
1438
|
-
this.onInvalidate?.(account.uuid);
|
|
1439
|
-
} else {
|
|
1440
|
-
await this.persistFailure(account, result.permanent);
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
} catch (error) {
|
|
1444
|
-
debugLog2(this.client, `Proactive refresh check error: ${error}`);
|
|
1445
|
-
} finally {
|
|
1446
|
-
if (token === this.runToken) {
|
|
1447
|
-
const intervalMs = getConfig2().proactive_refresh_interval_seconds * 1e3;
|
|
1448
|
-
this.scheduleNext(token, intervalMs);
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
async persistFailure(account, permanent) {
|
|
1453
|
-
try {
|
|
1454
|
-
const accountUuid = account.uuid;
|
|
1455
|
-
if (!accountUuid) return;
|
|
1456
|
-
if (permanent) {
|
|
1457
|
-
const removed = await this.store.removeAccount(accountUuid);
|
|
1458
|
-
if (!removed) return;
|
|
1459
|
-
this.onInvalidate?.(accountUuid);
|
|
1460
|
-
await this.clearOpenCodeAuthIfNoAccountsRemain();
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
await this.store.mutateStorage((storage) => {
|
|
1464
|
-
const target = storage.accounts.find((entry) => entry.uuid === accountUuid);
|
|
1465
|
-
if (!target) return;
|
|
1466
|
-
target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
|
|
1467
|
-
const maxFailures = getConfig2().max_consecutive_auth_failures;
|
|
1468
|
-
const usableCount = storage.accounts.filter(
|
|
1469
|
-
(entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== accountUuid
|
|
1470
|
-
).length;
|
|
1471
|
-
if (target.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
|
|
1472
|
-
target.isAuthDisabled = true;
|
|
1473
|
-
target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
|
|
1474
|
-
}
|
|
1475
|
-
});
|
|
1476
|
-
} catch {
|
|
1477
|
-
debugLog2(this.client, `Failed to persist auth failure for ${account.uuid}`);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
async clearOpenCodeAuthIfNoAccountsRemain() {
|
|
1481
|
-
const storage = await this.store.load();
|
|
1482
|
-
if (storage.accounts.length > 0) return;
|
|
1483
|
-
await this.client.auth.set({
|
|
1484
|
-
path: { id: providerAuthId },
|
|
1485
|
-
body: getClearedOAuthBody()
|
|
1486
|
-
}).catch(() => {
|
|
1487
|
-
});
|
|
1488
|
-
}
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
// ../multi-account-core/src/rate-limit.ts
|
|
1493
|
-
var USAGE_FETCH_COOLDOWN_MS = 3e4;
|
|
1494
|
-
function createRateLimitHandlers(dependencies) {
|
|
1495
|
-
const {
|
|
1496
|
-
fetchUsage: fetchUsage2,
|
|
1497
|
-
getConfig: getConfig2,
|
|
1498
|
-
formatWaitTime: formatWaitTime2,
|
|
1499
|
-
getAccountLabel: getAccountLabel2,
|
|
1500
|
-
showToast: showToast2
|
|
1501
|
-
} = dependencies;
|
|
1502
|
-
function retryAfterMsFromResponse2(response) {
|
|
1503
|
-
const retryAfterMs = response.headers.get("retry-after-ms");
|
|
1504
|
-
if (retryAfterMs) {
|
|
1505
|
-
const parsed = parseInt(retryAfterMs, 10);
|
|
1506
|
-
if (!isNaN(parsed) && parsed > 0) return parsed;
|
|
1507
|
-
}
|
|
1508
|
-
const retryAfter = response.headers.get("retry-after");
|
|
1509
|
-
if (retryAfter) {
|
|
1510
|
-
const parsed = parseInt(retryAfter, 10);
|
|
1511
|
-
if (!isNaN(parsed) && parsed > 0) return parsed * 1e3;
|
|
1512
|
-
}
|
|
1513
|
-
return getConfig2().default_retry_after_ms;
|
|
1514
|
-
}
|
|
1515
|
-
function getResetMsFromUsage2(account) {
|
|
1516
|
-
const usage = account.cachedUsage;
|
|
1517
|
-
if (!usage) return null;
|
|
1518
|
-
const now = Date.now();
|
|
1519
|
-
const candidates = [];
|
|
1520
|
-
if (usage.five_hour?.resets_at) {
|
|
1521
|
-
const ms = Date.parse(usage.five_hour.resets_at) - now;
|
|
1522
|
-
if (ms > 0) candidates.push(ms);
|
|
1523
|
-
}
|
|
1524
|
-
if (usage.seven_day?.resets_at) {
|
|
1525
|
-
const ms = Date.parse(usage.seven_day.resets_at) - now;
|
|
1526
|
-
if (ms > 0) candidates.push(ms);
|
|
1527
|
-
}
|
|
1528
|
-
return candidates.length > 0 ? Math.min(...candidates) : null;
|
|
1529
|
-
}
|
|
1530
|
-
async function fetchUsageLimits2(accessToken, accountId) {
|
|
1531
|
-
if (!accessToken) return null;
|
|
1532
|
-
try {
|
|
1533
|
-
const result = await fetchUsage2(accessToken, accountId);
|
|
1534
|
-
return result.ok ? result.data : null;
|
|
1535
|
-
} catch {
|
|
1536
|
-
return null;
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
async function handleRateLimitResponse2(manager, client, account, response) {
|
|
1540
|
-
if (!account.uuid) return;
|
|
1541
|
-
const resetMs = getResetMsFromUsage2(account) ?? retryAfterMsFromResponse2(response);
|
|
1542
|
-
await manager.markRateLimited(account.uuid, resetMs);
|
|
1543
|
-
const shouldFetchUsage = account.accessToken && (!account.cachedUsageAt || Date.now() - account.cachedUsageAt > USAGE_FETCH_COOLDOWN_MS);
|
|
1544
|
-
if (shouldFetchUsage) {
|
|
1545
|
-
const usage = await fetchUsageLimits2(account.accessToken, account.accountId);
|
|
1546
|
-
if (usage) {
|
|
1547
|
-
await manager.applyUsageCache(account.uuid, usage);
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
if (manager.getAccountCount() > 1) {
|
|
1551
|
-
void showToast2(
|
|
1552
|
-
client,
|
|
1553
|
-
`${getAccountLabel2(account)} rate-limited (resets in ${formatWaitTime2(resetMs)}). Switching...`,
|
|
1554
|
-
"warning"
|
|
1555
|
-
);
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
return {
|
|
1559
|
-
retryAfterMsFromResponse: retryAfterMsFromResponse2,
|
|
1560
|
-
getResetMsFromUsage: getResetMsFromUsage2,
|
|
1561
|
-
fetchUsageLimits: fetchUsageLimits2,
|
|
1562
|
-
handleRateLimitResponse: handleRateLimitResponse2
|
|
1563
|
-
};
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
// ../multi-account-core/src/auth-migration.ts
|
|
1567
|
-
import { promises as fs5 } from "node:fs";
|
|
1568
|
-
import { join as join6 } from "node:path";
|
|
1569
|
-
var AUTH_JSON_FILENAME = "auth.json";
|
|
1570
|
-
function isValidOAuthCredential(value) {
|
|
1571
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1572
|
-
const candidate = value;
|
|
1573
|
-
return candidate.type === "oauth" && typeof candidate.refresh === "string" && candidate.refresh.length > 0;
|
|
1574
|
-
}
|
|
1575
|
-
function resolveAuthJsonPath() {
|
|
1576
|
-
return join6(getConfigDir2(), AUTH_JSON_FILENAME);
|
|
1577
|
-
}
|
|
1578
|
-
async function readAuthJson() {
|
|
1579
|
-
const authPath = resolveAuthJsonPath();
|
|
1580
|
-
let content;
|
|
1581
|
-
try {
|
|
1582
|
-
content = await fs5.readFile(authPath, "utf-8");
|
|
1583
|
-
} catch {
|
|
1584
|
-
return null;
|
|
1585
|
-
}
|
|
1586
|
-
try {
|
|
1587
|
-
const parsed = JSON.parse(content);
|
|
1588
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1589
|
-
return null;
|
|
1590
|
-
}
|
|
1591
|
-
return parsed;
|
|
1592
|
-
} catch {
|
|
1593
|
-
return null;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
async function migrateFromAuthJson(providerKey, store) {
|
|
1597
|
-
const storage = await store.load();
|
|
1598
|
-
const hasExistingAccounts = storage.accounts.length > 0;
|
|
1599
|
-
if (hasExistingAccounts) return false;
|
|
1600
|
-
const authData = await readAuthJson();
|
|
1601
|
-
if (!authData) return false;
|
|
1602
|
-
const providerCredential = authData[providerKey];
|
|
1603
|
-
if (!isValidOAuthCredential(providerCredential)) return false;
|
|
1604
|
-
const now = Date.now();
|
|
1605
|
-
const newAccount = {
|
|
1606
|
-
uuid: crypto.randomUUID(),
|
|
1607
|
-
refreshToken: providerCredential.refresh,
|
|
1608
|
-
accessToken: providerCredential.access,
|
|
1609
|
-
expiresAt: providerCredential.expires,
|
|
1610
|
-
addedAt: now,
|
|
1611
|
-
lastUsed: now,
|
|
1612
|
-
enabled: true,
|
|
1613
|
-
planTier: "",
|
|
1614
|
-
consecutiveAuthFailures: 0,
|
|
1615
|
-
isAuthDisabled: false
|
|
1616
|
-
};
|
|
1617
|
-
await store.addAccount(newAccount);
|
|
1618
|
-
await store.setActiveUuid(newAccount.uuid);
|
|
1619
|
-
return true;
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
// ../multi-account-core/src/ui/ansi.ts
|
|
1623
|
-
var ANSI = {
|
|
1624
|
-
hide: "\x1B[?25l",
|
|
1625
|
-
show: "\x1B[?25h",
|
|
1626
|
-
up: (n = 1) => `\x1B[${n}A`,
|
|
1627
|
-
down: (n = 1) => `\x1B[${n}B`,
|
|
1628
|
-
clearLine: "\x1B[2K",
|
|
1629
|
-
cyan: "\x1B[36m",
|
|
1630
|
-
green: "\x1B[32m",
|
|
1631
|
-
red: "\x1B[31m",
|
|
1632
|
-
yellow: "\x1B[33m",
|
|
1633
|
-
dim: "\x1B[2m",
|
|
1634
|
-
bold: "\x1B[1m",
|
|
1635
|
-
reset: "\x1B[0m"
|
|
1636
|
-
};
|
|
1637
|
-
function parseKey(data) {
|
|
1638
|
-
const s = data.toString();
|
|
1639
|
-
if (s === "\x1B[A" || s === "\x1BOA") return "up";
|
|
1640
|
-
if (s === "\x1B[B" || s === "\x1BOB") return "down";
|
|
1641
|
-
if (s === "\r" || s === "\n") return "enter";
|
|
1642
|
-
if (s === "") return "escape";
|
|
1643
|
-
if (s === "\x1B") return "escape-start";
|
|
1644
|
-
return null;
|
|
1645
|
-
}
|
|
1646
|
-
function isTTY() {
|
|
1647
|
-
return Boolean(process.stdin.isTTY);
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
// ../multi-account-core/src/ui/select.ts
|
|
1651
|
-
var ESCAPE_TIMEOUT_MS = 50;
|
|
1652
|
-
var COLOR_MAP = {
|
|
1653
|
-
red: ANSI.red,
|
|
1654
|
-
green: ANSI.green,
|
|
1655
|
-
yellow: ANSI.yellow,
|
|
1656
|
-
cyan: ANSI.cyan
|
|
1657
|
-
};
|
|
1658
|
-
async function select(items, options) {
|
|
1659
|
-
if (!isTTY()) {
|
|
1660
|
-
throw new Error("Interactive select requires a TTY terminal");
|
|
1661
|
-
}
|
|
1662
|
-
const enabledItems = items.filter((i) => !i.disabled && !i.separator);
|
|
1663
|
-
if (enabledItems.length === 0) {
|
|
1664
|
-
throw new Error("All items disabled");
|
|
1665
|
-
}
|
|
1666
|
-
if (enabledItems.length === 1) {
|
|
1667
|
-
return enabledItems[0].value;
|
|
1668
|
-
}
|
|
1669
|
-
const { message, subtitle } = options;
|
|
1670
|
-
const { stdin, stdout } = process;
|
|
1671
|
-
let cursor = items.findIndex((i) => !i.disabled && !i.separator);
|
|
1672
|
-
if (cursor === -1) cursor = 0;
|
|
1673
|
-
let escapeTimeout = null;
|
|
1674
|
-
let isCleanedUp = false;
|
|
1675
|
-
let isFirstRender = true;
|
|
1676
|
-
const getTotalLines = () => {
|
|
1677
|
-
const subtitleLines = subtitle ? 3 : 0;
|
|
1678
|
-
return 1 + subtitleLines + items.length + 1 + 1;
|
|
1679
|
-
};
|
|
1680
|
-
const renderItemLabel = (item, isSelected) => {
|
|
1681
|
-
const colorCode = item.color ? COLOR_MAP[item.color] ?? "" : "";
|
|
1682
|
-
if (item.disabled) {
|
|
1683
|
-
return `${ANSI.dim}${item.label} (unavailable)${ANSI.reset}`;
|
|
1684
|
-
}
|
|
1685
|
-
const hintSuffix = item.hint ? ` ${ANSI.dim}${item.hint}${ANSI.reset}` : "";
|
|
1686
|
-
if (isSelected) {
|
|
1687
|
-
const label = colorCode ? `${colorCode}${item.label}${ANSI.reset}` : item.label;
|
|
1688
|
-
return `${label}${hintSuffix}`;
|
|
1689
|
-
}
|
|
1690
|
-
const dimLabel = colorCode ? `${ANSI.dim}${colorCode}${item.label}${ANSI.reset}` : `${ANSI.dim}${item.label}${ANSI.reset}`;
|
|
1691
|
-
return `${dimLabel}${hintSuffix}`;
|
|
1692
|
-
};
|
|
1693
|
-
const render = () => {
|
|
1694
|
-
const totalLines = getTotalLines();
|
|
1695
|
-
if (!isFirstRender) {
|
|
1696
|
-
stdout.write(ANSI.up(totalLines) + "\r");
|
|
1697
|
-
}
|
|
1698
|
-
isFirstRender = false;
|
|
1699
|
-
stdout.write(`${ANSI.clearLine}${ANSI.dim}\u250C ${ANSI.reset}${message}
|
|
1700
|
-
`);
|
|
1701
|
-
if (subtitle) {
|
|
1702
|
-
stdout.write(`${ANSI.clearLine}${ANSI.dim}\u2502${ANSI.reset}
|
|
1703
|
-
`);
|
|
1704
|
-
stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u25C6${ANSI.reset} ${subtitle}
|
|
1705
|
-
`);
|
|
1706
|
-
stdout.write(`${ANSI.clearLine}
|
|
1707
|
-
`);
|
|
1708
|
-
}
|
|
1709
|
-
for (let i = 0; i < items.length; i++) {
|
|
1710
|
-
const item = items[i];
|
|
1711
|
-
if (!item) continue;
|
|
1712
|
-
if (item.separator) {
|
|
1713
|
-
stdout.write(`${ANSI.clearLine}${ANSI.dim}\u2502${ANSI.reset}
|
|
1714
|
-
`);
|
|
1715
|
-
continue;
|
|
1716
|
-
}
|
|
1717
|
-
const isSelected = i === cursor;
|
|
1718
|
-
const labelText = renderItemLabel(item, isSelected);
|
|
1719
|
-
const bullet = isSelected ? `${ANSI.green}\u25CF${ANSI.reset}` : `${ANSI.dim}\u25CB${ANSI.reset}`;
|
|
1720
|
-
stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2502${ANSI.reset} ${bullet} ${labelText}
|
|
1721
|
-
`);
|
|
1722
|
-
}
|
|
1723
|
-
stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2502${ANSI.reset} ${ANSI.dim}\u2191/\u2193 to select \u2022 Enter: confirm${ANSI.reset}
|
|
1724
|
-
`);
|
|
1725
|
-
stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2514${ANSI.reset}
|
|
1726
|
-
`);
|
|
1727
|
-
};
|
|
1728
|
-
return new Promise((resolve) => {
|
|
1729
|
-
const wasRaw = stdin.isRaw ?? false;
|
|
1730
|
-
const cleanup = () => {
|
|
1731
|
-
if (isCleanedUp) return;
|
|
1732
|
-
isCleanedUp = true;
|
|
1733
|
-
if (escapeTimeout) {
|
|
1734
|
-
clearTimeout(escapeTimeout);
|
|
1735
|
-
escapeTimeout = null;
|
|
1736
|
-
}
|
|
1737
|
-
try {
|
|
1738
|
-
stdin.removeListener("data", onKey);
|
|
1739
|
-
stdin.setRawMode(wasRaw);
|
|
1740
|
-
stdin.pause();
|
|
1741
|
-
stdout.write(ANSI.show);
|
|
1742
|
-
} catch {
|
|
1743
|
-
}
|
|
1744
|
-
process.removeListener("SIGINT", onSignal);
|
|
1745
|
-
process.removeListener("SIGTERM", onSignal);
|
|
1746
|
-
};
|
|
1747
|
-
const onSignal = () => {
|
|
1748
|
-
cleanup();
|
|
1749
|
-
resolve(null);
|
|
1750
|
-
};
|
|
1751
|
-
const finishWithValue = (value) => {
|
|
1752
|
-
cleanup();
|
|
1753
|
-
resolve(value);
|
|
1754
|
-
};
|
|
1755
|
-
const findNextSelectable = (from, direction) => {
|
|
1756
|
-
if (items.length === 0) return from;
|
|
1757
|
-
let next = from;
|
|
1758
|
-
do {
|
|
1759
|
-
next = (next + direction + items.length) % items.length;
|
|
1760
|
-
} while (items[next]?.disabled || items[next]?.separator);
|
|
1761
|
-
return next;
|
|
1762
|
-
};
|
|
1763
|
-
const onKey = (data) => {
|
|
1764
|
-
if (escapeTimeout) {
|
|
1765
|
-
clearTimeout(escapeTimeout);
|
|
1766
|
-
escapeTimeout = null;
|
|
1767
|
-
}
|
|
1768
|
-
const action = parseKey(data);
|
|
1769
|
-
switch (action) {
|
|
1770
|
-
case "up":
|
|
1771
|
-
cursor = findNextSelectable(cursor, -1);
|
|
1772
|
-
render();
|
|
1773
|
-
return;
|
|
1774
|
-
case "down":
|
|
1775
|
-
cursor = findNextSelectable(cursor, 1);
|
|
1776
|
-
render();
|
|
1777
|
-
return;
|
|
1778
|
-
case "enter":
|
|
1779
|
-
finishWithValue(items[cursor]?.value ?? null);
|
|
1780
|
-
return;
|
|
1781
|
-
case "escape":
|
|
1782
|
-
finishWithValue(null);
|
|
1783
|
-
return;
|
|
1784
|
-
case "escape-start":
|
|
1785
|
-
escapeTimeout = setTimeout(() => {
|
|
1786
|
-
finishWithValue(null);
|
|
1787
|
-
}, ESCAPE_TIMEOUT_MS);
|
|
1788
|
-
return;
|
|
1789
|
-
default:
|
|
1790
|
-
return;
|
|
1791
|
-
}
|
|
1792
|
-
};
|
|
1793
|
-
process.once("SIGINT", onSignal);
|
|
1794
|
-
process.once("SIGTERM", onSignal);
|
|
1795
|
-
try {
|
|
1796
|
-
stdin.setRawMode(true);
|
|
1797
|
-
} catch {
|
|
1798
|
-
cleanup();
|
|
1799
|
-
resolve(null);
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
|
-
stdin.resume();
|
|
1803
|
-
stdout.write(ANSI.hide);
|
|
1804
|
-
render();
|
|
1805
|
-
stdin.on("data", onKey);
|
|
1806
|
-
});
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
// ../multi-account-core/src/ui/confirm.ts
|
|
1810
|
-
async function confirm(message, defaultYes = false) {
|
|
1811
|
-
const items = defaultYes ? [
|
|
1812
|
-
{ label: "Yes", value: true },
|
|
1813
|
-
{ label: "No", value: false }
|
|
1814
|
-
] : [
|
|
1815
|
-
{ label: "No", value: false },
|
|
1816
|
-
{ label: "Yes", value: true }
|
|
1817
|
-
];
|
|
1818
|
-
const result = await select(items, { message });
|
|
1819
|
-
return result ?? false;
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
// ../multi-account-core/src/adapters/openai.ts
|
|
1823
|
-
var ISSUER = "https://auth.openai.com";
|
|
1824
|
-
var openAIOAuthAdapter = {
|
|
1825
|
-
id: "openai",
|
|
1826
|
-
authProviderId: "openai",
|
|
1827
|
-
modelDisplayName: "ChatGPT",
|
|
1828
|
-
statusToolName: "chatgpt_multiauth_status",
|
|
1829
|
-
authMethodLabel: "ChatGPT Plus/Pro (Multi-Auth)",
|
|
1830
|
-
serviceLogName: "chatgpt-multiauth",
|
|
1831
|
-
oauthClientId: "app_EMoamEEZ73f0CkXaXp7hrann",
|
|
1832
|
-
tokenEndpoint: `${ISSUER}/oauth/token`,
|
|
1833
|
-
usageEndpoint: "",
|
|
1834
|
-
profileEndpoint: "",
|
|
1835
|
-
oauthBetaHeader: "",
|
|
1836
|
-
requestBetaHeader: "",
|
|
1837
|
-
cliUserAgent: "opencode/1.1.53",
|
|
1838
|
-
cliVersion: "",
|
|
1839
|
-
billingSalt: "",
|
|
1840
|
-
toolPrefix: "mcp_",
|
|
1841
|
-
accountStorageFilename: "openai-multi-account-accounts.json",
|
|
1842
|
-
transform: {
|
|
1843
|
-
rewriteOpenCodeBranding: false,
|
|
1844
|
-
addToolPrefix: false,
|
|
1845
|
-
stripToolPrefixInResponse: false,
|
|
1846
|
-
enableMessagesBetaQuery: false
|
|
1847
|
-
},
|
|
1848
|
-
planLabels: {
|
|
1849
|
-
pro: "ChatGPT Pro",
|
|
1850
|
-
plus: "ChatGPT Plus",
|
|
1851
|
-
go: "ChatGPT Go",
|
|
1852
|
-
free: "Free"
|
|
1853
|
-
},
|
|
1854
|
-
supported: true
|
|
1855
|
-
};
|
|
5
|
+
// src/account-manager.ts
|
|
6
|
+
import { createAccountManagerForProvider } from "opencode-multi-account-core";
|
|
1856
7
|
|
|
1857
8
|
// src/constants.ts
|
|
9
|
+
import { openAIOAuthAdapter } from "opencode-multi-account-core";
|
|
1858
10
|
var OPENAI_OAUTH_ADAPTER = openAIOAuthAdapter;
|
|
1859
11
|
var OPENAI_CLIENT_ID = OPENAI_OAUTH_ADAPTER.oauthClientId;
|
|
1860
12
|
var OPENAI_TOKEN_ENDPOINT = OPENAI_OAUTH_ADAPTER.tokenEndpoint;
|
|
@@ -1867,95 +19,95 @@ var OAUTH_PORT = 1455;
|
|
|
1867
19
|
var OPENAI_BETA_HEADER = OPENAI_OAUTH_ADAPTER.requestBetaHeader;
|
|
1868
20
|
var OPENAI_CLI_USER_AGENT = OPENAI_OAUTH_ADAPTER.cliUserAgent;
|
|
1869
21
|
var TOOL_PREFIX = OPENAI_OAUTH_ADAPTER.toolPrefix;
|
|
1870
|
-
var
|
|
22
|
+
var ACCOUNTS_FILENAME = OPENAI_OAUTH_ADAPTER.accountStorageFilename;
|
|
1871
23
|
var PLAN_LABELS = OPENAI_OAUTH_ADAPTER.planLabels;
|
|
1872
24
|
var TOKEN_EXPIRY_BUFFER_MS = 6e4;
|
|
1873
25
|
var TOKEN_REFRESH_TIMEOUT_MS = 3e4;
|
|
1874
26
|
|
|
1875
27
|
// src/oauth.ts
|
|
1876
|
-
import * as
|
|
28
|
+
import * as v2 from "valibot";
|
|
1877
29
|
|
|
1878
30
|
// src/types.ts
|
|
1879
|
-
import * as
|
|
1880
|
-
var
|
|
1881
|
-
type:
|
|
1882
|
-
refresh:
|
|
1883
|
-
access:
|
|
1884
|
-
expires:
|
|
31
|
+
import * as v from "valibot";
|
|
32
|
+
var OAuthCredentialsSchema = v.object({
|
|
33
|
+
type: v.literal("oauth"),
|
|
34
|
+
refresh: v.string(),
|
|
35
|
+
access: v.string(),
|
|
36
|
+
expires: v.number()
|
|
1885
37
|
});
|
|
1886
|
-
var
|
|
1887
|
-
utilization:
|
|
1888
|
-
resets_at:
|
|
38
|
+
var UsageLimitEntrySchema = v.object({
|
|
39
|
+
utilization: v.number(),
|
|
40
|
+
resets_at: v.nullable(v.string())
|
|
1889
41
|
});
|
|
1890
|
-
var
|
|
1891
|
-
five_hour:
|
|
1892
|
-
seven_day:
|
|
1893
|
-
seven_day_sonnet:
|
|
42
|
+
var UsageLimitsSchema = v.object({
|
|
43
|
+
five_hour: v.optional(v.nullable(UsageLimitEntrySchema), null),
|
|
44
|
+
seven_day: v.optional(v.nullable(UsageLimitEntrySchema), null),
|
|
45
|
+
seven_day_sonnet: v.optional(v.nullable(UsageLimitEntrySchema), null)
|
|
1894
46
|
});
|
|
1895
|
-
var
|
|
1896
|
-
accessToken:
|
|
1897
|
-
expiresAt:
|
|
1898
|
-
refreshToken:
|
|
1899
|
-
uuid:
|
|
1900
|
-
accountId:
|
|
1901
|
-
email:
|
|
47
|
+
var CredentialRefreshPatchSchema = v.object({
|
|
48
|
+
accessToken: v.string(),
|
|
49
|
+
expiresAt: v.number(),
|
|
50
|
+
refreshToken: v.optional(v.string()),
|
|
51
|
+
uuid: v.optional(v.string()),
|
|
52
|
+
accountId: v.optional(v.string()),
|
|
53
|
+
email: v.optional(v.string())
|
|
1902
54
|
});
|
|
1903
|
-
var
|
|
1904
|
-
uuid:
|
|
1905
|
-
accountId:
|
|
1906
|
-
label:
|
|
1907
|
-
email:
|
|
1908
|
-
planTier:
|
|
1909
|
-
refreshToken:
|
|
1910
|
-
accessToken:
|
|
1911
|
-
expiresAt:
|
|
1912
|
-
addedAt:
|
|
1913
|
-
lastUsed:
|
|
1914
|
-
enabled:
|
|
1915
|
-
rateLimitResetAt:
|
|
1916
|
-
cachedUsage:
|
|
1917
|
-
cachedUsageAt:
|
|
1918
|
-
consecutiveAuthFailures:
|
|
1919
|
-
isAuthDisabled:
|
|
1920
|
-
authDisabledReason:
|
|
55
|
+
var StoredAccountSchema = v.object({
|
|
56
|
+
uuid: v.optional(v.string()),
|
|
57
|
+
accountId: v.optional(v.string()),
|
|
58
|
+
label: v.optional(v.string()),
|
|
59
|
+
email: v.optional(v.string()),
|
|
60
|
+
planTier: v.optional(v.string(), ""),
|
|
61
|
+
refreshToken: v.string(),
|
|
62
|
+
accessToken: v.optional(v.string()),
|
|
63
|
+
expiresAt: v.optional(v.number()),
|
|
64
|
+
addedAt: v.number(),
|
|
65
|
+
lastUsed: v.number(),
|
|
66
|
+
enabled: v.optional(v.boolean(), true),
|
|
67
|
+
rateLimitResetAt: v.optional(v.number()),
|
|
68
|
+
cachedUsage: v.optional(UsageLimitsSchema),
|
|
69
|
+
cachedUsageAt: v.optional(v.number()),
|
|
70
|
+
consecutiveAuthFailures: v.optional(v.number(), 0),
|
|
71
|
+
isAuthDisabled: v.optional(v.boolean(), false),
|
|
72
|
+
authDisabledReason: v.optional(v.string())
|
|
1921
73
|
});
|
|
1922
|
-
var
|
|
1923
|
-
version:
|
|
1924
|
-
accounts:
|
|
1925
|
-
activeAccountUuid:
|
|
74
|
+
var AccountStorageSchema = v.object({
|
|
75
|
+
version: v.literal(1),
|
|
76
|
+
accounts: v.optional(v.array(StoredAccountSchema), []),
|
|
77
|
+
activeAccountUuid: v.optional(v.string())
|
|
1926
78
|
});
|
|
1927
|
-
var TokenResponseSchema =
|
|
1928
|
-
id_token:
|
|
1929
|
-
access_token:
|
|
1930
|
-
refresh_token:
|
|
1931
|
-
expires_in:
|
|
79
|
+
var TokenResponseSchema = v.object({
|
|
80
|
+
id_token: v.optional(v.string()),
|
|
81
|
+
access_token: v.string(),
|
|
82
|
+
refresh_token: v.optional(v.string()),
|
|
83
|
+
expires_in: v.number()
|
|
1932
84
|
});
|
|
1933
|
-
var
|
|
1934
|
-
var
|
|
85
|
+
var AccountSelectionStrategySchema = v.picklist(["sticky", "round-robin", "hybrid"]);
|
|
86
|
+
var PluginConfigSchema = v.object({
|
|
1935
87
|
/** sticky: same account until failure, round-robin: rotate every request, hybrid: health+usage scoring */
|
|
1936
|
-
account_selection_strategy:
|
|
88
|
+
account_selection_strategy: v.optional(AccountSelectionStrategySchema, "sticky"),
|
|
1937
89
|
/** Use cross-process claim file to distribute parallel sessions across accounts */
|
|
1938
|
-
cross_process_claims:
|
|
90
|
+
cross_process_claims: v.optional(v.boolean(), true),
|
|
1939
91
|
/** Skip account when any usage tier utilization >= this % (100 = disabled) */
|
|
1940
|
-
soft_quota_threshold_percent:
|
|
92
|
+
soft_quota_threshold_percent: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(100)), 100),
|
|
1941
93
|
/** Minimum backoff after rate limit (ms) */
|
|
1942
|
-
rate_limit_min_backoff_ms:
|
|
94
|
+
rate_limit_min_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
|
|
1943
95
|
/** Default retry-after when header is missing (ms) */
|
|
1944
|
-
default_retry_after_ms:
|
|
96
|
+
default_retry_after_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 6e4),
|
|
1945
97
|
/** Consecutive auth failures before disabling account */
|
|
1946
|
-
max_consecutive_auth_failures:
|
|
98
|
+
max_consecutive_auth_failures: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 3),
|
|
1947
99
|
/** Backoff after token refresh failure (ms) */
|
|
1948
|
-
token_failure_backoff_ms:
|
|
100
|
+
token_failure_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
|
|
1949
101
|
/** Enable proactive background token refresh */
|
|
1950
|
-
proactive_refresh:
|
|
102
|
+
proactive_refresh: v.optional(v.boolean(), true),
|
|
1951
103
|
/** Seconds before expiry to trigger proactive refresh (default 30 min) */
|
|
1952
|
-
proactive_refresh_buffer_seconds:
|
|
104
|
+
proactive_refresh_buffer_seconds: v.optional(v.pipe(v.number(), v.minValue(60)), 1800),
|
|
1953
105
|
/** Interval between background refresh checks in seconds (default 5 min) */
|
|
1954
|
-
proactive_refresh_interval_seconds:
|
|
106
|
+
proactive_refresh_interval_seconds: v.optional(v.pipe(v.number(), v.minValue(30)), 300),
|
|
1955
107
|
/** Suppress toast notifications */
|
|
1956
|
-
quiet_mode:
|
|
108
|
+
quiet_mode: v.optional(v.boolean(), false),
|
|
1957
109
|
/** Enable debug logging */
|
|
1958
|
-
debug:
|
|
110
|
+
debug: v.optional(v.boolean(), false)
|
|
1959
111
|
});
|
|
1960
112
|
|
|
1961
113
|
// src/oauth.ts
|
|
@@ -2034,7 +186,7 @@ function tokenEndpoint() {
|
|
|
2034
186
|
return `${OAUTH_ISSUER}/oauth/token`;
|
|
2035
187
|
}
|
|
2036
188
|
function parseTokenResponse(json) {
|
|
2037
|
-
return
|
|
189
|
+
return v2.parse(TokenResponseSchema, json);
|
|
2038
190
|
}
|
|
2039
191
|
async function postTokenForm(body) {
|
|
2040
192
|
const response = await fetch(tokenEndpoint(), {
|
|
@@ -2233,7 +385,7 @@ function buildAuthorizeUrl(redirectUri, pkce, state) {
|
|
|
2233
385
|
}
|
|
2234
386
|
|
|
2235
387
|
// src/token.ts
|
|
2236
|
-
import * as
|
|
388
|
+
import * as v3 from "valibot";
|
|
2237
389
|
var PERMANENT_FAILURE_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
|
|
2238
390
|
var refreshMutexByAccountId = /* @__PURE__ */ new Map();
|
|
2239
391
|
function isTokenExpired(account) {
|
|
@@ -2272,7 +424,7 @@ async function refreshToken(currentRefreshToken, accountId, client) {
|
|
|
2272
424
|
});
|
|
2273
425
|
return { ok: false, permanent: isPermanent, status: response.status };
|
|
2274
426
|
}
|
|
2275
|
-
const json =
|
|
427
|
+
const json = v3.parse(TokenResponseSchema, await response.json());
|
|
2276
428
|
const patch = {
|
|
2277
429
|
accessToken: json.access_token,
|
|
2278
430
|
expiresAt: startTime + json.expires_in * 1e3,
|
|
@@ -2307,37 +459,61 @@ var AccountManager = createAccountManagerForProvider({
|
|
|
2307
459
|
refreshToken
|
|
2308
460
|
});
|
|
2309
461
|
|
|
462
|
+
// src/executor.ts
|
|
463
|
+
import { createExecutorForProvider } from "opencode-multi-account-core";
|
|
464
|
+
|
|
465
|
+
// src/rate-limit.ts
|
|
466
|
+
import { createRateLimitHandlers } from "opencode-multi-account-core";
|
|
467
|
+
|
|
2310
468
|
// src/config.ts
|
|
469
|
+
import {
|
|
470
|
+
getConfig,
|
|
471
|
+
initCoreConfig,
|
|
472
|
+
loadConfig,
|
|
473
|
+
resetConfigCache,
|
|
474
|
+
updateConfigField
|
|
475
|
+
} from "opencode-multi-account-core";
|
|
2311
476
|
initCoreConfig("codex-multiauth.json");
|
|
2312
477
|
|
|
2313
478
|
// src/utils.ts
|
|
479
|
+
import { setConfigGetter } from "opencode-multi-account-core";
|
|
480
|
+
import {
|
|
481
|
+
createMinimalClient,
|
|
482
|
+
debugLog,
|
|
483
|
+
formatWaitTime,
|
|
484
|
+
getAccountLabel,
|
|
485
|
+
getConfigDir,
|
|
486
|
+
getErrorCode,
|
|
487
|
+
showToast,
|
|
488
|
+
sleep
|
|
489
|
+
} from "opencode-multi-account-core";
|
|
2314
490
|
setConfigGetter(getConfig);
|
|
2315
491
|
|
|
2316
492
|
// src/usage.ts
|
|
2317
|
-
import * as
|
|
493
|
+
import * as v4 from "valibot";
|
|
2318
494
|
var OPENAI_AUTH_CLAIM = "https://api.openai.com/auth";
|
|
2319
495
|
var OPENAI_PROFILE_CLAIM = "https://api.openai.com/profile";
|
|
2320
|
-
var OpenAIAuthClaimSchema =
|
|
2321
|
-
chatgpt_plan_type:
|
|
2322
|
-
chatgpt_account_id:
|
|
2323
|
-
chatgpt_user_id:
|
|
496
|
+
var OpenAIAuthClaimSchema = v4.object({
|
|
497
|
+
chatgpt_plan_type: v4.optional(v4.string()),
|
|
498
|
+
chatgpt_account_id: v4.optional(v4.string()),
|
|
499
|
+
chatgpt_user_id: v4.optional(v4.string())
|
|
2324
500
|
});
|
|
2325
|
-
var OpenAIProfileClaimSchema =
|
|
2326
|
-
email:
|
|
501
|
+
var OpenAIProfileClaimSchema = v4.object({
|
|
502
|
+
email: v4.optional(v4.string())
|
|
2327
503
|
});
|
|
2328
|
-
var WhamRateLimitWindowSchema =
|
|
2329
|
-
used_percent:
|
|
2330
|
-
reset_after_seconds:
|
|
504
|
+
var WhamRateLimitWindowSchema = v4.object({
|
|
505
|
+
used_percent: v4.number(),
|
|
506
|
+
reset_after_seconds: v4.number()
|
|
2331
507
|
});
|
|
2332
|
-
var WhamUsageResponseSchema =
|
|
2333
|
-
plan_type:
|
|
2334
|
-
rate_limit:
|
|
2335
|
-
primary_window:
|
|
2336
|
-
secondary_window:
|
|
508
|
+
var WhamUsageResponseSchema = v4.object({
|
|
509
|
+
plan_type: v4.optional(v4.nullable(v4.string())),
|
|
510
|
+
rate_limit: v4.optional(v4.nullable(v4.object({
|
|
511
|
+
primary_window: v4.optional(v4.nullable(WhamRateLimitWindowSchema)),
|
|
512
|
+
secondary_window: v4.optional(v4.nullable(WhamRateLimitWindowSchema))
|
|
2337
513
|
}))),
|
|
2338
|
-
credits:
|
|
2339
|
-
balance:
|
|
2340
|
-
unlimited:
|
|
514
|
+
credits: v4.optional(v4.nullable(v4.object({
|
|
515
|
+
balance: v4.optional(v4.nullable(v4.string())),
|
|
516
|
+
unlimited: v4.optional(v4.nullable(v4.boolean()))
|
|
2341
517
|
})))
|
|
2342
518
|
});
|
|
2343
519
|
function secondsToISOResetTime(resetAfterSeconds) {
|
|
@@ -2359,7 +535,7 @@ async function fetchUsage(accessToken, accountId) {
|
|
|
2359
535
|
if (!response.ok) {
|
|
2360
536
|
return { ok: false, reason: `HTTP ${response.status} ${response.statusText}` };
|
|
2361
537
|
}
|
|
2362
|
-
const parsed =
|
|
538
|
+
const parsed = v4.safeParse(WhamUsageResponseSchema, await response.json());
|
|
2363
539
|
if (!parsed.success) {
|
|
2364
540
|
return { ok: false, reason: `Invalid response: ${parsed.issues[0]?.message ?? "unknown"}` };
|
|
2365
541
|
}
|
|
@@ -2377,7 +553,7 @@ async function fetchUsage(accessToken, accountId) {
|
|
|
2377
553
|
} : null,
|
|
2378
554
|
seven_day_sonnet: null
|
|
2379
555
|
};
|
|
2380
|
-
const validated =
|
|
556
|
+
const validated = v4.safeParse(UsageLimitsSchema, usage);
|
|
2381
557
|
if (!validated.success) {
|
|
2382
558
|
return { ok: false, reason: `Mapping error: ${validated.issues[0]?.message ?? "unknown"}` };
|
|
2383
559
|
}
|
|
@@ -2399,10 +575,10 @@ function fetchProfile(accessToken) {
|
|
|
2399
575
|
}
|
|
2400
576
|
const record = claims;
|
|
2401
577
|
const profileClaim = record[OPENAI_PROFILE_CLAIM];
|
|
2402
|
-
const profileParsed =
|
|
578
|
+
const profileParsed = v4.safeParse(OpenAIProfileClaimSchema, profileClaim ?? {});
|
|
2403
579
|
const email = profileParsed.success ? profileParsed.output.email : void 0;
|
|
2404
580
|
const authClaim = record[OPENAI_AUTH_CLAIM];
|
|
2405
|
-
const authParsed =
|
|
581
|
+
const authParsed = v4.safeParse(OpenAIAuthClaimSchema, authClaim ?? {});
|
|
2406
582
|
const planType = authParsed.success ? authParsed.output.chatgpt_plan_type ?? "" : "";
|
|
2407
583
|
const planTier = derivePlanTier(planType);
|
|
2408
584
|
return {
|
|
@@ -2422,7 +598,7 @@ function formatTimeRemaining(resetAt) {
|
|
|
2422
598
|
}
|
|
2423
599
|
function getUsageSummary(account) {
|
|
2424
600
|
if (!account.cachedUsage) return "no usage data";
|
|
2425
|
-
const parsed =
|
|
601
|
+
const parsed = v4.safeParse(UsageLimitsSchema, account.cachedUsage);
|
|
2426
602
|
if (!parsed.success) return "no usage data";
|
|
2427
603
|
const parts = [];
|
|
2428
604
|
const { five_hour, seven_day } = parsed.output;
|
|
@@ -2470,7 +646,22 @@ var { executeWithAccountRotation } = createExecutorForProvider("Codex", {
|
|
|
2470
646
|
});
|
|
2471
647
|
|
|
2472
648
|
// src/auth-handler.ts
|
|
2473
|
-
import * as
|
|
649
|
+
import * as v5 from "valibot";
|
|
650
|
+
|
|
651
|
+
// src/ui/ansi.ts
|
|
652
|
+
import {
|
|
653
|
+
ANSI,
|
|
654
|
+
isTTY,
|
|
655
|
+
parseKey
|
|
656
|
+
} from "opencode-multi-account-core";
|
|
657
|
+
|
|
658
|
+
// src/ui/select.ts
|
|
659
|
+
import {
|
|
660
|
+
select
|
|
661
|
+
} from "opencode-multi-account-core";
|
|
662
|
+
|
|
663
|
+
// src/ui/confirm.ts
|
|
664
|
+
import { confirm } from "opencode-multi-account-core";
|
|
2474
665
|
|
|
2475
666
|
// src/ui/auth-menu.ts
|
|
2476
667
|
function formatRelativeTime(timestamp) {
|
|
@@ -2684,15 +875,21 @@ function printQuotaError(account, error) {
|
|
|
2684
875
|
}
|
|
2685
876
|
|
|
2686
877
|
// src/account-store.ts
|
|
2687
|
-
|
|
878
|
+
import {
|
|
879
|
+
AccountStore,
|
|
880
|
+
setAccountsFilename
|
|
881
|
+
} from "opencode-multi-account-core";
|
|
882
|
+
setAccountsFilename(ACCOUNTS_FILENAME);
|
|
2688
883
|
|
|
2689
884
|
// src/auth-handler.ts
|
|
2690
|
-
import { randomUUID
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
885
|
+
import { randomUUID } from "crypto";
|
|
886
|
+
import { createInterface } from "readline";
|
|
887
|
+
import { exec } from "child_process";
|
|
888
|
+
var DeviceUserCodeResponseSchema = v5.object({
|
|
889
|
+
device_code: v5.string(),
|
|
890
|
+
user_code: v5.string(),
|
|
891
|
+
expires_in: v5.number(),
|
|
892
|
+
interval: v5.optional(v5.number())
|
|
2696
893
|
});
|
|
2697
894
|
function makeFailedFlowResult(message) {
|
|
2698
895
|
return {
|
|
@@ -2732,7 +929,7 @@ async function pollDeviceAuthToken(startResult) {
|
|
|
2732
929
|
})
|
|
2733
930
|
});
|
|
2734
931
|
if (response.ok) {
|
|
2735
|
-
return
|
|
932
|
+
return v5.parse(TokenResponseSchema, await response.json());
|
|
2736
933
|
}
|
|
2737
934
|
const payload = await response.json().catch(() => ({}));
|
|
2738
935
|
const error = typeof payload.error === "string" ? payload.error : "";
|
|
@@ -2785,7 +982,7 @@ async function startDeviceAuth() {
|
|
|
2785
982
|
if (!response.ok) {
|
|
2786
983
|
throw new Error(`Failed to start device authorization: ${response.status}`);
|
|
2787
984
|
}
|
|
2788
|
-
const startResult =
|
|
985
|
+
const startResult = v5.parse(DeviceUserCodeResponseSchema, await response.json());
|
|
2789
986
|
return {
|
|
2790
987
|
url: `${OAUTH_ISSUER}/codex/device`,
|
|
2791
988
|
instructions: `Enter code: ${startResult.user_code}`,
|
|
@@ -2869,7 +1066,7 @@ async function persistFallback(auth, accountId) {
|
|
|
2869
1066
|
const store = new AccountStore();
|
|
2870
1067
|
const now = Date.now();
|
|
2871
1068
|
const account = {
|
|
2872
|
-
uuid:
|
|
1069
|
+
uuid: randomUUID(),
|
|
2873
1070
|
accountId,
|
|
2874
1071
|
refreshToken: auth.refresh,
|
|
2875
1072
|
accessToken: auth.access,
|
|
@@ -3060,6 +1257,7 @@ Retrying authentication for ${label}...
|
|
|
3060
1257
|
}
|
|
3061
1258
|
|
|
3062
1259
|
// src/proactive-refresh.ts
|
|
1260
|
+
import { createProactiveRefreshQueueForProvider } from "opencode-multi-account-core";
|
|
3063
1261
|
var ProactiveRefreshQueue = createProactiveRefreshQueueForProvider({
|
|
3064
1262
|
providerAuthId: "openai",
|
|
3065
1263
|
getConfig,
|
|
@@ -3131,11 +1329,14 @@ function transformRequestUrl(input) {
|
|
|
3131
1329
|
}
|
|
3132
1330
|
|
|
3133
1331
|
// src/runtime-factory.ts
|
|
1332
|
+
import { TokenRefreshError } from "opencode-multi-account-core";
|
|
3134
1333
|
var AccountRuntimeFactory = class {
|
|
3135
1334
|
constructor(store, client) {
|
|
3136
1335
|
this.store = store;
|
|
3137
1336
|
this.client = client;
|
|
3138
1337
|
}
|
|
1338
|
+
store;
|
|
1339
|
+
client;
|
|
3139
1340
|
runtimes = /* @__PURE__ */ new Map();
|
|
3140
1341
|
initLocks = /* @__PURE__ */ new Map();
|
|
3141
1342
|
async getRuntime(uuid) {
|
|
@@ -3341,3 +1542,4 @@ var CodexMultiAuthPlugin = async (ctx) => {
|
|
|
3341
1542
|
export {
|
|
3342
1543
|
CodexMultiAuthPlugin
|
|
3343
1544
|
};
|
|
1545
|
+
//# sourceMappingURL=index.js.map
|