@magpie-community/opencode-qoder-auth 0.2.14 → 0.2.15
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 +118 -2
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -311,6 +311,64 @@ async function deviceSignIn(site) {
|
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
// exchangeExpiry is when a job token from /api/v1/jobToken/exchange runs
|
|
315
|
+
// out, as Qoder's own client reads it: expire_time or expires_at (a date)
|
|
316
|
+
// first, then expires_in (milliseconds), then a day.
|
|
317
|
+
function exchangeExpiry(jt) {
|
|
318
|
+
for (const k of ["expire_time", "expires_at"]) {
|
|
319
|
+
const at = Date.parse(String(jt?.[k] ?? "").trim())
|
|
320
|
+
if (!isNaN(at)) return at
|
|
321
|
+
}
|
|
322
|
+
if (jt?.expires_in > 0) return Date.now() + jt.expires_in
|
|
323
|
+
return Date.now() + DAY
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// exchangeToken is the job token from an exchange response, as Qoder's
|
|
327
|
+
// client reads it: token, device_token or access_token.
|
|
328
|
+
function exchangeToken(jt) {
|
|
329
|
+
return String(jt?.token ?? jt?.device_token ?? jt?.access_token ?? "").trim()
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// patSignIn exchanges a Personal Access Token for a job token, bypassing
|
|
333
|
+
// the OAuth device flow. PATs are long-lived (up to 1 year): the PAT is
|
|
334
|
+
// kept on the credential, and when the job token's refresh lapses the PAT
|
|
335
|
+
// is exchanged again rather than asking the user to sign in once more.
|
|
336
|
+
async function patSignIn(site, pat) {
|
|
337
|
+
const machineId = randomUUID()
|
|
338
|
+
const r = await openapi(site, "/api/v1/jobToken/exchange", { method: "POST", body: { personal_token: pat } })
|
|
339
|
+
if (r.status === 400 || r.status === 401 || r.status === 403)
|
|
340
|
+
throw Object.assign(new Error(`${site.name} refused the PAT — check it is still valid`), { status: r.status })
|
|
341
|
+
if (r.status !== 200) throw new Error(`${site.name} PAT exchange: status ${r.status}`)
|
|
342
|
+
const token = exchangeToken(r.json)
|
|
343
|
+
if (!token) throw new Error(`${site.name} PAT exchange: empty token in response`)
|
|
344
|
+
const refresh = String(r.json?.refresh_token ?? r.json?.refreshToken ?? "").trim()
|
|
345
|
+
const chat = { access: token, refresh, expires: exchangeExpiry(r.json) }
|
|
346
|
+
let email = ""
|
|
347
|
+
let name = ""
|
|
348
|
+
let uid = ""
|
|
349
|
+
try {
|
|
350
|
+
const ui = await openapi(site, "/api/v1/userinfo", { token })
|
|
351
|
+
if (ui.status >= 200 && ui.status < 300) {
|
|
352
|
+
;[email, name] = [ui.json?.email ?? "", ui.json?.name ?? ""]
|
|
353
|
+
uid = String(ui.json?.id ?? ui.json?.user_id ?? ui.json?.uid ?? "")
|
|
354
|
+
}
|
|
355
|
+
} catch {}
|
|
356
|
+
if (!uid) throw new Error(`${site.name} PAT sign-in: could not determine user id`)
|
|
357
|
+
return {
|
|
358
|
+
type: "success",
|
|
359
|
+
...chat,
|
|
360
|
+
accountId: email || uid,
|
|
361
|
+
uid,
|
|
362
|
+
email,
|
|
363
|
+
name: name || "",
|
|
364
|
+
machineId,
|
|
365
|
+
deviceToken: "",
|
|
366
|
+
deviceRefresh: "",
|
|
367
|
+
patAuth: true,
|
|
368
|
+
pat,
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
314
372
|
// SignInGone is a sign-in that can't sign a request: a 401. Only Qoder
|
|
315
373
|
// refusing the refresh token marks the account lapsed (expired), as the
|
|
316
374
|
// built-in's qoderRefreshFailed did; no refresh token, or no account,
|
|
@@ -1372,7 +1430,8 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1372
1430
|
return got
|
|
1373
1431
|
}
|
|
1374
1432
|
// renew spends a's chat refresh token, under the lock: the fields it
|
|
1375
|
-
// changes, recorded in spent.
|
|
1433
|
+
// changes, recorded in spent. A PAT account whose job refresh has lapsed
|
|
1434
|
+
// exchanges the PAT again: the PAT lasts a year, the refresh two days.
|
|
1376
1435
|
const renew = async (a) => {
|
|
1377
1436
|
const s = siteOf(site, a)
|
|
1378
1437
|
if (a.deviceChat) {
|
|
@@ -1381,7 +1440,22 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1381
1440
|
const dt = await refreshDevice(s, a.refresh, true)
|
|
1382
1441
|
return spend(a.refresh, { access: dt.token, refresh: dt.refresh, expires: dt.expires, deviceToken: dt.token, deviceRefresh: dt.refresh })
|
|
1383
1442
|
}
|
|
1384
|
-
|
|
1443
|
+
let jt
|
|
1444
|
+
try {
|
|
1445
|
+
jt = await refreshJob(s, a.refresh, a.accountId || a.email || a.uid)
|
|
1446
|
+
} catch (e) {
|
|
1447
|
+
if (!(e instanceof SignInGone) || !a.patAuth || !a.pat) throw e
|
|
1448
|
+
let ex
|
|
1449
|
+
try {
|
|
1450
|
+
ex = await patSignIn(s, a.pat)
|
|
1451
|
+
} catch (p) {
|
|
1452
|
+
// a PAT lapses too — out of date, or revoked: Qoder's refusal ends
|
|
1453
|
+
// the sign-in as a lapsed device one does, not as a server fault
|
|
1454
|
+
if (!p?.status) throw p
|
|
1455
|
+
throw new SignInGone(`${site.name} refused the PAT — sign in again with a new one`, true)
|
|
1456
|
+
}
|
|
1457
|
+
return spend(a.refresh, { access: ex.access, refresh: ex.refresh, expires: ex.expires })
|
|
1458
|
+
}
|
|
1385
1459
|
return spend(a.refresh, { access: jt.token, refresh: jt.refresh_token, expires: expiresAt(jt) })
|
|
1386
1460
|
}
|
|
1387
1461
|
// each account's listing, the model configs a request carries
|
|
@@ -1450,6 +1524,14 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1450
1524
|
try {
|
|
1451
1525
|
const cred = await fresh(getAuth)
|
|
1452
1526
|
if (renewed(cred)) signIn = "renewed"
|
|
1527
|
+
// A PAT account has no device token: the usage endpoint takes the job
|
|
1528
|
+
// token the exchange gave — a live PAT account answered 200 with a
|
|
1529
|
+
// qoder envelope. Its errors surface as the device path's do; there is
|
|
1530
|
+
// no second token here to rotate and retry with.
|
|
1531
|
+
if (cred.patAuth && !cred.deviceToken) {
|
|
1532
|
+
const env = await fetchUsage(siteOf(site, cred), cred.access)
|
|
1533
|
+
return { ...parseUsage(env), signIn }
|
|
1534
|
+
}
|
|
1453
1535
|
let env
|
|
1454
1536
|
try {
|
|
1455
1537
|
env = await fetchUsage(siteOf(site, cred), cred.deviceToken)
|
|
@@ -1583,6 +1665,40 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1583
1665
|
},
|
|
1584
1666
|
methods: [
|
|
1585
1667
|
{ type: "oauth", label: `Sign in with ${site.name}`, authorize: () => deviceSignIn(site) },
|
|
1668
|
+
// PAT sign-in: exchange a Personal Access Token for a job token,
|
|
1669
|
+
// bypassing the OAuth device flow. PATs last up to 1 year.
|
|
1670
|
+
// Not offered on Qoder CN: its CLI uses device chat directly and
|
|
1671
|
+
// never makes a job token, so a PAT exchange may not produce one
|
|
1672
|
+
// the chat endpoint accepts.
|
|
1673
|
+
...(site.id !== "qoder-cn"
|
|
1674
|
+
? [
|
|
1675
|
+
{
|
|
1676
|
+
type: "oauth",
|
|
1677
|
+
label: `Sign in with ${site.name} PAT`,
|
|
1678
|
+
prompts: [
|
|
1679
|
+
{
|
|
1680
|
+
type: "password",
|
|
1681
|
+
key: "pat",
|
|
1682
|
+
message: "Your Qoder Personal Access Token (from qoder.com/account/integrations)",
|
|
1683
|
+
placeholder: "pt-...",
|
|
1684
|
+
validate: (v) => (String(v ?? "").trim().startsWith("pt-") ? undefined : "PAT must start with pt-"),
|
|
1685
|
+
},
|
|
1686
|
+
],
|
|
1687
|
+
authorize: (inputs) => {
|
|
1688
|
+
const pat = String(inputs?.pat ?? "").trim()
|
|
1689
|
+
if (!pat) throw new Error("enter your Personal Access Token")
|
|
1690
|
+
return {
|
|
1691
|
+
url: `${site.web}/account/integrations`,
|
|
1692
|
+
instructions: `Create a PAT at ${site.web}/account/integrations if you don't have one`,
|
|
1693
|
+
method: "auto",
|
|
1694
|
+
async callback() {
|
|
1695
|
+
return patSignIn(site, pat)
|
|
1696
|
+
},
|
|
1697
|
+
}
|
|
1698
|
+
},
|
|
1699
|
+
},
|
|
1700
|
+
]
|
|
1701
|
+
: []),
|
|
1586
1702
|
// an enterprise VPC instance (Qoder CN), a method of its own so
|
|
1587
1703
|
// that the sign-in above asks nothing new
|
|
1588
1704
|
...(site.vpcDomain
|
package/package.json
CHANGED