tokentracker-cli 0.22.1 → 0.22.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +5 -3
  2. package/README.zh-CN.md +5 -3
  3. package/dashboard/dist/assets/{Card-DdDoM-Gd.js → Card-Ce7Bq6YO.js} +1 -1
  4. package/dashboard/dist/assets/{DashboardPage-DqsLRhn6.js → DashboardPage-K_pYvsl8.js} +1 -1
  5. package/dashboard/dist/assets/{DevicePage-Dz2WacAK.js → DevicePage-B0t441Pp.js} +1 -1
  6. package/dashboard/dist/assets/{FadeIn-CoY8tCtI.js → FadeIn-rL5OMeU2.js} +1 -1
  7. package/dashboard/dist/assets/{HeaderGithubStar-hr5WjsdY.js → HeaderGithubStar-CGS4pFqm.js} +1 -1
  8. package/dashboard/dist/assets/{IpCheckPage-Dkp3KLz5.js → IpCheckPage-Dg8ZSzBs.js} +1 -1
  9. package/dashboard/dist/assets/{LandingPage-BrXJJj3l.js → LandingPage-BRTdgQhg.js} +1 -1
  10. package/dashboard/dist/assets/{LeaderboardPage-DvrAcWn2.js → LeaderboardPage-DMQ92NBo.js} +1 -1
  11. package/dashboard/dist/assets/{LeaderboardProfilePage-KRQhdatN.js → LeaderboardProfilePage-BYY0tPSp.js} +1 -1
  12. package/dashboard/dist/assets/{LimitsPage-Ds-pKbQw.js → LimitsPage-IguYTVRL.js} +1 -1
  13. package/dashboard/dist/assets/{LoginPage-t-VKaIRw.js → LoginPage-Ct_y3yvb.js} +1 -1
  14. package/dashboard/dist/assets/{PopoverPopup-Cyf_sjKX.js → PopoverPopup-BKxuCZoU.js} +1 -1
  15. package/dashboard/dist/assets/{ProviderIcon-Dqb9ThUH.js → ProviderIcon-BW8DsxOn.js} +1 -1
  16. package/dashboard/dist/assets/{SettingsPage-DhiG9bbD.js → SettingsPage-BxDpsq6h.js} +1 -1
  17. package/dashboard/dist/assets/{SkillsPage-BdtuTu9f.js → SkillsPage-DGrfA2Ts.js} +1 -1
  18. package/dashboard/dist/assets/{WidgetsPage--n-BUbXK.js → WidgetsPage-DYLXbN7i.js} +1 -1
  19. package/dashboard/dist/assets/{WrappedPage-DRoff4y0.js → WrappedPage-C-NcEsy1.js} +1 -1
  20. package/dashboard/dist/assets/check-fi-rlObU.js +1 -0
  21. package/dashboard/dist/assets/{chevron-down-DvuPNrFG.js → chevron-down-sxrI4ACd.js} +1 -1
  22. package/dashboard/dist/assets/{download-LwtMKzsl.js → download-tZXJTa2X.js} +1 -1
  23. package/dashboard/dist/assets/{leaderboard-columns-C01mJBwn.js → leaderboard-columns-DjTC-eio.js} +1 -1
  24. package/dashboard/dist/assets/main-D55hG1yw.css +1 -0
  25. package/dashboard/dist/assets/{main-Csid8osN.js → main-DsrMFwQm.js} +2 -2
  26. package/dashboard/dist/assets/{use-limits-display-prefs-BP17xuqs.js → use-limits-display-prefs-CzOLYgu5.js} +1 -1
  27. package/dashboard/dist/assets/{use-native-settings-C-RMuUxI.js → use-native-settings-D6YjGLZJ.js} +1 -1
  28. package/dashboard/dist/assets/{use-reduced-motion-CPJKbom2.js → use-reduced-motion-0uADS5dP.js} +1 -1
  29. package/dashboard/dist/assets/{use-usage-limits-DIE-GP0H.js → use-usage-limits-xh-A-nDC.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/lib/cursor-config.js +21 -8
  34. package/src/lib/local-api.js +82 -8
  35. package/src/lib/pricing/seed-snapshot.json +1 -1
  36. package/src/lib/skills-manager.js +2 -0
  37. package/dashboard/dist/assets/check-BuIsQw1B.js +0 -1
  38. package/dashboard/dist/assets/main-A_x5MMU-.css +0 -1
@@ -1 +1 @@
1
- import{r as a}from"./main-Csid8osN.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-DsrMFwQm.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,a5 as x,r as n,aC as g,aD as m,aE as b,aF as u,aG as f,aA as h}from"./main-Csid8osN.js";import{C as y}from"./Card-DdDoM-Gd.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,a5 as x,r as n,aC as g,aD as m,aE as b,aF as u,aG as f,aA as h}from"./main-DsrMFwQm.js";import{C as y}from"./Card-Ce7Bq6YO.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{aH as t,aI as o,r,aJ as s}from"./main-Csid8osN.js";function u(){!t.current&&o();const[e]=r.useState(s.current);return e}export{u};
1
+ import{aH as t,aI as o,r,aJ as s}from"./main-DsrMFwQm.js";function u(){!t.current&&o();const[e]=r.useState(s.current);return e}export{u};
@@ -1 +1 @@
1
- import{r,aj as l}from"./main-Csid8osN.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,aj as l}from"./main-DsrMFwQm.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};
@@ -210,8 +210,8 @@
210
210
  ]
211
211
  }
212
212
  </script>
213
- <script type="module" crossorigin src="/assets/main-Csid8osN.js"></script>
214
- <link rel="stylesheet" crossorigin href="/assets/main-A_x5MMU-.css">
213
+ <script type="module" crossorigin src="/assets/main-DsrMFwQm.js"></script>
214
+ <link rel="stylesheet" crossorigin href="/assets/main-D55hG1yw.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-Csid8osN.js"></script>
55
- <link rel="stylesheet" crossorigin href="/assets/main-A_x5MMU-.css">
54
+ <script type="module" crossorigin src="/assets/main-DsrMFwQm.js"></script>
55
+ <link rel="stylesheet" crossorigin href="/assets/main-D55hG1yw.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.22.1",
3
+ "version": "0.22.3",
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": {
@@ -96,9 +96,12 @@ function readCursorAccessTokenFromStateDb(stateDbPath, deps = {}) {
96
96
  * Extract Cursor session cookie from local SQLite + cli-config.json.
97
97
  * Returns { cookie, userId } or null on failure.
98
98
  *
99
- * Cookie format: WorkosCursorSessionToken=user_XXXXX%3A%3A<jwt>
99
+ * Cookie format: WorkosCursorSessionToken=<userId>%3A%3A<jwt>
100
100
  * - JWT from state.vscdb → ItemTable → cursorAuth/accessToken
101
- * - userId from cli-config.json → authInfo.authId → "auth0|user_XXXXX"
101
+ * - userId from cli-config.json → authInfo.authId
102
+ * - native Cursor email/password: "auth0|user_XXXXX" → "user_XXXXX"
103
+ * - Google sign-in via WorkOS: "google-oauth2|<numeric>" → kept verbatim
104
+ * - other WorkOS subjects: "github|…", "oidc|…" → kept verbatim
102
105
  */
103
106
  function extractCursorSessionToken({ home, deps } = {}) {
104
107
  const { stateDbPath, cliConfigPath } = resolveCursorPaths({ home });
@@ -123,12 +126,24 @@ function extractCursorSessionToken({ home, deps } = {}) {
123
126
  return { cookie, userId };
124
127
  }
125
128
 
129
+ // WorkOS OAuth subject prefixes Cursor accepts as-is in the session cookie.
130
+ // Verified against cursor.com/api/usage-summary (issue #88).
131
+ const WORKOS_OAUTH_SUBJECT_RE = /^(google-oauth2|github|oidc|auth0)\|[^|]+$/;
132
+
133
+ function normalizeCursorSubject(subject) {
134
+ if (!subject) return null;
135
+ // Native Cursor: "auth0|user_XXXXX" → strip provider prefix, return "user_XXXXX"
136
+ const native = subject.match(/\|(user_[A-Za-z0-9_]+)$/);
137
+ if (native) return native[1];
138
+ // WorkOS-bridged OAuth: keep the full "<provider>|<id>" subject
139
+ if (WORKOS_OAUTH_SUBJECT_RE.test(subject)) return subject;
140
+ return null;
141
+ }
142
+
126
143
  function extractUserIdFromCliConfig(configPath) {
127
144
  try {
128
145
  const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
129
- const authId = config?.authInfo?.authId || "";
130
- const match = authId.match(/\|(user_[A-Za-z0-9_]+)/);
131
- return match ? match[1] : null;
146
+ return normalizeCursorSubject(config?.authInfo?.authId || "");
132
147
  } catch {
133
148
  return null;
134
149
  }
@@ -139,9 +154,7 @@ function extractUserIdFromJwt(jwt) {
139
154
  const parts = jwt.split(".");
140
155
  if (parts.length !== 3) return null;
141
156
  const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
142
- const sub = payload.sub || "";
143
- const match = sub.match(/(user_[A-Za-z0-9_]+)/);
144
- return match ? match[1] : null;
157
+ return normalizeCursorSubject(payload.sub || "");
145
158
  } catch {
146
159
  return null;
147
160
  }
@@ -674,6 +674,7 @@ function createLocalApiHandler({ queuePath }) {
674
674
  // Server-side cookie relay: captures auth cookies from InsForge cloud responses
675
675
  // so that both browser and WKWebView share the same login session via the proxy.
676
676
  // Persisted to disk so cookies survive server restarts.
677
+ const csrfRelayCookieName = "insforge_csrf_token";
677
678
  let relayCookies = new Map();
678
679
  const localAuthToken = crypto.randomBytes(24).toString("hex");
679
680
  const trackerDataDir = path.join(os.homedir(), ".tokentracker", "tracker");
@@ -755,6 +756,49 @@ function createLocalApiHandler({ queuePath }) {
755
756
  if (changed) persistRelayCookies();
756
757
  }
757
758
 
759
+ function getRelayCookieValue(name, { decode = false } = {}) {
760
+ const raw = relayCookies.get(name);
761
+ if (!raw || typeof raw !== "string") return "";
762
+ const pair = raw.split(";")[0] || "";
763
+ const eqIdx = pair.indexOf("=");
764
+ if (eqIdx < 1) return "";
765
+ const value = pair.substring(eqIdx + 1).trim();
766
+ if (!decode) return value;
767
+ try {
768
+ return decodeURIComponent(value);
769
+ } catch {
770
+ return value;
771
+ }
772
+ }
773
+
774
+ function captureAuthTokensFromBody(bodyBuffer, contentType) {
775
+ if (!bodyBuffer || !String(contentType || "").toLowerCase().includes("application/json")) return;
776
+ let parsed = null;
777
+ try {
778
+ parsed = JSON.parse(bodyBuffer.toString("utf8"));
779
+ } catch {
780
+ return;
781
+ }
782
+ let changed = false;
783
+ const token = typeof parsed?.csrfToken === "string" ? parsed.csrfToken.trim() : "";
784
+ if (token) {
785
+ const cookie = `${csrfRelayCookieName}=${encodeURIComponent(token)}; Path=/; SameSite=Lax`;
786
+ if (relayCookies.get(csrfRelayCookieName) !== cookie) {
787
+ relayCookies.set(csrfRelayCookieName, cookie);
788
+ changed = true;
789
+ }
790
+ }
791
+ const refreshToken = typeof parsed?.refreshToken === "string" ? parsed.refreshToken.trim() : "";
792
+ if (refreshToken) {
793
+ const cookie = `insforge_refresh_token=${encodeURIComponent(refreshToken)}; Path=/; HttpOnly; SameSite=Lax`;
794
+ if (relayCookies.get("insforge_refresh_token") !== cookie) {
795
+ relayCookies.set("insforge_refresh_token", cookie);
796
+ changed = true;
797
+ }
798
+ }
799
+ if (changed) persistRelayCookies();
800
+ }
801
+
758
802
  function normalizeCookieHeader(value) {
759
803
  if (Array.isArray(value)) return value.filter(Boolean).join("; ");
760
804
  return typeof value === "string" ? value : "";
@@ -852,25 +896,48 @@ function createLocalApiHandler({ queuePath }) {
852
896
  }
853
897
  const hasClientCookie = normalizeCookieHeader(proxyHeaders["cookie"]).trim().length > 0;
854
898
  const hasCsrfHeader = typeof proxyHeaders["x-csrf-token"] === "string" && proxyHeaders["x-csrf-token"].trim().length > 0;
855
- const shouldInjectRelayCookies =
856
- p !== "/api/auth/refresh" || hasClientCookie || hasCsrfHeader;
899
+ const relayCsrfToken = getRelayCookieValue(csrfRelayCookieName);
900
+ if (p === "/api/auth/refresh" && !hasCsrfHeader && relayCsrfToken) {
901
+ proxyHeaders["x-csrf-token"] = relayCsrfToken;
902
+ }
903
+ const hasEffectiveCsrfHeader =
904
+ hasCsrfHeader ||
905
+ (typeof proxyHeaders["x-csrf-token"] === "string" && proxyHeaders["x-csrf-token"].trim().length > 0);
906
+ let shouldInjectRelayCookies =
907
+ p !== "/api/auth/refresh" || hasClientCookie || hasEffectiveCsrfHeader;
908
+ const relayRefreshToken = getRelayCookieValue("insforge_refresh_token", { decode: true });
909
+ const shouldUseRelayRefreshFallback =
910
+ p === "/api/auth/refresh" && !hasClientCookie && !hasEffectiveCsrfHeader && relayRefreshToken;
911
+ if (shouldUseRelayRefreshFallback) {
912
+ shouldInjectRelayCookies = false;
913
+ }
857
914
 
858
915
  // Inject relay cookies so WebView benefits from browser's login session.
859
916
  // Refresh requests need either a browser cookie or an explicit CSRF token;
860
917
  // otherwise replaying a stale persisted refresh cookie just manufactures
861
918
  // Invalid CSRF errors on startup.
919
+ const originalCookieHeader = normalizeCookieHeader(proxyHeaders["cookie"]);
862
920
  const mergedCookie = shouldInjectRelayCookies
863
- ? buildRelayCookieHeader(proxyHeaders["cookie"])
864
- : normalizeCookieHeader(proxyHeaders["cookie"]);
921
+ ? buildRelayCookieHeader(originalCookieHeader)
922
+ : originalCookieHeader;
923
+ const injectedRelayCookies =
924
+ shouldInjectRelayCookies && relayCookies.size > 0 && mergedCookie !== originalCookieHeader;
865
925
  if (mergedCookie) proxyHeaders["cookie"] = mergedCookie;
866
926
 
867
927
  const bodyChunks = [];
868
928
  for await (const chunk of req) bodyChunks.push(chunk);
869
- const body = bodyChunks.length > 0 ? Buffer.concat(bodyChunks) : undefined;
870
- const proxyRes = await fetch(targetUrl, {
929
+ let proxyBody = bodyChunks.length > 0 ? Buffer.concat(bodyChunks) : undefined;
930
+ let effectiveTargetUrl = targetUrl;
931
+ if (shouldUseRelayRefreshFallback) {
932
+ effectiveTargetUrl = `${insforgeBase.replace(/\/$/, "")}/api/auth/refresh?client_type=mobile`;
933
+ proxyHeaders["content-type"] = "application/json";
934
+ delete proxyHeaders["content-length"];
935
+ proxyBody = Buffer.from(JSON.stringify({ refresh_token: relayRefreshToken }), "utf8");
936
+ }
937
+ const proxyRes = await fetch(effectiveTargetUrl, {
871
938
  method: req.method || "GET",
872
939
  headers: proxyHeaders,
873
- body,
940
+ body: proxyBody,
874
941
  credentials: "include",
875
942
  redirect: "manual",
876
943
  });
@@ -886,11 +953,18 @@ function createLocalApiHandler({ queuePath }) {
886
953
  });
887
954
  res.writeHead(proxyRes.status, Object.fromEntries(responseHeaders));
888
955
  const resBody = Buffer.from(await proxyRes.arrayBuffer());
956
+ if (proxyRes.status >= 200 && proxyRes.status < 300) {
957
+ if (p === "/api/auth/logout") {
958
+ clearRelayCookies("sign out");
959
+ } else {
960
+ captureAuthTokensFromBody(resBody, proxyRes.headers.get("content-type"));
961
+ }
962
+ }
889
963
  if (
890
964
  p === "/api/auth/refresh"
891
965
  && proxyRes.status === 403
966
+ && injectedRelayCookies
892
967
  && !hasClientCookie
893
- && !hasCsrfHeader
894
968
  && /invalid csrf token/i.test(resBody.toString("utf8"))
895
969
  ) {
896
970
  clearRelayCookies("stale refresh cookie without local CSRF context");