teamclaude-cloud 1.5.1 → 1.5.2

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.1",
3
+ "version": "1.5.2",
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/index.js CHANGED
@@ -315,15 +315,25 @@ async function serverCommand() {
315
315
  // running AccountManager's LIVE tokens (freshest) and re-reads the cloud session
316
316
  // from disk so a `cloud login` done after the server started is picked up.
317
317
  cloudPush: config.cloud ? (async () => {
318
- const fresh = (await loadConfig()) || config;
319
- const cloud = await ensureCloudSession(fresh);
320
- if (!cloud?.session?.access_token) throw new Error('Not logged in to cloud — run `teamclaude cloud login`');
318
+ // Use the running server's LIVE tokens (freshest) for each account.
321
319
  const accounts = (config.accounts || []).filter(a => a.type !== 'apikey').map(a => {
322
320
  const am = (a.accountUuid && accountManager.accounts.find(x => x.accountUuid === a.accountUuid))
323
321
  || accountManager.accounts.find(x => x.name === a.name);
324
322
  return am ? { ...a, accessToken: am.credential, refreshToken: am.refreshToken, expiresAt: am.expiresAt } : a;
325
323
  });
326
324
  if (!accounts.length) throw new Error('No OAuth accounts to push');
325
+ // Prefer the AUTH KEY (push to your own tenant, no expiring owner login).
326
+ const fresh = (await loadConfig()) || config;
327
+ const key = fresh.cloud?.pullKey || config.cloud?.pullKey;
328
+ if (key) {
329
+ const r = await cloudPushViaKey(resolveCloud(fresh), key, accounts);
330
+ const na = r.notAllowed ? `, ${r.notAllowed} not in the key's group` : '';
331
+ const sk = r.skipped ? `, ${r.skipped} skipped` : '';
332
+ return { summary: `${r.updated} updated, ${r.unchanged} unchanged${na}${sk} (via auth key)` };
333
+ }
334
+ // No key — owner login (also creates new accounts) + usage snapshot.
335
+ const cloud = await ensureCloudSession(fresh);
336
+ if (!cloud?.session?.access_token) throw new Error('No auth key and not logged in — pull once with a key, or run `teamclaude cloud login`');
327
337
  const usageByUuid = new Map();
328
338
  const quotaCache = await readQuotaCache();
329
339
  for (const entry of quotaCache?.accounts || []) {
@@ -331,7 +341,8 @@ async function serverCommand() {
331
341
  const usage = buildUsageFromQuota(entry);
332
342
  if (usage) usageByUuid.set(entry.accountUuid, usage);
333
343
  }
334
- return await cloudPush(cloud, accounts, usageByUuid);
344
+ const r = await cloudPush(cloud, accounts, usageByUuid);
345
+ return { summary: `${r.pushed} new, ${r.updated} updated, ${r.skipped} skipped${usageByUuid.size ? `, usage ${r.usage ?? 0}` : ''}` };
335
346
  }) : undefined,
336
347
  });
337
348
  hooks = {
@@ -1578,6 +1589,17 @@ async function ensureCloudSession(config) {
1578
1589
  // atomicConfigUpdate's documented cross-process limitation).
1579
1590
  await _cloudSessionRefresh;
1580
1591
  }
1592
+ // If the session is (still) expired and cannot be refreshed — a Google/SSO token
1593
+ // login (`cloud login --token`) stores NO refresh token, so its ~1h token simply
1594
+ // lapses — fail with an actionable message instead of letting the caller hit a
1595
+ // bare 401 unauthorized.
1596
+ const curExpMs = (Number(cloud.session.expires_at) || 0) * 1000;
1597
+ if (curExpMs && Date.now() >= curExpMs) {
1598
+ const hint = cloud.session.refresh_token
1599
+ ? 'Run `teamclaude cloud login` to sign in again.'
1600
+ : 'Re-copy the CLI login token from the dashboard (the "관리자 로그인" box in the Auth Keys section) and run `teamclaude cloud login --token`.';
1601
+ throw new Error(`Cloud session expired. ${hint}`);
1602
+ }
1581
1603
  return cloud;
1582
1604
  }
1583
1605
 
@@ -1705,14 +1727,42 @@ async function cloudLoginCommand(config) {
1705
1727
  console.log(`Logged in to cloud as ${email}.`);
1706
1728
  }
1707
1729
 
1730
+ // Push account tokens to the cloud using an AUTH KEY — no owner login needed. Reuses
1731
+ // the key-authenticated, group-scoped, freshest-wins /sync/report-token path, so the
1732
+ // tokens land in the KEY'S OWNER tenant (your personal key → your own account). This
1733
+ // UPDATES accounts already registered + in the key's group; an account the key can't
1734
+ // pull is reported as "not in the key's group" (create it via an owner login once).
1735
+ async function cloudPushViaKey(cloud, key, accounts) {
1736
+ const r = { updated: 0, unchanged: 0, notAllowed: 0, skipped: 0 };
1737
+ for (const a of accounts) {
1738
+ if (!a.accountUuid || !a.accessToken || !a.refreshToken) { r.skipped++; continue; }
1739
+ try {
1740
+ const res = await cloudReportToken(cloud, key, a.accountUuid, { accessToken: a.accessToken, refreshToken: a.refreshToken, expiresAt: a.expiresAt });
1741
+ if (res.action === 'updated') r.updated++; else r.unchanged++;
1742
+ } catch (e) {
1743
+ if (/\(40[34]\)/.test(e.message)) r.notAllowed++; else r.skipped++; // 403 not in group / 404 unknown account
1744
+ }
1745
+ }
1746
+ return r;
1747
+ }
1748
+
1708
1749
  async function cloudPushCommand(config) {
1709
- const cloud = await ensureCloudSession(config);
1710
- if (!cloud?.session?.access_token) { console.error('Not logged in. Run `teamclaude cloud login`.'); process.exit(1); }
1711
1750
  const accounts = (config.accounts || []).filter(a => a.type !== 'apikey');
1712
1751
  if (!accounts.length) { console.error('No OAuth accounts to push.'); process.exit(1); }
1713
- // Ride the proxy's quota snapshot (<config>.quota.json) along so the cloud
1714
- // dashboard can show 5h/7d/Fable utilization. Absence is fine usage rows
1715
- // are simply not written (the dashboard shows "no data", not a fake zero).
1752
+ // Prefer the AUTH KEY: pushing to your own tenant is the key's job — no separate,
1753
+ // short-lived owner login required (the key is the credential, same as pull).
1754
+ const key = config.cloud?.pullKey || process.env.TEAMCLAUDE_AUTH_KEY;
1755
+ if (key) {
1756
+ const r = await cloudPushViaKey(resolveCloud(config), key, accounts);
1757
+ const na = r.notAllowed ? `, ${r.notAllowed} not in the key's group` : '';
1758
+ const sk = r.skipped ? `, ${r.skipped} skipped (no/expired token)` : '';
1759
+ console.log(`Pushed tokens via auth key: ${r.updated} updated, ${r.unchanged} unchanged${na}${sk}.`);
1760
+ return;
1761
+ }
1762
+ // No auth key — fall back to an owner login (which also CREATES new accounts) and
1763
+ // rides the quota snapshot so the dashboard can show utilization.
1764
+ const cloud = await ensureCloudSession(config);
1765
+ if (!cloud?.session?.access_token) { console.error('No auth key found and not logged in. Pull once with `teamclaude cloud pull --key <key>`, or `teamclaude cloud login`.'); process.exit(1); }
1716
1766
  const usageByUuid = new Map();
1717
1767
  const quotaCache = await readQuotaCache();
1718
1768
  for (const entry of quotaCache?.accounts || []) {
package/src/tui.js CHANGED
@@ -399,8 +399,7 @@ export class TUI {
399
399
  try {
400
400
  this._addLog('Pushing account tokens to the cloud...');
401
401
  const r = await this.cloudPush();
402
- const usageNote = (r && r.usage != null) ? `, usage ${r.usage}` : '';
403
- this._addLog(`Cloud push: ${r?.pushed ?? 0} new, ${r?.updated ?? 0} updated, ${r?.skipped ?? 0} skipped${usageNote}`);
402
+ this._addLog(`Cloud push: ${r?.summary ?? 'done'}`);
404
403
  } catch (e) {
405
404
  this._addLog(`Cloud push failed: ${e.message}`);
406
405
  }