@magpie-community/opencode-cursor-auth 0.1.1 → 0.1.3
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/index.mjs +48 -25
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -207,6 +207,23 @@ async function whoIs(tok) {
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// plans are the plan each token's account is on, as `cursor-agent about`
|
|
211
|
+
// names it (GetPlanInfo's planName, magpie's card title), asked again after
|
|
212
|
+
// an hour.
|
|
213
|
+
const plans = new Map()
|
|
214
|
+
const PLAN_TTL = 3600_000
|
|
215
|
+
async function planOf(tok) {
|
|
216
|
+
const had = plans.get(tok)
|
|
217
|
+
if (had && Date.now() - had.at < PLAN_TTL) return had.plan
|
|
218
|
+
try {
|
|
219
|
+
const plan = String((await unary(API, "aiserver.v1.DashboardService/GetPlanInfo", tok))?.planInfo?.planName ?? "").trim()
|
|
220
|
+
if (plan) plans.set(tok, { plan, at: Date.now() })
|
|
221
|
+
return plan || had?.plan || ""
|
|
222
|
+
} catch {
|
|
223
|
+
return had?.plan ?? ""
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
210
227
|
// exchanged are API keys' tokens, by the key's hash.
|
|
211
228
|
const exchanged = new Map()
|
|
212
229
|
|
|
@@ -222,9 +239,9 @@ async function keyToken(key) {
|
|
|
222
239
|
body: "{}",
|
|
223
240
|
signal: AbortSignal.timeout(15_000),
|
|
224
241
|
}).catch((e) => {
|
|
225
|
-
throw Object.assign(new Error("
|
|
242
|
+
throw Object.assign(new Error("couldn't reach Cursor's API: " + e.message), { status: 502 })
|
|
226
243
|
})
|
|
227
|
-
if (res.status >= 500) throw Object.assign(new Error(`
|
|
244
|
+
if (res.status >= 500) throw Object.assign(new Error(`exchange_user_api_key: HTTP ${res.status}`), { status: 502 })
|
|
228
245
|
const j = res.ok ? await res.json().catch(() => null) : null
|
|
229
246
|
if (!j?.accessToken) throw new AuthError("Cursor didn't take this API key")
|
|
230
247
|
exchanged.set(h, { tok: j.accessToken, exp: expiry(j.accessToken) })
|
|
@@ -268,7 +285,7 @@ async function browserSignIn() {
|
|
|
268
285
|
try {
|
|
269
286
|
res = await fetch(`${API}/auth/poll?uuid=${uuid}&verifier=${verifier}`, { headers, signal: AbortSignal.timeout(15_000) })
|
|
270
287
|
} catch {
|
|
271
|
-
if (++errors >= 3) return { type: "failed" }
|
|
288
|
+
if (++errors >= 3) return { type: "failed", error: "couldn't reach Cursor" }
|
|
272
289
|
await sleep(wait)
|
|
273
290
|
continue
|
|
274
291
|
}
|
|
@@ -278,12 +295,12 @@ async function browserSignIn() {
|
|
|
278
295
|
continue
|
|
279
296
|
}
|
|
280
297
|
if (!res.ok) {
|
|
281
|
-
if (res.status === 403 || ++errors >= 3) return { type: "failed" }
|
|
298
|
+
if (res.status === 403 || ++errors >= 3) return { type: "failed", error: res.status === 403 ? "Cursor refused the sign-in (403)" : `Cursor's sign-in answered HTTP ${res.status}` }
|
|
282
299
|
await sleep(wait)
|
|
283
300
|
continue
|
|
284
301
|
}
|
|
285
302
|
const j = await res.json().catch(() => null)
|
|
286
|
-
if (!j?.accessToken || !("refreshToken" in j)) return { type: "failed" }
|
|
303
|
+
if (!j?.accessToken || !("refreshToken" in j)) return { type: "failed", error: "Cursor sent back no token" }
|
|
287
304
|
const who = await whoIs(j.accessToken)
|
|
288
305
|
return {
|
|
289
306
|
type: "success",
|
|
@@ -294,7 +311,7 @@ async function browserSignIn() {
|
|
|
294
311
|
...(who.plan ? { plan: who.plan } : {}),
|
|
295
312
|
}
|
|
296
313
|
}
|
|
297
|
-
return { type: "failed" }
|
|
314
|
+
return { type: "failed", error: "the sign-in timed out" }
|
|
298
315
|
},
|
|
299
316
|
}
|
|
300
317
|
}
|
|
@@ -309,9 +326,10 @@ async function cliSignIn() {
|
|
|
309
326
|
async callback() {
|
|
310
327
|
const tok = await cliToken()
|
|
311
328
|
const exp = expiry(tok)
|
|
312
|
-
if (!tok
|
|
329
|
+
if (!tok) return { type: "failed", error: "cursor-agent isn't signed in: run `cursor-agent login`" }
|
|
330
|
+
if (exp && exp <= Date.now()) return { type: "failed", error: "cursor-agent's sign-in has run out: run `cursor-agent login` again" }
|
|
313
331
|
const who = await whoIs(tok)
|
|
314
|
-
if (!who.email) return { type: "failed" }
|
|
332
|
+
if (!who.email) return { type: "failed", error: "Cursor couldn't say which account is signed in" }
|
|
315
333
|
return { type: "success", access: "", refresh: CLI_MARK, expires: 0, accountId: who.email, ...(who.plan ? { plan: who.plan } : {}) }
|
|
316
334
|
},
|
|
317
335
|
}
|
|
@@ -829,15 +847,21 @@ function failure(status, text) {
|
|
|
829
847
|
return statusOf(status, f.code ?? "", msg)
|
|
830
848
|
}
|
|
831
849
|
|
|
850
|
+
// signInExpired is a message of a token or session run out; a trial, a
|
|
851
|
+
// plan or a link run out is no lapsed sign-in, and magpie marks the
|
|
852
|
+
// account lapsed on a 401.
|
|
853
|
+
const signInExpired = (low) => low.includes("expired") && /token|session|sign[- ]?in|log[- ]?in|auth|credential/.test(low)
|
|
854
|
+
|
|
832
855
|
// statusOf is the status and words for Cursor's error of this code and
|
|
833
856
|
// message. A region the team isn't served in says so, not to sign in.
|
|
834
857
|
function statusOf(status, code, msg) {
|
|
835
858
|
msg ||= `HTTP ${status}`
|
|
836
859
|
const low = msg.toLowerCase()
|
|
837
|
-
|
|
860
|
+
// magpie names the provider before the message itself
|
|
861
|
+
const out = (status, message) => ({ status, message })
|
|
838
862
|
if (regional(msg)) return out(403, msg + " — Cursor serves your team only in some regions and turned this request away; signing in again won't change that")
|
|
839
863
|
if (code === "permission_denied") return out(403, msg)
|
|
840
|
-
if (code === "unauthenticated" || status === 401 || low
|
|
864
|
+
if (code === "unauthenticated" || status === 401 || signInExpired(low)) return out(401, msg + " — sign in to Cursor again")
|
|
841
865
|
if (code === "resource_exhausted" || low.includes("quota") || low.includes("rate limit") || low.includes("usage limit"))
|
|
842
866
|
return out(429, "usage limit reached: " + msg)
|
|
843
867
|
if (low.includes("too long") || low.includes("context length") || low.includes("too many tokens"))
|
|
@@ -1076,7 +1100,7 @@ function open(base, headers, signal) {
|
|
|
1076
1100
|
if (done) return
|
|
1077
1101
|
done = true
|
|
1078
1102
|
session.destroy()
|
|
1079
|
-
resolve({ error: { status: 502, message:
|
|
1103
|
+
resolve({ error: { status: 502, message: e?.message ?? String(e) } })
|
|
1080
1104
|
}
|
|
1081
1105
|
session.on("error", fail)
|
|
1082
1106
|
const req = session.request({
|
|
@@ -1151,7 +1175,7 @@ async function runOnce({ tok, base, id, tools, conv, signal }) {
|
|
|
1151
1175
|
// an error comes before anything of the answer: out of quota, a model the
|
|
1152
1176
|
// plan doesn't have — answered with its own status
|
|
1153
1177
|
const head = await wrapped.next()
|
|
1154
|
-
if (head.done) return { error: { status: 502, message: "
|
|
1178
|
+
if (head.done) return { error: { status: 502, message: "an empty reply" } }
|
|
1155
1179
|
if (head.value.error) {
|
|
1156
1180
|
await wrapped.return()
|
|
1157
1181
|
return { error: head.value.error }
|
|
@@ -1191,7 +1215,7 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1191
1215
|
if (f.end) {
|
|
1192
1216
|
const e = failure(200, f.data)
|
|
1193
1217
|
if (e.status < 200 || e.status > 299) yield { error: e }
|
|
1194
|
-
else if (said === 0 && calls === 0) yield { error: { status: 502, message: "
|
|
1218
|
+
else if (said === 0 && calls === 0) yield { error: { status: 502, message: "an empty reply" } }
|
|
1195
1219
|
else yield finish()
|
|
1196
1220
|
return
|
|
1197
1221
|
}
|
|
@@ -1274,13 +1298,13 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1274
1298
|
yield finish()
|
|
1275
1299
|
return
|
|
1276
1300
|
}
|
|
1277
|
-
yield { error: { status: 502, message: "
|
|
1301
|
+
yield { error: { status: 502, message: "the reply broke off: EOF" } }
|
|
1278
1302
|
} catch (e) {
|
|
1279
1303
|
if (calls > 0 && /EOF/.test(e?.message ?? "")) {
|
|
1280
1304
|
yield finish()
|
|
1281
1305
|
return
|
|
1282
1306
|
}
|
|
1283
|
-
yield { error: { status: 502, message: "
|
|
1307
|
+
yield { error: { status: 502, message: "the reply broke off: " + (e?.message ?? e) } }
|
|
1284
1308
|
}
|
|
1285
1309
|
}
|
|
1286
1310
|
|
|
@@ -1346,7 +1370,7 @@ async function answer(auth, chat, signal) {
|
|
|
1346
1370
|
try {
|
|
1347
1371
|
tok = await tokenOf(auth)
|
|
1348
1372
|
} catch (e) {
|
|
1349
|
-
return errorResponse({ status: e.status ?? 401, message: e.message
|
|
1373
|
+
return errorResponse({ status: e.status ?? 401, message: e.message })
|
|
1350
1374
|
}
|
|
1351
1375
|
const model = String(chat.model ?? "")
|
|
1352
1376
|
let raw = []
|
|
@@ -1438,13 +1462,13 @@ export async function CursorAuthPlugin() {
|
|
|
1438
1462
|
async fetch(input, init = {}) {
|
|
1439
1463
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
|
|
1440
1464
|
if (!/\/chat\/completions$/.test(new URL(url).pathname))
|
|
1441
|
-
return errorResponse({ status: 404, message: "
|
|
1465
|
+
return errorResponse({ status: 404, message: "only chat completions are served" })
|
|
1442
1466
|
let chat
|
|
1443
1467
|
try {
|
|
1444
1468
|
const b = init.body ?? (input instanceof Request ? await input.clone().text() : undefined)
|
|
1445
1469
|
chat = JSON.parse(typeof b === "string" ? b : new TextDecoder().decode(b))
|
|
1446
1470
|
} catch {
|
|
1447
|
-
return errorResponse({ status: 400, message: "
|
|
1471
|
+
return errorResponse({ status: 400, message: "a request that isn't JSON" })
|
|
1448
1472
|
}
|
|
1449
1473
|
return answer((await getAuth()) ?? auth, chat, init.signal ?? (input instanceof Request ? input.signal : undefined))
|
|
1450
1474
|
},
|
|
@@ -1464,7 +1488,8 @@ export async function CursorAuthPlugin() {
|
|
|
1464
1488
|
} catch (e) {
|
|
1465
1489
|
return { error: e.message, windows: [] }
|
|
1466
1490
|
}
|
|
1467
|
-
|
|
1491
|
+
const [u, plan] = await Promise.all([usage(tok, Object.keys(provider?.models ?? {})), planOf(tok)])
|
|
1492
|
+
return plan ? { ...u, plan } : u
|
|
1468
1493
|
},
|
|
1469
1494
|
},
|
|
1470
1495
|
async config(config) {
|
|
@@ -1487,12 +1512,10 @@ export async function CursorAuthPlugin() {
|
|
|
1487
1512
|
id: ID,
|
|
1488
1513
|
async models(provider, { auth } = {}) {
|
|
1489
1514
|
if (!auth) return provider.models
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
return provider.models
|
|
1495
|
-
}
|
|
1515
|
+
// a list Cursor couldn't give is a failure, not the few models
|
|
1516
|
+
// configured: magpie keeps the list it had
|
|
1517
|
+
const tok = await tokenOf(auth)
|
|
1518
|
+
return Object.fromEntries(offered(await usable(tok)).map((m) => [m.id, runtimeModel(m)]))
|
|
1496
1519
|
},
|
|
1497
1520
|
},
|
|
1498
1521
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-cursor-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|