opencode-auth-load-balancer 0.5.0 → 0.6.0

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