tokentracker-cli 1.5.8 → 1.5.10

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 (120) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-Ck1DmXtu.js → AchievementBadge-C1sNmjy1.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-DjvMoc2i.js → AchievementsPage-CEes3r9C.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-DvTHE8Vd.js → AchievementsSection-DUNbD6b3.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-COa2Rd6M.js → BadgeDetailModal-Qmr7VqYE.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-CeR3jSce.js → BotAnimated-jsDF2HFl.js} +1 -1
  6. package/dashboard/dist/assets/{Card-bQCEOe_K.js → Card-B_lOewQz.js} +1 -1
  7. package/dashboard/dist/assets/CloudCheckoutPage-C_JuBsD4.js +1 -0
  8. package/dashboard/dist/assets/{CloudPage-RygH3R8b.js → CloudPage-CzME2Zwr.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-E7YqsdAH.js → CommandPalette-C9-pZkuM.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-B5heTDUE.js → CommunityStatsModal-CykqgS9V.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-D3vd882N.js → ConfirmModal-fM615n-k.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-CK74JkE8.js → Controls-BUgYy1q2.js} +1 -1
  13. package/dashboard/dist/assets/{DashboardPage-DwTrlQZQ.js → DashboardPage-TMedKsgf.js} +1 -1
  14. package/dashboard/dist/assets/{DateRangePopover-BxVCPuEX.js → DateRangePopover-ChqqPFBz.js} +1 -1
  15. package/dashboard/dist/assets/DevicePage-BVM0mnCp.js +1 -0
  16. package/dashboard/dist/assets/{DialogClose-teXmwU1d.js → DialogClose-BSYpqJ7I.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-C-V5YErj.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-B3m-31sy.js → DialogTitle-Ck0801ZS.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-0wyvks4K.js → ElectricLogo-Duu4aJps.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-C60pLKXu.js → FadeIn-C0nDQZXG.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-CCEUzMqo.js → HeaderGithubStar-9_Cou9eQ.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-D9RtbhSZ.js → HoverTooltip-Dko6Ml2q.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-BCyW0SWY.js → IpCheckPage-CAy4LTD9.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-DCEofDST.js → LandingPage-De_k34Bm.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-CsTbalIm.js → LeaderboardPage-upmKCiU2.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-Crun5RUJ.js → LeaderboardProBadge-Bp_4HRGy.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-Bim9cms6.js → LeaderboardProfileModal-CfeTu2mg.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-D9pBE7f4.js → LeaderboardProfilePage-dwGCnqt1.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-FxEd8pgt.js +6 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-F_qczacX.js → LocalOnlyNotice-DITDSNOI.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-CQm_T5K2.js → LoginCard-B0Y5nGs3.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-BUf4mPCT.js → LoginModal-C7UJchsM.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-njdEUQEX.js → LoginPage-CuSeHCfR.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-COczIU1k.js → PetPage-DmKwqkVp.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-DxB1k4g7.js → PopoverPopup-CUOqufwL.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-BQfkoaQE.js → ProviderIcon-RNmBCqqB.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-PGRVaPTN.js → ResetPasswordPage-DzWUR5w4.js} +1 -1
  38. package/dashboard/dist/assets/{Select-CQVnpoBO.js → Select-8FQRH1NU.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-DmAn7osh.js → SelectItemText-Dxwcx_oH.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-BUURA-Vt.js → SelfHostPage-B7-Tn3dF.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-DuzSen0Q.js → ServiceStatusPage-BsfvaKsB.js} +1 -1
  42. package/dashboard/dist/assets/{SessionsPage-DndGq61W.js → SessionsPage-DW87f0ht.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-CHh0yqsZ.js → SettingsPage-DGgUxidi.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-CCaYXQWN.js → SkillsPage-BVLlMuY6.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-CGxUtq9x.js → TokenGalaxy-CO6GVguL.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-B_B4qQD-.js → TooltipPopup-Bevc5eEL.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-CdRQTLZL.js → TrendMonitor-2BCVwA7W.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-DP_w32wh.js → WidgetsPage-BP2OAK7_.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-C_DCqHUH.js → WrappedPage-DHaMc2cB.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-BBQzCxHU.js +45 -0
  51. package/dashboard/dist/assets/__vite-browser-external-D-6XhI1J.css +1 -0
  52. package/dashboard/dist/assets/{agent-logos-DlnSKc2A.js → agent-logos-CJnyo7vh.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-left-CjLKt_5C.js → arrow-left-Cc3forJV.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-right-Bh-Ezs8y.js → arrow-right-CmzbBPo3.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-C87CvEJ5.js → arrow-up-right-D91fA8Iw.js} +1 -1
  56. package/dashboard/dist/assets/{calendar-days-B5VHwV9j.js → calendar-days-CUz-Kohj.js} +1 -1
  57. package/dashboard/dist/assets/{check-D2uclldH.js → check-SYGeJ9ML.js} +1 -1
  58. package/dashboard/dist/assets/{chevron-down-Beacyoub.js → chevron-down-CNtNmxJK.js} +1 -1
  59. package/dashboard/dist/assets/{cloud-checkout-3Y7NDSG1.js → cloud-checkout-At2YeP3_.js} +1 -1
  60. package/dashboard/dist/assets/{composite-B28yORTL.js → composite-C01XLAWQ.js} +1 -1
  61. package/dashboard/dist/assets/{copy-C8fZJwms.js → copy-UBZcDOSY.js} +1 -1
  62. package/dashboard/dist/assets/{download-IIXYYmyo.js → download-BSroeXtu.js} +1 -1
  63. package/dashboard/dist/assets/{external-link-BEb0dms0.js → external-link-wc6SpmRT.js} +1 -1
  64. package/dashboard/dist/assets/{flame-gmfpE-PO.js → flame-Cwq8Msm_.js} +1 -1
  65. package/dashboard/dist/assets/{format-tokens-CKwy7NXi.js → format-tokens-BuH-wp-o.js} +1 -1
  66. package/dashboard/dist/assets/{gift-twpInP_O.js → gift-DmQ-nWNf.js} +1 -1
  67. package/dashboard/dist/assets/{icons-DQfkB5-5.js → icons-DceEefzQ.js} +1 -1
  68. package/dashboard/dist/assets/{index-D2-6Msd9.js → index-8CXny7rg.js} +1 -1
  69. package/dashboard/dist/assets/{index-ClwErGKW.js → index-CNJKLAOc.js} +1 -1
  70. package/dashboard/dist/assets/{index-ChVgw-dM.js → index-CuYxRxFQ.js} +1 -1
  71. package/dashboard/dist/assets/{index-DzUxfO5e.js → index-b-RBn81T.js} +1 -1
  72. package/dashboard/dist/assets/{info-BvndJDmD.js → info-BzwD8qDF.js} +1 -1
  73. package/dashboard/dist/assets/{link-2-BhU_5JKG.js → link-2-0X79vqnY.js} +1 -1
  74. package/dashboard/dist/assets/{loader-circle-BR_62BDE.js → loader-circle-Dto5GLrC.js} +1 -1
  75. package/dashboard/dist/assets/{local-usage-transfer-CfoyR0hM.js → local-usage-transfer-DLixSavG.js} +1 -1
  76. package/dashboard/dist/assets/{modifiers.esm-CnttlnRD.js → modifiers.esm-CL6p6jBJ.js} +1 -1
  77. package/dashboard/dist/assets/{monitor-smartphone-D4vzGxrq.js → monitor-smartphone-Bnaw35Qv.js} +1 -1
  78. package/dashboard/dist/assets/{play-BVWEKYTF.js → play-xmEUnE2_.js} +1 -1
  79. package/dashboard/dist/assets/{plus-B9GQ6VvV.js → plus-67TZHqQi.js} +1 -1
  80. package/dashboard/dist/assets/{provider-display-DCFgR7FO.js → provider-display-jd8V87bA.js} +1 -1
  81. package/dashboard/dist/assets/{react-BKUD3UPr.js → react-B68xm5mg.js} +1 -1
  82. package/dashboard/dist/assets/{safePolygon-DPIi3FGf.js → safePolygon-DiBm4xSb.js} +1 -1
  83. package/dashboard/dist/assets/{search-BBRJvYNz.js → search-DFZoQllp.js} +1 -1
  84. package/dashboard/dist/assets/{server-Cg3zBK6a.js → server-MOh3NKXh.js} +1 -1
  85. package/dashboard/dist/assets/{shield-check-BBzYplRb.js → shield-check-CqZ0Vz1G.js} +1 -1
  86. package/dashboard/dist/assets/{skills-api-CcAQ6fJX.js → skills-api-CvOFtmbw.js} +1 -1
  87. package/dashboard/dist/assets/{sortable.esm-kNieGfR9.js → sortable.esm-WjS3aHfu.js} +1 -1
  88. package/dashboard/dist/assets/{store-DQpd_ZKv.js → store-AWGUrRH9.js} +1 -1
  89. package/dashboard/dist/assets/{terminal-zj_c8_SG.js → terminal-C6wQj1wD.js} +1 -1
  90. package/dashboard/dist/assets/{trash-Sl7eOlF7.js → trash-pcTDGbRp.js} +1 -1
  91. package/dashboard/dist/assets/{use-community-stats-Bg-4yvQQ.js → use-community-stats-DmweXTA6.js} +1 -1
  92. package/dashboard/dist/assets/{use-latest-request-guard-COF23Bfs.js → use-latest-request-guard-tDJgPNLH.js} +1 -1
  93. package/dashboard/dist/assets/{use-limits-display-prefs-QOlhCOgQ.js → use-limits-display-prefs-C2cSKBpC.js} +1 -1
  94. package/dashboard/dist/assets/{use-native-settings-CKDqyirL.js → use-native-settings-CwhfcBmO.js} +1 -1
  95. package/dashboard/dist/assets/{use-ordered-list-mFHPMcF_.js → use-ordered-list-BP0VcBim.js} +1 -1
  96. package/dashboard/dist/assets/use-reduced-motion-C_uu1NvN.js +1 -0
  97. package/dashboard/dist/assets/{use-transform-CO5wZ0ML.js → use-transform-BSah0zOz.js} +1 -1
  98. package/dashboard/dist/assets/{use-usage-limits-CeKjW8jG.js → use-usage-limits-BENTyA06.js} +1 -1
  99. package/dashboard/dist/assets/{useCurrency-WruYNKxG.js → useCurrency-BPBG40V4.js} +1 -1
  100. package/dashboard/dist/assets/{usePositioner-C3NGxtZa.js → usePositioner-f0OVujvk.js} +1 -1
  101. package/dashboard/dist/assets/{useTokenFormat-ObRZ_T9W.js → useTokenFormat-DPbt6lUi.js} +1 -1
  102. package/dashboard/dist/assets/{zap-CyZ3Tf1D.js → zap-DLgyM5GN.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 +11 -21
  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/CloudCheckoutPage-DDH-3QOd.js +0 -1
  115. package/dashboard/dist/assets/DevicePage-B6x5fBdU.js +0 -1
  116. package/dashboard/dist/assets/DialogDescription-BSCcGkVT.js +0 -1
  117. package/dashboard/dist/assets/LimitsPage-CFhzwKzB.js +0 -6
  118. package/dashboard/dist/assets/__vite-browser-external-CMBWGkOq.css +0 -1
  119. package/dashboard/dist/assets/__vite-browser-external-Lw3ipvYF.js +0 -45
  120. package/dashboard/dist/assets/use-reduced-motion-B4rpC-Zt.js +0 -1
@@ -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,6 +3,7 @@ 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
8
 
8
9
  // ── Path resolution ──
@@ -32,7 +33,8 @@ function resolveCursorPaths({ home, platform = process.platform, env = process.e
32
33
  };
33
34
  }
34
35
 
35
- function isCursorInstalled({ home, platform, env } = {}) {
36
+ function isCursorInstalled({ home, platform, env, trackerDir } = {}) {
37
+ if (readCursorConnection({ home, trackerDir }).status !== "disconnected") return true;
36
38
  const { appDir } = resolveCursorPaths({ home, platform, env });
37
39
  try {
38
40
  return fs.statSync(appDir).isDirectory();
@@ -66,7 +68,7 @@ function readCursorAccessTokenFromStateDb(stateDbPath, deps = {}) {
66
68
  }
67
69
 
68
70
  /**
69
- * Extract Cursor session cookie from local SQLite + cli-config.json.
71
+ * Prefer TokenTracker's browser connection, then Cursor's plaintext local login.
70
72
  * Returns { cookie, userId } or null on failure.
71
73
  *
72
74
  * Cookie format: WorkosCursorSessionToken=<userId>%3A%3A<jwt>
@@ -76,19 +78,21 @@ function readCursorAccessTokenFromStateDb(stateDbPath, deps = {}) {
76
78
  * - Google sign-in via WorkOS: "google-oauth2|<numeric>" → kept verbatim
77
79
  * - other WorkOS subjects: "github|…", "oidc|…" → kept verbatim
78
80
  */
79
- function extractCursorSessionToken({ home, platform, env, deps } = {}) {
80
- const auth = extractCursorAuth({ home, platform, env, deps });
81
+ function extractCursorSessionToken({ home, platform, env, deps, trackerDir } = {}) {
82
+ const auth = extractCursorAuth({ home, platform, env, deps, trackerDir });
81
83
  if (!auth) return null;
82
84
  return { cookie: auth.cookie, userId: auth.userId };
83
85
  }
84
86
 
85
87
  /**
86
88
  * Extract the Cursor web session and API bearer token from Cursor's own local
87
- * 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
88
90
  * RPCs, while callers that only need the web cookie should continue using
89
91
  * extractCursorSessionToken so the token is not propagated unnecessarily.
90
92
  */
91
- function extractCursorAuth({ home, platform, env, deps } = {}) {
93
+ function extractCursorAuth({ home, platform, env, deps, trackerDir } = {}) {
94
+ const connection = readCursorConnection({ home, trackerDir });
95
+ if (connection.status !== "disconnected") return connection.auth || null;
92
96
  const { stateDbPath, cliConfigPath } = resolveCursorPaths({ home, platform, env });
93
97
 
94
98
  // 1. Extract JWT from SQLite
@@ -113,20 +117,6 @@ function extractCursorAuth({ home, platform, env, deps } = {}) {
113
117
  return { cookie, userId, accessToken: jwt };
114
118
  }
115
119
 
116
- // WorkOS OAuth subject prefixes Cursor accepts as-is in the session cookie.
117
- // Verified against cursor.com/api/usage-summary (issue #88).
118
- const WORKOS_OAUTH_SUBJECT_RE = /^(google-oauth2|github|oidc|auth0)\|[^|]+$/;
119
-
120
- function normalizeCursorSubject(subject) {
121
- if (!subject) return null;
122
- // Native Cursor: "auth0|user_XXXXX" → strip provider prefix, return "user_XXXXX"
123
- const native = subject.match(/\|(user_[A-Za-z0-9_]+)$/);
124
- if (native) return native[1];
125
- // WorkOS-bridged OAuth: keep the full "<provider>|<id>" subject
126
- if (WORKOS_OAUTH_SUBJECT_RE.test(subject)) return subject;
127
- return null;
128
- }
129
-
130
120
  function extractUserIdFromCliConfig(configPath) {
131
121
  try {
132
122
  const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
@@ -195,7 +185,7 @@ function resolveCursorRedirect(location, baseUrl) {
195
185
  * Fetch full usage CSV from Cursor API.
196
186
  * Returns raw CSV string or throws on error.
197
187
  */
198
- function fetchCursorUsageCsv({ cookie, timeoutMs = 30000, fetchImpl = fetch }) {
188
+ function fetchCursorUsageCsv({ cookie, timeoutMs = 90000, fetchImpl = fetch }) {
199
189
  return fetchImpl(CURSOR_CSV_URL, {
200
190
  method: "GET",
201
191
  headers: {
@@ -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