@magpie-community/opencode-cursor-auth 0.2.22 → 0.2.23

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 (2) hide show
  1. package/index.mjs +69 -4
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -117,6 +117,68 @@ function authFile() {
117
117
  return join(process.env.XDG_CONFIG_HOME || join(home, ".config"), "cursor", "auth.json")
118
118
  }
119
119
 
120
+ // cliConfigFile is cursor-agent's cli-config.json, where it keeps the team
121
+ // it signed in to (authInfo.activeTeamId); its directory found as the CLI
122
+ // finds it, on every OS.
123
+ function cliConfigFile() {
124
+ const dir = process.env.CURSOR_CONFIG_DIR?.trim()
125
+ || (process.env.XDG_CONFIG_HOME?.trim() ? join(process.env.XDG_CONFIG_HOME, "cursor") : join(homedir(), ".cursor"))
126
+ return join(dir, "cli-config.json")
127
+ }
128
+
129
+ // tokenSub is a JWT's subject (the auth id, "auth0|user_…"); "" when it
130
+ // doesn't say.
131
+ function tokenSub(tok) {
132
+ const parts = String(tok ?? "").split(".")
133
+ if (parts.length !== 3) return ""
134
+ try {
135
+ const sub = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"))?.sub
136
+ return typeof sub === "string" ? sub : ""
137
+ } catch {
138
+ return ""
139
+ }
140
+ }
141
+
142
+ const teamOf = (v) => (Number.isSafeInteger(v) && v > 0 ? v : undefined)
143
+
144
+ // activeTeamId is the team a request is billed to, as cursor-agent sends it
145
+ // (x-cursor-team-id): cli-config.json's authInfo.activeTeamId. Without it
146
+ // Cursor takes the token's default team, which in an organization may be one
147
+ // with on-demand spend turned off ("Team budget limit reached"). The config's
148
+ // team is only taken for the account cursor-agent signed in to: a token
149
+ // whose subject isn't its authInfo.authId, or a config with none, gets none.
150
+ // Read again after 10 s; cursor-agent rewrites the file in place, so a read
151
+ // that fails to parse keeps the last one, and only a missing file clears it.
152
+ const TEAM_TTL = 10_000
153
+ const teamRead = { at: 0, info: null }
154
+ async function activeTeamId(tok) {
155
+ if (Date.now() - teamRead.at >= TEAM_TTL) {
156
+ try {
157
+ teamRead.info = JSON.parse(await readFile(cliConfigFile(), "utf8"))?.authInfo ?? null
158
+ } catch (e) {
159
+ if (e?.code === "ENOENT") teamRead.info = null
160
+ }
161
+ teamRead.at = Date.now()
162
+ }
163
+ const info = teamRead.info
164
+ if (!info) return undefined
165
+ if (!info.authId || info.authId !== tokenSub(tok)) return undefined
166
+ return teamOf(info.activeTeamId)
167
+ }
168
+
169
+ // teamHeader names that team in a request's headers, and teamBody in
170
+ // GetFilteredUsageEvents' body; neither says anything when there is none.
171
+ // GetHardLimit and GetCurrentPeriodUsage go out with "{}", as cursor-agent
172
+ // sends them: the team rides in the header only.
173
+ async function teamHeader(tok) {
174
+ const id = await activeTeamId(tok)
175
+ return id ? { "x-cursor-team-id": String(id) } : {}
176
+ }
177
+ async function teamBody(tok) {
178
+ const id = await activeTeamId(tok)
179
+ return id ? { teamId: id } : {}
180
+ }
181
+
120
182
  // TOKEN_TTL is how long a token read from the Keychain is taken as it: a
121
183
  // sign-in reads it afresh, one made in cursor-agent is seen within it.
122
184
  // KEYCHAIN_DENIED is how long a read the user may have been asked about,
@@ -218,6 +280,7 @@ async function headersFor(tok) {
218
280
  "x-cursor-client-version": await clientVersion(),
219
281
  "x-cursor-client-type": "cli",
220
282
  "x-ghost-mode": "true",
283
+ ...await teamHeader(tok),
221
284
  }
222
285
  }
223
286
 
@@ -456,7 +519,7 @@ async function hardLimit(tok) {
456
519
  try {
457
520
  const res = await fetch(API + "/aiserver.v1.DashboardService/GetHardLimit", {
458
521
  method: "POST",
459
- headers: { Authorization: `Bearer ${tok}`, "Content-Type": "application/json", "Connect-Protocol-Version": "1" },
522
+ headers: { Authorization: `Bearer ${tok}`, "Content-Type": "application/json", "Connect-Protocol-Version": "1", ...await teamHeader(tok) },
460
523
  body: "{}",
461
524
  signal: AbortSignal.timeout(15_000),
462
525
  })
@@ -481,7 +544,7 @@ async function usage(tok, ids) {
481
544
  const [res, hard] = await Promise.all([
482
545
  fetch(API + "/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
483
546
  method: "POST",
484
- headers: { Authorization: `Bearer ${tok}`, "Content-Type": "application/json", "Connect-Protocol-Version": "1" },
547
+ headers: { Authorization: `Bearer ${tok}`, "Content-Type": "application/json", "Connect-Protocol-Version": "1", ...await teamHeader(tok) },
485
548
  body: "{}",
486
549
  signal: AbortSignal.timeout(15_000),
487
550
  }),
@@ -1515,7 +1578,7 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
1515
1578
  const res = await fetch(API + "/aiserver.v1.DashboardService/GetFilteredUsageEvents", {
1516
1579
  method: "POST",
1517
1580
  headers: await headersFor(tok),
1518
- body: JSON.stringify({ teamId: 0, startDate: String(since - 60_000), endDate: String(Date.now() + 60_000), page: 1, pageSize: 50 }),
1581
+ body: JSON.stringify({ ...await teamBody(tok), startDate: String(since - 60_000), endDate: String(Date.now() + 60_000), page: 1, pageSize: 50 }),
1519
1582
  signal: abortFetch && signal ? AbortSignal.any([signal, AbortSignal.timeout(STEP_WAIT.ask)]) : AbortSignal.timeout(STEP_WAIT.ask),
1520
1583
  })
1521
1584
  if (res.status >= 400 && res.status < 500 && res.status !== 429) {
@@ -2149,6 +2212,7 @@ async function runNative({ tok, base, id, tools, conv, convID, signal, maxMode,
2149
2212
  "x-cursor-client-type": "cli", // else Cursor adds a prompt of its own
2150
2213
  "x-ghost-mode": "true", // privacy mode: nothing kept for training
2151
2214
  "x-request-id": randomUUID(),
2215
+ ...await teamHeader(tok),
2152
2216
  // none of Cursor's own tools, only the caller's
2153
2217
  "x-cursor-agent-allowed-tools": "mcp_tool_call,get_mcp_tools_tool_call",
2154
2218
  },
@@ -2237,6 +2301,7 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
2237
2301
  "x-cursor-client-type": "cli", // else Cursor adds a prompt of its own
2238
2302
  "x-ghost-mode": "true", // privacy mode: nothing kept for training
2239
2303
  "x-request-id": randomUUID(),
2304
+ ...await teamHeader(tok),
2240
2305
  // none of Cursor's own tools, only the caller's
2241
2306
  "x-cursor-agent-allowed-tools": "mcp_tool_call,get_mcp_tools_tool_call",
2242
2307
  },
@@ -2887,4 +2952,4 @@ export async function CursorAuthPlugin() {
2887
2952
  }
2888
2953
 
2889
2954
  // for tests
2890
- export const _internal = { keychain, keychainToken, nativeTransport, onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, H2, H1, h2Blocked, open, openH1, openH2, STEP_WAIT, stepUsage, usageKey, ledger, owed, flights, misses, unreadable, catalog, usable, request, modelsOf, tokensOf, maxRequired, conversationID, usageOf, sessionOf, SESSION, errorResponse, kept, poolBase, conversation, buildRun, decode, exec, fields, pb, pbValue, pbAny, frame, frames, failure, toolsOf, expiry }
2955
+ export const _internal = { keychain, keychainToken, nativeTransport, onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, resetTeam: () => { teamRead.at = 0; teamRead.info = null }, expireTeam: () => (teamRead.at = 0),H2, H1, h2Blocked, open, openH1, openH2, STEP_WAIT, stepUsage, usageKey, ledger, owed, flights, misses, unreadable, catalog, usable, request, modelsOf, tokensOf, maxRequired, conversationID, usageOf, sessionOf, SESSION, errorResponse, kept, poolBase, conversation, buildRun, decode, exec, fields, pb, pbValue, pbAny, frame, frames, failure, toolsOf, expiry }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-cursor-auth",
3
- "version": "0.2.22",
3
+ "version": "0.2.23",
4
4
  "description": "Cursor subscriptions (Pro, Pro+, Ultra, Teams) in OpenCode and magpie, through the API cursor-agent talks to",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",