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.
- package/dashboard/dist/assets/{AchievementBadge-DIHKXKgm.js → AchievementBadge-DxevOMMv.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-U66jjKFf.js → AchievementsPage-wQh6dPiW.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-BAW79xKg.js → AchievementsSection-vYIgdSyO.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-IfomOw9B.js → BadgeDetailModal-isUD2b76.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-B33gSUM3.js → BotAnimated-DRT695hH.js} +1 -1
- package/dashboard/dist/assets/{Card-dtrkef-y.js → Card-CtiqYzpy.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-jYPgHJYE.js → CloudCheckoutPage-BjtTYFRB.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-BumdgM5w.js → CloudPage-Ca9vK72O.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-CFEeoIo3.js → CommandPalette-DFNVDGDw.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-DL3MrPmb.js → CommunityStatsModal-CBhB1FCI.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-BRdtCv2g.js → ConfirmModal-BpxbxobQ.js} +1 -1
- package/dashboard/dist/assets/{Controls-Bb-_ZAsT.js → Controls-alGdX7nF.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-GXwt6DoC.js → DashboardPage-BzcooUrM.js} +1 -1
- package/dashboard/dist/assets/{DateRangePopover-CHkFCFkE.js → DateRangePopover-CV7k3TK2.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-Da753sLS.js → DevicePage-BK3-XhwI.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-4A_jISLc.js → DialogClose-D5qE4Ncq.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-bgtuNhON.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-C0oQdI9n.js → DialogTitle-DD0PN4lA.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-Bv-sB-O-.js → ElectricLogo-BkeGognF.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Ch2AuGxY.js → FadeIn-WKLfxTCh.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-C_dy8a5K.js → HeaderGithubStar-CAe4FRv5.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-iuzo5YeN.js → HoverTooltip-By_5OH4b.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BKEID9fw.js → IpCheckPage-CsLsYWer.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DZt5JWZ7.js → LandingPage-Dz7Kwfno.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-CYrCQF5P.js → LeaderboardPage-Bwk5cUdL.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-B8vG2SAW.js → LeaderboardProBadge-DuuM_Qd4.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-DpAvBJPT.js → LeaderboardProfileModal-dLdTP76d.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CofOk8GO.js → LeaderboardProfilePage-BC8q5Yiz.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-B_joQM6T.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-BBVCEm5Q.js → LocalOnlyNotice-Df1TymDu.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-DcPazHo1.js → LoginCard-CSadewRf.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-CGXPaeWD.js → LoginModal-xa_R4jkx.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-Ct6Al1pz.js → LoginPage-DQKRFd-i.js} +1 -1
- package/dashboard/dist/assets/{PetPage-8NPEAa-c.js → PetPage-DXjUI-Fm.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-R69ks89h.js → PopoverPopup-C9nxf0YB.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-svF44YE0.js → ProviderIcon-COeK1iE9.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DJIYKu8C.js → ResetPasswordPage-yFhu0KjP.js} +1 -1
- package/dashboard/dist/assets/{Select-BVK4XZD5.js → Select-WAuTo5wB.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-Bh74D8lr.js → SelectItemText-Ft1My1rn.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-8gj2lAFp.js → SelfHostPage-BxF88Cud.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-CWPV_Mou.js → ServiceStatusPage-Bwphp1_T.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-Bo5plK6O.js → SessionsPage-BDZoT8S9.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-BptJrWn-.js → SettingsPage-BpQqI7sS.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-CRgGXnJS.js → SkillsPage-DPS8DP98.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-dgOy0Eo7.js → TokenGalaxy-Bqdu32RZ.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-DpGnyoFz.js → TooltipPopup-CpC_9aso.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-BEy_GbMg.js → TrendMonitor-DVWB1_L7.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-ChUelFrP.js → WidgetsPage-BQSjbWew.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-n63RsX8z.js → WrappedPage-DLQyBtR9.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-D-6XhI1J.css +1 -0
- package/dashboard/dist/assets/__vite-browser-external-DtVR8sgH.js +45 -0
- package/dashboard/dist/assets/{agent-logos-DQ1h5uQF.js → agent-logos-BcEyTuIb.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-Bo3Sg_cg.js → arrow-left-C8aE6WcJ.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-DdxkL8Ul.js → arrow-right-Dt4QZ45w.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CpvelPD2.js → arrow-up-right-DR4MOP3X.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-BvNVya0e.js → calendar-days-DHwLFNka.js} +1 -1
- package/dashboard/dist/assets/{check-tO40Glrg.js → check-D57wJVNi.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-Dnrra3mC.js → chevron-down-1Am_McS5.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-COC4zB8s.js → cloud-checkout-D1uji-1j.js} +1 -1
- package/dashboard/dist/assets/{composite-C0NgnRTl.js → composite-CWdL18RN.js} +1 -1
- package/dashboard/dist/assets/{copy-DTKN2SO4.js → copy-Bv-Of2CY.js} +1 -1
- package/dashboard/dist/assets/{download-CSq2Yd3D.js → download-C6CZhQJ_.js} +1 -1
- package/dashboard/dist/assets/{external-link-BtGedmuQ.js → external-link-DluFPan_.js} +1 -1
- package/dashboard/dist/assets/{flame-ChjXIFSx.js → flame-Q-O2BjcZ.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CXqZY114.js → format-tokens-D6xfDQk6.js} +1 -1
- package/dashboard/dist/assets/{gift-YFwBvsUA.js → gift-CjyNlhMb.js} +1 -1
- package/dashboard/dist/assets/{icons-DCiBpbK0.js → icons-C7bLOnL0.js} +1 -1
- package/dashboard/dist/assets/{index-B7sRX2EC.js → index-C0c77eg0.js} +1 -1
- package/dashboard/dist/assets/{index-Dpmdg98T.js → index-CuFScIzn.js} +1 -1
- package/dashboard/dist/assets/{index-XSya-vft.js → index-CxtJQkWU.js} +1 -1
- package/dashboard/dist/assets/{index-DMqIbTL4.js → index-dN_XEOTc.js} +1 -1
- package/dashboard/dist/assets/{info-PYzcoobJ.js → info-DVIccqnI.js} +1 -1
- package/dashboard/dist/assets/{link-2-BxSrdKyR.js → link-2-C70V-255.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-CgvCCDTH.js → loader-circle-B-Rab-PT.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-eTzMdhbs.js → local-usage-transfer-qvjPuuZr.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-B796CRz-.js → modifiers.esm-BHs-2K1Y.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-M5nbcGRy.js → monitor-smartphone-CgrOD78L.js} +1 -1
- package/dashboard/dist/assets/{play-12ZzPH9r.js → play-Ip40EFaD.js} +1 -1
- package/dashboard/dist/assets/{plus-OqDA7xZy.js → plus-CbCC4SC4.js} +1 -1
- package/dashboard/dist/assets/{provider-display-JlEBv1VB.js → provider-display-CZGQ6_EQ.js} +1 -1
- package/dashboard/dist/assets/{react-CeDcnQho.js → react-CgvoKjQY.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-B_SGhQbo.js → safePolygon-BxArCd9t.js} +1 -1
- package/dashboard/dist/assets/{search-D9wz4rWp.js → search-Cc1m7nId.js} +1 -1
- package/dashboard/dist/assets/{server-UCMw4AQF.js → server-B9RX1Vuc.js} +1 -1
- package/dashboard/dist/assets/{shield-check-lJ0-Z5Ot.js → shield-check-DFuUPXVr.js} +1 -1
- package/dashboard/dist/assets/{skills-api-Da-WZbvW.js → skills-api-NyM68NUT.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-Bj1ZkbqW.js → sortable.esm-CYnTi7kq.js} +1 -1
- package/dashboard/dist/assets/{store-Bpwwxxsy.js → store-DDaZ-mNZ.js} +1 -1
- package/dashboard/dist/assets/{terminal-BHQMC4hm.js → terminal-RKmu3r3J.js} +1 -1
- package/dashboard/dist/assets/{trash-yTV6vhGX.js → trash-V0PYmC5p.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-rrE4Ohic.js → use-community-stats-48odwOSW.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-D4Y1aCyd.js → use-latest-request-guard-CwOWDCBH.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-ChHr4XEF.js → use-limits-display-prefs-Lo9XLIUG.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-BmoCls3D.js → use-native-settings-BjPp_hnp.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BZSSYcLx.js → use-ordered-list-DdTbfk26.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-C58neEWc.js +1 -0
- package/dashboard/dist/assets/{use-transform-CJO1b0Iu.js → use-transform-CmzDZQtp.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BbAC_LWP.js → use-usage-limits-DT684bcx.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-B2enOMwJ.js → useCurrency-CNQ1UMsM.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-BaoW4QRL.js → usePositioner-KdfYUR_c.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-B2bbeEu-.js → useTokenFormat-CregnoYF.js} +1 -1
- package/dashboard/dist/assets/{zap-CT5a8-_j.js → zap-CMv1W1Qs.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/sync.js +2 -2
- package/src/lib/cursor-config.js +21 -54
- package/src/lib/cursor-login.js +157 -0
- package/src/lib/local-api.js +43 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/usage-limits.js +14 -9
- package/dashboard/dist/assets/DialogDescription-CiEhDvt9.js +0 -1
- package/dashboard/dist/assets/LimitsPage-DXknvxtI.js +0 -6
- package/dashboard/dist/assets/__vite-browser-external-CMBWGkOq.css +0 -1
- package/dashboard/dist/assets/__vite-browser-external-D_JnEwiu.js +0 -45
- package/dashboard/dist/assets/use-reduced-motion-CKTIXp4-.js +0 -1
- package/src/lib/cursor-safe-storage.js +0 -119
package/src/commands/sync.js
CHANGED
|
@@ -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) {
|
package/src/lib/cursor-config.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
|
94
|
-
* - userId from cli-config.json → authInfo.authId
|
|
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
|
|
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 =
|
|
120
|
-
|
|
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.
|
|
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 =
|
|
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 };
|
package/src/lib/local-api.js
CHANGED
|
@@ -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
|