opencode-auth-load-balancer 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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) {
@@ -13417,6 +13430,10 @@ function makeAccount(providerID, label, tokens) {
13417
13430
  account.orgId = tokens.orgId;
13418
13431
  if (tokens.refreshExpires)
13419
13432
  account.refreshExpires = tokens.refreshExpires;
13433
+ if (tokens.refresh && !tokens.inferenceOnly) {
13434
+ account.loginRev = 1;
13435
+ account.loginAt = Date.now();
13436
+ }
13420
13437
  return account;
13421
13438
  }
13422
13439
  function recordLostLogin(row, login, reason) {
@@ -13439,8 +13456,10 @@ function mergeLoginUsage(row, usage) {
13439
13456
  };
13440
13457
  }
13441
13458
  function applyTokens(row, tokens) {
13442
- if (tokens.refresh)
13459
+ if (tokens.refresh) {
13443
13460
  row.refresh = tokens.refresh;
13461
+ bumpLoginRev(row);
13462
+ }
13444
13463
  row.access = tokens.access;
13445
13464
  row.expires = tokens.expires;
13446
13465
  row.disabledReason = null;
@@ -13650,6 +13669,15 @@ function mergeHeaders(input, init) {
13650
13669
  return headers;
13651
13670
  }
13652
13671
 
13672
+ // src/refresh-hooks.ts
13673
+ function setRefreshHooks(hooks) {
13674
+ current = hooks;
13675
+ }
13676
+ function getRefreshHooks() {
13677
+ return current;
13678
+ }
13679
+ var current = null;
13680
+
13653
13681
  // src/refresh.ts
13654
13682
  function needsRefresh(account, now) {
13655
13683
  return !account.access || account.expires - REFRESH_SKEW_MS <= now;
@@ -13708,6 +13736,7 @@ async function commitRefresh(accountId, attempt, next) {
13708
13736
  if (!sameGeneration(stored, attempt))
13709
13737
  return tokensOf(stored);
13710
13738
  applyTokensTo(stored, next);
13739
+ bumpLoginRev(stored);
13711
13740
  stored.tokenGen = attempt.gen + 1;
13712
13741
  if (stored.disabledReason !== MANUAL_DISABLED_REASON)
13713
13742
  stored.disabledReason = null;
@@ -13734,11 +13763,31 @@ async function resolveInvalidGrant(adapter, accountId, attempt, reason) {
13734
13763
  return null;
13735
13764
  });
13736
13765
  }
13766
+ async function pullNewer(account, recovery) {
13767
+ try {
13768
+ const adopted = await getRefreshHooks()?.pull?.(account, recovery);
13769
+ return adopted ? await readPoolAccount(account.id) : undefined;
13770
+ } catch {
13771
+ return;
13772
+ }
13773
+ }
13774
+ function announceRefresh() {
13775
+ try {
13776
+ getRefreshHooks()?.afterRefresh?.();
13777
+ } catch {
13778
+ ignore();
13779
+ }
13780
+ }
13737
13781
  async function runRefresh(adapter, account, now, rejectedAccess) {
13738
13782
  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);
13783
+ const read = await readPoolAccount(account.id) ?? account;
13784
+ const usable = (row) => row.access !== rejectedAccess && !needsRefresh(row, Math.max(now, Date.now()));
13785
+ if (usable(read))
13786
+ return tokensOf(read);
13787
+ const pulled = await pullNewer(read, false);
13788
+ if (pulled && usable(pulled))
13789
+ return tokensOf(pulled);
13790
+ const latest = pulled ?? read;
13742
13791
  const attempt = {
13743
13792
  refresh: latest.refresh,
13744
13793
  gen: genOf(latest)
@@ -13749,6 +13798,9 @@ async function runRefresh(adapter, account, now, rejectedAccess) {
13749
13798
  } catch (error) {
13750
13799
  if (!(error instanceof Error) || !isInvalidGrant(error))
13751
13800
  throw error;
13801
+ const recovered = await pullNewer(latest, true);
13802
+ if (recovered)
13803
+ return tokensOf(recovered);
13752
13804
  const adopted = await resolveInvalidGrant(adapter, account.id, attempt, refusalReason(error));
13753
13805
  if (adopted)
13754
13806
  return adopted;
@@ -13776,7 +13828,9 @@ async function ensureAccessToken(adapter, account, now, rejectedAccess) {
13776
13828
  const job = runRefresh(adapter, account, now, rejectedAccess);
13777
13829
  inflight.set(account.id, job);
13778
13830
  try {
13779
- return await settleRefresh(adapter, account, job);
13831
+ const access = await settleRefresh(adapter, account, job);
13832
+ announceRefresh();
13833
+ return access;
13780
13834
  } finally {
13781
13835
  inflight.delete(account.id);
13782
13836
  }
@@ -13975,7 +14029,7 @@ var init_session = __esm(() => {
13975
14029
  // src/token-mint.ts
13976
14030
  function autoTokenEnabled(env = process.env) {
13977
14031
  const raw = env.OPENCODE_AUTH_LB_ANTHROPIC_AUTO_TOKEN?.trim().toLowerCase();
13978
- return raw !== "0" && raw !== "false" && raw !== "no" && raw !== "off";
14032
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
13979
14033
  }
13980
14034
  function tokenDue(account, now) {
13981
14035
  if (!account.refresh || account.disabledReason)
@@ -14004,6 +14058,7 @@ async function mintToken(adapter, accountId) {
14004
14058
  if (minted.refresh !== row.refresh) {
14005
14059
  row.refresh = minted.refresh;
14006
14060
  row.tokenGen = attempt.gen + 1;
14061
+ bumpLoginRev(row);
14007
14062
  }
14008
14063
  if (minted.refreshExpires)
14009
14064
  row.refreshExpires = minted.refreshExpires;
@@ -14469,16 +14524,16 @@ function adopt(candidate) {
14469
14524
  const trimmed = candidate.trim();
14470
14525
  if (!VERSION_RE.test(trimmed))
14471
14526
  return;
14472
- if (compareVersions2(trimmed, current) <= 0)
14527
+ if (compareVersions2(trimmed, current2) <= 0)
14473
14528
  return;
14474
- current = trimmed;
14529
+ current2 = trimmed;
14475
14530
  }
14476
14531
  function envOverride() {
14477
14532
  const raw = process.env[CLAUDE_CODE_VERSION_ENV]?.trim();
14478
14533
  return raw && VERSION_RE.test(raw) ? raw : undefined;
14479
14534
  }
14480
14535
  function claudeCodeVersion() {
14481
- return envOverride() ?? current;
14536
+ return envOverride() ?? current2;
14482
14537
  }
14483
14538
  function userAgent() {
14484
14539
  return `claude-cli/${claudeCodeVersion()} (external, cli)`;
@@ -14513,7 +14568,7 @@ async function refreshClaudeCodeVersion(now) {
14513
14568
  await persist(now);
14514
14569
  }
14515
14570
  async function persist(now) {
14516
- await writeJsonAtomic(versionCacheFilePath(), JSON.stringify({ version: current, fetchedAt: now })).catch(ignore);
14571
+ await writeJsonAtomic(versionCacheFilePath(), JSON.stringify({ version: current2, fetchedAt: now })).catch(ignore);
14517
14572
  }
14518
14573
  async function recoverClaudeCodeVersion(res, now) {
14519
14574
  if (envOverride())
@@ -14526,12 +14581,12 @@ async function recoverClaudeCodeVersion(res, now) {
14526
14581
  }
14527
14582
  if (!text.includes(CLAUDE_CODE_VERSION_GATE_CODE))
14528
14583
  return false;
14529
- const before = current;
14584
+ const before = current2;
14530
14585
  const required = CLAUDE_CODE_REQUIRED_VERSION_RE.exec(text)?.[1];
14531
14586
  if (required !== undefined)
14532
14587
  adopt(required);
14533
14588
  await refreshClaudeCodeVersion(now).catch(ignore);
14534
- if (current === before)
14589
+ if (current2 === before)
14535
14590
  return false;
14536
14591
  await persist(now);
14537
14592
  return true;
@@ -14547,7 +14602,7 @@ async function primeClaudeCodeVersion(now) {
14547
14602
  }
14548
14603
  refreshClaudeCodeVersion(now).catch(ignore);
14549
14604
  }
14550
- var VERSION_RE, current;
14605
+ var VERSION_RE, current2;
14551
14606
  var init_version = __esm(() => {
14552
14607
  init_paths();
14553
14608
  init_store();
@@ -14555,7 +14610,7 @@ var init_version = __esm(() => {
14555
14610
  init_usage_http();
14556
14611
  init_constants();
14557
14612
  VERSION_RE = /^\d+\.\d+\.\d+$/;
14558
- current = FALLBACK_CLAUDE_CODE_VERSION;
14613
+ current2 = FALLBACK_CLAUDE_CODE_VERSION;
14559
14614
  });
14560
14615
 
14561
14616
  // src/providers/anthropic/cch.ts
@@ -16867,6 +16922,12 @@ var init_prime = __esm(() => {
16867
16922
  init_select();
16868
16923
  });
16869
16924
 
16925
+ // src/sync/enabled.ts
16926
+ function syncEnabled(env = process.env) {
16927
+ const raw = env.OPENCODE_AUTH_LB_SYNC?.trim().toLowerCase();
16928
+ return raw !== "0" && raw !== "false" && raw !== "no" && raw !== "off";
16929
+ }
16930
+
16870
16931
  // src/sync/errors.ts
16871
16932
  function describeSyncError(error) {
16872
16933
  return error instanceof SyncError ? error.message : "Sync failed.";
@@ -16974,10 +17035,7 @@ var init_crypto = __esm(() => {
16974
17035
  B64URL_RE = /^[\w-]+$/;
16975
17036
  });
16976
17037
 
16977
- // src/sync/payload.ts
16978
- function entryKey(entry) {
16979
- return `${entry.origin}/${entry.id}`;
16980
- }
17038
+ // src/sync/text.ts
16981
17039
  function isControl(code) {
16982
17040
  return code < 32 || code >= 127 && code <= 159 || code === 1564 || code >= 8203 && code <= 8207 || code >= 8232 && code <= 8238 || code >= 8288 && code <= 8297 || code === 65279;
16983
17041
  }
@@ -16987,6 +17045,106 @@ function printable(text) {
16987
17045
  function boundedString(value, max) {
16988
17046
  return typeof value === "string" && value.length > 0 && value.length <= max ? value : undefined;
16989
17047
  }
17048
+ var MAX_LABEL = 80;
17049
+
17050
+ // src/sync/login-payload.ts
17051
+ function loginKey(providerID, accountId) {
17052
+ return `${providerID}:${accountId}`;
17053
+ }
17054
+ function word(value, max) {
17055
+ const text = boundedString(value, max);
17056
+ return text !== undefined && NO_SPACE_RE.test(text) ? text : undefined;
17057
+ }
17058
+ function positive(value) {
17059
+ return isFiniteNumber(value) && value > 0 ? value : undefined;
17060
+ }
17061
+ function parseLogin(raw) {
17062
+ if (!isPlainObject2(raw))
17063
+ return null;
17064
+ const providerID = word(raw.providerID, MAX_ID);
17065
+ const accountId = word(raw.accountId, MAX_ID);
17066
+ const label = printable(boundedString(raw.label, MAX_LABEL) ?? "");
17067
+ const [access, refresh] = [
17068
+ word(raw.access, MAX_TOKEN),
17069
+ word(raw.refresh, MAX_TOKEN)
17070
+ ];
17071
+ const { rev, at } = raw;
17072
+ if (!providerID || !LOGIN_PROVIDERS.has(providerID) || !accountId)
17073
+ return null;
17074
+ if (raw.key !== loginKey(providerID, accountId))
17075
+ return null;
17076
+ const expires = positive(raw.expires);
17077
+ if (!label || !access || !refresh || !expires)
17078
+ return null;
17079
+ if (typeof rev !== "number" || !Number.isSafeInteger(rev) || rev < 1)
17080
+ return null;
17081
+ if (!isFiniteNumber(at) || at < 0)
17082
+ return null;
17083
+ const orgId = word(raw.orgId, MAX_ID);
17084
+ const refreshExpires = positive(raw.refreshExpires);
17085
+ return {
17086
+ key: loginKey(providerID, accountId),
17087
+ providerID,
17088
+ label,
17089
+ access,
17090
+ refresh,
17091
+ expires,
17092
+ accountId,
17093
+ ...orgId ? { orgId } : {},
17094
+ ...refreshExpires ? { refreshExpires } : {},
17095
+ rev,
17096
+ at
17097
+ };
17098
+ }
17099
+ function sortLogins(logins) {
17100
+ return [...logins].sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
17101
+ }
17102
+ function loginsDigest(logins) {
17103
+ return fingerprint(JSON.stringify(sortLogins(logins)));
17104
+ }
17105
+ function compareLogins(a, b) {
17106
+ if (a.rev !== b.rev)
17107
+ return a.rev < b.rev ? -1 : 1;
17108
+ if (a.at !== b.at)
17109
+ return a.at < b.at ? -1 : 1;
17110
+ const [x, y] = [fingerprint(a.refresh), fingerprint(b.refresh)];
17111
+ return x < y ? -1 : x > y ? 1 : 0;
17112
+ }
17113
+ function parseLoginList(rawList) {
17114
+ const parsed = {
17115
+ logins: [],
17116
+ loginsListed: new Set,
17117
+ loginsUnreadable: 0
17118
+ };
17119
+ for (const raw of rawList) {
17120
+ const key = isPlainObject2(raw) ? boundedString(raw.key, MAX_KEY) : undefined;
17121
+ if (key)
17122
+ parsed.loginsListed.add(key);
17123
+ const login = parseLogin(raw);
17124
+ if (!login)
17125
+ parsed.loginsUnreadable += 1;
17126
+ else if (!parsed.logins.some((l) => l.key === login.key))
17127
+ parsed.logins.push(login);
17128
+ }
17129
+ return parsed;
17130
+ }
17131
+ var MAX_LOGINS = 64, MAX_TOKEN = 8192, MAX_ID = 128, MAX_KEY = 300, NO_SPACE_RE, LOGIN_PROVIDERS;
17132
+ var init_login_payload = __esm(() => {
17133
+ init_util2();
17134
+ init_crypto();
17135
+ NO_SPACE_RE = /^\S+$/;
17136
+ LOGIN_PROVIDERS = new Set([
17137
+ "anthropic",
17138
+ "openai",
17139
+ "kimi-code-plan-cn",
17140
+ "kimi-code-plan-global"
17141
+ ]);
17142
+ });
17143
+
17144
+ // src/sync/payload.ts
17145
+ function entryKey(entry) {
17146
+ return `${entry.origin}/${entry.id}`;
17147
+ }
16990
17148
  function keyOf(raw) {
16991
17149
  if (!isPlainObject2(raw))
16992
17150
  return null;
@@ -17027,11 +17185,12 @@ function sortEntries(entries) {
17027
17185
  function entriesDigest(entries) {
17028
17186
  return fingerprint(JSON.stringify(sortEntries(entries)));
17029
17187
  }
17030
- function buildPayload(entries, at) {
17188
+ function buildPayload(entries, at, logins = []) {
17031
17189
  return JSON.stringify({
17032
17190
  v: PAYLOAD_VERSION,
17033
17191
  at,
17034
- entries: sortEntries(entries)
17192
+ entries: sortEntries(entries),
17193
+ logins: sortLogins(logins)
17035
17194
  });
17036
17195
  }
17037
17196
  function parsePayload(text) {
@@ -17043,11 +17202,12 @@ function parsePayload(text) {
17043
17202
  }
17044
17203
  if (!isPlainObject2(json) || !Array.isArray(json.entries))
17045
17204
  throw new SyncError("bad-blob");
17046
- if (json.v !== PAYLOAD_VERSION)
17205
+ if (json.v !== PAYLOAD_VERSION && json.v !== ENTRIES_ONLY_VERSION)
17047
17206
  throw new SyncError("bad-version");
17048
- if (!isFiniteNumber(json.at))
17207
+ const rawLogins = json.v === PAYLOAD_VERSION ? json.logins ?? [] : [];
17208
+ if (!isFiniteNumber(json.at) || !Array.isArray(rawLogins))
17049
17209
  throw new SyncError("bad-blob");
17050
- if (json.entries.length > MAX_ENTRIES)
17210
+ if (json.entries.length > MAX_ENTRIES || rawLogins.length > MAX_LOGINS)
17051
17211
  throw new SyncError("too-large");
17052
17212
  const listed = new Set;
17053
17213
  const entries = [];
@@ -17066,13 +17226,20 @@ function parsePayload(text) {
17066
17226
  else
17067
17227
  unreadable += 1;
17068
17228
  }
17069
- return { at: json.at, entries, listed, unreadable };
17229
+ return {
17230
+ at: json.at,
17231
+ entries,
17232
+ listed,
17233
+ unreadable,
17234
+ ...parseLoginList(rawLogins)
17235
+ };
17070
17236
  }
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;
17237
+ 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
17238
  var init_payload = __esm(() => {
17073
17239
  init_util2();
17074
17240
  init_crypto();
17075
17241
  init_errors3();
17242
+ init_login_payload();
17076
17243
  ID_RE = /^[\w-]{1,64}$/;
17077
17244
  ORIGIN_RE = /^[\da-f]{32}$/;
17078
17245
  CLAUDE_TOKEN_RE = /^sk-ant-oat\d+-[\w-]+$/;
@@ -17318,6 +17485,12 @@ function isRef(value) {
17318
17485
  function isSkip(value) {
17319
17486
  return isPlainObject2(value) && typeof value.fingerprint === "string" && typeof value.blocker === "string" && typeof value.holds === "string";
17320
17487
  }
17488
+ function isMark(value) {
17489
+ return value === true;
17490
+ }
17491
+ function marks(from) {
17492
+ return from && Object.keys(from).length > 0 ? newMap(from) : undefined;
17493
+ }
17321
17494
  function readMap(raw, accept) {
17322
17495
  const map = newMap();
17323
17496
  if (isPlainObject2(raw)) {
@@ -17341,6 +17514,8 @@ function parseState(text) {
17341
17514
  return null;
17342
17515
  const { owner, etag, syncedAt, uploadedDigest, write } = json;
17343
17516
  const { uploadedAt, appliedAt, retryAt, pollAt, writeCheckAt } = json;
17517
+ const loginKnown = marks(readMap(json.loginKnown, isMark));
17518
+ const loginSuppressed = marks(readMap(json.loginSuppressed, isMark));
17344
17519
  return {
17345
17520
  v: 1,
17346
17521
  gistId: json.gistId,
@@ -17348,6 +17523,8 @@ function parseState(text) {
17348
17523
  creator: json.creator === true,
17349
17524
  imported: readMap(json.imported, isRef),
17350
17525
  skipped: readMap(json.skipped, isSkip),
17526
+ ...loginKnown ? { loginKnown } : {},
17527
+ ...loginSuppressed ? { loginSuppressed } : {},
17351
17528
  ...typeof owner === "string" ? { owner } : {},
17352
17529
  ...isWrite(write) ? { write } : {},
17353
17530
  ...typeof etag === "string" ? { etag } : {},
@@ -17577,6 +17754,179 @@ async function discoverGithubToken(run = ghAuthToken, env = process.env) {
17577
17754
  var GH_TIMEOUT_MS = 5000;
17578
17755
  var init_github_auth = () => {};
17579
17756
 
17757
+ // src/sync/logins.ts
17758
+ function loginsSyncEnabled(env = process.env) {
17759
+ const raw = env.OPENCODE_AUTH_LB_SYNC_LOGINS?.trim().toLowerCase();
17760
+ return raw !== "0" && raw !== "false" && raw !== "no" && raw !== "off";
17761
+ }
17762
+ function rowKey(row) {
17763
+ const live = row.refresh !== "" || row.inferenceToken !== undefined && row.lostLogins?.oauth !== undefined;
17764
+ return LOGIN_PROVIDERS.has(row.providerID) && row.accountId && live ? loginKey(row.providerID, row.accountId) : null;
17765
+ }
17766
+ function heldMarks(accounts) {
17767
+ const held = newMap();
17768
+ for (const row of accounts) {
17769
+ const key = rowKey(row);
17770
+ if (key)
17771
+ held[key] = true;
17772
+ }
17773
+ return held;
17774
+ }
17775
+ function ownLogins(pool) {
17776
+ const best = new Map;
17777
+ for (const row of pool.accounts) {
17778
+ if (!row.refresh || !row.accountId)
17779
+ continue;
17780
+ if (row.disabledReason || row.lostLogins?.oauth)
17781
+ continue;
17782
+ const login = parseLogin({
17783
+ key: loginKey(row.providerID, row.accountId),
17784
+ providerID: row.providerID,
17785
+ label: printable(row.label).slice(0, MAX_LABEL).trim() || row.providerID,
17786
+ access: row.access,
17787
+ refresh: row.refresh,
17788
+ expires: row.expires,
17789
+ accountId: row.accountId,
17790
+ orgId: row.orgId,
17791
+ refreshExpires: row.refreshExpires,
17792
+ ...loginVersion(row)
17793
+ });
17794
+ const prior = login ? best.get(login.key) : undefined;
17795
+ if (login && (!prior || compareLogins(login, prior) > 0))
17796
+ best.set(login.key, login);
17797
+ }
17798
+ return sortLogins([...best.values()]).slice(0, MAX_LOGINS);
17799
+ }
17800
+ function mergeLogins(gist, own) {
17801
+ const merged = new Map(gist.map((login) => [login.key, login]));
17802
+ for (const login of own) {
17803
+ const prior = merged.get(login.key);
17804
+ if (prior ? compareLogins(login, prior) > 0 : merged.size < MAX_LOGINS)
17805
+ merged.set(login.key, login);
17806
+ }
17807
+ return sortLogins([...merged.values()]);
17808
+ }
17809
+ function ownCopies(next, own) {
17810
+ return own.filter((login) => next.some((n) => n.key === login.key && n.refresh === login.refresh)).length;
17811
+ }
17812
+ function publishDigest(entries, logins) {
17813
+ return logins.length === 0 ? entriesDigest(entries) : fingerprint(`${entriesDigest(entries)}/${loginsDigest(logins)}`);
17814
+ }
17815
+ function sameLogin(row, login) {
17816
+ return rowKey(row) === login.key && (row.orgId === undefined || login.orgId === undefined || row.orgId === login.orgId);
17817
+ }
17818
+ function versionOf(row) {
17819
+ return { ...loginVersion(row), refresh: row.refresh };
17820
+ }
17821
+ function overwriteLocked(row, login) {
17822
+ const attempt = { refresh: row.refresh, gen: genOf(row) };
17823
+ return mutatePool((pool) => {
17824
+ const cur = findAccount(pool, row.id);
17825
+ if (!cur || !sameGeneration(cur, attempt))
17826
+ return false;
17827
+ cur.access = login.access;
17828
+ cur.refresh = login.refresh;
17829
+ cur.expires = login.expires;
17830
+ if (login.refreshExpires)
17831
+ cur.refreshExpires = login.refreshExpires;
17832
+ else
17833
+ delete cur.refreshExpires;
17834
+ if (login.orgId)
17835
+ cur.orgId = login.orgId;
17836
+ cur.loginRev = login.rev;
17837
+ cur.loginAt = login.at;
17838
+ cur.tokenGen = genOf(cur) + 1;
17839
+ clearLostLogin(cur, "oauth");
17840
+ if (cur.disabledReason?.startsWith(AUTO_DISABLED_PREFIX))
17841
+ cur.disabledReason = null;
17842
+ return true;
17843
+ });
17844
+ }
17845
+ function overwrite(row, login) {
17846
+ return withRefreshLock(row.providerID, row.id, () => overwriteLocked(row, login));
17847
+ }
17848
+ async function create(login) {
17849
+ const tokens = {
17850
+ access: login.access,
17851
+ refresh: login.refresh,
17852
+ expires: login.expires,
17853
+ accountId: login.accountId,
17854
+ ...login.orgId ? { orgId: login.orgId } : {},
17855
+ ...login.refreshExpires ? { refreshExpires: login.refreshExpires } : {}
17856
+ };
17857
+ return mutatePool((pool) => {
17858
+ if (pool.accounts.some((row) => sameLogin(row, login)))
17859
+ return false;
17860
+ const manual = new Set(pool.accounts.filter((row) => row.disabledReason === MANUAL_DISABLED_REASON).map((row) => row.id));
17861
+ const row = placeTokens(pool, login.providerID, tokens, uniqueLabel(pool, login.label), undefined);
17862
+ row.loginRev = login.rev;
17863
+ row.loginAt = login.at;
17864
+ row.tokenGen = genOf(row) + 1;
17865
+ if (manual.has(row.id))
17866
+ row.disabledReason = MANUAL_DISABLED_REASON;
17867
+ return true;
17868
+ });
17869
+ }
17870
+ async function adopt2(login) {
17871
+ const row = (await readPool()).accounts.find((a) => sameLogin(a, login));
17872
+ if (!row)
17873
+ return create(login);
17874
+ return compareLogins(login, versionOf(row)) > 0 ? overwrite(row, login) : false;
17875
+ }
17876
+ async function outrank(login, now) {
17877
+ const row = (await readPool()).accounts.find((a) => sameLogin(a, login));
17878
+ if (!row || compareLogins(versionOf(row), login) > 0)
17879
+ return;
17880
+ const attempt = { refresh: row.refresh, gen: genOf(row) };
17881
+ await mutatePool((pool) => {
17882
+ const cur = findAccount(pool, row.id);
17883
+ if (!cur || !sameGeneration(cur, attempt))
17884
+ return;
17885
+ cur.loginRev = login.rev + 1;
17886
+ cur.loginAt = now;
17887
+ });
17888
+ }
17889
+ async function applyLogins(gist, marks, now) {
17890
+ const known = newMap(marks.known);
17891
+ const suppressed = newMap(marks.suppressed);
17892
+ const held = heldMarks((await readPool()).accounts);
17893
+ for (const key of Object.keys(known)) {
17894
+ if (held[key])
17895
+ continue;
17896
+ suppressed[key] = true;
17897
+ delete known[key];
17898
+ }
17899
+ for (const key of Object.keys(suppressed)) {
17900
+ if (!held[key])
17901
+ continue;
17902
+ delete suppressed[key];
17903
+ const login = gist.find((l) => l.key === key);
17904
+ if (login)
17905
+ await outrank(login, now);
17906
+ }
17907
+ let adopted = 0;
17908
+ for (const login of gist)
17909
+ if (!suppressed[login.key] && await adopt2(login))
17910
+ adopted += 1;
17911
+ return {
17912
+ adopted,
17913
+ known: heldMarks((await readPool()).accounts),
17914
+ suppressed
17915
+ };
17916
+ }
17917
+ var AUTO_DISABLED_PREFIX = "invalid_grant: re-login required";
17918
+ var init_logins = __esm(() => {
17919
+ init_accounts();
17920
+ init_store();
17921
+ init_refresh();
17922
+ init_types();
17923
+ init_apply();
17924
+ init_crypto();
17925
+ init_login_payload();
17926
+ init_payload();
17927
+ init_state();
17928
+ });
17929
+
17580
17930
  // src/sync/origin.ts
17581
17931
  import { randomBytes as randomBytes2 } from "node:crypto";
17582
17932
  import { readFile as readFile6 } from "node:fs/promises";
@@ -17677,6 +18027,10 @@ var init_timing = __esm(() => {
17677
18027
  function count(n, one, many) {
17678
18028
  return `${n} ${n === 1 ? one : many}`;
17679
18029
  }
18030
+ function uploadedMessage(entries, logins) {
18031
+ const credentials = count(entries, "credential", "credentials");
18032
+ return logins === 0 ? `Uploaded ${credentials}.` : `Uploaded ${credentials} and ${count(logins, "login", "logins")}.`;
18033
+ }
17680
18034
  function recordApplied(gistId, at, applied, syncedAt) {
17681
18035
  return updateSyncState((cur) => cur?.gistId === gistId ? {
17682
18036
  ...cur,
@@ -17700,7 +18054,8 @@ async function runCycle(deps, force, report) {
17700
18054
  const pool = await readPool();
17701
18055
  const backedOff = state.write === "denied" && !force && now() < (state.writeCheckAt ?? 0);
17702
18056
  const mayWrite = token !== null && !backedOff;
17703
- const before = entriesDigest(ownEntries(pool, origin, state.imported));
18057
+ const sharing = loginsSyncEnabled();
18058
+ const before = publishDigest(ownEntries(pool, origin, state.imported), sharing ? ownLogins(pool) : []);
17704
18059
  const dirty = mayWrite && before !== state.uploadedDigest;
17705
18060
  report.retryMs = dirty ? PUBLISH_RETRY_MS : POLL_MS;
17706
18061
  const reread = force || dirty || hasFreedSkips(pool.accounts, state.skipped);
@@ -17722,8 +18077,17 @@ async function runCycle(deps, force, report) {
17722
18077
  ...deps.adapters ? { adapters: deps.adapters } : {},
17723
18078
  afterPass: (soFar) => recordApplied(state.gistId, snapshot.at, soFar, now())
17724
18079
  });
17725
- const own = ownEntries(await readPool(), origin, applied?.imported ?? state.imported);
17726
- const unreadable = snapshot.unreadable > 0;
18080
+ const adoptedLogins = stale || !sharing ? null : await applyLogins(snapshot.logins, { known: state.loginKnown, suppressed: state.loginSuppressed }, now());
18081
+ if (adoptedLogins)
18082
+ await updateSyncState((cur) => cur?.gistId === state.gistId ? {
18083
+ ...cur,
18084
+ loginKnown: marks(adoptedLogins.known),
18085
+ loginSuppressed: marks(adoptedLogins.suppressed)
18086
+ } : cur);
18087
+ const settled = await readPool();
18088
+ const own = ownEntries(settled, origin, applied?.imported ?? state.imported);
18089
+ const ownShared = sharing ? ownLogins(settled) : [];
18090
+ const unreadable = snapshot.unreadable > 0 || snapshot.loginsUnreadable > 0;
17727
18091
  let write = standing;
17728
18092
  let wrote = null;
17729
18093
  let stamped = 0;
@@ -17733,12 +18097,16 @@ async function runCycle(deps, force, report) {
17733
18097
  if (mayWrite && !unreadable) {
17734
18098
  const others = snapshot.entries.filter((e) => e.origin !== origin);
17735
18099
  const next = sortEntries([...others, ...listable(own, others, origin)]);
17736
- const same = JSON.stringify(next) === JSON.stringify(sortEntries(snapshot.entries));
18100
+ const nextLogins = sharing ? mergeLogins(snapshot.logins, ownShared) : snapshot.logins;
18101
+ const same = JSON.stringify(next) === JSON.stringify(sortEntries(snapshot.entries)) && loginsDigest(nextLogins) === loginsDigest(snapshot.logins);
17737
18102
  if (!same || stale) {
17738
18103
  stamped = Math.max(now(), snapshot.at + 1, (state.uploadedAt ?? 0) + 1, (state.appliedAt ?? 0) + 1);
17739
18104
  try {
17740
- await updateGist(token, state.gistId, seal(buildPayload(next, stamped), key), now());
17741
- wrote = next.filter((e) => e.origin === origin);
18105
+ await updateGist(token, state.gistId, seal(buildPayload(next, stamped, nextLogins), key), now());
18106
+ wrote = {
18107
+ entries: next.filter((e) => e.origin === origin).length,
18108
+ logins: ownCopies(nextLogins, ownShared)
18109
+ };
17742
18110
  } catch (error) {
17743
18111
  if (!(error instanceof SyncError) || error.code !== "write-denied")
17744
18112
  throw error;
@@ -17754,18 +18122,22 @@ async function runCycle(deps, force, report) {
17754
18122
  etag: wrote || stale || deferred > 0 || denied ? undefined : read.etag,
17755
18123
  write,
17756
18124
  writeCheckAt: denied ? now() + WRITE_BACKOFF_MS : backedOff ? cur.writeCheckAt : undefined,
17757
- uploadedDigest: mayWrite && !unreadable && !denied ? entriesDigest(own) : cur.uploadedDigest,
18125
+ uploadedDigest: mayWrite && !unreadable && !denied ? publishDigest(own, ownShared) : cur.uploadedDigest,
17758
18126
  ...wrote ? { uploadedAt: stamped } : {}
17759
18127
  } : cur);
17760
18128
  const parts = [];
17761
18129
  const moved = applied ? applied.added + applied.updated + applied.removed + deferred : 0;
18130
+ const adopted = adoptedLogins?.adopted ?? 0;
17762
18131
  if (applied && moved > 0) {
17763
18132
  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)
18133
+ const login = adopted > 0 ? `; ${count(adopted, "login", "logins")} adopted` : "";
18134
+ parts.push(`Synced: ${applied.added} added, ${applied.updated} updated, ${applied.removed} removed${tail}${login}.`);
18135
+ } else if (adopted > 0)
18136
+ parts.push(`Synced: ${count(adopted, "login", "logins")} adopted.`);
18137
+ else if (!wrote)
17766
18138
  parts.push("Up to date.");
17767
18139
  if (wrote)
17768
- parts.push(`Uploaded ${count(wrote.length, "credential", "credentials")}.`);
18140
+ parts.push(uploadedMessage(wrote.entries, wrote.logins));
17769
18141
  if (denied)
17770
18142
  parts.push(new SyncError("write-denied").message);
17771
18143
  if (mayWrite && unreadable && state.write !== "unreadable")
@@ -17780,6 +18152,8 @@ var init_cycle = __esm(() => {
17780
18152
  init_errors3();
17781
18153
  init_gist();
17782
18154
  init_github_auth();
18155
+ init_login_payload();
18156
+ init_logins();
17783
18157
  init_merge();
17784
18158
  init_origin();
17785
18159
  init_own();
@@ -17789,9 +18163,6 @@ var init_cycle = __esm(() => {
17789
18163
  });
17790
18164
 
17791
18165
  // src/sync/engine.ts
17792
- function count2(n, one, many) {
17793
- return `${n} ${n === 1 ? one : many}`;
17794
- }
17795
18166
  function createEngine(deps) {
17796
18167
  const { now } = deps;
17797
18168
  async function settle(work, reqAt, scheduled) {
@@ -17824,10 +18195,14 @@ function createEngine(deps) {
17824
18195
  if (!token)
17825
18196
  throw new SyncError("no-auth");
17826
18197
  const origin = await machineOrigin();
17827
- const own = ownEntries(await readPool(), origin, newRefs());
18198
+ const pool = await readPool();
18199
+ const own = ownEntries(pool, origin, newRefs());
18200
+ const sharing = loginsSyncEnabled();
18201
+ const shared = sharing ? ownLogins(pool) : [];
17828
18202
  const key = generateKey();
17829
18203
  const at = now();
17830
- const gist = await createGist(token, seal(buildPayload(own, at), key), now());
18204
+ const gist = await createGist(token, seal(buildPayload(own, at, shared), key), now());
18205
+ const loginKnown = sharing ? marks(heldMarks(pool.accounts)) : undefined;
17831
18206
  await updateSyncState(() => ({
17832
18207
  v: 1,
17833
18208
  gistId: gist.id,
@@ -17836,21 +18211,23 @@ function createEngine(deps) {
17836
18211
  ...gist.owner ? { owner: gist.owner } : {},
17837
18212
  write: "ok",
17838
18213
  syncedAt: now(),
17839
- uploadedDigest: entriesDigest(own),
18214
+ uploadedDigest: publishDigest(own, shared),
17840
18215
  uploadedAt: at,
17841
18216
  appliedAt: at,
17842
18217
  pollAt: now() + POLL_MS,
17843
18218
  imported: newRefs(),
17844
- skipped: newMap()
18219
+ skipped: newMap(),
18220
+ ...loginKnown ? { loginKnown } : {}
17845
18221
  }));
17846
- return `Uploaded ${count2(own.length, "credential", "credentials")}.`;
18222
+ const message = uploadedMessage(own.length, shared.length);
18223
+ return own.length + shared.length === 0 ? `${message} ${NOTHING_TO_SHARE}` : message;
17847
18224
  }
17848
18225
  async function unpublishedDigest() {
17849
18226
  const state = await readSyncState();
17850
18227
  if (!state || state.write === "no-token" || state.write === "unreadable" || state.write === "denied" && now() < (state.writeCheckAt ?? 0))
17851
18228
  return null;
17852
- const own = ownEntries(await readPool(), await machineOrigin(), state.imported);
17853
- const digest = entriesDigest(own);
18229
+ const pool = await readPool();
18230
+ const digest = publishDigest(ownEntries(pool, await machineOrigin(), state.imported), loginsSyncEnabled() ? ownLogins(pool) : []);
17854
18231
  return digest === state.uploadedDigest ? null : digest;
17855
18232
  }
17856
18233
  const engine = {
@@ -17872,7 +18249,9 @@ function createEngine(deps) {
17872
18249
  creator: prior?.creator ?? false,
17873
18250
  ...prior?.owner ? { owner: prior.owner } : {},
17874
18251
  imported: prior?.imported ?? newRefs(),
17875
- skipped: prior?.skipped ?? newMap()
18252
+ skipped: prior?.skipped ?? newMap(),
18253
+ ...prior?.loginKnown ? { loginKnown: prior.loginKnown } : {},
18254
+ ...prior?.loginSuppressed ? { loginSuppressed: prior.loginSuppressed } : {}
17876
18255
  };
17877
18256
  });
17878
18257
  return settle(cycle(false), reqAt, true);
@@ -17899,6 +18278,7 @@ function createEngine(deps) {
17899
18278
  };
17900
18279
  return engine;
17901
18280
  }
18281
+ var NOTHING_TO_SHARE = "No static credential or shareable OAuth login on this machine.";
17902
18282
  var init_engine = __esm(() => {
17903
18283
  init_store();
17904
18284
  init_crypto();
@@ -17906,6 +18286,7 @@ var init_engine = __esm(() => {
17906
18286
  init_errors3();
17907
18287
  init_gist();
17908
18288
  init_github_auth();
18289
+ init_logins();
17909
18290
  init_origin();
17910
18291
  init_own();
17911
18292
  init_payload();
@@ -17913,6 +18294,47 @@ var init_engine = __esm(() => {
17913
18294
  init_timing();
17914
18295
  });
17915
18296
 
18297
+ // src/sync/login-hooks.ts
18298
+ function createPullLogin(runGh) {
18299
+ return async function pullLogin(account, _recovery) {
18300
+ const { providerID, accountId } = account;
18301
+ if (!syncEnabled() || !loginsSyncEnabled())
18302
+ return false;
18303
+ if (!LOGIN_PROVIDERS.has(providerID) || !accountId || !account.refresh)
18304
+ return false;
18305
+ try {
18306
+ const state = await readSyncState();
18307
+ if (!state)
18308
+ return false;
18309
+ const token = await discoverGithubToken(runGh) ?? undefined;
18310
+ const read = await readGist(state.gistId, undefined, Date.now(), token);
18311
+ if (!read.changed)
18312
+ return false;
18313
+ const key = decodeKey(state.key) ?? Buffer.alloc(32);
18314
+ const wanted = loginKey(providerID, accountId);
18315
+ const login = parsePayload(open(read.content, key)).logins.find((l) => l.key === wanted);
18316
+ if (!login || !sameLogin(account, login))
18317
+ return false;
18318
+ if (compareLogins(login, versionOf(account)) <= 0)
18319
+ return false;
18320
+ return await overwriteLocked(account, login);
18321
+ } catch {
18322
+ return false;
18323
+ }
18324
+ };
18325
+ }
18326
+ var pullLogin;
18327
+ var init_login_hooks = __esm(() => {
18328
+ init_crypto();
18329
+ init_gist();
18330
+ init_github_auth();
18331
+ init_login_payload();
18332
+ init_logins();
18333
+ init_payload();
18334
+ init_state();
18335
+ pullLogin = createPullLogin();
18336
+ });
18337
+
17916
18338
  // src/sync/intent.ts
17917
18339
  import { randomUUID as randomUUID3 } from "node:crypto";
17918
18340
  import { access, readFile as readFile7, rename as rename2, rm as rm3 } from "node:fs/promises";
@@ -18114,10 +18536,6 @@ var init_loop = __esm(() => {
18114
18536
  });
18115
18537
 
18116
18538
  // 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
18539
  function startSync() {
18122
18540
  if (!syncEnabled())
18123
18541
  return null;
@@ -18126,11 +18544,45 @@ function startSync() {
18126
18544
  now: Date.now
18127
18545
  });
18128
18546
  loop.start();
18129
- return loop;
18547
+ if (!loginsSyncEnabled())
18548
+ return loop;
18549
+ setRefreshHooks({
18550
+ pull: pullLogin,
18551
+ afterRefresh: () => void publishAfterLogin().catch(ignore)
18552
+ });
18553
+ return {
18554
+ ...loop,
18555
+ dispose() {
18556
+ setRefreshHooks(null);
18557
+ return loop.dispose();
18558
+ }
18559
+ };
18130
18560
  }
18561
+ async function publishAfterLogin(options = {}) {
18562
+ const { engine = createEngine({ now: Date.now }) } = options;
18563
+ const { timeoutMs = LOGIN_PUBLISH_MS } = options;
18564
+ let timer;
18565
+ const publish = async () => {
18566
+ if (!syncEnabled() || !loginsSyncEnabled() || !await readSyncState())
18567
+ return;
18568
+ await withCycleLock(() => engine.sync(), timeoutMs);
18569
+ };
18570
+ await Promise.race([
18571
+ publish().catch(ignore),
18572
+ new Promise((resolve) => {
18573
+ timer = setTimeout(resolve, timeoutMs);
18574
+ })
18575
+ ]);
18576
+ clearTimeout(timer);
18577
+ }
18578
+ var LOGIN_PUBLISH_MS = 20000;
18131
18579
  var init_start = __esm(() => {
18580
+ init_util2();
18132
18581
  init_engine();
18582
+ init_login_hooks();
18583
+ init_logins();
18133
18584
  init_loop();
18585
+ init_state();
18134
18586
  });
18135
18587
 
18136
18588
  // src/legacy.ts
@@ -18158,6 +18610,8 @@ function buildAuthHook(adapter, client, pending) {
18158
18610
  if (minted || account.inferenceToken !== undefined && account.refresh)
18159
18611
  notifyPaired(client, adapter.id, account);
18160
18612
  await refreshUsageInBackground(adapter, Date.now()).catch(ignore);
18613
+ if (tokens.refresh && !tokens.inferenceOnly)
18614
+ await publishAfterLogin();
18161
18615
  };
18162
18616
  const oauthSuccess = (tokens) => ({
18163
18617
  type: "success",