tokentracker-cli 0.23.1 → 0.24.0

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 (39) hide show
  1. package/dashboard/dist/assets/{Card-CQ4ezOtn.js → Card-C22jvaH-.js} +1 -1
  2. package/dashboard/dist/assets/{DashboardPage-DCixFQNF.js → DashboardPage-Dv_4x35m.js} +1 -1
  3. package/dashboard/dist/assets/{DevicePage-C-d4jlSG.js → DevicePage-BjhiTjFW.js} +1 -1
  4. package/dashboard/dist/assets/{FadeIn-DEPo9u33.js → FadeIn-CHxDANbJ.js} +1 -1
  5. package/dashboard/dist/assets/{HeaderGithubStar-CRKWJSGQ.js → HeaderGithubStar-CIfUeDxZ.js} +1 -1
  6. package/dashboard/dist/assets/IpCheckPage-DIa0AzUn.js +15 -0
  7. package/dashboard/dist/assets/{LandingPage-Bv5OIhRJ.js → LandingPage-sh7tcPKd.js} +1 -1
  8. package/dashboard/dist/assets/{LeaderboardPage-ea_pS5m7.js → LeaderboardPage-Dg6zcGG1.js} +1 -1
  9. package/dashboard/dist/assets/{LeaderboardProfilePage-DMzZmwP4.js → LeaderboardProfilePage-BOJJ-T57.js} +1 -1
  10. package/dashboard/dist/assets/{LimitsPage-CWaDaTZ9.js → LimitsPage-CdMhF5lF.js} +1 -1
  11. package/dashboard/dist/assets/{LoginPage-BhmcrPVS.js → LoginPage-BbJO2VUw.js} +1 -1
  12. package/dashboard/dist/assets/{PopoverPopup-bhZXkxTE.js → PopoverPopup-Csxg6Urf.js} +1 -1
  13. package/dashboard/dist/assets/{ProviderIcon-BFsXZu44.js → ProviderIcon-BdfV-wMB.js} +1 -1
  14. package/dashboard/dist/assets/{SettingsPage-Dw5iy_IJ.js → SettingsPage-B0UsE-Cz.js} +1 -1
  15. package/dashboard/dist/assets/{SkillsPage-Dz1X6g_o.js → SkillsPage-CpV5GRRc.js} +1 -1
  16. package/dashboard/dist/assets/{WidgetsPage-D8RbtXHy.js → WidgetsPage-DdQEZR9N.js} +1 -1
  17. package/dashboard/dist/assets/{WrappedPage-Chw83YEb.js → WrappedPage-DKYCqqJa.js} +1 -1
  18. package/dashboard/dist/assets/check-CO0YnJo0.js +1 -0
  19. package/dashboard/dist/assets/{chevron-down-DK-3H5UY.js → chevron-down-Dh0EAuAs.js} +1 -1
  20. package/dashboard/dist/assets/{download-lOWPegbv.js → download-B-u2224w.js} +1 -1
  21. package/dashboard/dist/assets/{info-DOAPbTJ3.js → info-Bax52N3e.js} +1 -1
  22. package/dashboard/dist/assets/{leaderboard-columns-qXgnSz3Y.js → leaderboard-columns-CmMWLqut.js} +1 -1
  23. package/dashboard/dist/assets/main-JP_EYeq-.css +1 -0
  24. package/dashboard/dist/assets/{main-uV9_FTHb.js → main-uaLO1Ae7.js} +142 -17
  25. package/dashboard/dist/assets/{use-limits-display-prefs-DtRFIKBE.js → use-limits-display-prefs-BVawA_Rp.js} +1 -1
  26. package/dashboard/dist/assets/{use-native-settings-BRDZatug.js → use-native-settings-DHtj5DnD.js} +1 -1
  27. package/dashboard/dist/assets/{use-reduced-motion-CYXeA5H_.js → use-reduced-motion-8tbVQBYw.js} +1 -1
  28. package/dashboard/dist/assets/{use-usage-limits-RfcCMHky.js → use-usage-limits-SwQjCSa8.js} +1 -1
  29. package/dashboard/dist/assets/{useCurrency-B0LcQvSj.js → useCurrency-Bc8I7sE1.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 +2 -1
  34. package/src/commands/serve.js +57 -2
  35. package/src/lib/local-api.js +89 -51
  36. package/src/lib/pricing/seed-snapshot.json +1 -1
  37. package/dashboard/dist/assets/IpCheckPage-BscvPch9.js +0 -1
  38. package/dashboard/dist/assets/check-Bs81cB4C.js +0 -1
  39. package/dashboard/dist/assets/main-0bSXJNLn.css +0 -1
@@ -1 +1 @@
1
- import{r as a}from"./main-uV9_FTHb.js";const d=["claude","codex","cursor","gemini","kimi","kiro","copilot","antigravity"],S={claude:"Claude",codex:"Codex",cursor:"Cursor",gemini:"Gemini",kimi:"Kimi",kiro:"Kiro",copilot:"GitHub Copilot",antigravity:"Antigravity"},v={claude:"/brand-logos/claude-code.svg",codex:"/brand-logos/codex.svg",cursor:"/brand-logos/cursor.svg",gemini:"/brand-logos/gemini.svg",kimi:"/brand-logos/kimi.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 w(){if(typeof window>"u")return[...d];try{const i=window.localStorage.getItem(l);if(!i)return[...d];const s=JSON.parse(i);if(!Array.isArray(s))return[...d];const r=s.filter(c=>d.includes(c));for(const c of d)r.includes(c)||r.push(c);return r}catch{return[...d]}}function y(){const i=Object.fromEntries(d.map(s=>[s,!0]));if(typeof window>"u")return i;try{const s=window.localStorage.getItem(g);if(!s)return i;const r=JSON.parse(s);if(!r||typeof r!="object")return i;const c={...i};for(const u of d)typeof r[u]=="boolean"&&(c[u]=r[u]);return c}catch{return i}}function C(){const[i,s]=a.useState(w),[r,c]=a.useState(y);a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(l,JSON.stringify(i))}catch{}},[i]),a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(g,JSON.stringify(r))}catch{}},[r]),a.useEffect(()=>{if(typeof window>"u")return;const o=t=>{t.key===l&&s(w()),t.key===g&&c(y())};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 n=[...t];return[n[e-1],n[e]]=[n[e],n[e-1]],n})},[]),p=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<0||e>=t.length-1)return t;const n=[...t];return[n[e],n[e+1]]=[n[e+1],n[e]],n})},[]),O=a.useCallback((o,t)=>{o!==t&&s(e=>{const n=e.indexOf(o),m=e.indexOf(t);if(n<0||m<0)return e;const f=[...e],[I]=f.splice(n,1);return f.splice(m,0,I),f})},[]),k=a.useCallback(()=>{s([...d]),c(Object.fromEntries(d.map(o=>[o,!0])))},[]),x=a.useMemo(()=>i.filter(o=>r[o]!==!1),[i,r]);return{order:i,visibility:r,visibleOrdered:x,toggle:u,moveUp:b,moveDown:p,moveToward:O,reset:k}}export{v as L,S as a,C as u};
1
+ import{r as a}from"./main-uaLO1Ae7.js";const d=["claude","codex","cursor","gemini","kimi","kiro","copilot","antigravity"],S={claude:"Claude",codex:"Codex",cursor:"Cursor",gemini:"Gemini",kimi:"Kimi",kiro:"Kiro",copilot:"GitHub Copilot",antigravity:"Antigravity"},v={claude:"/brand-logos/claude-code.svg",codex:"/brand-logos/codex.svg",cursor:"/brand-logos/cursor.svg",gemini:"/brand-logos/gemini.svg",kimi:"/brand-logos/kimi.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 w(){if(typeof window>"u")return[...d];try{const i=window.localStorage.getItem(l);if(!i)return[...d];const s=JSON.parse(i);if(!Array.isArray(s))return[...d];const r=s.filter(c=>d.includes(c));for(const c of d)r.includes(c)||r.push(c);return r}catch{return[...d]}}function y(){const i=Object.fromEntries(d.map(s=>[s,!0]));if(typeof window>"u")return i;try{const s=window.localStorage.getItem(g);if(!s)return i;const r=JSON.parse(s);if(!r||typeof r!="object")return i;const c={...i};for(const u of d)typeof r[u]=="boolean"&&(c[u]=r[u]);return c}catch{return i}}function C(){const[i,s]=a.useState(w),[r,c]=a.useState(y);a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(l,JSON.stringify(i))}catch{}},[i]),a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(g,JSON.stringify(r))}catch{}},[r]),a.useEffect(()=>{if(typeof window>"u")return;const o=t=>{t.key===l&&s(w()),t.key===g&&c(y())};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 n=[...t];return[n[e-1],n[e]]=[n[e],n[e-1]],n})},[]),p=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<0||e>=t.length-1)return t;const n=[...t];return[n[e],n[e+1]]=[n[e+1],n[e]],n})},[]),O=a.useCallback((o,t)=>{o!==t&&s(e=>{const n=e.indexOf(o),m=e.indexOf(t);if(n<0||m<0)return e;const f=[...e],[I]=f.splice(n,1);return f.splice(m,0,I),f})},[]),k=a.useCallback(()=>{s([...d]),c(Object.fromEntries(d.map(o=>[o,!0])))},[]),x=a.useMemo(()=>i.filter(o=>r[o]!==!1),[i,r]);return{order:i,visibility:r,visibleOrdered:x,toggle:u,moveUp:b,moveDown:p,moveToward:O,reset:k}}export{v as L,S as a,C as u};
@@ -1 +1 @@
1
- import{C as a,a7 as x,r as n,aH as g,aI as m,aJ as b,aK as u,aL as f,aF as h}from"./main-uV9_FTHb.js";import{C as y}from"./Card-CQ4ezOtn.js";function p({checked:s,onChange:t,disabled:e,ariaLabel:i}){return a.jsx("button",{type:"button",role:"switch","aria-checked":s,"aria-label":i,onClick:t,disabled:e,className:x("relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-oai-brand-500 disabled:opacity-50 disabled:cursor-not-allowed",s?"bg-oai-brand-500":"bg-oai-gray-300 dark:bg-oai-gray-700"),children:a.jsx("span",{className:x("inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",s?"translate-x-[18px]":"translate-x-[3px]")})})}function j({label:s,hint:t,control:e}){return a.jsxs("div",{className:"flex items-center justify-between gap-4 py-3",children:[a.jsxs("div",{className:"min-w-0 flex-1",children:[a.jsx("div",{className:"text-sm text-oai-gray-900 dark:text-oai-gray-200",children:s}),t?a.jsx("div",{className:"mt-0.5 text-xs text-oai-gray-500 dark:text-oai-gray-400",children:t}):null]}),a.jsx("div",{className:"shrink-0",children:e})]})}function N({title:s,subtitle:t,action:e,children:i}){return a.jsxs(y,{children:[a.jsxs("div",{className:"mb-3 flex items-start justify-between gap-4",children:[a.jsxs("div",{className:"min-w-0 flex-1",children:[a.jsx("h2",{className:"text-sm font-medium text-oai-gray-500 dark:text-oai-gray-300 uppercase tracking-wide",children:s}),t?a.jsx("p",{className:"mt-1 truncate text-xs text-oai-gray-500 dark:text-oai-gray-400",children:t}):null]}),e?a.jsx("div",{className:"shrink-0",children:e}):null]}),a.jsx("div",{className:"-mb-3 divide-y divide-oai-gray-200/60 dark:divide-oai-gray-800/60",children:i})]})}function w({options:s,value:t,onChange:e}){return a.jsx("div",{className:"inline-flex items-center rounded-lg border border-oai-gray-200 bg-oai-gray-50 p-0.5 dark:border-oai-gray-800 dark:bg-oai-gray-900",children:s.map(({value:i,label:d,Icon:l})=>{const r=t===i;return a.jsxs("button",{type:"button",onClick:()=>e(i),"aria-pressed":r,className:x("inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors",r?"bg-white text-oai-black shadow-sm dark:bg-oai-gray-800 dark:text-white":"text-oai-gray-500 hover:text-oai-black dark:text-oai-gray-400 dark:hover:text-white"),children:[l?a.jsx(l,{className:"h-3.5 w-3.5","aria-hidden":!0}):null,a.jsx("span",{children:d})]},i)})})}function S(){const[s,t]=n.useState(null),e=g()&&m();n.useEffect(()=>{if(!e)return;const r=b(o=>t(o));return u(),r},[e]);const i=n.useCallback((r,o)=>{e&&(t(c=>c&&{...c,[r]:o}),f(r,o))},[e]),d=n.useCallback(r=>{e&&h(r)},[e]),l=n.useCallback(()=>{e&&u()},[e]);return{available:e,settings:s,setSetting:i,runAction:d,refresh:l}}export{N as S,p as T,j as a,w as b,S as u};
1
+ import{C as a,a7 as x,r as n,aH as g,aI as m,aJ as b,aK as u,aL as f,aF as h}from"./main-uaLO1Ae7.js";import{C as y}from"./Card-C22jvaH-.js";function p({checked:s,onChange:t,disabled:e,ariaLabel:i}){return a.jsx("button",{type:"button",role:"switch","aria-checked":s,"aria-label":i,onClick:t,disabled:e,className:x("relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-oai-brand-500 disabled:opacity-50 disabled:cursor-not-allowed",s?"bg-oai-brand-500":"bg-oai-gray-300 dark:bg-oai-gray-700"),children:a.jsx("span",{className:x("inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",s?"translate-x-[18px]":"translate-x-[3px]")})})}function j({label:s,hint:t,control:e}){return a.jsxs("div",{className:"flex items-center justify-between gap-4 py-3",children:[a.jsxs("div",{className:"min-w-0 flex-1",children:[a.jsx("div",{className:"text-sm text-oai-gray-900 dark:text-oai-gray-200",children:s}),t?a.jsx("div",{className:"mt-0.5 text-xs text-oai-gray-500 dark:text-oai-gray-400",children:t}):null]}),a.jsx("div",{className:"shrink-0",children:e})]})}function N({title:s,subtitle:t,action:e,children:i}){return a.jsxs(y,{children:[a.jsxs("div",{className:"mb-3 flex items-start justify-between gap-4",children:[a.jsxs("div",{className:"min-w-0 flex-1",children:[a.jsx("h2",{className:"text-sm font-medium text-oai-gray-500 dark:text-oai-gray-300 uppercase tracking-wide",children:s}),t?a.jsx("p",{className:"mt-1 truncate text-xs text-oai-gray-500 dark:text-oai-gray-400",children:t}):null]}),e?a.jsx("div",{className:"shrink-0",children:e}):null]}),a.jsx("div",{className:"-mb-3 divide-y divide-oai-gray-200/60 dark:divide-oai-gray-800/60",children:i})]})}function w({options:s,value:t,onChange:e}){return a.jsx("div",{className:"inline-flex items-center rounded-lg border border-oai-gray-200 bg-oai-gray-50 p-0.5 dark:border-oai-gray-800 dark:bg-oai-gray-900",children:s.map(({value:i,label:d,Icon:l})=>{const r=t===i;return a.jsxs("button",{type:"button",onClick:()=>e(i),"aria-pressed":r,className:x("inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors",r?"bg-white text-oai-black shadow-sm dark:bg-oai-gray-800 dark:text-white":"text-oai-gray-500 hover:text-oai-black dark:text-oai-gray-400 dark:hover:text-white"),children:[l?a.jsx(l,{className:"h-3.5 w-3.5","aria-hidden":!0}):null,a.jsx("span",{children:d})]},i)})})}function S(){const[s,t]=n.useState(null),e=g()&&m();n.useEffect(()=>{if(!e)return;const r=b(o=>t(o));return u(),r},[e]);const i=n.useCallback((r,o)=>{e&&(t(c=>c&&{...c,[r]:o}),f(r,o))},[e]),d=n.useCallback(r=>{e&&h(r)},[e]),l=n.useCallback(()=>{e&&u()},[e]);return{available:e,settings:s,setSetting:i,runAction:d,refresh:l}}export{N as S,p as T,j as a,w as b,S as u};
@@ -1 +1 @@
1
- import{aM as t,aN as o,r,aO as s}from"./main-uV9_FTHb.js";function u(){!t.current&&o();const[e]=r.useState(s.current);return e}export{u};
1
+ import{aM as t,aN as o,r,aO as s}from"./main-uaLO1Ae7.js";function u(){!t.current&&o();const[e]=r.useState(s.current);return e}export{u};
@@ -1 +1 @@
1
- import{r,al as l}from"./main-uV9_FTHb.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,al as l}from"./main-uaLO1Ae7.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 +1 @@
1
- import{r as e,aA as t,aB as r,I as c}from"./main-uV9_FTHb.js";const a=Object.freeze({currency:c,rate:1,symbol:"$",rates:{...r},rateSource:"default",rateFetchedAt:null,setCurrency:()=>{}});function u(){return e.useContext(t)??a}export{u};
1
+ import{r as e,aA as t,aB as r,I as c}from"./main-uaLO1Ae7.js";const a=Object.freeze({currency:c,rate:1,symbol:"$",rates:{...r},rateSource:"default",rateFetchedAt:null,setCurrency:()=>{}});function u(){return e.useContext(t)??a}export{u};
@@ -210,8 +210,8 @@
210
210
  ]
211
211
  }
212
212
  </script>
213
- <script type="module" crossorigin src="/assets/main-uV9_FTHb.js"></script>
214
- <link rel="stylesheet" crossorigin href="/assets/main-0bSXJNLn.css">
213
+ <script type="module" crossorigin src="/assets/main-uaLO1Ae7.js"></script>
214
+ <link rel="stylesheet" crossorigin href="/assets/main-JP_EYeq-.css">
215
215
  </head>
216
216
  <body>
217
217
  <main class="aeo-seed-content" aria-label="Token Tracker AI-readable summary">
@@ -51,8 +51,8 @@
51
51
  "description": "Shareable Token Tracker dashboard snapshot."
52
52
  }
53
53
  </script>
54
- <script type="module" crossorigin src="/assets/main-uV9_FTHb.js"></script>
55
- <link rel="stylesheet" crossorigin href="/assets/main-0bSXJNLn.css">
54
+ <script type="module" crossorigin src="/assets/main-uaLO1Ae7.js"></script>
55
+ <link rel="stylesheet" crossorigin href="/assets/main-JP_EYeq-.css">
56
56
  </head>
57
57
  <body>
58
58
  <main class="aeo-seed-content" aria-label="Token Tracker share page summary">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokentracker-cli",
3
- "version": "0.23.1",
3
+ "version": "0.24.0",
4
4
  "description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Gemini, Kiro, OpenCode, OpenClaw, Every Code, Hermes, GitHub Copilot, Kimi Code, CodeBuddy, Grok Build, oh-my-pi, pi, Craft Agents, Kilo CLI, Kilo Code, Roo Code, Zed Agent, Goose)",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -63,7 +63,7 @@ function printHelp() {
63
63
  "",
64
64
  "Usage:",
65
65
  " npx tokentracker Open local dashboard",
66
- " npx tokentracker [--debug] serve [--port 7680] [--no-open] [--no-sync]",
66
+ " npx tokentracker [--debug] serve [--port 7680] [--allowed-hosts <host[,host...]>] [--no-open] [--no-sync]",
67
67
  " npx tokentracker [--debug] init [--yes] [--dry-run] [--no-open] [--link-code <code>]",
68
68
  " npx tokentracker [--debug] sync [--auto] [--drain] [--from-openclaw]",
69
69
  " npx tokentracker [--debug] status [--probe-keychain] [--probe-keychain-details]",
@@ -82,6 +82,7 @@ function printHelp() {
82
82
  " - OpenClaw hook auto-links when OpenClaw is installed (requires gateway restart).",
83
83
  " - auto sync waits for a device token.",
84
84
  " - optional: --dashboard-url for hosted landing.",
85
+ " - serve: --allowed-hosts marks extra hostnames as local-dashboard auth proxy origins (for tunnels/previews).",
85
86
  " - sync parses ~/.codex/sessions/**/rollout-*.jsonl and ~/.code/sessions/**/rollout-*.jsonl, then uploads token deltas.",
86
87
  " - --from-openclaw marks sync runs triggered by OpenClaw hooks.",
87
88
  " - --debug shows original backend errors.",
@@ -87,7 +87,7 @@ async function cmdServe(argv) {
87
87
  }
88
88
 
89
89
  // 3. Create handler
90
- const handleApi = createLocalApiHandler({ queuePath });
90
+ const handleApi = createLocalApiHandler({ queuePath, allowedHosts: opts.allowedHosts });
91
91
 
92
92
  const server = http.createServer(async (req, res) => {
93
93
  try {
@@ -104,6 +104,11 @@ async function cmdServe(argv) {
104
104
  }
105
105
 
106
106
  // API routes
107
+ if (url.pathname === "/api/dashboard-config") {
108
+ serveDashboardConfig(res, { allowedHosts: opts.allowedHosts });
109
+ return;
110
+ }
111
+
107
112
  if (
108
113
  url.pathname.startsWith("/functions/")
109
114
  || url.pathname.startsWith("/api/")
@@ -224,19 +229,66 @@ function resolveDashboardDir() {
224
229
  return null;
225
230
  }
226
231
 
232
+ function splitHostList(value) {
233
+ if (Array.isArray(value)) return value.flatMap(splitHostList);
234
+ if (typeof value !== "string") return [];
235
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
236
+ }
237
+
238
+ function normalizeAllowedHost(value) {
239
+ if (typeof value !== "string") return null;
240
+ const raw = value.trim();
241
+ if (!raw || raw.includes("*") || /\s/.test(raw)) return null;
242
+ try {
243
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `http://${raw}`;
244
+ const url = new URL(withScheme);
245
+ if (!url.hostname || url.username || url.password) return null;
246
+ return url.hostname.toLowerCase();
247
+ } catch (_e) {
248
+ return null;
249
+ }
250
+ }
251
+
252
+ function normalizeAllowedHosts(values) {
253
+ const out = [];
254
+ const seen = new Set();
255
+ for (const item of splitHostList(values)) {
256
+ const host = normalizeAllowedHost(item);
257
+ if (!host || seen.has(host)) continue;
258
+ seen.add(host);
259
+ out.push(host);
260
+ }
261
+ return out;
262
+ }
263
+
264
+ function serveDashboardConfig(res, { allowedHosts } = {}) {
265
+ const body = Buffer.from(JSON.stringify({ allowedHosts: normalizeAllowedHosts(allowedHosts) }), "utf8");
266
+ res.writeHead(200, {
267
+ "Content-Type": "application/json; charset=utf-8",
268
+ "Content-Length": body.length,
269
+ "Cache-Control": "no-store",
270
+ });
271
+ res.end(body);
272
+ }
273
+
227
274
  function parseArgs(argv) {
228
- const opts = { port: DEFAULT_PORT, open: true, sync: true };
275
+ const opts = { port: DEFAULT_PORT, open: true, sync: true, allowedHosts: [] };
229
276
  for (let i = 0; i < argv.length; i++) {
230
277
  const arg = argv[i];
231
278
  if (arg === "--port" && i + 1 < argv.length) {
232
279
  const n = parseInt(argv[++i], 10);
233
280
  if (Number.isFinite(n) && n > 0 && n < 65536) opts.port = n;
281
+ } else if (arg === "--allowed-hosts" && i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
282
+ opts.allowedHosts.push(...normalizeAllowedHosts(argv[++i]));
283
+ } else if (arg.startsWith("--allowed-hosts=")) {
284
+ opts.allowedHosts.push(...normalizeAllowedHosts(arg.slice("--allowed-hosts=".length)));
234
285
  } else if (arg === "--no-open") {
235
286
  opts.open = false;
236
287
  } else if (arg === "--no-sync") {
237
288
  opts.sync = false;
238
289
  }
239
290
  }
291
+ opts.allowedHosts = normalizeAllowedHosts(opts.allowedHosts);
240
292
  return opts;
241
293
  }
242
294
 
@@ -246,4 +298,7 @@ module.exports = {
246
298
  NPM_PACKAGE_NAME,
247
299
  LOCAL_BIND_HOST,
248
300
  getLocalServerUrl,
301
+ parseArgs,
302
+ normalizeAllowedHosts,
303
+ serveDashboardConfig,
249
304
  };
@@ -457,13 +457,45 @@ function isLoopbackHostname(hostname) {
457
457
  return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1" || hostname === "[::1]";
458
458
  }
459
459
 
460
- function hasAllowedLoopbackOrigin(headers = {}) {
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));
461
489
  const candidates = [headers.origin, headers.referer];
462
490
  for (const raw of candidates) {
463
491
  if (raw == null || raw === "") continue;
464
492
  try {
465
493
  const url = new URL(String(raw));
466
- if (url.protocol !== "http:" || !isLoopbackHostname(url.hostname)) return false;
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;
467
499
  } catch (_e) {
468
500
  return false;
469
501
  }
@@ -641,35 +673,25 @@ function json(res, data, status) {
641
673
  }
642
674
 
643
675
  // ---------------------------------------------------------------------------
644
- // IP check proxy (issue #81): ip.net.coffee/claude/ sets X-Frame-Options:
645
- // SAMEORIGIN so the dashboard iframe is blocked. We reverse-proxy through
646
- // /proxy/ipcheck/* so requests stay same-origin to 127.0.0.1, then strip the
647
- // embedding-hostile headers and rewrite root-relative URLs in HTML so the
648
- // page's own /api/* and /claude/* fetches route back through us.
676
+ // IP check API proxy: dashboard/src/pages/IpCheckPage.jsx is a native React
677
+ // page that calls ip.net.coffee's data endpoints (/api/iprisk, /api/geoip,
678
+ // /api/dns/result, /favicons, /claude/status.json). Browser-side fetch can't
679
+ // hit them cross-origin from the dashboard, so we reverse-proxy /proxy/ipcheck/*
680
+ // to https://ip.net.coffee/* and strip embedding-hostile headers.
681
+ // (Previously this proxy also served the upstream HTML page for an iframe;
682
+ // the iframe and its HTML-rewrite path have been removed.)
649
683
  // ---------------------------------------------------------------------------
650
684
 
651
685
  const IP_CHECK_PROXY_PREFIX = "/proxy/ipcheck";
652
686
  const IP_CHECK_TARGET = "https://ip.net.coffee";
653
687
 
654
- function rewriteIpCheckHtml(html) {
655
- const prefix = IP_CHECK_PROXY_PREFIX;
656
- return html
657
- .replace(
658
- /(\s(?:href|src|action)\s*=\s*["'])\/(?!\/|proxy\/ipcheck\/)/g,
659
- `$1${prefix}/`,
660
- )
661
- .replace(
662
- /(fetch\s*\(\s*["'`])\/(?!\/|proxy\/ipcheck\/)/g,
663
- `$1${prefix}/`,
664
- );
665
- }
666
-
667
688
  // ---------------------------------------------------------------------------
668
689
  // Main handler factory
669
690
  // ---------------------------------------------------------------------------
670
691
 
671
- function createLocalApiHandler({ queuePath }) {
692
+ function createLocalApiHandler({ queuePath, allowedHosts = [] } = {}) {
672
693
  const qp = queuePath || resolveQueuePath();
694
+ const allowedLocalOriginHosts = normalizeAllowedHosts(allowedHosts);
673
695
 
674
696
  // Server-side cookie relay: captures auth cookies from InsForge cloud responses
675
697
  // so that both browser and WKWebView share the same login session via the proxy.
@@ -804,9 +826,10 @@ function createLocalApiHandler({ queuePath }) {
804
826
  return typeof value === "string" ? value : "";
805
827
  }
806
828
 
807
- function buildRelayCookieHeader(clientCookieHeader) {
829
+ function buildRelayCookieHeader(clientCookieHeader, { relayPrecedenceNames = [] } = {}) {
808
830
  const normalizedClientCookieHeader = normalizeCookieHeader(clientCookieHeader);
809
831
  if (relayCookies.size === 0) return normalizedClientCookieHeader;
832
+ const relayPrecedence = new Set(relayPrecedenceNames);
810
833
  const clientPairs = new Map();
811
834
  if (normalizedClientCookieHeader) {
812
835
  for (const part of normalizedClientCookieHeader.split(";")) {
@@ -816,9 +839,10 @@ function createLocalApiHandler({ queuePath }) {
816
839
  if (n) clientPairs.set(n, part.trim());
817
840
  }
818
841
  }
819
- // Merge relay cookies (client takes precedence)
842
+ // Merge relay cookies. Normal requests keep client precedence; refresh
843
+ // recovery can opt relay cookies into precedence over stale WebView cookies.
820
844
  for (const [name, raw] of relayCookies) {
821
- if (clientPairs.has(name)) continue;
845
+ if (clientPairs.has(name) && !relayPrecedence.has(name)) continue;
822
846
  const scIdx = raw.indexOf(";");
823
847
  const pair = scIdx > 0 ? raw.substring(0, scIdx).trim() : raw;
824
848
  clientPairs.set(name, pair);
@@ -839,7 +863,7 @@ function createLocalApiHandler({ queuePath }) {
839
863
  ? headerToken.trim()
840
864
  : cookieToken || "";
841
865
  if (!token || token !== localAuthToken) return false;
842
- return hasAllowedLoopbackOrigin(req?.headers || {});
866
+ return hasAllowedLoopbackOrigin(req?.headers || {}, allowedLocalOriginHosts);
843
867
  }
844
868
 
845
869
  return async function handleLocalApi(req, res, url) {
@@ -897,7 +921,7 @@ function createLocalApiHandler({ queuePath }) {
897
921
  const hasClientCookie = normalizeCookieHeader(proxyHeaders["cookie"]).trim().length > 0;
898
922
  const hasCsrfHeader = typeof proxyHeaders["x-csrf-token"] === "string" && proxyHeaders["x-csrf-token"].trim().length > 0;
899
923
  const relayCsrfToken = getRelayCookieValue(csrfRelayCookieName);
900
- if (p === "/api/auth/refresh" && !hasCsrfHeader && relayCsrfToken) {
924
+ if (p === "/api/auth/refresh" && relayCsrfToken) {
901
925
  proxyHeaders["x-csrf-token"] = relayCsrfToken;
902
926
  }
903
927
  const hasEffectiveCsrfHeader =
@@ -918,7 +942,11 @@ function createLocalApiHandler({ queuePath }) {
918
942
  // Invalid CSRF errors on startup.
919
943
  const originalCookieHeader = normalizeCookieHeader(proxyHeaders["cookie"]);
920
944
  const mergedCookie = shouldInjectRelayCookies
921
- ? buildRelayCookieHeader(originalCookieHeader)
945
+ ? buildRelayCookieHeader(originalCookieHeader, {
946
+ relayPrecedenceNames: p === "/api/auth/refresh"
947
+ ? [csrfRelayCookieName, "insforge_refresh_token"]
948
+ : [],
949
+ })
922
950
  : originalCookieHeader;
923
951
  const injectedRelayCookies =
924
952
  shouldInjectRelayCookies && relayCookies.size > 0 && mergedCookie !== originalCookieHeader;
@@ -976,34 +1004,49 @@ function createLocalApiHandler({ queuePath }) {
976
1004
  return true;
977
1005
  }
978
1006
 
979
- // --- ip-check proxy: reverse-proxy ip.net.coffee/claude/ (issue #81) ---
1007
+ // --- ip-check proxy: reverse-proxy ip.net.coffee (issue #81) ---
1008
+ // Lock-down: GET/HEAD only, restricted path prefixes, do not forward
1009
+ // browser credentials or fingerprintable headers. Without these limits
1010
+ // /proxy/ipcheck is an open reverse-proxy any local process can abuse
1011
+ // (exfiltrate dashboard cookies, anonymously POST through user IP).
980
1012
  if (p.startsWith(`${IP_CHECK_PROXY_PREFIX}/`) || p === IP_CHECK_PROXY_PREFIX) {
1013
+ const method = String(req.method || "GET").toUpperCase();
1014
+ if (method !== "GET" && method !== "HEAD") {
1015
+ json(res, { error: "Method Not Allowed" }, 405);
1016
+ return true;
1017
+ }
981
1018
  const targetPath = p === IP_CHECK_PROXY_PREFIX
982
1019
  ? "/"
983
1020
  : p.slice(IP_CHECK_PROXY_PREFIX.length) || "/";
1021
+ const ALLOWED_PREFIXES = [
1022
+ "/api/geoip/",
1023
+ "/api/geoip-batch",
1024
+ "/api/iprisk/",
1025
+ "/api/dns/result/",
1026
+ "/claude/status.json",
1027
+ "/favicons/",
1028
+ "/ip/",
1029
+ ];
1030
+ if (!ALLOWED_PREFIXES.some((prefix) => targetPath.startsWith(prefix))) {
1031
+ json(res, { error: "Path not allowed" }, 403);
1032
+ return true;
1033
+ }
984
1034
  const targetUrl = `${IP_CHECK_TARGET}${targetPath}${url.search || ""}`;
985
1035
  try {
986
- const proxyHeaders = {};
987
- for (const [key, value] of Object.entries(req.headers)) {
988
- const lk = key.toLowerCase();
989
- if (["host", "connection", "referer", "origin"].includes(lk)) continue;
990
- proxyHeaders[key] = value;
991
- }
992
- proxyHeaders["host"] = "ip.net.coffee";
993
- proxyHeaders["referer"] = `${IP_CHECK_TARGET}${targetPath}`;
994
-
995
- const method = String(req.method || "GET").toUpperCase();
996
- let body;
997
- if (method !== "GET" && method !== "HEAD") {
998
- const chunks = [];
999
- for await (const chunk of req) chunks.push(chunk);
1000
- if (chunks.length > 0) body = Buffer.concat(chunks);
1001
- }
1036
+ // Whitelist forwarded headers — no cookies, no auth, no fingerprintable
1037
+ // identity. Only what the upstream needs to negotiate content.
1038
+ const proxyHeaders = {
1039
+ host: "ip.net.coffee",
1040
+ accept: req.headers["accept"] || "*/*",
1041
+ "accept-language": req.headers["accept-language"] || "en",
1042
+ "accept-encoding": req.headers["accept-encoding"] || "gzip",
1043
+ "user-agent": "TokenTracker/IPCheck (https://www.tokentracker.cc)",
1044
+ referer: `${IP_CHECK_TARGET}${targetPath}`,
1045
+ };
1002
1046
 
1003
1047
  const proxyRes = await fetch(targetUrl, {
1004
1048
  method,
1005
1049
  headers: proxyHeaders,
1006
- body,
1007
1050
  redirect: "manual",
1008
1051
  });
1009
1052
 
@@ -1022,12 +1065,7 @@ function createLocalApiHandler({ queuePath }) {
1022
1065
  ([k]) => !stripped.has(k.toLowerCase()),
1023
1066
  );
1024
1067
 
1025
- const contentType = proxyRes.headers.get("content-type") || "";
1026
- let resBody = Buffer.from(await proxyRes.arrayBuffer());
1027
- if (contentType.toLowerCase().includes("text/html")) {
1028
- resBody = Buffer.from(rewriteIpCheckHtml(resBody.toString("utf8")), "utf8");
1029
- }
1030
-
1068
+ const resBody = Buffer.from(await proxyRes.arrayBuffer());
1031
1069
  res.writeHead(proxyRes.status, Object.fromEntries(responseHeaders));
1032
1070
  res.end(resBody);
1033
1071
  } catch (e) {