tokentracker-cli 1.5.7 → 1.5.9

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 (119) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-DIHKXKgm.js → AchievementBadge-DxevOMMv.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-U66jjKFf.js → AchievementsPage-wQh6dPiW.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-BAW79xKg.js → AchievementsSection-vYIgdSyO.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-IfomOw9B.js → BadgeDetailModal-isUD2b76.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-B33gSUM3.js → BotAnimated-DRT695hH.js} +1 -1
  6. package/dashboard/dist/assets/{Card-dtrkef-y.js → Card-CtiqYzpy.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-jYPgHJYE.js → CloudCheckoutPage-BjtTYFRB.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-BumdgM5w.js → CloudPage-Ca9vK72O.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-CFEeoIo3.js → CommandPalette-DFNVDGDw.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-DL3MrPmb.js → CommunityStatsModal-CBhB1FCI.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-BRdtCv2g.js → ConfirmModal-BpxbxobQ.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-Bb-_ZAsT.js → Controls-alGdX7nF.js} +1 -1
  13. package/dashboard/dist/assets/{DashboardPage-GXwt6DoC.js → DashboardPage-BzcooUrM.js} +1 -1
  14. package/dashboard/dist/assets/{DateRangePopover-CHkFCFkE.js → DateRangePopover-CV7k3TK2.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-Da753sLS.js → DevicePage-BK3-XhwI.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-4A_jISLc.js → DialogClose-D5qE4Ncq.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-bgtuNhON.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-C0oQdI9n.js → DialogTitle-DD0PN4lA.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-Bv-sB-O-.js → ElectricLogo-BkeGognF.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-Ch2AuGxY.js → FadeIn-WKLfxTCh.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-C_dy8a5K.js → HeaderGithubStar-CAe4FRv5.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-iuzo5YeN.js → HoverTooltip-By_5OH4b.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-BKEID9fw.js → IpCheckPage-CsLsYWer.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-DZt5JWZ7.js → LandingPage-Dz7Kwfno.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-CYrCQF5P.js → LeaderboardPage-Bwk5cUdL.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-B8vG2SAW.js → LeaderboardProBadge-DuuM_Qd4.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-DpAvBJPT.js → LeaderboardProfileModal-dLdTP76d.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-CofOk8GO.js → LeaderboardProfilePage-BC8q5Yiz.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-B_joQM6T.js +6 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-BBVCEm5Q.js → LocalOnlyNotice-Df1TymDu.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-DcPazHo1.js → LoginCard-CSadewRf.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-CGXPaeWD.js → LoginModal-xa_R4jkx.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-Ct6Al1pz.js → LoginPage-DQKRFd-i.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-8NPEAa-c.js → PetPage-DXjUI-Fm.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-R69ks89h.js → PopoverPopup-C9nxf0YB.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-svF44YE0.js → ProviderIcon-COeK1iE9.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-DJIYKu8C.js → ResetPasswordPage-yFhu0KjP.js} +1 -1
  38. package/dashboard/dist/assets/{Select-BVK4XZD5.js → Select-WAuTo5wB.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-Bh74D8lr.js → SelectItemText-Ft1My1rn.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-8gj2lAFp.js → SelfHostPage-BxF88Cud.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-CWPV_Mou.js → ServiceStatusPage-Bwphp1_T.js} +1 -1
  42. package/dashboard/dist/assets/{SessionsPage-Bo5plK6O.js → SessionsPage-BDZoT8S9.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-BptJrWn-.js → SettingsPage-BpQqI7sS.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-CRgGXnJS.js → SkillsPage-DPS8DP98.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-dgOy0Eo7.js → TokenGalaxy-Bqdu32RZ.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-DpGnyoFz.js → TooltipPopup-CpC_9aso.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-BEy_GbMg.js → TrendMonitor-DVWB1_L7.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-ChUelFrP.js → WidgetsPage-BQSjbWew.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-n63RsX8z.js → WrappedPage-DLQyBtR9.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-D-6XhI1J.css +1 -0
  51. package/dashboard/dist/assets/__vite-browser-external-DtVR8sgH.js +45 -0
  52. package/dashboard/dist/assets/{agent-logos-DQ1h5uQF.js → agent-logos-BcEyTuIb.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-left-Bo3Sg_cg.js → arrow-left-C8aE6WcJ.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-right-DdxkL8Ul.js → arrow-right-Dt4QZ45w.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-CpvelPD2.js → arrow-up-right-DR4MOP3X.js} +1 -1
  56. package/dashboard/dist/assets/{calendar-days-BvNVya0e.js → calendar-days-DHwLFNka.js} +1 -1
  57. package/dashboard/dist/assets/{check-tO40Glrg.js → check-D57wJVNi.js} +1 -1
  58. package/dashboard/dist/assets/{chevron-down-Dnrra3mC.js → chevron-down-1Am_McS5.js} +1 -1
  59. package/dashboard/dist/assets/{cloud-checkout-COC4zB8s.js → cloud-checkout-D1uji-1j.js} +1 -1
  60. package/dashboard/dist/assets/{composite-C0NgnRTl.js → composite-CWdL18RN.js} +1 -1
  61. package/dashboard/dist/assets/{copy-DTKN2SO4.js → copy-Bv-Of2CY.js} +1 -1
  62. package/dashboard/dist/assets/{download-CSq2Yd3D.js → download-C6CZhQJ_.js} +1 -1
  63. package/dashboard/dist/assets/{external-link-BtGedmuQ.js → external-link-DluFPan_.js} +1 -1
  64. package/dashboard/dist/assets/{flame-ChjXIFSx.js → flame-Q-O2BjcZ.js} +1 -1
  65. package/dashboard/dist/assets/{format-tokens-CXqZY114.js → format-tokens-D6xfDQk6.js} +1 -1
  66. package/dashboard/dist/assets/{gift-YFwBvsUA.js → gift-CjyNlhMb.js} +1 -1
  67. package/dashboard/dist/assets/{icons-DCiBpbK0.js → icons-C7bLOnL0.js} +1 -1
  68. package/dashboard/dist/assets/{index-B7sRX2EC.js → index-C0c77eg0.js} +1 -1
  69. package/dashboard/dist/assets/{index-Dpmdg98T.js → index-CuFScIzn.js} +1 -1
  70. package/dashboard/dist/assets/{index-XSya-vft.js → index-CxtJQkWU.js} +1 -1
  71. package/dashboard/dist/assets/{index-DMqIbTL4.js → index-dN_XEOTc.js} +1 -1
  72. package/dashboard/dist/assets/{info-PYzcoobJ.js → info-DVIccqnI.js} +1 -1
  73. package/dashboard/dist/assets/{link-2-BxSrdKyR.js → link-2-C70V-255.js} +1 -1
  74. package/dashboard/dist/assets/{loader-circle-CgvCCDTH.js → loader-circle-B-Rab-PT.js} +1 -1
  75. package/dashboard/dist/assets/{local-usage-transfer-eTzMdhbs.js → local-usage-transfer-qvjPuuZr.js} +1 -1
  76. package/dashboard/dist/assets/{modifiers.esm-B796CRz-.js → modifiers.esm-BHs-2K1Y.js} +1 -1
  77. package/dashboard/dist/assets/{monitor-smartphone-M5nbcGRy.js → monitor-smartphone-CgrOD78L.js} +1 -1
  78. package/dashboard/dist/assets/{play-12ZzPH9r.js → play-Ip40EFaD.js} +1 -1
  79. package/dashboard/dist/assets/{plus-OqDA7xZy.js → plus-CbCC4SC4.js} +1 -1
  80. package/dashboard/dist/assets/{provider-display-JlEBv1VB.js → provider-display-CZGQ6_EQ.js} +1 -1
  81. package/dashboard/dist/assets/{react-CeDcnQho.js → react-CgvoKjQY.js} +1 -1
  82. package/dashboard/dist/assets/{safePolygon-B_SGhQbo.js → safePolygon-BxArCd9t.js} +1 -1
  83. package/dashboard/dist/assets/{search-D9wz4rWp.js → search-Cc1m7nId.js} +1 -1
  84. package/dashboard/dist/assets/{server-UCMw4AQF.js → server-B9RX1Vuc.js} +1 -1
  85. package/dashboard/dist/assets/{shield-check-lJ0-Z5Ot.js → shield-check-DFuUPXVr.js} +1 -1
  86. package/dashboard/dist/assets/{skills-api-Da-WZbvW.js → skills-api-NyM68NUT.js} +1 -1
  87. package/dashboard/dist/assets/{sortable.esm-Bj1ZkbqW.js → sortable.esm-CYnTi7kq.js} +1 -1
  88. package/dashboard/dist/assets/{store-Bpwwxxsy.js → store-DDaZ-mNZ.js} +1 -1
  89. package/dashboard/dist/assets/{terminal-BHQMC4hm.js → terminal-RKmu3r3J.js} +1 -1
  90. package/dashboard/dist/assets/{trash-yTV6vhGX.js → trash-V0PYmC5p.js} +1 -1
  91. package/dashboard/dist/assets/{use-community-stats-rrE4Ohic.js → use-community-stats-48odwOSW.js} +1 -1
  92. package/dashboard/dist/assets/{use-latest-request-guard-D4Y1aCyd.js → use-latest-request-guard-CwOWDCBH.js} +1 -1
  93. package/dashboard/dist/assets/{use-limits-display-prefs-ChHr4XEF.js → use-limits-display-prefs-Lo9XLIUG.js} +1 -1
  94. package/dashboard/dist/assets/{use-native-settings-BmoCls3D.js → use-native-settings-BjPp_hnp.js} +1 -1
  95. package/dashboard/dist/assets/{use-ordered-list-BZSSYcLx.js → use-ordered-list-DdTbfk26.js} +1 -1
  96. package/dashboard/dist/assets/use-reduced-motion-C58neEWc.js +1 -0
  97. package/dashboard/dist/assets/{use-transform-CJO1b0Iu.js → use-transform-CmzDZQtp.js} +1 -1
  98. package/dashboard/dist/assets/{use-usage-limits-BbAC_LWP.js → use-usage-limits-DT684bcx.js} +1 -1
  99. package/dashboard/dist/assets/{useCurrency-B2enOMwJ.js → useCurrency-CNQ1UMsM.js} +1 -1
  100. package/dashboard/dist/assets/{usePositioner-BaoW4QRL.js → usePositioner-KdfYUR_c.js} +1 -1
  101. package/dashboard/dist/assets/{useTokenFormat-B2bbeEu-.js → useTokenFormat-CregnoYF.js} +1 -1
  102. package/dashboard/dist/assets/{zap-CT5a8-_j.js → zap-CMv1W1Qs.js} +1 -1
  103. package/dashboard/dist/index.html +2 -2
  104. package/dashboard/dist/ip-check.html +2 -2
  105. package/dashboard/dist/leaderboard.html +2 -2
  106. package/dashboard/dist/share.html +2 -2
  107. package/package.json +1 -1
  108. package/src/commands/sync.js +2 -2
  109. package/src/lib/cursor-config.js +21 -54
  110. package/src/lib/cursor-login.js +157 -0
  111. package/src/lib/local-api.js +43 -0
  112. package/src/lib/pricing/seed-snapshot.json +1 -1
  113. package/src/lib/usage-limits.js +14 -9
  114. package/dashboard/dist/assets/DialogDescription-CiEhDvt9.js +0 -1
  115. package/dashboard/dist/assets/LimitsPage-DXknvxtI.js +0 -6
  116. package/dashboard/dist/assets/__vite-browser-external-CMBWGkOq.css +0 -1
  117. package/dashboard/dist/assets/__vite-browser-external-D_JnEwiu.js +0 -45
  118. package/dashboard/dist/assets/use-reduced-motion-CKTIXp4-.js +0 -1
  119. package/src/lib/cursor-safe-storage.js +0 -119
@@ -2221,8 +2221,8 @@ async function cmdSync(argv, context = {}) {
2221
2221
  const cursorInstalled = cursorSyncDeps.isInstalled || isCursorInstalled;
2222
2222
  const cursorAuthExtractor = cursorSyncDeps.extractAuth || extractCursorSessionToken;
2223
2223
  const cursorUsageFetcher = cursorSyncDeps.fetchUsageCsv || fetchCursorUsageCsv;
2224
- if (sourceAllowed("cursor") && cursorInstalled({ home })) {
2225
- const cursorAuth = cursorAuthExtractor({ home });
2224
+ if (sourceAllowed("cursor") && cursorInstalled({ home, trackerDir })) {
2225
+ const cursorAuth = cursorAuthExtractor({ home, trackerDir });
2226
2226
  if (cursorAuth) {
2227
2227
  try {
2228
2228
  if (progress?.enabled) {
@@ -3,8 +3,8 @@ const path = require("node:path");
3
3
  const fs = require("node:fs");
4
4
 
5
5
  const { readJson } = require("./fs");
6
+ const { readCursorConnection, normalizeCursorSubject } = require("./cursor-login");
6
7
  const { readSqliteFirstValue } = require("./sqlite-reader");
7
- const { decodeCursorAccessToken } = require("./cursor-safe-storage");
8
8
 
9
9
  // ── Path resolution ──
10
10
 
@@ -33,7 +33,8 @@ function resolveCursorPaths({ home, platform = process.platform, env = process.e
33
33
  };
34
34
  }
35
35
 
36
- function isCursorInstalled({ home, platform, env } = {}) {
36
+ function isCursorInstalled({ home, platform, env, trackerDir } = {}) {
37
+ if (readCursorConnection({ home, trackerDir }).status !== "disconnected") return true;
37
38
  const { appDir } = resolveCursorPaths({ home, platform, env });
38
39
  try {
39
40
  return fs.statSync(appDir).isDirectory();
@@ -55,11 +56,7 @@ function cursorDebugLog(message, env = process.env) {
55
56
  }
56
57
 
57
58
  function readCursorAccessTokenFromStateDb(stateDbPath, deps = {}) {
58
- return readCursorStateValue(stateDbPath, CURSOR_ACCESS_TOKEN_SQL, deps);
59
- }
60
-
61
- function readCursorStateValue(stateDbPath, sql, deps = {}) {
62
- return readSqliteFirstValue(stateDbPath, sql, "value", {
59
+ return readSqliteFirstValue(stateDbPath, CURSOR_ACCESS_TOKEN_SQL, "value", {
63
60
  execFileSync: deps.execFileSync,
64
61
  requireFn: deps.requireFn,
65
62
  env: deps.env,
@@ -70,60 +67,46 @@ function readCursorStateValue(stateDbPath, sql, deps = {}) {
70
67
  });
71
68
  }
72
69
 
73
- // Identity lookup does not access the OS secret store or decrypt the token.
74
- function readCursorUserIdFromStateDb(stateDbPath, deps = {}) {
75
- const profile = readCursorStateValue(stateDbPath,
76
- "SELECT value FROM ItemTable WHERE key = 'cursorAuth/cachedScopedProfile';", deps);
77
- try {
78
- const userId = normalizeCursorSubject(JSON.parse(profile)?.authId);
79
- if (userId) return userId;
80
- } catch {
81
- // A missing or malformed cache must not prevent the membership fallback.
82
- }
83
- const membership = readCursorStateValue(stateDbPath,
84
- "SELECT value FROM ItemTable WHERE key = 'cursorAuth/stripeMembershipAuthId';", deps);
85
- return normalizeCursorSubject(membership);
86
- }
87
-
88
70
  /**
89
- * Extract Cursor session cookie from local SQLite + cli-config.json.
71
+ * Prefer TokenTracker's browser connection, then Cursor's plaintext local login.
90
72
  * Returns { cookie, userId } or null on failure.
91
73
  *
92
74
  * Cookie format: WorkosCursorSessionToken=<userId>%3A%3A<jwt>
93
- * - JWT (plaintext or safeStorage) from state.vscdb → cursorAuth/accessToken
94
- * - userId from cli-config.json → authInfo.authId, state.vscdb, or JWT subject
75
+ * - JWT from state.vscdb → ItemTable → cursorAuth/accessToken
76
+ * - userId from cli-config.json → authInfo.authId
95
77
  * - native Cursor email/password: "auth0|user_XXXXX" → "user_XXXXX"
96
78
  * - Google sign-in via WorkOS: "google-oauth2|<numeric>" → kept verbatim
97
79
  * - other WorkOS subjects: "github|…", "oidc|…" → kept verbatim
98
80
  */
99
- function extractCursorSessionToken({ home, platform, env, deps } = {}) {
100
- const auth = extractCursorAuth({ home, platform, env, deps });
81
+ function extractCursorSessionToken({ home, platform, env, deps, trackerDir } = {}) {
82
+ const auth = extractCursorAuth({ home, platform, env, deps, trackerDir });
101
83
  if (!auth) return null;
102
84
  return { cookie: auth.cookie, userId: auth.userId };
103
85
  }
104
86
 
105
87
  /**
106
88
  * Extract the Cursor web session and API bearer token from Cursor's own local
107
- * login state. The raw access token is needed for Cursor's first-party Connect
89
+ * login state or an explicit TokenTracker browser connection. The raw access token is needed for Cursor's first-party Connect
108
90
  * RPCs, while callers that only need the web cookie should continue using
109
91
  * extractCursorSessionToken so the token is not propagated unnecessarily.
110
92
  */
111
- function extractCursorAuth({ home, platform, env, deps } = {}) {
112
- const { appDir, stateDbPath, cliConfigPath } = resolveCursorPaths({ home, platform, env });
93
+ function extractCursorAuth({ home, platform, env, deps, trackerDir } = {}) {
94
+ const connection = readCursorConnection({ home, trackerDir });
95
+ if (connection.status !== "disconnected") return connection.auth || null;
96
+ const { stateDbPath, cliConfigPath } = resolveCursorPaths({ home, platform, env });
113
97
 
114
98
  // 1. Extract JWT from SQLite
115
99
  if (!fs.existsSync(stateDbPath)) {
116
100
  cursorDebugLog(`Cursor state DB not found at ${stateDbPath}`, deps?.env);
117
101
  return null;
118
102
  }
119
- const jwt = decodeCursorAccessToken(readCursorAccessTokenFromStateDb(stateDbPath, deps), {
120
- platform, appDir, deps,
121
- });
122
- if (!jwt) return null;
103
+ const jwt = readCursorAccessTokenFromStateDb(stateDbPath, deps);
104
+ // Encrypted Cursor Safe Storage values must not be treated as JWTs and must
105
+ // never trigger OS keychain decryption (see #1029 revert / #1043).
106
+ if (!jwt || jwt.length < 10 || jwt.startsWith('cursor-safe-storage:')) return null;
123
107
 
124
- // 2. New Cursor versions keep identity in SQLite instead of cli-config.json.
125
- let userId = extractUserIdFromCliConfig(cliConfigPath)
126
- || readCursorUserIdFromStateDb(stateDbPath, deps);
108
+ // 2. Extract userId — try cli-config.json first, fall back to JWT decode
109
+ let userId = extractUserIdFromCliConfig(cliConfigPath);
127
110
  if (!userId) {
128
111
  userId = extractUserIdFromJwt(jwt);
129
112
  }
@@ -134,21 +117,6 @@ function extractCursorAuth({ home, platform, env, deps } = {}) {
134
117
  return { cookie, userId, accessToken: jwt };
135
118
  }
136
119
 
137
- // WorkOS OAuth subject prefixes Cursor accepts as-is in the session cookie.
138
- // Verified against cursor.com/api/usage-summary (issue #88).
139
- const WORKOS_OAUTH_SUBJECT_RE = /^(google-oauth2|github|oidc|auth0)\|[^|]+$/;
140
-
141
- function normalizeCursorSubject(subject) {
142
- if (typeof subject !== "string" || !subject || /[\s;\x00-\x1f\x7f]/.test(subject)) return null;
143
- if (/^user_[A-Za-z0-9_]+$/.test(subject)) return subject;
144
- // Native Cursor: "auth0|user_XXXXX" → strip provider prefix, return "user_XXXXX"
145
- const native = subject.match(/\|(user_[A-Za-z0-9_]+)$/);
146
- if (native) return native[1];
147
- // WorkOS-bridged OAuth: keep the full "<provider>|<id>" subject
148
- if (WORKOS_OAUTH_SUBJECT_RE.test(subject)) return subject;
149
- return null;
150
- }
151
-
152
120
  function extractUserIdFromCliConfig(configPath) {
153
121
  try {
154
122
  const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
@@ -217,7 +185,7 @@ function resolveCursorRedirect(location, baseUrl) {
217
185
  * Fetch full usage CSV from Cursor API.
218
186
  * Returns raw CSV string or throws on error.
219
187
  */
220
- function fetchCursorUsageCsv({ cookie, timeoutMs = 30000, fetchImpl = fetch }) {
188
+ function fetchCursorUsageCsv({ cookie, timeoutMs = 90000, fetchImpl = fetch }) {
221
189
  return fetchImpl(CURSOR_CSV_URL, {
222
190
  method: "GET",
223
191
  headers: {
@@ -631,7 +599,6 @@ module.exports = {
631
599
  resolveCursorPaths,
632
600
  isCursorInstalled,
633
601
  readCursorAccessTokenFromStateDb,
634
- readCursorUserIdFromStateDb,
635
602
  extractCursorAuth,
636
603
  extractCursorSessionToken,
637
604
  fetchCursorUsageCsv,
@@ -0,0 +1,157 @@
1
+ const crypto = require("node:crypto");
2
+ const fs = require("node:fs");
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+
6
+ const LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
7
+
8
+ function cursorSessionPath({ home = os.homedir(), trackerDir } = {}) {
9
+ return path.join(trackerDir || path.join(home, ".tokentracker", "tracker"), "cursor-session.json");
10
+ }
11
+
12
+ function normalizeCursorSubject(subject) {
13
+ if (typeof subject !== "string") return null;
14
+ const native = subject.match(/\|(user_[A-Za-z0-9_]+)$/);
15
+ if (native) return native[1];
16
+ return /^(google-oauth2|github|oidc|auth0)\|[^|\s;]+$/.test(subject) ? subject : null;
17
+ }
18
+
19
+ function cursorAuthFromToken(accessToken, nowMs = Date.now()) {
20
+ if (typeof accessToken !== "string" || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(accessToken)) return null;
21
+ try {
22
+ const payload = JSON.parse(Buffer.from(accessToken.split(".")[1], "base64url").toString());
23
+ const userId = normalizeCursorSubject(payload.sub);
24
+ if (!userId || !Number.isFinite(payload.exp) || payload.exp * 1000 <= nowMs + 60_000) return null;
25
+ return {
26
+ accessToken,
27
+ userId,
28
+ cookie: `WorkosCursorSessionToken=${userId}%3A%3A${accessToken}`,
29
+ expiresAt: new Date(payload.exp * 1000).toISOString(),
30
+ source: "browser",
31
+ };
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function readCursorConnection(options = {}) {
38
+ let raw;
39
+ try {
40
+ raw = fs.readFileSync(cursorSessionPath(options), "utf8");
41
+ } catch (error) {
42
+ return { status: error.code === "ENOENT" ? "disconnected" : "expired" };
43
+ }
44
+ try {
45
+ const auth = cursorAuthFromToken(JSON.parse(raw).accessToken);
46
+ return auth ? { status: "connected", auth } : { status: "expired" };
47
+ } catch {
48
+ return { status: "expired" };
49
+ }
50
+ }
51
+
52
+ // Only TokenTracker's explicitly authorized access token is stored. The short
53
+ // synchronous commit keeps a cancelled/replaced login from writing afterwards.
54
+ function writeCursorSession(options, accessToken) {
55
+ const target = cursorSessionPath(options);
56
+ fs.mkdirSync(path.dirname(target), { recursive: true });
57
+ const temp = `${target}.${crypto.randomUUID()}.tmp`;
58
+ try {
59
+ fs.writeFileSync(temp, JSON.stringify({ accessToken }) + "\n", { mode: 0o600, flag: "wx" });
60
+ fs.renameSync(temp, target);
61
+ } finally {
62
+ try { fs.unlinkSync(temp); } catch (error) { if (error.code !== "ENOENT") throw error; }
63
+ }
64
+ }
65
+
66
+ function createCursorLogin({ trackerDir, home, fetchImpl = fetch, now = Date.now, validateSession } = {}) {
67
+ const options = { trackerDir, home };
68
+ let pending = null;
69
+
70
+ function start() {
71
+ const attemptId = crypto.randomUUID();
72
+ const verifier = crypto.randomBytes(32).toString("base64url");
73
+ const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
74
+ const url = new URL("https://cursor.com/loginDeepControl");
75
+ for (const [key, value] of Object.entries({ challenge, uuid: attemptId, mode: "login", redirectTarget: "cli", supportsSelectedTeamLogin: "true" })) {
76
+ url.searchParams.set(key, value);
77
+ }
78
+ pending = { attemptId, verifier, expiresAt: now() + LOGIN_TIMEOUT_MS, promise: null };
79
+ return { status: "pending", attemptId, authUrl: url.toString(), expiresAt: pending.expiresAt };
80
+ }
81
+
82
+ async function finishPoll(attempt) {
83
+ const expired = () => pending !== attempt || now() >= attempt.expiresAt;
84
+ try {
85
+ const url = new URL("https://api2.cursor.sh/auth/poll");
86
+ url.searchParams.set("uuid", attempt.attemptId);
87
+ url.searchParams.set("verifier", attempt.verifier);
88
+ const response = await fetchImpl(url.toString(), {
89
+ redirect: "error",
90
+ signal: AbortSignal.timeout(15_000),
91
+ headers: { Accept: "application/json" },
92
+ });
93
+ if (expired()) return { status: "expired" };
94
+ if (response.status === 404) return { status: "pending" };
95
+ if (!response.ok) {
96
+ if (response.status >= 500 || response.status === 429) return { status: "pending" };
97
+ pending = null;
98
+ return { status: "failed" };
99
+ }
100
+ const body = await response.json();
101
+ if (expired()) return { status: "expired" };
102
+ const auth = cursorAuthFromToken(body?.accessToken, now());
103
+ if (!auth) {
104
+ pending = null;
105
+ return { status: "failed" };
106
+ }
107
+ // A decoded JWT alone is not proof of a usable Cursor session.
108
+ try {
109
+ if (validateSession) await validateSession(auth);
110
+ else await require("./cursor-config").fetchCursorUsageSummary({ cookie: auth.cookie, fetchImpl });
111
+ } catch {
112
+ if (pending === attempt) pending = null;
113
+ return { status: "failed" };
114
+ }
115
+ if (expired()) return { status: "expired" };
116
+ try { writeCursorSession(options, auth.accessToken); } catch {
117
+ pending = null;
118
+ return { status: "failed" };
119
+ }
120
+ pending = null;
121
+ return { status: "connected", expiresAt: auth.expiresAt };
122
+ } catch {
123
+ return { status: expired() ? "expired" : "pending" };
124
+ }
125
+ }
126
+
127
+ function poll(attemptId) {
128
+ if (!pending || pending.attemptId !== attemptId || now() >= pending.expiresAt) {
129
+ return Promise.resolve({ status: "expired" });
130
+ }
131
+ const attempt = pending;
132
+ if (!attempt.promise) {
133
+ attempt.promise = finishPoll(attempt).finally(() => { attempt.promise = null; });
134
+ }
135
+ return attempt.promise;
136
+ }
137
+
138
+ function cancel(attemptId) {
139
+ if (pending?.attemptId === attemptId) pending = null;
140
+ return { status: "cancelled" };
141
+ }
142
+
143
+ function disconnect() {
144
+ pending = null;
145
+ try { fs.unlinkSync(cursorSessionPath(options)); } catch (error) { if (error.code !== "ENOENT") throw error; }
146
+ return { status: "disconnected" };
147
+ }
148
+
149
+ function status() {
150
+ const connection = readCursorConnection(options);
151
+ return { status: connection.status, ...(connection.auth ? { expiresAt: connection.auth.expiresAt } : {}) };
152
+ }
153
+
154
+ return { start, poll, cancel, disconnect, status };
155
+ }
156
+
157
+ module.exports = { createCursorLogin, cursorSessionPath, readCursorConnection, normalizeCursorSubject };
@@ -1166,6 +1166,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
1166
1166
  let relayCookies = new Map();
1167
1167
  const localAuthToken = crypto.randomBytes(24).toString("hex");
1168
1168
  const trackerDataDir = isolatedTrackerDataDir || path.dirname(qp);
1169
+ const cursorLogin = require("./cursor-login").createCursorLogin({ trackerDir: trackerDataDir });
1169
1170
  const cookiePath = path.join(trackerDataDir, "relay-cookies.json");
1170
1171
  const localSyncDeviceTokenStore = createCloudDeviceTokenStore(path.join(trackerDataDir, "cloud-device-token.json"));
1171
1172
  const localSyncDeviceTokenCache = new Map();
@@ -3279,6 +3280,48 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
3279
3280
  return true;
3280
3281
  }
3281
3282
 
3283
+ // Explicit Cursor browser authorization. Never return the verifier or session token.
3284
+ if (p === "/functions/tokentracker-cursor-connection") {
3285
+ if (!isAuthorizedLocalMutation(req)) {
3286
+ json(res, { error: "Unauthorized" }, 401);
3287
+ return true;
3288
+ }
3289
+ res.setHeader("Cache-Control", "no-store");
3290
+ const method = String(req.method || "GET").toUpperCase();
3291
+ if (method === "GET") {
3292
+ json(res, cursorLogin.status());
3293
+ return true;
3294
+ }
3295
+ if (method !== "POST") {
3296
+ json(res, { error: "Method Not Allowed" }, 405);
3297
+ return true;
3298
+ }
3299
+ let body;
3300
+ try { body = await readJsonBody(req); } catch {
3301
+ json(res, { error: "Invalid JSON body" }, 400);
3302
+ return true;
3303
+ }
3304
+ try {
3305
+ let result;
3306
+ switch (body?.action) {
3307
+ case "start": result = cursorLogin.start(); break;
3308
+ case "poll": result = await cursorLogin.poll(body.attemptId); break;
3309
+ case "cancel": result = cursorLogin.cancel(body.attemptId); break;
3310
+ case "disconnect": result = cursorLogin.disconnect(); break;
3311
+ default:
3312
+ json(res, { error: "Unknown Cursor connection action" }, 400);
3313
+ return true;
3314
+ }
3315
+ if (result.status === "connected" || result.status === "disconnected") {
3316
+ require("./usage-limits").resetUsageLimitsCache({ invalidateInFlight: true });
3317
+ }
3318
+ json(res, result);
3319
+ } catch {
3320
+ json(res, { error: "Could not update the Cursor connection" }, 500);
3321
+ }
3322
+ return true;
3323
+ }
3324
+
3282
3325
  // --- provider API keys saved from the Limits panel ---
3283
3326
  // The key lives in a 0600 file next to the queue, never in the launch
3284
3327
  // environment: a global `launchctl setenv` reaches every launchd app on