opencode-auth-load-balancer 0.5.1 → 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 +1898 -234
- 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 +12 -0
- 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 +75 -0
- 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);
|
|
@@ -13173,13 +13373,22 @@ async function commitRefresh(accountId, attempt, next) {
|
|
|
13173
13373
|
return next;
|
|
13174
13374
|
});
|
|
13175
13375
|
}
|
|
13176
|
-
async function resolveInvalidGrant(adapter, accountId, attempt) {
|
|
13376
|
+
async function resolveInvalidGrant(adapter, accountId, attempt, reason) {
|
|
13177
13377
|
return mutatePool((pool) => {
|
|
13178
13378
|
const stored = findAccount(pool, accountId);
|
|
13179
13379
|
if (!stored)
|
|
13180
13380
|
return null;
|
|
13181
13381
|
if (!sameGeneration(stored, attempt))
|
|
13182
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
|
+
}
|
|
13183
13392
|
stored.disabledReason = disableReason(adapter, stored.label);
|
|
13184
13393
|
return null;
|
|
13185
13394
|
});
|
|
@@ -13197,9 +13406,9 @@ async function runRefresh(adapter, account, now) {
|
|
|
13197
13406
|
const next = await adapter.refresh(attempt.refresh);
|
|
13198
13407
|
return await commitRefresh(account.id, attempt, next);
|
|
13199
13408
|
} catch (error) {
|
|
13200
|
-
if (!isInvalidGrant(error))
|
|
13409
|
+
if (!(error instanceof Error) || !isInvalidGrant(error))
|
|
13201
13410
|
throw error;
|
|
13202
|
-
const adopted = await resolveInvalidGrant(adapter, account.id, attempt);
|
|
13411
|
+
const adopted = await resolveInvalidGrant(adapter, account.id, attempt, refusalReason(error));
|
|
13203
13412
|
if (adopted)
|
|
13204
13413
|
return adopted;
|
|
13205
13414
|
throw error;
|
|
@@ -13395,22 +13604,81 @@ function deriveSessionKey(headers, body) {
|
|
|
13395
13604
|
}
|
|
13396
13605
|
}
|
|
13397
13606
|
|
|
13398
|
-
// src/
|
|
13399
|
-
|
|
13400
|
-
|
|
13401
|
-
|
|
13402
|
-
|
|
13403
|
-
|
|
13404
|
-
|
|
13405
|
-
|
|
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;
|
|
13406
13624
|
}
|
|
13407
|
-
function
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
|
|
13411
|
-
|
|
13412
|
-
|
|
13413
|
-
|
|
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);
|
|
13414
13682
|
}
|
|
13415
13683
|
|
|
13416
13684
|
// src/providers/types.ts
|
|
@@ -13661,6 +13929,24 @@ async function postToken(body) {
|
|
|
13661
13929
|
signal: AbortSignal.timeout(OAUTH_HTTP_TIMEOUT_MS)
|
|
13662
13930
|
});
|
|
13663
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
|
+
}
|
|
13664
13950
|
async function authorize() {
|
|
13665
13951
|
const pkce = await generatePKCE();
|
|
13666
13952
|
const state = generateState();
|
|
@@ -13697,7 +13983,7 @@ async function exchange(input, verifier, redirectUri, expectedState) {
|
|
|
13697
13983
|
const json = await readExchangeResponse(result);
|
|
13698
13984
|
if (!json)
|
|
13699
13985
|
return null;
|
|
13700
|
-
return toTokenSet(json, "");
|
|
13986
|
+
return withAccount(toTokenSet(json, ""), json, true);
|
|
13701
13987
|
}
|
|
13702
13988
|
async function refresh(refreshToken) {
|
|
13703
13989
|
const response = await postToken({
|
|
@@ -13706,36 +13992,78 @@ async function refresh(refreshToken) {
|
|
|
13706
13992
|
client_id: CLIENT_ID
|
|
13707
13993
|
});
|
|
13708
13994
|
const json = await readRefreshResponse(response);
|
|
13709
|
-
return toTokenSet(json, refreshToken);
|
|
13995
|
+
return withAccount(toTokenSet(json, refreshToken), json, false);
|
|
13710
13996
|
}
|
|
13711
|
-
|
|
13712
|
-
|
|
13713
|
-
|
|
13714
|
-
|
|
13715
|
-
|
|
13716
|
-
|
|
13717
|
-
|
|
13718
|
-
|
|
13719
|
-
|
|
13720
|
-
|
|
13721
|
-
try {
|
|
13722
|
-
response = await fetch(url, {
|
|
13723
|
-
headers,
|
|
13724
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
13725
|
-
});
|
|
13726
|
-
} catch {
|
|
13727
|
-
return null;
|
|
13728
|
-
}
|
|
13729
|
-
if (!response.ok) {
|
|
13730
|
-
await response.body?.cancel().catch(ignore);
|
|
13731
|
-
return null;
|
|
13732
|
-
}
|
|
13733
|
-
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);
|
|
13734
14007
|
}
|
|
13735
14008
|
|
|
13736
|
-
// src/providers/
|
|
13737
|
-
|
|
13738
|
-
|
|
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) {
|
|
13739
14067
|
const left = a.split(".");
|
|
13740
14068
|
const right = b.split(".");
|
|
13741
14069
|
for (let i = 0;i < 3; i++) {
|
|
@@ -14087,39 +14415,6 @@ function createStrippedStream(response) {
|
|
|
14087
14415
|
});
|
|
14088
14416
|
}
|
|
14089
14417
|
|
|
14090
|
-
// src/providers/usage-headers.ts
|
|
14091
|
-
function headerWindow(utilRaw, resetRaw, divisor) {
|
|
14092
|
-
const util = Number(utilRaw);
|
|
14093
|
-
if (!Number.isFinite(util))
|
|
14094
|
-
return null;
|
|
14095
|
-
return {
|
|
14096
|
-
utilization: clamp01(util / divisor),
|
|
14097
|
-
resetAt: secondsToMs(Number(resetRaw))
|
|
14098
|
-
};
|
|
14099
|
-
}
|
|
14100
|
-
function parseWindowPairHeaders(headers, spec) {
|
|
14101
|
-
const h = headers.get(spec.hourlyUtil);
|
|
14102
|
-
const w = headers.get(spec.weeklyUtil);
|
|
14103
|
-
if (h === null && w === null)
|
|
14104
|
-
return null;
|
|
14105
|
-
const out = {};
|
|
14106
|
-
const hourly = h === null ? null : headerWindow(h, headers.get(spec.hourlyReset), spec.divisor);
|
|
14107
|
-
const weekly = w === null ? null : headerWindow(w, headers.get(spec.weeklyReset), spec.divisor);
|
|
14108
|
-
if (hourly)
|
|
14109
|
-
out.hourly = hourly;
|
|
14110
|
-
if (weekly)
|
|
14111
|
-
out.weekly = weekly;
|
|
14112
|
-
return hourly || weekly ? out : null;
|
|
14113
|
-
}
|
|
14114
|
-
function endpointWindowFrom(w, utilOf, divisor, resetOf) {
|
|
14115
|
-
if (!w)
|
|
14116
|
-
return { utilization: 0, resetAt: 0 };
|
|
14117
|
-
const util = utilOf(w);
|
|
14118
|
-
if (!isFiniteNumber(util))
|
|
14119
|
-
return null;
|
|
14120
|
-
return { utilization: clamp01(util / divisor), resetAt: resetOf(w) };
|
|
14121
|
-
}
|
|
14122
|
-
|
|
14123
14418
|
// src/providers/anthropic/usage.ts
|
|
14124
14419
|
var HEADER_SPEC = {
|
|
14125
14420
|
hourlyUtil: "anthropic-ratelimit-unified-5h-utilization",
|
|
@@ -14151,7 +14446,46 @@ function parseResetAt(value) {
|
|
|
14151
14446
|
function endpointWindow(w) {
|
|
14152
14447
|
return endpointWindowFrom(w, (win) => win.utilization, 100, (win) => parseResetAt(win.resets_at));
|
|
14153
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
|
+
}
|
|
14154
14486
|
async function fetchUsage(account, now) {
|
|
14487
|
+
if (!account.refresh)
|
|
14488
|
+
return probeUsage(account.access, now);
|
|
14155
14489
|
const json = await fetchJson(USAGE_URL, {
|
|
14156
14490
|
authorization: `Bearer ${account.access}`,
|
|
14157
14491
|
"anthropic-beta": "oauth-2025-04-20",
|
|
@@ -14169,12 +14503,50 @@ async function fetchUsage(account, now) {
|
|
|
14169
14503
|
};
|
|
14170
14504
|
}
|
|
14171
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
|
+
|
|
14172
14542
|
// src/providers/anthropic/adapter.ts
|
|
14173
14543
|
var anthropicAdapter = {
|
|
14174
14544
|
id: PROVIDER_ID,
|
|
14175
14545
|
authorize,
|
|
14176
14546
|
exchange,
|
|
14177
14547
|
refresh,
|
|
14548
|
+
tokenLogin: setupTokenLogin,
|
|
14549
|
+
mintInferenceToken,
|
|
14178
14550
|
applyAuth(headers, account) {
|
|
14179
14551
|
setOAuthHeaders(headers, account.access);
|
|
14180
14552
|
},
|
|
@@ -14828,7 +15200,7 @@ async function applyCooldown(accountId, fallbackMs, kind) {
|
|
|
14828
15200
|
}
|
|
14829
15201
|
}));
|
|
14830
15202
|
}
|
|
14831
|
-
async function recordRotation(adapter, accountId, res, fallbackMs, now, kind,
|
|
15203
|
+
async function recordRotation(adapter, accountId, res, fallbackMs, now, kind, revoked) {
|
|
14832
15204
|
const partial = adapter.parseUsageHeaders(res.headers);
|
|
14833
15205
|
const until = cooldownUntilFrom(res, fallbackMs, now);
|
|
14834
15206
|
await bestEffort("rotation", () => mutatePool((pool) => {
|
|
@@ -14841,8 +15213,10 @@ async function recordRotation(adapter, accountId, res, fallbackMs, now, kind, re
|
|
|
14841
15213
|
account.cooldownUntil = until;
|
|
14842
15214
|
account.cooldownKind = kind;
|
|
14843
15215
|
}
|
|
14844
|
-
if (
|
|
14845
|
-
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
|
+
}
|
|
14846
15220
|
}));
|
|
14847
15221
|
}
|
|
14848
15222
|
async function recordModelCooldown(accountId, tier, until, partial, now) {
|
|
@@ -14951,6 +15325,7 @@ function createLoadBalancedFetch(adapter, hooks = {}, models = [], pending) {
|
|
|
14951
15325
|
let pendingTierFallback = null;
|
|
14952
15326
|
let tierSkipped = null;
|
|
14953
15327
|
let clientVersionRetried = false;
|
|
15328
|
+
let rejectedTokens = null;
|
|
14954
15329
|
let usageSeeded = false;
|
|
14955
15330
|
const waitDeadline = Date.now() + cfg.maxWaitMs;
|
|
14956
15331
|
for (;; ) {
|
|
@@ -14959,6 +15334,7 @@ function createLoadBalancedFetch(adapter, hooks = {}, models = [], pending) {
|
|
|
14959
15334
|
if (!usageSeeded) {
|
|
14960
15335
|
usageSeeded = true;
|
|
14961
15336
|
refreshAllUsageInBackground(now, pool).catch(ignore);
|
|
15337
|
+
maintainTokens(adapter, now, pool).catch(ignore);
|
|
14962
15338
|
}
|
|
14963
15339
|
if (pendingRef && pending) {
|
|
14964
15340
|
const recovery = classifyProviderRecovery(pool.accounts, adapter.id, cfg, now, USAGE_REFRESH_TTL_MS);
|
|
@@ -15022,10 +15398,12 @@ function createLoadBalancedFetch(adapter, hooks = {}, models = [], pending) {
|
|
|
15022
15398
|
continue;
|
|
15023
15399
|
}
|
|
15024
15400
|
}
|
|
15401
|
+
const token = account.inferenceToken !== undefined && !rejectedTokens?.has(account.inferenceToken) ? account.inferenceToken : undefined;
|
|
15025
15402
|
try {
|
|
15026
|
-
|
|
15403
|
+
if (token === undefined)
|
|
15404
|
+
await ensureAccessToken(adapter, account, now);
|
|
15027
15405
|
const headers = new Headers(baseHeaders);
|
|
15028
|
-
adapter.applyAuth(headers, account);
|
|
15406
|
+
adapter.applyAuth(headers, token === undefined ? account : { ...account, access: token });
|
|
15029
15407
|
if (DEBUG)
|
|
15030
15408
|
log(`-> ${adapter.id} via ${account.label}${sticky ? " (sticky)" : ""}${degraded ? " (degraded)" : ""}`);
|
|
15031
15409
|
const res = await fetch(transformedUrl, {
|
|
@@ -15035,6 +15413,7 @@ function createLoadBalancedFetch(adapter, hooks = {}, models = [], pending) {
|
|
|
15035
15413
|
});
|
|
15036
15414
|
const cls = adapter.classifyError(res.status);
|
|
15037
15415
|
if (cls === "account" || cls === "auth") {
|
|
15416
|
+
const rejection = res.status === 401 ? errorSummary(401, await res.text().catch(() => "")) : "";
|
|
15038
15417
|
await res.body?.cancel().catch(ignore);
|
|
15039
15418
|
if (cls === "account" && typeof currentBody === "string") {
|
|
15040
15419
|
const reactiveNow = Date.now();
|
|
@@ -15052,8 +15431,18 @@ function createLoadBalancedFetch(adapter, hooks = {}, models = [], pending) {
|
|
|
15052
15431
|
continue;
|
|
15053
15432
|
}
|
|
15054
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
|
+
}
|
|
15055
15444
|
const ms = cls === "auth" ? AUTH_COOLDOWN_MS : ACCOUNT_COOLDOWN_MS;
|
|
15056
|
-
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);
|
|
15057
15446
|
if (cls === "account") {
|
|
15058
15447
|
waitableCooldownIds ??= new Set;
|
|
15059
15448
|
waitableCooldownIds.add(account.id);
|
|
@@ -15108,6 +15497,47 @@ function createLoadBalancedFetch(adapter, hooks = {}, models = [], pending) {
|
|
|
15108
15497
|
};
|
|
15109
15498
|
}
|
|
15110
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
|
+
|
|
15111
15541
|
// src/status.ts
|
|
15112
15542
|
var PROVIDER_NAMES = {
|
|
15113
15543
|
anthropic: "Claude",
|
|
@@ -15137,6 +15567,9 @@ function toStatus(account, now, current, available, rank) {
|
|
|
15137
15567
|
cooldownUntil: account.cooldownUntil,
|
|
15138
15568
|
modelCooldownsUntil: account.modelCooldownsUntil ?? {},
|
|
15139
15569
|
disabledReason: account.disabledReason,
|
|
15570
|
+
login: credentialTag(account),
|
|
15571
|
+
warnings: loginWarnings(account, now),
|
|
15572
|
+
lostNotes: lostLoginNotes(account, now),
|
|
15140
15573
|
current,
|
|
15141
15574
|
rank
|
|
15142
15575
|
};
|
|
@@ -15250,8 +15683,12 @@ function renderStatus(providers, now = Date.now()) {
|
|
|
15250
15683
|
lines.push(` # ${"account".padEnd(width + 3)}weekly 5h resets state`);
|
|
15251
15684
|
for (const a of p.accounts) {
|
|
15252
15685
|
const mark = a.current ? "▶" : " ";
|
|
15253
|
-
|
|
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}`);
|
|
15254
15688
|
}
|
|
15689
|
+
for (const a of p.accounts)
|
|
15690
|
+
for (const note of a.lostNotes)
|
|
15691
|
+
lines.push(` ! ${a.label}: ${note}`);
|
|
15255
15692
|
lines.push("");
|
|
15256
15693
|
}
|
|
15257
15694
|
return lines.join(`
|
|
@@ -15276,6 +15713,14 @@ async function notifyOnSwitch(client, providerID, account) {
|
|
|
15276
15713
|
duration: 4000
|
|
15277
15714
|
});
|
|
15278
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
|
+
}
|
|
15279
15724
|
var lastFallbackToasted = new Map;
|
|
15280
15725
|
var LAST_FALLBACK_TOASTED_MAX = 256;
|
|
15281
15726
|
async function notifyModelFallback(client, providerID, account, fromModel, toModel, fromTier) {
|
|
@@ -15852,6 +16297,1193 @@ async function primeInUse(providerID, now) {
|
|
|
15852
16297
|
});
|
|
15853
16298
|
}
|
|
15854
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
|
+
|
|
15855
17487
|
// src/index.ts
|
|
15856
17488
|
var PROVIDER_LABELS = {
|
|
15857
17489
|
anthropic: "Claude Pro/Max",
|
|
@@ -15871,8 +17503,17 @@ function zeroOutCost(provider) {
|
|
|
15871
17503
|
function buildAuthHook(adapter, client, pending) {
|
|
15872
17504
|
const label = PROVIDER_LABELS[adapter.id] ?? adapter.id;
|
|
15873
17505
|
const startDeviceLogin = adapter.startDeviceLogin;
|
|
17506
|
+
const tokenLogin = adapter.tokenLogin;
|
|
15874
17507
|
const register = async (tokens) => {
|
|
15875
|
-
|
|
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);
|
|
15876
17517
|
await refreshUsageInBackground(adapter, Date.now()).catch(ignore);
|
|
15877
17518
|
};
|
|
15878
17519
|
const oauthSuccess = (tokens) => ({
|
|
@@ -15888,6 +17529,7 @@ function buildAuthHook(adapter, client, pending) {
|
|
|
15888
17529
|
zeroOutCost(provider);
|
|
15889
17530
|
await adapter.prime?.(Date.now()).catch(ignore);
|
|
15890
17531
|
refreshUsageInBackground(adapter, Date.now()).then(() => primeInUse(adapter.id, Date.now())).catch(ignore);
|
|
17532
|
+
maintainTokens(adapter, Date.now()).catch(ignore);
|
|
15891
17533
|
return {
|
|
15892
17534
|
apiKey: "",
|
|
15893
17535
|
fetch: createLoadBalancedFetch(adapter, {
|
|
@@ -15942,7 +17584,25 @@ function buildAuthHook(adapter, client, pending) {
|
|
|
15942
17584
|
}
|
|
15943
17585
|
};
|
|
15944
17586
|
}
|
|
15945
|
-
}
|
|
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
|
+
] : []
|
|
15946
17606
|
]
|
|
15947
17607
|
};
|
|
15948
17608
|
}
|
|
@@ -15985,76 +17645,80 @@ var OpenAILoadBalancerPlugin = createProviderPlugin(openaiAdapter);
|
|
|
15985
17645
|
var KimiCodeLoadBalancerPlugin = createProviderPlugin(kimiCodeAdapter);
|
|
15986
17646
|
var KimiCodeGlobalLoadBalancerPlugin = createProviderPlugin(kimiCodeGlobalAdapter);
|
|
15987
17647
|
var lbResult = (output) => ({ title: "Auth Load Balancer", output });
|
|
15988
|
-
var AuthLoadBalancerStatusPlugin = async (input) =>
|
|
15989
|
-
|
|
15990
|
-
|
|
15991
|
-
|
|
15992
|
-
|
|
15993
|
-
|
|
15994
|
-
|
|
15995
|
-
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
|
|
16000
|
-
|
|
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}
|
|
16001
17664
|
|
|
16002
17665
|
${pending}` : status);
|
|
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
|
-
|
|
16057
|
-
}
|
|
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
|
+
};
|
|
16058
17722
|
export {
|
|
16059
17723
|
AnthropicLoadBalancerPlugin,
|
|
16060
17724
|
AuthLoadBalancerStatusPlugin,
|