opencode-auth-load-balancer 0.5.1 → 0.6.0

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