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/v2-entry.js CHANGED
@@ -41438,6 +41438,15 @@ var init_tool = __esm(() => {
41438
41438
  tool2.schema = exports_external2;
41439
41439
  });
41440
41440
 
41441
+ // src/login-rev.ts
41442
+ function bumpLoginRev(row, now = Date.now()) {
41443
+ row.loginRev = (row.loginRev ?? 1) + 1;
41444
+ row.loginAt = now;
41445
+ }
41446
+ function loginVersion(row) {
41447
+ return { rev: row.loginRev ?? 1, at: row.loginAt ?? 0 };
41448
+ }
41449
+
41441
41450
  // src/types.ts
41442
41451
  function emptyUsage() {
41443
41452
  return { hourly: null, weekly: null, capturedAt: 0 };
@@ -41868,6 +41877,10 @@ function normalizeAccounts(rows) {
41868
41877
  delete row.orgId;
41869
41878
  if (!isFiniteNumber(row.refreshExpires) || row.refreshExpires <= 0)
41870
41879
  delete row.refreshExpires;
41880
+ if (typeof row.loginRev !== "number" || !Number.isSafeInteger(row.loginRev) || row.loginRev < 1)
41881
+ delete row.loginRev;
41882
+ if (!isFiniteNumber(row.loginAt) || row.loginAt < 0)
41883
+ delete row.loginAt;
41871
41884
  if (row.inferenceToken === undefined || !isFiniteNumber(row.inferenceExpires) || row.inferenceExpires <= 0)
41872
41885
  delete row.inferenceExpires;
41873
41886
  if (row.lostLogins !== undefined) {
@@ -42130,6 +42143,14 @@ var init_score_core = __esm(() => {
42130
42143
  });
42131
42144
 
42132
42145
  // src/usage-merge.ts
42146
+ function reconcileQuotaCooldown(account, cfg, now) {
42147
+ if (account.cooldownKind === "auth" || account.cooldownKind === "transient")
42148
+ return;
42149
+ if (account.cooldownUntil > now + MAX_TRANSIENT_COOLDOWN_MS && !isExhausted(account, cfg, now)) {
42150
+ account.cooldownUntil = 0;
42151
+ delete account.cooldownKind;
42152
+ }
42153
+ }
42133
42154
  function rollWeeklyAnchorForward(anchor, now) {
42134
42155
  if (!Number.isFinite(anchor) || anchor <= 0)
42135
42156
  return 0;
@@ -42146,8 +42167,10 @@ function preserveWeeklyAnchor(incoming, stored, now) {
42146
42167
  return incoming;
42147
42168
  return { utilization: incoming.utilization, resetAt: anchor };
42148
42169
  }
42170
+ var MAX_TRANSIENT_COOLDOWN_MS;
42149
42171
  var init_usage_merge = __esm(() => {
42150
42172
  init_score_core();
42173
+ MAX_TRANSIENT_COOLDOWN_MS = 5 * 60 * 1000;
42151
42174
  });
42152
42175
 
42153
42176
  // src/accounts.ts
@@ -42175,6 +42198,10 @@ function makeAccount(providerID, label, tokens) {
42175
42198
  account.orgId = tokens.orgId;
42176
42199
  if (tokens.refreshExpires)
42177
42200
  account.refreshExpires = tokens.refreshExpires;
42201
+ if (tokens.refresh && !tokens.inferenceOnly) {
42202
+ account.loginRev = 1;
42203
+ account.loginAt = Date.now();
42204
+ }
42178
42205
  return account;
42179
42206
  }
42180
42207
  function recordLostLogin(row, login, reason) {
@@ -42195,10 +42222,14 @@ function mergeLoginUsage(row, usage) {
42195
42222
  weekly: usage.weekly ? preserveWeeklyAnchor(usage.weekly, row.usage.weekly, usage.capturedAt) : row.usage.weekly,
42196
42223
  capturedAt: usage.weekly ? usage.capturedAt : row.usage.capturedAt
42197
42224
  };
42225
+ if (usage.weekly)
42226
+ reconcileQuotaCooldown(row, loadScoreConfig(), usage.capturedAt);
42198
42227
  }
42199
42228
  function applyTokens(row, tokens) {
42200
- if (tokens.refresh)
42229
+ if (tokens.refresh) {
42201
42230
  row.refresh = tokens.refresh;
42231
+ bumpLoginRev(row);
42232
+ }
42202
42233
  row.access = tokens.access;
42203
42234
  row.expires = tokens.expires;
42204
42235
  row.disabledReason = null;
@@ -42286,6 +42317,7 @@ async function bootstrapFromOpencodeAuth(providerID, getAuth, tokensFromApiKey)
42286
42317
  var init_accounts = __esm(() => {
42287
42318
  init_pairing();
42288
42319
  init_store();
42320
+ init_score_core();
42289
42321
  init_types2();
42290
42322
  init_usage_merge();
42291
42323
  });
@@ -42408,6 +42440,15 @@ function mergeHeaders(input, init) {
42408
42440
  return headers;
42409
42441
  }
42410
42442
 
42443
+ // src/refresh-hooks.ts
42444
+ function setRefreshHooks(hooks) {
42445
+ current = hooks;
42446
+ }
42447
+ function getRefreshHooks() {
42448
+ return current;
42449
+ }
42450
+ var current = null;
42451
+
42411
42452
  // src/refresh.ts
42412
42453
  function needsRefresh(account, now) {
42413
42454
  return !account.access || account.expires - REFRESH_SKEW_MS <= now;
@@ -42466,6 +42507,7 @@ async function commitRefresh(accountId, attempt, next) {
42466
42507
  if (!sameGeneration(stored, attempt))
42467
42508
  return tokensOf(stored);
42468
42509
  applyTokensTo(stored, next);
42510
+ bumpLoginRev(stored);
42469
42511
  stored.tokenGen = attempt.gen + 1;
42470
42512
  if (stored.disabledReason !== MANUAL_DISABLED_REASON)
42471
42513
  stored.disabledReason = null;
@@ -42492,11 +42534,31 @@ async function resolveInvalidGrant(adapter, accountId, attempt, reason) {
42492
42534
  return null;
42493
42535
  });
42494
42536
  }
42537
+ async function pullNewer(account, recovery) {
42538
+ try {
42539
+ const adopted = await getRefreshHooks()?.pull?.(account, recovery);
42540
+ return adopted ? await readPoolAccount(account.id) : undefined;
42541
+ } catch {
42542
+ return;
42543
+ }
42544
+ }
42545
+ function announceRefresh() {
42546
+ try {
42547
+ getRefreshHooks()?.afterRefresh?.();
42548
+ } catch {
42549
+ ignore();
42550
+ }
42551
+ }
42495
42552
  async function runRefresh(adapter, account, now, rejectedAccess) {
42496
42553
  return withLock(refreshLockDir(adapter.id, account.id), REFRESH_LOCK, async () => {
42497
- const latest = await readPoolAccount(account.id) ?? account;
42498
- if (latest.access !== rejectedAccess && !needsRefresh(latest, Math.max(now, Date.now())))
42499
- return tokensOf(latest);
42554
+ const read = await readPoolAccount(account.id) ?? account;
42555
+ const usable = (row) => row.access !== rejectedAccess && !needsRefresh(row, Math.max(now, Date.now()));
42556
+ if (usable(read))
42557
+ return tokensOf(read);
42558
+ const pulled = await pullNewer(read, false);
42559
+ if (pulled && usable(pulled))
42560
+ return tokensOf(pulled);
42561
+ const latest = pulled ?? read;
42500
42562
  const attempt = {
42501
42563
  refresh: latest.refresh,
42502
42564
  gen: genOf(latest)
@@ -42507,6 +42569,9 @@ async function runRefresh(adapter, account, now, rejectedAccess) {
42507
42569
  } catch (error) {
42508
42570
  if (!(error instanceof Error) || !isInvalidGrant(error))
42509
42571
  throw error;
42572
+ const recovered = await pullNewer(latest, true);
42573
+ if (recovered)
42574
+ return tokensOf(recovered);
42510
42575
  const adopted = await resolveInvalidGrant(adapter, account.id, attempt, refusalReason(error));
42511
42576
  if (adopted)
42512
42577
  return adopted;
@@ -42534,7 +42599,9 @@ async function ensureAccessToken(adapter, account, now, rejectedAccess) {
42534
42599
  const job = runRefresh(adapter, account, now, rejectedAccess);
42535
42600
  inflight.set(account.id, job);
42536
42601
  try {
42537
- return await settleRefresh(adapter, account, job);
42602
+ const access = await settleRefresh(adapter, account, job);
42603
+ announceRefresh();
42604
+ return access;
42538
42605
  } finally {
42539
42606
  inflight.delete(account.id);
42540
42607
  }
@@ -42733,7 +42800,7 @@ var init_session = __esm(() => {
42733
42800
  // src/token-mint.ts
42734
42801
  function autoTokenEnabled(env = process.env) {
42735
42802
  const raw = env.OPENCODE_AUTH_LB_ANTHROPIC_AUTO_TOKEN?.trim().toLowerCase();
42736
- return raw !== "0" && raw !== "false" && raw !== "no" && raw !== "off";
42803
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
42737
42804
  }
42738
42805
  function tokenDue(account, now) {
42739
42806
  if (!account.refresh || account.disabledReason)
@@ -42762,6 +42829,7 @@ async function mintToken(adapter, accountId) {
42762
42829
  if (minted.refresh !== row.refresh) {
42763
42830
  row.refresh = minted.refresh;
42764
42831
  row.tokenGen = attempt.gen + 1;
42832
+ bumpLoginRev(row);
42765
42833
  }
42766
42834
  if (minted.refreshExpires)
42767
42835
  row.refreshExpires = minted.refreshExpires;
@@ -43191,24 +43259,46 @@ var init_usage_headers = __esm(() => {
43191
43259
  });
43192
43260
 
43193
43261
  // src/providers/usage-http.ts
43194
- async function fetchJson(url, headers, timeoutMs) {
43195
- let response;
43262
+ async function request(url, headers, timeoutMs) {
43196
43263
  try {
43197
- response = await fetch(url, {
43264
+ return await fetch(url, {
43198
43265
  headers,
43199
43266
  signal: AbortSignal.timeout(timeoutMs)
43200
43267
  });
43201
43268
  } catch {
43202
43269
  return null;
43203
43270
  }
43271
+ }
43272
+ async function readJson(response) {
43273
+ if (!response)
43274
+ return null;
43204
43275
  if (!response.ok) {
43205
43276
  await response.body?.cancel().catch(ignore);
43206
43277
  return null;
43207
43278
  }
43208
43279
  return await response.json().catch(() => null);
43209
43280
  }
43281
+ async function fetchJson(url, headers, timeoutMs) {
43282
+ return readJson(await request(url, headers, timeoutMs));
43283
+ }
43284
+ async function fetchUsageJson(url, headers, timeoutMs) {
43285
+ const response = await request(url, headers, timeoutMs);
43286
+ if (response?.status === 401) {
43287
+ await response.body?.cancel().catch(ignore);
43288
+ throw new UsageAuthenticationError;
43289
+ }
43290
+ return readJson(response);
43291
+ }
43292
+ var UsageAuthenticationError;
43210
43293
  var init_usage_http = __esm(() => {
43211
43294
  init_util4();
43295
+ UsageAuthenticationError = class UsageAuthenticationError extends Error {
43296
+ status = 401;
43297
+ constructor() {
43298
+ super("Usage endpoint rejected the access token (401)");
43299
+ this.name = "UsageAuthenticationError";
43300
+ }
43301
+ };
43212
43302
  });
43213
43303
 
43214
43304
  // src/providers/anthropic/version.ts
@@ -43227,16 +43317,16 @@ function adopt(candidate) {
43227
43317
  const trimmed = candidate.trim();
43228
43318
  if (!VERSION_RE.test(trimmed))
43229
43319
  return;
43230
- if (compareVersions2(trimmed, current) <= 0)
43320
+ if (compareVersions2(trimmed, current2) <= 0)
43231
43321
  return;
43232
- current = trimmed;
43322
+ current2 = trimmed;
43233
43323
  }
43234
43324
  function envOverride() {
43235
43325
  const raw = process.env[CLAUDE_CODE_VERSION_ENV]?.trim();
43236
43326
  return raw && VERSION_RE.test(raw) ? raw : undefined;
43237
43327
  }
43238
43328
  function claudeCodeVersion() {
43239
- return envOverride() ?? current;
43329
+ return envOverride() ?? current2;
43240
43330
  }
43241
43331
  function userAgent() {
43242
43332
  return `claude-cli/${claudeCodeVersion()} (external, cli)`;
@@ -43271,7 +43361,7 @@ async function refreshClaudeCodeVersion(now) {
43271
43361
  await persist(now);
43272
43362
  }
43273
43363
  async function persist(now) {
43274
- await writeJsonAtomic(versionCacheFilePath(), JSON.stringify({ version: current, fetchedAt: now })).catch(ignore);
43364
+ await writeJsonAtomic(versionCacheFilePath(), JSON.stringify({ version: current2, fetchedAt: now })).catch(ignore);
43275
43365
  }
43276
43366
  async function recoverClaudeCodeVersion(res, now) {
43277
43367
  if (envOverride())
@@ -43284,12 +43374,12 @@ async function recoverClaudeCodeVersion(res, now) {
43284
43374
  }
43285
43375
  if (!text.includes(CLAUDE_CODE_VERSION_GATE_CODE))
43286
43376
  return false;
43287
- const before = current;
43377
+ const before = current2;
43288
43378
  const required = CLAUDE_CODE_REQUIRED_VERSION_RE.exec(text)?.[1];
43289
43379
  if (required !== undefined)
43290
43380
  adopt(required);
43291
43381
  await refreshClaudeCodeVersion(now).catch(ignore);
43292
- if (current === before)
43382
+ if (current2 === before)
43293
43383
  return false;
43294
43384
  await persist(now);
43295
43385
  return true;
@@ -43305,7 +43395,7 @@ async function primeClaudeCodeVersion(now) {
43305
43395
  }
43306
43396
  refreshClaudeCodeVersion(now).catch(ignore);
43307
43397
  }
43308
- var VERSION_RE, current;
43398
+ var VERSION_RE, current2;
43309
43399
  var init_version = __esm(() => {
43310
43400
  init_paths();
43311
43401
  init_store();
@@ -43313,7 +43403,7 @@ var init_version = __esm(() => {
43313
43403
  init_usage_http();
43314
43404
  init_constants();
43315
43405
  VERSION_RE = /^\d+\.\d+\.\d+$/;
43316
- current = FALLBACK_CLAUDE_CODE_VERSION;
43406
+ current2 = FALLBACK_CLAUDE_CODE_VERSION;
43317
43407
  });
43318
43408
 
43319
43409
  // src/providers/anthropic/cch.ts
@@ -43648,7 +43738,7 @@ async function probeUsage(access, now) {
43648
43738
  async function fetchUsage(account, now) {
43649
43739
  if (!account.refresh)
43650
43740
  return probeUsage(account.access, now);
43651
- const json = await fetchJson(USAGE_URL, {
43741
+ const json = await fetchUsageJson(USAGE_URL, {
43652
43742
  authorization: `Bearer ${account.access}`,
43653
43743
  "anthropic-beta": "oauth-2025-04-20",
43654
43744
  "user-agent": usageUserAgent(),
@@ -43980,7 +44070,7 @@ function parseUsages(json, now) {
43980
44070
  return hourly || weekly ? { hourly, weekly, capturedAt: now } : null;
43981
44071
  }
43982
44072
  function getUsages(baseUrl, key) {
43983
- return fetchJson(`${baseUrl}/usages`, { authorization: `Bearer ${key}`, accept: "application/json" }, USAGE_HTTP_TIMEOUT_MS);
44073
+ return fetchUsageJson(`${baseUrl}/usages`, { authorization: `Bearer ${key}`, accept: "application/json" }, USAGE_HTTP_TIMEOUT_MS);
43984
44074
  }
43985
44075
  function usageFetcher(baseUrl) {
43986
44076
  return async (account, now) => parseUsages(await getUsages(baseUrl, account.access), now);
@@ -44254,7 +44344,7 @@ async function fetchUsage2(account, now) {
44254
44344
  const accountId = resolveAccountId(account);
44255
44345
  if (accountId)
44256
44346
  headers["chatgpt-account-id"] = accountId;
44257
- const json = await fetchJson(USAGE_URL2, headers, USAGE_HTTP_TIMEOUT_MS);
44347
+ const json = await fetchUsageJson(USAGE_URL2, headers, USAGE_HTTP_TIMEOUT_MS);
44258
44348
  const rateLimit = json?.rate_limit;
44259
44349
  if (!isPlainObject4(rateLimit))
44260
44350
  return null;
@@ -44343,7 +44433,7 @@ async function refreshUsage(adapters, now, poolSnapshot) {
44343
44433
  aliveIds?.add(account.id);
44344
44434
  if (account.disabledReason && account.disabledReason !== MANUAL_DISABLED_REASON)
44345
44435
  continue;
44346
- if (account.usage.capturedAt !== 0 && now - account.usage.capturedAt <= USAGE_REFRESH_TTL_MS)
44436
+ if (account.usage.capturedAt !== 0 && now - account.usage.capturedAt < USAGE_REFRESH_TTL_MS)
44347
44437
  continue;
44348
44438
  const adapter = adapterFor(adapters, account.providerID);
44349
44439
  if (!adapter)
@@ -44365,7 +44455,16 @@ async function refreshUsage(adapters, now, poolSnapshot) {
44365
44455
  await Promise.all(stale.map(async ({ adapter, account }) => {
44366
44456
  try {
44367
44457
  await ensureAccessToken(adapter, account, now);
44368
- const snapshot = await adapter.fetchUsage(account, now);
44458
+ const rejectedAccess = account.access;
44459
+ let snapshot;
44460
+ try {
44461
+ snapshot = await adapter.fetchUsage(account, now);
44462
+ } catch (error) {
44463
+ if (!(error instanceof UsageAuthenticationError) || !account.refresh)
44464
+ throw error;
44465
+ await ensureAccessToken(adapter, account, now, rejectedAccess);
44466
+ snapshot = await adapter.fetchUsage(account, now);
44467
+ }
44369
44468
  if (snapshot) {
44370
44469
  await mutatePool((p) => {
44371
44470
  const stored = findAccount(p, account.id);
@@ -44375,10 +44474,8 @@ async function refreshUsage(adapters, now, poolSnapshot) {
44375
44474
  weekly: snapshot.weekly === null ? stored.usage.weekly : preserveWeeklyAnchor(snapshot.weekly, stored.usage.weekly, now),
44376
44475
  capturedAt: snapshot.weekly === null ? stored.usage.capturedAt : now
44377
44476
  };
44378
- if (snapshot.weekly !== null && stored.cooldownUntil > now + MAX_TRANSIENT_COOLDOWN_MS && !isExhausted(stored, scoreCfg, now)) {
44379
- stored.cooldownUntil = 0;
44380
- delete stored.cooldownKind;
44381
- }
44477
+ if (snapshot.weekly !== null && stored.cooldownUntil === account.cooldownUntil && stored.cooldownKind === account.cooldownKind)
44478
+ reconcileQuotaCooldown(stored, scoreCfg, now);
44382
44479
  }
44383
44480
  });
44384
44481
  }
@@ -44391,16 +44488,17 @@ function refreshUsageInBackground(adapter, now, poolSnapshot) {
44391
44488
  function refreshAllUsageInBackground(now, poolSnapshot) {
44392
44489
  return refreshUsage(ADAPTERS, now, poolSnapshot);
44393
44490
  }
44394
- var USAGE_REFRESH_TTL_MS, MAX_TRANSIENT_COOLDOWN_MS, lastPoll;
44491
+ var USAGE_REFRESH_TTL_MS, lastPoll;
44395
44492
  var init_usage_refresh = __esm(() => {
44396
44493
  init_store();
44397
44494
  init_registry();
44495
+ init_usage_http();
44398
44496
  init_refresh();
44399
44497
  init_score_core();
44400
44498
  init_types2();
44401
44499
  init_usage_merge();
44500
+ init_usage_merge();
44402
44501
  USAGE_REFRESH_TTL_MS = 5 * 60 * 1000;
44403
- MAX_TRANSIENT_COOLDOWN_MS = 5 * 60 * 1000;
44404
44502
  lastPoll = new Map;
44405
44503
  });
44406
44504
 
@@ -45625,6 +45723,12 @@ var init_prime = __esm(() => {
45625
45723
  init_select();
45626
45724
  });
45627
45725
 
45726
+ // src/sync/enabled.ts
45727
+ function syncEnabled(env = process.env) {
45728
+ const raw = env.OPENCODE_AUTH_LB_SYNC?.trim().toLowerCase();
45729
+ return raw !== "0" && raw !== "false" && raw !== "no" && raw !== "off";
45730
+ }
45731
+
45628
45732
  // src/sync/errors.ts
45629
45733
  function describeSyncError(error) {
45630
45734
  return error instanceof SyncError ? error.message : "Sync failed.";
@@ -45732,10 +45836,7 @@ var init_crypto = __esm(() => {
45732
45836
  B64URL_RE = /^[\w-]+$/;
45733
45837
  });
45734
45838
 
45735
- // src/sync/payload.ts
45736
- function entryKey(entry) {
45737
- return `${entry.origin}/${entry.id}`;
45738
- }
45839
+ // src/sync/text.ts
45739
45840
  function isControl(code) {
45740
45841
  return code < 32 || code >= 127 && code <= 159 || code === 1564 || code >= 8203 && code <= 8207 || code >= 8232 && code <= 8238 || code >= 8288 && code <= 8297 || code === 65279;
45741
45842
  }
@@ -45745,6 +45846,106 @@ function printable(text) {
45745
45846
  function boundedString(value, max) {
45746
45847
  return typeof value === "string" && value.length > 0 && value.length <= max ? value : undefined;
45747
45848
  }
45849
+ var MAX_LABEL = 80;
45850
+
45851
+ // src/sync/login-payload.ts
45852
+ function loginKey(providerID, accountId) {
45853
+ return `${providerID}:${accountId}`;
45854
+ }
45855
+ function word(value, max) {
45856
+ const text = boundedString(value, max);
45857
+ return text !== undefined && NO_SPACE_RE.test(text) ? text : undefined;
45858
+ }
45859
+ function positive(value) {
45860
+ return isFiniteNumber(value) && value > 0 ? value : undefined;
45861
+ }
45862
+ function parseLogin(raw) {
45863
+ if (!isPlainObject4(raw))
45864
+ return null;
45865
+ const providerID = word(raw.providerID, MAX_ID);
45866
+ const accountId = word(raw.accountId, MAX_ID);
45867
+ const label = printable(boundedString(raw.label, MAX_LABEL) ?? "");
45868
+ const [access, refresh] = [
45869
+ word(raw.access, MAX_TOKEN),
45870
+ word(raw.refresh, MAX_TOKEN)
45871
+ ];
45872
+ const { rev, at } = raw;
45873
+ if (!providerID || !LOGIN_PROVIDERS.has(providerID) || !accountId)
45874
+ return null;
45875
+ if (raw.key !== loginKey(providerID, accountId))
45876
+ return null;
45877
+ const expires = positive(raw.expires);
45878
+ if (!label || !access || !refresh || !expires)
45879
+ return null;
45880
+ if (typeof rev !== "number" || !Number.isSafeInteger(rev) || rev < 1)
45881
+ return null;
45882
+ if (!isFiniteNumber(at) || at < 0)
45883
+ return null;
45884
+ const orgId = word(raw.orgId, MAX_ID);
45885
+ const refreshExpires = positive(raw.refreshExpires);
45886
+ return {
45887
+ key: loginKey(providerID, accountId),
45888
+ providerID,
45889
+ label,
45890
+ access,
45891
+ refresh,
45892
+ expires,
45893
+ accountId,
45894
+ ...orgId ? { orgId } : {},
45895
+ ...refreshExpires ? { refreshExpires } : {},
45896
+ rev,
45897
+ at
45898
+ };
45899
+ }
45900
+ function sortLogins(logins) {
45901
+ return [...logins].sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
45902
+ }
45903
+ function loginsDigest(logins) {
45904
+ return fingerprint(JSON.stringify(sortLogins(logins)));
45905
+ }
45906
+ function compareLogins(a, b) {
45907
+ if (a.rev !== b.rev)
45908
+ return a.rev < b.rev ? -1 : 1;
45909
+ if (a.at !== b.at)
45910
+ return a.at < b.at ? -1 : 1;
45911
+ const [x, y] = [fingerprint(a.refresh), fingerprint(b.refresh)];
45912
+ return x < y ? -1 : x > y ? 1 : 0;
45913
+ }
45914
+ function parseLoginList(rawList) {
45915
+ const parsed = {
45916
+ logins: [],
45917
+ loginsListed: new Set,
45918
+ loginsUnreadable: 0
45919
+ };
45920
+ for (const raw of rawList) {
45921
+ const key = isPlainObject4(raw) ? boundedString(raw.key, MAX_KEY) : undefined;
45922
+ if (key)
45923
+ parsed.loginsListed.add(key);
45924
+ const login = parseLogin(raw);
45925
+ if (!login)
45926
+ parsed.loginsUnreadable += 1;
45927
+ else if (!parsed.logins.some((l) => l.key === login.key))
45928
+ parsed.logins.push(login);
45929
+ }
45930
+ return parsed;
45931
+ }
45932
+ var MAX_LOGINS = 64, MAX_TOKEN = 8192, MAX_ID = 128, MAX_KEY = 300, NO_SPACE_RE, LOGIN_PROVIDERS;
45933
+ var init_login_payload = __esm(() => {
45934
+ init_util4();
45935
+ init_crypto();
45936
+ NO_SPACE_RE = /^\S+$/;
45937
+ LOGIN_PROVIDERS = new Set([
45938
+ "anthropic",
45939
+ "openai",
45940
+ "kimi-code-plan-cn",
45941
+ "kimi-code-plan-global"
45942
+ ]);
45943
+ });
45944
+
45945
+ // src/sync/payload.ts
45946
+ function entryKey(entry) {
45947
+ return `${entry.origin}/${entry.id}`;
45948
+ }
45748
45949
  function keyOf(raw) {
45749
45950
  if (!isPlainObject4(raw))
45750
45951
  return null;
@@ -45785,11 +45986,12 @@ function sortEntries(entries) {
45785
45986
  function entriesDigest(entries) {
45786
45987
  return fingerprint(JSON.stringify(sortEntries(entries)));
45787
45988
  }
45788
- function buildPayload(entries, at) {
45989
+ function buildPayload(entries, at, logins = []) {
45789
45990
  return JSON.stringify({
45790
45991
  v: PAYLOAD_VERSION,
45791
45992
  at,
45792
- entries: sortEntries(entries)
45993
+ entries: sortEntries(entries),
45994
+ logins: sortLogins(logins)
45793
45995
  });
45794
45996
  }
45795
45997
  function parsePayload(text) {
@@ -45801,11 +46003,12 @@ function parsePayload(text) {
45801
46003
  }
45802
46004
  if (!isPlainObject4(json) || !Array.isArray(json.entries))
45803
46005
  throw new SyncError("bad-blob");
45804
- if (json.v !== PAYLOAD_VERSION)
46006
+ if (json.v !== PAYLOAD_VERSION && json.v !== ENTRIES_ONLY_VERSION)
45805
46007
  throw new SyncError("bad-version");
45806
- if (!isFiniteNumber(json.at))
46008
+ const rawLogins = json.v === PAYLOAD_VERSION ? json.logins ?? [] : [];
46009
+ if (!isFiniteNumber(json.at) || !Array.isArray(rawLogins))
45807
46010
  throw new SyncError("bad-blob");
45808
- if (json.entries.length > MAX_ENTRIES)
46011
+ if (json.entries.length > MAX_ENTRIES || rawLogins.length > MAX_LOGINS)
45809
46012
  throw new SyncError("too-large");
45810
46013
  const listed = new Set;
45811
46014
  const entries = [];
@@ -45824,13 +46027,20 @@ function parsePayload(text) {
45824
46027
  else
45825
46028
  unreadable += 1;
45826
46029
  }
45827
- return { at: json.at, entries, listed, unreadable };
46030
+ return {
46031
+ at: json.at,
46032
+ entries,
46033
+ listed,
46034
+ unreadable,
46035
+ ...parseLoginList(rawLogins)
46036
+ };
45828
46037
  }
45829
- 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;
46038
+ 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;
45830
46039
  var init_payload = __esm(() => {
45831
46040
  init_util4();
45832
46041
  init_crypto();
45833
46042
  init_errors6();
46043
+ init_login_payload();
45834
46044
  ID_RE = /^[\w-]{1,64}$/;
45835
46045
  ORIGIN_RE = /^[\da-f]{32}$/;
45836
46046
  CLAUDE_TOKEN_RE = /^sk-ant-oat\d+-[\w-]+$/;
@@ -45920,7 +46130,7 @@ function backoffFrom(res, now) {
45920
46130
  function isRateLimited(res) {
45921
46131
  return res.status === 429 || res.status === 403 && (res.headers.get("x-ratelimit-remaining") === "0" || res.headers.has("retry-after"));
45922
46132
  }
45923
- async function request(url, init, now) {
46133
+ async function request2(url, init, now) {
45924
46134
  let res;
45925
46135
  try {
45926
46136
  res = await globalThis.fetch(url, {
@@ -45972,7 +46182,7 @@ async function boundedText(res) {
45972
46182
  }
45973
46183
  return Buffer.concat(chunks).toString("utf8");
45974
46184
  }
45975
- async function readJson(res) {
46185
+ async function readJson2(res) {
45976
46186
  if (!res.ok)
45977
46187
  throw new SyncError("http");
45978
46188
  const text = await boundedText(res);
@@ -45987,7 +46197,7 @@ async function readJson(res) {
45987
46197
  return json;
45988
46198
  }
45989
46199
  async function createGist(token, content, now = Date.now()) {
45990
- const res = await request(API, {
46200
+ const res = await request2(API, {
45991
46201
  method: "POST",
45992
46202
  headers: { ...headers(token), "content-type": "application/json" },
45993
46203
  body: JSON.stringify({
@@ -45996,7 +46206,7 @@ async function createGist(token, content, now = Date.now()) {
45996
46206
  files: { [GIST_FILE]: { content } }
45997
46207
  })
45998
46208
  }, now);
45999
- const json = await readJson(res);
46209
+ const json = await readJson2(res);
46000
46210
  const owner = isPlainObject4(json.owner) ? json.owner.login : undefined;
46001
46211
  if (typeof json.id !== "string" || !GIST_ID_RE.test(json.id))
46002
46212
  throw new SyncError("http");
@@ -46008,7 +46218,7 @@ async function createGist(token, content, now = Date.now()) {
46008
46218
  async function updateGist(token, id, content, now = Date.now()) {
46009
46219
  let res;
46010
46220
  try {
46011
- res = await request(`${API}/${id}`, {
46221
+ res = await request2(`${API}/${id}`, {
46012
46222
  method: "PATCH",
46013
46223
  headers: { ...headers(token), "content-type": "application/json" },
46014
46224
  body: JSON.stringify({ files: { [GIST_FILE]: { content } } })
@@ -46020,10 +46230,10 @@ async function updateGist(token, id, content, now = Date.now()) {
46020
46230
  }
46021
46231
  if (res.status === 401 || res.status === 403)
46022
46232
  throw new SyncError("write-denied");
46023
- await readJson(res);
46233
+ await readJson2(res);
46024
46234
  }
46025
46235
  async function readGist(id, etag, now = Date.now(), token) {
46026
- const ask = (auth) => request(`${API}/${id}`, {
46236
+ const ask = (auth) => request2(`${API}/${id}`, {
46027
46237
  headers: {
46028
46238
  ...headers(auth),
46029
46239
  ...etag ? { "if-none-match": etag } : {}
@@ -46034,7 +46244,7 @@ async function readGist(id, etag, now = Date.now(), token) {
46034
46244
  res = await ask();
46035
46245
  if (res.status === 304)
46036
46246
  return { changed: false };
46037
- const json = await readJson(res);
46247
+ const json = await readJson2(res);
46038
46248
  const file = isPlainObject4(json.files) ? json.files[GIST_FILE] : undefined;
46039
46249
  if (!isPlainObject4(file) || typeof file.content !== "string")
46040
46250
  throw new SyncError("bad-blob");
@@ -46076,6 +46286,12 @@ function isRef(value) {
46076
46286
  function isSkip(value) {
46077
46287
  return isPlainObject4(value) && typeof value.fingerprint === "string" && typeof value.blocker === "string" && typeof value.holds === "string";
46078
46288
  }
46289
+ function isMark(value) {
46290
+ return value === true;
46291
+ }
46292
+ function marks(from) {
46293
+ return from && Object.keys(from).length > 0 ? newMap(from) : undefined;
46294
+ }
46079
46295
  function readMap(raw, accept) {
46080
46296
  const map = newMap();
46081
46297
  if (isPlainObject4(raw)) {
@@ -46099,6 +46315,8 @@ function parseState(text) {
46099
46315
  return null;
46100
46316
  const { owner, etag, syncedAt, uploadedDigest, write } = json;
46101
46317
  const { uploadedAt, appliedAt, retryAt, pollAt, writeCheckAt } = json;
46318
+ const loginKnown = marks(readMap(json.loginKnown, isMark));
46319
+ const loginSuppressed = marks(readMap(json.loginSuppressed, isMark));
46102
46320
  return {
46103
46321
  v: 1,
46104
46322
  gistId: json.gistId,
@@ -46106,6 +46324,8 @@ function parseState(text) {
46106
46324
  creator: json.creator === true,
46107
46325
  imported: readMap(json.imported, isRef),
46108
46326
  skipped: readMap(json.skipped, isSkip),
46327
+ ...loginKnown ? { loginKnown } : {},
46328
+ ...loginSuppressed ? { loginSuppressed } : {},
46109
46329
  ...typeof owner === "string" ? { owner } : {},
46110
46330
  ...isWrite(write) ? { write } : {},
46111
46331
  ...typeof etag === "string" ? { etag } : {},
@@ -46335,6 +46555,179 @@ async function discoverGithubToken(run = ghAuthToken, env = process.env) {
46335
46555
  var GH_TIMEOUT_MS = 5000;
46336
46556
  var init_github_auth = () => {};
46337
46557
 
46558
+ // src/sync/logins.ts
46559
+ function loginsSyncEnabled(env = process.env) {
46560
+ const raw = env.OPENCODE_AUTH_LB_SYNC_LOGINS?.trim().toLowerCase();
46561
+ return raw !== "0" && raw !== "false" && raw !== "no" && raw !== "off";
46562
+ }
46563
+ function rowKey(row) {
46564
+ const live = row.refresh !== "" || row.inferenceToken !== undefined && row.lostLogins?.oauth !== undefined;
46565
+ return LOGIN_PROVIDERS.has(row.providerID) && row.accountId && live ? loginKey(row.providerID, row.accountId) : null;
46566
+ }
46567
+ function heldMarks(accounts) {
46568
+ const held = newMap();
46569
+ for (const row of accounts) {
46570
+ const key = rowKey(row);
46571
+ if (key)
46572
+ held[key] = true;
46573
+ }
46574
+ return held;
46575
+ }
46576
+ function ownLogins(pool) {
46577
+ const best = new Map;
46578
+ for (const row of pool.accounts) {
46579
+ if (!row.refresh || !row.accountId)
46580
+ continue;
46581
+ if (row.disabledReason || row.lostLogins?.oauth)
46582
+ continue;
46583
+ const login = parseLogin({
46584
+ key: loginKey(row.providerID, row.accountId),
46585
+ providerID: row.providerID,
46586
+ label: printable(row.label).slice(0, MAX_LABEL).trim() || row.providerID,
46587
+ access: row.access,
46588
+ refresh: row.refresh,
46589
+ expires: row.expires,
46590
+ accountId: row.accountId,
46591
+ orgId: row.orgId,
46592
+ refreshExpires: row.refreshExpires,
46593
+ ...loginVersion(row)
46594
+ });
46595
+ const prior = login ? best.get(login.key) : undefined;
46596
+ if (login && (!prior || compareLogins(login, prior) > 0))
46597
+ best.set(login.key, login);
46598
+ }
46599
+ return sortLogins([...best.values()]).slice(0, MAX_LOGINS);
46600
+ }
46601
+ function mergeLogins(gist, own) {
46602
+ const merged = new Map(gist.map((login) => [login.key, login]));
46603
+ for (const login of own) {
46604
+ const prior = merged.get(login.key);
46605
+ if (prior ? compareLogins(login, prior) > 0 : merged.size < MAX_LOGINS)
46606
+ merged.set(login.key, login);
46607
+ }
46608
+ return sortLogins([...merged.values()]);
46609
+ }
46610
+ function ownCopies(next, own) {
46611
+ return own.filter((login) => next.some((n) => n.key === login.key && n.refresh === login.refresh)).length;
46612
+ }
46613
+ function publishDigest(entries, logins) {
46614
+ return logins.length === 0 ? entriesDigest(entries) : fingerprint(`${entriesDigest(entries)}/${loginsDigest(logins)}`);
46615
+ }
46616
+ function sameLogin(row, login) {
46617
+ return rowKey(row) === login.key && (row.orgId === undefined || login.orgId === undefined || row.orgId === login.orgId);
46618
+ }
46619
+ function versionOf(row) {
46620
+ return { ...loginVersion(row), refresh: row.refresh };
46621
+ }
46622
+ function overwriteLocked(row, login) {
46623
+ const attempt = { refresh: row.refresh, gen: genOf(row) };
46624
+ return mutatePool((pool) => {
46625
+ const cur = findAccount(pool, row.id);
46626
+ if (!cur || !sameGeneration(cur, attempt))
46627
+ return false;
46628
+ cur.access = login.access;
46629
+ cur.refresh = login.refresh;
46630
+ cur.expires = login.expires;
46631
+ if (login.refreshExpires)
46632
+ cur.refreshExpires = login.refreshExpires;
46633
+ else
46634
+ delete cur.refreshExpires;
46635
+ if (login.orgId)
46636
+ cur.orgId = login.orgId;
46637
+ cur.loginRev = login.rev;
46638
+ cur.loginAt = login.at;
46639
+ cur.tokenGen = genOf(cur) + 1;
46640
+ clearLostLogin(cur, "oauth");
46641
+ if (cur.disabledReason?.startsWith(AUTO_DISABLED_PREFIX))
46642
+ cur.disabledReason = null;
46643
+ return true;
46644
+ });
46645
+ }
46646
+ function overwrite(row, login) {
46647
+ return withRefreshLock(row.providerID, row.id, () => overwriteLocked(row, login));
46648
+ }
46649
+ async function create(login) {
46650
+ const tokens = {
46651
+ access: login.access,
46652
+ refresh: login.refresh,
46653
+ expires: login.expires,
46654
+ accountId: login.accountId,
46655
+ ...login.orgId ? { orgId: login.orgId } : {},
46656
+ ...login.refreshExpires ? { refreshExpires: login.refreshExpires } : {}
46657
+ };
46658
+ return mutatePool((pool) => {
46659
+ if (pool.accounts.some((row) => sameLogin(row, login)))
46660
+ return false;
46661
+ const manual = new Set(pool.accounts.filter((row) => row.disabledReason === MANUAL_DISABLED_REASON).map((row) => row.id));
46662
+ const row = placeTokens(pool, login.providerID, tokens, uniqueLabel(pool, login.label), undefined);
46663
+ row.loginRev = login.rev;
46664
+ row.loginAt = login.at;
46665
+ row.tokenGen = genOf(row) + 1;
46666
+ if (manual.has(row.id))
46667
+ row.disabledReason = MANUAL_DISABLED_REASON;
46668
+ return true;
46669
+ });
46670
+ }
46671
+ async function adopt2(login) {
46672
+ const row = (await readPool()).accounts.find((a) => sameLogin(a, login));
46673
+ if (!row)
46674
+ return create(login);
46675
+ return compareLogins(login, versionOf(row)) > 0 ? overwrite(row, login) : false;
46676
+ }
46677
+ async function outrank(login, now) {
46678
+ const row = (await readPool()).accounts.find((a) => sameLogin(a, login));
46679
+ if (!row || compareLogins(versionOf(row), login) > 0)
46680
+ return;
46681
+ const attempt = { refresh: row.refresh, gen: genOf(row) };
46682
+ await mutatePool((pool) => {
46683
+ const cur = findAccount(pool, row.id);
46684
+ if (!cur || !sameGeneration(cur, attempt))
46685
+ return;
46686
+ cur.loginRev = login.rev + 1;
46687
+ cur.loginAt = now;
46688
+ });
46689
+ }
46690
+ async function applyLogins(gist, marks, now) {
46691
+ const known = newMap(marks.known);
46692
+ const suppressed = newMap(marks.suppressed);
46693
+ const held = heldMarks((await readPool()).accounts);
46694
+ for (const key of Object.keys(known)) {
46695
+ if (held[key])
46696
+ continue;
46697
+ suppressed[key] = true;
46698
+ delete known[key];
46699
+ }
46700
+ for (const key of Object.keys(suppressed)) {
46701
+ if (!held[key])
46702
+ continue;
46703
+ delete suppressed[key];
46704
+ const login = gist.find((l) => l.key === key);
46705
+ if (login)
46706
+ await outrank(login, now);
46707
+ }
46708
+ let adopted = 0;
46709
+ for (const login of gist)
46710
+ if (!suppressed[login.key] && await adopt2(login))
46711
+ adopted += 1;
46712
+ return {
46713
+ adopted,
46714
+ known: heldMarks((await readPool()).accounts),
46715
+ suppressed
46716
+ };
46717
+ }
46718
+ var AUTO_DISABLED_PREFIX = "invalid_grant: re-login required";
46719
+ var init_logins = __esm(() => {
46720
+ init_accounts();
46721
+ init_store();
46722
+ init_refresh();
46723
+ init_types2();
46724
+ init_apply();
46725
+ init_crypto();
46726
+ init_login_payload();
46727
+ init_payload();
46728
+ init_state();
46729
+ });
46730
+
46338
46731
  // src/sync/origin.ts
46339
46732
  import { randomBytes as randomBytes2 } from "node:crypto";
46340
46733
  import { readFile as readFile6 } from "node:fs/promises";
@@ -46435,6 +46828,10 @@ var init_timing = __esm(() => {
46435
46828
  function count(n, one, many) {
46436
46829
  return `${n} ${n === 1 ? one : many}`;
46437
46830
  }
46831
+ function uploadedMessage(entries, logins) {
46832
+ const credentials = count(entries, "credential", "credentials");
46833
+ return logins === 0 ? `Uploaded ${credentials}.` : `Uploaded ${credentials} and ${count(logins, "login", "logins")}.`;
46834
+ }
46438
46835
  function recordApplied(gistId, at, applied, syncedAt) {
46439
46836
  return updateSyncState((cur) => cur?.gistId === gistId ? {
46440
46837
  ...cur,
@@ -46458,7 +46855,8 @@ async function runCycle(deps, force, report) {
46458
46855
  const pool = await readPool();
46459
46856
  const backedOff = state.write === "denied" && !force && now() < (state.writeCheckAt ?? 0);
46460
46857
  const mayWrite = token !== null && !backedOff;
46461
- const before = entriesDigest(ownEntries(pool, origin, state.imported));
46858
+ const sharing = loginsSyncEnabled();
46859
+ const before = publishDigest(ownEntries(pool, origin, state.imported), sharing ? ownLogins(pool) : []);
46462
46860
  const dirty = mayWrite && before !== state.uploadedDigest;
46463
46861
  report.retryMs = dirty ? PUBLISH_RETRY_MS : POLL_MS;
46464
46862
  const reread = force || dirty || hasFreedSkips(pool.accounts, state.skipped);
@@ -46480,8 +46878,17 @@ async function runCycle(deps, force, report) {
46480
46878
  ...deps.adapters ? { adapters: deps.adapters } : {},
46481
46879
  afterPass: (soFar) => recordApplied(state.gistId, snapshot.at, soFar, now())
46482
46880
  });
46483
- const own = ownEntries(await readPool(), origin, applied?.imported ?? state.imported);
46484
- const unreadable = snapshot.unreadable > 0;
46881
+ const adoptedLogins = stale || !sharing ? null : await applyLogins(snapshot.logins, { known: state.loginKnown, suppressed: state.loginSuppressed }, now());
46882
+ if (adoptedLogins)
46883
+ await updateSyncState((cur) => cur?.gistId === state.gistId ? {
46884
+ ...cur,
46885
+ loginKnown: marks(adoptedLogins.known),
46886
+ loginSuppressed: marks(adoptedLogins.suppressed)
46887
+ } : cur);
46888
+ const settled = await readPool();
46889
+ const own = ownEntries(settled, origin, applied?.imported ?? state.imported);
46890
+ const ownShared = sharing ? ownLogins(settled) : [];
46891
+ const unreadable = snapshot.unreadable > 0 || snapshot.loginsUnreadable > 0;
46485
46892
  let write = standing;
46486
46893
  let wrote = null;
46487
46894
  let stamped = 0;
@@ -46491,12 +46898,16 @@ async function runCycle(deps, force, report) {
46491
46898
  if (mayWrite && !unreadable) {
46492
46899
  const others = snapshot.entries.filter((e) => e.origin !== origin);
46493
46900
  const next = sortEntries([...others, ...listable(own, others, origin)]);
46494
- const same = JSON.stringify(next) === JSON.stringify(sortEntries(snapshot.entries));
46901
+ const nextLogins = sharing ? mergeLogins(snapshot.logins, ownShared) : snapshot.logins;
46902
+ const same = JSON.stringify(next) === JSON.stringify(sortEntries(snapshot.entries)) && loginsDigest(nextLogins) === loginsDigest(snapshot.logins);
46495
46903
  if (!same || stale) {
46496
46904
  stamped = Math.max(now(), snapshot.at + 1, (state.uploadedAt ?? 0) + 1, (state.appliedAt ?? 0) + 1);
46497
46905
  try {
46498
- await updateGist(token, state.gistId, seal(buildPayload(next, stamped), key), now());
46499
- wrote = next.filter((e) => e.origin === origin);
46906
+ await updateGist(token, state.gistId, seal(buildPayload(next, stamped, nextLogins), key), now());
46907
+ wrote = {
46908
+ entries: next.filter((e) => e.origin === origin).length,
46909
+ logins: ownCopies(nextLogins, ownShared)
46910
+ };
46500
46911
  } catch (error) {
46501
46912
  if (!(error instanceof SyncError) || error.code !== "write-denied")
46502
46913
  throw error;
@@ -46512,18 +46923,22 @@ async function runCycle(deps, force, report) {
46512
46923
  etag: wrote || stale || deferred > 0 || denied ? undefined : read.etag,
46513
46924
  write,
46514
46925
  writeCheckAt: denied ? now() + WRITE_BACKOFF_MS : backedOff ? cur.writeCheckAt : undefined,
46515
- uploadedDigest: mayWrite && !unreadable && !denied ? entriesDigest(own) : cur.uploadedDigest,
46926
+ uploadedDigest: mayWrite && !unreadable && !denied ? publishDigest(own, ownShared) : cur.uploadedDigest,
46516
46927
  ...wrote ? { uploadedAt: stamped } : {}
46517
46928
  } : cur);
46518
46929
  const parts = [];
46519
46930
  const moved = applied ? applied.added + applied.updated + applied.removed + deferred : 0;
46931
+ const adopted = adoptedLogins?.adopted ?? 0;
46520
46932
  if (applied && moved > 0) {
46521
46933
  const tail = deferred > 0 ? `; ${count(deferred, "entry", "entries")} could not be verified yet` : "";
46522
- parts.push(`Synced: ${applied.added} added, ${applied.updated} updated, ${applied.removed} removed${tail}.`);
46523
- } else if (!wrote)
46934
+ const login = adopted > 0 ? `; ${count(adopted, "login", "logins")} adopted` : "";
46935
+ parts.push(`Synced: ${applied.added} added, ${applied.updated} updated, ${applied.removed} removed${tail}${login}.`);
46936
+ } else if (adopted > 0)
46937
+ parts.push(`Synced: ${count(adopted, "login", "logins")} adopted.`);
46938
+ else if (!wrote)
46524
46939
  parts.push("Up to date.");
46525
46940
  if (wrote)
46526
- parts.push(`Uploaded ${count(wrote.length, "credential", "credentials")}.`);
46941
+ parts.push(uploadedMessage(wrote.entries, wrote.logins));
46527
46942
  if (denied)
46528
46943
  parts.push(new SyncError("write-denied").message);
46529
46944
  if (mayWrite && unreadable && state.write !== "unreadable")
@@ -46538,6 +46953,8 @@ var init_cycle = __esm(() => {
46538
46953
  init_errors6();
46539
46954
  init_gist();
46540
46955
  init_github_auth();
46956
+ init_login_payload();
46957
+ init_logins();
46541
46958
  init_merge();
46542
46959
  init_origin();
46543
46960
  init_own();
@@ -46547,9 +46964,6 @@ var init_cycle = __esm(() => {
46547
46964
  });
46548
46965
 
46549
46966
  // src/sync/engine.ts
46550
- function count2(n, one, many) {
46551
- return `${n} ${n === 1 ? one : many}`;
46552
- }
46553
46967
  function createEngine(deps) {
46554
46968
  const { now } = deps;
46555
46969
  async function settle(work, reqAt, scheduled) {
@@ -46582,10 +46996,14 @@ function createEngine(deps) {
46582
46996
  if (!token)
46583
46997
  throw new SyncError("no-auth");
46584
46998
  const origin = await machineOrigin();
46585
- const own = ownEntries(await readPool(), origin, newRefs());
46999
+ const pool = await readPool();
47000
+ const own = ownEntries(pool, origin, newRefs());
47001
+ const sharing = loginsSyncEnabled();
47002
+ const shared = sharing ? ownLogins(pool) : [];
46586
47003
  const key = generateKey();
46587
47004
  const at = now();
46588
- const gist = await createGist(token, seal(buildPayload(own, at), key), now());
47005
+ const gist = await createGist(token, seal(buildPayload(own, at, shared), key), now());
47006
+ const loginKnown = sharing ? marks(heldMarks(pool.accounts)) : undefined;
46589
47007
  await updateSyncState(() => ({
46590
47008
  v: 1,
46591
47009
  gistId: gist.id,
@@ -46594,21 +47012,23 @@ function createEngine(deps) {
46594
47012
  ...gist.owner ? { owner: gist.owner } : {},
46595
47013
  write: "ok",
46596
47014
  syncedAt: now(),
46597
- uploadedDigest: entriesDigest(own),
47015
+ uploadedDigest: publishDigest(own, shared),
46598
47016
  uploadedAt: at,
46599
47017
  appliedAt: at,
46600
47018
  pollAt: now() + POLL_MS,
46601
47019
  imported: newRefs(),
46602
- skipped: newMap()
47020
+ skipped: newMap(),
47021
+ ...loginKnown ? { loginKnown } : {}
46603
47022
  }));
46604
- return `Uploaded ${count2(own.length, "credential", "credentials")}.`;
47023
+ const message = uploadedMessage(own.length, shared.length);
47024
+ return own.length + shared.length === 0 ? `${message} ${NOTHING_TO_SHARE}` : message;
46605
47025
  }
46606
47026
  async function unpublishedDigest() {
46607
47027
  const state = await readSyncState();
46608
47028
  if (!state || state.write === "no-token" || state.write === "unreadable" || state.write === "denied" && now() < (state.writeCheckAt ?? 0))
46609
47029
  return null;
46610
- const own = ownEntries(await readPool(), await machineOrigin(), state.imported);
46611
- const digest = entriesDigest(own);
47030
+ const pool = await readPool();
47031
+ const digest = publishDigest(ownEntries(pool, await machineOrigin(), state.imported), loginsSyncEnabled() ? ownLogins(pool) : []);
46612
47032
  return digest === state.uploadedDigest ? null : digest;
46613
47033
  }
46614
47034
  const engine = {
@@ -46630,7 +47050,9 @@ function createEngine(deps) {
46630
47050
  creator: prior?.creator ?? false,
46631
47051
  ...prior?.owner ? { owner: prior.owner } : {},
46632
47052
  imported: prior?.imported ?? newRefs(),
46633
- skipped: prior?.skipped ?? newMap()
47053
+ skipped: prior?.skipped ?? newMap(),
47054
+ ...prior?.loginKnown ? { loginKnown: prior.loginKnown } : {},
47055
+ ...prior?.loginSuppressed ? { loginSuppressed: prior.loginSuppressed } : {}
46634
47056
  };
46635
47057
  });
46636
47058
  return settle(cycle(false), reqAt, true);
@@ -46657,6 +47079,7 @@ function createEngine(deps) {
46657
47079
  };
46658
47080
  return engine;
46659
47081
  }
47082
+ var NOTHING_TO_SHARE = "No static credential or shareable OAuth login on this machine.";
46660
47083
  var init_engine = __esm(() => {
46661
47084
  init_store();
46662
47085
  init_crypto();
@@ -46664,6 +47087,7 @@ var init_engine = __esm(() => {
46664
47087
  init_errors6();
46665
47088
  init_gist();
46666
47089
  init_github_auth();
47090
+ init_logins();
46667
47091
  init_origin();
46668
47092
  init_own();
46669
47093
  init_payload();
@@ -46671,6 +47095,47 @@ var init_engine = __esm(() => {
46671
47095
  init_timing();
46672
47096
  });
46673
47097
 
47098
+ // src/sync/login-hooks.ts
47099
+ function createPullLogin(runGh) {
47100
+ return async function pullLogin(account, _recovery) {
47101
+ const { providerID, accountId } = account;
47102
+ if (!syncEnabled() || !loginsSyncEnabled())
47103
+ return false;
47104
+ if (!LOGIN_PROVIDERS.has(providerID) || !accountId || !account.refresh)
47105
+ return false;
47106
+ try {
47107
+ const state = await readSyncState();
47108
+ if (!state)
47109
+ return false;
47110
+ const token = await discoverGithubToken(runGh) ?? undefined;
47111
+ const read = await readGist(state.gistId, undefined, Date.now(), token);
47112
+ if (!read.changed)
47113
+ return false;
47114
+ const key = decodeKey(state.key) ?? Buffer.alloc(32);
47115
+ const wanted = loginKey(providerID, accountId);
47116
+ const login = parsePayload(open2(read.content, key)).logins.find((l) => l.key === wanted);
47117
+ if (!login || !sameLogin(account, login))
47118
+ return false;
47119
+ if (compareLogins(login, versionOf(account)) <= 0)
47120
+ return false;
47121
+ return await overwriteLocked(account, login);
47122
+ } catch {
47123
+ return false;
47124
+ }
47125
+ };
47126
+ }
47127
+ var pullLogin;
47128
+ var init_login_hooks = __esm(() => {
47129
+ init_crypto();
47130
+ init_gist();
47131
+ init_github_auth();
47132
+ init_login_payload();
47133
+ init_logins();
47134
+ init_payload();
47135
+ init_state();
47136
+ pullLogin = createPullLogin();
47137
+ });
47138
+
46674
47139
  // src/sync/intent.ts
46675
47140
  import { randomUUID as randomUUID3 } from "node:crypto";
46676
47141
  import { access, readFile as readFile7, rename as rename2, rm as rm3 } from "node:fs/promises";
@@ -46872,10 +47337,6 @@ var init_loop = __esm(() => {
46872
47337
  });
46873
47338
 
46874
47339
  // src/sync/start.ts
46875
- function syncEnabled(env = process.env) {
46876
- const raw = env.OPENCODE_AUTH_LB_SYNC?.trim().toLowerCase();
46877
- return raw !== "0" && raw !== "false" && raw !== "no" && raw !== "off";
46878
- }
46879
47340
  function startSync() {
46880
47341
  if (!syncEnabled())
46881
47342
  return null;
@@ -46884,11 +47345,110 @@ function startSync() {
46884
47345
  now: Date.now
46885
47346
  });
46886
47347
  loop.start();
46887
- return loop;
47348
+ if (!loginsSyncEnabled())
47349
+ return loop;
47350
+ setRefreshHooks({
47351
+ pull: pullLogin,
47352
+ afterRefresh: () => void publishAfterLogin().catch(ignore)
47353
+ });
47354
+ return {
47355
+ ...loop,
47356
+ dispose() {
47357
+ setRefreshHooks(null);
47358
+ return loop.dispose();
47359
+ }
47360
+ };
47361
+ }
47362
+ async function publishAfterLogin(options = {}) {
47363
+ const { engine = createEngine({ now: Date.now }) } = options;
47364
+ const { timeoutMs = LOGIN_PUBLISH_MS } = options;
47365
+ let timer;
47366
+ const publish = async () => {
47367
+ if (!syncEnabled() || !loginsSyncEnabled() || !await readSyncState())
47368
+ return;
47369
+ await withCycleLock(() => engine.sync(), timeoutMs);
47370
+ };
47371
+ await Promise.race([
47372
+ publish().catch(ignore),
47373
+ new Promise((resolve) => {
47374
+ timer = setTimeout(resolve, timeoutMs);
47375
+ })
47376
+ ]);
47377
+ clearTimeout(timer);
46888
47378
  }
47379
+ var LOGIN_PUBLISH_MS = 20000;
46889
47380
  var init_start = __esm(() => {
47381
+ init_util4();
46890
47382
  init_engine();
47383
+ init_login_hooks();
47384
+ init_logins();
47385
+ init_loop();
47386
+ init_state();
47387
+ });
47388
+
47389
+ // src/usage-poll.ts
47390
+ import { readFile as readFile8 } from "node:fs/promises";
47391
+ async function nextPollAt(path) {
47392
+ try {
47393
+ const value = JSON.parse(await readFile8(path, "utf8"));
47394
+ return isPlainObject4(value) && isFiniteNumber(value.nextPollAt) ? value.nextPollAt : 0;
47395
+ } catch (error) {
47396
+ if (error instanceof SyntaxError || error instanceof Error && "code" in error && error.code === "ENOENT")
47397
+ return 0;
47398
+ throw error;
47399
+ }
47400
+ }
47401
+ function startUsagePoll(deps = {}) {
47402
+ const now = deps.now ?? Date.now;
47403
+ const refresh = deps.refresh ?? refreshAllUsageInBackground;
47404
+ const timers = deps.timers ?? realTimers;
47405
+ let disposed = false;
47406
+ let inflight;
47407
+ async function poll() {
47408
+ const path = `${poolFilePath()}.usage-poll.json`;
47409
+ try {
47410
+ await withLock(`${path}.lock`, {
47411
+ staleMs: 120000,
47412
+ timeoutMs: 0,
47413
+ retryMs: 25,
47414
+ heartbeatMs: 5000
47415
+ }, async () => {
47416
+ const at = now();
47417
+ if (at < await nextPollAt(path))
47418
+ return;
47419
+ await writeJsonAtomic(path, JSON.stringify({ nextPollAt: at + USAGE_REFRESH_TTL_MS }));
47420
+ await refresh(at);
47421
+ });
47422
+ } catch (error) {
47423
+ if (!(error instanceof LockTimeoutError))
47424
+ throw error;
47425
+ }
47426
+ }
47427
+ function tick() {
47428
+ if (disposed)
47429
+ return Promise.resolve();
47430
+ inflight ??= poll().finally(() => {
47431
+ inflight = undefined;
47432
+ });
47433
+ return inflight;
47434
+ }
47435
+ const timer = timers.set(() => void tick().catch(ignore), USAGE_REFRESH_TTL_MS);
47436
+ return {
47437
+ tick,
47438
+ async dispose() {
47439
+ disposed = true;
47440
+ timers.clear(timer);
47441
+ await inflight;
47442
+ }
47443
+ };
47444
+ }
47445
+ var init_usage_poll = __esm(() => {
47446
+ init_lock();
47447
+ init_paths();
47448
+ init_store();
46891
47449
  init_loop();
47450
+ init_usage_refresh();
47451
+ init_util4();
46892
47452
  });
46893
47453
 
46894
47454
  // src/legacy.ts
@@ -46916,6 +47476,8 @@ function buildAuthHook(adapter, client, pending) {
46916
47476
  if (minted || account.inferenceToken !== undefined && account.refresh)
46917
47477
  notifyPaired(client, adapter.id, account);
46918
47478
  await refreshUsageInBackground(adapter, Date.now()).catch(ignore);
47479
+ if (tokens.refresh && !tokens.inferenceOnly)
47480
+ await publishAfterLogin();
46919
47481
  };
46920
47482
  const oauthSuccess = (tokens) => ({
46921
47483
  type: "success",
@@ -47043,8 +47605,12 @@ function createProviderPlugin(adapter) {
47043
47605
  }
47044
47606
  var PROVIDER_LABELS, AnthropicLoadBalancerPlugin, OpenAILoadBalancerPlugin, KimiCodeLoadBalancerPlugin, KimiCodeGlobalLoadBalancerPlugin, lbResult = (output) => ({ title: "Auth Load Balancer", output }), AuthLoadBalancerStatusPlugin = async (input) => {
47045
47607
  const sync = startSync();
47608
+ const usage = startUsagePoll();
47046
47609
  return {
47047
- dispose: async () => sync?.dispose(),
47610
+ dispose: async () => {
47611
+ await usage.dispose();
47612
+ await sync?.dispose();
47613
+ },
47048
47614
  tool: {
47049
47615
  auth_lb_status: tool2({
47050
47616
  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.",
@@ -47133,6 +47699,7 @@ var init_legacy = __esm(() => {
47133
47699
  init_start();
47134
47700
  init_token_mint();
47135
47701
  init_types2();
47702
+ init_usage_poll();
47136
47703
  init_usage_refresh();
47137
47704
  init_util4();
47138
47705
  PROVIDER_LABELS = {