opencode-auth-load-balancer 0.8.1 → 0.9.1

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
@@ -12680,6 +12680,15 @@ var init_tool = __esm(() => {
12680
12680
  tool.schema = exports_external;
12681
12681
  });
12682
12682
 
12683
+ // src/login-rev.ts
12684
+ function bumpLoginRev(row, now = Date.now()) {
12685
+ row.loginRev = (row.loginRev ?? 1) + 1;
12686
+ row.loginAt = now;
12687
+ }
12688
+ function loginVersion(row) {
12689
+ return { rev: row.loginRev ?? 1, at: row.loginAt ?? 0 };
12690
+ }
12691
+
12683
12692
  // src/types.ts
12684
12693
  function emptyUsage() {
12685
12694
  return { hourly: null, weekly: null, capturedAt: 0 };
@@ -13110,6 +13119,10 @@ function normalizeAccounts(rows) {
13110
13119
  delete row.orgId;
13111
13120
  if (!isFiniteNumber(row.refreshExpires) || row.refreshExpires <= 0)
13112
13121
  delete row.refreshExpires;
13122
+ if (typeof row.loginRev !== "number" || !Number.isSafeInteger(row.loginRev) || row.loginRev < 1)
13123
+ delete row.loginRev;
13124
+ if (!isFiniteNumber(row.loginAt) || row.loginAt < 0)
13125
+ delete row.loginAt;
13113
13126
  if (row.inferenceToken === undefined || !isFiniteNumber(row.inferenceExpires) || row.inferenceExpires <= 0)
13114
13127
  delete row.inferenceExpires;
13115
13128
  if (row.lostLogins !== undefined) {
@@ -13372,6 +13385,14 @@ var init_score_core = __esm(() => {
13372
13385
  });
13373
13386
 
13374
13387
  // src/usage-merge.ts
13388
+ function reconcileQuotaCooldown(account, cfg, now) {
13389
+ if (account.cooldownKind === "auth" || account.cooldownKind === "transient")
13390
+ return;
13391
+ if (account.cooldownUntil > now + MAX_TRANSIENT_COOLDOWN_MS && !isExhausted(account, cfg, now)) {
13392
+ account.cooldownUntil = 0;
13393
+ delete account.cooldownKind;
13394
+ }
13395
+ }
13375
13396
  function rollWeeklyAnchorForward(anchor, now) {
13376
13397
  if (!Number.isFinite(anchor) || anchor <= 0)
13377
13398
  return 0;
@@ -13388,8 +13409,10 @@ function preserveWeeklyAnchor(incoming, stored, now) {
13388
13409
  return incoming;
13389
13410
  return { utilization: incoming.utilization, resetAt: anchor };
13390
13411
  }
13412
+ var MAX_TRANSIENT_COOLDOWN_MS;
13391
13413
  var init_usage_merge = __esm(() => {
13392
13414
  init_score_core();
13415
+ MAX_TRANSIENT_COOLDOWN_MS = 5 * 60 * 1000;
13393
13416
  });
13394
13417
 
13395
13418
  // src/accounts.ts
@@ -13417,6 +13440,10 @@ function makeAccount(providerID, label, tokens) {
13417
13440
  account.orgId = tokens.orgId;
13418
13441
  if (tokens.refreshExpires)
13419
13442
  account.refreshExpires = tokens.refreshExpires;
13443
+ if (tokens.refresh && !tokens.inferenceOnly) {
13444
+ account.loginRev = 1;
13445
+ account.loginAt = Date.now();
13446
+ }
13420
13447
  return account;
13421
13448
  }
13422
13449
  function recordLostLogin(row, login, reason) {
@@ -13437,10 +13464,14 @@ function mergeLoginUsage(row, usage) {
13437
13464
  weekly: usage.weekly ? preserveWeeklyAnchor(usage.weekly, row.usage.weekly, usage.capturedAt) : row.usage.weekly,
13438
13465
  capturedAt: usage.weekly ? usage.capturedAt : row.usage.capturedAt
13439
13466
  };
13467
+ if (usage.weekly)
13468
+ reconcileQuotaCooldown(row, loadScoreConfig(), usage.capturedAt);
13440
13469
  }
13441
13470
  function applyTokens(row, tokens) {
13442
- if (tokens.refresh)
13471
+ if (tokens.refresh) {
13443
13472
  row.refresh = tokens.refresh;
13473
+ bumpLoginRev(row);
13474
+ }
13444
13475
  row.access = tokens.access;
13445
13476
  row.expires = tokens.expires;
13446
13477
  row.disabledReason = null;
@@ -13528,6 +13559,7 @@ async function bootstrapFromOpencodeAuth(providerID, getAuth, tokensFromApiKey)
13528
13559
  var init_accounts = __esm(() => {
13529
13560
  init_pairing();
13530
13561
  init_store();
13562
+ init_score_core();
13531
13563
  init_types();
13532
13564
  init_usage_merge();
13533
13565
  });
@@ -13650,6 +13682,15 @@ function mergeHeaders(input, init) {
13650
13682
  return headers;
13651
13683
  }
13652
13684
 
13685
+ // src/refresh-hooks.ts
13686
+ function setRefreshHooks(hooks) {
13687
+ current = hooks;
13688
+ }
13689
+ function getRefreshHooks() {
13690
+ return current;
13691
+ }
13692
+ var current = null;
13693
+
13653
13694
  // src/refresh.ts
13654
13695
  function needsRefresh(account, now) {
13655
13696
  return !account.access || account.expires - REFRESH_SKEW_MS <= now;
@@ -13708,6 +13749,7 @@ async function commitRefresh(accountId, attempt, next) {
13708
13749
  if (!sameGeneration(stored, attempt))
13709
13750
  return tokensOf(stored);
13710
13751
  applyTokensTo(stored, next);
13752
+ bumpLoginRev(stored);
13711
13753
  stored.tokenGen = attempt.gen + 1;
13712
13754
  if (stored.disabledReason !== MANUAL_DISABLED_REASON)
13713
13755
  stored.disabledReason = null;
@@ -13734,11 +13776,31 @@ async function resolveInvalidGrant(adapter, accountId, attempt, reason) {
13734
13776
  return null;
13735
13777
  });
13736
13778
  }
13779
+ async function pullNewer(account, recovery) {
13780
+ try {
13781
+ const adopted = await getRefreshHooks()?.pull?.(account, recovery);
13782
+ return adopted ? await readPoolAccount(account.id) : undefined;
13783
+ } catch {
13784
+ return;
13785
+ }
13786
+ }
13787
+ function announceRefresh() {
13788
+ try {
13789
+ getRefreshHooks()?.afterRefresh?.();
13790
+ } catch {
13791
+ ignore();
13792
+ }
13793
+ }
13737
13794
  async function runRefresh(adapter, account, now, rejectedAccess) {
13738
13795
  return withLock(refreshLockDir(adapter.id, account.id), REFRESH_LOCK, async () => {
13739
- const latest = await readPoolAccount(account.id) ?? account;
13740
- if (latest.access !== rejectedAccess && !needsRefresh(latest, Math.max(now, Date.now())))
13741
- return tokensOf(latest);
13796
+ const read = await readPoolAccount(account.id) ?? account;
13797
+ const usable = (row) => row.access !== rejectedAccess && !needsRefresh(row, Math.max(now, Date.now()));
13798
+ if (usable(read))
13799
+ return tokensOf(read);
13800
+ const pulled = await pullNewer(read, false);
13801
+ if (pulled && usable(pulled))
13802
+ return tokensOf(pulled);
13803
+ const latest = pulled ?? read;
13742
13804
  const attempt = {
13743
13805
  refresh: latest.refresh,
13744
13806
  gen: genOf(latest)
@@ -13749,6 +13811,9 @@ async function runRefresh(adapter, account, now, rejectedAccess) {
13749
13811
  } catch (error) {
13750
13812
  if (!(error instanceof Error) || !isInvalidGrant(error))
13751
13813
  throw error;
13814
+ const recovered = await pullNewer(latest, true);
13815
+ if (recovered)
13816
+ return tokensOf(recovered);
13752
13817
  const adopted = await resolveInvalidGrant(adapter, account.id, attempt, refusalReason(error));
13753
13818
  if (adopted)
13754
13819
  return adopted;
@@ -13776,7 +13841,9 @@ async function ensureAccessToken(adapter, account, now, rejectedAccess) {
13776
13841
  const job = runRefresh(adapter, account, now, rejectedAccess);
13777
13842
  inflight.set(account.id, job);
13778
13843
  try {
13779
- return await settleRefresh(adapter, account, job);
13844
+ const access = await settleRefresh(adapter, account, job);
13845
+ announceRefresh();
13846
+ return access;
13780
13847
  } finally {
13781
13848
  inflight.delete(account.id);
13782
13849
  }
@@ -13975,7 +14042,7 @@ var init_session = __esm(() => {
13975
14042
  // src/token-mint.ts
13976
14043
  function autoTokenEnabled(env = process.env) {
13977
14044
  const raw = env.OPENCODE_AUTH_LB_ANTHROPIC_AUTO_TOKEN?.trim().toLowerCase();
13978
- return raw !== "0" && raw !== "false" && raw !== "no" && raw !== "off";
14045
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
13979
14046
  }
13980
14047
  function tokenDue(account, now) {
13981
14048
  if (!account.refresh || account.disabledReason)
@@ -14004,6 +14071,7 @@ async function mintToken(adapter, accountId) {
14004
14071
  if (minted.refresh !== row.refresh) {
14005
14072
  row.refresh = minted.refresh;
14006
14073
  row.tokenGen = attempt.gen + 1;
14074
+ bumpLoginRev(row);
14007
14075
  }
14008
14076
  if (minted.refreshExpires)
14009
14077
  row.refreshExpires = minted.refreshExpires;
@@ -14433,24 +14501,46 @@ var init_usage_headers = __esm(() => {
14433
14501
  });
14434
14502
 
14435
14503
  // src/providers/usage-http.ts
14436
- async function fetchJson(url, headers, timeoutMs) {
14437
- let response;
14504
+ async function request(url, headers, timeoutMs) {
14438
14505
  try {
14439
- response = await fetch(url, {
14506
+ return await fetch(url, {
14440
14507
  headers,
14441
14508
  signal: AbortSignal.timeout(timeoutMs)
14442
14509
  });
14443
14510
  } catch {
14444
14511
  return null;
14445
14512
  }
14513
+ }
14514
+ async function readJson(response) {
14515
+ if (!response)
14516
+ return null;
14446
14517
  if (!response.ok) {
14447
14518
  await response.body?.cancel().catch(ignore);
14448
14519
  return null;
14449
14520
  }
14450
14521
  return await response.json().catch(() => null);
14451
14522
  }
14523
+ async function fetchJson(url, headers, timeoutMs) {
14524
+ return readJson(await request(url, headers, timeoutMs));
14525
+ }
14526
+ async function fetchUsageJson(url, headers, timeoutMs) {
14527
+ const response = await request(url, headers, timeoutMs);
14528
+ if (response?.status === 401) {
14529
+ await response.body?.cancel().catch(ignore);
14530
+ throw new UsageAuthenticationError;
14531
+ }
14532
+ return readJson(response);
14533
+ }
14534
+ var UsageAuthenticationError;
14452
14535
  var init_usage_http = __esm(() => {
14453
14536
  init_util2();
14537
+ UsageAuthenticationError = class UsageAuthenticationError extends Error {
14538
+ status = 401;
14539
+ constructor() {
14540
+ super("Usage endpoint rejected the access token (401)");
14541
+ this.name = "UsageAuthenticationError";
14542
+ }
14543
+ };
14454
14544
  });
14455
14545
 
14456
14546
  // src/providers/anthropic/version.ts
@@ -14469,16 +14559,16 @@ function adopt(candidate) {
14469
14559
  const trimmed = candidate.trim();
14470
14560
  if (!VERSION_RE.test(trimmed))
14471
14561
  return;
14472
- if (compareVersions2(trimmed, current) <= 0)
14562
+ if (compareVersions2(trimmed, current2) <= 0)
14473
14563
  return;
14474
- current = trimmed;
14564
+ current2 = trimmed;
14475
14565
  }
14476
14566
  function envOverride() {
14477
14567
  const raw = process.env[CLAUDE_CODE_VERSION_ENV]?.trim();
14478
14568
  return raw && VERSION_RE.test(raw) ? raw : undefined;
14479
14569
  }
14480
14570
  function claudeCodeVersion() {
14481
- return envOverride() ?? current;
14571
+ return envOverride() ?? current2;
14482
14572
  }
14483
14573
  function userAgent() {
14484
14574
  return `claude-cli/${claudeCodeVersion()} (external, cli)`;
@@ -14513,7 +14603,7 @@ async function refreshClaudeCodeVersion(now) {
14513
14603
  await persist(now);
14514
14604
  }
14515
14605
  async function persist(now) {
14516
- await writeJsonAtomic(versionCacheFilePath(), JSON.stringify({ version: current, fetchedAt: now })).catch(ignore);
14606
+ await writeJsonAtomic(versionCacheFilePath(), JSON.stringify({ version: current2, fetchedAt: now })).catch(ignore);
14517
14607
  }
14518
14608
  async function recoverClaudeCodeVersion(res, now) {
14519
14609
  if (envOverride())
@@ -14526,12 +14616,12 @@ async function recoverClaudeCodeVersion(res, now) {
14526
14616
  }
14527
14617
  if (!text.includes(CLAUDE_CODE_VERSION_GATE_CODE))
14528
14618
  return false;
14529
- const before = current;
14619
+ const before = current2;
14530
14620
  const required = CLAUDE_CODE_REQUIRED_VERSION_RE.exec(text)?.[1];
14531
14621
  if (required !== undefined)
14532
14622
  adopt(required);
14533
14623
  await refreshClaudeCodeVersion(now).catch(ignore);
14534
- if (current === before)
14624
+ if (current2 === before)
14535
14625
  return false;
14536
14626
  await persist(now);
14537
14627
  return true;
@@ -14547,7 +14637,7 @@ async function primeClaudeCodeVersion(now) {
14547
14637
  }
14548
14638
  refreshClaudeCodeVersion(now).catch(ignore);
14549
14639
  }
14550
- var VERSION_RE, current;
14640
+ var VERSION_RE, current2;
14551
14641
  var init_version = __esm(() => {
14552
14642
  init_paths();
14553
14643
  init_store();
@@ -14555,7 +14645,7 @@ var init_version = __esm(() => {
14555
14645
  init_usage_http();
14556
14646
  init_constants();
14557
14647
  VERSION_RE = /^\d+\.\d+\.\d+$/;
14558
- current = FALLBACK_CLAUDE_CODE_VERSION;
14648
+ current2 = FALLBACK_CLAUDE_CODE_VERSION;
14559
14649
  });
14560
14650
 
14561
14651
  // src/providers/anthropic/cch.ts
@@ -14890,7 +14980,7 @@ async function probeUsage(access, now) {
14890
14980
  async function fetchUsage(account, now) {
14891
14981
  if (!account.refresh)
14892
14982
  return probeUsage(account.access, now);
14893
- const json = await fetchJson(USAGE_URL, {
14983
+ const json = await fetchUsageJson(USAGE_URL, {
14894
14984
  authorization: `Bearer ${account.access}`,
14895
14985
  "anthropic-beta": "oauth-2025-04-20",
14896
14986
  "user-agent": usageUserAgent(),
@@ -15222,7 +15312,7 @@ function parseUsages(json, now) {
15222
15312
  return hourly || weekly ? { hourly, weekly, capturedAt: now } : null;
15223
15313
  }
15224
15314
  function getUsages(baseUrl, key) {
15225
- return fetchJson(`${baseUrl}/usages`, { authorization: `Bearer ${key}`, accept: "application/json" }, USAGE_HTTP_TIMEOUT_MS);
15315
+ return fetchUsageJson(`${baseUrl}/usages`, { authorization: `Bearer ${key}`, accept: "application/json" }, USAGE_HTTP_TIMEOUT_MS);
15226
15316
  }
15227
15317
  function usageFetcher(baseUrl) {
15228
15318
  return async (account, now) => parseUsages(await getUsages(baseUrl, account.access), now);
@@ -15496,7 +15586,7 @@ async function fetchUsage2(account, now) {
15496
15586
  const accountId = resolveAccountId(account);
15497
15587
  if (accountId)
15498
15588
  headers["chatgpt-account-id"] = accountId;
15499
- const json = await fetchJson(USAGE_URL2, headers, USAGE_HTTP_TIMEOUT_MS);
15589
+ const json = await fetchUsageJson(USAGE_URL2, headers, USAGE_HTTP_TIMEOUT_MS);
15500
15590
  const rateLimit = json?.rate_limit;
15501
15591
  if (!isPlainObject2(rateLimit))
15502
15592
  return null;
@@ -15585,7 +15675,7 @@ async function refreshUsage(adapters, now, poolSnapshot) {
15585
15675
  aliveIds?.add(account.id);
15586
15676
  if (account.disabledReason && account.disabledReason !== MANUAL_DISABLED_REASON)
15587
15677
  continue;
15588
- if (account.usage.capturedAt !== 0 && now - account.usage.capturedAt <= USAGE_REFRESH_TTL_MS)
15678
+ if (account.usage.capturedAt !== 0 && now - account.usage.capturedAt < USAGE_REFRESH_TTL_MS)
15589
15679
  continue;
15590
15680
  const adapter = adapterFor(adapters, account.providerID);
15591
15681
  if (!adapter)
@@ -15607,7 +15697,16 @@ async function refreshUsage(adapters, now, poolSnapshot) {
15607
15697
  await Promise.all(stale.map(async ({ adapter, account }) => {
15608
15698
  try {
15609
15699
  await ensureAccessToken(adapter, account, now);
15610
- const snapshot = await adapter.fetchUsage(account, now);
15700
+ const rejectedAccess = account.access;
15701
+ let snapshot;
15702
+ try {
15703
+ snapshot = await adapter.fetchUsage(account, now);
15704
+ } catch (error) {
15705
+ if (!(error instanceof UsageAuthenticationError) || !account.refresh)
15706
+ throw error;
15707
+ await ensureAccessToken(adapter, account, now, rejectedAccess);
15708
+ snapshot = await adapter.fetchUsage(account, now);
15709
+ }
15611
15710
  if (snapshot) {
15612
15711
  await mutatePool((p) => {
15613
15712
  const stored = findAccount(p, account.id);
@@ -15617,10 +15716,8 @@ async function refreshUsage(adapters, now, poolSnapshot) {
15617
15716
  weekly: snapshot.weekly === null ? stored.usage.weekly : preserveWeeklyAnchor(snapshot.weekly, stored.usage.weekly, now),
15618
15717
  capturedAt: snapshot.weekly === null ? stored.usage.capturedAt : now
15619
15718
  };
15620
- if (snapshot.weekly !== null && stored.cooldownUntil > now + MAX_TRANSIENT_COOLDOWN_MS && !isExhausted(stored, scoreCfg, now)) {
15621
- stored.cooldownUntil = 0;
15622
- delete stored.cooldownKind;
15623
- }
15719
+ if (snapshot.weekly !== null && stored.cooldownUntil === account.cooldownUntil && stored.cooldownKind === account.cooldownKind)
15720
+ reconcileQuotaCooldown(stored, scoreCfg, now);
15624
15721
  }
15625
15722
  });
15626
15723
  }
@@ -15633,16 +15730,17 @@ function refreshUsageInBackground(adapter, now, poolSnapshot) {
15633
15730
  function refreshAllUsageInBackground(now, poolSnapshot) {
15634
15731
  return refreshUsage(ADAPTERS, now, poolSnapshot);
15635
15732
  }
15636
- var USAGE_REFRESH_TTL_MS, MAX_TRANSIENT_COOLDOWN_MS, lastPoll;
15733
+ var USAGE_REFRESH_TTL_MS, lastPoll;
15637
15734
  var init_usage_refresh = __esm(() => {
15638
15735
  init_store();
15639
15736
  init_registry();
15737
+ init_usage_http();
15640
15738
  init_refresh();
15641
15739
  init_score_core();
15642
15740
  init_types();
15643
15741
  init_usage_merge();
15742
+ init_usage_merge();
15644
15743
  USAGE_REFRESH_TTL_MS = 5 * 60 * 1000;
15645
- MAX_TRANSIENT_COOLDOWN_MS = 5 * 60 * 1000;
15646
15744
  lastPoll = new Map;
15647
15745
  });
15648
15746
 
@@ -16867,6 +16965,12 @@ var init_prime = __esm(() => {
16867
16965
  init_select();
16868
16966
  });
16869
16967
 
16968
+ // src/sync/enabled.ts
16969
+ function syncEnabled(env = process.env) {
16970
+ const raw = env.OPENCODE_AUTH_LB_SYNC?.trim().toLowerCase();
16971
+ return raw !== "0" && raw !== "false" && raw !== "no" && raw !== "off";
16972
+ }
16973
+
16870
16974
  // src/sync/errors.ts
16871
16975
  function describeSyncError(error) {
16872
16976
  return error instanceof SyncError ? error.message : "Sync failed.";
@@ -16974,10 +17078,7 @@ var init_crypto = __esm(() => {
16974
17078
  B64URL_RE = /^[\w-]+$/;
16975
17079
  });
16976
17080
 
16977
- // src/sync/payload.ts
16978
- function entryKey(entry) {
16979
- return `${entry.origin}/${entry.id}`;
16980
- }
17081
+ // src/sync/text.ts
16981
17082
  function isControl(code) {
16982
17083
  return code < 32 || code >= 127 && code <= 159 || code === 1564 || code >= 8203 && code <= 8207 || code >= 8232 && code <= 8238 || code >= 8288 && code <= 8297 || code === 65279;
16983
17084
  }
@@ -16987,6 +17088,106 @@ function printable(text) {
16987
17088
  function boundedString(value, max) {
16988
17089
  return typeof value === "string" && value.length > 0 && value.length <= max ? value : undefined;
16989
17090
  }
17091
+ var MAX_LABEL = 80;
17092
+
17093
+ // src/sync/login-payload.ts
17094
+ function loginKey(providerID, accountId) {
17095
+ return `${providerID}:${accountId}`;
17096
+ }
17097
+ function word(value, max) {
17098
+ const text = boundedString(value, max);
17099
+ return text !== undefined && NO_SPACE_RE.test(text) ? text : undefined;
17100
+ }
17101
+ function positive(value) {
17102
+ return isFiniteNumber(value) && value > 0 ? value : undefined;
17103
+ }
17104
+ function parseLogin(raw) {
17105
+ if (!isPlainObject2(raw))
17106
+ return null;
17107
+ const providerID = word(raw.providerID, MAX_ID);
17108
+ const accountId = word(raw.accountId, MAX_ID);
17109
+ const label = printable(boundedString(raw.label, MAX_LABEL) ?? "");
17110
+ const [access, refresh] = [
17111
+ word(raw.access, MAX_TOKEN),
17112
+ word(raw.refresh, MAX_TOKEN)
17113
+ ];
17114
+ const { rev, at } = raw;
17115
+ if (!providerID || !LOGIN_PROVIDERS.has(providerID) || !accountId)
17116
+ return null;
17117
+ if (raw.key !== loginKey(providerID, accountId))
17118
+ return null;
17119
+ const expires = positive(raw.expires);
17120
+ if (!label || !access || !refresh || !expires)
17121
+ return null;
17122
+ if (typeof rev !== "number" || !Number.isSafeInteger(rev) || rev < 1)
17123
+ return null;
17124
+ if (!isFiniteNumber(at) || at < 0)
17125
+ return null;
17126
+ const orgId = word(raw.orgId, MAX_ID);
17127
+ const refreshExpires = positive(raw.refreshExpires);
17128
+ return {
17129
+ key: loginKey(providerID, accountId),
17130
+ providerID,
17131
+ label,
17132
+ access,
17133
+ refresh,
17134
+ expires,
17135
+ accountId,
17136
+ ...orgId ? { orgId } : {},
17137
+ ...refreshExpires ? { refreshExpires } : {},
17138
+ rev,
17139
+ at
17140
+ };
17141
+ }
17142
+ function sortLogins(logins) {
17143
+ return [...logins].sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
17144
+ }
17145
+ function loginsDigest(logins) {
17146
+ return fingerprint(JSON.stringify(sortLogins(logins)));
17147
+ }
17148
+ function compareLogins(a, b) {
17149
+ if (a.rev !== b.rev)
17150
+ return a.rev < b.rev ? -1 : 1;
17151
+ if (a.at !== b.at)
17152
+ return a.at < b.at ? -1 : 1;
17153
+ const [x, y] = [fingerprint(a.refresh), fingerprint(b.refresh)];
17154
+ return x < y ? -1 : x > y ? 1 : 0;
17155
+ }
17156
+ function parseLoginList(rawList) {
17157
+ const parsed = {
17158
+ logins: [],
17159
+ loginsListed: new Set,
17160
+ loginsUnreadable: 0
17161
+ };
17162
+ for (const raw of rawList) {
17163
+ const key = isPlainObject2(raw) ? boundedString(raw.key, MAX_KEY) : undefined;
17164
+ if (key)
17165
+ parsed.loginsListed.add(key);
17166
+ const login = parseLogin(raw);
17167
+ if (!login)
17168
+ parsed.loginsUnreadable += 1;
17169
+ else if (!parsed.logins.some((l) => l.key === login.key))
17170
+ parsed.logins.push(login);
17171
+ }
17172
+ return parsed;
17173
+ }
17174
+ var MAX_LOGINS = 64, MAX_TOKEN = 8192, MAX_ID = 128, MAX_KEY = 300, NO_SPACE_RE, LOGIN_PROVIDERS;
17175
+ var init_login_payload = __esm(() => {
17176
+ init_util2();
17177
+ init_crypto();
17178
+ NO_SPACE_RE = /^\S+$/;
17179
+ LOGIN_PROVIDERS = new Set([
17180
+ "anthropic",
17181
+ "openai",
17182
+ "kimi-code-plan-cn",
17183
+ "kimi-code-plan-global"
17184
+ ]);
17185
+ });
17186
+
17187
+ // src/sync/payload.ts
17188
+ function entryKey(entry) {
17189
+ return `${entry.origin}/${entry.id}`;
17190
+ }
16990
17191
  function keyOf(raw) {
16991
17192
  if (!isPlainObject2(raw))
16992
17193
  return null;
@@ -17027,11 +17228,12 @@ function sortEntries(entries) {
17027
17228
  function entriesDigest(entries) {
17028
17229
  return fingerprint(JSON.stringify(sortEntries(entries)));
17029
17230
  }
17030
- function buildPayload(entries, at) {
17231
+ function buildPayload(entries, at, logins = []) {
17031
17232
  return JSON.stringify({
17032
17233
  v: PAYLOAD_VERSION,
17033
17234
  at,
17034
- entries: sortEntries(entries)
17235
+ entries: sortEntries(entries),
17236
+ logins: sortLogins(logins)
17035
17237
  });
17036
17238
  }
17037
17239
  function parsePayload(text) {
@@ -17043,11 +17245,12 @@ function parsePayload(text) {
17043
17245
  }
17044
17246
  if (!isPlainObject2(json) || !Array.isArray(json.entries))
17045
17247
  throw new SyncError("bad-blob");
17046
- if (json.v !== PAYLOAD_VERSION)
17248
+ if (json.v !== PAYLOAD_VERSION && json.v !== ENTRIES_ONLY_VERSION)
17047
17249
  throw new SyncError("bad-version");
17048
- if (!isFiniteNumber(json.at))
17250
+ const rawLogins = json.v === PAYLOAD_VERSION ? json.logins ?? [] : [];
17251
+ if (!isFiniteNumber(json.at) || !Array.isArray(rawLogins))
17049
17252
  throw new SyncError("bad-blob");
17050
- if (json.entries.length > MAX_ENTRIES)
17253
+ if (json.entries.length > MAX_ENTRIES || rawLogins.length > MAX_LOGINS)
17051
17254
  throw new SyncError("too-large");
17052
17255
  const listed = new Set;
17053
17256
  const entries = [];
@@ -17066,13 +17269,20 @@ function parsePayload(text) {
17066
17269
  else
17067
17270
  unreadable += 1;
17068
17271
  }
17069
- return { at: json.at, entries, listed, unreadable };
17272
+ return {
17273
+ at: json.at,
17274
+ entries,
17275
+ listed,
17276
+ unreadable,
17277
+ ...parseLoginList(rawLogins)
17278
+ };
17070
17279
  }
17071
- var PAYLOAD_VERSION = 2, MAX_ENTRIES = 64, MAX_LABEL = 80, MAX_SECRET = 512, MAX_FIELD = 128, ID_RE, ORIGIN_RE, CLAUDE_TOKEN_RE, API_KEY_RE, SECRET_SHAPES;
17280
+ var PAYLOAD_VERSION = 3, ENTRIES_ONLY_VERSION = 2, MAX_ENTRIES = 64, MAX_SECRET = 512, MAX_FIELD = 128, ID_RE, ORIGIN_RE, CLAUDE_TOKEN_RE, API_KEY_RE, SECRET_SHAPES;
17072
17281
  var init_payload = __esm(() => {
17073
17282
  init_util2();
17074
17283
  init_crypto();
17075
17284
  init_errors3();
17285
+ init_login_payload();
17076
17286
  ID_RE = /^[\w-]{1,64}$/;
17077
17287
  ORIGIN_RE = /^[\da-f]{32}$/;
17078
17288
  CLAUDE_TOKEN_RE = /^sk-ant-oat\d+-[\w-]+$/;
@@ -17162,7 +17372,7 @@ function backoffFrom(res, now) {
17162
17372
  function isRateLimited(res) {
17163
17373
  return res.status === 429 || res.status === 403 && (res.headers.get("x-ratelimit-remaining") === "0" || res.headers.has("retry-after"));
17164
17374
  }
17165
- async function request(url, init, now) {
17375
+ async function request2(url, init, now) {
17166
17376
  let res;
17167
17377
  try {
17168
17378
  res = await globalThis.fetch(url, {
@@ -17214,7 +17424,7 @@ async function boundedText(res) {
17214
17424
  }
17215
17425
  return Buffer.concat(chunks).toString("utf8");
17216
17426
  }
17217
- async function readJson(res) {
17427
+ async function readJson2(res) {
17218
17428
  if (!res.ok)
17219
17429
  throw new SyncError("http");
17220
17430
  const text = await boundedText(res);
@@ -17229,7 +17439,7 @@ async function readJson(res) {
17229
17439
  return json;
17230
17440
  }
17231
17441
  async function createGist(token, content, now = Date.now()) {
17232
- const res = await request(API, {
17442
+ const res = await request2(API, {
17233
17443
  method: "POST",
17234
17444
  headers: { ...headers(token), "content-type": "application/json" },
17235
17445
  body: JSON.stringify({
@@ -17238,7 +17448,7 @@ async function createGist(token, content, now = Date.now()) {
17238
17448
  files: { [GIST_FILE]: { content } }
17239
17449
  })
17240
17450
  }, now);
17241
- const json = await readJson(res);
17451
+ const json = await readJson2(res);
17242
17452
  const owner = isPlainObject2(json.owner) ? json.owner.login : undefined;
17243
17453
  if (typeof json.id !== "string" || !GIST_ID_RE.test(json.id))
17244
17454
  throw new SyncError("http");
@@ -17250,7 +17460,7 @@ async function createGist(token, content, now = Date.now()) {
17250
17460
  async function updateGist(token, id, content, now = Date.now()) {
17251
17461
  let res;
17252
17462
  try {
17253
- res = await request(`${API}/${id}`, {
17463
+ res = await request2(`${API}/${id}`, {
17254
17464
  method: "PATCH",
17255
17465
  headers: { ...headers(token), "content-type": "application/json" },
17256
17466
  body: JSON.stringify({ files: { [GIST_FILE]: { content } } })
@@ -17262,10 +17472,10 @@ async function updateGist(token, id, content, now = Date.now()) {
17262
17472
  }
17263
17473
  if (res.status === 401 || res.status === 403)
17264
17474
  throw new SyncError("write-denied");
17265
- await readJson(res);
17475
+ await readJson2(res);
17266
17476
  }
17267
17477
  async function readGist(id, etag, now = Date.now(), token) {
17268
- const ask = (auth) => request(`${API}/${id}`, {
17478
+ const ask = (auth) => request2(`${API}/${id}`, {
17269
17479
  headers: {
17270
17480
  ...headers(auth),
17271
17481
  ...etag ? { "if-none-match": etag } : {}
@@ -17276,7 +17486,7 @@ async function readGist(id, etag, now = Date.now(), token) {
17276
17486
  res = await ask();
17277
17487
  if (res.status === 304)
17278
17488
  return { changed: false };
17279
- const json = await readJson(res);
17489
+ const json = await readJson2(res);
17280
17490
  const file = isPlainObject2(json.files) ? json.files[GIST_FILE] : undefined;
17281
17491
  if (!isPlainObject2(file) || typeof file.content !== "string")
17282
17492
  throw new SyncError("bad-blob");
@@ -17318,6 +17528,12 @@ function isRef(value) {
17318
17528
  function isSkip(value) {
17319
17529
  return isPlainObject2(value) && typeof value.fingerprint === "string" && typeof value.blocker === "string" && typeof value.holds === "string";
17320
17530
  }
17531
+ function isMark(value) {
17532
+ return value === true;
17533
+ }
17534
+ function marks(from) {
17535
+ return from && Object.keys(from).length > 0 ? newMap(from) : undefined;
17536
+ }
17321
17537
  function readMap(raw, accept) {
17322
17538
  const map = newMap();
17323
17539
  if (isPlainObject2(raw)) {
@@ -17341,6 +17557,8 @@ function parseState(text) {
17341
17557
  return null;
17342
17558
  const { owner, etag, syncedAt, uploadedDigest, write } = json;
17343
17559
  const { uploadedAt, appliedAt, retryAt, pollAt, writeCheckAt } = json;
17560
+ const loginKnown = marks(readMap(json.loginKnown, isMark));
17561
+ const loginSuppressed = marks(readMap(json.loginSuppressed, isMark));
17344
17562
  return {
17345
17563
  v: 1,
17346
17564
  gistId: json.gistId,
@@ -17348,6 +17566,8 @@ function parseState(text) {
17348
17566
  creator: json.creator === true,
17349
17567
  imported: readMap(json.imported, isRef),
17350
17568
  skipped: readMap(json.skipped, isSkip),
17569
+ ...loginKnown ? { loginKnown } : {},
17570
+ ...loginSuppressed ? { loginSuppressed } : {},
17351
17571
  ...typeof owner === "string" ? { owner } : {},
17352
17572
  ...isWrite(write) ? { write } : {},
17353
17573
  ...typeof etag === "string" ? { etag } : {},
@@ -17577,6 +17797,179 @@ async function discoverGithubToken(run = ghAuthToken, env = process.env) {
17577
17797
  var GH_TIMEOUT_MS = 5000;
17578
17798
  var init_github_auth = () => {};
17579
17799
 
17800
+ // src/sync/logins.ts
17801
+ function loginsSyncEnabled(env = process.env) {
17802
+ const raw = env.OPENCODE_AUTH_LB_SYNC_LOGINS?.trim().toLowerCase();
17803
+ return raw !== "0" && raw !== "false" && raw !== "no" && raw !== "off";
17804
+ }
17805
+ function rowKey(row) {
17806
+ const live = row.refresh !== "" || row.inferenceToken !== undefined && row.lostLogins?.oauth !== undefined;
17807
+ return LOGIN_PROVIDERS.has(row.providerID) && row.accountId && live ? loginKey(row.providerID, row.accountId) : null;
17808
+ }
17809
+ function heldMarks(accounts) {
17810
+ const held = newMap();
17811
+ for (const row of accounts) {
17812
+ const key = rowKey(row);
17813
+ if (key)
17814
+ held[key] = true;
17815
+ }
17816
+ return held;
17817
+ }
17818
+ function ownLogins(pool) {
17819
+ const best = new Map;
17820
+ for (const row of pool.accounts) {
17821
+ if (!row.refresh || !row.accountId)
17822
+ continue;
17823
+ if (row.disabledReason || row.lostLogins?.oauth)
17824
+ continue;
17825
+ const login = parseLogin({
17826
+ key: loginKey(row.providerID, row.accountId),
17827
+ providerID: row.providerID,
17828
+ label: printable(row.label).slice(0, MAX_LABEL).trim() || row.providerID,
17829
+ access: row.access,
17830
+ refresh: row.refresh,
17831
+ expires: row.expires,
17832
+ accountId: row.accountId,
17833
+ orgId: row.orgId,
17834
+ refreshExpires: row.refreshExpires,
17835
+ ...loginVersion(row)
17836
+ });
17837
+ const prior = login ? best.get(login.key) : undefined;
17838
+ if (login && (!prior || compareLogins(login, prior) > 0))
17839
+ best.set(login.key, login);
17840
+ }
17841
+ return sortLogins([...best.values()]).slice(0, MAX_LOGINS);
17842
+ }
17843
+ function mergeLogins(gist, own) {
17844
+ const merged = new Map(gist.map((login) => [login.key, login]));
17845
+ for (const login of own) {
17846
+ const prior = merged.get(login.key);
17847
+ if (prior ? compareLogins(login, prior) > 0 : merged.size < MAX_LOGINS)
17848
+ merged.set(login.key, login);
17849
+ }
17850
+ return sortLogins([...merged.values()]);
17851
+ }
17852
+ function ownCopies(next, own) {
17853
+ return own.filter((login) => next.some((n) => n.key === login.key && n.refresh === login.refresh)).length;
17854
+ }
17855
+ function publishDigest(entries, logins) {
17856
+ return logins.length === 0 ? entriesDigest(entries) : fingerprint(`${entriesDigest(entries)}/${loginsDigest(logins)}`);
17857
+ }
17858
+ function sameLogin(row, login) {
17859
+ return rowKey(row) === login.key && (row.orgId === undefined || login.orgId === undefined || row.orgId === login.orgId);
17860
+ }
17861
+ function versionOf(row) {
17862
+ return { ...loginVersion(row), refresh: row.refresh };
17863
+ }
17864
+ function overwriteLocked(row, login) {
17865
+ const attempt = { refresh: row.refresh, gen: genOf(row) };
17866
+ return mutatePool((pool) => {
17867
+ const cur = findAccount(pool, row.id);
17868
+ if (!cur || !sameGeneration(cur, attempt))
17869
+ return false;
17870
+ cur.access = login.access;
17871
+ cur.refresh = login.refresh;
17872
+ cur.expires = login.expires;
17873
+ if (login.refreshExpires)
17874
+ cur.refreshExpires = login.refreshExpires;
17875
+ else
17876
+ delete cur.refreshExpires;
17877
+ if (login.orgId)
17878
+ cur.orgId = login.orgId;
17879
+ cur.loginRev = login.rev;
17880
+ cur.loginAt = login.at;
17881
+ cur.tokenGen = genOf(cur) + 1;
17882
+ clearLostLogin(cur, "oauth");
17883
+ if (cur.disabledReason?.startsWith(AUTO_DISABLED_PREFIX))
17884
+ cur.disabledReason = null;
17885
+ return true;
17886
+ });
17887
+ }
17888
+ function overwrite(row, login) {
17889
+ return withRefreshLock(row.providerID, row.id, () => overwriteLocked(row, login));
17890
+ }
17891
+ async function create(login) {
17892
+ const tokens = {
17893
+ access: login.access,
17894
+ refresh: login.refresh,
17895
+ expires: login.expires,
17896
+ accountId: login.accountId,
17897
+ ...login.orgId ? { orgId: login.orgId } : {},
17898
+ ...login.refreshExpires ? { refreshExpires: login.refreshExpires } : {}
17899
+ };
17900
+ return mutatePool((pool) => {
17901
+ if (pool.accounts.some((row) => sameLogin(row, login)))
17902
+ return false;
17903
+ const manual = new Set(pool.accounts.filter((row) => row.disabledReason === MANUAL_DISABLED_REASON).map((row) => row.id));
17904
+ const row = placeTokens(pool, login.providerID, tokens, uniqueLabel(pool, login.label), undefined);
17905
+ row.loginRev = login.rev;
17906
+ row.loginAt = login.at;
17907
+ row.tokenGen = genOf(row) + 1;
17908
+ if (manual.has(row.id))
17909
+ row.disabledReason = MANUAL_DISABLED_REASON;
17910
+ return true;
17911
+ });
17912
+ }
17913
+ async function adopt2(login) {
17914
+ const row = (await readPool()).accounts.find((a) => sameLogin(a, login));
17915
+ if (!row)
17916
+ return create(login);
17917
+ return compareLogins(login, versionOf(row)) > 0 ? overwrite(row, login) : false;
17918
+ }
17919
+ async function outrank(login, now) {
17920
+ const row = (await readPool()).accounts.find((a) => sameLogin(a, login));
17921
+ if (!row || compareLogins(versionOf(row), login) > 0)
17922
+ return;
17923
+ const attempt = { refresh: row.refresh, gen: genOf(row) };
17924
+ await mutatePool((pool) => {
17925
+ const cur = findAccount(pool, row.id);
17926
+ if (!cur || !sameGeneration(cur, attempt))
17927
+ return;
17928
+ cur.loginRev = login.rev + 1;
17929
+ cur.loginAt = now;
17930
+ });
17931
+ }
17932
+ async function applyLogins(gist, marks, now) {
17933
+ const known = newMap(marks.known);
17934
+ const suppressed = newMap(marks.suppressed);
17935
+ const held = heldMarks((await readPool()).accounts);
17936
+ for (const key of Object.keys(known)) {
17937
+ if (held[key])
17938
+ continue;
17939
+ suppressed[key] = true;
17940
+ delete known[key];
17941
+ }
17942
+ for (const key of Object.keys(suppressed)) {
17943
+ if (!held[key])
17944
+ continue;
17945
+ delete suppressed[key];
17946
+ const login = gist.find((l) => l.key === key);
17947
+ if (login)
17948
+ await outrank(login, now);
17949
+ }
17950
+ let adopted = 0;
17951
+ for (const login of gist)
17952
+ if (!suppressed[login.key] && await adopt2(login))
17953
+ adopted += 1;
17954
+ return {
17955
+ adopted,
17956
+ known: heldMarks((await readPool()).accounts),
17957
+ suppressed
17958
+ };
17959
+ }
17960
+ var AUTO_DISABLED_PREFIX = "invalid_grant: re-login required";
17961
+ var init_logins = __esm(() => {
17962
+ init_accounts();
17963
+ init_store();
17964
+ init_refresh();
17965
+ init_types();
17966
+ init_apply();
17967
+ init_crypto();
17968
+ init_login_payload();
17969
+ init_payload();
17970
+ init_state();
17971
+ });
17972
+
17580
17973
  // src/sync/origin.ts
17581
17974
  import { randomBytes as randomBytes2 } from "node:crypto";
17582
17975
  import { readFile as readFile6 } from "node:fs/promises";
@@ -17677,6 +18070,10 @@ var init_timing = __esm(() => {
17677
18070
  function count(n, one, many) {
17678
18071
  return `${n} ${n === 1 ? one : many}`;
17679
18072
  }
18073
+ function uploadedMessage(entries, logins) {
18074
+ const credentials = count(entries, "credential", "credentials");
18075
+ return logins === 0 ? `Uploaded ${credentials}.` : `Uploaded ${credentials} and ${count(logins, "login", "logins")}.`;
18076
+ }
17680
18077
  function recordApplied(gistId, at, applied, syncedAt) {
17681
18078
  return updateSyncState((cur) => cur?.gistId === gistId ? {
17682
18079
  ...cur,
@@ -17700,7 +18097,8 @@ async function runCycle(deps, force, report) {
17700
18097
  const pool = await readPool();
17701
18098
  const backedOff = state.write === "denied" && !force && now() < (state.writeCheckAt ?? 0);
17702
18099
  const mayWrite = token !== null && !backedOff;
17703
- const before = entriesDigest(ownEntries(pool, origin, state.imported));
18100
+ const sharing = loginsSyncEnabled();
18101
+ const before = publishDigest(ownEntries(pool, origin, state.imported), sharing ? ownLogins(pool) : []);
17704
18102
  const dirty = mayWrite && before !== state.uploadedDigest;
17705
18103
  report.retryMs = dirty ? PUBLISH_RETRY_MS : POLL_MS;
17706
18104
  const reread = force || dirty || hasFreedSkips(pool.accounts, state.skipped);
@@ -17722,8 +18120,17 @@ async function runCycle(deps, force, report) {
17722
18120
  ...deps.adapters ? { adapters: deps.adapters } : {},
17723
18121
  afterPass: (soFar) => recordApplied(state.gistId, snapshot.at, soFar, now())
17724
18122
  });
17725
- const own = ownEntries(await readPool(), origin, applied?.imported ?? state.imported);
17726
- const unreadable = snapshot.unreadable > 0;
18123
+ const adoptedLogins = stale || !sharing ? null : await applyLogins(snapshot.logins, { known: state.loginKnown, suppressed: state.loginSuppressed }, now());
18124
+ if (adoptedLogins)
18125
+ await updateSyncState((cur) => cur?.gistId === state.gistId ? {
18126
+ ...cur,
18127
+ loginKnown: marks(adoptedLogins.known),
18128
+ loginSuppressed: marks(adoptedLogins.suppressed)
18129
+ } : cur);
18130
+ const settled = await readPool();
18131
+ const own = ownEntries(settled, origin, applied?.imported ?? state.imported);
18132
+ const ownShared = sharing ? ownLogins(settled) : [];
18133
+ const unreadable = snapshot.unreadable > 0 || snapshot.loginsUnreadable > 0;
17727
18134
  let write = standing;
17728
18135
  let wrote = null;
17729
18136
  let stamped = 0;
@@ -17733,12 +18140,16 @@ async function runCycle(deps, force, report) {
17733
18140
  if (mayWrite && !unreadable) {
17734
18141
  const others = snapshot.entries.filter((e) => e.origin !== origin);
17735
18142
  const next = sortEntries([...others, ...listable(own, others, origin)]);
17736
- const same = JSON.stringify(next) === JSON.stringify(sortEntries(snapshot.entries));
18143
+ const nextLogins = sharing ? mergeLogins(snapshot.logins, ownShared) : snapshot.logins;
18144
+ const same = JSON.stringify(next) === JSON.stringify(sortEntries(snapshot.entries)) && loginsDigest(nextLogins) === loginsDigest(snapshot.logins);
17737
18145
  if (!same || stale) {
17738
18146
  stamped = Math.max(now(), snapshot.at + 1, (state.uploadedAt ?? 0) + 1, (state.appliedAt ?? 0) + 1);
17739
18147
  try {
17740
- await updateGist(token, state.gistId, seal(buildPayload(next, stamped), key), now());
17741
- wrote = next.filter((e) => e.origin === origin);
18148
+ await updateGist(token, state.gistId, seal(buildPayload(next, stamped, nextLogins), key), now());
18149
+ wrote = {
18150
+ entries: next.filter((e) => e.origin === origin).length,
18151
+ logins: ownCopies(nextLogins, ownShared)
18152
+ };
17742
18153
  } catch (error) {
17743
18154
  if (!(error instanceof SyncError) || error.code !== "write-denied")
17744
18155
  throw error;
@@ -17754,18 +18165,22 @@ async function runCycle(deps, force, report) {
17754
18165
  etag: wrote || stale || deferred > 0 || denied ? undefined : read.etag,
17755
18166
  write,
17756
18167
  writeCheckAt: denied ? now() + WRITE_BACKOFF_MS : backedOff ? cur.writeCheckAt : undefined,
17757
- uploadedDigest: mayWrite && !unreadable && !denied ? entriesDigest(own) : cur.uploadedDigest,
18168
+ uploadedDigest: mayWrite && !unreadable && !denied ? publishDigest(own, ownShared) : cur.uploadedDigest,
17758
18169
  ...wrote ? { uploadedAt: stamped } : {}
17759
18170
  } : cur);
17760
18171
  const parts = [];
17761
18172
  const moved = applied ? applied.added + applied.updated + applied.removed + deferred : 0;
18173
+ const adopted = adoptedLogins?.adopted ?? 0;
17762
18174
  if (applied && moved > 0) {
17763
18175
  const tail = deferred > 0 ? `; ${count(deferred, "entry", "entries")} could not be verified yet` : "";
17764
- parts.push(`Synced: ${applied.added} added, ${applied.updated} updated, ${applied.removed} removed${tail}.`);
17765
- } else if (!wrote)
18176
+ const login = adopted > 0 ? `; ${count(adopted, "login", "logins")} adopted` : "";
18177
+ parts.push(`Synced: ${applied.added} added, ${applied.updated} updated, ${applied.removed} removed${tail}${login}.`);
18178
+ } else if (adopted > 0)
18179
+ parts.push(`Synced: ${count(adopted, "login", "logins")} adopted.`);
18180
+ else if (!wrote)
17766
18181
  parts.push("Up to date.");
17767
18182
  if (wrote)
17768
- parts.push(`Uploaded ${count(wrote.length, "credential", "credentials")}.`);
18183
+ parts.push(uploadedMessage(wrote.entries, wrote.logins));
17769
18184
  if (denied)
17770
18185
  parts.push(new SyncError("write-denied").message);
17771
18186
  if (mayWrite && unreadable && state.write !== "unreadable")
@@ -17780,6 +18195,8 @@ var init_cycle = __esm(() => {
17780
18195
  init_errors3();
17781
18196
  init_gist();
17782
18197
  init_github_auth();
18198
+ init_login_payload();
18199
+ init_logins();
17783
18200
  init_merge();
17784
18201
  init_origin();
17785
18202
  init_own();
@@ -17789,9 +18206,6 @@ var init_cycle = __esm(() => {
17789
18206
  });
17790
18207
 
17791
18208
  // src/sync/engine.ts
17792
- function count2(n, one, many) {
17793
- return `${n} ${n === 1 ? one : many}`;
17794
- }
17795
18209
  function createEngine(deps) {
17796
18210
  const { now } = deps;
17797
18211
  async function settle(work, reqAt, scheduled) {
@@ -17824,10 +18238,14 @@ function createEngine(deps) {
17824
18238
  if (!token)
17825
18239
  throw new SyncError("no-auth");
17826
18240
  const origin = await machineOrigin();
17827
- const own = ownEntries(await readPool(), origin, newRefs());
18241
+ const pool = await readPool();
18242
+ const own = ownEntries(pool, origin, newRefs());
18243
+ const sharing = loginsSyncEnabled();
18244
+ const shared = sharing ? ownLogins(pool) : [];
17828
18245
  const key = generateKey();
17829
18246
  const at = now();
17830
- const gist = await createGist(token, seal(buildPayload(own, at), key), now());
18247
+ const gist = await createGist(token, seal(buildPayload(own, at, shared), key), now());
18248
+ const loginKnown = sharing ? marks(heldMarks(pool.accounts)) : undefined;
17831
18249
  await updateSyncState(() => ({
17832
18250
  v: 1,
17833
18251
  gistId: gist.id,
@@ -17836,21 +18254,23 @@ function createEngine(deps) {
17836
18254
  ...gist.owner ? { owner: gist.owner } : {},
17837
18255
  write: "ok",
17838
18256
  syncedAt: now(),
17839
- uploadedDigest: entriesDigest(own),
18257
+ uploadedDigest: publishDigest(own, shared),
17840
18258
  uploadedAt: at,
17841
18259
  appliedAt: at,
17842
18260
  pollAt: now() + POLL_MS,
17843
18261
  imported: newRefs(),
17844
- skipped: newMap()
18262
+ skipped: newMap(),
18263
+ ...loginKnown ? { loginKnown } : {}
17845
18264
  }));
17846
- return `Uploaded ${count2(own.length, "credential", "credentials")}.`;
18265
+ const message = uploadedMessage(own.length, shared.length);
18266
+ return own.length + shared.length === 0 ? `${message} ${NOTHING_TO_SHARE}` : message;
17847
18267
  }
17848
18268
  async function unpublishedDigest() {
17849
18269
  const state = await readSyncState();
17850
18270
  if (!state || state.write === "no-token" || state.write === "unreadable" || state.write === "denied" && now() < (state.writeCheckAt ?? 0))
17851
18271
  return null;
17852
- const own = ownEntries(await readPool(), await machineOrigin(), state.imported);
17853
- const digest = entriesDigest(own);
18272
+ const pool = await readPool();
18273
+ const digest = publishDigest(ownEntries(pool, await machineOrigin(), state.imported), loginsSyncEnabled() ? ownLogins(pool) : []);
17854
18274
  return digest === state.uploadedDigest ? null : digest;
17855
18275
  }
17856
18276
  const engine = {
@@ -17872,7 +18292,9 @@ function createEngine(deps) {
17872
18292
  creator: prior?.creator ?? false,
17873
18293
  ...prior?.owner ? { owner: prior.owner } : {},
17874
18294
  imported: prior?.imported ?? newRefs(),
17875
- skipped: prior?.skipped ?? newMap()
18295
+ skipped: prior?.skipped ?? newMap(),
18296
+ ...prior?.loginKnown ? { loginKnown: prior.loginKnown } : {},
18297
+ ...prior?.loginSuppressed ? { loginSuppressed: prior.loginSuppressed } : {}
17876
18298
  };
17877
18299
  });
17878
18300
  return settle(cycle(false), reqAt, true);
@@ -17899,6 +18321,7 @@ function createEngine(deps) {
17899
18321
  };
17900
18322
  return engine;
17901
18323
  }
18324
+ var NOTHING_TO_SHARE = "No static credential or shareable OAuth login on this machine.";
17902
18325
  var init_engine = __esm(() => {
17903
18326
  init_store();
17904
18327
  init_crypto();
@@ -17906,6 +18329,7 @@ var init_engine = __esm(() => {
17906
18329
  init_errors3();
17907
18330
  init_gist();
17908
18331
  init_github_auth();
18332
+ init_logins();
17909
18333
  init_origin();
17910
18334
  init_own();
17911
18335
  init_payload();
@@ -17913,6 +18337,47 @@ var init_engine = __esm(() => {
17913
18337
  init_timing();
17914
18338
  });
17915
18339
 
18340
+ // src/sync/login-hooks.ts
18341
+ function createPullLogin(runGh) {
18342
+ return async function pullLogin(account, _recovery) {
18343
+ const { providerID, accountId } = account;
18344
+ if (!syncEnabled() || !loginsSyncEnabled())
18345
+ return false;
18346
+ if (!LOGIN_PROVIDERS.has(providerID) || !accountId || !account.refresh)
18347
+ return false;
18348
+ try {
18349
+ const state = await readSyncState();
18350
+ if (!state)
18351
+ return false;
18352
+ const token = await discoverGithubToken(runGh) ?? undefined;
18353
+ const read = await readGist(state.gistId, undefined, Date.now(), token);
18354
+ if (!read.changed)
18355
+ return false;
18356
+ const key = decodeKey(state.key) ?? Buffer.alloc(32);
18357
+ const wanted = loginKey(providerID, accountId);
18358
+ const login = parsePayload(open(read.content, key)).logins.find((l) => l.key === wanted);
18359
+ if (!login || !sameLogin(account, login))
18360
+ return false;
18361
+ if (compareLogins(login, versionOf(account)) <= 0)
18362
+ return false;
18363
+ return await overwriteLocked(account, login);
18364
+ } catch {
18365
+ return false;
18366
+ }
18367
+ };
18368
+ }
18369
+ var pullLogin;
18370
+ var init_login_hooks = __esm(() => {
18371
+ init_crypto();
18372
+ init_gist();
18373
+ init_github_auth();
18374
+ init_login_payload();
18375
+ init_logins();
18376
+ init_payload();
18377
+ init_state();
18378
+ pullLogin = createPullLogin();
18379
+ });
18380
+
17916
18381
  // src/sync/intent.ts
17917
18382
  import { randomUUID as randomUUID3 } from "node:crypto";
17918
18383
  import { access, readFile as readFile7, rename as rename2, rm as rm3 } from "node:fs/promises";
@@ -18114,10 +18579,6 @@ var init_loop = __esm(() => {
18114
18579
  });
18115
18580
 
18116
18581
  // src/sync/start.ts
18117
- function syncEnabled(env = process.env) {
18118
- const raw = env.OPENCODE_AUTH_LB_SYNC?.trim().toLowerCase();
18119
- return raw !== "0" && raw !== "false" && raw !== "no" && raw !== "off";
18120
- }
18121
18582
  function startSync() {
18122
18583
  if (!syncEnabled())
18123
18584
  return null;
@@ -18126,11 +18587,110 @@ function startSync() {
18126
18587
  now: Date.now
18127
18588
  });
18128
18589
  loop.start();
18129
- return loop;
18590
+ if (!loginsSyncEnabled())
18591
+ return loop;
18592
+ setRefreshHooks({
18593
+ pull: pullLogin,
18594
+ afterRefresh: () => void publishAfterLogin().catch(ignore)
18595
+ });
18596
+ return {
18597
+ ...loop,
18598
+ dispose() {
18599
+ setRefreshHooks(null);
18600
+ return loop.dispose();
18601
+ }
18602
+ };
18603
+ }
18604
+ async function publishAfterLogin(options = {}) {
18605
+ const { engine = createEngine({ now: Date.now }) } = options;
18606
+ const { timeoutMs = LOGIN_PUBLISH_MS } = options;
18607
+ let timer;
18608
+ const publish = async () => {
18609
+ if (!syncEnabled() || !loginsSyncEnabled() || !await readSyncState())
18610
+ return;
18611
+ await withCycleLock(() => engine.sync(), timeoutMs);
18612
+ };
18613
+ await Promise.race([
18614
+ publish().catch(ignore),
18615
+ new Promise((resolve) => {
18616
+ timer = setTimeout(resolve, timeoutMs);
18617
+ })
18618
+ ]);
18619
+ clearTimeout(timer);
18130
18620
  }
18621
+ var LOGIN_PUBLISH_MS = 20000;
18131
18622
  var init_start = __esm(() => {
18623
+ init_util2();
18132
18624
  init_engine();
18625
+ init_login_hooks();
18626
+ init_logins();
18627
+ init_loop();
18628
+ init_state();
18629
+ });
18630
+
18631
+ // src/usage-poll.ts
18632
+ import { readFile as readFile8 } from "node:fs/promises";
18633
+ async function nextPollAt(path) {
18634
+ try {
18635
+ const value = JSON.parse(await readFile8(path, "utf8"));
18636
+ return isPlainObject2(value) && isFiniteNumber(value.nextPollAt) ? value.nextPollAt : 0;
18637
+ } catch (error) {
18638
+ if (error instanceof SyntaxError || error instanceof Error && "code" in error && error.code === "ENOENT")
18639
+ return 0;
18640
+ throw error;
18641
+ }
18642
+ }
18643
+ function startUsagePoll(deps = {}) {
18644
+ const now = deps.now ?? Date.now;
18645
+ const refresh = deps.refresh ?? refreshAllUsageInBackground;
18646
+ const timers = deps.timers ?? realTimers;
18647
+ let disposed = false;
18648
+ let inflight;
18649
+ async function poll() {
18650
+ const path = `${poolFilePath()}.usage-poll.json`;
18651
+ try {
18652
+ await withLock(`${path}.lock`, {
18653
+ staleMs: 120000,
18654
+ timeoutMs: 0,
18655
+ retryMs: 25,
18656
+ heartbeatMs: 5000
18657
+ }, async () => {
18658
+ const at = now();
18659
+ if (at < await nextPollAt(path))
18660
+ return;
18661
+ await writeJsonAtomic(path, JSON.stringify({ nextPollAt: at + USAGE_REFRESH_TTL_MS }));
18662
+ await refresh(at);
18663
+ });
18664
+ } catch (error) {
18665
+ if (!(error instanceof LockTimeoutError))
18666
+ throw error;
18667
+ }
18668
+ }
18669
+ function tick() {
18670
+ if (disposed)
18671
+ return Promise.resolve();
18672
+ inflight ??= poll().finally(() => {
18673
+ inflight = undefined;
18674
+ });
18675
+ return inflight;
18676
+ }
18677
+ const timer = timers.set(() => void tick().catch(ignore), USAGE_REFRESH_TTL_MS);
18678
+ return {
18679
+ tick,
18680
+ async dispose() {
18681
+ disposed = true;
18682
+ timers.clear(timer);
18683
+ await inflight;
18684
+ }
18685
+ };
18686
+ }
18687
+ var init_usage_poll = __esm(() => {
18688
+ init_lock();
18689
+ init_paths();
18690
+ init_store();
18133
18691
  init_loop();
18692
+ init_usage_refresh();
18693
+ init_util2();
18134
18694
  });
18135
18695
 
18136
18696
  // src/legacy.ts
@@ -18158,6 +18718,8 @@ function buildAuthHook(adapter, client, pending) {
18158
18718
  if (minted || account.inferenceToken !== undefined && account.refresh)
18159
18719
  notifyPaired(client, adapter.id, account);
18160
18720
  await refreshUsageInBackground(adapter, Date.now()).catch(ignore);
18721
+ if (tokens.refresh && !tokens.inferenceOnly)
18722
+ await publishAfterLogin();
18161
18723
  };
18162
18724
  const oauthSuccess = (tokens) => ({
18163
18725
  type: "success",
@@ -18285,8 +18847,12 @@ function createProviderPlugin(adapter) {
18285
18847
  }
18286
18848
  var PROVIDER_LABELS, AnthropicLoadBalancerPlugin, OpenAILoadBalancerPlugin, KimiCodeLoadBalancerPlugin, KimiCodeGlobalLoadBalancerPlugin, lbResult = (output) => ({ title: "Auth Load Balancer", output }), AuthLoadBalancerStatusPlugin = async (input) => {
18287
18849
  const sync = startSync();
18850
+ const usage = startUsagePoll();
18288
18851
  return {
18289
- dispose: async () => sync?.dispose(),
18852
+ dispose: async () => {
18853
+ await usage.dispose();
18854
+ await sync?.dispose();
18855
+ },
18290
18856
  tool: {
18291
18857
  auth_lb_status: tool({
18292
18858
  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.",
@@ -18375,6 +18941,7 @@ var init_legacy = __esm(() => {
18375
18941
  init_start();
18376
18942
  init_token_mint();
18377
18943
  init_types();
18944
+ init_usage_poll();
18378
18945
  init_usage_refresh();
18379
18946
  init_util2();
18380
18947
  PROVIDER_LABELS = {