opencode-auth-load-balancer 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -3
- package/dist/accounts.d.ts +22 -2
- package/dist/index.js +1901 -236
- package/dist/login-health.d.ts +12 -0
- package/dist/notify.d.ts +6 -0
- package/dist/pairing.d.ts +22 -0
- package/dist/pool/paths.d.ts +8 -0
- package/dist/providers/anthropic/constants.d.ts +21 -0
- package/dist/providers/anthropic/oauth.d.ts +9 -0
- package/dist/providers/anthropic/setup-token.d.ts +14 -0
- package/dist/providers/anthropic/usage.d.ts +13 -0
- package/dist/providers/types.d.ts +27 -0
- package/dist/refresh.d.ts +13 -1
- package/dist/status.d.ts +6 -0
- package/dist/sync/apply.d.ts +66 -0
- package/dist/sync/crypto.d.ts +19 -0
- package/dist/sync/cycle.d.ts +19 -0
- package/dist/sync/engine.d.ts +44 -0
- package/dist/sync/errors.d.ts +9 -0
- package/dist/sync/gist.d.ts +33 -0
- package/dist/sync/github-auth.d.ts +15 -0
- package/dist/sync/intent.d.ts +29 -0
- package/dist/sync/loop.d.ts +40 -0
- package/dist/sync/merge.d.ts +49 -0
- package/dist/sync/origin.d.ts +2 -0
- package/dist/sync/own.d.ts +31 -0
- package/dist/sync/payload.d.ts +50 -0
- package/dist/sync/start.d.ts +5 -0
- package/dist/sync/state.d.ts +79 -0
- package/dist/sync/timing.d.ts +4 -0
- package/dist/token-mint.d.ts +25 -0
- package/dist/types.d.ts +82 -3
- package/dist/usage-refresh.d.ts +1 -1
- package/dist/util.d.ts +7 -0
- package/package.json +1 -1
- package/tui/auth-load-balancer-tui.logic.ts +363 -7
- package/tui/auth-load-balancer-tui.view.tsx +299 -107
package/dist/index.js
CHANGED
|
@@ -12337,16 +12337,64 @@ tool.schema = exports_external;
|
|
|
12337
12337
|
// src/accounts.ts
|
|
12338
12338
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
12339
12339
|
|
|
12340
|
-
// src/pool/store.ts
|
|
12341
|
-
import { mkdir as mkdir2, readFile as readFile2, rename, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
12342
|
-
import { dirname as dirname2 } from "node:path";
|
|
12343
|
-
|
|
12344
12340
|
// src/types.ts
|
|
12345
12341
|
function emptyUsage() {
|
|
12346
12342
|
return { hourly: null, weekly: null, capturedAt: 0 };
|
|
12347
12343
|
}
|
|
12344
|
+
var STATIC_CREDENTIAL_EXPIRES = Number.MAX_SAFE_INTEGER;
|
|
12348
12345
|
var MANUAL_DISABLED_REASON = "manually disabled";
|
|
12349
12346
|
|
|
12347
|
+
// src/pairing.ts
|
|
12348
|
+
function holdsCredential(row, tokens) {
|
|
12349
|
+
if (tokens.inferenceOnly)
|
|
12350
|
+
return row.inferenceToken === tokens.access;
|
|
12351
|
+
return !!tokens.refresh && row.refresh === tokens.refresh || !!tokens.accountId && row.accountId === tokens.accountId;
|
|
12352
|
+
}
|
|
12353
|
+
var WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
12354
|
+
var RESET_TOLERANCE_MS = 2 * 60 * 1000;
|
|
12355
|
+
function resetGap(a, b, period) {
|
|
12356
|
+
const d = Math.abs(a.resetAt - b.resetAt);
|
|
12357
|
+
return period ? Math.min(d % period, period - d % period) : d;
|
|
12358
|
+
}
|
|
12359
|
+
function sameAccount(a, b, now) {
|
|
12360
|
+
const live = (w) => w && w.resetAt > now ? w : undefined;
|
|
12361
|
+
const [h1, h2] = [live(a?.hourly), live(b.hourly)];
|
|
12362
|
+
if (h1 && h2 && resetGap(h1, h2, 0) > RESET_TOLERANCE_MS)
|
|
12363
|
+
return false;
|
|
12364
|
+
const [w1, w2] = [a?.weekly, b.weekly];
|
|
12365
|
+
if (!w1?.resetAt || !w2?.resetAt)
|
|
12366
|
+
return;
|
|
12367
|
+
return resetGap(w1, w2, WEEK_MS) <= RESET_TOLERANCE_MS;
|
|
12368
|
+
}
|
|
12369
|
+
function orgPartner(rows, tokens) {
|
|
12370
|
+
if (!tokens.orgId)
|
|
12371
|
+
return;
|
|
12372
|
+
const now = Date.now();
|
|
12373
|
+
const candidates = rows.filter((row) => row.orgId === tokens.orgId && sameAccount(tokens.usage, row.usage, now) !== false);
|
|
12374
|
+
const only = candidates.length === 1 ? candidates[0] : undefined;
|
|
12375
|
+
if (!only)
|
|
12376
|
+
return;
|
|
12377
|
+
if (!tokens.inferenceOnly)
|
|
12378
|
+
return only.refresh ? undefined : only;
|
|
12379
|
+
const confirmed = sameAccount(tokens.usage, only.usage, now) === true;
|
|
12380
|
+
return only.inferenceToken === undefined || tokenDead(only) || confirmed ? only : undefined;
|
|
12381
|
+
}
|
|
12382
|
+
function tokenDead(row) {
|
|
12383
|
+
return !!row.disabledReason && row.disabledReason !== MANUAL_DISABLED_REASON;
|
|
12384
|
+
}
|
|
12385
|
+
function tokenSharer(rows, tokens) {
|
|
12386
|
+
if (!tokens.orgId)
|
|
12387
|
+
return;
|
|
12388
|
+
const now = Date.now();
|
|
12389
|
+
const candidates = rows.filter((row) => row.orgId === tokens.orgId && sameAccount(tokens.usage, row.usage, now) !== false);
|
|
12390
|
+
const only = candidates.length === 1 ? candidates[0] : undefined;
|
|
12391
|
+
return only?.inferenceToken !== undefined && !tokenDead(only) ? only : undefined;
|
|
12392
|
+
}
|
|
12393
|
+
|
|
12394
|
+
// src/pool/store.ts
|
|
12395
|
+
import { mkdir as mkdir2, readFile as readFile2, rename, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
12396
|
+
import { dirname as dirname2 } from "node:path";
|
|
12397
|
+
|
|
12350
12398
|
// src/providers/http-timeouts.ts
|
|
12351
12399
|
var OAUTH_HTTP_TIMEOUT_MS = 30000;
|
|
12352
12400
|
var USAGE_HTTP_TIMEOUT_MS = 30000;
|
|
@@ -12389,6 +12437,19 @@ function isFiniteNumber(v) {
|
|
|
12389
12437
|
function isPlainObject2(value) {
|
|
12390
12438
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12391
12439
|
}
|
|
12440
|
+
function errorSummary(status, body) {
|
|
12441
|
+
let detail = body;
|
|
12442
|
+
try {
|
|
12443
|
+
const json = JSON.parse(body);
|
|
12444
|
+
if (isPlainObject2(json)) {
|
|
12445
|
+
const err = json.error;
|
|
12446
|
+
const parts = isPlainObject2(err) ? [err.type, err.message] : [err, json.error_description ?? json.message];
|
|
12447
|
+
detail = parts.filter((part) => typeof part === "string").join(": ");
|
|
12448
|
+
}
|
|
12449
|
+
} catch {}
|
|
12450
|
+
const line = `${status} ${detail}`.replace(/\s+/g, " ").trim();
|
|
12451
|
+
return line.length > 160 ? `${line.slice(0, 159)}…` : line;
|
|
12452
|
+
}
|
|
12392
12453
|
function joinBlockTexts(content, separator) {
|
|
12393
12454
|
if (typeof content === "string")
|
|
12394
12455
|
return content;
|
|
@@ -12582,6 +12643,30 @@ function versionCacheFilePath() {
|
|
|
12582
12643
|
cachedVersion = { override, xdg, path };
|
|
12583
12644
|
return path;
|
|
12584
12645
|
}
|
|
12646
|
+
var syncPathMemo = new Map;
|
|
12647
|
+
function dataFilePath(file) {
|
|
12648
|
+
const override = process.env.OPENCODE_AUTH_LB_DIR;
|
|
12649
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
12650
|
+
const key = `${override ?? ""}\x00${xdg ?? ""}`;
|
|
12651
|
+
const hit = syncPathMemo.get(file);
|
|
12652
|
+
if (hit?.key === key)
|
|
12653
|
+
return hit.path;
|
|
12654
|
+
const path = join2(resolveDataDir({ override, xdgDataHome: xdg }, homedir()), file);
|
|
12655
|
+
syncPathMemo.set(file, { key, path });
|
|
12656
|
+
return path;
|
|
12657
|
+
}
|
|
12658
|
+
function syncStateFilePath() {
|
|
12659
|
+
return dataFilePath("auth-load-balancer-sync.json");
|
|
12660
|
+
}
|
|
12661
|
+
function syncOriginFilePath() {
|
|
12662
|
+
return dataFilePath("auth-load-balancer-sync-origin.json");
|
|
12663
|
+
}
|
|
12664
|
+
function syncIntentFilePath() {
|
|
12665
|
+
return dataFilePath("auth-load-balancer-sync-intent.json");
|
|
12666
|
+
}
|
|
12667
|
+
function syncStatusFilePath() {
|
|
12668
|
+
return dataFilePath("auth-load-balancer-sync-status.json");
|
|
12669
|
+
}
|
|
12585
12670
|
|
|
12586
12671
|
// src/pool/store.ts
|
|
12587
12672
|
function emptyPool() {
|
|
@@ -12696,6 +12781,24 @@ function normalizeAccounts(rows) {
|
|
|
12696
12781
|
if (row.accountId !== null && typeof row.accountId !== "string") {
|
|
12697
12782
|
row.accountId = null;
|
|
12698
12783
|
}
|
|
12784
|
+
if (typeof row.inferenceToken !== "string" || row.inferenceToken === "")
|
|
12785
|
+
delete row.inferenceToken;
|
|
12786
|
+
if (typeof row.orgId !== "string" || row.orgId === "")
|
|
12787
|
+
delete row.orgId;
|
|
12788
|
+
if (!isFiniteNumber(row.refreshExpires) || row.refreshExpires <= 0)
|
|
12789
|
+
delete row.refreshExpires;
|
|
12790
|
+
if (row.inferenceToken === undefined || !isFiniteNumber(row.inferenceExpires) || row.inferenceExpires <= 0)
|
|
12791
|
+
delete row.inferenceExpires;
|
|
12792
|
+
if (row.lostLogins !== undefined) {
|
|
12793
|
+
const lost = isPlainObject2(row.lostLogins) ? row.lostLogins : {};
|
|
12794
|
+
for (const login of ["oauth", "token"]) {
|
|
12795
|
+
const entry = lost[login];
|
|
12796
|
+
if (!isPlainObject2(entry) || !isFiniteNumber(entry.at) || typeof entry.reason !== "string")
|
|
12797
|
+
delete lost[login];
|
|
12798
|
+
}
|
|
12799
|
+
if (lost.oauth === undefined && lost.token === undefined)
|
|
12800
|
+
delete row.lostLogins;
|
|
12801
|
+
}
|
|
12699
12802
|
if (row.tokenGen !== undefined && !Number.isFinite(row.tokenGen)) {
|
|
12700
12803
|
row.tokenGen = 0;
|
|
12701
12804
|
}
|
|
@@ -12810,87 +12913,16 @@ async function readPoolAccount(id) {
|
|
|
12810
12913
|
return findAccount(await readPool(), id);
|
|
12811
12914
|
}
|
|
12812
12915
|
|
|
12813
|
-
// src/accounts.ts
|
|
12814
|
-
function makeAccount(providerID, label, tokens) {
|
|
12815
|
-
return {
|
|
12816
|
-
id: randomUUID2(),
|
|
12817
|
-
providerID,
|
|
12818
|
-
label,
|
|
12819
|
-
access: tokens.access,
|
|
12820
|
-
refresh: tokens.refresh,
|
|
12821
|
-
expires: tokens.expires,
|
|
12822
|
-
tokenGen: 0,
|
|
12823
|
-
accountId: tokens.accountId ?? null,
|
|
12824
|
-
usage: emptyUsage(),
|
|
12825
|
-
cooldownUntil: 0,
|
|
12826
|
-
disabledReason: null
|
|
12827
|
-
};
|
|
12828
|
-
}
|
|
12829
|
-
function applyTokens(row, tokens) {
|
|
12830
|
-
if (tokens.refresh)
|
|
12831
|
-
row.refresh = tokens.refresh;
|
|
12832
|
-
row.access = tokens.access;
|
|
12833
|
-
row.expires = tokens.expires;
|
|
12834
|
-
row.disabledReason = null;
|
|
12835
|
-
if (tokens.accountId)
|
|
12836
|
-
row.accountId = tokens.accountId;
|
|
12837
|
-
return row;
|
|
12838
|
-
}
|
|
12839
|
-
async function addAccount(providerID, tokens, label) {
|
|
12840
|
-
return mutatePool((pool) => {
|
|
12841
|
-
const intent = pool.relogin?.providerID === providerID ? pool.relogin : undefined;
|
|
12842
|
-
if (intent)
|
|
12843
|
-
delete pool.relogin;
|
|
12844
|
-
const existing = pool.accounts.find((a) => a.providerID === providerID && (tokens.refresh && a.refresh === tokens.refresh || tokens.accountId && a.accountId === tokens.accountId));
|
|
12845
|
-
if (existing)
|
|
12846
|
-
return applyTokens(existing, tokens);
|
|
12847
|
-
const target = intent ? pool.accounts.find((account) => account.id === intent.accountId) : undefined;
|
|
12848
|
-
if (target)
|
|
12849
|
-
return applyTokens(target, tokens);
|
|
12850
|
-
const used = new Set(pool.accounts.map((a) => a.label));
|
|
12851
|
-
let n = 1;
|
|
12852
|
-
while (used.has(`${providerID}-${n}`))
|
|
12853
|
-
n++;
|
|
12854
|
-
const account = makeAccount(providerID, label ?? `${providerID}-${n}`, tokens);
|
|
12855
|
-
pool.accounts.push(account);
|
|
12856
|
-
return account;
|
|
12857
|
-
});
|
|
12858
|
-
}
|
|
12859
|
-
function importableTokens(auth, tokensFromApiKey) {
|
|
12860
|
-
if (auth.type === "api")
|
|
12861
|
-
return tokensFromApiKey && auth.key ? tokensFromApiKey(auth.key) : null;
|
|
12862
|
-
if (auth.type !== "oauth" || !auth.access || !auth.refresh)
|
|
12863
|
-
return null;
|
|
12864
|
-
return {
|
|
12865
|
-
access: auth.access,
|
|
12866
|
-
refresh: auth.refresh,
|
|
12867
|
-
expires: auth.expires ?? 0
|
|
12868
|
-
};
|
|
12869
|
-
}
|
|
12870
|
-
async function bootstrapFromOpencodeAuth(providerID, getAuth, tokensFromApiKey) {
|
|
12871
|
-
const auth = await getAuth().catch(() => null);
|
|
12872
|
-
const tokens = auth && importableTokens(auth, tokensFromApiKey);
|
|
12873
|
-
if (!tokens)
|
|
12874
|
-
return;
|
|
12875
|
-
if ((await readPool()).accounts.some((a) => a.providerID === providerID))
|
|
12876
|
-
return;
|
|
12877
|
-
await mutatePool((pool) => {
|
|
12878
|
-
if (pool.accounts.some((a) => a.providerID === providerID))
|
|
12879
|
-
return;
|
|
12880
|
-
pool.accounts.push(makeAccount(providerID, `${providerID}-1`, tokens));
|
|
12881
|
-
});
|
|
12882
|
-
}
|
|
12883
|
-
|
|
12884
12916
|
// src/scheduler/score-core.ts
|
|
12885
12917
|
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
12886
|
-
var
|
|
12918
|
+
var WEEK_MS2 = 7 * DAY_MS;
|
|
12887
12919
|
var HOUR_MS = 60 * 60 * 1000;
|
|
12888
12920
|
var HOURLY_WINDOW_MS = 5 * HOUR_MS;
|
|
12889
12921
|
var RESET_CUSHION_DAYS = 0.25;
|
|
12890
12922
|
var SCORE_DEFAULTS = {
|
|
12891
12923
|
hourlyInfluence: 0.5,
|
|
12892
12924
|
minResetMs: 5 * 60 * 1000,
|
|
12893
|
-
weekWindowMs:
|
|
12925
|
+
weekWindowMs: WEEK_MS2,
|
|
12894
12926
|
exhaustedAt: 0.999,
|
|
12895
12927
|
migrateAt: 0.95,
|
|
12896
12928
|
weeklyDrainTarget: 0.98
|
|
@@ -12952,7 +12984,7 @@ function weeklyUrgency(account, cfg, now) {
|
|
|
12952
12984
|
if (drainable === 0)
|
|
12953
12985
|
return 0;
|
|
12954
12986
|
const resetAt = weekly?.resetAt ?? 0;
|
|
12955
|
-
const ms = resetAt > now ? resetAt - now : weekly ? resetAt === 0 ? cfg.minResetMs :
|
|
12987
|
+
const ms = resetAt > now ? resetAt - now : weekly ? resetAt === 0 ? cfg.minResetMs : WEEK_MS2 - (now - resetAt) % WEEK_MS2 : cfg.weekWindowMs;
|
|
12956
12988
|
const days = Math.max(ms, cfg.minResetMs) / DAY_MS + RESET_CUSHION_DAYS;
|
|
12957
12989
|
return drainable / (days * days);
|
|
12958
12990
|
}
|
|
@@ -12980,6 +13012,157 @@ function compareRanked(x, y) {
|
|
|
12980
13012
|
return x.weeklyUtil - y.weeklyUtil;
|
|
12981
13013
|
}
|
|
12982
13014
|
|
|
13015
|
+
// src/usage-merge.ts
|
|
13016
|
+
function rollWeeklyAnchorForward(anchor, now) {
|
|
13017
|
+
if (!Number.isFinite(anchor) || anchor <= 0)
|
|
13018
|
+
return 0;
|
|
13019
|
+
if (anchor > now)
|
|
13020
|
+
return anchor;
|
|
13021
|
+
const periods = Math.floor((now - anchor) / WEEK_MS2) + 1;
|
|
13022
|
+
return anchor + periods * WEEK_MS2;
|
|
13023
|
+
}
|
|
13024
|
+
function preserveWeeklyAnchor(incoming, stored, now) {
|
|
13025
|
+
if (!incoming || incoming.resetAt > 0)
|
|
13026
|
+
return incoming;
|
|
13027
|
+
const anchor = rollWeeklyAnchorForward(stored?.resetAt ?? 0, now);
|
|
13028
|
+
if (anchor === 0)
|
|
13029
|
+
return incoming;
|
|
13030
|
+
return { utilization: incoming.utilization, resetAt: anchor };
|
|
13031
|
+
}
|
|
13032
|
+
|
|
13033
|
+
// src/accounts.ts
|
|
13034
|
+
function makeAccount(providerID, label, tokens) {
|
|
13035
|
+
const account = {
|
|
13036
|
+
id: randomUUID2(),
|
|
13037
|
+
providerID,
|
|
13038
|
+
label,
|
|
13039
|
+
access: tokens.access,
|
|
13040
|
+
refresh: tokens.refresh,
|
|
13041
|
+
expires: tokens.expires,
|
|
13042
|
+
tokenGen: 0,
|
|
13043
|
+
accountId: tokens.accountId ?? null,
|
|
13044
|
+
usage: tokens.usage ?? emptyUsage(),
|
|
13045
|
+
cooldownUntil: 0,
|
|
13046
|
+
disabledReason: null
|
|
13047
|
+
};
|
|
13048
|
+
if (tokens.inferenceOnly) {
|
|
13049
|
+
account.inferenceToken = tokens.access;
|
|
13050
|
+
if (tokens.inferenceExpires)
|
|
13051
|
+
account.inferenceExpires = tokens.inferenceExpires;
|
|
13052
|
+
}
|
|
13053
|
+
if (tokens.orgId)
|
|
13054
|
+
account.orgId = tokens.orgId;
|
|
13055
|
+
if (tokens.refreshExpires)
|
|
13056
|
+
account.refreshExpires = tokens.refreshExpires;
|
|
13057
|
+
return account;
|
|
13058
|
+
}
|
|
13059
|
+
function recordLostLogin(row, login, reason) {
|
|
13060
|
+
row.lostLogins = { ...row.lostLogins, [login]: { at: Date.now(), reason } };
|
|
13061
|
+
}
|
|
13062
|
+
function clearLostLogin(row, login) {
|
|
13063
|
+
if (!row.lostLogins?.[login])
|
|
13064
|
+
return;
|
|
13065
|
+
delete row.lostLogins[login];
|
|
13066
|
+
if (!row.lostLogins.oauth && !row.lostLogins.token)
|
|
13067
|
+
delete row.lostLogins;
|
|
13068
|
+
}
|
|
13069
|
+
function mergeLoginUsage(row, usage) {
|
|
13070
|
+
if (!usage)
|
|
13071
|
+
return;
|
|
13072
|
+
row.usage = {
|
|
13073
|
+
hourly: usage.hourly ?? row.usage.hourly,
|
|
13074
|
+
weekly: usage.weekly ? preserveWeeklyAnchor(usage.weekly, row.usage.weekly, usage.capturedAt) : row.usage.weekly,
|
|
13075
|
+
capturedAt: usage.weekly ? usage.capturedAt : row.usage.capturedAt
|
|
13076
|
+
};
|
|
13077
|
+
}
|
|
13078
|
+
function applyTokens(row, tokens) {
|
|
13079
|
+
if (tokens.refresh)
|
|
13080
|
+
row.refresh = tokens.refresh;
|
|
13081
|
+
row.access = tokens.access;
|
|
13082
|
+
row.expires = tokens.expires;
|
|
13083
|
+
row.disabledReason = null;
|
|
13084
|
+
if (tokens.accountId)
|
|
13085
|
+
row.accountId = tokens.accountId;
|
|
13086
|
+
if (tokens.orgId)
|
|
13087
|
+
row.orgId = tokens.orgId;
|
|
13088
|
+
if (tokens.refreshExpires)
|
|
13089
|
+
row.refreshExpires = tokens.refreshExpires;
|
|
13090
|
+
clearLostLogin(row, "oauth");
|
|
13091
|
+
mergeLoginUsage(row, tokens.usage);
|
|
13092
|
+
return row;
|
|
13093
|
+
}
|
|
13094
|
+
function attachToken(row, tokens) {
|
|
13095
|
+
row.inferenceToken = tokens.access;
|
|
13096
|
+
delete row.inferenceExpires;
|
|
13097
|
+
if (tokens.inferenceExpires)
|
|
13098
|
+
row.inferenceExpires = tokens.inferenceExpires;
|
|
13099
|
+
if (!row.refresh) {
|
|
13100
|
+
row.access = tokens.access;
|
|
13101
|
+
row.expires = tokens.expires;
|
|
13102
|
+
}
|
|
13103
|
+
if (tokens.orgId)
|
|
13104
|
+
row.orgId = tokens.orgId;
|
|
13105
|
+
row.disabledReason = null;
|
|
13106
|
+
clearLostLogin(row, "token");
|
|
13107
|
+
mergeLoginUsage(row, tokens.usage);
|
|
13108
|
+
return row;
|
|
13109
|
+
}
|
|
13110
|
+
async function dropInferenceToken(accountId, token, reason) {
|
|
13111
|
+
await mutatePool((pool) => {
|
|
13112
|
+
const row = findAccount(pool, accountId);
|
|
13113
|
+
if (row?.inferenceToken !== token)
|
|
13114
|
+
return;
|
|
13115
|
+
delete row.inferenceToken;
|
|
13116
|
+
delete row.inferenceExpires;
|
|
13117
|
+
recordLostLogin(row, "token", reason);
|
|
13118
|
+
});
|
|
13119
|
+
}
|
|
13120
|
+
async function addAccount(providerID, tokens, label) {
|
|
13121
|
+
return mutatePool((pool) => {
|
|
13122
|
+
const intent = pool.relogin?.providerID === providerID ? pool.relogin : undefined;
|
|
13123
|
+
if (intent)
|
|
13124
|
+
delete pool.relogin;
|
|
13125
|
+
return placeTokens(pool, providerID, tokens, label, intent?.accountId);
|
|
13126
|
+
});
|
|
13127
|
+
}
|
|
13128
|
+
function placeTokens(pool, providerID, tokens, label, hintedId) {
|
|
13129
|
+
const rows = pool.accounts.filter((a) => a.providerID === providerID);
|
|
13130
|
+
const target = rows.find((a) => holdsCredential(a, tokens)) ?? (hintedId === undefined ? undefined : rows.find((a) => a.id === hintedId)) ?? orgPartner(rows, tokens);
|
|
13131
|
+
if (target)
|
|
13132
|
+
return tokens.inferenceOnly ? attachToken(target, tokens) : applyTokens(target, tokens);
|
|
13133
|
+
const used = new Set(pool.accounts.map((a) => a.label));
|
|
13134
|
+
let n = 1;
|
|
13135
|
+
while (used.has(`${providerID}-${n}`))
|
|
13136
|
+
n++;
|
|
13137
|
+
const account = makeAccount(providerID, label ?? `${providerID}-${n}`, tokens);
|
|
13138
|
+
pool.accounts.push(account);
|
|
13139
|
+
return account;
|
|
13140
|
+
}
|
|
13141
|
+
function importableTokens(auth, tokensFromApiKey) {
|
|
13142
|
+
if (auth.type === "api")
|
|
13143
|
+
return tokensFromApiKey && auth.key ? tokensFromApiKey(auth.key) : null;
|
|
13144
|
+
if (auth.type !== "oauth" || !auth.access || !auth.refresh)
|
|
13145
|
+
return null;
|
|
13146
|
+
return {
|
|
13147
|
+
access: auth.access,
|
|
13148
|
+
refresh: auth.refresh,
|
|
13149
|
+
expires: auth.expires ?? 0
|
|
13150
|
+
};
|
|
13151
|
+
}
|
|
13152
|
+
async function bootstrapFromOpencodeAuth(providerID, getAuth, tokensFromApiKey) {
|
|
13153
|
+
const auth = await getAuth().catch(() => null);
|
|
13154
|
+
const tokens = auth && importableTokens(auth, tokensFromApiKey);
|
|
13155
|
+
if (!tokens)
|
|
13156
|
+
return;
|
|
13157
|
+
if ((await readPool()).accounts.some((a) => a.providerID === providerID))
|
|
13158
|
+
return;
|
|
13159
|
+
await mutatePool((pool) => {
|
|
13160
|
+
if (pool.accounts.some((a) => a.providerID === providerID))
|
|
13161
|
+
return;
|
|
13162
|
+
pool.accounts.push(makeAccount(providerID, `${providerID}-1`, tokens));
|
|
13163
|
+
});
|
|
13164
|
+
}
|
|
13165
|
+
|
|
12983
13166
|
// src/pending/recovery.ts
|
|
12984
13167
|
function exhaustedWindows(account, cfg, now) {
|
|
12985
13168
|
const windows = [];
|
|
@@ -13038,6 +13221,11 @@ var AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
|
13038
13221
|
var CODE_CALLBACK_URL = "https://platform.claude.com/oauth/code/callback";
|
|
13039
13222
|
var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
13040
13223
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
13224
|
+
var MESSAGES_URL = "https://api.anthropic.com/v1/messages";
|
|
13225
|
+
var ANTHROPIC_VERSION = "2023-06-01";
|
|
13226
|
+
var USAGE_PROBE_MODEL = "claude-haiku-4-5";
|
|
13227
|
+
var SETUP_TOKEN_DOCS_URL = "https://code.claude.com/docs/en/authentication#generate-a-long-lived-token";
|
|
13228
|
+
var ORGANIZATION_ID_HEADER = "anthropic-organization-id";
|
|
13041
13229
|
var OAUTH_SCOPES = [
|
|
13042
13230
|
"org:create_api_key",
|
|
13043
13231
|
"user:profile",
|
|
@@ -13046,6 +13234,8 @@ var OAUTH_SCOPES = [
|
|
|
13046
13234
|
"user:mcp_servers",
|
|
13047
13235
|
"user:file_upload"
|
|
13048
13236
|
];
|
|
13237
|
+
var INFERENCE_SCOPE = "user:inference";
|
|
13238
|
+
var INFERENCE_TOKEN_LIFETIME_S = 365 * 24 * 60 * 60;
|
|
13049
13239
|
var TOOL_PREFIX = "mcp_";
|
|
13050
13240
|
var DEFAULT_OPUS_FALLBACK_MODEL = "claude-sonnet-4-6";
|
|
13051
13241
|
var DEFAULT_FAMILY_ORDER = [
|
|
@@ -13132,6 +13322,11 @@ function isInvalidGrant(error) {
|
|
|
13132
13322
|
}
|
|
13133
13323
|
return error.message.includes("invalid_grant");
|
|
13134
13324
|
}
|
|
13325
|
+
var REFRESH_FAILURE_RE = /^Token refresh failed: (\d+) — ([\s\S]*)$/;
|
|
13326
|
+
function refusalReason(error) {
|
|
13327
|
+
const m = REFRESH_FAILURE_RE.exec(error.message);
|
|
13328
|
+
return m ? errorSummary(Number(m[1]), m[2] ?? "") : error.message;
|
|
13329
|
+
}
|
|
13135
13330
|
function genOf(account) {
|
|
13136
13331
|
return account.tokenGen ?? 0;
|
|
13137
13332
|
}
|
|
@@ -13155,10 +13350,15 @@ function applyTokensTo(account, tokens) {
|
|
|
13155
13350
|
account.expires = tokens.expires;
|
|
13156
13351
|
if (tokens.accountId)
|
|
13157
13352
|
account.accountId = tokens.accountId;
|
|
13353
|
+
if (tokens.refreshExpires)
|
|
13354
|
+
account.refreshExpires = tokens.refreshExpires;
|
|
13158
13355
|
}
|
|
13159
13356
|
function refreshLockDir(providerID, accountId) {
|
|
13160
13357
|
return `${poolFilePath()}.refresh.${providerID}.${accountId}.lock`;
|
|
13161
13358
|
}
|
|
13359
|
+
function withRefreshLock(providerID, accountId, fn) {
|
|
13360
|
+
return withLock(refreshLockDir(providerID, accountId), REFRESH_LOCK, fn);
|
|
13361
|
+
}
|
|
13162
13362
|
async function commitRefresh(accountId, attempt, next) {
|
|
13163
13363
|
return mutatePool((pool) => {
|
|
13164
13364
|
const stored = findAccount(pool, accountId);
|
|
@@ -13168,17 +13368,27 @@ async function commitRefresh(accountId, attempt, next) {
|
|
|
13168
13368
|
return tokensOf(stored);
|
|
13169
13369
|
applyTokensTo(stored, next);
|
|
13170
13370
|
stored.tokenGen = attempt.gen + 1;
|
|
13171
|
-
stored.disabledReason
|
|
13371
|
+
if (stored.disabledReason !== MANUAL_DISABLED_REASON)
|
|
13372
|
+
stored.disabledReason = null;
|
|
13172
13373
|
return next;
|
|
13173
13374
|
});
|
|
13174
13375
|
}
|
|
13175
|
-
async function resolveInvalidGrant(adapter, accountId, attempt) {
|
|
13376
|
+
async function resolveInvalidGrant(adapter, accountId, attempt, reason) {
|
|
13176
13377
|
return mutatePool((pool) => {
|
|
13177
13378
|
const stored = findAccount(pool, accountId);
|
|
13178
13379
|
if (!stored)
|
|
13179
13380
|
return null;
|
|
13180
13381
|
if (!sameGeneration(stored, attempt))
|
|
13181
13382
|
return tokensOf(stored);
|
|
13383
|
+
recordLostLogin(stored, "oauth", reason);
|
|
13384
|
+
if (stored.inferenceToken !== undefined) {
|
|
13385
|
+
stored.access = stored.inferenceToken;
|
|
13386
|
+
stored.refresh = "";
|
|
13387
|
+
stored.expires = STATIC_CREDENTIAL_EXPIRES;
|
|
13388
|
+
stored.tokenGen = attempt.gen + 1;
|
|
13389
|
+
delete stored.refreshExpires;
|
|
13390
|
+
return tokensOf(stored);
|
|
13391
|
+
}
|
|
13182
13392
|
stored.disabledReason = disableReason(adapter, stored.label);
|
|
13183
13393
|
return null;
|
|
13184
13394
|
});
|
|
@@ -13196,9 +13406,9 @@ async function runRefresh(adapter, account, now) {
|
|
|
13196
13406
|
const next = await adapter.refresh(attempt.refresh);
|
|
13197
13407
|
return await commitRefresh(account.id, attempt, next);
|
|
13198
13408
|
} catch (error) {
|
|
13199
|
-
if (!isInvalidGrant(error))
|
|
13409
|
+
if (!(error instanceof Error) || !isInvalidGrant(error))
|
|
13200
13410
|
throw error;
|
|
13201
|
-
const adopted = await resolveInvalidGrant(adapter, account.id, attempt);
|
|
13411
|
+
const adopted = await resolveInvalidGrant(adapter, account.id, attempt, refusalReason(error));
|
|
13202
13412
|
if (adopted)
|
|
13203
13413
|
return adopted;
|
|
13204
13414
|
throw error;
|
|
@@ -13394,22 +13604,81 @@ function deriveSessionKey(headers, body) {
|
|
|
13394
13604
|
}
|
|
13395
13605
|
}
|
|
13396
13606
|
|
|
13397
|
-
// src/
|
|
13398
|
-
|
|
13399
|
-
|
|
13400
|
-
|
|
13401
|
-
|
|
13402
|
-
|
|
13403
|
-
|
|
13404
|
-
|
|
13607
|
+
// src/token-mint.ts
|
|
13608
|
+
var DAY_MS2 = 24 * 60 * 60 * 1000;
|
|
13609
|
+
var RENEW_WITHIN_MS = 30 * DAY_MS2;
|
|
13610
|
+
var MIN_LIFETIME_MS = 2 * RENEW_WITHIN_MS;
|
|
13611
|
+
var RETRY_MS = 6 * 60 * 60 * 1000;
|
|
13612
|
+
var LAST_ATTEMPT_MAX = 256;
|
|
13613
|
+
var lastAttempt = new Map;
|
|
13614
|
+
function autoTokenEnabled(env = process.env) {
|
|
13615
|
+
const raw = env.OPENCODE_AUTH_LB_ANTHROPIC_AUTO_TOKEN?.trim().toLowerCase();
|
|
13616
|
+
return raw !== "0" && raw !== "false" && raw !== "no" && raw !== "off";
|
|
13617
|
+
}
|
|
13618
|
+
function tokenDue(account, now) {
|
|
13619
|
+
if (!account.refresh || account.disabledReason)
|
|
13620
|
+
return false;
|
|
13621
|
+
if (account.inferenceToken === undefined)
|
|
13622
|
+
return true;
|
|
13623
|
+
return account.inferenceExpires !== undefined && account.inferenceExpires - now <= RENEW_WITHIN_MS;
|
|
13405
13624
|
}
|
|
13406
|
-
function
|
|
13407
|
-
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
|
|
13411
|
-
|
|
13412
|
-
|
|
13625
|
+
async function mintToken(adapter, accountId) {
|
|
13626
|
+
const mint = adapter.mintInferenceToken;
|
|
13627
|
+
if (!mint)
|
|
13628
|
+
return false;
|
|
13629
|
+
return withRefreshLock(adapter.id, accountId, async () => {
|
|
13630
|
+
const latest = await readPoolAccount(accountId);
|
|
13631
|
+
if (!latest || !tokenDue(latest, Date.now()))
|
|
13632
|
+
return false;
|
|
13633
|
+
const attempt = {
|
|
13634
|
+
refresh: latest.refresh,
|
|
13635
|
+
gen: genOf(latest)
|
|
13636
|
+
};
|
|
13637
|
+
const minted = await mint(attempt.refresh);
|
|
13638
|
+
return mutatePool((pool) => {
|
|
13639
|
+
const row = findAccount(pool, accountId);
|
|
13640
|
+
if (!row || !sameGeneration(row, attempt))
|
|
13641
|
+
return false;
|
|
13642
|
+
if (minted.refresh !== row.refresh) {
|
|
13643
|
+
row.refresh = minted.refresh;
|
|
13644
|
+
row.tokenGen = attempt.gen + 1;
|
|
13645
|
+
}
|
|
13646
|
+
if (minted.refreshExpires)
|
|
13647
|
+
row.refreshExpires = minted.refreshExpires;
|
|
13648
|
+
const now = Date.now();
|
|
13649
|
+
if (minted.expires - now < MIN_LIFETIME_MS || !tokenDue(row, now))
|
|
13650
|
+
return false;
|
|
13651
|
+
row.inferenceToken = minted.access;
|
|
13652
|
+
row.inferenceExpires = minted.expires;
|
|
13653
|
+
clearLostLogin(row, "token");
|
|
13654
|
+
return true;
|
|
13655
|
+
});
|
|
13656
|
+
});
|
|
13657
|
+
}
|
|
13658
|
+
async function maintainTokens(adapter, now, poolSnapshot) {
|
|
13659
|
+
if (!adapter.mintInferenceToken || !autoTokenEnabled())
|
|
13660
|
+
return;
|
|
13661
|
+
const pool = poolSnapshot ?? await readPool();
|
|
13662
|
+
let due;
|
|
13663
|
+
for (const account of pool.accounts) {
|
|
13664
|
+
if (account.providerID !== adapter.id || !tokenDue(account, now))
|
|
13665
|
+
continue;
|
|
13666
|
+
const last = lastAttempt.get(account.id);
|
|
13667
|
+
if (last !== undefined && now - last < RETRY_MS)
|
|
13668
|
+
continue;
|
|
13669
|
+
setBounded(lastAttempt, account.id, now, LAST_ATTEMPT_MAX);
|
|
13670
|
+
due ??= [];
|
|
13671
|
+
due.push(account.id);
|
|
13672
|
+
}
|
|
13673
|
+
if (due)
|
|
13674
|
+
await Promise.all(due.map((id) => mintToken(adapter, id).catch(ignore)));
|
|
13675
|
+
}
|
|
13676
|
+
async function mintAtLogin(adapter, account) {
|
|
13677
|
+
const now = Date.now();
|
|
13678
|
+
if (!adapter.mintInferenceToken || !autoTokenEnabled() || !tokenDue(account, now))
|
|
13679
|
+
return false;
|
|
13680
|
+
setBounded(lastAttempt, account.id, now, LAST_ATTEMPT_MAX);
|
|
13681
|
+
return mintToken(adapter, account.id).catch(() => false);
|
|
13413
13682
|
}
|
|
13414
13683
|
|
|
13415
13684
|
// src/providers/types.ts
|
|
@@ -13660,6 +13929,24 @@ async function postToken(body) {
|
|
|
13660
13929
|
signal: AbortSignal.timeout(OAUTH_HTTP_TIMEOUT_MS)
|
|
13661
13930
|
});
|
|
13662
13931
|
}
|
|
13932
|
+
var ASSUMED_LOGIN_LIFETIME_MS = 30 * 24 * 60 * 60 * 1000;
|
|
13933
|
+
function uuidOf(value) {
|
|
13934
|
+
return isPlainObject2(value) && typeof value.uuid === "string" && value.uuid ? value.uuid : undefined;
|
|
13935
|
+
}
|
|
13936
|
+
function withAccount(tokens, json, atLogin) {
|
|
13937
|
+
const accountId = uuidOf(json.account);
|
|
13938
|
+
const orgId = uuidOf(json.organization);
|
|
13939
|
+
if (accountId)
|
|
13940
|
+
tokens.accountId = accountId;
|
|
13941
|
+
if (orgId)
|
|
13942
|
+
tokens.orgId = orgId;
|
|
13943
|
+
const lifetime = json.refresh_token_expires_in;
|
|
13944
|
+
if (isFiniteNumber(lifetime) && lifetime > 0)
|
|
13945
|
+
tokens.refreshExpires = Date.now() + lifetime * 1000;
|
|
13946
|
+
else if (atLogin)
|
|
13947
|
+
tokens.refreshExpires = Date.now() + ASSUMED_LOGIN_LIFETIME_MS;
|
|
13948
|
+
return tokens;
|
|
13949
|
+
}
|
|
13663
13950
|
async function authorize() {
|
|
13664
13951
|
const pkce = await generatePKCE();
|
|
13665
13952
|
const state = generateState();
|
|
@@ -13696,7 +13983,7 @@ async function exchange(input, verifier, redirectUri, expectedState) {
|
|
|
13696
13983
|
const json = await readExchangeResponse(result);
|
|
13697
13984
|
if (!json)
|
|
13698
13985
|
return null;
|
|
13699
|
-
return toTokenSet(json, "");
|
|
13986
|
+
return withAccount(toTokenSet(json, ""), json, true);
|
|
13700
13987
|
}
|
|
13701
13988
|
async function refresh(refreshToken) {
|
|
13702
13989
|
const response = await postToken({
|
|
@@ -13705,36 +13992,78 @@ async function refresh(refreshToken) {
|
|
|
13705
13992
|
client_id: CLIENT_ID
|
|
13706
13993
|
});
|
|
13707
13994
|
const json = await readRefreshResponse(response);
|
|
13708
|
-
return toTokenSet(json, refreshToken);
|
|
13995
|
+
return withAccount(toTokenSet(json, refreshToken), json, false);
|
|
13709
13996
|
}
|
|
13710
|
-
|
|
13711
|
-
|
|
13712
|
-
|
|
13713
|
-
|
|
13714
|
-
|
|
13715
|
-
|
|
13716
|
-
|
|
13717
|
-
|
|
13718
|
-
|
|
13719
|
-
|
|
13720
|
-
try {
|
|
13721
|
-
response = await fetch(url, {
|
|
13722
|
-
headers,
|
|
13723
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
13724
|
-
});
|
|
13725
|
-
} catch {
|
|
13726
|
-
return null;
|
|
13727
|
-
}
|
|
13728
|
-
if (!response.ok) {
|
|
13729
|
-
await response.body?.cancel().catch(ignore);
|
|
13730
|
-
return null;
|
|
13731
|
-
}
|
|
13732
|
-
return await response.json().catch(() => null);
|
|
13997
|
+
async function mintInferenceToken(refreshToken) {
|
|
13998
|
+
const response = await postToken({
|
|
13999
|
+
grant_type: "refresh_token",
|
|
14000
|
+
refresh_token: refreshToken,
|
|
14001
|
+
client_id: CLIENT_ID,
|
|
14002
|
+
scope: INFERENCE_SCOPE,
|
|
14003
|
+
expires_in: INFERENCE_TOKEN_LIFETIME_S
|
|
14004
|
+
});
|
|
14005
|
+
const json = await readRefreshResponse(response);
|
|
14006
|
+
return withAccount(toTokenSet(json, refreshToken), json, false);
|
|
13733
14007
|
}
|
|
13734
14008
|
|
|
13735
|
-
// src/providers/
|
|
13736
|
-
|
|
13737
|
-
|
|
14009
|
+
// src/providers/usage-headers.ts
|
|
14010
|
+
function headerWindow(utilRaw, resetRaw, divisor) {
|
|
14011
|
+
const util = Number(utilRaw);
|
|
14012
|
+
if (!Number.isFinite(util))
|
|
14013
|
+
return null;
|
|
14014
|
+
return {
|
|
14015
|
+
utilization: clamp01(util / divisor),
|
|
14016
|
+
resetAt: secondsToMs(Number(resetRaw))
|
|
14017
|
+
};
|
|
14018
|
+
}
|
|
14019
|
+
function parseWindowPairHeaders(headers, spec) {
|
|
14020
|
+
const h = headers.get(spec.hourlyUtil);
|
|
14021
|
+
const w = headers.get(spec.weeklyUtil);
|
|
14022
|
+
if (h === null && w === null)
|
|
14023
|
+
return null;
|
|
14024
|
+
const out = {};
|
|
14025
|
+
const hourly = h === null ? null : headerWindow(h, headers.get(spec.hourlyReset), spec.divisor);
|
|
14026
|
+
const weekly = w === null ? null : headerWindow(w, headers.get(spec.weeklyReset), spec.divisor);
|
|
14027
|
+
if (hourly)
|
|
14028
|
+
out.hourly = hourly;
|
|
14029
|
+
if (weekly)
|
|
14030
|
+
out.weekly = weekly;
|
|
14031
|
+
return hourly || weekly ? out : null;
|
|
14032
|
+
}
|
|
14033
|
+
function endpointWindowFrom(w, utilOf, divisor, resetOf) {
|
|
14034
|
+
if (!w)
|
|
14035
|
+
return { utilization: 0, resetAt: 0 };
|
|
14036
|
+
const util = utilOf(w);
|
|
14037
|
+
if (!isFiniteNumber(util))
|
|
14038
|
+
return null;
|
|
14039
|
+
return { utilization: clamp01(util / divisor), resetAt: resetOf(w) };
|
|
14040
|
+
}
|
|
14041
|
+
|
|
14042
|
+
// src/providers/usage-http.ts
|
|
14043
|
+
async function fetchJson(url, headers, timeoutMs) {
|
|
14044
|
+
let response;
|
|
14045
|
+
try {
|
|
14046
|
+
response = await fetch(url, {
|
|
14047
|
+
headers,
|
|
14048
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
14049
|
+
});
|
|
14050
|
+
} catch {
|
|
14051
|
+
return null;
|
|
14052
|
+
}
|
|
14053
|
+
if (!response.ok) {
|
|
14054
|
+
await response.body?.cancel().catch(ignore);
|
|
14055
|
+
return null;
|
|
14056
|
+
}
|
|
14057
|
+
return await response.json().catch(() => null);
|
|
14058
|
+
}
|
|
14059
|
+
|
|
14060
|
+
// src/providers/anthropic/cch.ts
|
|
14061
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
14062
|
+
|
|
14063
|
+
// src/providers/anthropic/version.ts
|
|
14064
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
14065
|
+
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
14066
|
+
function compareVersions2(a, b) {
|
|
13738
14067
|
const left = a.split(".");
|
|
13739
14068
|
const right = b.split(".");
|
|
13740
14069
|
for (let i = 0;i < 3; i++) {
|
|
@@ -14086,39 +14415,6 @@ function createStrippedStream(response) {
|
|
|
14086
14415
|
});
|
|
14087
14416
|
}
|
|
14088
14417
|
|
|
14089
|
-
// src/providers/usage-headers.ts
|
|
14090
|
-
function headerWindow(utilRaw, resetRaw, divisor) {
|
|
14091
|
-
const util = Number(utilRaw);
|
|
14092
|
-
if (!Number.isFinite(util))
|
|
14093
|
-
return null;
|
|
14094
|
-
return {
|
|
14095
|
-
utilization: clamp01(util / divisor),
|
|
14096
|
-
resetAt: secondsToMs(Number(resetRaw))
|
|
14097
|
-
};
|
|
14098
|
-
}
|
|
14099
|
-
function parseWindowPairHeaders(headers, spec) {
|
|
14100
|
-
const h = headers.get(spec.hourlyUtil);
|
|
14101
|
-
const w = headers.get(spec.weeklyUtil);
|
|
14102
|
-
if (h === null && w === null)
|
|
14103
|
-
return null;
|
|
14104
|
-
const out = {};
|
|
14105
|
-
const hourly = h === null ? null : headerWindow(h, headers.get(spec.hourlyReset), spec.divisor);
|
|
14106
|
-
const weekly = w === null ? null : headerWindow(w, headers.get(spec.weeklyReset), spec.divisor);
|
|
14107
|
-
if (hourly)
|
|
14108
|
-
out.hourly = hourly;
|
|
14109
|
-
if (weekly)
|
|
14110
|
-
out.weekly = weekly;
|
|
14111
|
-
return hourly || weekly ? out : null;
|
|
14112
|
-
}
|
|
14113
|
-
function endpointWindowFrom(w, utilOf, divisor, resetOf) {
|
|
14114
|
-
if (!w)
|
|
14115
|
-
return { utilization: 0, resetAt: 0 };
|
|
14116
|
-
const util = utilOf(w);
|
|
14117
|
-
if (!isFiniteNumber(util))
|
|
14118
|
-
return null;
|
|
14119
|
-
return { utilization: clamp01(util / divisor), resetAt: resetOf(w) };
|
|
14120
|
-
}
|
|
14121
|
-
|
|
14122
14418
|
// src/providers/anthropic/usage.ts
|
|
14123
14419
|
var HEADER_SPEC = {
|
|
14124
14420
|
hourlyUtil: "anthropic-ratelimit-unified-5h-utilization",
|
|
@@ -14150,7 +14446,46 @@ function parseResetAt(value) {
|
|
|
14150
14446
|
function endpointWindow(w) {
|
|
14151
14447
|
return endpointWindowFrom(w, (win) => win.utilization, 100, (win) => parseResetAt(win.resets_at));
|
|
14152
14448
|
}
|
|
14449
|
+
function sendUsageProbe(access) {
|
|
14450
|
+
const headers = new Headers({
|
|
14451
|
+
"content-type": "application/json",
|
|
14452
|
+
"anthropic-version": ANTHROPIC_VERSION
|
|
14453
|
+
});
|
|
14454
|
+
setOAuthHeaders(headers, access);
|
|
14455
|
+
return fetch(rewriteUrl(MESSAGES_URL), {
|
|
14456
|
+
method: "POST",
|
|
14457
|
+
headers,
|
|
14458
|
+
body: rewriteRequestBody(JSON.stringify({
|
|
14459
|
+
model: USAGE_PROBE_MODEL,
|
|
14460
|
+
max_tokens: 1,
|
|
14461
|
+
messages: [{ role: "user", content: "quota" }]
|
|
14462
|
+
})),
|
|
14463
|
+
signal: AbortSignal.timeout(USAGE_HTTP_TIMEOUT_MS)
|
|
14464
|
+
});
|
|
14465
|
+
}
|
|
14466
|
+
function probeSnapshot(headers, now) {
|
|
14467
|
+
const partial = parseUsageHeaders(headers);
|
|
14468
|
+
if (!partial)
|
|
14469
|
+
return null;
|
|
14470
|
+
return {
|
|
14471
|
+
hourly: partial.hourly ?? null,
|
|
14472
|
+
weekly: partial.weekly ?? null,
|
|
14473
|
+
capturedAt: partial.weekly ? now : 0
|
|
14474
|
+
};
|
|
14475
|
+
}
|
|
14476
|
+
async function probeUsage(access, now) {
|
|
14477
|
+
let res;
|
|
14478
|
+
try {
|
|
14479
|
+
res = await sendUsageProbe(access);
|
|
14480
|
+
} catch {
|
|
14481
|
+
return null;
|
|
14482
|
+
}
|
|
14483
|
+
await res.body?.cancel().catch(ignore);
|
|
14484
|
+
return probeSnapshot(res.headers, now);
|
|
14485
|
+
}
|
|
14153
14486
|
async function fetchUsage(account, now) {
|
|
14487
|
+
if (!account.refresh)
|
|
14488
|
+
return probeUsage(account.access, now);
|
|
14154
14489
|
const json = await fetchJson(USAGE_URL, {
|
|
14155
14490
|
authorization: `Bearer ${account.access}`,
|
|
14156
14491
|
"anthropic-beta": "oauth-2025-04-20",
|
|
@@ -14168,12 +14503,50 @@ async function fetchUsage(account, now) {
|
|
|
14168
14503
|
};
|
|
14169
14504
|
}
|
|
14170
14505
|
|
|
14506
|
+
// src/providers/anthropic/setup-token.ts
|
|
14507
|
+
var SETUP_TOKEN_RE = /^sk-ant-oat\d+-[\w-]+$/;
|
|
14508
|
+
async function exchangeSetupToken(input) {
|
|
14509
|
+
const token = input.replace(/\s+/g, "");
|
|
14510
|
+
if (!SETUP_TOKEN_RE.test(token))
|
|
14511
|
+
return null;
|
|
14512
|
+
let res;
|
|
14513
|
+
try {
|
|
14514
|
+
res = await sendUsageProbe(token);
|
|
14515
|
+
} catch {
|
|
14516
|
+
return null;
|
|
14517
|
+
}
|
|
14518
|
+
await res.body?.cancel().catch(ignore);
|
|
14519
|
+
if (res.status === 401 || res.status === 403)
|
|
14520
|
+
return null;
|
|
14521
|
+
const tokens = {
|
|
14522
|
+
access: token,
|
|
14523
|
+
refresh: "",
|
|
14524
|
+
expires: STATIC_CREDENTIAL_EXPIRES,
|
|
14525
|
+
inferenceOnly: true
|
|
14526
|
+
};
|
|
14527
|
+
const orgId = res.headers.get(ORGANIZATION_ID_HEADER);
|
|
14528
|
+
if (orgId)
|
|
14529
|
+
tokens.orgId = orgId;
|
|
14530
|
+
const usage = probeSnapshot(res.headers, Date.now());
|
|
14531
|
+
if (usage)
|
|
14532
|
+
tokens.usage = usage;
|
|
14533
|
+
return tokens;
|
|
14534
|
+
}
|
|
14535
|
+
var setupTokenLogin = {
|
|
14536
|
+
label: "setup-token",
|
|
14537
|
+
url: SETUP_TOKEN_DOCS_URL,
|
|
14538
|
+
instructions: "Run `claude setup-token`, approve it in the browser, then paste the token it prints (sk-ant-oat01-…) here:",
|
|
14539
|
+
exchange: exchangeSetupToken
|
|
14540
|
+
};
|
|
14541
|
+
|
|
14171
14542
|
// src/providers/anthropic/adapter.ts
|
|
14172
14543
|
var anthropicAdapter = {
|
|
14173
14544
|
id: PROVIDER_ID,
|
|
14174
14545
|
authorize,
|
|
14175
14546
|
exchange,
|
|
14176
14547
|
refresh,
|
|
14548
|
+
tokenLogin: setupTokenLogin,
|
|
14549
|
+
mintInferenceToken,
|
|
14177
14550
|
applyAuth(headers, account) {
|
|
14178
14551
|
setOAuthHeaders(headers, account.access);
|
|
14179
14552
|
},
|
|
@@ -14721,7 +15094,7 @@ async function refreshUsage(adapters, now, poolSnapshot) {
|
|
|
14721
15094
|
let stale;
|
|
14722
15095
|
for (const account of pool.accounts) {
|
|
14723
15096
|
aliveIds?.add(account.id);
|
|
14724
|
-
if (account.disabledReason)
|
|
15097
|
+
if (account.disabledReason && account.disabledReason !== MANUAL_DISABLED_REASON)
|
|
14725
15098
|
continue;
|
|
14726
15099
|
if (account.usage.capturedAt !== 0 && now - account.usage.capturedAt <= USAGE_REFRESH_TTL_MS)
|
|
14727
15100
|
continue;
|
|
@@ -14827,7 +15200,7 @@ async function applyCooldown(accountId, fallbackMs, kind) {
|
|
|
14827
15200
|
}
|
|
14828
15201
|
}));
|
|
14829
15202
|
}
|
|
14830
|
-
async function recordRotation(adapter, accountId, res, fallbackMs, now, kind,
|
|
15203
|
+
async function recordRotation(adapter, accountId, res, fallbackMs, now, kind, revoked) {
|
|
14831
15204
|
const partial = adapter.parseUsageHeaders(res.headers);
|
|
14832
15205
|
const until = cooldownUntilFrom(res, fallbackMs, now);
|
|
14833
15206
|
await bestEffort("rotation", () => mutatePool((pool) => {
|
|
@@ -14840,8 +15213,10 @@ async function recordRotation(adapter, accountId, res, fallbackMs, now, kind, re
|
|
|
14840
15213
|
account.cooldownUntil = until;
|
|
14841
15214
|
account.cooldownKind = kind;
|
|
14842
15215
|
}
|
|
14843
|
-
if (
|
|
14844
|
-
account.disabledReason = `
|
|
15216
|
+
if (revoked && account.access === revoked.key) {
|
|
15217
|
+
account.disabledReason = `credential rejected (401): re-login required (${adapter.id}:${account.label})`;
|
|
15218
|
+
recordLostLogin(account, "token", revoked.reason);
|
|
15219
|
+
}
|
|
14845
15220
|
}));
|
|
14846
15221
|
}
|
|
14847
15222
|
async function recordModelCooldown(accountId, tier, until, partial, now) {
|
|
@@ -14950,6 +15325,7 @@ function createLoadBalancedFetch(adapter, hooks = {}, models = [], pending) {
|
|
|
14950
15325
|
let pendingTierFallback = null;
|
|
14951
15326
|
let tierSkipped = null;
|
|
14952
15327
|
let clientVersionRetried = false;
|
|
15328
|
+
let rejectedTokens = null;
|
|
14953
15329
|
let usageSeeded = false;
|
|
14954
15330
|
const waitDeadline = Date.now() + cfg.maxWaitMs;
|
|
14955
15331
|
for (;; ) {
|
|
@@ -14958,6 +15334,7 @@ function createLoadBalancedFetch(adapter, hooks = {}, models = [], pending) {
|
|
|
14958
15334
|
if (!usageSeeded) {
|
|
14959
15335
|
usageSeeded = true;
|
|
14960
15336
|
refreshAllUsageInBackground(now, pool).catch(ignore);
|
|
15337
|
+
maintainTokens(adapter, now, pool).catch(ignore);
|
|
14961
15338
|
}
|
|
14962
15339
|
if (pendingRef && pending) {
|
|
14963
15340
|
const recovery = classifyProviderRecovery(pool.accounts, adapter.id, cfg, now, USAGE_REFRESH_TTL_MS);
|
|
@@ -15021,10 +15398,12 @@ function createLoadBalancedFetch(adapter, hooks = {}, models = [], pending) {
|
|
|
15021
15398
|
continue;
|
|
15022
15399
|
}
|
|
15023
15400
|
}
|
|
15401
|
+
const token = account.inferenceToken !== undefined && !rejectedTokens?.has(account.inferenceToken) ? account.inferenceToken : undefined;
|
|
15024
15402
|
try {
|
|
15025
|
-
|
|
15403
|
+
if (token === undefined)
|
|
15404
|
+
await ensureAccessToken(adapter, account, now);
|
|
15026
15405
|
const headers = new Headers(baseHeaders);
|
|
15027
|
-
adapter.applyAuth(headers, account);
|
|
15406
|
+
adapter.applyAuth(headers, token === undefined ? account : { ...account, access: token });
|
|
15028
15407
|
if (DEBUG)
|
|
15029
15408
|
log(`-> ${adapter.id} via ${account.label}${sticky ? " (sticky)" : ""}${degraded ? " (degraded)" : ""}`);
|
|
15030
15409
|
const res = await fetch(transformedUrl, {
|
|
@@ -15034,6 +15413,7 @@ function createLoadBalancedFetch(adapter, hooks = {}, models = [], pending) {
|
|
|
15034
15413
|
});
|
|
15035
15414
|
const cls = adapter.classifyError(res.status);
|
|
15036
15415
|
if (cls === "account" || cls === "auth") {
|
|
15416
|
+
const rejection = res.status === 401 ? errorSummary(401, await res.text().catch(() => "")) : "";
|
|
15037
15417
|
await res.body?.cancel().catch(ignore);
|
|
15038
15418
|
if (cls === "account" && typeof currentBody === "string") {
|
|
15039
15419
|
const reactiveNow = Date.now();
|
|
@@ -15051,8 +15431,18 @@ function createLoadBalancedFetch(adapter, hooks = {}, models = [], pending) {
|
|
|
15051
15431
|
continue;
|
|
15052
15432
|
}
|
|
15053
15433
|
}
|
|
15434
|
+
if (res.status === 401 && token !== undefined && account.refresh) {
|
|
15435
|
+
rejectedTokens ??= new Set;
|
|
15436
|
+
rejectedTokens.add(token);
|
|
15437
|
+
await bestEffort("drop-token", () => dropInferenceToken(account.id, token, rejection));
|
|
15438
|
+
tried.delete(account.id);
|
|
15439
|
+
lastError = new Error(`${adapter.id} account "${account.label}" rejected its setup-token`);
|
|
15440
|
+
if (DEBUG)
|
|
15441
|
+
log(`!! ${account.label} setup-token 401 -> retrying on its OAuth login`);
|
|
15442
|
+
continue;
|
|
15443
|
+
}
|
|
15054
15444
|
const ms = cls === "auth" ? AUTH_COOLDOWN_MS : ACCOUNT_COOLDOWN_MS;
|
|
15055
|
-
await recordRotation(adapter, account.id, res, ms, Date.now(), cls === "auth" ? "auth" : "quota", res.status === 401 &&
|
|
15445
|
+
await recordRotation(adapter, account.id, res, ms, Date.now(), cls === "auth" ? "auth" : "quota", res.status === 401 && !account.refresh ? { key: account.access, reason: rejection } : undefined);
|
|
15056
15446
|
if (cls === "account") {
|
|
15057
15447
|
waitableCooldownIds ??= new Set;
|
|
15058
15448
|
waitableCooldownIds.add(account.id);
|
|
@@ -15107,6 +15497,47 @@ function createLoadBalancedFetch(adapter, hooks = {}, models = [], pending) {
|
|
|
15107
15497
|
};
|
|
15108
15498
|
}
|
|
15109
15499
|
|
|
15500
|
+
// src/login-health.ts
|
|
15501
|
+
var DAY_MS3 = 24 * 60 * 60 * 1000;
|
|
15502
|
+
var LOGIN_EXPIRY_WARNING_MS = 3 * DAY_MS3;
|
|
15503
|
+
function credentialTag(account) {
|
|
15504
|
+
if (account.inferenceToken === undefined)
|
|
15505
|
+
return "";
|
|
15506
|
+
return account.refresh ? "token+oauth" : "token";
|
|
15507
|
+
}
|
|
15508
|
+
function loginWarnings(account, now) {
|
|
15509
|
+
if (account.disabledReason)
|
|
15510
|
+
return [];
|
|
15511
|
+
const warnings = [];
|
|
15512
|
+
if (account.lostLogins?.oauth)
|
|
15513
|
+
warnings.push("oauth re-login");
|
|
15514
|
+
if (account.lostLogins?.token)
|
|
15515
|
+
warnings.push("token re-login");
|
|
15516
|
+
const left = (account.refreshExpires ?? 0) - now;
|
|
15517
|
+
if (account.refresh && left > 0 && left <= LOGIN_EXPIRY_WARNING_MS)
|
|
15518
|
+
warnings.push(`oauth expires ${Math.ceil(left / DAY_MS3)}d`);
|
|
15519
|
+
return warnings;
|
|
15520
|
+
}
|
|
15521
|
+
function ago(at, now) {
|
|
15522
|
+
const mins = Math.floor((now - at) / 60000);
|
|
15523
|
+
if (mins < 1)
|
|
15524
|
+
return "just now";
|
|
15525
|
+
if (mins < 120)
|
|
15526
|
+
return `${mins}m ago`;
|
|
15527
|
+
const hrs = Math.floor(mins / 60);
|
|
15528
|
+
return hrs < 48 ? `${hrs}h ago` : `${Math.floor(hrs / 24)}d ago`;
|
|
15529
|
+
}
|
|
15530
|
+
function lostLoginNotes(account, now) {
|
|
15531
|
+
const notes = [];
|
|
15532
|
+
for (const login of ["oauth", "token"]) {
|
|
15533
|
+
const lost = account.lostLogins?.[login];
|
|
15534
|
+
const name = login === "oauth" ? "OAuth login" : "token";
|
|
15535
|
+
if (lost)
|
|
15536
|
+
notes.push(`${name} lost ${ago(lost.at, now)} — ${lost.reason}`);
|
|
15537
|
+
}
|
|
15538
|
+
return notes;
|
|
15539
|
+
}
|
|
15540
|
+
|
|
15110
15541
|
// src/status.ts
|
|
15111
15542
|
var PROVIDER_NAMES = {
|
|
15112
15543
|
anthropic: "Claude",
|
|
@@ -15136,6 +15567,9 @@ function toStatus(account, now, current, available, rank) {
|
|
|
15136
15567
|
cooldownUntil: account.cooldownUntil,
|
|
15137
15568
|
modelCooldownsUntil: account.modelCooldownsUntil ?? {},
|
|
15138
15569
|
disabledReason: account.disabledReason,
|
|
15570
|
+
login: credentialTag(account),
|
|
15571
|
+
warnings: loginWarnings(account, now),
|
|
15572
|
+
lostNotes: lostLoginNotes(account, now),
|
|
15139
15573
|
current,
|
|
15140
15574
|
rank
|
|
15141
15575
|
};
|
|
@@ -15249,8 +15683,12 @@ function renderStatus(providers, now = Date.now()) {
|
|
|
15249
15683
|
lines.push(` # ${"account".padEnd(width + 3)}weekly 5h resets state`);
|
|
15250
15684
|
for (const a of p.accounts) {
|
|
15251
15685
|
const mark = a.current ? "▶" : " ";
|
|
15252
|
-
|
|
15686
|
+
const notes = [a.login, ...a.warnings].filter(Boolean).map((note) => ` · ${note}`).join("");
|
|
15687
|
+
lines.push(` ${String(a.rank).padEnd(2)} ${mark} ${padDisplayEnd(a.label, width)} ${pct(a.weeklyUtil).padStart(5)} ${pct(a.hourlyUtil).padStart(5)} ${relTime(a.weeklyResetAt, now).padStart(7)} ${stateOf(a, now)}${notes}`);
|
|
15253
15688
|
}
|
|
15689
|
+
for (const a of p.accounts)
|
|
15690
|
+
for (const note of a.lostNotes)
|
|
15691
|
+
lines.push(` ! ${a.label}: ${note}`);
|
|
15254
15692
|
lines.push("");
|
|
15255
15693
|
}
|
|
15256
15694
|
return lines.join(`
|
|
@@ -15275,6 +15713,14 @@ async function notifyOnSwitch(client, providerID, account) {
|
|
|
15275
15713
|
duration: 4000
|
|
15276
15714
|
});
|
|
15277
15715
|
}
|
|
15716
|
+
async function notifyPaired(client, providerID, account) {
|
|
15717
|
+
await postToast(client, {
|
|
15718
|
+
title: `${providerName(providerID)} account`,
|
|
15719
|
+
message: `▶ ${account.label} · inference: setup-token · usage: OAuth`,
|
|
15720
|
+
variant: "success",
|
|
15721
|
+
duration: 6000
|
|
15722
|
+
});
|
|
15723
|
+
}
|
|
15278
15724
|
var lastFallbackToasted = new Map;
|
|
15279
15725
|
var LAST_FALLBACK_TOASTED_MAX = 256;
|
|
15280
15726
|
async function notifyModelFallback(client, providerID, account, fromModel, toModel, fromTier) {
|
|
@@ -15851,6 +16297,1193 @@ async function primeInUse(providerID, now) {
|
|
|
15851
16297
|
});
|
|
15852
16298
|
}
|
|
15853
16299
|
|
|
16300
|
+
// src/sync/crypto.ts
|
|
16301
|
+
import {
|
|
16302
|
+
createCipheriv,
|
|
16303
|
+
createDecipheriv,
|
|
16304
|
+
createHash as createHash6,
|
|
16305
|
+
randomBytes
|
|
16306
|
+
} from "node:crypto";
|
|
16307
|
+
|
|
16308
|
+
// src/sync/errors.ts
|
|
16309
|
+
var MESSAGES = {
|
|
16310
|
+
"bad-link": "That is not a gist link with a key (https://gist.github.com/<user>/<id>#<key>).",
|
|
16311
|
+
"not-set-up": "Sync is not set up on this machine.",
|
|
16312
|
+
"bad-blob": "The gist does not hold a sync file.",
|
|
16313
|
+
"bad-version": "The gist was written by a newer version; update this plugin.",
|
|
16314
|
+
"rolled-back": "The gist holds an older snapshot than one already applied; ignoring it.",
|
|
16315
|
+
decrypt: "The gist could not be decrypted: the link key is wrong or the gist was changed.",
|
|
16316
|
+
"too-large": "The gist is larger than a sync file can be.",
|
|
16317
|
+
"rate-limited": "GitHub is rate limiting this machine; trying again later.",
|
|
16318
|
+
"not-found": "The gist no longer exists.",
|
|
16319
|
+
network: "GitHub could not be reached.",
|
|
16320
|
+
"no-auth": "Uploading needs GITHUB_TOKEN or GH_TOKEN (gist scope), or a logged-in gh CLI.",
|
|
16321
|
+
http: "GitHub refused the request.",
|
|
16322
|
+
"write-denied": "This GitHub account cannot update the gist; downloading only."
|
|
16323
|
+
};
|
|
16324
|
+
|
|
16325
|
+
class SyncError extends Error {
|
|
16326
|
+
code;
|
|
16327
|
+
retryAfterMs;
|
|
16328
|
+
constructor(code, retryAfterMs) {
|
|
16329
|
+
super(MESSAGES[code]);
|
|
16330
|
+
this.code = code;
|
|
16331
|
+
this.retryAfterMs = retryAfterMs;
|
|
16332
|
+
this.name = "SyncError";
|
|
16333
|
+
}
|
|
16334
|
+
}
|
|
16335
|
+
function describeSyncError(error) {
|
|
16336
|
+
return error instanceof SyncError ? error.message : "Sync failed.";
|
|
16337
|
+
}
|
|
16338
|
+
|
|
16339
|
+
// src/sync/crypto.ts
|
|
16340
|
+
var BLOB_VERSION = 1;
|
|
16341
|
+
var MAX_BLOB_BYTES = 256 * 1024;
|
|
16342
|
+
var KEY_BYTES = 32;
|
|
16343
|
+
var NONCE_BYTES = 12;
|
|
16344
|
+
var TAG_BYTES = 16;
|
|
16345
|
+
var AAD = Buffer.from("opencode-auth-load-balancer/gist-sync/v1");
|
|
16346
|
+
var KEY_RE = /^[\w-]{43}$/;
|
|
16347
|
+
var B64URL_RE = /^[\w-]+$/;
|
|
16348
|
+
function generateKey() {
|
|
16349
|
+
return randomBytes(KEY_BYTES);
|
|
16350
|
+
}
|
|
16351
|
+
function encodeKey(key) {
|
|
16352
|
+
return key.toString("base64url");
|
|
16353
|
+
}
|
|
16354
|
+
function decodeKey(text) {
|
|
16355
|
+
return KEY_RE.test(text) ? Buffer.from(text, "base64url") : null;
|
|
16356
|
+
}
|
|
16357
|
+
function fingerprint(secret) {
|
|
16358
|
+
return createHash6("sha256").update(secret).digest("hex").slice(0, 32);
|
|
16359
|
+
}
|
|
16360
|
+
function seal(plaintext, key) {
|
|
16361
|
+
const nonce = randomBytes(NONCE_BYTES);
|
|
16362
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
16363
|
+
cipher.setAAD(AAD);
|
|
16364
|
+
const data = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
16365
|
+
return JSON.stringify({
|
|
16366
|
+
v: BLOB_VERSION,
|
|
16367
|
+
n: nonce.toString("base64url"),
|
|
16368
|
+
c: data.toString("base64url"),
|
|
16369
|
+
t: cipher.getAuthTag().toString("base64url")
|
|
16370
|
+
});
|
|
16371
|
+
}
|
|
16372
|
+
function parseBlob(text) {
|
|
16373
|
+
if (Buffer.byteLength(text) > MAX_BLOB_BYTES)
|
|
16374
|
+
throw new SyncError("too-large");
|
|
16375
|
+
let json;
|
|
16376
|
+
try {
|
|
16377
|
+
json = JSON.parse(text);
|
|
16378
|
+
} catch {
|
|
16379
|
+
throw new SyncError("bad-blob");
|
|
16380
|
+
}
|
|
16381
|
+
if (!isPlainObject2(json))
|
|
16382
|
+
throw new SyncError("bad-blob");
|
|
16383
|
+
if (typeof json.v === "number" && json.v !== BLOB_VERSION)
|
|
16384
|
+
throw new SyncError("bad-version");
|
|
16385
|
+
const { n, c, t } = json;
|
|
16386
|
+
if (json.v !== BLOB_VERSION || typeof n !== "string" || typeof c !== "string" || typeof t !== "string" || !B64URL_RE.test(n) || !B64URL_RE.test(c) || !B64URL_RE.test(t))
|
|
16387
|
+
throw new SyncError("bad-blob");
|
|
16388
|
+
const nonce = Buffer.from(n, "base64url");
|
|
16389
|
+
const tag = Buffer.from(t, "base64url");
|
|
16390
|
+
if (nonce.length !== NONCE_BYTES || tag.length !== TAG_BYTES)
|
|
16391
|
+
throw new SyncError("bad-blob");
|
|
16392
|
+
return { nonce, data: Buffer.from(c, "base64url"), tag };
|
|
16393
|
+
}
|
|
16394
|
+
function open(text, key) {
|
|
16395
|
+
const { nonce, data, tag } = parseBlob(text);
|
|
16396
|
+
try {
|
|
16397
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
16398
|
+
decipher.setAAD(AAD);
|
|
16399
|
+
decipher.setAuthTag(tag);
|
|
16400
|
+
return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf8");
|
|
16401
|
+
} catch {
|
|
16402
|
+
throw new SyncError("decrypt");
|
|
16403
|
+
}
|
|
16404
|
+
}
|
|
16405
|
+
|
|
16406
|
+
// src/sync/payload.ts
|
|
16407
|
+
var PAYLOAD_VERSION = 2;
|
|
16408
|
+
var MAX_ENTRIES = 64;
|
|
16409
|
+
var MAX_LABEL = 80;
|
|
16410
|
+
var MAX_SECRET = 512;
|
|
16411
|
+
var MAX_FIELD = 128;
|
|
16412
|
+
var ID_RE = /^[\w-]{1,64}$/;
|
|
16413
|
+
var ORIGIN_RE = /^[\da-f]{32}$/;
|
|
16414
|
+
var CLAUDE_TOKEN_RE = /^sk-ant-oat\d+-[\w-]+$/;
|
|
16415
|
+
var API_KEY_RE = /^\S{8,512}$/;
|
|
16416
|
+
var SECRET_SHAPES = new Map([
|
|
16417
|
+
["anthropic", CLAUDE_TOKEN_RE],
|
|
16418
|
+
["kimi-code-plan-cn", API_KEY_RE],
|
|
16419
|
+
["kimi-code-plan-global", API_KEY_RE]
|
|
16420
|
+
]);
|
|
16421
|
+
function entryKey(entry) {
|
|
16422
|
+
return `${entry.origin}/${entry.id}`;
|
|
16423
|
+
}
|
|
16424
|
+
function isControl(code) {
|
|
16425
|
+
return code < 32 || code >= 127 && code <= 159 || code === 1564 || code >= 8203 && code <= 8207 || code >= 8232 && code <= 8238 || code >= 8288 && code <= 8297 || code === 65279;
|
|
16426
|
+
}
|
|
16427
|
+
function printable(text) {
|
|
16428
|
+
return Array.from(text, (ch) => isControl(ch.codePointAt(0) ?? 0) ? " " : ch).join("").trim();
|
|
16429
|
+
}
|
|
16430
|
+
function boundedString(value, max) {
|
|
16431
|
+
return typeof value === "string" && value.length > 0 && value.length <= max ? value : undefined;
|
|
16432
|
+
}
|
|
16433
|
+
function keyOf(raw) {
|
|
16434
|
+
if (!isPlainObject2(raw))
|
|
16435
|
+
return null;
|
|
16436
|
+
const { origin, id } = raw;
|
|
16437
|
+
return typeof origin === "string" && ORIGIN_RE.test(origin) && typeof id === "string" && ID_RE.test(id) ? `${origin}/${id}` : null;
|
|
16438
|
+
}
|
|
16439
|
+
function parseEntry(raw) {
|
|
16440
|
+
if (!isPlainObject2(raw))
|
|
16441
|
+
return null;
|
|
16442
|
+
const origin = boundedString(raw.origin, 32);
|
|
16443
|
+
const id = boundedString(raw.id, 64);
|
|
16444
|
+
const providerID = boundedString(raw.providerID, MAX_FIELD);
|
|
16445
|
+
const label = printable(boundedString(raw.label, MAX_LABEL) ?? "");
|
|
16446
|
+
const secret = boundedString(raw.secret, MAX_SECRET);
|
|
16447
|
+
const shape = providerID ? SECRET_SHAPES.get(providerID) : undefined;
|
|
16448
|
+
if (!origin || !ORIGIN_RE.test(origin))
|
|
16449
|
+
return null;
|
|
16450
|
+
if (!id || !ID_RE.test(id) || !providerID || !label || !secret)
|
|
16451
|
+
return null;
|
|
16452
|
+
if (!shape?.test(secret))
|
|
16453
|
+
return null;
|
|
16454
|
+
const expiresAt = providerID === "anthropic" && isFiniteNumber(raw.expiresAt) && raw.expiresAt > 0 ? raw.expiresAt : undefined;
|
|
16455
|
+
return {
|
|
16456
|
+
origin,
|
|
16457
|
+
id,
|
|
16458
|
+
providerID,
|
|
16459
|
+
label,
|
|
16460
|
+
secret,
|
|
16461
|
+
...expiresAt ? { expiresAt } : {}
|
|
16462
|
+
};
|
|
16463
|
+
}
|
|
16464
|
+
function sortEntries(entries) {
|
|
16465
|
+
return [...entries].sort((a, b) => {
|
|
16466
|
+
const [x, y] = [entryKey(a), entryKey(b)];
|
|
16467
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
16468
|
+
});
|
|
16469
|
+
}
|
|
16470
|
+
function entriesDigest(entries) {
|
|
16471
|
+
return fingerprint(JSON.stringify(sortEntries(entries)));
|
|
16472
|
+
}
|
|
16473
|
+
function buildPayload(entries, at) {
|
|
16474
|
+
return JSON.stringify({
|
|
16475
|
+
v: PAYLOAD_VERSION,
|
|
16476
|
+
at,
|
|
16477
|
+
entries: sortEntries(entries)
|
|
16478
|
+
});
|
|
16479
|
+
}
|
|
16480
|
+
function parsePayload(text) {
|
|
16481
|
+
let json;
|
|
16482
|
+
try {
|
|
16483
|
+
json = JSON.parse(text);
|
|
16484
|
+
} catch {
|
|
16485
|
+
throw new SyncError("bad-blob");
|
|
16486
|
+
}
|
|
16487
|
+
if (!isPlainObject2(json) || !Array.isArray(json.entries))
|
|
16488
|
+
throw new SyncError("bad-blob");
|
|
16489
|
+
if (json.v !== PAYLOAD_VERSION)
|
|
16490
|
+
throw new SyncError("bad-version");
|
|
16491
|
+
if (!isFiniteNumber(json.at))
|
|
16492
|
+
throw new SyncError("bad-blob");
|
|
16493
|
+
if (json.entries.length > MAX_ENTRIES)
|
|
16494
|
+
throw new SyncError("too-large");
|
|
16495
|
+
const listed = new Set;
|
|
16496
|
+
const entries = [];
|
|
16497
|
+
let unreadable = 0;
|
|
16498
|
+
for (const raw of json.entries) {
|
|
16499
|
+
const key = keyOf(raw);
|
|
16500
|
+
const entry = parseEntry(raw);
|
|
16501
|
+
if (key) {
|
|
16502
|
+
const seen = listed.has(key);
|
|
16503
|
+
listed.add(key);
|
|
16504
|
+
if (seen && entry)
|
|
16505
|
+
continue;
|
|
16506
|
+
}
|
|
16507
|
+
if (entry)
|
|
16508
|
+
entries.push(entry);
|
|
16509
|
+
else
|
|
16510
|
+
unreadable += 1;
|
|
16511
|
+
}
|
|
16512
|
+
return { at: json.at, entries, listed, unreadable };
|
|
16513
|
+
}
|
|
16514
|
+
|
|
16515
|
+
// src/sync/merge.ts
|
|
16516
|
+
function holdsFingerprint(row, digest) {
|
|
16517
|
+
return row.inferenceToken !== undefined && fingerprint(row.inferenceToken) === digest || !row.refresh && fingerprint(row.access) === digest;
|
|
16518
|
+
}
|
|
16519
|
+
function staticFingerprint(row) {
|
|
16520
|
+
if (row.inferenceToken !== undefined)
|
|
16521
|
+
return fingerprint(row.inferenceToken);
|
|
16522
|
+
return row.refresh ? "" : fingerprint(row.access);
|
|
16523
|
+
}
|
|
16524
|
+
function recorded(map, key) {
|
|
16525
|
+
return Object.hasOwn(map, key) ? map[key] : undefined;
|
|
16526
|
+
}
|
|
16527
|
+
function blockerStands(accounts, skip) {
|
|
16528
|
+
const row = accounts.find((a) => a.id === skip.blocker);
|
|
16529
|
+
return !!row && (skip.holds === "" || holdsFingerprint(row, skip.holds));
|
|
16530
|
+
}
|
|
16531
|
+
function hasFreedSkips(accounts, skipped) {
|
|
16532
|
+
return Object.values(skipped).some((skip) => !blockerStands(accounts, skip));
|
|
16533
|
+
}
|
|
16534
|
+
function planMerge(accounts, snapshot, memory) {
|
|
16535
|
+
const { origin, imported, skipped } = memory;
|
|
16536
|
+
const imports = [];
|
|
16537
|
+
const drops = [];
|
|
16538
|
+
for (const entry of snapshot.entries) {
|
|
16539
|
+
if (entry.origin === origin)
|
|
16540
|
+
continue;
|
|
16541
|
+
const key = entryKey(entry);
|
|
16542
|
+
const previous = recorded(imported, key);
|
|
16543
|
+
const skip = recorded(skipped, key);
|
|
16544
|
+
const digest = fingerprint(entry.secret);
|
|
16545
|
+
if (previous?.fingerprint === digest)
|
|
16546
|
+
continue;
|
|
16547
|
+
if (skip?.fingerprint === digest && blockerStands(accounts, skip))
|
|
16548
|
+
continue;
|
|
16549
|
+
const heldLocally = accounts.some((a) => a.providerID === entry.providerID && holdsFingerprint(a, digest));
|
|
16550
|
+
if (previous && heldLocally)
|
|
16551
|
+
drops.push([key, previous]);
|
|
16552
|
+
else if (!heldLocally)
|
|
16553
|
+
imports.push({ entry, ...previous ? { previous } : {} });
|
|
16554
|
+
}
|
|
16555
|
+
for (const [key, ref] of Object.entries(imported))
|
|
16556
|
+
if (!snapshot.listed.has(key))
|
|
16557
|
+
drops.push([key, ref]);
|
|
16558
|
+
const forgets = Object.keys(skipped).filter((key) => !snapshot.listed.has(key));
|
|
16559
|
+
return { imports, drops, forgets };
|
|
16560
|
+
}
|
|
16561
|
+
|
|
16562
|
+
// src/sync/state.ts
|
|
16563
|
+
import { readFile as readFile5, rm as rm2 } from "node:fs/promises";
|
|
16564
|
+
|
|
16565
|
+
// src/sync/gist.ts
|
|
16566
|
+
var GIST_FILE = "auth-load-balancer-sync.json";
|
|
16567
|
+
var API = "https://api.github.com/gists";
|
|
16568
|
+
var REQUEST_TIMEOUT_MS = 15000;
|
|
16569
|
+
var MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
16570
|
+
var MIN_BACKOFF_MS = 60000;
|
|
16571
|
+
var MAX_BACKOFF_MS = 3600000;
|
|
16572
|
+
var DEFAULT_BACKOFF_MS = 15 * 60000;
|
|
16573
|
+
var GIST_ID_RE = /^[\da-f]{20,40}$/i;
|
|
16574
|
+
var OWNER_RE = /^[\w-]{1,39}$/;
|
|
16575
|
+
function parseGistLink(link) {
|
|
16576
|
+
let url;
|
|
16577
|
+
try {
|
|
16578
|
+
url = new URL(link.trim());
|
|
16579
|
+
} catch {
|
|
16580
|
+
return null;
|
|
16581
|
+
}
|
|
16582
|
+
if (url.protocol !== "https:" || url.hostname !== "gist.github.com")
|
|
16583
|
+
return null;
|
|
16584
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
16585
|
+
const id = parts.at(-1);
|
|
16586
|
+
const owner = parts.length === 2 ? parts[0] : undefined;
|
|
16587
|
+
const key = decodeKey(url.hash.slice(1));
|
|
16588
|
+
if (!id || parts.length > 2 || !GIST_ID_RE.test(id) || owner !== undefined && !OWNER_RE.test(owner) || !key)
|
|
16589
|
+
return null;
|
|
16590
|
+
return { id, key };
|
|
16591
|
+
}
|
|
16592
|
+
function backoffFrom(res, now) {
|
|
16593
|
+
const retryAfter = Number(res.headers.get("retry-after"));
|
|
16594
|
+
const reset = Number(res.headers.get("x-ratelimit-reset"));
|
|
16595
|
+
const wait = retryAfter > 0 ? retryAfter * 1000 : reset > 0 ? reset * 1000 - now : DEFAULT_BACKOFF_MS;
|
|
16596
|
+
return Math.min(MAX_BACKOFF_MS, Math.max(MIN_BACKOFF_MS, wait));
|
|
16597
|
+
}
|
|
16598
|
+
function isRateLimited(res) {
|
|
16599
|
+
return res.status === 429 || res.status === 403 && (res.headers.get("x-ratelimit-remaining") === "0" || res.headers.has("retry-after"));
|
|
16600
|
+
}
|
|
16601
|
+
async function request(url, init, now) {
|
|
16602
|
+
let res;
|
|
16603
|
+
try {
|
|
16604
|
+
res = await globalThis.fetch(url, {
|
|
16605
|
+
...init,
|
|
16606
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
16607
|
+
});
|
|
16608
|
+
} catch {
|
|
16609
|
+
throw new SyncError("network");
|
|
16610
|
+
}
|
|
16611
|
+
if (res.status === 404)
|
|
16612
|
+
throw new SyncError("not-found");
|
|
16613
|
+
if (isRateLimited(res))
|
|
16614
|
+
throw new SyncError("rate-limited", backoffFrom(res, now));
|
|
16615
|
+
return res;
|
|
16616
|
+
}
|
|
16617
|
+
function headers(token) {
|
|
16618
|
+
return {
|
|
16619
|
+
accept: "application/vnd.github+json",
|
|
16620
|
+
"x-github-api-version": "2022-11-28",
|
|
16621
|
+
"user-agent": "opencode-auth-load-balancer",
|
|
16622
|
+
...token ? { authorization: `Bearer ${token}` } : {}
|
|
16623
|
+
};
|
|
16624
|
+
}
|
|
16625
|
+
async function boundedText(res) {
|
|
16626
|
+
const declared = Number(res.headers.get("content-length"));
|
|
16627
|
+
if (declared > MAX_RESPONSE_BYTES) {
|
|
16628
|
+
await res.body?.cancel().catch(ignore);
|
|
16629
|
+
throw new SyncError("too-large");
|
|
16630
|
+
}
|
|
16631
|
+
const reader = res.body?.getReader();
|
|
16632
|
+
if (!reader)
|
|
16633
|
+
return "";
|
|
16634
|
+
const chunks = [];
|
|
16635
|
+
let total = 0;
|
|
16636
|
+
try {
|
|
16637
|
+
for (;; ) {
|
|
16638
|
+
const { done, value } = await reader.read();
|
|
16639
|
+
if (done)
|
|
16640
|
+
break;
|
|
16641
|
+
total += value.byteLength;
|
|
16642
|
+
if (total > MAX_RESPONSE_BYTES) {
|
|
16643
|
+
await reader.cancel().catch(ignore);
|
|
16644
|
+
throw new SyncError("too-large");
|
|
16645
|
+
}
|
|
16646
|
+
chunks.push(value);
|
|
16647
|
+
}
|
|
16648
|
+
} catch (error) {
|
|
16649
|
+
throw error instanceof SyncError ? error : new SyncError("network");
|
|
16650
|
+
}
|
|
16651
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
16652
|
+
}
|
|
16653
|
+
async function readJson(res) {
|
|
16654
|
+
if (!res.ok)
|
|
16655
|
+
throw new SyncError("http");
|
|
16656
|
+
const text = await boundedText(res);
|
|
16657
|
+
let json;
|
|
16658
|
+
try {
|
|
16659
|
+
json = JSON.parse(text);
|
|
16660
|
+
} catch {
|
|
16661
|
+
throw new SyncError("http");
|
|
16662
|
+
}
|
|
16663
|
+
if (!isPlainObject2(json))
|
|
16664
|
+
throw new SyncError("http");
|
|
16665
|
+
return json;
|
|
16666
|
+
}
|
|
16667
|
+
async function createGist(token, content, now = Date.now()) {
|
|
16668
|
+
const res = await request(API, {
|
|
16669
|
+
method: "POST",
|
|
16670
|
+
headers: { ...headers(token), "content-type": "application/json" },
|
|
16671
|
+
body: JSON.stringify({
|
|
16672
|
+
description: "opencode-auth-load-balancer sync (encrypted)",
|
|
16673
|
+
public: false,
|
|
16674
|
+
files: { [GIST_FILE]: { content } }
|
|
16675
|
+
})
|
|
16676
|
+
}, now);
|
|
16677
|
+
const json = await readJson(res);
|
|
16678
|
+
const owner = isPlainObject2(json.owner) ? json.owner.login : undefined;
|
|
16679
|
+
if (typeof json.id !== "string" || !GIST_ID_RE.test(json.id))
|
|
16680
|
+
throw new SyncError("http");
|
|
16681
|
+
return {
|
|
16682
|
+
id: json.id,
|
|
16683
|
+
...typeof owner === "string" && OWNER_RE.test(owner) ? { owner } : {}
|
|
16684
|
+
};
|
|
16685
|
+
}
|
|
16686
|
+
async function updateGist(token, id, content, now = Date.now()) {
|
|
16687
|
+
let res;
|
|
16688
|
+
try {
|
|
16689
|
+
res = await request(`${API}/${id}`, {
|
|
16690
|
+
method: "PATCH",
|
|
16691
|
+
headers: { ...headers(token), "content-type": "application/json" },
|
|
16692
|
+
body: JSON.stringify({ files: { [GIST_FILE]: { content } } })
|
|
16693
|
+
}, now);
|
|
16694
|
+
} catch (error) {
|
|
16695
|
+
if (error instanceof SyncError && error.code === "not-found")
|
|
16696
|
+
throw new SyncError("write-denied");
|
|
16697
|
+
throw error;
|
|
16698
|
+
}
|
|
16699
|
+
if (res.status === 401 || res.status === 403)
|
|
16700
|
+
throw new SyncError("write-denied");
|
|
16701
|
+
await readJson(res);
|
|
16702
|
+
}
|
|
16703
|
+
async function readGist(id, etag, now = Date.now(), token) {
|
|
16704
|
+
const ask = (auth) => request(`${API}/${id}`, {
|
|
16705
|
+
headers: {
|
|
16706
|
+
...headers(auth),
|
|
16707
|
+
...etag ? { "if-none-match": etag } : {}
|
|
16708
|
+
}
|
|
16709
|
+
}, now);
|
|
16710
|
+
let res = await ask(token);
|
|
16711
|
+
if (res.status === 401 && token)
|
|
16712
|
+
res = await ask();
|
|
16713
|
+
if (res.status === 304)
|
|
16714
|
+
return { changed: false };
|
|
16715
|
+
const json = await readJson(res);
|
|
16716
|
+
const file = isPlainObject2(json.files) ? json.files[GIST_FILE] : undefined;
|
|
16717
|
+
if (!isPlainObject2(file) || typeof file.content !== "string")
|
|
16718
|
+
throw new SyncError("bad-blob");
|
|
16719
|
+
if (file.truncated === true || file.content.length > MAX_BLOB_BYTES)
|
|
16720
|
+
throw new SyncError("too-large");
|
|
16721
|
+
const next = res.headers.get("etag");
|
|
16722
|
+
return {
|
|
16723
|
+
changed: true,
|
|
16724
|
+
content: file.content,
|
|
16725
|
+
...next ? { etag: next } : {}
|
|
16726
|
+
};
|
|
16727
|
+
}
|
|
16728
|
+
|
|
16729
|
+
// src/sync/state.ts
|
|
16730
|
+
var WRITES = ["ok", "no-token", "denied", "unreadable"];
|
|
16731
|
+
function newMap(from) {
|
|
16732
|
+
const map = Object.create(null);
|
|
16733
|
+
if (from)
|
|
16734
|
+
for (const [key, value] of Object.entries(from))
|
|
16735
|
+
map[key] = value;
|
|
16736
|
+
return map;
|
|
16737
|
+
}
|
|
16738
|
+
function newRefs(from) {
|
|
16739
|
+
return newMap(from);
|
|
16740
|
+
}
|
|
16741
|
+
var STATE_LOCK = {
|
|
16742
|
+
staleMs: 30000,
|
|
16743
|
+
timeoutMs: 30000,
|
|
16744
|
+
retryMs: 25,
|
|
16745
|
+
heartbeatMs: 5000
|
|
16746
|
+
};
|
|
16747
|
+
function isRef(value) {
|
|
16748
|
+
return isPlainObject2(value) && typeof value.accountId === "string" && typeof value.fingerprint === "string";
|
|
16749
|
+
}
|
|
16750
|
+
function isSkip(value) {
|
|
16751
|
+
return isPlainObject2(value) && typeof value.fingerprint === "string" && typeof value.blocker === "string" && typeof value.holds === "string";
|
|
16752
|
+
}
|
|
16753
|
+
function readMap(raw, accept) {
|
|
16754
|
+
const map = newMap();
|
|
16755
|
+
if (isPlainObject2(raw)) {
|
|
16756
|
+
for (const [key, value] of Object.entries(raw))
|
|
16757
|
+
if (accept(value))
|
|
16758
|
+
map[key] = value;
|
|
16759
|
+
}
|
|
16760
|
+
return map;
|
|
16761
|
+
}
|
|
16762
|
+
function isWrite(value) {
|
|
16763
|
+
return typeof value === "string" && WRITES.includes(value);
|
|
16764
|
+
}
|
|
16765
|
+
function parseState(text) {
|
|
16766
|
+
let json;
|
|
16767
|
+
try {
|
|
16768
|
+
json = JSON.parse(text);
|
|
16769
|
+
} catch {
|
|
16770
|
+
return null;
|
|
16771
|
+
}
|
|
16772
|
+
if (!isPlainObject2(json) || json.v !== 1 || typeof json.gistId !== "string" || !GIST_ID_RE.test(json.gistId) || typeof json.key !== "string" || !decodeKey(json.key))
|
|
16773
|
+
return null;
|
|
16774
|
+
const { owner, etag, syncedAt, uploadedDigest, write } = json;
|
|
16775
|
+
const { uploadedAt, appliedAt, retryAt, pollAt, writeCheckAt } = json;
|
|
16776
|
+
return {
|
|
16777
|
+
v: 1,
|
|
16778
|
+
gistId: json.gistId,
|
|
16779
|
+
key: json.key,
|
|
16780
|
+
creator: json.creator === true,
|
|
16781
|
+
imported: readMap(json.imported, isRef),
|
|
16782
|
+
skipped: readMap(json.skipped, isSkip),
|
|
16783
|
+
...typeof owner === "string" ? { owner } : {},
|
|
16784
|
+
...isWrite(write) ? { write } : {},
|
|
16785
|
+
...typeof etag === "string" ? { etag } : {},
|
|
16786
|
+
...isFiniteNumber(syncedAt) ? { syncedAt } : {},
|
|
16787
|
+
...typeof uploadedDigest === "string" ? { uploadedDigest } : {},
|
|
16788
|
+
...isFiniteNumber(uploadedAt) ? { uploadedAt } : {},
|
|
16789
|
+
...isFiniteNumber(appliedAt) ? { appliedAt } : {},
|
|
16790
|
+
...isFiniteNumber(retryAt) ? { retryAt } : {},
|
|
16791
|
+
...isFiniteNumber(pollAt) ? { pollAt } : {},
|
|
16792
|
+
...isFiniteNumber(writeCheckAt) ? { writeCheckAt } : {}
|
|
16793
|
+
};
|
|
16794
|
+
}
|
|
16795
|
+
async function readSyncState() {
|
|
16796
|
+
const text = await readFile5(syncStateFilePath(), "utf8").catch(() => null);
|
|
16797
|
+
return text === null ? null : parseState(text);
|
|
16798
|
+
}
|
|
16799
|
+
async function updateSyncState(fn) {
|
|
16800
|
+
return withLock(`${syncStateFilePath()}.lock`, STATE_LOCK, async () => {
|
|
16801
|
+
const next = fn(await readSyncState());
|
|
16802
|
+
if (next === null)
|
|
16803
|
+
await rm2(syncStateFilePath(), { force: true });
|
|
16804
|
+
else
|
|
16805
|
+
await writeJsonAtomic(syncStateFilePath(), JSON.stringify(next));
|
|
16806
|
+
return next;
|
|
16807
|
+
});
|
|
16808
|
+
}
|
|
16809
|
+
async function writeSyncStatus(status) {
|
|
16810
|
+
await writeJsonAtomic(syncStatusFilePath(), JSON.stringify(status)).catch(ignore);
|
|
16811
|
+
}
|
|
16812
|
+
|
|
16813
|
+
// src/sync/apply.ts
|
|
16814
|
+
async function exchangeSecret(adapter, secret) {
|
|
16815
|
+
try {
|
|
16816
|
+
if (adapter.tokenLogin)
|
|
16817
|
+
return await adapter.tokenLogin.exchange(secret);
|
|
16818
|
+
if (adapter.tokensFromApiKey)
|
|
16819
|
+
return await adapter.exchange(secret, "", "", "");
|
|
16820
|
+
} catch {
|
|
16821
|
+
return null;
|
|
16822
|
+
}
|
|
16823
|
+
return null;
|
|
16824
|
+
}
|
|
16825
|
+
function dropRow(pool, id) {
|
|
16826
|
+
pool.accounts = pool.accounts.filter((a) => a.id !== id);
|
|
16827
|
+
for (const [provider, selected] of Object.entries(pool.lastSelected))
|
|
16828
|
+
if (selected === id)
|
|
16829
|
+
delete pool.lastSelected[provider];
|
|
16830
|
+
for (const [key, session] of Object.entries(pool.sessions))
|
|
16831
|
+
if (session.accountId === id)
|
|
16832
|
+
delete pool.sessions[key];
|
|
16833
|
+
}
|
|
16834
|
+
function release3(pool, row) {
|
|
16835
|
+
if (row.refresh) {
|
|
16836
|
+
delete row.inferenceToken;
|
|
16837
|
+
delete row.inferenceExpires;
|
|
16838
|
+
} else
|
|
16839
|
+
dropRow(pool, row.id);
|
|
16840
|
+
}
|
|
16841
|
+
function uniqueLabel(pool, label) {
|
|
16842
|
+
const used = new Set(pool.accounts.map((a) => a.label));
|
|
16843
|
+
let candidate = label;
|
|
16844
|
+
for (let n = 2;used.has(candidate); n++)
|
|
16845
|
+
candidate = `${label} (${n})`;
|
|
16846
|
+
return candidate;
|
|
16847
|
+
}
|
|
16848
|
+
async function strip(ref) {
|
|
16849
|
+
return mutatePool((pool) => {
|
|
16850
|
+
const row = findAccount(pool, ref.accountId);
|
|
16851
|
+
if (!row || !holdsFingerprint(row, ref.fingerprint))
|
|
16852
|
+
return false;
|
|
16853
|
+
release3(pool, row);
|
|
16854
|
+
return true;
|
|
16855
|
+
});
|
|
16856
|
+
}
|
|
16857
|
+
function land(pool, { entry, previous }, tokens) {
|
|
16858
|
+
const digest = fingerprint(entry.secret);
|
|
16859
|
+
const sameProvider = (a) => a.providerID === entry.providerID;
|
|
16860
|
+
const owned = previous ? pool.accounts.find((a) => a.id === previous.accountId && holdsFingerprint(a, previous.fingerprint)) : undefined;
|
|
16861
|
+
const prev = owned && sameProvider(owned) ? owned : undefined;
|
|
16862
|
+
if (owned && !prev)
|
|
16863
|
+
release3(pool, owned);
|
|
16864
|
+
const blocker = pool.accounts.find((a) => a !== prev && sameProvider(a) && (holdsFingerprint(a, digest) || !tokens.inferenceOnly && holdsCredential(a, tokens))) ?? (tokens.inferenceOnly && !prev ? tokenSharer(pool.accounts.filter(sameProvider), tokens) : undefined);
|
|
16865
|
+
if (blocker) {
|
|
16866
|
+
if (prev)
|
|
16867
|
+
release3(pool, prev);
|
|
16868
|
+
return {
|
|
16869
|
+
kind: "skipped",
|
|
16870
|
+
blocker: blocker.id,
|
|
16871
|
+
holds: staticFingerprint(blocker)
|
|
16872
|
+
};
|
|
16873
|
+
}
|
|
16874
|
+
return {
|
|
16875
|
+
kind: "placed",
|
|
16876
|
+
row: placeTokens(pool, entry.providerID, tokens, uniqueLabel(pool, entry.label), prev?.id)
|
|
16877
|
+
};
|
|
16878
|
+
}
|
|
16879
|
+
async function applyPlan(plan, memory, adapters = ADAPTERS) {
|
|
16880
|
+
const result = {
|
|
16881
|
+
imported: newRefs(memory.imported),
|
|
16882
|
+
skipped: newMap(memory.skipped),
|
|
16883
|
+
added: 0,
|
|
16884
|
+
updated: 0,
|
|
16885
|
+
removed: 0,
|
|
16886
|
+
deferred: 0
|
|
16887
|
+
};
|
|
16888
|
+
for (const key of plan.forgets)
|
|
16889
|
+
delete result.skipped[key];
|
|
16890
|
+
for (const job of plan.imports) {
|
|
16891
|
+
const { entry, previous } = job;
|
|
16892
|
+
const key = entryKey(entry);
|
|
16893
|
+
try {
|
|
16894
|
+
const adapter = adapterFor(adapters, entry.providerID);
|
|
16895
|
+
const tokens = adapter && await exchangeSecret(adapter, entry.secret);
|
|
16896
|
+
if (!tokens) {
|
|
16897
|
+
result.deferred += 1;
|
|
16898
|
+
continue;
|
|
16899
|
+
}
|
|
16900
|
+
if (tokens.inferenceOnly && entry.expiresAt)
|
|
16901
|
+
tokens.inferenceExpires = entry.expiresAt;
|
|
16902
|
+
const landing = await mutatePool((pool) => land(pool, job, tokens));
|
|
16903
|
+
if (landing.kind === "skipped") {
|
|
16904
|
+
delete result.imported[key];
|
|
16905
|
+
result.skipped[key] = {
|
|
16906
|
+
fingerprint: fingerprint(entry.secret),
|
|
16907
|
+
blocker: landing.blocker,
|
|
16908
|
+
holds: landing.holds
|
|
16909
|
+
};
|
|
16910
|
+
} else {
|
|
16911
|
+
delete result.skipped[key];
|
|
16912
|
+
result.imported[key] = {
|
|
16913
|
+
accountId: landing.row.id,
|
|
16914
|
+
fingerprint: fingerprint(entry.secret)
|
|
16915
|
+
};
|
|
16916
|
+
if (previous)
|
|
16917
|
+
result.updated += 1;
|
|
16918
|
+
else
|
|
16919
|
+
result.added += 1;
|
|
16920
|
+
}
|
|
16921
|
+
} catch {
|
|
16922
|
+
result.deferred += 1;
|
|
16923
|
+
}
|
|
16924
|
+
}
|
|
16925
|
+
for (const [key, ref] of plan.drops) {
|
|
16926
|
+
try {
|
|
16927
|
+
if (await strip(ref))
|
|
16928
|
+
result.removed += 1;
|
|
16929
|
+
delete result.imported[key];
|
|
16930
|
+
} catch {
|
|
16931
|
+
result.deferred += 1;
|
|
16932
|
+
}
|
|
16933
|
+
}
|
|
16934
|
+
return result;
|
|
16935
|
+
}
|
|
16936
|
+
var MAX_PASSES = 3;
|
|
16937
|
+
async function applySnapshot(snapshot, memory, options = {}) {
|
|
16938
|
+
const { adapters = ADAPTERS, afterPass } = options;
|
|
16939
|
+
const total = {
|
|
16940
|
+
imported: newRefs(memory.imported),
|
|
16941
|
+
skipped: newMap(memory.skipped),
|
|
16942
|
+
added: 0,
|
|
16943
|
+
updated: 0,
|
|
16944
|
+
removed: 0,
|
|
16945
|
+
deferred: 0
|
|
16946
|
+
};
|
|
16947
|
+
for (let pass = 0;pass < MAX_PASSES; pass++) {
|
|
16948
|
+
const plan = planMerge((await readPool()).accounts, snapshot, {
|
|
16949
|
+
origin: memory.origin,
|
|
16950
|
+
imported: total.imported,
|
|
16951
|
+
skipped: total.skipped
|
|
16952
|
+
});
|
|
16953
|
+
const done = await applyPlan(plan, total, adapters);
|
|
16954
|
+
total.imported = done.imported;
|
|
16955
|
+
total.skipped = done.skipped;
|
|
16956
|
+
total.added += done.added;
|
|
16957
|
+
total.updated += done.updated;
|
|
16958
|
+
total.removed += done.removed;
|
|
16959
|
+
total.deferred = done.deferred;
|
|
16960
|
+
await afterPass?.(total);
|
|
16961
|
+
if (done.removed === 0)
|
|
16962
|
+
break;
|
|
16963
|
+
}
|
|
16964
|
+
return total;
|
|
16965
|
+
}
|
|
16966
|
+
|
|
16967
|
+
// src/sync/github-auth.ts
|
|
16968
|
+
import { execFile } from "node:child_process";
|
|
16969
|
+
var GH_TIMEOUT_MS = 5000;
|
|
16970
|
+
function ghAuthToken(exec = execFile) {
|
|
16971
|
+
return new Promise((resolve, reject) => {
|
|
16972
|
+
exec("gh", ["auth", "token"], { timeout: GH_TIMEOUT_MS, windowsHide: true, maxBuffer: 64 * 1024 }, (error, stdout) => error ? reject(error) : resolve(stdout));
|
|
16973
|
+
});
|
|
16974
|
+
}
|
|
16975
|
+
async function discoverGithubToken(run = ghAuthToken, env = process.env) {
|
|
16976
|
+
for (const name of ["GITHUB_TOKEN", "GH_TOKEN"]) {
|
|
16977
|
+
const value = env[name]?.trim();
|
|
16978
|
+
if (value)
|
|
16979
|
+
return value;
|
|
16980
|
+
}
|
|
16981
|
+
const token = (await run().catch(() => "")).trim();
|
|
16982
|
+
return token || null;
|
|
16983
|
+
}
|
|
16984
|
+
|
|
16985
|
+
// src/sync/origin.ts
|
|
16986
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
16987
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
16988
|
+
var ORIGIN_LOCK = {
|
|
16989
|
+
staleMs: 30000,
|
|
16990
|
+
timeoutMs: 30000,
|
|
16991
|
+
retryMs: 25,
|
|
16992
|
+
heartbeatMs: 5000
|
|
16993
|
+
};
|
|
16994
|
+
async function readOrigin() {
|
|
16995
|
+
const text = await readFile6(syncOriginFilePath(), "utf8").catch(() => null);
|
|
16996
|
+
if (text === null)
|
|
16997
|
+
return null;
|
|
16998
|
+
try {
|
|
16999
|
+
const json = JSON.parse(text);
|
|
17000
|
+
return isPlainObject2(json) && typeof json.origin === "string" && ORIGIN_RE.test(json.origin) ? json.origin : null;
|
|
17001
|
+
} catch {
|
|
17002
|
+
return null;
|
|
17003
|
+
}
|
|
17004
|
+
}
|
|
17005
|
+
async function machineOrigin() {
|
|
17006
|
+
const known = await readOrigin();
|
|
17007
|
+
if (known)
|
|
17008
|
+
return known;
|
|
17009
|
+
return withLock(`${syncOriginFilePath()}.lock`, ORIGIN_LOCK, async () => {
|
|
17010
|
+
const raced = await readOrigin();
|
|
17011
|
+
if (raced)
|
|
17012
|
+
return raced;
|
|
17013
|
+
const origin = randomBytes2(16).toString("hex");
|
|
17014
|
+
await writeJsonAtomic(syncOriginFilePath(), JSON.stringify({ origin }));
|
|
17015
|
+
return origin;
|
|
17016
|
+
});
|
|
17017
|
+
}
|
|
17018
|
+
|
|
17019
|
+
// src/sync/own.ts
|
|
17020
|
+
function staticSecret(row) {
|
|
17021
|
+
if (row.providerID === "anthropic")
|
|
17022
|
+
return row.inferenceToken;
|
|
17023
|
+
return !row.refresh && row.expires === STATIC_CREDENTIAL_EXPIRES ? row.access : undefined;
|
|
17024
|
+
}
|
|
17025
|
+
function importedRows(accounts, imported) {
|
|
17026
|
+
const ids = new Set;
|
|
17027
|
+
for (const ref of Object.values(imported)) {
|
|
17028
|
+
const row = accounts.find((a) => a.id === ref.accountId);
|
|
17029
|
+
if (row && holdsFingerprint(row, ref.fingerprint))
|
|
17030
|
+
ids.add(row.id);
|
|
17031
|
+
}
|
|
17032
|
+
return ids;
|
|
17033
|
+
}
|
|
17034
|
+
function ownEntries(pool, origin, imported) {
|
|
17035
|
+
const fromSync = importedRows(pool.accounts, imported);
|
|
17036
|
+
const entries = [];
|
|
17037
|
+
for (const row of pool.accounts) {
|
|
17038
|
+
if (!SECRET_SHAPES.has(row.providerID) || fromSync.has(row.id))
|
|
17039
|
+
continue;
|
|
17040
|
+
if (row.disabledReason || row.lostLogins?.token)
|
|
17041
|
+
continue;
|
|
17042
|
+
const entry = parseEntry({
|
|
17043
|
+
origin,
|
|
17044
|
+
id: row.id,
|
|
17045
|
+
providerID: row.providerID,
|
|
17046
|
+
label: printable(row.label).slice(0, MAX_LABEL).trim() || row.providerID,
|
|
17047
|
+
secret: staticSecret(row),
|
|
17048
|
+
expiresAt: row.inferenceExpires
|
|
17049
|
+
});
|
|
17050
|
+
if (entry)
|
|
17051
|
+
entries.push(entry);
|
|
17052
|
+
}
|
|
17053
|
+
return sortEntries(entries).slice(0, MAX_ENTRIES);
|
|
17054
|
+
}
|
|
17055
|
+
function listable(own, others, origin) {
|
|
17056
|
+
const taken = new Set(others.filter((e) => e.origin < origin).map((e) => fingerprint(e.secret)));
|
|
17057
|
+
return own.filter((e) => !taken.has(fingerprint(e.secret))).slice(0, Math.max(0, MAX_ENTRIES - others.length));
|
|
17058
|
+
}
|
|
17059
|
+
|
|
17060
|
+
// src/sync/timing.ts
|
|
17061
|
+
var POLL_MS = 15 * 60000;
|
|
17062
|
+
var PUBLISH_RETRY_MS = 5 * 60000;
|
|
17063
|
+
|
|
17064
|
+
// src/sync/cycle.ts
|
|
17065
|
+
var WRITE_BACKOFF_MS = 3600000;
|
|
17066
|
+
var UNREADABLE_NOTE = "The gist has entries this version cannot read; update this plugin to upload.";
|
|
17067
|
+
function count(n, one, many) {
|
|
17068
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
17069
|
+
}
|
|
17070
|
+
function recordApplied(gistId, at, applied, syncedAt) {
|
|
17071
|
+
return updateSyncState((cur) => cur?.gistId === gistId ? {
|
|
17072
|
+
...cur,
|
|
17073
|
+
imported: applied.imported,
|
|
17074
|
+
skipped: applied.skipped,
|
|
17075
|
+
appliedAt: Math.max(at, cur.appliedAt ?? 0),
|
|
17076
|
+
syncedAt,
|
|
17077
|
+
etag: undefined
|
|
17078
|
+
} : cur);
|
|
17079
|
+
}
|
|
17080
|
+
async function runCycle(deps, force, report) {
|
|
17081
|
+
const { now } = deps;
|
|
17082
|
+
const state = await readSyncState();
|
|
17083
|
+
if (!state)
|
|
17084
|
+
throw new SyncError("not-set-up");
|
|
17085
|
+
const key = decodeKey(state.key) ?? Buffer.alloc(32);
|
|
17086
|
+
const origin = await machineOrigin();
|
|
17087
|
+
const token = await discoverGithubToken(deps.runGh);
|
|
17088
|
+
if (force && !token)
|
|
17089
|
+
throw new SyncError("no-auth");
|
|
17090
|
+
const pool = await readPool();
|
|
17091
|
+
const backedOff = state.write === "denied" && !force && now() < (state.writeCheckAt ?? 0);
|
|
17092
|
+
const mayWrite = token !== null && !backedOff;
|
|
17093
|
+
const before = entriesDigest(ownEntries(pool, origin, state.imported));
|
|
17094
|
+
const dirty = mayWrite && before !== state.uploadedDigest;
|
|
17095
|
+
report.retryMs = dirty ? PUBLISH_RETRY_MS : POLL_MS;
|
|
17096
|
+
const reread = force || dirty || hasFreedSkips(pool.accounts, state.skipped);
|
|
17097
|
+
const auth = token ?? undefined;
|
|
17098
|
+
let read = await readGist(state.gistId, state.etag, now(), auth);
|
|
17099
|
+
if (!read.changed && reread)
|
|
17100
|
+
read = await readGist(state.gistId, undefined, now(), auth);
|
|
17101
|
+
const standing = token === null ? "no-token" : backedOff ? "denied" : "ok";
|
|
17102
|
+
if (!read.changed) {
|
|
17103
|
+
const write = standing === "ok" && state.write === "unreadable" ? "unreadable" : standing;
|
|
17104
|
+
await updateSyncState((cur) => cur?.gistId === state.gistId ? { ...cur, syncedAt: now(), write } : cur);
|
|
17105
|
+
return "Up to date.";
|
|
17106
|
+
}
|
|
17107
|
+
const snapshot = parsePayload(open(read.content, key));
|
|
17108
|
+
const stale = state.appliedAt !== undefined && snapshot.at < state.appliedAt;
|
|
17109
|
+
if (stale && !mayWrite)
|
|
17110
|
+
throw new SyncError("rolled-back");
|
|
17111
|
+
const applied = stale ? null : await applySnapshot(snapshot, { origin, imported: state.imported, skipped: state.skipped }, {
|
|
17112
|
+
...deps.adapters ? { adapters: deps.adapters } : {},
|
|
17113
|
+
afterPass: (soFar) => recordApplied(state.gistId, snapshot.at, soFar, now())
|
|
17114
|
+
});
|
|
17115
|
+
const own = ownEntries(await readPool(), origin, applied?.imported ?? state.imported);
|
|
17116
|
+
const unreadable = snapshot.unreadable > 0;
|
|
17117
|
+
let write = standing;
|
|
17118
|
+
let wrote = null;
|
|
17119
|
+
let stamped = 0;
|
|
17120
|
+
let denied = false;
|
|
17121
|
+
if (mayWrite && unreadable)
|
|
17122
|
+
write = "unreadable";
|
|
17123
|
+
if (mayWrite && !unreadable) {
|
|
17124
|
+
const others = snapshot.entries.filter((e) => e.origin !== origin);
|
|
17125
|
+
const next = sortEntries([...others, ...listable(own, others, origin)]);
|
|
17126
|
+
const same = JSON.stringify(next) === JSON.stringify(sortEntries(snapshot.entries));
|
|
17127
|
+
if (!same || stale) {
|
|
17128
|
+
stamped = Math.max(now(), snapshot.at + 1, (state.uploadedAt ?? 0) + 1, (state.appliedAt ?? 0) + 1);
|
|
17129
|
+
try {
|
|
17130
|
+
await updateGist(token, state.gistId, seal(buildPayload(next, stamped), key), now());
|
|
17131
|
+
wrote = next.filter((e) => e.origin === origin);
|
|
17132
|
+
} catch (error) {
|
|
17133
|
+
if (!(error instanceof SyncError) || error.code !== "write-denied")
|
|
17134
|
+
throw error;
|
|
17135
|
+
denied = true;
|
|
17136
|
+
write = "denied";
|
|
17137
|
+
}
|
|
17138
|
+
}
|
|
17139
|
+
}
|
|
17140
|
+
const deferred = applied?.deferred ?? 0;
|
|
17141
|
+
await updateSyncState((cur) => cur?.gistId === state.gistId ? {
|
|
17142
|
+
...cur,
|
|
17143
|
+
syncedAt: now(),
|
|
17144
|
+
etag: wrote || stale || deferred > 0 || denied ? undefined : read.etag,
|
|
17145
|
+
write,
|
|
17146
|
+
writeCheckAt: denied ? now() + WRITE_BACKOFF_MS : backedOff ? cur.writeCheckAt : undefined,
|
|
17147
|
+
uploadedDigest: mayWrite && !unreadable && !denied ? entriesDigest(own) : cur.uploadedDigest,
|
|
17148
|
+
...wrote ? { uploadedAt: stamped } : {}
|
|
17149
|
+
} : cur);
|
|
17150
|
+
const parts = [];
|
|
17151
|
+
const moved = applied ? applied.added + applied.updated + applied.removed + deferred : 0;
|
|
17152
|
+
if (applied && moved > 0) {
|
|
17153
|
+
const tail = deferred > 0 ? `; ${count(deferred, "entry", "entries")} could not be verified yet` : "";
|
|
17154
|
+
parts.push(`Synced: ${applied.added} added, ${applied.updated} updated, ${applied.removed} removed${tail}.`);
|
|
17155
|
+
} else if (!wrote)
|
|
17156
|
+
parts.push("Up to date.");
|
|
17157
|
+
if (wrote)
|
|
17158
|
+
parts.push(`Uploaded ${count(wrote.length, "credential", "credentials")}.`);
|
|
17159
|
+
if (denied)
|
|
17160
|
+
parts.push(new SyncError("write-denied").message);
|
|
17161
|
+
if (mayWrite && unreadable && state.write !== "unreadable")
|
|
17162
|
+
parts.push(UNREADABLE_NOTE);
|
|
17163
|
+
return parts.join(" ");
|
|
17164
|
+
}
|
|
17165
|
+
|
|
17166
|
+
// src/sync/engine.ts
|
|
17167
|
+
function count2(n, one, many) {
|
|
17168
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
17169
|
+
}
|
|
17170
|
+
function createEngine(deps) {
|
|
17171
|
+
const { now } = deps;
|
|
17172
|
+
async function settle(work, reqAt, scheduled) {
|
|
17173
|
+
const report = { retryMs: POLL_MS };
|
|
17174
|
+
let outcome;
|
|
17175
|
+
try {
|
|
17176
|
+
outcome = { ok: true, message: await work(report) };
|
|
17177
|
+
} catch (error) {
|
|
17178
|
+
outcome = {
|
|
17179
|
+
ok: false,
|
|
17180
|
+
message: describeSyncError(error),
|
|
17181
|
+
...error instanceof SyncError && error.retryAfterMs ? { backoffMs: error.retryAfterMs } : {}
|
|
17182
|
+
};
|
|
17183
|
+
}
|
|
17184
|
+
if (scheduled) {
|
|
17185
|
+
const wait = outcome.backoffMs ?? report.retryMs;
|
|
17186
|
+
await updateSyncState((cur) => cur && (outcome.ok ? { ...cur, pollAt: now() + POLL_MS, retryAt: undefined } : { ...cur, retryAt: now() + wait }));
|
|
17187
|
+
}
|
|
17188
|
+
await writeSyncStatus({
|
|
17189
|
+
at: now(),
|
|
17190
|
+
ok: outcome.ok,
|
|
17191
|
+
message: outcome.message,
|
|
17192
|
+
...reqAt === undefined ? {} : { reqAt }
|
|
17193
|
+
});
|
|
17194
|
+
return outcome;
|
|
17195
|
+
}
|
|
17196
|
+
const cycle = (force) => (report) => runCycle(deps, force, report);
|
|
17197
|
+
async function create() {
|
|
17198
|
+
const token = await discoverGithubToken(deps.runGh);
|
|
17199
|
+
if (!token)
|
|
17200
|
+
throw new SyncError("no-auth");
|
|
17201
|
+
const origin = await machineOrigin();
|
|
17202
|
+
const own = ownEntries(await readPool(), origin, newRefs());
|
|
17203
|
+
const key = generateKey();
|
|
17204
|
+
const at = now();
|
|
17205
|
+
const gist = await createGist(token, seal(buildPayload(own, at), key), now());
|
|
17206
|
+
await updateSyncState(() => ({
|
|
17207
|
+
v: 1,
|
|
17208
|
+
gistId: gist.id,
|
|
17209
|
+
key: encodeKey(key),
|
|
17210
|
+
creator: true,
|
|
17211
|
+
...gist.owner ? { owner: gist.owner } : {},
|
|
17212
|
+
write: "ok",
|
|
17213
|
+
syncedAt: now(),
|
|
17214
|
+
uploadedDigest: entriesDigest(own),
|
|
17215
|
+
uploadedAt: at,
|
|
17216
|
+
appliedAt: at,
|
|
17217
|
+
pollAt: now() + POLL_MS,
|
|
17218
|
+
imported: newRefs(),
|
|
17219
|
+
skipped: newMap()
|
|
17220
|
+
}));
|
|
17221
|
+
return `Uploaded ${count2(own.length, "credential", "credentials")}.`;
|
|
17222
|
+
}
|
|
17223
|
+
async function unpublishedDigest() {
|
|
17224
|
+
const state = await readSyncState();
|
|
17225
|
+
if (!state || state.write === "no-token" || state.write === "unreadable" || state.write === "denied" && now() < (state.writeCheckAt ?? 0))
|
|
17226
|
+
return null;
|
|
17227
|
+
const own = ownEntries(await readPool(), await machineOrigin(), state.imported);
|
|
17228
|
+
const digest = entriesDigest(own);
|
|
17229
|
+
return digest === state.uploadedDigest ? null : digest;
|
|
17230
|
+
}
|
|
17231
|
+
const engine = {
|
|
17232
|
+
sync: (reqAt) => settle(cycle(false), reqAt, true),
|
|
17233
|
+
async upload(fresh, reqAt) {
|
|
17234
|
+
const state = await readSyncState();
|
|
17235
|
+
return fresh || !state ? settle(create, reqAt, false) : settle(cycle(true), reqAt, true);
|
|
17236
|
+
},
|
|
17237
|
+
async subscribe(link, reqAt) {
|
|
17238
|
+
const parsed = parseGistLink(link);
|
|
17239
|
+
if (!parsed)
|
|
17240
|
+
return settle(() => Promise.reject(new SyncError("bad-link")), reqAt, false);
|
|
17241
|
+
await updateSyncState((cur) => {
|
|
17242
|
+
const prior = cur?.gistId === parsed.id ? cur : null;
|
|
17243
|
+
return {
|
|
17244
|
+
v: 1,
|
|
17245
|
+
gistId: parsed.id,
|
|
17246
|
+
key: encodeKey(parsed.key),
|
|
17247
|
+
creator: prior?.creator ?? false,
|
|
17248
|
+
...prior?.owner ? { owner: prior.owner } : {},
|
|
17249
|
+
imported: prior?.imported ?? newRefs(),
|
|
17250
|
+
skipped: prior?.skipped ?? newMap()
|
|
17251
|
+
};
|
|
17252
|
+
});
|
|
17253
|
+
return settle(cycle(false), reqAt, true);
|
|
17254
|
+
},
|
|
17255
|
+
async forget(reqAt) {
|
|
17256
|
+
return settle(async () => {
|
|
17257
|
+
await updateSyncState(() => null);
|
|
17258
|
+
return "Stopped syncing. Imported accounts stay in the pool.";
|
|
17259
|
+
}, reqAt, false);
|
|
17260
|
+
},
|
|
17261
|
+
unpublishedDigest,
|
|
17262
|
+
async backgroundPoll(gistId) {
|
|
17263
|
+
const state = await readSyncState();
|
|
17264
|
+
if (state?.gistId !== gistId || now() < Math.max(state.pollAt ?? 0, state.retryAt ?? 0))
|
|
17265
|
+
return null;
|
|
17266
|
+
return engine.sync();
|
|
17267
|
+
},
|
|
17268
|
+
async backgroundPublish(gistId, digest) {
|
|
17269
|
+
const state = await readSyncState();
|
|
17270
|
+
if (state?.gistId !== gistId || now() < (state.retryAt ?? 0) || await unpublishedDigest() !== digest)
|
|
17271
|
+
return null;
|
|
17272
|
+
return engine.sync();
|
|
17273
|
+
}
|
|
17274
|
+
};
|
|
17275
|
+
return engine;
|
|
17276
|
+
}
|
|
17277
|
+
|
|
17278
|
+
// src/sync/intent.ts
|
|
17279
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
17280
|
+
import { access, readFile as readFile7, rename as rename2, rm as rm3 } from "node:fs/promises";
|
|
17281
|
+
var INTENT_TTL_MS = 10 * 60000;
|
|
17282
|
+
var INTENT_SKEW_MS = 60000;
|
|
17283
|
+
var MAX_LINK_CHARS = 2048;
|
|
17284
|
+
var CLAIM_WAIT_MS = 2000;
|
|
17285
|
+
function claimLock(timeoutMs) {
|
|
17286
|
+
return { staleMs: 1e4, timeoutMs, retryMs: 10, heartbeatMs: 2000 };
|
|
17287
|
+
}
|
|
17288
|
+
var ACTIONS = [
|
|
17289
|
+
"upload",
|
|
17290
|
+
"upload-new",
|
|
17291
|
+
"subscribe",
|
|
17292
|
+
"sync",
|
|
17293
|
+
"forget"
|
|
17294
|
+
];
|
|
17295
|
+
function isAction(value) {
|
|
17296
|
+
return typeof value === "string" && ACTIONS.includes(value);
|
|
17297
|
+
}
|
|
17298
|
+
function parseIntent(text, now) {
|
|
17299
|
+
let json;
|
|
17300
|
+
try {
|
|
17301
|
+
json = JSON.parse(text);
|
|
17302
|
+
} catch {
|
|
17303
|
+
return null;
|
|
17304
|
+
}
|
|
17305
|
+
if (!isPlainObject2(json) || !isAction(json.action) || !isFiniteNumber(json.at) || now - json.at > INTENT_TTL_MS || json.at - now > INTENT_SKEW_MS)
|
|
17306
|
+
return null;
|
|
17307
|
+
const link = typeof json.link === "string" && json.link.length <= MAX_LINK_CHARS ? json.link : undefined;
|
|
17308
|
+
return { action: json.action, at: json.at, ...link ? { link } : {} };
|
|
17309
|
+
}
|
|
17310
|
+
function intentPending() {
|
|
17311
|
+
return access(syncIntentFilePath()).then(() => true, () => false);
|
|
17312
|
+
}
|
|
17313
|
+
async function takeIntent(now, waitMs = CLAIM_WAIT_MS, afterClaim) {
|
|
17314
|
+
const path = syncIntentFilePath();
|
|
17315
|
+
if (!await intentPending())
|
|
17316
|
+
return null;
|
|
17317
|
+
try {
|
|
17318
|
+
return await withLock(`${path}.lock`, claimLock(waitMs), async () => {
|
|
17319
|
+
const claimed = `${path}.${randomUUID3()}.claimed`;
|
|
17320
|
+
try {
|
|
17321
|
+
await rename2(path, claimed);
|
|
17322
|
+
} catch {
|
|
17323
|
+
return null;
|
|
17324
|
+
}
|
|
17325
|
+
try {
|
|
17326
|
+
await afterClaim?.();
|
|
17327
|
+
return parseIntent(await readFile7(claimed, "utf8"), now);
|
|
17328
|
+
} finally {
|
|
17329
|
+
await rm3(claimed, { force: true });
|
|
17330
|
+
}
|
|
17331
|
+
});
|
|
17332
|
+
} catch {
|
|
17333
|
+
return null;
|
|
17334
|
+
}
|
|
17335
|
+
}
|
|
17336
|
+
|
|
17337
|
+
// src/sync/loop.ts
|
|
17338
|
+
var TICK_MS = 1000;
|
|
17339
|
+
var CHANGE_CHECK_MS = 5000;
|
|
17340
|
+
var DEBOUNCE_MS = 1e4;
|
|
17341
|
+
var LOCK_WAIT_MS = 500;
|
|
17342
|
+
var DISPOSE_WAIT_MS = 2000;
|
|
17343
|
+
var realTimers = {
|
|
17344
|
+
set(run, ms) {
|
|
17345
|
+
const handle = setInterval(run, ms);
|
|
17346
|
+
handle.unref();
|
|
17347
|
+
return handle;
|
|
17348
|
+
},
|
|
17349
|
+
clear: (handle) => clearInterval(handle)
|
|
17350
|
+
};
|
|
17351
|
+
async function withCycleLock(fn, waitMs) {
|
|
17352
|
+
const options = {
|
|
17353
|
+
staleMs: 20000,
|
|
17354
|
+
timeoutMs: waitMs,
|
|
17355
|
+
retryMs: 25,
|
|
17356
|
+
heartbeatMs: 5000
|
|
17357
|
+
};
|
|
17358
|
+
try {
|
|
17359
|
+
return await withLock(`${syncStateFilePath()}.cycle.lock`, options, fn);
|
|
17360
|
+
} catch (error) {
|
|
17361
|
+
if (error instanceof LockTimeoutError)
|
|
17362
|
+
return;
|
|
17363
|
+
throw error;
|
|
17364
|
+
}
|
|
17365
|
+
}
|
|
17366
|
+
var fileIntents = { pending: intentPending, take: takeIntent };
|
|
17367
|
+
function createSyncLoop(deps) {
|
|
17368
|
+
const { engine, now } = deps;
|
|
17369
|
+
const timers = deps.timers ?? realTimers;
|
|
17370
|
+
const intents = deps.intents ?? fileIntents;
|
|
17371
|
+
const lockWaitMs = deps.lockWaitMs ?? LOCK_WAIT_MS;
|
|
17372
|
+
const disposeWaitMs = deps.disposeWaitMs ?? DISPOSE_WAIT_MS;
|
|
17373
|
+
let handle;
|
|
17374
|
+
let busy = false;
|
|
17375
|
+
let disposed = false;
|
|
17376
|
+
let inflight = Promise.resolve();
|
|
17377
|
+
let checkAt = 0;
|
|
17378
|
+
let pending = null;
|
|
17379
|
+
function run(intent) {
|
|
17380
|
+
switch (intent.action) {
|
|
17381
|
+
case "upload":
|
|
17382
|
+
return engine.upload(false, intent.at);
|
|
17383
|
+
case "upload-new":
|
|
17384
|
+
return engine.upload(true, intent.at);
|
|
17385
|
+
case "subscribe":
|
|
17386
|
+
return engine.subscribe(intent.link ?? "", intent.at);
|
|
17387
|
+
case "sync":
|
|
17388
|
+
return engine.sync(intent.at);
|
|
17389
|
+
case "forget":
|
|
17390
|
+
return engine.forget(intent.at);
|
|
17391
|
+
}
|
|
17392
|
+
}
|
|
17393
|
+
async function serveRequest(t) {
|
|
17394
|
+
await withCycleLock(async () => {
|
|
17395
|
+
const intent = await intents.take(t);
|
|
17396
|
+
if (intent)
|
|
17397
|
+
await run(intent);
|
|
17398
|
+
}, lockWaitMs);
|
|
17399
|
+
pending = null;
|
|
17400
|
+
}
|
|
17401
|
+
async function publishWhenSettled(t, state) {
|
|
17402
|
+
if (t < (state.retryAt ?? 0))
|
|
17403
|
+
return;
|
|
17404
|
+
const digest = await engine.unpublishedDigest();
|
|
17405
|
+
if (digest === null) {
|
|
17406
|
+
pending = null;
|
|
17407
|
+
return;
|
|
17408
|
+
}
|
|
17409
|
+
if (pending?.digest !== digest) {
|
|
17410
|
+
pending = { digest, since: t };
|
|
17411
|
+
return;
|
|
17412
|
+
}
|
|
17413
|
+
if (t - pending.since < DEBOUNCE_MS)
|
|
17414
|
+
return;
|
|
17415
|
+
const outcome = await withCycleLock(() => engine.backgroundPublish(state.gistId, digest), lockWaitMs);
|
|
17416
|
+
if (outcome?.ok)
|
|
17417
|
+
pending = null;
|
|
17418
|
+
}
|
|
17419
|
+
async function background(t) {
|
|
17420
|
+
if (t < checkAt)
|
|
17421
|
+
return;
|
|
17422
|
+
checkAt = t + CHANGE_CHECK_MS;
|
|
17423
|
+
const state = await readSyncState();
|
|
17424
|
+
if (!state)
|
|
17425
|
+
return;
|
|
17426
|
+
if (t >= Math.max(state.pollAt ?? 0, state.retryAt ?? 0)) {
|
|
17427
|
+
await withCycleLock(() => engine.backgroundPoll(state.gistId), lockWaitMs);
|
|
17428
|
+
return;
|
|
17429
|
+
}
|
|
17430
|
+
await publishWhenSettled(t, state);
|
|
17431
|
+
}
|
|
17432
|
+
async function step() {
|
|
17433
|
+
try {
|
|
17434
|
+
const t = now();
|
|
17435
|
+
if (await intents.pending())
|
|
17436
|
+
await serveRequest(t);
|
|
17437
|
+
else
|
|
17438
|
+
await background(t);
|
|
17439
|
+
} catch {}
|
|
17440
|
+
}
|
|
17441
|
+
function tick() {
|
|
17442
|
+
if (busy || disposed)
|
|
17443
|
+
return Promise.resolve();
|
|
17444
|
+
busy = true;
|
|
17445
|
+
inflight = step().finally(() => {
|
|
17446
|
+
busy = false;
|
|
17447
|
+
});
|
|
17448
|
+
return inflight;
|
|
17449
|
+
}
|
|
17450
|
+
return {
|
|
17451
|
+
start() {
|
|
17452
|
+
handle = timers.set(() => void tick().catch(ignore), TICK_MS);
|
|
17453
|
+
tick().catch(ignore);
|
|
17454
|
+
},
|
|
17455
|
+
async dispose() {
|
|
17456
|
+
disposed = true;
|
|
17457
|
+
timers.clear(handle);
|
|
17458
|
+
let timer;
|
|
17459
|
+
await Promise.race([
|
|
17460
|
+
inflight,
|
|
17461
|
+
new Promise((resolve) => {
|
|
17462
|
+
timer = setTimeout(resolve, disposeWaitMs);
|
|
17463
|
+
})
|
|
17464
|
+
]);
|
|
17465
|
+
clearTimeout(timer);
|
|
17466
|
+
},
|
|
17467
|
+
tick
|
|
17468
|
+
};
|
|
17469
|
+
}
|
|
17470
|
+
|
|
17471
|
+
// src/sync/start.ts
|
|
17472
|
+
function syncEnabled(env = process.env) {
|
|
17473
|
+
const raw = env.OPENCODE_AUTH_LB_SYNC?.trim().toLowerCase();
|
|
17474
|
+
return raw !== "0" && raw !== "false" && raw !== "no" && raw !== "off";
|
|
17475
|
+
}
|
|
17476
|
+
function startSync() {
|
|
17477
|
+
if (!syncEnabled())
|
|
17478
|
+
return null;
|
|
17479
|
+
const loop = createSyncLoop({
|
|
17480
|
+
engine: createEngine({ now: Date.now }),
|
|
17481
|
+
now: Date.now
|
|
17482
|
+
});
|
|
17483
|
+
loop.start();
|
|
17484
|
+
return loop;
|
|
17485
|
+
}
|
|
17486
|
+
|
|
15854
17487
|
// src/index.ts
|
|
15855
17488
|
var PROVIDER_LABELS = {
|
|
15856
17489
|
anthropic: "Claude Pro/Max",
|
|
@@ -15870,8 +17503,17 @@ function zeroOutCost(provider) {
|
|
|
15870
17503
|
function buildAuthHook(adapter, client, pending) {
|
|
15871
17504
|
const label = PROVIDER_LABELS[adapter.id] ?? adapter.id;
|
|
15872
17505
|
const startDeviceLogin = adapter.startDeviceLogin;
|
|
17506
|
+
const tokenLogin = adapter.tokenLogin;
|
|
15873
17507
|
const register = async (tokens) => {
|
|
15874
|
-
|
|
17508
|
+
if (!tokens.usage) {
|
|
17509
|
+
const usage = await adapter.fetchUsage(makeAccount(adapter.id, "", tokens), Date.now()).catch(ignore);
|
|
17510
|
+
if (usage)
|
|
17511
|
+
tokens.usage = usage;
|
|
17512
|
+
}
|
|
17513
|
+
const account = await addAccount(adapter.id, tokens);
|
|
17514
|
+
const minted = await mintAtLogin(adapter, account);
|
|
17515
|
+
if (minted || account.inferenceToken !== undefined && account.refresh)
|
|
17516
|
+
notifyPaired(client, adapter.id, account);
|
|
15875
17517
|
await refreshUsageInBackground(adapter, Date.now()).catch(ignore);
|
|
15876
17518
|
};
|
|
15877
17519
|
const oauthSuccess = (tokens) => ({
|
|
@@ -15887,6 +17529,7 @@ function buildAuthHook(adapter, client, pending) {
|
|
|
15887
17529
|
zeroOutCost(provider);
|
|
15888
17530
|
await adapter.prime?.(Date.now()).catch(ignore);
|
|
15889
17531
|
refreshUsageInBackground(adapter, Date.now()).then(() => primeInUse(adapter.id, Date.now())).catch(ignore);
|
|
17532
|
+
maintainTokens(adapter, Date.now()).catch(ignore);
|
|
15890
17533
|
return {
|
|
15891
17534
|
apiKey: "",
|
|
15892
17535
|
fetch: createLoadBalancedFetch(adapter, {
|
|
@@ -15941,7 +17584,25 @@ function buildAuthHook(adapter, client, pending) {
|
|
|
15941
17584
|
}
|
|
15942
17585
|
};
|
|
15943
17586
|
}
|
|
15944
|
-
}
|
|
17587
|
+
},
|
|
17588
|
+
...tokenLogin ? [
|
|
17589
|
+
{
|
|
17590
|
+
label: `${label} ${tokenLogin.label} (add account to load balancer)`,
|
|
17591
|
+
type: "oauth",
|
|
17592
|
+
authorize: async () => ({
|
|
17593
|
+
url: tokenLogin.url,
|
|
17594
|
+
instructions: tokenLogin.instructions,
|
|
17595
|
+
method: "code",
|
|
17596
|
+
callback: async (code) => {
|
|
17597
|
+
const tokens = await tokenLogin.exchange(code);
|
|
17598
|
+
if (!tokens)
|
|
17599
|
+
return { type: "failed" };
|
|
17600
|
+
await register(tokens);
|
|
17601
|
+
return oauthSuccess(tokens);
|
|
17602
|
+
}
|
|
17603
|
+
})
|
|
17604
|
+
}
|
|
17605
|
+
] : []
|
|
15945
17606
|
]
|
|
15946
17607
|
};
|
|
15947
17608
|
}
|
|
@@ -15984,76 +17645,80 @@ var OpenAILoadBalancerPlugin = createProviderPlugin(openaiAdapter);
|
|
|
15984
17645
|
var KimiCodeLoadBalancerPlugin = createProviderPlugin(kimiCodeAdapter);
|
|
15985
17646
|
var KimiCodeGlobalLoadBalancerPlugin = createProviderPlugin(kimiCodeGlobalAdapter);
|
|
15986
17647
|
var lbResult = (output) => ({ title: "Auth Load Balancer", output });
|
|
15987
|
-
var AuthLoadBalancerStatusPlugin = async (input) =>
|
|
15988
|
-
|
|
15989
|
-
|
|
15990
|
-
|
|
15991
|
-
|
|
15992
|
-
|
|
15993
|
-
|
|
15994
|
-
|
|
15995
|
-
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
|
|
17648
|
+
var AuthLoadBalancerStatusPlugin = async (input) => {
|
|
17649
|
+
const sync = startSync();
|
|
17650
|
+
return {
|
|
17651
|
+
dispose: async () => sync?.dispose(),
|
|
17652
|
+
tool: {
|
|
17653
|
+
auth_lb_status: tool({
|
|
17654
|
+
description: "Show the auth load-balancer pool: the in-use account per provider, each account's weekly and 5h usage, cooldowns, and the ranked next-candidate accounts. Refreshes stale usage from the provider usage endpoints (throttled) before rendering.",
|
|
17655
|
+
args: {},
|
|
17656
|
+
execute: async () => {
|
|
17657
|
+
const now = Date.now();
|
|
17658
|
+
const pool = await readPool();
|
|
17659
|
+
await refreshAllUsageInBackground(now, pool).catch(ignore);
|
|
17660
|
+
const renderedAt = Date.now();
|
|
17661
|
+
const status = renderStatus(await readStatus(renderedAt), renderedAt);
|
|
17662
|
+
const pending = renderPendingStatus(await listPendingForWorkspace(input.worktree || input.directory), renderedAt);
|
|
17663
|
+
return lbResult(pending ? `${status}
|
|
16000
17664
|
|
|
16001
17665
|
${pending}` : status);
|
|
16002
|
-
|
|
16003
|
-
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16013
|
-
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
16018
|
-
|
|
16019
|
-
|
|
16020
|
-
|
|
16021
|
-
|
|
16022
|
-
|
|
16023
|
-
|
|
16024
|
-
|
|
16025
|
-
|
|
16026
|
-
|
|
16027
|
-
|
|
16028
|
-
|
|
16029
|
-
|
|
16030
|
-
|
|
16031
|
-
|
|
16032
|
-
|
|
16033
|
-
|
|
16034
|
-
|
|
16035
|
-
|
|
16036
|
-
|
|
16037
|
-
|
|
16038
|
-
|
|
16039
|
-
|
|
16040
|
-
|
|
16041
|
-
|
|
16042
|
-
|
|
16043
|
-
|
|
16044
|
-
|
|
16045
|
-
|
|
16046
|
-
|
|
16047
|
-
|
|
16048
|
-
|
|
16049
|
-
|
|
16050
|
-
|
|
16051
|
-
|
|
16052
|
-
|
|
16053
|
-
|
|
16054
|
-
|
|
16055
|
-
|
|
16056
|
-
}
|
|
17666
|
+
}
|
|
17667
|
+
}),
|
|
17668
|
+
auth_lb_rename: tool({
|
|
17669
|
+
description: "Rename a pooled account. Match the account by its current label or its id, then set a new label. The new label appears in the switch toast, the auth_lb_status dashboard, and the TUI bar/sidebar.",
|
|
17670
|
+
args: {
|
|
17671
|
+
account: tool.schema.string().describe("Current label or id of the account to rename"),
|
|
17672
|
+
name: tool.schema.string().describe("New label for the account")
|
|
17673
|
+
},
|
|
17674
|
+
execute: async ({ account, name }) => {
|
|
17675
|
+
const trimmed = name.trim();
|
|
17676
|
+
if (!trimmed)
|
|
17677
|
+
return lbResult("New label must not be empty.");
|
|
17678
|
+
const result = await mutatePool((pool) => {
|
|
17679
|
+
const target = pool.accounts.find((a) => a.id === account || a.label === account);
|
|
17680
|
+
if (!target)
|
|
17681
|
+
return {
|
|
17682
|
+
ok: false,
|
|
17683
|
+
reason: "missing",
|
|
17684
|
+
labels: pool.accounts.map((a) => `${a.label} (${a.providerID})`)
|
|
17685
|
+
};
|
|
17686
|
+
if (pool.accounts.some((a) => a.id !== target.id && a.label === trimmed))
|
|
17687
|
+
return { ok: false, reason: "taken" };
|
|
17688
|
+
const previous = target.label;
|
|
17689
|
+
target.label = trimmed;
|
|
17690
|
+
return { ok: true, previous };
|
|
17691
|
+
});
|
|
17692
|
+
if (!result.ok)
|
|
17693
|
+
return lbResult(result.reason === "taken" ? `Label "${trimmed}" is already used by another account.` : `No account matching "${account}". Available: ${result.labels.join(", ") || "(none)"}`);
|
|
17694
|
+
return lbResult(`Renamed "${result.previous}" → "${trimmed}".`);
|
|
17695
|
+
}
|
|
17696
|
+
}),
|
|
17697
|
+
auth_lb_disable: tool({
|
|
17698
|
+
description: "Disable a pooled account so the load balancer stops selecting it (match by current label or id), or re-enable a previously disabled one with `enable: true`. A disabled account is skipped by scheduling and shows as `disabled` in the auth_lb_status dashboard — separate from an account that needs re-login.",
|
|
17699
|
+
args: {
|
|
17700
|
+
account: tool.schema.string().describe("Current label or id of the account to disable/enable"),
|
|
17701
|
+
enable: tool.schema.boolean().optional().describe("Set true to re-enable a previously disabled account; omit or false to disable it")
|
|
17702
|
+
},
|
|
17703
|
+
execute: async ({ account, enable }) => {
|
|
17704
|
+
const result = await mutatePool((pool) => {
|
|
17705
|
+
const target = pool.accounts.find((a) => a.id === account || a.label === account);
|
|
17706
|
+
if (!target)
|
|
17707
|
+
return {
|
|
17708
|
+
ok: false,
|
|
17709
|
+
labels: pool.accounts.map((a) => `${a.label} (${a.providerID})`)
|
|
17710
|
+
};
|
|
17711
|
+
target.disabledReason = enable ? null : MANUAL_DISABLED_REASON;
|
|
17712
|
+
return { ok: true, label: target.label, enabled: !!enable };
|
|
17713
|
+
});
|
|
17714
|
+
if (!result.ok)
|
|
17715
|
+
return lbResult(`No account matching "${account}". Available: ${result.labels.join(", ") || "(none)"}`);
|
|
17716
|
+
return lbResult(result.enabled ? `Enabled "${result.label}" — back in the load-balancer rotation.` : `Disabled "${result.label}" — the load balancer will skip it until you re-enable it.`);
|
|
17717
|
+
}
|
|
17718
|
+
})
|
|
17719
|
+
}
|
|
17720
|
+
};
|
|
17721
|
+
};
|
|
16057
17722
|
export {
|
|
16058
17723
|
AnthropicLoadBalancerPlugin,
|
|
16059
17724
|
AuthLoadBalancerStatusPlugin,
|