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.
- package/dashboard/dist/assets/{AchievementBadge-Ck1DmXtu.js → AchievementBadge-C1sNmjy1.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-DjvMoc2i.js → AchievementsPage-CEes3r9C.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DvTHE8Vd.js → AchievementsSection-DUNbD6b3.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-COa2Rd6M.js → BadgeDetailModal-Qmr7VqYE.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CeR3jSce.js → BotAnimated-jsDF2HFl.js} +1 -1
- package/dashboard/dist/assets/{Card-bQCEOe_K.js → Card-B_lOewQz.js} +1 -1
- package/dashboard/dist/assets/CloudCheckoutPage-C_JuBsD4.js +1 -0
- package/dashboard/dist/assets/{CloudPage-RygH3R8b.js → CloudPage-CzME2Zwr.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-E7YqsdAH.js → CommandPalette-C9-pZkuM.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-B5heTDUE.js → CommunityStatsModal-CykqgS9V.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-D3vd882N.js → ConfirmModal-fM615n-k.js} +1 -1
- package/dashboard/dist/assets/{Controls-CK74JkE8.js → Controls-BUgYy1q2.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-DwTrlQZQ.js → DashboardPage-TMedKsgf.js} +1 -1
- package/dashboard/dist/assets/{DateRangePopover-BxVCPuEX.js → DateRangePopover-ChqqPFBz.js} +1 -1
- package/dashboard/dist/assets/DevicePage-BVM0mnCp.js +1 -0
- package/dashboard/dist/assets/{DialogClose-teXmwU1d.js → DialogClose-BSYpqJ7I.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-C-V5YErj.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-B3m-31sy.js → DialogTitle-Ck0801ZS.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-0wyvks4K.js → ElectricLogo-Duu4aJps.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-C60pLKXu.js → FadeIn-C0nDQZXG.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-CCEUzMqo.js → HeaderGithubStar-9_Cou9eQ.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-D9RtbhSZ.js → HoverTooltip-Dko6Ml2q.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BCyW0SWY.js → IpCheckPage-CAy4LTD9.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DCEofDST.js → LandingPage-De_k34Bm.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-CsTbalIm.js → LeaderboardPage-upmKCiU2.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-Crun5RUJ.js → LeaderboardProBadge-Bp_4HRGy.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-Bim9cms6.js → LeaderboardProfileModal-CfeTu2mg.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-D9pBE7f4.js → LeaderboardProfilePage-dwGCnqt1.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-FxEd8pgt.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-F_qczacX.js → LocalOnlyNotice-DITDSNOI.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-CQm_T5K2.js → LoginCard-B0Y5nGs3.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-BUf4mPCT.js → LoginModal-C7UJchsM.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-njdEUQEX.js → LoginPage-CuSeHCfR.js} +1 -1
- package/dashboard/dist/assets/{PetPage-COczIU1k.js → PetPage-DmKwqkVp.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-DxB1k4g7.js → PopoverPopup-CUOqufwL.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BQfkoaQE.js → ProviderIcon-RNmBCqqB.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-PGRVaPTN.js → ResetPasswordPage-DzWUR5w4.js} +1 -1
- package/dashboard/dist/assets/{Select-CQVnpoBO.js → Select-8FQRH1NU.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DmAn7osh.js → SelectItemText-Dxwcx_oH.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-BUURA-Vt.js → SelfHostPage-B7-Tn3dF.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-DuzSen0Q.js → ServiceStatusPage-BsfvaKsB.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-DndGq61W.js → SessionsPage-DW87f0ht.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-CHh0yqsZ.js → SettingsPage-DGgUxidi.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-CCaYXQWN.js → SkillsPage-BVLlMuY6.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-CGxUtq9x.js → TokenGalaxy-CO6GVguL.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-B_B4qQD-.js → TooltipPopup-Bevc5eEL.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-CdRQTLZL.js → TrendMonitor-2BCVwA7W.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-DP_w32wh.js → WidgetsPage-BP2OAK7_.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-C_DCqHUH.js → WrappedPage-DHaMc2cB.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-BBQzCxHU.js +45 -0
- package/dashboard/dist/assets/__vite-browser-external-D-6XhI1J.css +1 -0
- package/dashboard/dist/assets/{agent-logos-DlnSKc2A.js → agent-logos-CJnyo7vh.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-CjLKt_5C.js → arrow-left-Cc3forJV.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-Bh-Ezs8y.js → arrow-right-CmzbBPo3.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-C87CvEJ5.js → arrow-up-right-D91fA8Iw.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-B5VHwV9j.js → calendar-days-CUz-Kohj.js} +1 -1
- package/dashboard/dist/assets/{check-D2uclldH.js → check-SYGeJ9ML.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-Beacyoub.js → chevron-down-CNtNmxJK.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-3Y7NDSG1.js → cloud-checkout-At2YeP3_.js} +1 -1
- package/dashboard/dist/assets/{composite-B28yORTL.js → composite-C01XLAWQ.js} +1 -1
- package/dashboard/dist/assets/{copy-C8fZJwms.js → copy-UBZcDOSY.js} +1 -1
- package/dashboard/dist/assets/{download-IIXYYmyo.js → download-BSroeXtu.js} +1 -1
- package/dashboard/dist/assets/{external-link-BEb0dms0.js → external-link-wc6SpmRT.js} +1 -1
- package/dashboard/dist/assets/{flame-gmfpE-PO.js → flame-Cwq8Msm_.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CKwy7NXi.js → format-tokens-BuH-wp-o.js} +1 -1
- package/dashboard/dist/assets/{gift-twpInP_O.js → gift-DmQ-nWNf.js} +1 -1
- package/dashboard/dist/assets/{icons-DQfkB5-5.js → icons-DceEefzQ.js} +1 -1
- package/dashboard/dist/assets/{index-D2-6Msd9.js → index-8CXny7rg.js} +1 -1
- package/dashboard/dist/assets/{index-ClwErGKW.js → index-CNJKLAOc.js} +1 -1
- package/dashboard/dist/assets/{index-ChVgw-dM.js → index-CuYxRxFQ.js} +1 -1
- package/dashboard/dist/assets/{index-DzUxfO5e.js → index-b-RBn81T.js} +1 -1
- package/dashboard/dist/assets/{info-BvndJDmD.js → info-BzwD8qDF.js} +1 -1
- package/dashboard/dist/assets/{link-2-BhU_5JKG.js → link-2-0X79vqnY.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-BR_62BDE.js → loader-circle-Dto5GLrC.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-CfoyR0hM.js → local-usage-transfer-DLixSavG.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-CnttlnRD.js → modifiers.esm-CL6p6jBJ.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-D4vzGxrq.js → monitor-smartphone-Bnaw35Qv.js} +1 -1
- package/dashboard/dist/assets/{play-BVWEKYTF.js → play-xmEUnE2_.js} +1 -1
- package/dashboard/dist/assets/{plus-B9GQ6VvV.js → plus-67TZHqQi.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DCFgR7FO.js → provider-display-jd8V87bA.js} +1 -1
- package/dashboard/dist/assets/{react-BKUD3UPr.js → react-B68xm5mg.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-DPIi3FGf.js → safePolygon-DiBm4xSb.js} +1 -1
- package/dashboard/dist/assets/{search-BBRJvYNz.js → search-DFZoQllp.js} +1 -1
- package/dashboard/dist/assets/{server-Cg3zBK6a.js → server-MOh3NKXh.js} +1 -1
- package/dashboard/dist/assets/{shield-check-BBzYplRb.js → shield-check-CqZ0Vz1G.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CcAQ6fJX.js → skills-api-CvOFtmbw.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-kNieGfR9.js → sortable.esm-WjS3aHfu.js} +1 -1
- package/dashboard/dist/assets/{store-DQpd_ZKv.js → store-AWGUrRH9.js} +1 -1
- package/dashboard/dist/assets/{terminal-zj_c8_SG.js → terminal-C6wQj1wD.js} +1 -1
- package/dashboard/dist/assets/{trash-Sl7eOlF7.js → trash-pcTDGbRp.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-Bg-4yvQQ.js → use-community-stats-DmweXTA6.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-COF23Bfs.js → use-latest-request-guard-tDJgPNLH.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-QOlhCOgQ.js → use-limits-display-prefs-C2cSKBpC.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-CKDqyirL.js → use-native-settings-CwhfcBmO.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-mFHPMcF_.js → use-ordered-list-BP0VcBim.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-C_uu1NvN.js +1 -0
- package/dashboard/dist/assets/{use-transform-CO5wZ0ML.js → use-transform-BSah0zOz.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-CeKjW8jG.js → use-usage-limits-BENTyA06.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-WruYNKxG.js → useCurrency-BPBG40V4.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-C3NGxtZa.js → usePositioner-f0OVujvk.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-ObRZ_T9W.js → useTokenFormat-DPbt6lUi.js} +1 -1
- package/dashboard/dist/assets/{zap-CyZ3Tf1D.js → zap-DLgyM5GN.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 +11 -21
- 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/CloudCheckoutPage-DDH-3QOd.js +0 -1
- package/dashboard/dist/assets/DevicePage-B6x5fBdU.js +0 -1
- package/dashboard/dist/assets/DialogDescription-BSCcGkVT.js +0 -1
- package/dashboard/dist/assets/LimitsPage-CFhzwKzB.js +0 -6
- package/dashboard/dist/assets/__vite-browser-external-CMBWGkOq.css +0 -1
- package/dashboard/dist/assets/__vite-browser-external-Lw3ipvYF.js +0 -45
- package/dashboard/dist/assets/use-reduced-motion-B4rpC-Zt.js +0 -1
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,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
|
-
*
|
|
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 =
|
|
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 };
|
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
|