teamclaude-cloud 1.5.6 → 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 +36 -9
- package/src/mask.js +9 -0
- package/src/tui.js +2 -3
package/package.json
CHANGED
package/src/config.js
CHANGED
package/src/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createProxyServer } from './server.js';
|
|
|
9
9
|
import { importCredentials, loginOAuth, fetchProfile, refreshAccessToken, isTokenExpiringSoon } from './oauth.js';
|
|
10
10
|
import { cloudLogin, cloudValidateOwner, cloudRefreshSession, cloudPush, cloudPushUsage, cloudReportUsage, cloudPull, cloudRefreshToken, cloudReportToken, cloudKeyCreate, cloudKeyList, cloudKeyRevoke, cloudKeyMove, cloudGroupList, cloudGroupCreate, cloudGroupRename, cloudGroupDelete, cloudGroupSetAccounts, cloudAccountSetEnabled, buildUsageFromQuota, mergePulledAccounts, resolveCloud, DEFAULT_CLOUD, keySource, reconcileRemovedAccounts } from './cloud.js';
|
|
11
11
|
import { TUI } from './tui.js';
|
|
12
|
-
import { maskIf, maskEmail } from './mask.js';
|
|
12
|
+
import { maskIf, maskEmail, maskEmailsIn } from './mask.js';
|
|
13
13
|
|
|
14
14
|
const args = process.argv.slice(2);
|
|
15
15
|
const command = args[0];
|
|
@@ -989,7 +989,7 @@ async function accountsCommand() {
|
|
|
989
989
|
const src = a.source ? `, ${a.source}` : '';
|
|
990
990
|
console.log(` [${i + 1}] ${maskIf(a.name, config.maskMode)} (${status}${src})`);
|
|
991
991
|
if (hasProfile && p.email && p.email !== a.name) console.log(` Email: ${maskIf(p.email, config.maskMode)}`);
|
|
992
|
-
if (hasProfile && p.orgName) console.log(` Org: ${p.orgName}`);
|
|
992
|
+
if (hasProfile && p.orgName) console.log(` Org: ${maskEmailsIn(p.orgName, config.maskMode)}`);
|
|
993
993
|
if (verbose && a.expiresAt) {
|
|
994
994
|
const remaining = a.expiresAt - Date.now();
|
|
995
995
|
if (remaining <= 0) {
|
|
@@ -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
|
package/src/mask.js
CHANGED
|
@@ -29,3 +29,12 @@ export function maskEmail(value) {
|
|
|
29
29
|
export function maskIf(value, on) {
|
|
30
30
|
return on ? maskEmail(value) : value;
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
// Mask every email SUBSTRING inside a free-form string (log lines, "X's Organization",
|
|
34
|
+
// generic table cells) — non-email text is left intact. Use this where an email is
|
|
35
|
+
// embedded in a larger string, not the whole value.
|
|
36
|
+
const EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
37
|
+
export function maskEmailsIn(value, on) {
|
|
38
|
+
if (!on || typeof value !== 'string') return value;
|
|
39
|
+
return value.replace(EMAIL_RE, (m) => maskEmail(m));
|
|
40
|
+
}
|
package/src/tui.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { importCredentials, fetchProfile } from './oauth.js';
|
|
2
|
-
import { maskIf } from './mask.js';
|
|
2
|
+
import { maskIf, maskEmailsIn } from './mask.js';
|
|
3
3
|
|
|
4
4
|
// ── ANSI helpers ─────────────────────────────────────────────
|
|
5
5
|
|
|
@@ -214,8 +214,7 @@ export class TUI {
|
|
|
214
214
|
// (Codex HIGH ×3). Covers TUI-generated lines AND redirected server/account-manager
|
|
215
215
|
// console output, so ordinary request/activity logging can't bypass mask mode.
|
|
216
216
|
_maskLog(s) {
|
|
217
|
-
|
|
218
|
-
return s.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, (m) => maskIf(m, true));
|
|
217
|
+
return maskEmailsIn(s, this.config?.maskMode === true);
|
|
219
218
|
}
|
|
220
219
|
|
|
221
220
|
// Mask a single account name for direct render (active-requests line, etc.).
|