tokentracker-cli 1.1.9 → 1.1.11

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.
Files changed (103) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-BeNHapqp.js → AchievementBadge-DuEuFaXg.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-vNH31cFv.js → AchievementsPage-BTTFtk_Z.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-ChGSmCUP.js → AchievementsSection-BZppKteZ.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-CN0tcpEE.js → BadgeDetailModal-TzmxXy8k.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-CKKYEJt-.js → BotAnimated-4hWTFjwp.js} +1 -1
  6. package/dashboard/dist/assets/{Card-Dx3Pj7SE.js → Card-HigkIm_b.js} +1 -1
  7. package/dashboard/dist/assets/{ClawdAnimated-ChJEskg4.js → ClawdAnimated-BRoihVSd.js} +2 -2
  8. package/dashboard/dist/assets/{CommandPalette-D4-sgyxP.js → CommandPalette-Bx_11gJk.js} +1 -1
  9. package/dashboard/dist/assets/{CommunityStatsModal-NUDCSEve.js → CommunityStatsModal-BjDPCBol.js} +1 -1
  10. package/dashboard/dist/assets/{ConfirmModal-q4EXjOda.js → ConfirmModal-DTo93RpG.js} +1 -1
  11. package/dashboard/dist/assets/{Controls-OjbHepPz.js → Controls-BvWTXyfI.js} +1 -1
  12. package/dashboard/dist/assets/{DashboardPage-F3swcQnu.js → DashboardPage--1_GuEr-.js} +1 -1
  13. package/dashboard/dist/assets/DevicePage-BVupGt9K.js +1 -0
  14. package/dashboard/dist/assets/{DialogClose-DSp5s5nL.js → DialogClose-BlQJpi7n.js} +1 -1
  15. package/dashboard/dist/assets/DialogDescription-DE2BF-HX.js +1 -0
  16. package/dashboard/dist/assets/{DialogTitle-M5ojVj4-.js → DialogTitle-CDmxp9ap.js} +1 -1
  17. package/dashboard/dist/assets/{FadeIn-CWDRIc_C.js → FadeIn-DPercU5n.js} +1 -1
  18. package/dashboard/dist/assets/{HeaderGithubStar-DR4Px_eh.js → HeaderGithubStar-DeVzeBIo.js} +1 -1
  19. package/dashboard/dist/assets/{HoverTooltip-CsfynzUO.js → HoverTooltip-Duo2Icus.js} +1 -1
  20. package/dashboard/dist/assets/{IpCheckPage-C8Y1OFOn.js → IpCheckPage-DgYiaRrT.js} +1 -1
  21. package/dashboard/dist/assets/{LandingPage-BnmP1NUm.js → LandingPage-CYTR4PbQ.js} +2 -2
  22. package/dashboard/dist/assets/{LeaderboardAvatar-DprHPy4v.js → LeaderboardAvatar-B67pMaIl.js} +1 -1
  23. package/dashboard/dist/assets/{LeaderboardPage-BI-ryUMj.js → LeaderboardPage-2wIMkBgq.js} +2 -2
  24. package/dashboard/dist/assets/{LeaderboardProfileModal-r-wgLtCo.js → LeaderboardProfileModal-D3VxkFlx.js} +3 -3
  25. package/dashboard/dist/assets/{LeaderboardProfilePage-qBvdPj6z.js → LeaderboardProfilePage-BS01jN5M.js} +1 -1
  26. package/dashboard/dist/assets/{LimitsPage--ObDuhSD.js → LimitsPage-BGRZW-aH.js} +1 -1
  27. package/dashboard/dist/assets/{LocalOnlyNotice-Cvg81MIm.js → LocalOnlyNotice-Do9KUdi_.js} +1 -1
  28. package/dashboard/dist/assets/{LoginCard-T1vufQym.js → LoginCard-CzAXLXfZ.js} +1 -1
  29. package/dashboard/dist/assets/{LoginModal-CElUuzhg.js → LoginModal-D1J8zG0C.js} +1 -1
  30. package/dashboard/dist/assets/{LoginPage-Decz8Qyn.js → LoginPage-DeZJNMLe.js} +1 -1
  31. package/dashboard/dist/assets/{PetPage-CitsUTsW.js → PetPage-D2oqJ05F.js} +1 -1
  32. package/dashboard/dist/assets/{PopoverPopup-wx5xna8c.js → PopoverPopup-BYzzQJTD.js} +1 -1
  33. package/dashboard/dist/assets/{ProviderIcon-DeCQ9ytY.js → ProviderIcon-BfFkS-OF.js} +1 -1
  34. package/dashboard/dist/assets/{ResetPasswordPage-BklHQ_i8.js → ResetPasswordPage-BFyYvG24.js} +1 -1
  35. package/dashboard/dist/assets/{SegmentedControl-DqbXKZfk.js → SegmentedControl-D5USR3JT.js} +1 -1
  36. package/dashboard/dist/assets/{Select-BPhSzg5T.js → Select-DmFQdVA1.js} +1 -1
  37. package/dashboard/dist/assets/{SelectItemText-BEqonLIf.js → SelectItemText-B5MXGLN-.js} +1 -1
  38. package/dashboard/dist/assets/{ServiceStatusPage-DnAv3J6d.js → ServiceStatusPage-Dvm5eUJK.js} +1 -1
  39. package/dashboard/dist/assets/{SessionsPage-Fr5rFC4h.js → SessionsPage-DQKezzUW.js} +1 -1
  40. package/dashboard/dist/assets/{SettingsPage-DYB-pyw2.js → SettingsPage-DbUDye5r.js} +2 -2
  41. package/dashboard/dist/assets/{SkillsPage-B9aU8WEc.js → SkillsPage-DyX_-dcr.js} +1 -1
  42. package/dashboard/dist/assets/{TokenGalaxy-ybV8JSOT.js → TokenGalaxy-aK3jhWW0.js} +1 -1
  43. package/dashboard/dist/assets/{ToolbarRootContext-DTAiJvnu.js → ToolbarRootContext-CY_SULMC.js} +1 -1
  44. package/dashboard/dist/assets/{TrendMonitor-BCpOG6Vh.js → TrendMonitor-BH6Jx8nS.js} +1 -1
  45. package/dashboard/dist/assets/{WidgetsPage-Ypvi7zXN.js → WidgetsPage-D8tzK-7m.js} +1 -1
  46. package/dashboard/dist/assets/{WrappedPage-ZetHSH8Z.js → WrappedPage-BjZPa_al.js} +1 -1
  47. package/dashboard/dist/assets/__vite-browser-external-VmN0WaC3.js +45 -0
  48. package/dashboard/dist/assets/{agent-logos-BP-j-Uhx.js → agent-logos-QSoXRwwJ.js} +1 -1
  49. package/dashboard/dist/assets/{arrow-up-right-BDfZ3gqx.js → arrow-up-right-C3WUmTcI.js} +1 -1
  50. package/dashboard/dist/assets/{check-DMSqoEnk.js → check-Gz5mnj6f.js} +1 -1
  51. package/dashboard/dist/assets/{composite-BdUpN78Q.js → composite-DJhoahpv.js} +1 -1
  52. package/dashboard/dist/assets/{copy-CSp3uU1K.js → copy-C5RiL5JQ.js} +1 -1
  53. package/dashboard/dist/assets/{download-5KK5dq2E.js → download-CccMGCc1.js} +1 -1
  54. package/dashboard/dist/assets/{external-link-CetdvgSB.js → external-link-fdU7Lj6p.js} +1 -1
  55. package/dashboard/dist/assets/{flame-CkGhWtYT.js → flame-CYEjossP.js} +1 -1
  56. package/dashboard/dist/assets/{format-tokens-CnlGC8B4.js → format-tokens-xXCznLkf.js} +1 -1
  57. package/dashboard/dist/assets/{icons-BwUwzIE1.js → icons-CYNYszil.js} +1 -1
  58. package/dashboard/dist/assets/{index-BLiO8Wso.js → index-CQ5GHXiW.js} +1 -1
  59. package/dashboard/dist/assets/{index-D_-EkG8M.js → index-Cgfb050k.js} +1 -1
  60. package/dashboard/dist/assets/{index-DQV-HPM-.js → index-Czz7x_MZ.js} +1 -1
  61. package/dashboard/dist/assets/{info-KSAnlMrp.js → info-Zae50fSC.js} +1 -1
  62. package/dashboard/dist/assets/{link-2-BGXe__2Q.js → link-2-DW_mHLzI.js} +1 -1
  63. package/dashboard/dist/assets/{loader-circle-D7ZxwMME.js → loader-circle-Bl7dBXh1.js} +1 -1
  64. package/dashboard/dist/assets/{maximize-2-D80lpJ50.js → maximize-2-DTP25Fl_.js} +1 -1
  65. package/dashboard/dist/assets/{modifiers.esm-DStyqxn4.js → modifiers.esm-CYXpOF3i.js} +1 -1
  66. package/dashboard/dist/assets/{monitor-smartphone-D282iI2t.js → monitor-smartphone--TLNLuLe.js} +1 -1
  67. package/dashboard/dist/assets/{plus-Cn55gGq1.js → plus-DgeFv3Xc.js} +1 -1
  68. package/dashboard/dist/assets/{provider-display-DvH8Y5lo.js → provider-display-crAVn3qa.js} +1 -1
  69. package/dashboard/dist/assets/{react-B-73Uvu6.js → react-QPulAuJP.js} +1 -1
  70. package/dashboard/dist/assets/{search-DpOq2L2S.js → search-DIUmGfar.js} +1 -1
  71. package/dashboard/dist/assets/{skills-api-CCIV2ZIq.js → skills-api-09qiTRNb.js} +1 -1
  72. package/dashboard/dist/assets/{sortable.esm-BjIamCRg.js → sortable.esm-BbsbmOc2.js} +1 -1
  73. package/dashboard/dist/assets/{store-CmFrvUue.js → store-DdyzwTsy.js} +1 -1
  74. package/dashboard/dist/assets/{terminal-CqeGAXgs.js → terminal-JpDnkjVA.js} +1 -1
  75. package/dashboard/dist/assets/{trash-CMLW5F5L.js → trash-CRsyysiP.js} +1 -1
  76. package/dashboard/dist/assets/{use-community-stats-BKS7Gd-z.js → use-community-stats-D3PwUyvR.js} +1 -1
  77. package/dashboard/dist/assets/{use-latest-request-guard-I80QDt_F.js → use-latest-request-guard-DTCniVRw.js} +1 -1
  78. package/dashboard/dist/assets/{use-limits-display-prefs-CPLY7mBi.js → use-limits-display-prefs-BbYcvoh_.js} +1 -1
  79. package/dashboard/dist/assets/{use-ordered-list-wlw260-2.js → use-ordered-list-ypqFhcs4.js} +1 -1
  80. package/dashboard/dist/assets/use-reduced-motion-Cdn1VrG0.js +1 -0
  81. package/dashboard/dist/assets/{use-session-efficiency-pref-BdVVDyew.js → use-session-efficiency-pref-C5BZmpkY.js} +1 -1
  82. package/dashboard/dist/assets/{use-transform-Daiv5LsX.js → use-transform-C3bGaC_U.js} +1 -1
  83. package/dashboard/dist/assets/{use-usage-limits-CK1zK4GT.js → use-usage-limits-BRLNaZEv.js} +1 -1
  84. package/dashboard/dist/assets/{useCurrency-yxDqAL1Q.js → useCurrency-BianRgdW.js} +1 -1
  85. package/dashboard/dist/assets/{useTokenFormat-BTYDgiyJ.js → useTokenFormat-DO4vxqhW.js} +1 -1
  86. package/dashboard/dist/assets/{zap-CfNiBLFJ.js → zap-BJTChg1t.js} +1 -1
  87. package/dashboard/dist/index.html +1 -1
  88. package/dashboard/dist/ip-check.html +1 -1
  89. package/dashboard/dist/leaderboard.html +1 -1
  90. package/dashboard/dist/share.html +1 -1
  91. package/package.json +1 -1
  92. package/src/commands/init.js +1 -1
  93. package/src/commands/sync.js +28 -6
  94. package/src/lib/cloud-account.js +23 -12
  95. package/src/lib/cloud-device-token.js +74 -0
  96. package/src/lib/cloud-sync-prefs.js +14 -0
  97. package/src/lib/local-api.js +104 -46
  98. package/src/lib/pricing/seed-snapshot.json +1 -1
  99. package/src/lib/upload-throttle.js +4 -1
  100. package/dashboard/dist/assets/DevicePage-CghTfQLN.js +0 -1
  101. package/dashboard/dist/assets/DialogDescription-Ln30UAVH.js +0 -1
  102. package/dashboard/dist/assets/__vite-browser-external-C_FI6biS.js +0 -45
  103. package/dashboard/dist/assets/use-reduced-motion-ClrSsBXW.js +0 -1
@@ -13,6 +13,7 @@ const {
13
13
  const { accountSlugFor, fetchAccountUsage, mintAccessToken, invalidateCloudAccountPayloadCache, getCloudAccountSessionGeneration } = require("./cloud-account");
14
14
  const { getOrCreateMachineId, computeStableMachineId } = require("./machine-id");
15
15
  const { functionUrlFor, fetchFunctionResponse } = require("./function-url");
16
+ const { createCloudDeviceTokenStore } = require("./cloud-device-token");
16
17
 
17
18
  const SYNC_TIMEOUT_MS = 120_000;
18
19
  // A failed account-view request should not stall every dashboard refresh
@@ -896,7 +897,14 @@ function runSyncCommand(extraEnv = {}, opts = {}) {
896
897
  if (opts.drain === true) args.push("--drain");
897
898
  if (opts.waitForLock === true) args.push("--wait-for-lock");
898
899
  const child = spawn(process.execPath, args, {
899
- env: { ...process.env, ...extraEnv },
900
+ env: {
901
+ ...process.env, ...extraEnv,
902
+ ...(extraEnv.TOKENTRACKER_DEVICE_TOKEN ? {
903
+ TOKENTRACKER_INSFORGE_BASE_URL: extraEnv.TOKENTRACKER_INSFORGE_BASE_URL || resolveRuntimeConfig().baseUrl,
904
+ TOKENTRACKER_INSFORGE_ANON_KEY: resolveRuntimeConfig().anonKey,
905
+ } : {}),
906
+ TOKENTRACKER_LOCAL_SYNC_DEVICE_TOKEN: extraEnv.TOKENTRACKER_DEVICE_TOKEN || "",
907
+ },
900
908
  stdio: ["ignore", "pipe", "pipe"],
901
909
  });
902
910
  let stdout = "";
@@ -1144,19 +1152,29 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
1144
1152
  const localAuthToken = crypto.randomBytes(24).toString("hex");
1145
1153
  const trackerDataDir = path.join(os.homedir(), ".tokentracker", "tracker");
1146
1154
  const cookiePath = path.join(trackerDataDir, "relay-cookies.json");
1155
+ const localSyncDeviceTokenStore = createCloudDeviceTokenStore(path.join(trackerDataDir, "cloud-device-token.json"));
1147
1156
  const localSyncDeviceTokenCache = new Map();
1148
1157
  const localSyncDeviceTokenInflight = new Map();
1149
1158
  let localSyncDeviceTokenUserId = "";
1150
1159
  function clearLocalSyncDeviceTokenCache() {
1151
1160
  localSyncDeviceTokenCache.clear();
1152
1161
  localSyncDeviceTokenInflight.clear();
1162
+ localSyncDeviceTokenStore.clear();
1153
1163
  localSyncDeviceTokenUserId = "";
1154
1164
  }
1155
- function currentCloudUploadFailure(syncStartedAt, { backoff = false } = {}) {
1165
+ function invalidateLocalSyncDeviceToken(token) {
1166
+ if (!token) return;
1167
+ for (const [key, value] of localSyncDeviceTokenCache) {
1168
+ if (value === token) localSyncDeviceTokenCache.delete(key);
1169
+ }
1170
+ localSyncDeviceTokenStore.invalidate(token);
1171
+ }
1172
+ function currentCloudUploadFailure(syncStartedAt, { backoff = false, deviceToken, attemptId } = {}) {
1156
1173
  try {
1157
1174
  const state = JSON.parse(fs.readFileSync(path.join(path.dirname(qp), "upload.throttle.json"), "utf8"));
1158
- if (Date.parse(state.lastErrorAt || "") >= syncStartedAt && Number(state.lastErrorStatus) > 0) {
1159
- if (state.lastErrorCode === "CLOUD_DEVICE_TOKEN_REJECTED") clearLocalSyncDeviceTokenCache();
1175
+ if (attemptId && state.lastErrorAttemptId === attemptId &&
1176
+ Date.parse(state.lastErrorAt || "") >= syncStartedAt && Number(state.lastErrorStatus) > 0) {
1177
+ if (state.lastErrorCode === "CLOUD_DEVICE_TOKEN_REJECTED") invalidateLocalSyncDeviceToken(deviceToken);
1160
1178
  return { code: state.lastErrorCode || "CLOUD_UPLOAD_FAILED", status: Number(state.lastErrorStatus) };
1161
1179
  }
1162
1180
  if (backoff && Number(state.backoffUntilMs || 0) > syncStartedAt) {
@@ -1170,7 +1188,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
1170
1188
  // request for one account must not clear another account's failure), and
1171
1189
  // suppress the duplicate fan-out while the first cloud probe is pending.
1172
1190
  const accountViewFailures = new Map();
1173
- const accountViewInFlight = new Set();
1191
+ const accountViewInFlight = new Map();
1174
1192
  let accountUploadState = readAccountUploadState();
1175
1193
 
1176
1194
  function readAccountUploadState() {
@@ -1339,23 +1357,25 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
1339
1357
  // sync is on. Cloud sync is a dashboard (WebView) preference persisted in
1340
1358
  // localStorage; the dashboard mirrors it here via POST
1341
1359
  // /functions/tokentracker-cloud-sync-pref so the auth-unaware popover can key
1342
- // off the same flag. Defaults ON, exactly like the dashboard toggle — every
1343
- // consumer additionally requires a relayed refresh token, so the default only
1344
- // takes effect for signed-in users; an explicit {enabled:false} still wins.
1360
+ // off the same flag. Defaults OFF, exactly like the dashboard toggle;
1361
+ // existing saved choices are preserved.
1345
1362
  const cloudSyncPrefPath = path.join(trackerDataDir, "cloud-sync-pref.json");
1346
1363
  let cloudSyncPrefCache;
1347
1364
  let cloudSyncPrefGeneration = 0;
1348
1365
  function getCloudSyncPref() {
1349
1366
  if (cloudSyncPrefCache === undefined) {
1350
- try {
1351
- cloudSyncPrefCache = JSON.parse(fs.readFileSync(cloudSyncPrefPath, "utf8"))?.enabled !== false;
1352
- } catch {
1353
- cloudSyncPrefCache = true;
1354
- }
1367
+ cloudSyncPrefCache = require("./cloud-sync-prefs").readCloudSyncEnabled(trackerDataDir);
1355
1368
  }
1356
1369
  return cloudSyncPrefCache;
1357
1370
  }
1358
- function setCloudSyncPref(enabled) {
1371
+ function setCloudSyncPref(enabled, changedAtMs = Date.now()) {
1372
+ // Multiple dashboard windows can deliver mirrors out of order. Keep the
1373
+ // newest saved choice; old files without an ordering marker remain valid.
1374
+ try {
1375
+ const saved = JSON.parse(fs.readFileSync(cloudSyncPrefPath, "utf8"));
1376
+ if (Number(saved?.changedAtMs || 0) > changedAtMs ||
1377
+ (changedAtMs > 0 && saved?.changedAtMs === changedAtMs && saved?.enabled === false && enabled)) return;
1378
+ } catch { /* first choice or an unreadable preference */ }
1359
1379
  if (getCloudSyncPref() !== Boolean(enabled)) {
1360
1380
  cloudSyncPrefGeneration += 1;
1361
1381
  clearLocalSyncDeviceTokenCache();
@@ -1366,7 +1386,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
1366
1386
  if (!fs.existsSync(trackerDataDir)) fs.mkdirSync(trackerDataDir, { recursive: true });
1367
1387
  fs.writeFileSync(
1368
1388
  cloudSyncPrefPath,
1369
- JSON.stringify({ enabled: cloudSyncPrefCache, updatedAt: new Date().toISOString() }),
1389
+ JSON.stringify({ enabled: cloudSyncPrefCache, changedAtMs, updatedAt: new Date().toISOString() }),
1370
1390
  { encoding: "utf8", mode: 0o600 },
1371
1391
  );
1372
1392
  } catch (e) {
@@ -1417,6 +1437,10 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
1417
1437
  anonKey: runtime.anonKey,
1418
1438
  refreshToken,
1419
1439
  timeoutMs: runtime.httpTimeoutMs,
1440
+ throwOnFailure: true,
1441
+ }).catch((error) => {
1442
+ if (error.status === 401 && sessionAtStart === getCloudAccountSessionGeneration()) clearLocalSyncDeviceTokenCache();
1443
+ throw error;
1420
1444
  });
1421
1445
  if (!minted?.accessToken || sessionAtStart !== getCloudAccountSessionGeneration()) return null;
1422
1446
  const rotatedRefreshToken =
@@ -1439,8 +1463,14 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
1439
1463
  if (userId) localSyncDeviceTokenUserId = userId;
1440
1464
  const identity = userId || `refresh:${rotatedRefreshToken || refreshToken}`;
1441
1465
  const cacheKey = localSyncDeviceTokenCacheKey(identity, machineId, baseUrl);
1442
- const cachedToken = localSyncDeviceTokenCache.get(cacheKey);
1443
- if (cachedToken) return cachedToken;
1466
+ const binding = { userId, baseUrl, machineId };
1467
+ const cachedToken = localSyncDeviceTokenCache.get(cacheKey) ||
1468
+ (userId ? localSyncDeviceTokenStore.get(binding) : null);
1469
+ if (cachedToken) {
1470
+ localSyncDeviceTokenCache.set(cacheKey, cachedToken);
1471
+ if (userId) localSyncDeviceTokenStore.set(binding, cachedToken);
1472
+ return cachedToken;
1473
+ }
1444
1474
  const inflightToken = localSyncDeviceTokenInflight.get(cacheKey);
1445
1475
  if (inflightToken?.sessionGeneration === sessionAtStart) return inflightToken.promise;
1446
1476
 
@@ -1482,13 +1512,19 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
1482
1512
  });
1483
1513
  if (sessionAtStart !== getCloudAccountSessionGeneration() || !getCloudSyncPref() || cloudPrefAtStart !== cloudSyncPrefGeneration) return null;
1484
1514
  if (!res.ok) {
1485
- if (res.status === 401) clearLocalSyncDeviceTokenCache();
1515
+ if (res.status === 401 && sessionAtStart === getCloudAccountSessionGeneration()) {
1516
+ localSyncDeviceTokenCache.delete(cacheKey);
1517
+ localSyncDeviceTokenStore.clear(binding);
1518
+ }
1486
1519
  return null;
1487
1520
  }
1488
1521
  const data = await res.json().catch(() => null);
1489
1522
  const token = typeof data?.token === "string" ? data.token.trim() : "";
1490
1523
  if (sessionAtStart !== getCloudAccountSessionGeneration() || !getCloudSyncPref() || cloudPrefAtStart !== cloudSyncPrefGeneration) return null;
1491
- if (token) localSyncDeviceTokenCache.set(cacheKey, token);
1524
+ if (token) {
1525
+ localSyncDeviceTokenCache.set(cacheKey, token);
1526
+ if (userId) localSyncDeviceTokenStore.set(binding, token);
1527
+ }
1492
1528
  return token || null;
1493
1529
  })();
1494
1530
  localSyncDeviceTokenInflight.set(cacheKey, { promise: issuePromise, sessionGeneration: sessionAtStart });
@@ -1535,32 +1571,46 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
1535
1571
  invalidateAfterAccountUpload();
1536
1572
  const runtime = resolveRuntimeConfig();
1537
1573
  const failureKey = `${runtime.baseUrl}\0${refreshToken}`;
1538
- const requestKey = `${failureKey}\0${usageSlug}\0${url.searchParams.toString()}`;
1574
+ const sessionAtStart = getCloudAccountSessionGeneration();
1575
+ const cloudPrefAtStart = cloudSyncPrefGeneration;
1576
+ const params = new URLSearchParams(url.searchParams);
1577
+ params.sort();
1578
+ const requestKey = `${failureKey}\0${sessionAtStart}\0${cloudPrefAtStart}\0${accountUploadState}\0${usageSlug}\0${params}`;
1539
1579
  if (url.searchParams.get("refresh") !== "1" && accountViewFailureUntil(failureKey) > Date.now()) {
1540
1580
  return "transient-backoff";
1541
1581
  }
1542
- // Six native dashboard requests normally arrive together. If the cloud is
1543
- // already being probed for this session, let the remaining requests use
1544
- // their local fallback instead of opening more timeout-bound sockets.
1545
- if (accountViewInFlight.has(requestKey)) {
1546
- return "transient-inflight";
1547
- }
1548
- accountViewInFlight.add(requestKey);
1549
1582
  const envTimeout = process.env.TOKENTRACKER_HTTP_TIMEOUT_MS
1550
1583
  ? parseInt(process.env.TOKENTRACKER_HTTP_TIMEOUT_MS, 10)
1551
1584
  : 0;
1552
1585
  const timeoutMs = envTimeout > 0 ? envTimeout : 4000;
1553
- const sessionAtStart = getCloudAccountSessionGeneration();
1554
- const cloudPrefAtStart = cloudSyncPrefGeneration;
1555
1586
  try {
1556
- const out = await fetchAccountUsage({
1557
- usageSlug,
1558
- searchParams: url.searchParams,
1559
- baseUrl: runtime.baseUrl || DEFAULT_BASE_URL,
1560
- anonKey: runtime.anonKey,
1561
- refreshToken,
1562
- timeoutMs,
1563
- });
1587
+ // An identical overlapping read shares the entire probe, including its
1588
+ // token mint. A healthy pending read must not look like a cloud outage
1589
+ // and trigger the native client's account recovery loop.
1590
+ let pending = accountViewInFlight.get(requestKey);
1591
+ if (!pending) {
1592
+ pending = fetchAccountUsage({
1593
+ usageSlug,
1594
+ searchParams: url.searchParams,
1595
+ baseUrl: runtime.baseUrl || DEFAULT_BASE_URL,
1596
+ anonKey: runtime.anonKey,
1597
+ refreshToken,
1598
+ timeoutMs,
1599
+ onSessionRefreshed: (session) => {
1600
+ if (sessionAtStart !== getCloudAccountSessionGeneration()) {
1601
+ throw Object.assign(new Error("Account session changed"), { code: "auth_session_changed" });
1602
+ }
1603
+ if (session.refreshToken) setRelayRefreshToken(session.refreshToken);
1604
+ if (session.csrfToken) setRelayCsrfToken(session.csrfToken);
1605
+ },
1606
+ });
1607
+ accountViewInFlight.set(requestKey, pending);
1608
+ const cleanup = () => {
1609
+ if (accountViewInFlight.get(requestKey) === pending) accountViewInFlight.delete(requestKey);
1610
+ };
1611
+ pending.then(cleanup, cleanup);
1612
+ }
1613
+ const out = await pending;
1564
1614
  // `null` here means the slug has no cloud equivalent, or the session
1565
1615
  // vanished mid-request — a routing/session fact, not an outage.
1566
1616
  if (!out) return ACCOUNT_FALLBACK_SIGNED_OUT;
@@ -1571,8 +1621,6 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
1571
1621
  rememberAccountViewFailure(failureKey);
1572
1622
  return "transient-upstream";
1573
1623
  }
1574
- if (out.rotatedRefreshToken) setRelayRefreshToken(out.rotatedRefreshToken);
1575
- if (out.rotatedCsrfToken) setRelayCsrfToken(out.rotatedCsrfToken);
1576
1624
  if (!getCloudSyncPref()) return ACCOUNT_FALLBACK_CLOUD_SYNC_OFF;
1577
1625
  if (cloudPrefAtStart !== cloudSyncPrefGeneration) {
1578
1626
  json(res, { error: "Cloud sync preference changed", code: "CLOUD_SYNC_CHANGED" }, 409);
@@ -1587,8 +1635,15 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
1587
1635
  res.end(JSON.stringify(out.data));
1588
1636
  return "served";
1589
1637
  } catch (e) {
1590
- if (e.code === "auth_session_changed") {
1591
- json(res, { error: "Account session changed", code: e.code }, 409);
1638
+ const currentSession = getCloudAccountSessionGeneration();
1639
+ if (e.code === "auth_session_changed" ||
1640
+ (sessionAtStart !== currentSession && e.invalidatedSessionGeneration !== currentSession)) {
1641
+ json(res, { error: "Account session changed", code: "auth_session_changed" }, 409);
1642
+ return "served";
1643
+ }
1644
+ if (!getCloudSyncPref()) return ACCOUNT_FALLBACK_CLOUD_SYNC_OFF;
1645
+ if (cloudPrefAtStart !== cloudSyncPrefGeneration) {
1646
+ json(res, { error: "Cloud sync preference changed", code: "CLOUD_SYNC_CHANGED" }, 409);
1592
1647
  return "served";
1593
1648
  }
1594
1649
  // Signed in + cloud sync on, but the cloud read failed (offline, token
@@ -1601,8 +1656,6 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
1601
1656
  }
1602
1657
  rememberAccountViewFailure(failureKey);
1603
1658
  return reason;
1604
- } finally {
1605
- accountViewInFlight.delete(requestKey);
1606
1659
  }
1607
1660
  }
1608
1661
 
@@ -2162,6 +2215,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
2162
2215
  }
2163
2216
  }
2164
2217
  const syncStartedAt = Date.now();
2218
+ extraEnv.TOKENTRACKER_LOCAL_SYNC_ATTEMPT_ID = crypto.randomUUID();
2165
2219
  const result = await runSyncCommand(extraEnv, {
2166
2220
  drain,
2167
2221
  auto,
@@ -2171,11 +2225,11 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
2171
2225
  waitForLock:
2172
2226
  !drain && !background && Boolean(extraEnv.TOKENTRACKER_DEVICE_TOKEN),
2173
2227
  }).catch((error) => {
2174
- const failure = currentCloudUploadFailure(syncStartedAt, { backoff: auto && drain });
2228
+ const failure = currentCloudUploadFailure(syncStartedAt, { backoff: auto && drain, deviceToken: extraEnv.TOKENTRACKER_DEVICE_TOKEN, attemptId: extraEnv.TOKENTRACKER_LOCAL_SYNC_ATTEMPT_ID });
2175
2229
  if (failure) Object.assign(error, failure);
2176
2230
  throw error;
2177
2231
  });
2178
- const failure = currentCloudUploadFailure(syncStartedAt);
2232
+ const failure = currentCloudUploadFailure(syncStartedAt, { deviceToken: extraEnv.TOKENTRACKER_DEVICE_TOKEN, attemptId: extraEnv.TOKENTRACKER_LOCAL_SYNC_ATTEMPT_ID });
2179
2233
  if (failure) throw Object.assign(new Error("Cloud upload failed"), failure);
2180
2234
  invalidateAfterAccountUpload();
2181
2235
  if (drain) invalidateCloudAccountPayloadCache();
@@ -2823,7 +2877,11 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
2823
2877
  json(res, { ok: false, error: "enabled must be a boolean" }, 400);
2824
2878
  return true;
2825
2879
  }
2826
- setCloudSyncPref(body.enabled);
2880
+ if (body.changedAtMs != null && (!Number.isSafeInteger(body.changedAtMs) || body.changedAtMs < 0)) {
2881
+ json(res, { ok: false, error: "changedAtMs must be a nonnegative safe integer" }, 400);
2882
+ return true;
2883
+ }
2884
+ setCloudSyncPref(body.enabled, body.changedAtMs ?? Date.now());
2827
2885
  json(res, { ok: true, enabled: getCloudSyncPref() });
2828
2886
  return true;
2829
2887
  }