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 +1 -1
- package/src/config.js +1 -0
- package/src/index.js +34 -7
package/package.json
CHANGED
package/src/config.js
CHANGED
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
|
-
|
|
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
|
-
//
|
|
1619
|
-
//
|
|
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
|