tokentracker-cli 1.2.2 → 1.3.3
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/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/dashboard/dist/assets/AchievementBadge-BaqdyZaV.js +1 -0
- package/dashboard/dist/assets/AchievementsPage-B4Ju0guv.js +2 -0
- package/dashboard/dist/assets/{AchievementsSection-BDqKRGyL.js → AchievementsSection-CDUUEfIi.js} +1 -1
- package/dashboard/dist/assets/BadgeDetailModal-B-XMJAvL.js +1 -0
- package/dashboard/dist/assets/{BotAnimated-OQbsVw1O.js → BotAnimated-Dx_vvTtC.js} +1 -1
- package/dashboard/dist/assets/Card-D-5mABVt.js +1 -0
- package/dashboard/dist/assets/CloudCheckoutPage-BELoh56h.js +1 -0
- package/dashboard/dist/assets/CloudPage-CfBruNxe.js +1 -0
- package/dashboard/dist/assets/CloudUsageExport-BoFU7xCT.js +4 -0
- package/dashboard/dist/assets/CommandPalette-BmmKZWn-.js +1 -0
- package/dashboard/dist/assets/CommunityStatsModal-BF77zVhN.js +1 -0
- package/dashboard/dist/assets/{ConfirmModal-Dhu_qmiM.js → ConfirmModal-BQ-xB1S_.js} +1 -1
- package/dashboard/dist/assets/Controls-BnTeX7T4.js +1 -0
- package/dashboard/dist/assets/DashboardPage-BWulHa5J.js +1 -0
- package/dashboard/dist/assets/{DevicePage-Bvz7JJwt.js → DevicePage-Dg6XkIaY.js} +1 -1
- package/dashboard/dist/assets/DialogClose-DAyilsH-.js +1 -0
- package/dashboard/dist/assets/DialogDescription-j4XnRy6y.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-C8vpXqg8.js → DialogTitle-B1iNGD2S.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-DDF5PqTX.js → FadeIn-ChVf45WW.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-CNzQFPol.js → HeaderGithubStar-B740_G7a.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BKg9Z6gg.js → HoverTooltip-BzP5FudR.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BTdrLt08.js → IpCheckPage-BuZH3F14.js} +1 -1
- package/dashboard/dist/assets/LandingPage-Dtgq_caz.js +2 -0
- package/dashboard/dist/assets/LeaderboardPage-Di1kEg2O.js +2 -0
- package/dashboard/dist/assets/LeaderboardProBadge-h8FfR0au.js +1 -0
- package/dashboard/dist/assets/LeaderboardProfileModal-OUZvO6u-.js +73 -0
- package/dashboard/dist/assets/{LeaderboardProfilePage-CoqcDSTw.js → LeaderboardProfilePage-bHI1oTEK.js} +2 -2
- package/dashboard/dist/assets/{LimitsPage-3RuSFnn-.js → LimitsPage-DKojB73_.js} +2 -2
- package/dashboard/dist/assets/{LocalOnlyNotice-D-OrIXty.js → LocalOnlyNotice-BaEq6qN7.js} +1 -1
- package/dashboard/dist/assets/LoginCard-Dn5Xy4If.js +1 -0
- package/dashboard/dist/assets/LoginModal-C5h-5GOA.js +1 -0
- package/dashboard/dist/assets/LoginPage-BpX8Tgjj.js +1 -0
- package/dashboard/dist/assets/PetPage-BPBsUHyL.js +1 -0
- package/dashboard/dist/assets/PopoverPopup-CVTiEVHs.js +1 -0
- package/dashboard/dist/assets/{ProviderIcon-BTYVbUti.js → ProviderIcon-BshO2uER.js} +1 -1
- package/dashboard/dist/assets/ResetPasswordPage-RTFTD8Dr.js +1 -0
- package/dashboard/dist/assets/{Select-D8N2TJz1.js → Select-CtTceC4T.js} +1 -1
- package/dashboard/dist/assets/SelectItemText-C7xi8iGp.js +1 -0
- package/dashboard/dist/assets/SelfHostPage-BlwTt8XM.js +1 -0
- package/dashboard/dist/assets/{ServiceStatusPage-Cb3k_C4g.js → ServiceStatusPage-BkIcJpYK.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-BtH5iLbi.js → SessionsPage-okHobk_L.js} +3 -3
- package/dashboard/dist/assets/SettingsPage-C-cc6wcF.js +2 -0
- package/dashboard/dist/assets/SkillsPage-D2LlKKJN.js +1 -0
- package/dashboard/dist/assets/{TokenGalaxy-BGRF4I_8.js → TokenGalaxy-DiXUpXKF.js} +2 -2
- package/dashboard/dist/assets/TrendMonitor-C88S-5aa.js +19 -0
- package/dashboard/dist/assets/WidgetsPage-WsEFPNwp.js +1 -0
- package/dashboard/dist/assets/{WrappedPage-BqyB7ezg.js → WrappedPage-ClEo-RmR.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-DFRX9dp-.css +1 -0
- package/dashboard/dist/assets/__vite-browser-external-DUJeOWgd.js +45 -0
- package/dashboard/dist/assets/agent-logos-BM9jlReI.js +1 -0
- package/dashboard/dist/assets/arrow-left-BEM-HVmC.js +1 -0
- package/dashboard/dist/assets/arrow-right-CwBz5dwb.js +1 -0
- package/dashboard/dist/assets/{arrow-up-right-CdzAw406.js → arrow-up-right-jPmRN562.js} +1 -1
- package/dashboard/dist/assets/{check-DhEiHlwq.js → check-CKwHc4Hv.js} +1 -1
- package/dashboard/dist/assets/cloud-checkout-nYNHDFoR.js +1 -0
- package/dashboard/dist/assets/composite-B4Z3F65o.js +1 -0
- package/dashboard/dist/assets/{download-kGPw24XQ.js → download-CJUnX1Bs.js} +1 -1
- package/dashboard/dist/assets/{external-link-SOFbVRrH.js → external-link-B5yXxdPy.js} +1 -1
- package/dashboard/dist/assets/{flame-BRdc7HDz.js → flame-CKjAluj_.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DhIeweGK.js → format-tokens-mk49THtv.js} +1 -1
- package/dashboard/dist/assets/{host-mode-APftzzvz.js → host-mode-efKlSYjX.js} +1 -1
- package/dashboard/dist/assets/{icons-D-wu8eUg.js → icons-ZB1ZthjH.js} +1 -1
- package/dashboard/dist/assets/{index-D2g1tPGl.js → index-BImBG3eS.js} +1 -1
- package/dashboard/dist/assets/{index-CiWf_0mc.js → index-BgdM7kBl.js} +1 -1
- package/dashboard/dist/assets/{index-CZ3D1Q7c.js → index-DweUMAUD.js} +1 -1
- package/dashboard/dist/assets/{info-Qgc5VctK.js → info-CyDY7NYM.js} +1 -1
- package/dashboard/dist/assets/{link-2-Cg6bEvjQ.js → link-2-Dn2soiyd.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-Cd7h4LcE.js → loader-circle-CNT630zn.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-B7hOvflM.js → modifiers.esm-WPx0bc2K.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-wFv5VQQf.js → monitor-smartphone-C-qC0RAt.js} +1 -1
- package/dashboard/dist/assets/os-DMmhX9tk.js +1 -0
- package/dashboard/dist/assets/{plus-0xHQei42.js → plus-Cx82jb7o.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DNqVkqfp.js → provider-display-CodMmiRb.js} +2 -2
- package/dashboard/dist/assets/{react-CfB5zBqR.js → react-B7SkZTgg.js} +1 -1
- package/dashboard/dist/assets/safePolygon-CKpREa4K.js +1 -0
- package/dashboard/dist/assets/{search-DMtc6bBy.js → search-Ol3OZBm4.js} +1 -1
- package/dashboard/dist/assets/server-DFCbznC7.js +1 -0
- package/dashboard/dist/assets/shield-check-YcmLwVZ6.js +1 -0
- package/dashboard/dist/assets/skills-api-C-9vK884.js +1 -0
- package/dashboard/dist/assets/{sortable.esm-O7ntEYCi.js → sortable.esm-CKC9NuR-.js} +1 -1
- package/dashboard/dist/assets/store-C9yWYHfg.js +1 -0
- package/dashboard/dist/assets/{terminal-Y3qDL50U.js → terminal-B3_fCLFs.js} +1 -1
- package/dashboard/dist/assets/{trash-D_PaPrXq.js → trash-CZGfgbFa.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DGTBo_4v.js → use-community-stats-CW-qkYn7.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-BpfCferm.js → use-latest-request-guard-DHv7vfFE.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-Drs82lSi.js → use-limits-display-prefs-NZ-AAYD5.js} +1 -1
- package/dashboard/dist/assets/use-native-settings-CWiLqfZh.js +1 -0
- package/dashboard/dist/assets/{use-ordered-list-CClTBWi0.js → use-ordered-list-Df8pw-WJ.js} +1 -1
- package/dashboard/dist/assets/use-pet-settings-Bg2CDprr.js +2 -0
- package/dashboard/dist/assets/use-reduced-motion-iDZEUyua.js +1 -0
- package/dashboard/dist/assets/{use-transform-CA5mW4u8.js → use-transform-BHwEAfmO.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-CSxFFn3_.js +1 -0
- package/dashboard/dist/assets/{useCurrency-CaOhUMcm.js → useCurrency-pwClnCZ5.js} +1 -1
- package/dashboard/dist/assets/usePositioner-B0j1orGj.js +1 -0
- package/dashboard/dist/assets/useTokenFormat-BMvVAe6V.js +1 -0
- package/dashboard/dist/assets/{zap-Cqur2X5s.js → zap-DvX2aYx5.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/pricing.html +6 -5
- package/dashboard/dist/privacy.html +3 -3
- package/dashboard/dist/share.html +2 -2
- package/dashboard/dist/terms.html +6 -6
- package/package.json +6 -2
- package/src/commands/device-login.js +60 -33
- package/src/commands/init.js +26 -7
- package/src/commands/serve.js +3 -3
- package/src/commands/sync.js +43 -22
- package/src/lib/atomic-json.js +27 -0
- package/src/lib/bearer-token.js +11 -0
- package/src/lib/cloud-account.js +4 -1
- package/src/lib/local-api.js +282 -69
- package/src/lib/openclaw-cli.js +104 -0
- package/src/lib/openclaw-hook.js +1 -46
- package/src/lib/openclaw-session-plugin.js +1 -59
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/proxy-env.js +8 -3
- package/src/lib/rollout.js +15 -10
- package/src/lib/runtime-config.js +84 -14
- package/src/lib/skills-manager.js +8 -1
- package/src/lib/static-server.js +19 -4
- package/src/lib/upload-throttle.js +18 -4
- package/dashboard/dist/assets/AchievementBadge-B67_Cp1T.js +0 -1
- package/dashboard/dist/assets/AchievementsPage-DNJ5n0Bn.js +0 -2
- package/dashboard/dist/assets/BadgeDetailModal-DFL1MPcg.js +0 -1
- package/dashboard/dist/assets/Card-dqzakOoX.js +0 -1
- package/dashboard/dist/assets/CommandPalette-8B38qUs0.js +0 -1
- package/dashboard/dist/assets/CommunityStatsModal-DnbCiGms.js +0 -1
- package/dashboard/dist/assets/Controls-BgQflA-3.js +0 -1
- package/dashboard/dist/assets/DashboardPage-DeLRhWU_.js +0 -1
- package/dashboard/dist/assets/DialogClose-DUxjaRqf.js +0 -1
- package/dashboard/dist/assets/DialogDescription-h6XMtzIU.js +0 -1
- package/dashboard/dist/assets/LandingPage-cjoagPAb.js +0 -2
- package/dashboard/dist/assets/LeaderboardAvatar-CJdG51hR.js +0 -1
- package/dashboard/dist/assets/LeaderboardPage-D-yf2rOQ.js +0 -2
- package/dashboard/dist/assets/LeaderboardProfileModal--hrfpUPF.js +0 -73
- package/dashboard/dist/assets/LoginCard-BiMyV2Uf.js +0 -1
- package/dashboard/dist/assets/LoginModal-D79CMZmf.js +0 -1
- package/dashboard/dist/assets/LoginPage-BLyGuPlG.js +0 -1
- package/dashboard/dist/assets/PetPage-Bv0K4k2t.js +0 -1
- package/dashboard/dist/assets/PopoverPopup-3eucO8QU.js +0 -1
- package/dashboard/dist/assets/ResetPasswordPage-VYMl84_-.js +0 -1
- package/dashboard/dist/assets/SelectItemText-CoTR0Uwm.js +0 -1
- package/dashboard/dist/assets/SettingsPage-huGkex5G.js +0 -2
- package/dashboard/dist/assets/SkillsPage-B9usVOTU.js +0 -1
- package/dashboard/dist/assets/TrendMonitor-B8SKBsWO.js +0 -19
- package/dashboard/dist/assets/WidgetsPage-o_91L3Zi.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-Cj9D7QBh.js +0 -45
- package/dashboard/dist/assets/__vite-browser-external-gKkiQad5.css +0 -1
- package/dashboard/dist/assets/agent-logos-CEcgxIBU.js +0 -1
- package/dashboard/dist/assets/composite-DO7_ilHE.js +0 -1
- package/dashboard/dist/assets/skills-api-BeN184fz.js +0 -1
- package/dashboard/dist/assets/store-D-FZbBaC.js +0 -1
- package/dashboard/dist/assets/use-pet-settings-DvaQ7XrL.js +0 -2
- package/dashboard/dist/assets/use-reduced-motion-Dg10dVuu.js +0 -1
- package/dashboard/dist/assets/use-session-efficiency-pref-BiYRXVlN.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-B0uafB4O.js +0 -1
- package/dashboard/dist/assets/useTokenFormat-JjQCKdIr.js +0 -1
package/src/lib/local-api.js
CHANGED
|
@@ -3,7 +3,7 @@ const os = require("node:os");
|
|
|
3
3
|
const path = require("node:path");
|
|
4
4
|
const { spawn } = require("node:child_process");
|
|
5
5
|
const crypto = require("node:crypto");
|
|
6
|
-
const { DEFAULT_BASE_URL, resolveRuntimeConfig } = require("./runtime-config");
|
|
6
|
+
const { DEFAULT_BASE_URL, resolveRuntimeConfig, assertInsforgeRuntime, normalizeInstanceBaseUrl, clearDeviceIdentity, resetInstanceState } = require("./runtime-config");
|
|
7
7
|
const {
|
|
8
8
|
filterRowsByUsageScope,
|
|
9
9
|
getSourceScope,
|
|
@@ -14,6 +14,8 @@ const { accountSlugFor, fetchAccountUsage, mintAccessToken, invalidateCloudAccou
|
|
|
14
14
|
const { getOrCreateMachineId, computeStableMachineId } = require("./machine-id");
|
|
15
15
|
const { functionUrlFor, fetchFunctionResponse } = require("./function-url");
|
|
16
16
|
const { createCloudDeviceTokenStore } = require("./cloud-device-token");
|
|
17
|
+
const { parseBearerToken } = require("./bearer-token");
|
|
18
|
+
const { writeJsonAtomicSync } = require("./atomic-json");
|
|
17
19
|
|
|
18
20
|
const SYNC_TIMEOUT_MS = 120_000;
|
|
19
21
|
// A failed account-view request should not stall every dashboard refresh
|
|
@@ -806,18 +808,11 @@ function normalizeRemoteHttpBaseUrl(value) {
|
|
|
806
808
|
}
|
|
807
809
|
}
|
|
808
810
|
|
|
809
|
-
function resolveAllowedInsforgeBaseUrl(value) {
|
|
811
|
+
function resolveAllowedInsforgeBaseUrl(value, runtime = resolveRuntimeConfig()) {
|
|
810
812
|
const requested = normalizeRemoteHttpBaseUrl(value);
|
|
811
813
|
if (!requested) return null;
|
|
812
814
|
|
|
813
|
-
|
|
814
|
-
const allowed = new Set(
|
|
815
|
-
[runtime.baseUrl, DEFAULT_BASE_URL]
|
|
816
|
-
.map((entry) => normalizeRemoteHttpBaseUrl(entry))
|
|
817
|
-
.filter(Boolean),
|
|
818
|
-
);
|
|
819
|
-
|
|
820
|
-
return allowed.has(requested) ? requested : null;
|
|
815
|
+
return requested === normalizeRemoteHttpBaseUrl(runtime.baseUrl) ? requested : null;
|
|
821
816
|
}
|
|
822
817
|
|
|
823
818
|
function parseCookieHeader(value) {
|
|
@@ -890,24 +885,29 @@ function readBodyLimited(req, maxBytes) {
|
|
|
890
885
|
|
|
891
886
|
function runSyncCommand(extraEnv = {}, opts = {}) {
|
|
892
887
|
return new Promise((resolve, reject) => {
|
|
893
|
-
const args =
|
|
888
|
+
const args = opts.trackerDataDir
|
|
889
|
+
? ["-e", `require(${JSON.stringify(path.resolve(__dirname, "../commands/sync.js"))}).cmdSync(process.argv.slice(1), ${JSON.stringify({ trackerDataDir: opts.trackerDataDir, home: path.dirname(opts.trackerDataDir), ...(opts.syncContext || {}) })}).catch(e => { console.error(e.message); process.exitCode = 1; })`, "--"]
|
|
890
|
+
: [TRACKER_BIN, "sync"];
|
|
894
891
|
if (opts.auto === true) args.push("--auto");
|
|
895
892
|
if (opts.background === true) args.push("--background");
|
|
896
893
|
if (opts.publishAccount === true) args.push("--publish-account");
|
|
897
894
|
if (opts.allLocalSources === true) args.push("--all-local-sources");
|
|
898
895
|
if (opts.drain === true) args.push("--drain");
|
|
899
896
|
if (opts.waitForLock === true) args.push("--wait-for-lock");
|
|
897
|
+
if (opts.signal?.aborted) { reject(Object.assign(new Error("Account session changed"), { code: "auth_session_changed", status: 409 })); return; }
|
|
900
898
|
const child = spawn(process.execPath, args, {
|
|
901
899
|
env: {
|
|
902
900
|
...process.env, ...extraEnv,
|
|
903
901
|
...(extraEnv.TOKENTRACKER_DEVICE_TOKEN ? {
|
|
904
|
-
TOKENTRACKER_INSFORGE_BASE_URL: extraEnv.TOKENTRACKER_INSFORGE_BASE_URL
|
|
905
|
-
TOKENTRACKER_INSFORGE_ANON_KEY:
|
|
902
|
+
TOKENTRACKER_INSFORGE_BASE_URL: extraEnv.TOKENTRACKER_INSFORGE_BASE_URL,
|
|
903
|
+
TOKENTRACKER_INSFORGE_ANON_KEY: extraEnv.TOKENTRACKER_INSFORGE_ANON_KEY || "",
|
|
906
904
|
} : {}),
|
|
907
905
|
TOKENTRACKER_LOCAL_SYNC_DEVICE_TOKEN: extraEnv.TOKENTRACKER_DEVICE_TOKEN || "",
|
|
908
906
|
},
|
|
909
907
|
stdio: ["ignore", "pipe", "pipe"],
|
|
910
908
|
});
|
|
909
|
+
const cancel = () => child.kill("SIGTERM");
|
|
910
|
+
opts.signal?.addEventListener("abort", cancel, { once: true });
|
|
911
911
|
let stdout = "";
|
|
912
912
|
let stderr = "";
|
|
913
913
|
let settled = false;
|
|
@@ -915,18 +915,13 @@ function runSyncCommand(extraEnv = {}, opts = {}) {
|
|
|
915
915
|
if (settled) return;
|
|
916
916
|
settled = true;
|
|
917
917
|
clearTimeout(tid);
|
|
918
|
+
opts.signal?.removeEventListener("abort", cancel);
|
|
918
919
|
fn(v);
|
|
919
920
|
};
|
|
921
|
+
let timedOut = false;
|
|
920
922
|
const tid = setTimeout(() => {
|
|
923
|
+
timedOut = true;
|
|
921
924
|
child.kill("SIGTERM");
|
|
922
|
-
finish(
|
|
923
|
-
reject,
|
|
924
|
-
Object.assign(new Error("Sync timed out"), {
|
|
925
|
-
code: "SYNC_TIMEOUT",
|
|
926
|
-
stdout: trimOutput(stdout),
|
|
927
|
-
stderr: trimOutput(stderr),
|
|
928
|
-
}),
|
|
929
|
-
);
|
|
930
925
|
}, SYNC_TIMEOUT_MS);
|
|
931
926
|
child.stdout?.on("data", (c) => {
|
|
932
927
|
stdout += c;
|
|
@@ -938,6 +933,8 @@ function runSyncCommand(extraEnv = {}, opts = {}) {
|
|
|
938
933
|
finish(reject, Object.assign(e, { stdout: trimOutput(stdout), stderr: trimOutput(stderr) }));
|
|
939
934
|
});
|
|
940
935
|
child.on("close", (code) => {
|
|
936
|
+
if (timedOut) { finish(reject, Object.assign(new Error("Sync timed out"), { code: "SYNC_TIMEOUT", stdout: trimOutput(stdout), stderr: trimOutput(stderr) })); return; }
|
|
937
|
+
if (opts.signal?.aborted) { finish(reject, Object.assign(new Error("Account session changed"), { code: "auth_session_changed", status: 409 })); return; }
|
|
941
938
|
const r = { code: code ?? 1, stdout: trimOutput(stdout), stderr: trimOutput(stderr) };
|
|
942
939
|
if (code === 0) {
|
|
943
940
|
finish(resolve, r);
|
|
@@ -1141,7 +1138,7 @@ function buildProxyHeaders(headers) {
|
|
|
1141
1138
|
// Main handler factory
|
|
1142
1139
|
// ---------------------------------------------------------------------------
|
|
1143
1140
|
|
|
1144
|
-
function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
1141
|
+
function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir: isolatedTrackerDataDir = null, syncContext = null }) {
|
|
1145
1142
|
const qp = queuePath || resolveQueuePath();
|
|
1146
1143
|
const normalizedServerVersion = typeof serverVersion === "string" ? serverVersion.trim() : "";
|
|
1147
1144
|
|
|
@@ -1151,13 +1148,24 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1151
1148
|
const csrfRelayCookieName = "insforge_csrf_token";
|
|
1152
1149
|
let relayCookies = new Map();
|
|
1153
1150
|
const localAuthToken = crypto.randomBytes(24).toString("hex");
|
|
1154
|
-
const trackerDataDir = path.
|
|
1151
|
+
const trackerDataDir = isolatedTrackerDataDir || path.dirname(qp);
|
|
1155
1152
|
const cookiePath = path.join(trackerDataDir, "relay-cookies.json");
|
|
1156
1153
|
const localSyncDeviceTokenStore = createCloudDeviceTokenStore(path.join(trackerDataDir, "cloud-device-token.json"));
|
|
1157
1154
|
const localSyncDeviceTokenCache = new Map();
|
|
1158
1155
|
const localSyncDeviceTokenInflight = new Map();
|
|
1159
1156
|
let localSyncDeviceTokenUserId = "";
|
|
1157
|
+
let localCloudSession = null;
|
|
1158
|
+
let localCloudGeneration = 0;
|
|
1159
|
+
const activeCloudSyncs = new Set();
|
|
1160
|
+
const localSyncDeviceMetadata = new Map();
|
|
1161
|
+
function invalidateLocalCloudSession() {
|
|
1162
|
+
localCloudGeneration += 1;
|
|
1163
|
+
localCloudSession = null;
|
|
1164
|
+
for (const controller of activeCloudSyncs) controller.abort();
|
|
1165
|
+
}
|
|
1160
1166
|
function clearLocalSyncDeviceTokenCache() {
|
|
1167
|
+
invalidateLocalCloudSession();
|
|
1168
|
+
localSyncDeviceMetadata.clear();
|
|
1161
1169
|
localSyncDeviceTokenCache.clear();
|
|
1162
1170
|
localSyncDeviceTokenInflight.clear();
|
|
1163
1171
|
localSyncDeviceTokenStore.clear();
|
|
@@ -1176,10 +1184,10 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1176
1184
|
if (attemptId && state.lastErrorAttemptId === attemptId &&
|
|
1177
1185
|
Date.parse(state.lastErrorAt || "") >= syncStartedAt && Number(state.lastErrorStatus) > 0) {
|
|
1178
1186
|
if (state.lastErrorCode === "CLOUD_DEVICE_TOKEN_REJECTED") invalidateLocalSyncDeviceToken(deviceToken);
|
|
1179
|
-
return { code: state.lastErrorCode || "CLOUD_UPLOAD_FAILED", status: Number(state.lastErrorStatus) };
|
|
1187
|
+
return { code: state.lastErrorCode || "CLOUD_UPLOAD_FAILED", status: Number(state.lastErrorStatus), retryAfterMs: Math.max(0, Math.max(Number(state.backoffUntilMs || 0), Number(state.nextAllowedAtMs || 0)) - Date.now()), nextAllowedAt: state.nextAllowedAtMs ? new Date(state.nextAllowedAtMs).toISOString() : null };
|
|
1180
1188
|
}
|
|
1181
|
-
if (backoff && Number(state.backoffUntilMs || 0) > syncStartedAt) {
|
|
1182
|
-
return { code: "SYNC_UPLOAD_BACKOFF", status: 429 };
|
|
1189
|
+
if (backoff && Math.max(Number(state.backoffUntilMs || 0), Number(state.nextAllowedAtMs || 0)) > syncStartedAt) {
|
|
1190
|
+
return { code: "SYNC_UPLOAD_BACKOFF", status: 429, retryAfterMs: Math.max(0, Math.max(Number(state.backoffUntilMs || 0), Number(state.nextAllowedAtMs || 0)) - Date.now()), nextAllowedAt: state.nextAllowedAtMs ? new Date(state.nextAllowedAtMs).toISOString() : null };
|
|
1183
1191
|
}
|
|
1184
1192
|
} catch { /* no upload failure was recorded */ }
|
|
1185
1193
|
return null;
|
|
@@ -1191,6 +1199,46 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1191
1199
|
const accountViewFailures = new Map();
|
|
1192
1200
|
const accountViewInFlight = new Map();
|
|
1193
1201
|
let accountUploadState = readAccountUploadState();
|
|
1202
|
+
const runtimeConfigPath = path.join(path.dirname(qp), "config.json");
|
|
1203
|
+
const instancePath = path.join(trackerDataDir, "runtime-instance.json");
|
|
1204
|
+
const issuedAccessTokenInstances = new Map();
|
|
1205
|
+
let instanceFingerprint = "";
|
|
1206
|
+
try { instanceFingerprint = JSON.parse(fs.readFileSync(instancePath, "utf8")).fingerprint || ""; } catch { /* first start */ }
|
|
1207
|
+
|
|
1208
|
+
function fingerprintFor(runtime) {
|
|
1209
|
+
return crypto.createHash("sha256").update(`${runtime.baseUrl}\0${runtime.anonKey || ""}`).digest("hex");
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function getRuntime() {
|
|
1213
|
+
let config = {};
|
|
1214
|
+
let invalid = false;
|
|
1215
|
+
try { config = JSON.parse(fs.readFileSync(runtimeConfigPath, "utf8")); }
|
|
1216
|
+
catch (error) { invalid = error.code !== "ENOENT"; }
|
|
1217
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) { config = {}; invalid = true; }
|
|
1218
|
+
const runtime = resolveRuntimeConfig({ config: config || {}, env: process.env });
|
|
1219
|
+
if (invalid) runtime.configurationError = "invalid_local_config";
|
|
1220
|
+
if (runtime.configurationError || (!fs.existsSync(runtimeConfigPath) && !fs.existsSync(qp))) return runtime;
|
|
1221
|
+
const fingerprint = fingerprintFor(runtime);
|
|
1222
|
+
if (fingerprint !== instanceFingerprint) {
|
|
1223
|
+
if (instanceFingerprint || runtime.baseUrl !== DEFAULT_BASE_URL) {
|
|
1224
|
+
clearRelayCookies("backend instance changed");
|
|
1225
|
+
_nativeAuthPending = false; _nativeAuthExpiry = 0;
|
|
1226
|
+
resetInstanceState(path.dirname(qp));
|
|
1227
|
+
if (trackerDataDir !== path.dirname(qp)) resetInstanceState(trackerDataDir);
|
|
1228
|
+
accountViewFailures.clear(); accountViewInFlight.clear();
|
|
1229
|
+
if (["deviceToken", "deviceId", "user_id"].some(key => config?.[key]) &&
|
|
1230
|
+
normalizeInstanceBaseUrl(config.deviceTokenBaseUrl) !== runtime.baseUrl) {
|
|
1231
|
+
config = clearDeviceIdentity(config);
|
|
1232
|
+
writeJsonAtomicSync(runtimeConfigPath, config);
|
|
1233
|
+
runtime.deviceToken = null;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
fs.mkdirSync(trackerDataDir, { recursive: true });
|
|
1237
|
+
fs.writeFileSync(instancePath, JSON.stringify({ fingerprint }), { mode: 0o600 });
|
|
1238
|
+
instanceFingerprint = fingerprint;
|
|
1239
|
+
}
|
|
1240
|
+
return runtime;
|
|
1241
|
+
}
|
|
1194
1242
|
|
|
1195
1243
|
function readAccountUploadState() {
|
|
1196
1244
|
try {
|
|
@@ -1322,7 +1370,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1322
1370
|
}
|
|
1323
1371
|
}
|
|
1324
1372
|
|
|
1325
|
-
function captureAuthTokensFromBody(bodyBuffer, contentType, { newLogin = false } = {}) {
|
|
1373
|
+
function captureAuthTokensFromBody(bodyBuffer, contentType, { newLogin = false, sourceFingerprint } = {}) {
|
|
1326
1374
|
if (!bodyBuffer || !String(contentType || "").toLowerCase().includes("application/json")) return;
|
|
1327
1375
|
let parsed = null;
|
|
1328
1376
|
try {
|
|
@@ -1330,6 +1378,11 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1330
1378
|
} catch {
|
|
1331
1379
|
return;
|
|
1332
1380
|
}
|
|
1381
|
+
if (sourceFingerprint && typeof parsed?.accessToken === "string" && parsed.accessToken.length <= 8192) {
|
|
1382
|
+
const hash = crypto.createHash("sha256").update(parsed.accessToken).digest("hex");
|
|
1383
|
+
if (!issuedAccessTokenInstances.has(hash)) issuedAccessTokenInstances.set(hash, sourceFingerprint);
|
|
1384
|
+
while (issuedAccessTokenInstances.size > 64) issuedAccessTokenInstances.delete(issuedAccessTokenInstances.keys().next().value);
|
|
1385
|
+
}
|
|
1333
1386
|
let changed = false;
|
|
1334
1387
|
const token = typeof parsed?.csrfToken === "string" ? parsed.csrfToken.trim() : "";
|
|
1335
1388
|
if (token) {
|
|
@@ -1422,23 +1475,17 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1422
1475
|
async function issueDeviceTokenForLocalSync(queuePathForMachineId, options = {}) {
|
|
1423
1476
|
if (!getCloudSyncPref()) return null;
|
|
1424
1477
|
const refreshToken = getRefreshTokenForCloud();
|
|
1425
|
-
if (!refreshToken) return null;
|
|
1478
|
+
if (!options.accessToken && !refreshToken) return null;
|
|
1426
1479
|
const machineId = getOrCreateMachineId(queuePathForMachineId);
|
|
1427
1480
|
if (!machineId) return null;
|
|
1428
|
-
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
normalizeRemoteHttpBaseUrl(options.baseUrl) ||
|
|
1432
|
-
normalizeRemoteHttpBaseUrl(runtime.baseUrl) ||
|
|
1433
|
-
normalizeRemoteHttpBaseUrl(DEFAULT_BASE_URL);
|
|
1481
|
+
const runtime = assertInsforgeRuntime(getRuntime());
|
|
1482
|
+
const baseUrl = normalizeRemoteHttpBaseUrl(options.baseUrl) ||
|
|
1483
|
+
normalizeRemoteHttpBaseUrl(runtime.baseUrl);
|
|
1434
1484
|
const sessionAtStart = getCloudAccountSessionGeneration();
|
|
1435
1485
|
const cloudPrefAtStart = cloudSyncPrefGeneration;
|
|
1436
|
-
const
|
|
1437
|
-
|
|
1438
|
-
anonKey: runtime.anonKey,
|
|
1439
|
-
refreshToken,
|
|
1440
|
-
timeoutMs: runtime.httpTimeoutMs,
|
|
1441
|
-
throwOnFailure: true,
|
|
1486
|
+
const generationAtStart = localCloudGeneration;
|
|
1487
|
+
const minted = options.accessToken ? { accessToken: options.accessToken } : await mintAccessToken({
|
|
1488
|
+
baseUrl, anonKey: runtime.anonKey, refreshToken, timeoutMs: runtime.httpTimeoutMs, throwOnFailure: true,
|
|
1442
1489
|
}).catch((error) => {
|
|
1443
1490
|
if (error.status === 401 && sessionAtStart === getCloudAccountSessionGeneration()) clearLocalSyncDeviceTokenCache();
|
|
1444
1491
|
throw error;
|
|
@@ -1451,13 +1498,16 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1451
1498
|
if (rotatedRefreshToken) setRelayRefreshToken(rotatedRefreshToken);
|
|
1452
1499
|
if (minted.csrfToken) setRelayCsrfToken(minted.csrfToken);
|
|
1453
1500
|
if (!getCloudSyncPref() || cloudPrefAtStart !== cloudSyncPrefGeneration) return null;
|
|
1454
|
-
//
|
|
1455
|
-
//
|
|
1501
|
+
// Refresh validates native ownership. A browser JWT must additionally pass
|
|
1502
|
+
// token issuance upstream before the mirror receives an upload binding.
|
|
1456
1503
|
let userId = "";
|
|
1457
1504
|
try {
|
|
1458
1505
|
const payload = JSON.parse(Buffer.from(minted.accessToken.split(".")[1], "base64url").toString("utf8"));
|
|
1459
1506
|
userId = typeof payload.sub === "string" ? payload.sub : "";
|
|
1460
1507
|
} catch { /* legacy opaque access tokens retain their isolated refresh key */ }
|
|
1508
|
+
if (options.expectedOwnerId && userId !== options.expectedOwnerId) {
|
|
1509
|
+
throw Object.assign(new Error("Account owner mismatch"), { status: 409, code: "auth_owner_mismatch" });
|
|
1510
|
+
}
|
|
1461
1511
|
if (userId && localSyncDeviceTokenUserId && userId !== localSyncDeviceTokenUserId) {
|
|
1462
1512
|
clearLocalSyncDeviceTokenCache();
|
|
1463
1513
|
}
|
|
@@ -1465,15 +1515,18 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1465
1515
|
const identity = userId || `refresh:${rotatedRefreshToken || refreshToken}`;
|
|
1466
1516
|
const cacheKey = localSyncDeviceTokenCacheKey(identity, machineId, baseUrl);
|
|
1467
1517
|
const binding = { userId, baseUrl, machineId };
|
|
1468
|
-
const cachedToken = localSyncDeviceTokenCache.get(cacheKey) ||
|
|
1469
|
-
(userId ? localSyncDeviceTokenStore.get(binding) : null);
|
|
1518
|
+
const cachedToken = !options.accessToken && (localSyncDeviceTokenCache.get(cacheKey) ||
|
|
1519
|
+
(userId ? localSyncDeviceTokenStore.get(binding) : null));
|
|
1470
1520
|
if (cachedToken) {
|
|
1471
1521
|
localSyncDeviceTokenCache.set(cacheKey, cachedToken);
|
|
1472
1522
|
if (userId) localSyncDeviceTokenStore.set(binding, cachedToken);
|
|
1473
|
-
return cachedToken;
|
|
1523
|
+
return options.returnSession ? localSyncDeviceMetadata.get(cachedToken) || null : cachedToken;
|
|
1474
1524
|
}
|
|
1475
1525
|
const inflightToken = localSyncDeviceTokenInflight.get(cacheKey);
|
|
1476
|
-
if (inflightToken?.sessionGeneration === sessionAtStart
|
|
1526
|
+
if (inflightToken?.sessionGeneration === sessionAtStart && inflightToken.generation === generationAtStart) {
|
|
1527
|
+
const token = await inflightToken.promise;
|
|
1528
|
+
return options.returnSession ? localSyncDeviceMetadata.get(token) || null : token;
|
|
1529
|
+
}
|
|
1477
1530
|
|
|
1478
1531
|
const issuePromise = (async () => {
|
|
1479
1532
|
const root = String(baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
@@ -1511,26 +1564,34 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1511
1564
|
}).finally(() => {
|
|
1512
1565
|
if (timeoutId) clearTimeout(timeoutId);
|
|
1513
1566
|
});
|
|
1514
|
-
|
|
1567
|
+
getRuntime();
|
|
1568
|
+
if (sessionAtStart !== getCloudAccountSessionGeneration() || !getCloudSyncPref() || cloudPrefAtStart !== cloudSyncPrefGeneration || generationAtStart !== localCloudGeneration) return null;
|
|
1515
1569
|
if (!res.ok) {
|
|
1516
1570
|
if (res.status === 401 && sessionAtStart === getCloudAccountSessionGeneration()) {
|
|
1517
1571
|
localSyncDeviceTokenCache.delete(cacheKey);
|
|
1518
1572
|
localSyncDeviceTokenStore.clear(binding);
|
|
1519
1573
|
}
|
|
1574
|
+
const denied = await res.json().catch(() => ({}));
|
|
1575
|
+
if (typeof denied?.code === "string" && denied.code.startsWith("cloud_")) {
|
|
1576
|
+
throw Object.assign(new Error(denied.code), { code: denied.code, status: res.status,
|
|
1577
|
+
recoveryUrl: denied.recovery_url });
|
|
1578
|
+
}
|
|
1520
1579
|
return null;
|
|
1521
1580
|
}
|
|
1522
1581
|
const data = await res.json().catch(() => null);
|
|
1523
1582
|
const token = typeof data?.token === "string" ? data.token.trim() : "";
|
|
1524
|
-
if (sessionAtStart !== getCloudAccountSessionGeneration() || !getCloudSyncPref() || cloudPrefAtStart !== cloudSyncPrefGeneration) return null;
|
|
1583
|
+
if (sessionAtStart !== getCloudAccountSessionGeneration() || !getCloudSyncPref() || cloudPrefAtStart !== cloudSyncPrefGeneration || generationAtStart !== localCloudGeneration) return null;
|
|
1525
1584
|
if (token) {
|
|
1585
|
+
localSyncDeviceMetadata.set(token, { token, deviceId: data.device_id, issuedAt: data.created_at || new Date().toISOString() });
|
|
1526
1586
|
localSyncDeviceTokenCache.set(cacheKey, token);
|
|
1527
1587
|
if (userId) localSyncDeviceTokenStore.set(binding, token);
|
|
1528
1588
|
}
|
|
1529
1589
|
return token || null;
|
|
1530
1590
|
})();
|
|
1531
|
-
localSyncDeviceTokenInflight.set(cacheKey, { promise: issuePromise, sessionGeneration: sessionAtStart });
|
|
1591
|
+
localSyncDeviceTokenInflight.set(cacheKey, { promise: issuePromise, sessionGeneration: sessionAtStart, generation: generationAtStart });
|
|
1532
1592
|
try {
|
|
1533
|
-
|
|
1593
|
+
const token = await issuePromise;
|
|
1594
|
+
return options.returnSession ? localSyncDeviceMetadata.get(token) || null : token;
|
|
1534
1595
|
} finally {
|
|
1535
1596
|
if (localSyncDeviceTokenInflight.get(cacheKey)?.promise === issuePromise) localSyncDeviceTokenInflight.delete(cacheKey);
|
|
1536
1597
|
}
|
|
@@ -1553,6 +1614,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1553
1614
|
if (err.name === "AbortError" || err.name === "TimeoutError" || err.code === "auth_timeout") {
|
|
1554
1615
|
return "transient-timeout";
|
|
1555
1616
|
}
|
|
1617
|
+
if (typeof err.code === "string" && err.code.startsWith("cloud_")) return err.code.replaceAll("_", "-");
|
|
1556
1618
|
if (err.code === "auth_rejected" || err.code === "auth_invalid" || err.status === 401 || err.status === 403) {
|
|
1557
1619
|
return "transient-auth";
|
|
1558
1620
|
}
|
|
@@ -1570,7 +1632,8 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1570
1632
|
const refreshToken = getRefreshTokenForCloud();
|
|
1571
1633
|
if (!refreshToken) return ACCOUNT_FALLBACK_SIGNED_OUT;
|
|
1572
1634
|
invalidateAfterAccountUpload();
|
|
1573
|
-
const runtime =
|
|
1635
|
+
const runtime = getRuntime();
|
|
1636
|
+
if (runtime.configurationError) return ACCOUNT_FALLBACK_SIGNED_OUT;
|
|
1574
1637
|
const failureKey = `${runtime.baseUrl}\0${refreshToken}`;
|
|
1575
1638
|
const sessionAtStart = getCloudAccountSessionGeneration();
|
|
1576
1639
|
const cloudPrefAtStart = cloudSyncPrefGeneration;
|
|
@@ -1598,6 +1661,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1598
1661
|
refreshToken,
|
|
1599
1662
|
timeoutMs,
|
|
1600
1663
|
onSessionRefreshed: (session) => {
|
|
1664
|
+
getRuntime();
|
|
1601
1665
|
if (sessionAtStart !== getCloudAccountSessionGeneration()) {
|
|
1602
1666
|
throw Object.assign(new Error("Account session changed"), { code: "auth_session_changed" });
|
|
1603
1667
|
}
|
|
@@ -1612,6 +1676,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1612
1676
|
pending.then(cleanup, cleanup);
|
|
1613
1677
|
}
|
|
1614
1678
|
const out = await pending;
|
|
1679
|
+
getRuntime();
|
|
1615
1680
|
// `null` here means the slug has no cloud equivalent, or the session
|
|
1616
1681
|
// vanished mid-request — a routing/session fact, not an outage.
|
|
1617
1682
|
if (!out) return ACCOUNT_FALLBACK_SIGNED_OUT;
|
|
@@ -1652,7 +1717,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1652
1717
|
// erroring, but say so: this is a *temporary* downgrade and the client
|
|
1653
1718
|
// must not treat it as the user's real data scope.
|
|
1654
1719
|
const reason = classifyAccountFallback(e);
|
|
1655
|
-
if (
|
|
1720
|
+
if (getRuntime().debug) {
|
|
1656
1721
|
console.warn(`[LocalAPI] account view fallback (${reason}) for ${usageSlug}:`, e?.message || e);
|
|
1657
1722
|
}
|
|
1658
1723
|
rememberAccountViewFailure(failureKey);
|
|
@@ -1707,6 +1772,45 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1707
1772
|
|
|
1708
1773
|
return async function handleLocalApi(req, res, url) {
|
|
1709
1774
|
const p = url.pathname;
|
|
1775
|
+
// Session recovery and the one-time native callback marker belong to local
|
|
1776
|
+
// clients. Reject foreign requests before consuming either, including preflights.
|
|
1777
|
+
const nativeMarkerRead = p === "/api/auth-bridge/verifier" && String(req.method).toUpperCase() === "GET";
|
|
1778
|
+
if (p.startsWith("/api/auth/") || nativeMarkerRead) {
|
|
1779
|
+
if (!hasAllowedLoopbackOrigin(req.headers || {}) || req.headers?.["sec-fetch-site"] === "cross-site") {
|
|
1780
|
+
json(res, { code: "untrusted_auth_origin", error: "Authentication requests must come from the local dashboard." }, 403);
|
|
1781
|
+
return true;
|
|
1782
|
+
}
|
|
1783
|
+
if (String(req.method).toUpperCase() === "OPTIONS") {
|
|
1784
|
+
res.writeHead(204, {
|
|
1785
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
1786
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
1787
|
+
});
|
|
1788
|
+
res.end();
|
|
1789
|
+
return true;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
const selectedRuntime = getRuntime();
|
|
1793
|
+
const callerInstance = req.headers?.["x-tokentracker-instance"];
|
|
1794
|
+
if (callerInstance && normalizeInstanceBaseUrl(callerInstance) !== selectedRuntime.baseUrl) {
|
|
1795
|
+
json(res, { code: "instance_changed", error: "The configured backend instance changed. Reload and sign in again." }, 409);
|
|
1796
|
+
return true;
|
|
1797
|
+
}
|
|
1798
|
+
const bearer = parseBearerToken(req.headers?.authorization);
|
|
1799
|
+
const issuedFor = bearer && issuedAccessTokenInstances.get(crypto.createHash("sha256").update(bearer).digest("hex"));
|
|
1800
|
+
if (issuedFor && issuedFor !== fingerprintFor(selectedRuntime)) {
|
|
1801
|
+
json(res, { code: "instance_changed", error: "This session belongs to another backend instance. Sign in again." }, 409);
|
|
1802
|
+
return true;
|
|
1803
|
+
}
|
|
1804
|
+
if (p === "/api/runtime-config.js") {
|
|
1805
|
+
if (String(req.method).toUpperCase() !== "GET") { json(res, { error: "Method Not Allowed" }, 405); return true; }
|
|
1806
|
+
const descriptor = { baseUrl: normalizeInstanceBaseUrl(selectedRuntime.baseUrl) || "",
|
|
1807
|
+
anonKey: selectedRuntime.configurationError ? null : selectedRuntime.anonKey,
|
|
1808
|
+
dashboardUrl: normalizeInstanceBaseUrl(selectedRuntime.dashboardUrl) || "", configurationError: selectedRuntime.configurationError };
|
|
1809
|
+
const value = JSON.stringify(descriptor).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1810
|
+
res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
1811
|
+
res.end(`window.__TOKENTRACKER_RUNTIME_CONFIG__=${value};`);
|
|
1812
|
+
return true;
|
|
1813
|
+
}
|
|
1710
1814
|
|
|
1711
1815
|
if (p === "/api/local-auth") {
|
|
1712
1816
|
if (String(req.method || "GET").toUpperCase() !== "GET") {
|
|
@@ -1820,12 +1924,32 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1820
1924
|
|
|
1821
1925
|
// --- auth proxy: forward /api/auth/* to InsForge cloud ---
|
|
1822
1926
|
if (p.startsWith("/api/auth/")) {
|
|
1927
|
+
if (String(req.method).toUpperCase() === "POST" && /^\/api\/auth\/(?:logout|sessions|sign-in|oauth\/exchange)$/.test(p)) {
|
|
1928
|
+
clearLocalSyncDeviceTokenCache();
|
|
1929
|
+
invalidateCloudAccountPayloadCache({ sessionChanged: true });
|
|
1930
|
+
}
|
|
1823
1931
|
const authSessionAtStart = getCloudAccountSessionGeneration();
|
|
1824
|
-
const runtime =
|
|
1932
|
+
const runtime = getRuntime();
|
|
1933
|
+
try { assertInsforgeRuntime(runtime); } catch (error) { json(res, { code: error.code, error: error.message }, 503); return true; }
|
|
1825
1934
|
const insforgeBase = runtime.baseUrl || DEFAULT_BASE_URL;
|
|
1935
|
+
if (req.headers.apikey && req.headers.apikey !== runtime.anonKey) {
|
|
1936
|
+
json(res, { code: "instance_changed", error: "The public client key belongs to another backend instance." }, 409);
|
|
1937
|
+
return true;
|
|
1938
|
+
}
|
|
1826
1939
|
try {
|
|
1827
1940
|
const targetUrl = `${insforgeBase.replace(/\/$/, "")}${p}${url.search || ""}`;
|
|
1828
1941
|
const proxyHeaders = buildProxyHeaders(req.headers);
|
|
1942
|
+
// Browser cookies survive changes to the loopback server's backend.
|
|
1943
|
+
// Only this instance's captured cookies can be relayed, including when
|
|
1944
|
+
// switching from a custom instance back to the official service.
|
|
1945
|
+
const hadBrowserCookies = Boolean(normalizeCookieHeader(proxyHeaders.cookie).trim());
|
|
1946
|
+
proxyHeaders.cookie = hadBrowserCookies ? buildRelayCookieHeader("") : "";
|
|
1947
|
+
if (proxyHeaders["x-csrf-token"] !== getRelayCookieValue(csrfRelayCookieName)) delete proxyHeaders["x-csrf-token"];
|
|
1948
|
+
if (insforgeBase !== DEFAULT_BASE_URL && proxyHeaders.authorization && !callerInstance) {
|
|
1949
|
+
json(res, { code: "instance_changed", error: "Reload the local dashboard before using a custom backend session." }, 409);
|
|
1950
|
+
return true;
|
|
1951
|
+
}
|
|
1952
|
+
proxyHeaders.apikey = runtime.anonKey;
|
|
1829
1953
|
const hasClientCookie = normalizeCookieHeader(proxyHeaders["cookie"]).trim().length > 0;
|
|
1830
1954
|
const hasCsrfHeader = typeof proxyHeaders["x-csrf-token"] === "string" && proxyHeaders["x-csrf-token"].trim().length > 0;
|
|
1831
1955
|
const relayCsrfToken = getRelayCookieValue(csrfRelayCookieName);
|
|
@@ -1865,6 +1989,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1865
1989
|
const injectedRelayCookies =
|
|
1866
1990
|
shouldInjectRelayCookies && relayCookies.size > 0 && mergedCookie !== originalCookieHeader;
|
|
1867
1991
|
if (mergedCookie) proxyHeaders["cookie"] = mergedCookie;
|
|
1992
|
+
else delete proxyHeaders["cookie"];
|
|
1868
1993
|
|
|
1869
1994
|
const bodyChunks = [];
|
|
1870
1995
|
for await (const chunk of req) bodyChunks.push(chunk);
|
|
@@ -1915,6 +2040,11 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1915
2040
|
}
|
|
1916
2041
|
}
|
|
1917
2042
|
|
|
2043
|
+
const latestRuntime = getRuntime();
|
|
2044
|
+
if (latestRuntime.configurationError || latestRuntime.baseUrl !== runtime.baseUrl || latestRuntime.anonKey !== runtime.anonKey) {
|
|
2045
|
+
json(res, { code: "instance_changed", error: "The configured backend changed during authentication. Reload and sign in again." }, 409);
|
|
2046
|
+
return true;
|
|
2047
|
+
}
|
|
1918
2048
|
// Error responses must not mutate relay state: a 403's deletion
|
|
1919
2049
|
// set-cookie (insforge_refresh_token=; Expires=1970) would otherwise
|
|
1920
2050
|
// destroy a still-valid persisted session.
|
|
@@ -1939,7 +2069,9 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
1939
2069
|
if (p === "/api/auth/logout") {
|
|
1940
2070
|
clearRelayCookies("sign out");
|
|
1941
2071
|
} else {
|
|
1942
|
-
captureAuthTokensFromBody(resBody, proxyRes.headers.get("content-type"), {
|
|
2072
|
+
captureAuthTokensFromBody(resBody, proxyRes.headers.get("content-type"), {
|
|
2073
|
+
newLogin: p !== "/api/auth/refresh", sourceFingerprint: fingerprintFor(runtime),
|
|
2074
|
+
});
|
|
1943
2075
|
}
|
|
1944
2076
|
}
|
|
1945
2077
|
if (
|
|
@@ -2134,6 +2266,36 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
2134
2266
|
return true;
|
|
2135
2267
|
}
|
|
2136
2268
|
|
|
2269
|
+
// The browser supplies its current JWT only to prove ownership upstream.
|
|
2270
|
+
// Uploads use this server-held binding, never a caller's opaque device token.
|
|
2271
|
+
if (p === "/functions/tokentracker-cloud-session") {
|
|
2272
|
+
if (String(req.method).toUpperCase() !== "POST") { json(res, { error: "Method Not Allowed" }, 405); return true; }
|
|
2273
|
+
if (!isAuthorizedLocalMutation(req)) { json(res, { error: "Unauthorized" }, 401); return true; }
|
|
2274
|
+
try {
|
|
2275
|
+
const body = await readJsonBody(req);
|
|
2276
|
+
assertInsforgeRuntime(getRuntime());
|
|
2277
|
+
if (body.clear === true) {
|
|
2278
|
+
if (body.session_id && body.session_id === localCloudSession?.id) clearLocalSyncDeviceTokenCache();
|
|
2279
|
+
json(res, { ok: true }); return true;
|
|
2280
|
+
}
|
|
2281
|
+
if (!getCloudSyncPref()) { json(res, { code: "CLOUD_SYNC_DISABLED" }, 409); return true; }
|
|
2282
|
+
const accessToken = String(req.headers.authorization || "").replace(/^Bearer /i, "").trim();
|
|
2283
|
+
const ownerId = typeof body.expectedOwnerId === "string" ? body.expectedOwnerId.trim() : "";
|
|
2284
|
+
if (!accessToken || !ownerId) { json(res, { code: "auth_required" }, 401); return true; }
|
|
2285
|
+
const baseUrl = resolveAllowedInsforgeBaseUrl(body.insforgeBaseUrl || getRuntime().baseUrl, getRuntime());
|
|
2286
|
+
if (!baseUrl) { json(res, { error: "Unsupported insforgeBaseUrl override" }, 400); return true; }
|
|
2287
|
+
clearLocalSyncDeviceTokenCache();
|
|
2288
|
+
const generation = localCloudGeneration;
|
|
2289
|
+
await Promise.all([...activeCloudSyncs].map(controller => controller.done).filter(Boolean));
|
|
2290
|
+
const issued = await issueDeviceTokenForLocalSync(qp, { baseUrl, accessToken, expectedOwnerId: ownerId, returnSession: true });
|
|
2291
|
+
if (generation !== localCloudGeneration) { json(res, { code: "auth_session_changed" }, 409); return true; }
|
|
2292
|
+
if (!issued?.token || !issued.deviceId) { json(res, { code: "auth_rejected" }, 401); return true; }
|
|
2293
|
+
localCloudSession = { id: crypto.randomUUID(), ownerId, baseUrl, generation, token: issued.token };
|
|
2294
|
+
json(res, { session_id: localCloudSession.id, device_id: issued.deviceId, created_at: issued.issuedAt });
|
|
2295
|
+
} catch (error) { json(res, { error: error.message, code: error.code || null, recovery_url: error.recoveryUrl || null }, error.status || 502); }
|
|
2296
|
+
return true;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2137
2299
|
// --- local-sync (POST) ---
|
|
2138
2300
|
if (p === "/functions/tokentracker-local-sync") {
|
|
2139
2301
|
if (String(req.method || "GET").toUpperCase() !== "POST") {
|
|
@@ -2151,8 +2313,15 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
2151
2313
|
} catch {
|
|
2152
2314
|
body = {};
|
|
2153
2315
|
}
|
|
2154
|
-
const
|
|
2316
|
+
const currentRuntime = getRuntime();
|
|
2317
|
+
if (body.drain === true || body.cloudSessionId || body.insforgeBaseUrl || body.expectedOwnerId) assertInsforgeRuntime(currentRuntime);
|
|
2318
|
+
const extraEnv = { TOKENTRACKER_CLOUD_RECHECK: "0", TOKENTRACKER_INSFORGE_BASE_URL: currentRuntime.baseUrl, TOKENTRACKER_INSFORGE_ANON_KEY: currentRuntime.anonKey || "" };
|
|
2155
2319
|
const drain = body.drain === true;
|
|
2320
|
+
// A user action after membership/resume may probe the server again.
|
|
2321
|
+
// The server still enforces membership, paused slots and its deadline.
|
|
2322
|
+
if (drain && body.recheckCloudAccess === true && body.auto !== true) {
|
|
2323
|
+
extraEnv.TOKENTRACKER_CLOUD_RECHECK = "1";
|
|
2324
|
+
}
|
|
2156
2325
|
// Native Sync Now combines an incremental background scan with a full
|
|
2157
2326
|
// cloud drain. A plain {drain:true} request remains an exhaustive sync.
|
|
2158
2327
|
const auto = body.auto === true;
|
|
@@ -2167,16 +2336,20 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
2167
2336
|
if (background && body.nativeOnlyWsl === true) {
|
|
2168
2337
|
extraEnv.TOKENTRACKER_WSL_MODE = "native-only";
|
|
2169
2338
|
}
|
|
2170
|
-
if (
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2339
|
+
if (body.deviceToken != null && !background) {
|
|
2340
|
+
json(res, { ok: false, error: "Use the current cloud session", code: getCloudSyncPref() ? "auth_session_required" : "CLOUD_SYNC_DISABLED" }, 409);
|
|
2341
|
+
return true;
|
|
2342
|
+
}
|
|
2343
|
+
const browserSession = body.cloudSessionId ? localCloudSession : null;
|
|
2344
|
+
if (body.cloudSessionId === "" || (body.expectedOwnerId && !body.cloudSessionId)) { json(res, { ok: false, code: "auth_session_required" }, 409); return true; }
|
|
2345
|
+
if (body.cloudSessionId && (!browserSession || body.cloudSessionId !== browserSession.id ||
|
|
2346
|
+
body.expectedOwnerId !== browserSession.ownerId || browserSession.generation !== localCloudGeneration || !getCloudSyncPref())) {
|
|
2347
|
+
json(res, { ok: false, code: "auth_session_changed" }, 409); return true;
|
|
2176
2348
|
}
|
|
2349
|
+
if (browserSession) extraEnv.TOKENTRACKER_DEVICE_TOKEN = browserSession.token;
|
|
2177
2350
|
let localSyncBaseUrl = null;
|
|
2178
2351
|
if (body.insforgeBaseUrl != null) {
|
|
2179
|
-
const allowedBaseUrl = resolveAllowedInsforgeBaseUrl(body.insforgeBaseUrl);
|
|
2352
|
+
const allowedBaseUrl = resolveAllowedInsforgeBaseUrl(body.insforgeBaseUrl, getRuntime());
|
|
2180
2353
|
if (!allowedBaseUrl) {
|
|
2181
2354
|
json(res, { ok: false, error: "Unsupported insforgeBaseUrl override" }, 400);
|
|
2182
2355
|
return true;
|
|
@@ -2184,6 +2357,11 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
2184
2357
|
extraEnv.TOKENTRACKER_INSFORGE_BASE_URL = allowedBaseUrl;
|
|
2185
2358
|
localSyncBaseUrl = allowedBaseUrl;
|
|
2186
2359
|
}
|
|
2360
|
+
if (browserSession) {
|
|
2361
|
+
extraEnv.TOKENTRACKER_INSFORGE_BASE_URL = browserSession.baseUrl;
|
|
2362
|
+
localSyncBaseUrl = browserSession.baseUrl;
|
|
2363
|
+
}
|
|
2364
|
+
const uploadGeneration = localCloudGeneration;
|
|
2187
2365
|
if ((!background || publishAccount) &&
|
|
2188
2366
|
!extraEnv.TOKENTRACKER_DEVICE_TOKEN &&
|
|
2189
2367
|
getCloudSyncPref() &&
|
|
@@ -2192,9 +2370,10 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
2192
2370
|
const issuerSessionAtStart = getCloudAccountSessionGeneration();
|
|
2193
2371
|
const issuerCloudPrefAtStart = cloudSyncPrefGeneration;
|
|
2194
2372
|
try {
|
|
2195
|
-
issuedToken = await issueDeviceTokenForLocalSync(qp, { baseUrl: localSyncBaseUrl });
|
|
2373
|
+
issuedToken = await issueDeviceTokenForLocalSync(qp, { baseUrl: localSyncBaseUrl, expectedOwnerId: localCloudSession?.ownerId });
|
|
2196
2374
|
} catch (e) {
|
|
2197
|
-
if (
|
|
2375
|
+
if (e?.code?.startsWith("cloud_") || e?.code === "auth_owner_mismatch") throw e;
|
|
2376
|
+
if (getRuntime().debug) {
|
|
2198
2377
|
console.warn("[LocalAPI] local sync device token issue failed:", e?.message || e);
|
|
2199
2378
|
}
|
|
2200
2379
|
}
|
|
@@ -2215,9 +2394,30 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
2215
2394
|
extraEnv.TOKENTRACKER_DEVICE_TOKEN = issuedToken;
|
|
2216
2395
|
}
|
|
2217
2396
|
}
|
|
2397
|
+
if (extraEnv.TOKENTRACKER_DEVICE_TOKEN && uploadGeneration !== localCloudGeneration) {
|
|
2398
|
+
json(res, { ok: false, code: "auth_session_changed" }, 409); return true;
|
|
2399
|
+
}
|
|
2400
|
+
await Promise.all([...activeCloudSyncs].filter(controller => controller.signal.aborted).map(controller => controller.done).filter(Boolean));
|
|
2401
|
+
if (extraEnv.TOKENTRACKER_DEVICE_TOKEN && uploadGeneration !== localCloudGeneration) throw Object.assign(new Error("Account session changed"), { code: "auth_session_changed", status: 409 });
|
|
2402
|
+
if (extraEnv.TOKENTRACKER_DEVICE_TOKEN && localSyncDeviceTokenUserId) {
|
|
2403
|
+
const binding = JSON.stringify({ ownerId: localSyncDeviceTokenUserId, baseUrl: localSyncBaseUrl || getRuntime().baseUrl });
|
|
2404
|
+
const ownerPath = path.join(path.dirname(qp), "cloud-upload-owner.json");
|
|
2405
|
+
let previous = "";
|
|
2406
|
+
try { previous = fs.readFileSync(ownerPath, "utf8"); } catch { /* an unbound legacy cursor cannot prove its previous owner */ }
|
|
2407
|
+
if (previous !== binding) {
|
|
2408
|
+
fs.writeFileSync(path.join(path.dirname(qp), "queue.state.json"), JSON.stringify({ offset: 0, updatedAt: new Date().toISOString() }), { mode: 0o600 });
|
|
2409
|
+
try { fs.unlinkSync(path.join(path.dirname(qp), "upload.throttle.json")); } catch { /* no cooldown yet */ }
|
|
2410
|
+
}
|
|
2411
|
+
fs.writeFileSync(ownerPath, binding, { mode: 0o600 });
|
|
2412
|
+
}
|
|
2413
|
+
const controller = new AbortController();
|
|
2414
|
+
if (extraEnv.TOKENTRACKER_DEVICE_TOKEN) activeCloudSyncs.add(controller);
|
|
2218
2415
|
const syncStartedAt = Date.now();
|
|
2219
2416
|
extraEnv.TOKENTRACKER_LOCAL_SYNC_ATTEMPT_ID = crypto.randomUUID();
|
|
2220
|
-
const
|
|
2417
|
+
const syncPromise = runSyncCommand(extraEnv, {
|
|
2418
|
+
signal: controller.signal,
|
|
2419
|
+
trackerDataDir: isolatedTrackerDataDir,
|
|
2420
|
+
syncContext,
|
|
2221
2421
|
drain,
|
|
2222
2422
|
auto,
|
|
2223
2423
|
background,
|
|
@@ -2225,11 +2425,15 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
2225
2425
|
allLocalSources,
|
|
2226
2426
|
waitForLock:
|
|
2227
2427
|
!drain && !background && Boolean(extraEnv.TOKENTRACKER_DEVICE_TOKEN),
|
|
2228
|
-
})
|
|
2428
|
+
});
|
|
2429
|
+
controller.done = syncPromise.catch(() => {});
|
|
2430
|
+
const result = await syncPromise.catch((error) => {
|
|
2431
|
+
if (uploadGeneration !== localCloudGeneration) throw Object.assign(new Error("Account session changed"), { code: "auth_session_changed", status: 409 });
|
|
2229
2432
|
const failure = currentCloudUploadFailure(syncStartedAt, { backoff: auto && drain, deviceToken: extraEnv.TOKENTRACKER_DEVICE_TOKEN, attemptId: extraEnv.TOKENTRACKER_LOCAL_SYNC_ATTEMPT_ID });
|
|
2230
2433
|
if (failure) Object.assign(error, failure);
|
|
2231
2434
|
throw error;
|
|
2232
|
-
});
|
|
2435
|
+
}).finally(() => activeCloudSyncs.delete(controller));
|
|
2436
|
+
if (extraEnv.TOKENTRACKER_DEVICE_TOKEN && uploadGeneration !== localCloudGeneration) throw Object.assign(new Error("Account session changed"), { code: "auth_session_changed", status: 409 });
|
|
2233
2437
|
const failure = currentCloudUploadFailure(syncStartedAt, { deviceToken: extraEnv.TOKENTRACKER_DEVICE_TOKEN, attemptId: extraEnv.TOKENTRACKER_LOCAL_SYNC_ATTEMPT_ID });
|
|
2234
2438
|
if (failure) throw Object.assign(new Error("Cloud upload failed"), failure);
|
|
2235
2439
|
invalidateAfterAccountUpload();
|
|
@@ -2240,9 +2444,18 @@ function createLocalApiHandler({ queuePath, serverVersion = null }) {
|
|
|
2240
2444
|
} catch (_e) {
|
|
2241
2445
|
// ignore if module load fails
|
|
2242
2446
|
}
|
|
2243
|
-
|
|
2447
|
+
let cloudAccess = { uploaded: false, pending: true };
|
|
2448
|
+
try {
|
|
2449
|
+
const state = JSON.parse(fs.readFileSync(path.join(path.dirname(qp), "upload.throttle.json"), "utf8"));
|
|
2450
|
+
if (state.nextAllowedAtMs) cloudAccess.next_allowed_at = new Date(state.nextAllowedAtMs).toISOString();
|
|
2451
|
+
cloudAccess.uploaded = state.lastSuccessAttemptId === extraEnv.TOKENTRACKER_LOCAL_SYNC_ATTEMPT_ID &&
|
|
2452
|
+
Number(state.lastSuccessMs || 0) >= syncStartedAt;
|
|
2453
|
+
const queueState = JSON.parse(fs.readFileSync(path.join(path.dirname(qp), "queue.state.json"), "utf8"));
|
|
2454
|
+
cloudAccess.pending = fs.statSync(qp).size > Number(queueState.offset || 0);
|
|
2455
|
+
} catch { /* no upload has been attempted yet */ }
|
|
2456
|
+
json(res, { ok: true, ...result, ...cloudAccess });
|
|
2244
2457
|
} catch (e) {
|
|
2245
|
-
json(res, { ok: false, error: e?.message, code: e?.code ?? null, stdout: e?.stdout || "", stderr: e?.stderr || "" }, e?.status || 500);
|
|
2458
|
+
json(res, { ok: false, error: e?.message, code: e?.code ?? null, recovery_url: e?.recoveryUrl ?? null, next_allowed_at: e?.nextAllowedAt ?? null, retry_after_seconds: e?.retryAfterMs ? Math.ceil(e.retryAfterMs / 1000) : null, stdout: e?.stdout || "", stderr: e?.stderr || "" }, e?.status || 500);
|
|
2246
2459
|
}
|
|
2247
2460
|
return true;
|
|
2248
2461
|
}
|