teamclaude-cloud 1.4.1 → 1.4.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.4.1",
3
+ "version": "1.4.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",
@@ -35,4 +35,4 @@
35
35
  "engines": {
36
36
  "node": ">=18.0.0"
37
37
  }
38
- }
38
+ }
package/src/cloud.js CHANGED
@@ -305,6 +305,27 @@ export async function cloudRefreshToken(cloud, authKey, accountUuid, minRemainin
305
305
  };
306
306
  }
307
307
 
308
+ /**
309
+ * Report a locally-rotated token back to the cloud (auth-key authenticated).
310
+ * When a consumer had to refresh a token LOCALLY (cloud refresh unreachable), it
311
+ * MUST push the rotated token back — otherwise the cloud keeps a now-dead refresh
312
+ * token and every other consumer (and the cloud's own /sync/refresh) fails with
313
+ * invalid_grant. Freshest-token-wins on the server, scoped to the key's group.
314
+ * Best-effort at the call site: a failure to report never breaks the local refresh.
315
+ */
316
+ export async function cloudReportToken(cloud, authKey, accountUuid, { accessToken, refreshToken, expiresAt } = {}) {
317
+ requireCloud(cloud);
318
+ if (!authKey) throw new Error('Auth key required for token report.');
319
+ if (!accountUuid) throw new Error('accountUuid required for token report.');
320
+ const res = await httpJson(`${fnBase(cloud)}/sync/report-token`, {
321
+ method: 'POST',
322
+ headers: fnHeaders(cloud, { 'x-teamclaude-key': authKey }),
323
+ body: { accountUuid, accessToken, refreshToken, expiresAt: normalizeExpiresAt(expiresAt) || 0 },
324
+ });
325
+ if (!res.ok) throw new Error(`Cloud token report failed (${res.status}): ${describe(res.data)}`);
326
+ return { action: res.data.action || 'skipped' };
327
+ }
328
+
308
329
  /**
309
330
  * Pull allowed accounts+tokens using an auth key (no owner session needed).
310
331
  * Returns `{ accounts, allowed, groupId, source }`:
package/src/index.js CHANGED
@@ -7,7 +7,7 @@ import { loadOrCreateConfig, loadConfig, saveConfig, atomicConfigUpdate, getConf
7
7
  import { AccountManager } from './account-manager.js';
8
8
  import { createProxyServer } from './server.js';
9
9
  import { importCredentials, loginOAuth, fetchProfile, refreshAccessToken, isTokenExpiringSoon } from './oauth.js';
10
- import { cloudLogin, cloudRefreshSession, cloudPush, cloudPushUsage, cloudReportUsage, cloudPull, cloudRefreshToken, cloudKeyCreate, cloudKeyList, cloudKeyRevoke, cloudKeyMove, cloudGroupList, cloudGroupCreate, cloudGroupRename, cloudGroupDelete, cloudGroupSetAccounts, cloudAccountSetEnabled, buildUsageFromQuota, mergePulledAccounts, resolveCloud, DEFAULT_CLOUD, keySource, reconcileRemovedAccounts } from './cloud.js';
10
+ import { cloudLogin, 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
12
 
13
13
  const args = process.argv.slice(2);
@@ -1432,23 +1432,38 @@ const _autoPushInFlight = new Set();
1432
1432
  */
1433
1433
  async function maybePushRefreshedToken(config, account, newTokens) {
1434
1434
  const key = account.accountUuid || account.name;
1435
+ const hasOwner = !!(config.cloud && config.cloud.session && config.cloud.session.access_token);
1436
+ const pullKey = config.cloud?.pullKey || process.env.TEAMCLAUDE_AUTH_KEY;
1437
+ // Nothing to sync back to unless we're an admin (owner session) OR a consumer
1438
+ // with a pull key. A consumer that rotated a token LOCALLY (cloud refresh was
1439
+ // unreachable) MUST report it back, or the cloud keeps a now-dead refresh token
1440
+ // and every consumer — plus the cloud's own /sync/refresh — fails invalid_grant.
1441
+ if (!hasOwner && !(pullKey && account.accountUuid)) return;
1435
1442
  try {
1436
- if (!config.cloud || !config.cloud.session || !config.cloud.session.access_token) return; // not an admin/pusher
1437
1443
  if (_autoPushInFlight.has(key)) return;
1438
1444
  _autoPushInFlight.add(key);
1439
- const cloud = await ensureCloudSession(config).catch(() => config.cloud);
1440
- await cloudPush(cloud, [{
1441
- accountUuid: account.accountUuid,
1442
- name: account.name,
1443
- subscriptionType: account.subscriptionType,
1444
- tier: account.tier,
1445
- type: account.type,
1446
- accessToken: newTokens.accessToken,
1447
- refreshToken: newTokens.refreshToken,
1448
- expiresAt: newTokens.expiresAt,
1449
- }]);
1445
+ if (hasOwner) {
1446
+ const cloud = await ensureCloudSession(config).catch(() => config.cloud);
1447
+ await cloudPush(cloud, [{
1448
+ accountUuid: account.accountUuid,
1449
+ name: account.name,
1450
+ subscriptionType: account.subscriptionType,
1451
+ tier: account.tier,
1452
+ type: account.type,
1453
+ accessToken: newTokens.accessToken,
1454
+ refreshToken: newTokens.refreshToken,
1455
+ expiresAt: newTokens.expiresAt,
1456
+ }]);
1457
+ } else {
1458
+ // Consumer path: key-authenticated write-back (freshest-wins on the server).
1459
+ await cloudReportToken(resolveCloud(config), pullKey, account.accountUuid, {
1460
+ accessToken: newTokens.accessToken,
1461
+ refreshToken: newTokens.refreshToken,
1462
+ expiresAt: newTokens.expiresAt,
1463
+ });
1464
+ }
1450
1465
  } catch (err) {
1451
- console.error(`[TeamClaude] Cloud auto-push (token refresh) failed for ${account.name}: ${err.message}`);
1466
+ console.error(`[TeamClaude] Cloud token sync-back failed for ${account.name}: ${err.message}`);
1452
1467
  } finally {
1453
1468
  _autoPushInFlight.delete(key);
1454
1469
  }