tokentracker-cli 0.5.80 → 0.5.81

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 (21) hide show
  1. package/dashboard/dist/assets/{Card-Cv4eTIKD.js → Card-CvRCwmts.js} +1 -1
  2. package/dashboard/dist/assets/{DashboardPage-BLFOnvMn.js → DashboardPage-Q3m4isBc.js} +1 -1
  3. package/dashboard/dist/assets/{FadeIn-CIeY4GXM.js → FadeIn-7Sp96sG1.js} +1 -1
  4. package/dashboard/dist/assets/{IpCheckPage-IsYc44dj.js → IpCheckPage-CU5zi7km.js} +1 -1
  5. package/dashboard/dist/assets/{LeaderboardPage-Su4flsQc.js → LeaderboardPage-C9g0lTkN.js} +1 -1
  6. package/dashboard/dist/assets/{LeaderboardProfilePage-bt2zrvtb.js → LeaderboardProfilePage-U3Oww62u.js} +1 -1
  7. package/dashboard/dist/assets/{LimitsPage-CojL1PZS.js → LimitsPage-DW-j3hsV.js} +1 -1
  8. package/dashboard/dist/assets/{ProviderIcon-C2Qp69XI.js → ProviderIcon-BJl3TO7B.js} +1 -1
  9. package/dashboard/dist/assets/{SettingsPage-FSVM_ozY.js → SettingsPage-DnSupHC-.js} +1 -1
  10. package/dashboard/dist/assets/{WidgetsPage-CeLvw5tR.js → WidgetsPage-DwSoxQql.js} +1 -1
  11. package/dashboard/dist/assets/{download-BK4EqMpL.js → download-DtuvJNeF.js} +1 -1
  12. package/dashboard/dist/assets/{leaderboard-columns-CxdAz5_V.js → leaderboard-columns-B1_Q_WZh.js} +1 -1
  13. package/dashboard/dist/assets/{main-CPsqG3PW.js → main-MimZZsgW.js} +188 -188
  14. package/dashboard/dist/assets/{use-limits-display-prefs-C-Y8vFA9.js → use-limits-display-prefs-CPfPbjXC.js} +1 -1
  15. package/dashboard/dist/assets/{use-usage-limits-CiHD5lbg.js → use-usage-limits-BvCjl0IL.js} +1 -1
  16. package/dashboard/dist/index.html +1 -1
  17. package/dashboard/dist/share.html +1 -1
  18. package/package.json +1 -1
  19. package/src/commands/serve.js +14 -4
  20. package/src/lib/local-api.js +135 -26
  21. package/src/lib/static-server.js +0 -1
@@ -1 +1 @@
1
- import{r as a}from"./main-CPsqG3PW.js";const d=["claude","codex","cursor","gemini","kiro","copilot","antigravity"],v={claude:"Claude",codex:"Codex",cursor:"Cursor",gemini:"Gemini",kiro:"Kiro",copilot:"GitHub Copilot",antigravity:"Antigravity"},k={claude:"/brand-logos/claude-code.svg",codex:"/brand-logos/codex.svg",cursor:"/brand-logos/cursor.svg",gemini:"/brand-logos/gemini.svg",kiro:"/brand-logos/kiro.svg",copilot:"/brand-logos/copilot.svg",antigravity:"/brand-logos/antigravity.svg"},l="tt.limits.providerOrder",g="tt.limits.providerVisibility";function y(){if(typeof window>"u")return[...d];try{const r=window.localStorage.getItem(l);if(!r)return[...d];const s=JSON.parse(r);if(!Array.isArray(s))return[...d];const n=s.filter(c=>d.includes(c));for(const c of d)n.includes(c)||n.push(c);return n}catch{return[...d]}}function m(){const r=Object.fromEntries(d.map(s=>[s,!0]));if(typeof window>"u")return r;try{const s=window.localStorage.getItem(g);if(!s)return r;const n=JSON.parse(s);if(!n||typeof n!="object")return r;const c={...r};for(const u of d)typeof n[u]=="boolean"&&(c[u]=n[u]);return c}catch{return r}}function C(){const[r,s]=a.useState(y),[n,c]=a.useState(m);a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(l,JSON.stringify(r))}catch{}},[r]),a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(g,JSON.stringify(n))}catch{}},[n]),a.useEffect(()=>{if(typeof window>"u")return;const o=t=>{t.key===l&&s(y()),t.key===g&&c(m())};return window.addEventListener("storage",o),()=>window.removeEventListener("storage",o)},[]);const u=a.useCallback(o=>{c(t=>({...t,[o]:!t[o]}))},[]),b=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<=0)return t;const i=[...t];return[i[e-1],i[e]]=[i[e],i[e-1]],i})},[]),p=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<0||e>=t.length-1)return t;const i=[...t];return[i[e],i[e+1]]=[i[e+1],i[e]],i})},[]),O=a.useCallback((o,t)=>{o!==t&&s(e=>{const i=e.indexOf(o),w=e.indexOf(t);if(i<0||w<0)return e;const f=[...e],[E]=f.splice(i,1);return f.splice(w,0,E),f})},[]),x=a.useCallback(()=>{s([...d]),c(Object.fromEntries(d.map(o=>[o,!0])))},[]),I=a.useMemo(()=>r.filter(o=>n[o]!==!1),[r,n]);return{order:r,visibility:n,visibleOrdered:I,toggle:u,moveUp:b,moveDown:p,moveToward:O,reset:x}}export{k as L,v as a,C as u};
1
+ import{r as a}from"./main-MimZZsgW.js";const d=["claude","codex","cursor","gemini","kiro","copilot","antigravity"],v={claude:"Claude",codex:"Codex",cursor:"Cursor",gemini:"Gemini",kiro:"Kiro",copilot:"GitHub Copilot",antigravity:"Antigravity"},k={claude:"/brand-logos/claude-code.svg",codex:"/brand-logos/codex.svg",cursor:"/brand-logos/cursor.svg",gemini:"/brand-logos/gemini.svg",kiro:"/brand-logos/kiro.svg",copilot:"/brand-logos/copilot.svg",antigravity:"/brand-logos/antigravity.svg"},l="tt.limits.providerOrder",g="tt.limits.providerVisibility";function y(){if(typeof window>"u")return[...d];try{const r=window.localStorage.getItem(l);if(!r)return[...d];const s=JSON.parse(r);if(!Array.isArray(s))return[...d];const n=s.filter(c=>d.includes(c));for(const c of d)n.includes(c)||n.push(c);return n}catch{return[...d]}}function m(){const r=Object.fromEntries(d.map(s=>[s,!0]));if(typeof window>"u")return r;try{const s=window.localStorage.getItem(g);if(!s)return r;const n=JSON.parse(s);if(!n||typeof n!="object")return r;const c={...r};for(const u of d)typeof n[u]=="boolean"&&(c[u]=n[u]);return c}catch{return r}}function C(){const[r,s]=a.useState(y),[n,c]=a.useState(m);a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(l,JSON.stringify(r))}catch{}},[r]),a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(g,JSON.stringify(n))}catch{}},[n]),a.useEffect(()=>{if(typeof window>"u")return;const o=t=>{t.key===l&&s(y()),t.key===g&&c(m())};return window.addEventListener("storage",o),()=>window.removeEventListener("storage",o)},[]);const u=a.useCallback(o=>{c(t=>({...t,[o]:!t[o]}))},[]),b=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<=0)return t;const i=[...t];return[i[e-1],i[e]]=[i[e],i[e-1]],i})},[]),p=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<0||e>=t.length-1)return t;const i=[...t];return[i[e],i[e+1]]=[i[e+1],i[e]],i})},[]),O=a.useCallback((o,t)=>{o!==t&&s(e=>{const i=e.indexOf(o),w=e.indexOf(t);if(i<0||w<0)return e;const f=[...e],[E]=f.splice(i,1);return f.splice(w,0,E),f})},[]),x=a.useCallback(()=>{s([...d]),c(Object.fromEntries(d.map(o=>[o,!0])))},[]),I=a.useMemo(()=>r.filter(o=>n[o]!==!1),[r,n]);return{order:r,visibility:n,visibleOrdered:I,toggle:u,moveUp:b,moveDown:p,moveToward:O,reset:x}}export{k as L,v as a,C as u};
@@ -1 +1 @@
1
- import{r,aM as l}from"./main-CPsqG3PW.js";function y(i){const[o,a]=r.useState(null),[u,s]=r.useState(null),[c,f]=r.useState(!0),n=!!i?.initialRefresh,g=r.useCallback(async()=>{try{const e=await l({refresh:!0});a(e&&typeof e=="object"?e:null),s(null)}catch(e){s(e?.message||String(e))}},[]);return r.useEffect(()=>{let e=!1;return(async()=>{try{const t=await l(n?{refresh:!0}:{});if(e)return;a(t&&typeof t=="object"?t:null),s(null)}catch(t){if(e)return;s(t?.message||String(t))}finally{e||f(!1)}})(),()=>{e=!0}},[n]),{data:o,error:u,isLoading:c,refresh:g}}export{y as u};
1
+ import{r,aM as l}from"./main-MimZZsgW.js";function y(i){const[o,a]=r.useState(null),[u,s]=r.useState(null),[c,f]=r.useState(!0),n=!!i?.initialRefresh,g=r.useCallback(async()=>{try{const e=await l({refresh:!0});a(e&&typeof e=="object"?e:null),s(null)}catch(e){s(e?.message||String(e))}},[]);return r.useEffect(()=>{let e=!1;return(async()=>{try{const t=await l(n?{refresh:!0}:{});if(e)return;a(t&&typeof t=="object"?t:null),s(null)}catch(t){if(e)return;s(t?.message||String(t))}finally{e||f(!1)}})(),()=>{e=!0}},[n]),{data:o,error:u,isLoading:c,refresh:g}}export{y as u};
@@ -135,7 +135,7 @@
135
135
  ]
136
136
  }
137
137
  </script>
138
- <script type="module" crossorigin src="/assets/main-CPsqG3PW.js"></script>
138
+ <script type="module" crossorigin src="/assets/main-MimZZsgW.js"></script>
139
139
  <link rel="stylesheet" crossorigin href="/assets/main-DRf20yyJ.css">
140
140
  </head>
141
141
  <body>
@@ -51,7 +51,7 @@
51
51
  "description": "Shareable Token Tracker dashboard snapshot."
52
52
  }
53
53
  </script>
54
- <script type="module" crossorigin src="/assets/main-CPsqG3PW.js"></script>
54
+ <script type="module" crossorigin src="/assets/main-MimZZsgW.js"></script>
55
55
  <link rel="stylesheet" crossorigin href="/assets/main-DRf20yyJ.css">
56
56
  </head>
57
57
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokentracker-cli",
3
- "version": "0.5.80",
3
+ "version": "0.5.81",
4
4
  "description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Kiro, Gemini, OpenCode, OpenClaw, Hermes, GitHub Copilot)",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -11,11 +11,16 @@ const { openInBrowser } = require("../lib/browser-auth");
11
11
 
12
12
  const DEFAULT_PORT = 7680;
13
13
  const NPM_PACKAGE_NAME = "tokentracker-cli";
14
+ const LOCAL_BIND_HOST = "127.0.0.1";
14
15
 
15
16
  function buildPortInUseHint(port) {
16
17
  return `Port ${port} is still in use after cleanup. Try: npx ${NPM_PACKAGE_NAME} serve --port ${port + 1}\n`;
17
18
  }
18
19
 
20
+ function getLocalServerUrl(port) {
21
+ return `http://${LOCAL_BIND_HOST}:${port}`;
22
+ }
23
+
19
24
  async function cmdServe(argv) {
20
25
  const opts = parseArgs(argv);
21
26
 
@@ -85,7 +90,6 @@ async function cmdServe(argv) {
85
90
  // CORS preflight
86
91
  if (req.method === "OPTIONS") {
87
92
  res.writeHead(204, {
88
- "Access-Control-Allow-Origin": "*",
89
93
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
90
94
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
91
95
  });
@@ -116,8 +120,8 @@ async function cmdServe(argv) {
116
120
  // 4. Listen (kill stale process on same port if needed)
117
121
  const port = opts.port;
118
122
  await ensurePortFree(port);
119
- server.listen(port, () => {
120
- const url = `http://localhost:${port}`;
123
+ server.listen(port, LOCAL_BIND_HOST, () => {
124
+ const url = getLocalServerUrl(port);
121
125
  process.stdout.write(
122
126
  [
123
127
  "",
@@ -226,4 +230,10 @@ function parseArgs(argv) {
226
230
  return opts;
227
231
  }
228
232
 
229
- module.exports = { cmdServe, buildPortInUseHint, NPM_PACKAGE_NAME };
233
+ module.exports = {
234
+ cmdServe,
235
+ buildPortInUseHint,
236
+ NPM_PACKAGE_NAME,
237
+ LOCAL_BIND_HOST,
238
+ getLocalServerUrl,
239
+ };
@@ -2,6 +2,8 @@ const fs = require("node:fs");
2
2
  const os = require("node:os");
3
3
  const path = require("node:path");
4
4
  const { spawn } = require("node:child_process");
5
+ const crypto = require("node:crypto");
6
+ const { DEFAULT_BASE_URL, resolveRuntimeConfig } = require("./runtime-config");
5
7
 
6
8
  const SYNC_TIMEOUT_MS = 120_000;
7
9
  const TRACKER_BIN = path.resolve(__dirname, "../../bin/tracker.js");
@@ -159,6 +161,29 @@ function readProjectQueueData(projectQueuePath) {
159
161
  return Array.from(seen.values());
160
162
  }
161
163
 
164
+ function isLegacyInclusiveCodexRow(row) {
165
+ if (!row || (row.source !== "codex" && row.source !== "every-code")) return false;
166
+ const inputTokens = Number(row.input_tokens || 0);
167
+ const cachedInputTokens = Number(row.cached_input_tokens || 0);
168
+ const outputTokens = Number(row.output_tokens || 0);
169
+ const totalTokens = Number(row.total_tokens || 0);
170
+ if (!Number.isFinite(inputTokens) || !Number.isFinite(cachedInputTokens)) return false;
171
+ if (cachedInputTokens <= 0 || inputTokens < cachedInputTokens) return false;
172
+ // Legacy Codex queue rows stored input inclusive of cache reads, while
173
+ // total_tokens remained input + output. Canonical rows keep input as pure
174
+ // non-cached input, so cache-heavy legacy rows can be identified by this
175
+ // exact invariant.
176
+ return totalTokens === inputTokens + outputTokens;
177
+ }
178
+
179
+ function normalizeQueueRow(row) {
180
+ if (!isLegacyInclusiveCodexRow(row)) return row;
181
+ return {
182
+ ...row,
183
+ input_tokens: Number(row.input_tokens || 0) - Number(row.cached_input_tokens || 0),
184
+ };
185
+ }
186
+
162
187
  function readQueueData(queuePath) {
163
188
  let raw;
164
189
  try {
@@ -194,7 +219,7 @@ function readQueueData(queuePath) {
194
219
  const seen = new Map();
195
220
  for (const row of parsed) {
196
221
  const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
197
- seen.set(key, row);
222
+ seen.set(key, normalizeQueueRow(row));
198
223
  }
199
224
  return Array.from(seen.values());
200
225
  }
@@ -361,6 +386,67 @@ function trimOutput(value, max = 4000) {
361
386
  return t.length <= max ? t : t.slice(t.length - max);
362
387
  }
363
388
 
389
+ function normalizeRemoteHttpBaseUrl(value) {
390
+ if (typeof value !== "string") return null;
391
+ const trimmed = value.trim();
392
+ if (!trimmed) return null;
393
+ try {
394
+ const url = new URL(trimmed);
395
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
396
+ url.username = "";
397
+ url.password = "";
398
+ url.hash = "";
399
+ return url.toString().replace(/\/$/, "");
400
+ } catch (_e) {
401
+ return null;
402
+ }
403
+ }
404
+
405
+ function resolveAllowedInsforgeBaseUrl(value) {
406
+ const requested = normalizeRemoteHttpBaseUrl(value);
407
+ if (!requested) return null;
408
+
409
+ const runtime = resolveRuntimeConfig();
410
+ const allowed = new Set(
411
+ [runtime.baseUrl, DEFAULT_BASE_URL]
412
+ .map((entry) => normalizeRemoteHttpBaseUrl(entry))
413
+ .filter(Boolean),
414
+ );
415
+
416
+ return allowed.has(requested) ? requested : null;
417
+ }
418
+
419
+ function parseCookieHeader(value) {
420
+ const out = new Map();
421
+ if (typeof value !== "string" || !value.trim()) return out;
422
+ for (const part of value.split(";")) {
423
+ const idx = part.indexOf("=");
424
+ if (idx < 1) continue;
425
+ const key = part.slice(0, idx).trim();
426
+ const rawValue = part.slice(idx + 1).trim();
427
+ if (key) out.set(key, rawValue);
428
+ }
429
+ return out;
430
+ }
431
+
432
+ function isLoopbackHostname(hostname) {
433
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1" || hostname === "[::1]";
434
+ }
435
+
436
+ function hasAllowedLoopbackOrigin(headers = {}) {
437
+ const candidates = [headers.origin, headers.referer];
438
+ for (const raw of candidates) {
439
+ if (raw == null || raw === "") continue;
440
+ try {
441
+ const url = new URL(String(raw));
442
+ if (url.protocol !== "http:" || !isLoopbackHostname(url.hostname)) return false;
443
+ } catch (_e) {
444
+ return false;
445
+ }
446
+ }
447
+ return true;
448
+ }
449
+
364
450
  function readJsonBody(req) {
365
451
  return new Promise((resolve, reject) => {
366
452
  const chunks = [];
@@ -526,7 +612,7 @@ function scanClaudeProjects(projectMap) {
526
612
  // ---------------------------------------------------------------------------
527
613
 
528
614
  function json(res, data, status) {
529
- res.writeHead(status || 200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
615
+ res.writeHead(status || 200, { "Content-Type": "application/json" });
530
616
  res.end(JSON.stringify(data));
531
617
  }
532
618
 
@@ -541,6 +627,7 @@ function createLocalApiHandler({ queuePath }) {
541
627
  // so that both browser and WKWebView share the same login session via the proxy.
542
628
  // Persisted to disk so cookies survive server restarts.
543
629
  let relayCookies = new Map();
630
+ const localAuthToken = crypto.randomBytes(24).toString("hex");
544
631
  const trackerDataDir = path.join(os.homedir(), ".tokentracker", "tracker");
545
632
  const cookiePath = path.join(trackerDataDir, "relay-cookies.json");
546
633
 
@@ -653,13 +740,40 @@ function createLocalApiHandler({ queuePath }) {
653
740
  let _nativeAuthPending = false;
654
741
  let _nativeAuthExpiry = 0;
655
742
 
743
+ function isAuthorizedLocalMutation(req) {
744
+ const headerToken = req?.headers?.["x-tokentracker-local-auth"];
745
+ const cookieToken = parseCookieHeader(req?.headers?.cookie).get("tokentracker_local_auth");
746
+ const token = typeof headerToken === "string" && headerToken.trim()
747
+ ? headerToken.trim()
748
+ : cookieToken || "";
749
+ if (!token || token !== localAuthToken) return false;
750
+ return hasAllowedLoopbackOrigin(req?.headers || {});
751
+ }
752
+
656
753
  return async function handleLocalApi(req, res, url) {
657
754
  const p = url.pathname;
658
755
 
756
+ if (p === "/api/local-auth") {
757
+ if (String(req.method || "GET").toUpperCase() !== "GET") {
758
+ json(res, { error: "Method Not Allowed" }, 405);
759
+ return true;
760
+ }
761
+ res.writeHead(200, {
762
+ "Content-Type": "application/json",
763
+ "Cache-Control": "no-store",
764
+ });
765
+ res.end(JSON.stringify({ token: localAuthToken }));
766
+ return true;
767
+ }
768
+
659
769
  // --- Auth bridge: native OAuth flag (WebView ↔ system browser) ---
660
770
  if (p === "/api/auth-bridge/verifier") {
661
771
  const method = String(req.method || "GET").toUpperCase();
662
772
  if (method === "PUT" || method === "POST") {
773
+ if (!isAuthorizedLocalMutation(req)) {
774
+ json(res, { error: "Unauthorized" }, 401);
775
+ return true;
776
+ }
663
777
  const body = await readJsonBody(req);
664
778
  _nativeAuthPending = Boolean(body?.native);
665
779
  _nativeAuthExpiry = Date.now() + 5 * 60 * 1000; // 5 min TTL
@@ -679,20 +793,8 @@ function createLocalApiHandler({ queuePath }) {
679
793
 
680
794
  // --- auth proxy: forward /api/auth/* to InsForge cloud ---
681
795
  if (p.startsWith("/api/auth/")) {
682
- const { DEFAULT_BASE_URL } = require("./runtime-config.js");
683
- let insforgeBase = process.env.TOKENTRACKER_INSFORGE_BASE_URL
684
- || process.env.INSFORGE_BASE_URL
685
- || "";
686
- if (!insforgeBase) {
687
- try {
688
- const cfgPath = path.join(os.homedir(), ".tokentracker", "tracker", "config.json");
689
- const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
690
- insforgeBase = cfg?.baseUrl || "";
691
- } catch { /* ignore */ }
692
- }
693
- if (!insforgeBase) {
694
- insforgeBase = DEFAULT_BASE_URL;
695
- }
796
+ const runtime = resolveRuntimeConfig();
797
+ const insforgeBase = runtime.baseUrl || DEFAULT_BASE_URL;
696
798
  try {
697
799
  const targetUrl = `${insforgeBase.replace(/\/$/, "")}${p}${url.search || ""}`;
698
800
  const proxyHeaders = {};
@@ -758,6 +860,10 @@ function createLocalApiHandler({ queuePath }) {
758
860
  json(res, { ok: false, error: "Method Not Allowed" }, 405);
759
861
  return true;
760
862
  }
863
+ if (!isAuthorizedLocalMutation(req)) {
864
+ json(res, { ok: false, error: "Unauthorized" }, 401);
865
+ return true;
866
+ }
761
867
  try {
762
868
  let body = {};
763
869
  try {
@@ -769,8 +875,13 @@ function createLocalApiHandler({ queuePath }) {
769
875
  if (typeof body.deviceToken === "string" && body.deviceToken.trim()) {
770
876
  extraEnv.TOKENTRACKER_DEVICE_TOKEN = body.deviceToken.trim();
771
877
  }
772
- if (typeof body.insforgeBaseUrl === "string" && /^https?:\/\//i.test(body.insforgeBaseUrl.trim())) {
773
- extraEnv.TOKENTRACKER_INSFORGE_BASE_URL = body.insforgeBaseUrl.trim();
878
+ if (body.insforgeBaseUrl != null) {
879
+ const allowedBaseUrl = resolveAllowedInsforgeBaseUrl(body.insforgeBaseUrl);
880
+ if (!allowedBaseUrl) {
881
+ json(res, { ok: false, error: "Unsupported insforgeBaseUrl override" }, 400);
882
+ return true;
883
+ }
884
+ extraEnv.TOKENTRACKER_INSFORGE_BASE_URL = allowedBaseUrl;
774
885
  }
775
886
  const result = await runSyncCommand(extraEnv);
776
887
  try {
@@ -949,14 +1060,11 @@ function createLocalApiHandler({ queuePath }) {
949
1060
  const sources = Array.from(bySource.values()).map((s) => {
950
1061
  s.models = Array.from(s.models.values())
951
1062
  .map((m) => {
952
- const p = getModelPricing(m.model);
953
- const cost =
954
- ((m.totals.input_tokens || 0) * (p.input || 0) +
955
- (m.totals.output_tokens || 0) * (p.output || 0) +
956
- (m.totals.cached_input_tokens || 0) * (p.cache_read || 0) +
957
- (m.totals.cache_creation_input_tokens || 0) * (p.cache_write || 0) +
958
- (m.totals.reasoning_output_tokens || 0) * (p.output || 0)) /
959
- 1_000_000;
1063
+ const cost = computeRowCost({
1064
+ ...m.totals,
1065
+ model: m.model,
1066
+ source: s.source,
1067
+ });
960
1068
  return { ...m, totals: { ...m.totals, total_cost_usd: cost.toFixed(6) } };
961
1069
  })
962
1070
  .sort((a, b) => b.totals.total_tokens - a.totals.total_tokens);
@@ -1119,6 +1227,7 @@ function createLocalApiHandler({ queuePath }) {
1119
1227
 
1120
1228
  module.exports = {
1121
1229
  createLocalApiHandler,
1230
+ resolveAllowedInsforgeBaseUrl,
1122
1231
  resolveQueuePath,
1123
1232
  // Exported for cross-consumer tests (pricing + native contract lock).
1124
1233
  MODEL_PRICING,
@@ -46,7 +46,6 @@ async function serveStaticFile(baseDir, pathname, res) {
46
46
  "Content-Type": contentType,
47
47
  "Content-Length": stat.size,
48
48
  "Cache-Control": isHtml ? "no-cache" : "public, max-age=31536000, immutable",
49
- "Access-Control-Allow-Origin": "*",
50
49
  });
51
50
 
52
51
  const stream = fs.createReadStream(filePath);