teamclaude-cloud 1.5.7 → 1.5.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamclaude-cloud",
3
- "version": "1.5.7",
3
+ "version": "1.5.8",
4
4
  "description": "Multi-account Claude proxy with quota-based rotation + cloud token sync and auth-key control",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/config.js CHANGED
@@ -160,3 +160,4 @@ export function atomicConfigUpdate(updater) {
160
160
  _configWriteChain = result.then(() => {}, () => {});
161
161
  return result;
162
162
  }
163
+
package/src/index.js CHANGED
@@ -1603,22 +1603,49 @@ async function ensureCloudSession(config) {
1603
1603
  // detection and loses its push (Codex HIGH). Serialize: all concurrent callers
1604
1604
  // share the SAME in-flight refresh instead of each rotating independently.
1605
1605
  if (!_cloudSessionRefresh) {
1606
- const rt = cloud.session.refresh_token;
1607
1606
  _cloudSessionRefresh = (async () => {
1608
1607
  try {
1609
- const s = await cloudRefreshSession(cloud, rt);
1608
+ // #24: the owner session has ONE single-use rotating Supabase refresh token.
1609
+ // A running server + a concurrent CLI can both try to rotate it — one wins,
1610
+ // the other trips reuse detection. Rather than a cross-process FILE LOCK
1611
+ // (whose steal/release edge-cases proved riskier than the rare race — Codex
1612
+ // re-confirmed), RECOVER via freshest-wins: only when THE REFRESH ITSELF
1613
+ // fails (a concurrent process may have already rotated), re-read disk and, if
1614
+ // it holds a fresh DIFFERENT session for the SAME cloud, adopt it. Net: one
1615
+ // rotation lands, the loser adopts it — no cascade, no lock.
1616
+ let s;
1617
+ try {
1618
+ s = await cloudRefreshSession(cloud, cloud.session.refresh_token);
1619
+ } catch (err) {
1620
+ // The refresh failed. Did a concurrent process already rotate + persist a
1621
+ // fresh session? (Scope the recovery to the REFRESH failure only — a
1622
+ // successful refresh whose disk-write later fails must NOT fall in here and
1623
+ // re-adopt the now-consumed old session.)
1624
+ const disk = await loadConfig();
1625
+ const ds = disk?.cloud?.session;
1626
+ const dsExp = (Number(ds?.expires_at) || 0) * 1000;
1627
+ if (disk?.cloud?.url === cloud.url && ds?.access_token
1628
+ && ds.refresh_token && ds.refresh_token !== cloud.session.refresh_token
1629
+ && dsExp && Date.now() + 30_000 < dsExp) {
1630
+ cloud.session = ds; // a concurrent process already refreshed — adopt theirs
1631
+ return ds;
1632
+ }
1633
+ throw err; // genuinely couldn't refresh (and nobody else did)
1634
+ }
1635
+ // Refresh succeeded → the new session is authoritative. Persisting it is
1636
+ // best-effort: a disk-write failure must not discard the valid in-memory
1637
+ // session (returning it keeps this process working; it re-persists next time).
1610
1638
  cloud.session = s;
1611
- await atomicConfigUpdate(c => { c.cloud = { ...(c.cloud || {}), session: s }; });
1639
+ await atomicConfigUpdate(c => { c.cloud = { ...(c.cloud || {}), session: s }; })
1640
+ .catch((e) => console.error(`[TeamClaude] Failed to persist refreshed cloud session: ${e.message}`));
1612
1641
  return s;
1613
1642
  } finally {
1614
1643
  _cloudSessionRefresh = null;
1615
1644
  }
1616
1645
  })();
1617
1646
  }
1618
- // NOTE: concurrent refreshes WITHIN this process share the single in-flight
1619
- // promise above (one rotation). Cross-PROCESS coordination is intentionally not
1620
- // done here — the codebase assumes a single proxy per config (see
1621
- // atomicConfigUpdate's documented cross-process limitation).
1647
+ // Concurrent refreshes WITHIN this process share the single in-flight promise;
1648
+ // the freshest-wins re-read above absorbs a cross-PROCESS rotation race.
1622
1649
  await _cloudSessionRefresh;
1623
1650
  }
1624
1651
  // If the session is (still) expired and cannot be refreshed — a Google/SSO token