@ipv9/tokentracker-cli 0.39.38 → 0.39.40

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 (80) hide show
  1. package/README.md +43 -11
  2. package/dashboard/dist/assets/{Card-DD4s0V6T.js → Card-JS4gjUZf.js} +1 -1
  3. package/dashboard/dist/assets/DashboardPage-BoXhzJb1.js +60 -0
  4. package/dashboard/dist/assets/{FadeIn-CUSBK0jm.js → FadeIn-DvLfvwys.js} +1 -1
  5. package/dashboard/dist/assets/{IpCheckPage-PHF4lPax.js → IpCheckPage-Dcfc0Uts.js} +1 -1
  6. package/dashboard/dist/assets/LimitsPage-Vz-GVdi9.js +2 -0
  7. package/dashboard/dist/assets/{LocalOnlyNotice-DCmtS_j5.js → LocalOnlyNotice-8xF5couh.js} +1 -1
  8. package/dashboard/dist/assets/PopoverPopup-7HrUzeRx.js +1 -0
  9. package/dashboard/dist/assets/Select-9XDsaO_C.js +1 -0
  10. package/dashboard/dist/assets/SelectItemText-DE5Fs9p6.js +12 -0
  11. package/dashboard/dist/assets/SettingsPage-CMnfAptE.js +1 -0
  12. package/dashboard/dist/assets/SkillsPage-CbvLHN0L.js +1 -0
  13. package/dashboard/dist/assets/WidgetsPage-CeGQlyt7.js +1 -0
  14. package/dashboard/dist/assets/WrappedPage-Bux8qLUC.js +1 -0
  15. package/dashboard/dist/assets/{arrow-up-right-CjaBQnwI.js → arrow-up-right-B7rCoZdW.js} +1 -1
  16. package/dashboard/dist/assets/{download-DLBqkSwp.js → download-CtTYpbcx.js} +1 -1
  17. package/dashboard/dist/assets/format-BuQKK6kQ.js +1 -0
  18. package/dashboard/dist/assets/limitDisplay-WwuTdS_a.js +1 -0
  19. package/dashboard/dist/assets/main-Ck4SUH4m.js +696 -0
  20. package/dashboard/dist/assets/main-ZrWkoMlr.css +1 -0
  21. package/dashboard/dist/assets/mock-data-Bvbe8tYK.js +1 -0
  22. package/dashboard/dist/assets/use-limits-display-prefs-BKWYPgeA.js +1 -0
  23. package/dashboard/dist/assets/use-native-settings-D7Nj1dXR.js +1 -0
  24. package/dashboard/dist/assets/useCurrency-BX3pvyhh.js +1 -0
  25. package/dashboard/dist/index.html +15 -27
  26. package/dashboard/dist/llms.txt +0 -2
  27. package/dashboard/dist/sitemap.xml +0 -12
  28. package/package.json +7 -4
  29. package/src/cli.js +4 -13
  30. package/src/commands/doctor.js +1 -3
  31. package/src/commands/init.js +63 -53
  32. package/src/commands/serve.js +2 -50
  33. package/src/commands/status.js +1 -55
  34. package/src/commands/sync.js +134 -227
  35. package/src/lib/{browser-auth.js → browser-open.js} +3 -149
  36. package/src/lib/claude-categorizer.js +204 -32
  37. package/src/lib/codex-context-breakdown.js +83 -7
  38. package/src/lib/codex-rollout-parser.js +52 -10
  39. package/src/lib/diagnostics.js +1 -49
  40. package/src/lib/doctor.js +12 -87
  41. package/src/lib/file-identity.js +69 -0
  42. package/src/lib/init-flow.js +6 -3
  43. package/src/lib/local-api.js +6 -364
  44. package/src/lib/pricing/curated-overrides.json +1 -1
  45. package/src/lib/pricing/seed-snapshot.json +1 -1
  46. package/src/lib/rollout.js +392 -144
  47. package/src/lib/runtime-config.js +0 -32
  48. package/src/lib/sqlite-reader.js +83 -8
  49. package/dashboard/dist/assets/ActivityHeatmap-BrehqUIb.js +0 -42
  50. package/dashboard/dist/assets/DashboardPage-CyUShBRJ.js +0 -19
  51. package/dashboard/dist/assets/DevicePage-BA74Su1l.js +0 -1
  52. package/dashboard/dist/assets/DialogTitle-DoKsz5wi.js +0 -1
  53. package/dashboard/dist/assets/InsforgeUserHeaderControls-Bk_U2dHr.js +0 -1
  54. package/dashboard/dist/assets/LandingPage-Bbeu84Iu.js +0 -4356
  55. package/dashboard/dist/assets/LeaderboardAvatar-ByqeB6bM.js +0 -1
  56. package/dashboard/dist/assets/LeaderboardPage-BeWXF00S.js +0 -6
  57. package/dashboard/dist/assets/LeaderboardProfileModal-C4yvbZd_.js +0 -72
  58. package/dashboard/dist/assets/LeaderboardProfilePage-D3cbSh5o.js +0 -1
  59. package/dashboard/dist/assets/LimitsPage-dS9C9Vdt.js +0 -2
  60. package/dashboard/dist/assets/LoginPage-Ch3UQoWj.js +0 -1
  61. package/dashboard/dist/assets/PopoverPopup-DjG0gjlA.js +0 -1
  62. package/dashboard/dist/assets/Select-HzIjWJpE.js +0 -1
  63. package/dashboard/dist/assets/SelectItemText-f0tCwtvZ.js +0 -1
  64. package/dashboard/dist/assets/SettingsPage-DJOZAlZy.js +0 -1
  65. package/dashboard/dist/assets/SkillsPage-BQcyJIdx.js +0 -1
  66. package/dashboard/dist/assets/WidgetsPage-B_vxuV09.js +0 -1
  67. package/dashboard/dist/assets/WrappedPage-CZN9Y3Vz.js +0 -1
  68. package/dashboard/dist/assets/index-BeoRn2gJ.js +0 -2
  69. package/dashboard/dist/assets/info-BAIxlHIB.js +0 -1
  70. package/dashboard/dist/assets/main-DAcSUW2t.css +0 -1
  71. package/dashboard/dist/assets/main-pi07CfeC.js +0 -1004
  72. package/dashboard/dist/assets/use-limits-display-prefs-CMfASZsh.js +0 -1
  73. package/dashboard/dist/assets/use-native-settings-CfPik-Ao.js +0 -1
  74. package/dashboard/dist/assets/use-spring-wsmJQRqb.js +0 -1
  75. package/dashboard/dist/assets/use-usage-limits-Bt0Dp6kx.js +0 -1
  76. package/dashboard/dist/assets/useCurrency-CsaAMc3j.js +0 -1
  77. package/dashboard/dist/assets/useOpenInteractionType-CPzi5Z8M.js +0 -12
  78. package/dashboard/dist/share.html +0 -140
  79. package/src/commands/device-login.js +0 -161
  80. package/src/lib/upload-throttle.js +0 -148
package/src/lib/doctor.js CHANGED
@@ -29,7 +29,8 @@ async function buildDoctorReport({
29
29
  checks.push(await checkCliEntrypoint(paths.cliPath));
30
30
  }
31
31
 
32
- checks.push(await checkNetwork({ baseUrl: runtime?.baseUrl || null, fetch }));
32
+ // No cloud reachability check: TokenTracker is local-only, so there is no
33
+ // remote endpoint whose availability could affect anything here.
33
34
 
34
35
  if (diagnostics) {
35
36
  checks.push(...buildDiagnosticsChecks(diagnostics));
@@ -142,10 +143,6 @@ async function commandExistsOnPath(command, env = process.env) {
142
143
 
143
144
  function buildRuntimeChecks(runtime = {}) {
144
145
  const checks = [];
145
- const baseUrl =
146
- typeof runtime.baseUrl === "string" && runtime.baseUrl.trim() ? runtime.baseUrl.trim() : null;
147
- const deviceToken =
148
- typeof runtime.deviceToken === "string" && runtime.deviceToken.trim() ? "set" : "unset";
149
146
  const dashboardUrl =
150
147
  typeof runtime.dashboardUrl === "string" && runtime.dashboardUrl.trim()
151
148
  ? runtime.dashboardUrl.trim()
@@ -154,29 +151,9 @@ function buildRuntimeChecks(runtime = {}) {
154
151
  ? Number(runtime.httpTimeoutMs)
155
152
  : null;
156
153
  const debug = Boolean(runtime.debug);
157
- const autoRetryNoSpawn = Boolean(runtime.autoRetryNoSpawn);
158
154
 
159
- checks.push({
160
- id: "runtime.base_url",
161
- status: baseUrl ? "ok" : "fail",
162
- detail: baseUrl ? "base_url set" : "base_url missing",
163
- critical: false,
164
- meta: {
165
- base_url: baseUrl,
166
- source: runtime?.sources?.baseUrl || null,
167
- },
168
- });
169
-
170
- checks.push({
171
- id: "runtime.device_token",
172
- status: deviceToken === "set" ? "ok" : "warn",
173
- detail: deviceToken === "set" ? "device token set" : "device token missing",
174
- critical: false,
175
- meta: {
176
- device_token: deviceToken,
177
- source: runtime?.sources?.deviceToken || null,
178
- },
179
- });
155
+ // base URL / device token checks removed: local-only, there is no remote
156
+ // endpoint to point at and no credential to hold.
180
157
 
181
158
  checks.push({
182
159
  id: "runtime.dashboard_url",
@@ -211,16 +188,11 @@ function buildRuntimeChecks(runtime = {}) {
211
188
  },
212
189
  });
213
190
 
214
- checks.push({
215
- id: "runtime.auto_retry_no_spawn",
216
- status: "ok",
217
- detail: autoRetryNoSpawn ? "auto retry spawn disabled" : "auto retry spawn enabled",
218
- critical: false,
219
- meta: {
220
- auto_retry_no_spawn: autoRetryNoSpawn,
221
- source: runtime?.sources?.autoRetryNoSpawn || null,
222
- },
223
- });
191
+ // runtime.auto_retry_no_spawn removed: scheduleAutoRetry and
192
+ // spawnAutoRetryProcess went with cloud upload, so this reported on a spawn
193
+ // path that cannot happen — and told anyone setting the env var that their
194
+ // opt-out had taken effect on nothing.
195
+
224
196
 
225
197
  return checks;
226
198
  }
@@ -346,48 +318,6 @@ async function checkCliEntrypoint(cliPath) {
346
318
  }
347
319
  }
348
320
 
349
- async function checkNetwork({ baseUrl, fetch }) {
350
- if (!baseUrl) {
351
- return {
352
- id: "network.base_url",
353
- status: "warn",
354
- detail: "base_url missing (skipped)",
355
- critical: false,
356
- meta: { base_url: null },
357
- };
358
- }
359
-
360
- const start = Date.now();
361
- try {
362
- if (typeof fetch !== "function") throw new Error("Missing fetch");
363
- const res = await fetch(baseUrl, { method: "GET" });
364
- const latency = Date.now() - start;
365
- return {
366
- id: "network.base_url",
367
- status: "ok",
368
- detail: `HTTP ${res.status} (reachable)`,
369
- critical: false,
370
- meta: {
371
- status_code: res.status,
372
- latency_ms: latency,
373
- base_url: baseUrl,
374
- },
375
- };
376
- } catch (err) {
377
- const latency = Date.now() - start;
378
- return {
379
- id: "network.base_url",
380
- status: "fail",
381
- detail: "Network error",
382
- critical: false,
383
- meta: {
384
- error: err?.message || String(err),
385
- latency_ms: latency,
386
- base_url: baseUrl,
387
- },
388
- };
389
- }
390
- }
391
321
 
392
322
  function buildDiagnosticsChecks(diagnostics) {
393
323
  const checks = [];
@@ -409,14 +339,9 @@ function buildDiagnosticsChecks(diagnostics) {
409
339
  meta: { configured: notifyConfigured },
410
340
  });
411
341
 
412
- const uploadError = diagnostics?.upload?.last_error || null;
413
- checks.push({
414
- id: "upload.last_error",
415
- status: uploadError ? "warn" : "ok",
416
- detail: uploadError ? "last upload error present" : "no upload errors",
417
- critical: false,
418
- meta: { last_error: uploadError ? uploadError.message || null : null },
419
- });
342
+ // The upload.last_error check is gone with cloud upload: `diagnostics.upload`
343
+ // no longer exists, so the check could only ever report "no upload errors" —
344
+ // a permanent green that says nothing.
420
345
 
421
346
  return checks;
422
347
  }
@@ -0,0 +1,69 @@
1
+ const crypto = require("node:crypto");
2
+ const fssync = require("node:fs");
3
+
4
+ // Fingerprint the first bytes of a file so rotation is detected even when the
5
+ // OS reuses the inode after unlink+recreate (Linux logrotate). Returns "" on
6
+ // any error so callers treat "no fingerprint" as "not changed". Also returns
7
+ // "" when the file is shorter than the fingerprint window: for a sub-256B
8
+ // file that is still being appended to, the window would extend into the
9
+ // not-yet-written append zone, so plain growth (not rotation) would shift
10
+ // the hash and produce a false "rotated" signal.
11
+ function readFileHeadSignature(filePath) {
12
+ try {
13
+ const fd = fssync.openSync(filePath, "r");
14
+ try {
15
+ const buf = Buffer.alloc(256);
16
+ const bytes = fssync.readSync(fd, buf, 0, 256, 0);
17
+ if (bytes < 256) return "";
18
+ return crypto.createHash("sha1").update(buf.subarray(0, bytes)).digest("hex");
19
+ } finally {
20
+ fssync.closeSync(fd);
21
+ }
22
+ } catch {
23
+ return "";
24
+ }
25
+ }
26
+
27
+ // A token that changes whenever a file's contents could have changed. Returns
28
+ // null when the path is not a readable regular file, which callers must treat
29
+ // as "cannot be reused" rather than "unchanged".
30
+ function fileIdentity(filePath) {
31
+ let st = null;
32
+ try {
33
+ st = fssync.statSync(filePath);
34
+ } catch {
35
+ return null;
36
+ }
37
+ if (!st.isFile()) return null;
38
+ return {
39
+ inode: st.ino || 0,
40
+ size: Number.isFinite(st.size) ? st.size : 0,
41
+ mtimeMs: Number.isFinite(st.mtimeMs) ? st.mtimeMs : 0,
42
+ head: readFileHeadSignature(filePath),
43
+ };
44
+ }
45
+
46
+ // Equality, not ordering. Deliberately not `cur.mtimeMs > prev.mtimeMs`: a
47
+ // clock that moves backwards, a restored mtime, or a truncation must all read
48
+ // as "changed" and force a re-read. The cost of a false "changed" is a wasted
49
+ // parse; the cost of a false "unchanged" is serving stale data forever.
50
+ function isUnchanged(prev, cur) {
51
+ if (!prev || !cur) return false;
52
+ // An empty head means the file was too short to fingerprint, so it carries
53
+ // no rotation signal — fall back to inode/size/mtime alone rather than
54
+ // treating "" as a mismatch.
55
+ const headChanged =
56
+ typeof prev.head === "string" && prev.head !== "" && prev.head !== cur.head;
57
+ return (
58
+ prev.inode === cur.inode &&
59
+ prev.size === cur.size &&
60
+ prev.mtimeMs === cur.mtimeMs &&
61
+ !headChanged
62
+ );
63
+ }
64
+
65
+ module.exports = {
66
+ readFileHeadSignature,
67
+ fileIdentity,
68
+ isUnchanged,
69
+ };
@@ -27,9 +27,12 @@ function renderAuthTransition({ authUrl, canAutoOpen }) {
27
27
  }
28
28
 
29
29
  function renderSuccessBox({ configPath, dashboardUrl }) {
30
- const identityLine = "Account linked.";
31
- const lines = ["You are all set!", "", identityLine, `Token saved to: ${configPath}`, ""];
32
- if (dashboardUrl) lines.push(`View your stats at: ${dashboardUrl}`);
30
+ // Local-only: there is no account and no remote dashboard. `dashboardUrl` is
31
+ // null on a clean install now that the cloud default is gone, so fall back to
32
+ // the address `serve` actually listens on — otherwise setup finishes without
33
+ // telling the user where to look.
34
+ const lines = ["You are all set!", "", `Config saved to: ${configPath}`, ""];
35
+ lines.push(`View your stats at: ${dashboardUrl || "http://localhost:7680"}`);
33
36
  lines.push("You can close this terminal window.");
34
37
  process.stdout.write(`${renderBox(lines)}\n`);
35
38
  }
@@ -3,7 +3,6 @@ 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");
7
6
  const {
8
7
  filterRowsByUsageScope,
9
8
  getSourceScope,
@@ -445,35 +444,7 @@ function trimOutput(value, max = 4000) {
445
444
  return t.length <= max ? t : t.slice(t.length - max);
446
445
  }
447
446
 
448
- function normalizeRemoteHttpBaseUrl(value) {
449
- if (typeof value !== "string") return null;
450
- const trimmed = value.trim();
451
- if (!trimmed) return null;
452
- try {
453
- const url = new URL(trimmed);
454
- if (url.protocol !== "http:" && url.protocol !== "https:") return null;
455
- url.username = "";
456
- url.password = "";
457
- url.hash = "";
458
- return url.toString().replace(/\/$/, "");
459
- } catch (_e) {
460
- return null;
461
- }
462
- }
463
447
 
464
- function resolveAllowedInsforgeBaseUrl(value) {
465
- const requested = normalizeRemoteHttpBaseUrl(value);
466
- if (!requested) return null;
467
-
468
- const runtime = resolveRuntimeConfig();
469
- const allowed = new Set(
470
- [runtime.baseUrl, DEFAULT_BASE_URL]
471
- .map((entry) => normalizeRemoteHttpBaseUrl(entry))
472
- .filter(Boolean),
473
- );
474
-
475
- return allowed.has(requested) ? requested : null;
476
- }
477
448
 
478
449
  function parseCookieHeader(value) {
479
450
  const out = new Map();
@@ -709,45 +680,6 @@ const HOP_BY_HOP_HEADERS = new Set([
709
680
  "trailers",
710
681
  ]);
711
682
 
712
- // Strip forbidden + hop-by-hop headers when forwarding an inbound request to
713
- // fetch(). Honours the Connection header's named-headers list (RFC 7230 §6.1)
714
- // so values like `Connection: keep-alive, x-custom` also drop x-custom.
715
- function buildProxyHeaders(headers) {
716
- const entries =
717
- headers && typeof headers.entries === "function"
718
- ? Array.from(headers.entries())
719
- : Object.entries(headers || {});
720
-
721
- const connectionNamed = new Set();
722
- const normalized = [];
723
- for (const [rawKey, rawValue] of entries) {
724
- if (rawValue == null) continue;
725
- const key = String(rawKey).toLowerCase();
726
- normalized.push([key, rawValue]);
727
- if (key === "connection") {
728
- const values = Array.isArray(rawValue) ? rawValue : [rawValue];
729
- for (const v of values) {
730
- String(v)
731
- .split(",")
732
- .map((part) => part.trim().toLowerCase())
733
- .filter(Boolean)
734
- .forEach((part) => connectionNamed.add(part));
735
- }
736
- }
737
- }
738
-
739
- const out = {};
740
- for (const [key, rawValue] of normalized) {
741
- if (HOP_BY_HOP_HEADERS.has(key) || connectionNamed.has(key)) continue;
742
- if (Array.isArray(rawValue)) {
743
- const joined = rawValue.filter((e) => e != null).map(String).join(", ");
744
- if (joined) out[key] = joined;
745
- continue;
746
- }
747
- out[key] = String(rawValue);
748
- }
749
- return out;
750
- }
751
683
 
752
684
  // ---------------------------------------------------------------------------
753
685
  // Main handler factory
@@ -756,169 +688,7 @@ function buildProxyHeaders(headers) {
756
688
  function createLocalApiHandler({ queuePath }) {
757
689
  const qp = queuePath || resolveQueuePath();
758
690
 
759
- // Server-side cookie relay: captures auth cookies from InsForge cloud responses
760
- // so that both browser and WKWebView share the same login session via the proxy.
761
- // Persisted to disk so cookies survive server restarts.
762
- const csrfRelayCookieName = "insforge_csrf_token";
763
- let relayCookies = new Map();
764
691
  const localAuthToken = crypto.randomBytes(24).toString("hex");
765
- const trackerDataDir = path.join(os.homedir(), ".tokentracker", "tracker");
766
- const cookiePath = path.join(trackerDataDir, "relay-cookies.json");
767
-
768
- // Load persisted cookies on startup
769
- try {
770
- if (!fs.existsSync(trackerDataDir)) fs.mkdirSync(trackerDataDir, { recursive: true });
771
- if (fs.existsSync(cookiePath)) {
772
- const content = fs.readFileSync(cookiePath, "utf8");
773
- const saved = JSON.parse(content);
774
- if (saved && typeof saved === "object") {
775
- let count = 0;
776
- for (const [k, v] of Object.entries(saved)) {
777
- relayCookies.set(k, v);
778
- count++;
779
- }
780
- if (count > 0) console.log(`[LocalAPI] Loaded ${count} relay cookies from ${cookiePath}`);
781
- }
782
- }
783
- } catch (e) {
784
- console.error("[LocalAPI] Failed to load relay cookies:", e.message);
785
- }
786
-
787
- function persistRelayCookies() {
788
- try {
789
- // Sticky semantics: never replace an existing on-disk session with an empty cookie map.
790
- if (relayCookies.size === 0) return;
791
-
792
- const json = JSON.stringify(Object.fromEntries(relayCookies));
793
- fs.writeFileSync(cookiePath, json, { encoding: "utf8", mode: 0o600 });
794
- } catch (e) {
795
- console.error("[LocalAPI] Failed to persist relay cookies:", e.message);
796
- }
797
- }
798
-
799
- function clearRelayCookies(reason) {
800
- if (relayCookies.size === 0) return;
801
- relayCookies.clear();
802
- try {
803
- if (fs.existsSync(cookiePath)) fs.unlinkSync(cookiePath);
804
- } catch (e) {
805
- console.error("[LocalAPI] Failed to clear relay cookies:", e.message);
806
- return;
807
- }
808
- if (reason) console.warn(`[LocalAPI] Cleared relay cookies: ${reason}`);
809
- }
810
-
811
- function captureSetCookies(headerValue) {
812
- if (!headerValue) return;
813
- const parts = headerValue.split(/,(?=\s*\w+=)/);
814
- let changed = false;
815
- for (const raw of parts) {
816
- const eqIdx = raw.indexOf("=");
817
- if (eqIdx < 1) continue;
818
- const name = raw.substring(0, eqIdx).trim();
819
- if (!name) continue;
820
-
821
- // Basic sticky logic: if it's a deletion cookie (Max-Age=0 or past date),
822
- // we only remove it if we have it.
823
- const lower = raw.toLowerCase();
824
- const isDeletion = lower.includes("max-age=0") || lower.includes("expires=thu, 01 jan 1970");
825
-
826
- if (isDeletion) {
827
- if (relayCookies.has(name)) {
828
- relayCookies.delete(name);
829
- changed = true;
830
- console.log(`[LocalAPI] Cookie deleted: ${name}`);
831
- }
832
- } else {
833
- const oldVal = relayCookies.get(name);
834
- if (oldVal !== raw.trim()) {
835
- relayCookies.set(name, raw.trim());
836
- changed = true;
837
- console.log(`[LocalAPI] Cookie captured: ${name}`);
838
- }
839
- }
840
- }
841
- if (changed) persistRelayCookies();
842
- }
843
-
844
- function getRelayCookieValue(name, { decode = false } = {}) {
845
- const raw = relayCookies.get(name);
846
- if (!raw || typeof raw !== "string") return "";
847
- const pair = raw.split(";")[0] || "";
848
- const eqIdx = pair.indexOf("=");
849
- if (eqIdx < 1) return "";
850
- const value = pair.substring(eqIdx + 1).trim();
851
- if (!decode) return value;
852
- try {
853
- return decodeURIComponent(value);
854
- } catch {
855
- return value;
856
- }
857
- }
858
-
859
- function captureAuthTokensFromBody(bodyBuffer, contentType) {
860
- if (!bodyBuffer || !String(contentType || "").toLowerCase().includes("application/json")) return;
861
- let parsed = null;
862
- try {
863
- parsed = JSON.parse(bodyBuffer.toString("utf8"));
864
- } catch {
865
- return;
866
- }
867
- let changed = false;
868
- const token = typeof parsed?.csrfToken === "string" ? parsed.csrfToken.trim() : "";
869
- if (token) {
870
- const cookie = `${csrfRelayCookieName}=${encodeURIComponent(token)}; Path=/; SameSite=Lax`;
871
- if (relayCookies.get(csrfRelayCookieName) !== cookie) {
872
- relayCookies.set(csrfRelayCookieName, cookie);
873
- changed = true;
874
- }
875
- }
876
- const refreshToken = typeof parsed?.refreshToken === "string" ? parsed.refreshToken.trim() : "";
877
- if (refreshToken) {
878
- const cookie = `insforge_refresh_token=${encodeURIComponent(refreshToken)}; Path=/; HttpOnly; SameSite=Lax`;
879
- if (relayCookies.get("insforge_refresh_token") !== cookie) {
880
- relayCookies.set("insforge_refresh_token", cookie);
881
- changed = true;
882
- }
883
- }
884
- if (changed) persistRelayCookies();
885
- }
886
-
887
- function normalizeCookieHeader(value) {
888
- if (Array.isArray(value)) return value.filter(Boolean).join("; ");
889
- return typeof value === "string" ? value : "";
890
- }
891
-
892
- function buildRelayCookieHeader(clientCookieHeader, { relayPrecedenceNames = [] } = {}) {
893
- const normalizedClientCookieHeader = normalizeCookieHeader(clientCookieHeader);
894
- if (relayCookies.size === 0) return normalizedClientCookieHeader;
895
- const relayPrecedence = new Set(relayPrecedenceNames);
896
- const clientPairs = new Map();
897
- if (normalizedClientCookieHeader) {
898
- for (const part of normalizedClientCookieHeader.split(";")) {
899
- const eqIdx = part.indexOf("=");
900
- if (eqIdx < 1) continue;
901
- const n = part.substring(0, eqIdx).trim();
902
- if (n) clientPairs.set(n, part.trim());
903
- }
904
- }
905
- // Merge relay cookies. Normal requests keep client precedence; refresh
906
- // recovery can opt relay cookies into precedence over stale WebView cookies.
907
- for (const [name, raw] of relayCookies) {
908
- if (clientPairs.has(name) && !relayPrecedence.has(name)) continue;
909
- const scIdx = raw.indexOf(";");
910
- const pair = scIdx > 0 ? raw.substring(0, scIdx).trim() : raw;
911
- clientPairs.set(name, pair);
912
- }
913
- return [...clientPairs.values()].join("; ");
914
- }
915
-
916
- // Ephemeral auth bridge: WebView sets a "native" flag before opening system browser
917
- // for OAuth. The callback page (in browser) checks this flag to decide whether to
918
- // relay the code back to the app or handle it as a normal web flow.
919
- let _nativeAuthPending = false;
920
- let _nativeAuthExpiry = 0;
921
-
922
692
  function isAuthorizedLocalMutation(req) {
923
693
  const headerToken = req?.headers?.["x-tokentracker-local-auth"];
924
694
  const cookieToken = parseCookieHeader(req?.headers?.cookie).get("tokentracker_local_auth");
@@ -945,124 +715,6 @@ function createLocalApiHandler({ queuePath }) {
945
715
  return true;
946
716
  }
947
717
 
948
- // --- Auth bridge: native OAuth flag (WebView ↔ system browser) ---
949
- if (p === "/api/auth-bridge/verifier") {
950
- const method = String(req.method || "GET").toUpperCase();
951
- if (method === "PUT" || method === "POST") {
952
- if (!isAuthorizedLocalMutation(req)) {
953
- json(res, { error: "Unauthorized" }, 401);
954
- return true;
955
- }
956
- const body = await readJsonBody(req);
957
- _nativeAuthPending = Boolean(body?.native);
958
- _nativeAuthExpiry = Date.now() + 5 * 60 * 1000; // 5 min TTL
959
- json(res, { ok: true });
960
- return true;
961
- }
962
- if (method === "GET") {
963
- const isNative = _nativeAuthPending && Date.now() < _nativeAuthExpiry;
964
- _nativeAuthPending = false; // one-time read
965
- _nativeAuthExpiry = 0;
966
- json(res, { native: isNative });
967
- return true;
968
- }
969
- json(res, { error: "Method Not Allowed" }, 405);
970
- return true;
971
- }
972
-
973
- // --- auth proxy: forward /api/auth/* to InsForge cloud ---
974
- if (p.startsWith("/api/auth/")) {
975
- const runtime = resolveRuntimeConfig();
976
- const insforgeBase = runtime.baseUrl || DEFAULT_BASE_URL;
977
- try {
978
- const targetUrl = `${insforgeBase.replace(/\/$/, "")}${p}${url.search || ""}`;
979
- const proxyHeaders = buildProxyHeaders(req.headers);
980
- const hasClientCookie = normalizeCookieHeader(proxyHeaders["cookie"]).trim().length > 0;
981
- const hasCsrfHeader = typeof proxyHeaders["x-csrf-token"] === "string" && proxyHeaders["x-csrf-token"].trim().length > 0;
982
- const relayCsrfToken = getRelayCookieValue(csrfRelayCookieName);
983
- if (p === "/api/auth/refresh" && relayCsrfToken) {
984
- proxyHeaders["x-csrf-token"] = relayCsrfToken;
985
- }
986
- const hasEffectiveCsrfHeader =
987
- hasCsrfHeader ||
988
- (typeof proxyHeaders["x-csrf-token"] === "string" && proxyHeaders["x-csrf-token"].trim().length > 0);
989
- let shouldInjectRelayCookies =
990
- p !== "/api/auth/refresh" || hasClientCookie || hasEffectiveCsrfHeader;
991
- const relayRefreshToken = getRelayCookieValue("insforge_refresh_token", { decode: true });
992
- const shouldUseRelayRefreshFallback =
993
- p === "/api/auth/refresh" && !hasClientCookie && !hasEffectiveCsrfHeader && relayRefreshToken;
994
- if (shouldUseRelayRefreshFallback) {
995
- shouldInjectRelayCookies = false;
996
- }
997
-
998
- // Inject relay cookies so WebView benefits from browser's login session.
999
- // Refresh requests need either a browser cookie or an explicit CSRF token;
1000
- // otherwise replaying a stale persisted refresh cookie just manufactures
1001
- // Invalid CSRF errors on startup.
1002
- const originalCookieHeader = normalizeCookieHeader(proxyHeaders["cookie"]);
1003
- const mergedCookie = shouldInjectRelayCookies
1004
- ? buildRelayCookieHeader(originalCookieHeader, {
1005
- relayPrecedenceNames: p === "/api/auth/refresh"
1006
- ? [csrfRelayCookieName, "insforge_refresh_token"]
1007
- : [],
1008
- })
1009
- : originalCookieHeader;
1010
- const injectedRelayCookies =
1011
- shouldInjectRelayCookies && relayCookies.size > 0 && mergedCookie !== originalCookieHeader;
1012
- if (mergedCookie) proxyHeaders["cookie"] = mergedCookie;
1013
-
1014
- const bodyChunks = [];
1015
- for await (const chunk of req) bodyChunks.push(chunk);
1016
- let proxyBody = bodyChunks.length > 0 ? Buffer.concat(bodyChunks) : undefined;
1017
- let effectiveTargetUrl = targetUrl;
1018
- if (shouldUseRelayRefreshFallback) {
1019
- effectiveTargetUrl = `${insforgeBase.replace(/\/$/, "")}/api/auth/refresh?client_type=mobile`;
1020
- proxyHeaders["content-type"] = "application/json";
1021
- delete proxyHeaders["content-length"];
1022
- proxyBody = Buffer.from(JSON.stringify({ refresh_token: relayRefreshToken }), "utf8");
1023
- }
1024
- const proxyRes = await fetch(effectiveTargetUrl, {
1025
- method: req.method || "GET",
1026
- headers: proxyHeaders,
1027
- body: proxyBody,
1028
- credentials: "include",
1029
- redirect: "manual",
1030
- });
1031
- const responseHeaders = [...proxyRes.headers.entries()]
1032
- .filter(([k]) => !["transfer-encoding", "connection"].includes(k.toLowerCase()))
1033
- .map(([k, v]) => {
1034
- if (k.toLowerCase() === "set-cookie") {
1035
- const rewritten = v.replace(/;\s*[Dd]omain=[^;]*/g, "; Domain=localhost");
1036
- captureSetCookies(rewritten);
1037
- return [k, rewritten];
1038
- }
1039
- return [k, v];
1040
- });
1041
- res.writeHead(proxyRes.status, Object.fromEntries(responseHeaders));
1042
- const resBody = Buffer.from(await proxyRes.arrayBuffer());
1043
- if (proxyRes.status >= 200 && proxyRes.status < 300) {
1044
- if (p === "/api/auth/logout") {
1045
- clearRelayCookies("sign out");
1046
- } else {
1047
- captureAuthTokensFromBody(resBody, proxyRes.headers.get("content-type"));
1048
- }
1049
- }
1050
- if (
1051
- p === "/api/auth/refresh"
1052
- && proxyRes.status === 403
1053
- && injectedRelayCookies
1054
- && !hasClientCookie
1055
- && /invalid csrf token/i.test(resBody.toString("utf8"))
1056
- ) {
1057
- clearRelayCookies("stale refresh cookie without local CSRF context");
1058
- }
1059
- res.end(resBody);
1060
- } catch (e) {
1061
- json(res, { error: `Auth proxy error: ${e?.message || e}` }, 502);
1062
- }
1063
- return true;
1064
- }
1065
-
1066
718
  // --- ip-check proxy: reverse-proxy ip.net.coffee (issue #81) ---
1067
719
  // Lock-down: GET/HEAD only, restricted path prefixes, do not forward
1068
720
  // browser credentials or fingerprintable headers. Without these limits
@@ -1101,7 +753,7 @@ function createLocalApiHandler({ queuePath }) {
1101
753
  accept: req.headers["accept"] || "*/*",
1102
754
  "accept-language": req.headers["accept-language"] || "en",
1103
755
  "accept-encoding": req.headers["accept-encoding"] || "gzip",
1104
- "user-agent": "TokenTracker/IPCheck (https://www.tokentracker.cc)",
756
+ "user-agent": "TokenTracker/IPCheck",
1105
757
  referer: `${IP_CHECK_TARGET}${targetPath}`,
1106
758
  };
1107
759
 
@@ -1207,7 +859,7 @@ function createLocalApiHandler({ queuePath }) {
1207
859
  headers: {
1208
860
  accept: req.headers["accept"] || "image/*",
1209
861
  "accept-language": req.headers["accept-language"] || "en",
1210
- "user-agent": "TokenTracker/AvatarProxy (https://www.tokentracker.cc)",
862
+ "user-agent": "TokenTracker/AvatarProxy",
1211
863
  },
1212
864
  });
1213
865
  if (!upstream.ok) {
@@ -1257,19 +909,10 @@ function createLocalApiHandler({ queuePath }) {
1257
909
  } catch {
1258
910
  body = {};
1259
911
  }
1260
- const extraEnv = {};
1261
- if (typeof body.deviceToken === "string" && body.deviceToken.trim()) {
1262
- extraEnv.TOKENTRACKER_DEVICE_TOKEN = body.deviceToken.trim();
1263
- }
1264
- if (body.insforgeBaseUrl != null) {
1265
- const allowedBaseUrl = resolveAllowedInsforgeBaseUrl(body.insforgeBaseUrl);
1266
- if (!allowedBaseUrl) {
1267
- json(res, { ok: false, error: "Unsupported insforgeBaseUrl override" }, 400);
1268
- return true;
1269
- }
1270
- extraEnv.TOKENTRACKER_INSFORGE_BASE_URL = allowedBaseUrl;
1271
- }
1272
- const result = await runSyncCommand(extraEnv);
912
+ // Local-only: the sync command takes no cloud credentials or endpoint
913
+ // overrides any more, so the request body carries nothing to forward.
914
+ void body;
915
+ const result = await runSyncCommand({});
1273
916
  try {
1274
917
  const { resetUsageLimitsCache } = require("./usage-limits");
1275
918
  resetUsageLimitsCache();
@@ -1876,7 +1519,6 @@ function createLocalApiHandler({ queuePath }) {
1876
1519
 
1877
1520
  module.exports = {
1878
1521
  createLocalApiHandler,
1879
- resolveAllowedInsforgeBaseUrl,
1880
1522
  resolveQueuePath,
1881
1523
  // Exported for cross-consumer tests (pricing + native contract lock).
1882
1524
  MODEL_PRICING,
@@ -3,7 +3,7 @@
3
3
  "note": "Curated price overrides. Always wins over LiteLLM. Two reasons to live here: (1) self-defined alias names that LiteLLM will never carry (kiro-*, hy3-*, composer-*, kimi-for-coding, free-tier OpenRouter routes); (2) prices we want to pin even if LiteLLM has the model (e.g. cache_write fields LiteLLM often omits). Units: USD per million tokens. Edit this file to override pricing without redeploying.",
4
4
  "units": "usd_per_million_tokens",
5
5
  "deepseek_v4_pro_discount_expiry": "2026-05-31T15:59:00Z — DeepSeek v4-pro is currently at a 75% promotional discount. After expiry the prices revert to 4x: input $1.74/M, output $3.48/M, cache_read $0.0145/M, cache_write $1.74/M. Update this file before the cutover.",
6
- "sonnet5_intro_price_expiry": "2026-08-31 — Sonnet 5 intro price 2/10 reverts to 3/15 sticker. The src pricing path auto-tracks LiteLLM (no edit needed), but the 5 dashboard/edge-patches MODEL_PRICING tables hardcode 2/10 and must be reverted to 3/15 manually + re-vendor the seed. See issue #16."
6
+ "sonnet5_intro_price_expiry": "2026-08-31 — Sonnet 5 intro price 2/10 reverts to 3/15 sticker. The src pricing path auto-tracks LiteLLM (no edit needed); re-vendor the seed after the cutover. See issue #16."
7
7
  },
8
8
  "exact": {
9
9
  "claude-fable-5": { "input": 10, "output": 50, "cache_read": 1, "cache_write": 12.5, "note": "Pinned from Anthropic public pricing until bundled LiteLLM seed carries Fable 5." },