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.
- package/dashboard/dist/assets/{AchievementBadge-BeNHapqp.js → AchievementBadge-DuEuFaXg.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-vNH31cFv.js → AchievementsPage-BTTFtk_Z.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-ChGSmCUP.js → AchievementsSection-BZppKteZ.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-CN0tcpEE.js → BadgeDetailModal-TzmxXy8k.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CKKYEJt-.js → BotAnimated-4hWTFjwp.js} +1 -1
- package/dashboard/dist/assets/{Card-Dx3Pj7SE.js → Card-HigkIm_b.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-ChJEskg4.js → ClawdAnimated-BRoihVSd.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-D4-sgyxP.js → CommandPalette-Bx_11gJk.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-NUDCSEve.js → CommunityStatsModal-BjDPCBol.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-q4EXjOda.js → ConfirmModal-DTo93RpG.js} +1 -1
- package/dashboard/dist/assets/{Controls-OjbHepPz.js → Controls-BvWTXyfI.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-F3swcQnu.js → DashboardPage--1_GuEr-.js} +1 -1
- package/dashboard/dist/assets/DevicePage-BVupGt9K.js +1 -0
- package/dashboard/dist/assets/{DialogClose-DSp5s5nL.js → DialogClose-BlQJpi7n.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DE2BF-HX.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-M5ojVj4-.js → DialogTitle-CDmxp9ap.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CWDRIc_C.js → FadeIn-DPercU5n.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DR4Px_eh.js → HeaderGithubStar-DeVzeBIo.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CsfynzUO.js → HoverTooltip-Duo2Icus.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-C8Y1OFOn.js → IpCheckPage-DgYiaRrT.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BnmP1NUm.js → LandingPage-CYTR4PbQ.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-DprHPy4v.js → LeaderboardAvatar-B67pMaIl.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BI-ryUMj.js → LeaderboardPage-2wIMkBgq.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-r-wgLtCo.js → LeaderboardProfileModal-D3VxkFlx.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-qBvdPj6z.js → LeaderboardProfilePage-BS01jN5M.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage--ObDuhSD.js → LimitsPage-BGRZW-aH.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-Cvg81MIm.js → LocalOnlyNotice-Do9KUdi_.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-T1vufQym.js → LoginCard-CzAXLXfZ.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-CElUuzhg.js → LoginModal-D1J8zG0C.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-Decz8Qyn.js → LoginPage-DeZJNMLe.js} +1 -1
- package/dashboard/dist/assets/{PetPage-CitsUTsW.js → PetPage-D2oqJ05F.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-wx5xna8c.js → PopoverPopup-BYzzQJTD.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-DeCQ9ytY.js → ProviderIcon-BfFkS-OF.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BklHQ_i8.js → ResetPasswordPage-BFyYvG24.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-DqbXKZfk.js → SegmentedControl-D5USR3JT.js} +1 -1
- package/dashboard/dist/assets/{Select-BPhSzg5T.js → Select-DmFQdVA1.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-BEqonLIf.js → SelectItemText-B5MXGLN-.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-DnAv3J6d.js → ServiceStatusPage-Dvm5eUJK.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-Fr5rFC4h.js → SessionsPage-DQKezzUW.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-DYB-pyw2.js → SettingsPage-DbUDye5r.js} +2 -2
- package/dashboard/dist/assets/{SkillsPage-B9aU8WEc.js → SkillsPage-DyX_-dcr.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-ybV8JSOT.js → TokenGalaxy-aK3jhWW0.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-DTAiJvnu.js → ToolbarRootContext-CY_SULMC.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-BCpOG6Vh.js → TrendMonitor-BH6Jx8nS.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-Ypvi7zXN.js → WidgetsPage-D8tzK-7m.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-ZetHSH8Z.js → WrappedPage-BjZPa_al.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-VmN0WaC3.js +45 -0
- package/dashboard/dist/assets/{agent-logos-BP-j-Uhx.js → agent-logos-QSoXRwwJ.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BDfZ3gqx.js → arrow-up-right-C3WUmTcI.js} +1 -1
- package/dashboard/dist/assets/{check-DMSqoEnk.js → check-Gz5mnj6f.js} +1 -1
- package/dashboard/dist/assets/{composite-BdUpN78Q.js → composite-DJhoahpv.js} +1 -1
- package/dashboard/dist/assets/{copy-CSp3uU1K.js → copy-C5RiL5JQ.js} +1 -1
- package/dashboard/dist/assets/{download-5KK5dq2E.js → download-CccMGCc1.js} +1 -1
- package/dashboard/dist/assets/{external-link-CetdvgSB.js → external-link-fdU7Lj6p.js} +1 -1
- package/dashboard/dist/assets/{flame-CkGhWtYT.js → flame-CYEjossP.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CnlGC8B4.js → format-tokens-xXCznLkf.js} +1 -1
- package/dashboard/dist/assets/{icons-BwUwzIE1.js → icons-CYNYszil.js} +1 -1
- package/dashboard/dist/assets/{index-BLiO8Wso.js → index-CQ5GHXiW.js} +1 -1
- package/dashboard/dist/assets/{index-D_-EkG8M.js → index-Cgfb050k.js} +1 -1
- package/dashboard/dist/assets/{index-DQV-HPM-.js → index-Czz7x_MZ.js} +1 -1
- package/dashboard/dist/assets/{info-KSAnlMrp.js → info-Zae50fSC.js} +1 -1
- package/dashboard/dist/assets/{link-2-BGXe__2Q.js → link-2-DW_mHLzI.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-D7ZxwMME.js → loader-circle-Bl7dBXh1.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-D80lpJ50.js → maximize-2-DTP25Fl_.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-DStyqxn4.js → modifiers.esm-CYXpOF3i.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-D282iI2t.js → monitor-smartphone--TLNLuLe.js} +1 -1
- package/dashboard/dist/assets/{plus-Cn55gGq1.js → plus-DgeFv3Xc.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DvH8Y5lo.js → provider-display-crAVn3qa.js} +1 -1
- package/dashboard/dist/assets/{react-B-73Uvu6.js → react-QPulAuJP.js} +1 -1
- package/dashboard/dist/assets/{search-DpOq2L2S.js → search-DIUmGfar.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CCIV2ZIq.js → skills-api-09qiTRNb.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-BjIamCRg.js → sortable.esm-BbsbmOc2.js} +1 -1
- package/dashboard/dist/assets/{store-CmFrvUue.js → store-DdyzwTsy.js} +1 -1
- package/dashboard/dist/assets/{terminal-CqeGAXgs.js → terminal-JpDnkjVA.js} +1 -1
- package/dashboard/dist/assets/{trash-CMLW5F5L.js → trash-CRsyysiP.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-BKS7Gd-z.js → use-community-stats-D3PwUyvR.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-I80QDt_F.js → use-latest-request-guard-DTCniVRw.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-CPLY7mBi.js → use-limits-display-prefs-BbYcvoh_.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-wlw260-2.js → use-ordered-list-ypqFhcs4.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-Cdn1VrG0.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-BdVVDyew.js → use-session-efficiency-pref-C5BZmpkY.js} +1 -1
- package/dashboard/dist/assets/{use-transform-Daiv5LsX.js → use-transform-C3bGaC_U.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-CK1zK4GT.js → use-usage-limits-BRLNaZEv.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-yxDqAL1Q.js → useCurrency-BianRgdW.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-BTYDgiyJ.js → useTokenFormat-DO4vxqhW.js} +1 -1
- package/dashboard/dist/assets/{zap-CfNiBLFJ.js → zap-BJTChg1t.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/init.js +1 -1
- package/src/commands/sync.js +28 -6
- package/src/lib/cloud-account.js +23 -12
- package/src/lib/cloud-device-token.js +74 -0
- package/src/lib/cloud-sync-prefs.js +14 -0
- package/src/lib/local-api.js +104 -46
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/upload-throttle.js +4 -1
- package/dashboard/dist/assets/DevicePage-CghTfQLN.js +0 -1
- package/dashboard/dist/assets/DialogDescription-Ln30UAVH.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-C_FI6biS.js +0 -45
- package/dashboard/dist/assets/use-reduced-motion-ClrSsBXW.js +0 -1
package/src/lib/local-api.js
CHANGED
|
@@ -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: {
|
|
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
|
|
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 (
|
|
1159
|
-
|
|
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
|
|
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
|
|
1343
|
-
//
|
|
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
|
-
|
|
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
|
|
1443
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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
|
-
|
|
1591
|
-
|
|
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
|
-
|
|
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
|
}
|