tokentracker-cli 0.23.2 → 0.24.1

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 (40) hide show
  1. package/dashboard/dist/assets/{Card-WqeqJCTJ.js → Card-B3zit51o.js} +1 -1
  2. package/dashboard/dist/assets/{DashboardPage-DfXV-T4E.js → DashboardPage-p0PukR6H.js} +1 -1
  3. package/dashboard/dist/assets/{DevicePage-CKQwUIFV.js → DevicePage-cYRFC4jD.js} +1 -1
  4. package/dashboard/dist/assets/{FadeIn-BvXHu5jo.js → FadeIn-BeLgcj1F.js} +1 -1
  5. package/dashboard/dist/assets/{HeaderGithubStar-BYgV73_y.js → HeaderGithubStar-DJJ-z5Wo.js} +1 -1
  6. package/dashboard/dist/assets/IpCheckPage-CxswyJC6.js +15 -0
  7. package/dashboard/dist/assets/{LandingPage-Dymt6fsi.js → LandingPage-Bc1ahbkE.js} +1 -1
  8. package/dashboard/dist/assets/{LeaderboardPage-BWJX1oGI.js → LeaderboardPage-khmZdEvK.js} +1 -1
  9. package/dashboard/dist/assets/{LeaderboardProfilePage-DmNl8aoL.js → LeaderboardProfilePage-NdMct8UA.js} +1 -1
  10. package/dashboard/dist/assets/{LimitsPage-D_q9Z5nB.js → LimitsPage-Dc-Rgfn2.js} +1 -1
  11. package/dashboard/dist/assets/{LoginPage-BvwX7KJg.js → LoginPage-nhgDDL4s.js} +1 -1
  12. package/dashboard/dist/assets/{PopoverPopup-CrBQ_8vY.js → PopoverPopup-D7c8Tufb.js} +1 -1
  13. package/dashboard/dist/assets/{ProviderIcon-Cb-YMfcK.js → ProviderIcon-B79ytQj1.js} +1 -1
  14. package/dashboard/dist/assets/{SettingsPage-u30V0Mqy.js → SettingsPage-Dv5A3ADR.js} +1 -1
  15. package/dashboard/dist/assets/{SkillsPage-ssktZGFE.js → SkillsPage-CJdMytgb.js} +1 -1
  16. package/dashboard/dist/assets/{WidgetsPage-_geExCIR.js → WidgetsPage-PteVAvZa.js} +1 -1
  17. package/dashboard/dist/assets/{WrappedPage-C5Wcs8L4.js → WrappedPage-r7rchUJS.js} +1 -1
  18. package/dashboard/dist/assets/check-CQ9GtDMA.js +1 -0
  19. package/dashboard/dist/assets/{chevron-down-BbPjwLP4.js → chevron-down-CNvz7vRS.js} +1 -1
  20. package/dashboard/dist/assets/{download-iIollDJi.js → download-HiydOOCL.js} +1 -1
  21. package/dashboard/dist/assets/{info-DjS23Duh.js → info-CWqFjI4Z.js} +1 -1
  22. package/dashboard/dist/assets/leaderboard-columns-BWyNquUf.js +1 -0
  23. package/dashboard/dist/assets/{main-Cb6vMAZa.js → main-C1bPtjN7.js} +142 -17
  24. package/dashboard/dist/assets/main-JP_EYeq-.css +1 -0
  25. package/dashboard/dist/assets/{use-limits-display-prefs-DZ9hgvef.js → use-limits-display-prefs-CTnU4ZMo.js} +1 -1
  26. package/dashboard/dist/assets/{use-native-settings-DS1TfSil.js → use-native-settings-DkTJDNZG.js} +1 -1
  27. package/dashboard/dist/assets/{use-reduced-motion-D82galUK.js → use-reduced-motion-DJE4Frme.js} +1 -1
  28. package/dashboard/dist/assets/{use-usage-limits-B-svr8YY.js → use-usage-limits-OjbraegI.js} +1 -1
  29. package/dashboard/dist/assets/{useCurrency-uA9QPua4.js → useCurrency-DJA7tRVi.js} +1 -1
  30. package/dashboard/dist/index.html +2 -2
  31. package/dashboard/dist/share.html +2 -2
  32. package/package.json +1 -1
  33. package/src/cli.js +1 -2
  34. package/src/commands/serve.js +2 -57
  35. package/src/lib/local-api.js +45 -79
  36. package/src/lib/pricing/seed-snapshot.json +1 -1
  37. package/dashboard/dist/assets/IpCheckPage-BjuJDGa8.js +0 -1
  38. package/dashboard/dist/assets/check-Cu-lm-wz.js +0 -1
  39. package/dashboard/dist/assets/leaderboard-columns-DTnRH3EZ.js +0 -1
  40. package/dashboard/dist/assets/main-0bSXJNLn.css +0 -1
@@ -457,45 +457,13 @@ function isLoopbackHostname(hostname) {
457
457
  return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1" || hostname === "[::1]";
458
458
  }
459
459
 
460
- function normalizeAllowedHost(value) {
461
- if (typeof value !== "string") return null;
462
- const raw = value.trim();
463
- if (!raw || raw.includes("*") || /\s/.test(raw)) return null;
464
- try {
465
- const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `http://${raw}`;
466
- const url = new URL(withScheme);
467
- if (!url.hostname || url.username || url.password) return null;
468
- return url.hostname.toLowerCase();
469
- } catch (_e) {
470
- return null;
471
- }
472
- }
473
-
474
- function normalizeAllowedHosts(values) {
475
- const raw = Array.isArray(values) ? values : [values];
476
- const out = [];
477
- const seen = new Set();
478
- for (const item of raw) {
479
- const host = normalizeAllowedHost(item);
480
- if (!host || seen.has(host)) continue;
481
- seen.add(host);
482
- out.push(host);
483
- }
484
- return out;
485
- }
486
-
487
- function hasAllowedLoopbackOrigin(headers = {}, allowedHosts = []) {
488
- const allowed = new Set(normalizeAllowedHosts(allowedHosts));
460
+ function hasAllowedLoopbackOrigin(headers = {}) {
489
461
  const candidates = [headers.origin, headers.referer];
490
462
  for (const raw of candidates) {
491
463
  if (raw == null || raw === "") continue;
492
464
  try {
493
465
  const url = new URL(String(raw));
494
- if (!["http:", "https:"].includes(url.protocol)) return false;
495
- const host = url.hostname.toLowerCase();
496
- if (url.protocol === "http:" && isLoopbackHostname(host)) continue;
497
- if (allowed.has(host)) continue;
498
- return false;
466
+ if (url.protocol !== "http:" || !isLoopbackHostname(url.hostname)) return false;
499
467
  } catch (_e) {
500
468
  return false;
501
469
  }
@@ -673,36 +641,24 @@ function json(res, data, status) {
673
641
  }
674
642
 
675
643
  // ---------------------------------------------------------------------------
676
- // IP check proxy (issue #81): ip.net.coffee/claude/ sets X-Frame-Options:
677
- // SAMEORIGIN so the dashboard iframe is blocked. We reverse-proxy through
678
- // /proxy/ipcheck/* so requests stay same-origin to 127.0.0.1, then strip the
679
- // embedding-hostile headers and rewrite root-relative URLs in HTML so the
680
- // page's own /api/* and /claude/* fetches route back through us.
644
+ // IP check API proxy: dashboard/src/pages/IpCheckPage.jsx is a native React
645
+ // page that calls ip.net.coffee's data endpoints (/api/iprisk, /api/geoip,
646
+ // /api/dns/result, /favicons, /claude/status.json). Browser-side fetch can't
647
+ // hit them cross-origin from the dashboard, so we reverse-proxy /proxy/ipcheck/*
648
+ // to https://ip.net.coffee/* and strip embedding-hostile headers.
649
+ // (Previously this proxy also served the upstream HTML page for an iframe;
650
+ // the iframe and its HTML-rewrite path have been removed.)
681
651
  // ---------------------------------------------------------------------------
682
652
 
683
653
  const IP_CHECK_PROXY_PREFIX = "/proxy/ipcheck";
684
654
  const IP_CHECK_TARGET = "https://ip.net.coffee";
685
655
 
686
- function rewriteIpCheckHtml(html) {
687
- const prefix = IP_CHECK_PROXY_PREFIX;
688
- return html
689
- .replace(
690
- /(\s(?:href|src|action)\s*=\s*["'])\/(?!\/|proxy\/ipcheck\/)/g,
691
- `$1${prefix}/`,
692
- )
693
- .replace(
694
- /(fetch\s*\(\s*["'`])\/(?!\/|proxy\/ipcheck\/)/g,
695
- `$1${prefix}/`,
696
- );
697
- }
698
-
699
656
  // ---------------------------------------------------------------------------
700
657
  // Main handler factory
701
658
  // ---------------------------------------------------------------------------
702
659
 
703
- function createLocalApiHandler({ queuePath, allowedHosts = [] } = {}) {
660
+ function createLocalApiHandler({ queuePath }) {
704
661
  const qp = queuePath || resolveQueuePath();
705
- const allowedLocalOriginHosts = normalizeAllowedHosts(allowedHosts);
706
662
 
707
663
  // Server-side cookie relay: captures auth cookies from InsForge cloud responses
708
664
  // so that both browser and WKWebView share the same login session via the proxy.
@@ -874,7 +830,7 @@ function createLocalApiHandler({ queuePath, allowedHosts = [] } = {}) {
874
830
  ? headerToken.trim()
875
831
  : cookieToken || "";
876
832
  if (!token || token !== localAuthToken) return false;
877
- return hasAllowedLoopbackOrigin(req?.headers || {}, allowedLocalOriginHosts);
833
+ return hasAllowedLoopbackOrigin(req?.headers || {});
878
834
  }
879
835
 
880
836
  return async function handleLocalApi(req, res, url) {
@@ -1015,34 +971,49 @@ function createLocalApiHandler({ queuePath, allowedHosts = [] } = {}) {
1015
971
  return true;
1016
972
  }
1017
973
 
1018
- // --- ip-check proxy: reverse-proxy ip.net.coffee/claude/ (issue #81) ---
974
+ // --- ip-check proxy: reverse-proxy ip.net.coffee (issue #81) ---
975
+ // Lock-down: GET/HEAD only, restricted path prefixes, do not forward
976
+ // browser credentials or fingerprintable headers. Without these limits
977
+ // /proxy/ipcheck is an open reverse-proxy any local process can abuse
978
+ // (exfiltrate dashboard cookies, anonymously POST through user IP).
1019
979
  if (p.startsWith(`${IP_CHECK_PROXY_PREFIX}/`) || p === IP_CHECK_PROXY_PREFIX) {
980
+ const method = String(req.method || "GET").toUpperCase();
981
+ if (method !== "GET" && method !== "HEAD") {
982
+ json(res, { error: "Method Not Allowed" }, 405);
983
+ return true;
984
+ }
1020
985
  const targetPath = p === IP_CHECK_PROXY_PREFIX
1021
986
  ? "/"
1022
987
  : p.slice(IP_CHECK_PROXY_PREFIX.length) || "/";
988
+ const ALLOWED_PREFIXES = [
989
+ "/api/geoip/",
990
+ "/api/geoip-batch",
991
+ "/api/iprisk/",
992
+ "/api/dns/result/",
993
+ "/claude/status.json",
994
+ "/favicons/",
995
+ "/ip/",
996
+ ];
997
+ if (!ALLOWED_PREFIXES.some((prefix) => targetPath.startsWith(prefix))) {
998
+ json(res, { error: "Path not allowed" }, 403);
999
+ return true;
1000
+ }
1023
1001
  const targetUrl = `${IP_CHECK_TARGET}${targetPath}${url.search || ""}`;
1024
1002
  try {
1025
- const proxyHeaders = {};
1026
- for (const [key, value] of Object.entries(req.headers)) {
1027
- const lk = key.toLowerCase();
1028
- if (["host", "connection", "referer", "origin"].includes(lk)) continue;
1029
- proxyHeaders[key] = value;
1030
- }
1031
- proxyHeaders["host"] = "ip.net.coffee";
1032
- proxyHeaders["referer"] = `${IP_CHECK_TARGET}${targetPath}`;
1033
-
1034
- const method = String(req.method || "GET").toUpperCase();
1035
- let body;
1036
- if (method !== "GET" && method !== "HEAD") {
1037
- const chunks = [];
1038
- for await (const chunk of req) chunks.push(chunk);
1039
- if (chunks.length > 0) body = Buffer.concat(chunks);
1040
- }
1003
+ // Whitelist forwarded headers — no cookies, no auth, no fingerprintable
1004
+ // identity. Only what the upstream needs to negotiate content.
1005
+ const proxyHeaders = {
1006
+ host: "ip.net.coffee",
1007
+ accept: req.headers["accept"] || "*/*",
1008
+ "accept-language": req.headers["accept-language"] || "en",
1009
+ "accept-encoding": req.headers["accept-encoding"] || "gzip",
1010
+ "user-agent": "TokenTracker/IPCheck (https://www.tokentracker.cc)",
1011
+ referer: `${IP_CHECK_TARGET}${targetPath}`,
1012
+ };
1041
1013
 
1042
1014
  const proxyRes = await fetch(targetUrl, {
1043
1015
  method,
1044
1016
  headers: proxyHeaders,
1045
- body,
1046
1017
  redirect: "manual",
1047
1018
  });
1048
1019
 
@@ -1061,12 +1032,7 @@ function createLocalApiHandler({ queuePath, allowedHosts = [] } = {}) {
1061
1032
  ([k]) => !stripped.has(k.toLowerCase()),
1062
1033
  );
1063
1034
 
1064
- const contentType = proxyRes.headers.get("content-type") || "";
1065
- let resBody = Buffer.from(await proxyRes.arrayBuffer());
1066
- if (contentType.toLowerCase().includes("text/html")) {
1067
- resBody = Buffer.from(rewriteIpCheckHtml(resBody.toString("utf8")), "utf8");
1068
- }
1069
-
1035
+ const resBody = Buffer.from(await proxyRes.arrayBuffer());
1070
1036
  res.writeHead(proxyRes.status, Object.fromEntries(responseHeaders));
1071
1037
  res.end(resBody);
1072
1038
  } catch (e) {