@magpie-community/opencode-qoder-auth 0.2.20 → 0.2.21
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/README.md +25 -0
- package/index.mjs +48 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,31 @@ hosts; [its section](#qoder-cn) says what differs.
|
|
|
22
22
|
3. The device token is traded for a job token, and the account's email and
|
|
23
23
|
name are read.
|
|
24
24
|
|
|
25
|
+
**Sign in with Qoder PAT** (and **Sign in with Qoder CN PAT**) skips the
|
|
26
|
+
device flow: a Personal Access Token from `<site>/account/integrations` is
|
|
27
|
+
posted to `/api/v1/jobToken/exchange` (`personal_token`), which answers a
|
|
28
|
+
job token, its refresh token and its expiry. The PAT is kept on the
|
|
29
|
+
credential, so a lapsed job refresh is exchanged again instead of asking
|
|
30
|
+
for the sign-in; PATs last up to a year. The job token signs the model list
|
|
31
|
+
and chat calls with the same COSY envelope either sign-in's does.
|
|
32
|
+
|
|
33
|
+
On Qoder CN this is the plugin's own reading of the protocol rather than
|
|
34
|
+
the CLI's path: qoder.cn's CLI signs requests with its device token and
|
|
35
|
+
never asks for a job token. Verified against a live qoder.cn account — the
|
|
36
|
+
exchange answers 200 with a `jt-` token, the signed model list answers 200
|
|
37
|
+
with that account's own models, and a chat completion returns its text.
|
|
38
|
+
|
|
39
|
+
A PAT account keeps the name its browser sign-in takes (`email || uid`): magpie
|
|
40
|
+
replaces an account on a new sign-in when both the accountId and the uid
|
|
41
|
+
match, so a renewed PAT sign-in names the same account rather than adding a
|
|
42
|
+
row beside the lapsed one.
|
|
43
|
+
|
|
44
|
+
A PAT account has no device token, so its usage read, campaigns pages and
|
|
45
|
+
daily check-in go out on the job token instead. The check-in works on it: a
|
|
46
|
+
live PAT account answered 200 to the campaigns list and `CLAIMED` to a claim,
|
|
47
|
+
so it earns the daily credits the browser sign-in does. That path has no
|
|
48
|
+
second token to rotate, so a refused call is the answer rather than a retry.
|
|
49
|
+
|
|
25
50
|
What is kept: the job token, its refresh token and expiry, the uid, the
|
|
26
51
|
device token and a machine id made for this sign-in. OpenCode keeps them in
|
|
27
52
|
`auth.json`; magpie keeps them in `plugin-auth.json`.
|
package/index.mjs
CHANGED
|
@@ -357,6 +357,10 @@ async function patSignIn(site, pat) {
|
|
|
357
357
|
return {
|
|
358
358
|
type: "success",
|
|
359
359
|
...chat,
|
|
360
|
+
// The same name its browser sign-in takes: magpie's plugin host replaces
|
|
361
|
+
// an account on a new sign-in when both the accountId and the uid match,
|
|
362
|
+
// so a renewed PAT sign-in must keep naming the same account. A suffix
|
|
363
|
+
// here would add a second row beside the lapsed one instead.
|
|
360
364
|
accountId: email || uid,
|
|
361
365
|
uid,
|
|
362
366
|
email,
|
|
@@ -1636,8 +1640,14 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1636
1640
|
const method = String(init.method ?? input?.method ?? "GET").toUpperCase()
|
|
1637
1641
|
try {
|
|
1638
1642
|
const machine = site.id === "qoder" ? await machineForCampaign(cred.uid) : {}
|
|
1639
|
-
|
|
1640
|
-
|
|
1643
|
+
// A PAT account has no device token: its job token signs the campaigns
|
|
1644
|
+
// pages too, as it does the usage read. Nothing is rotated on that
|
|
1645
|
+
// path — the refusal is the answer.
|
|
1646
|
+
const patToken = cred.patAuth && !cred.deviceToken
|
|
1647
|
+
const token = patToken ? cred.access : cred.deviceToken
|
|
1648
|
+
let res = await campaignCall(url, method, body, token, machine, init.signal)
|
|
1649
|
+
if (!patToken && (res.status === 401 || res.status === 403))
|
|
1650
|
+
res = await campaignCall(url, method, body, await deviceToken(getAuth, cred.deviceToken), machine, init.signal)
|
|
1641
1651
|
return signed(res, renewed(cred))
|
|
1642
1652
|
} catch (e) {
|
|
1643
1653
|
if (e?.expired) return signedInError(e)
|
|
@@ -1659,8 +1669,15 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1659
1669
|
if (!base) return { outcome: "inactive", message: "no campaigns page for this account" }
|
|
1660
1670
|
try {
|
|
1661
1671
|
const machine = site.id === "qoder" ? await machineForCampaign(cred.uid) : {}
|
|
1672
|
+
// A PAT account has no device token: the campaigns pages take the job
|
|
1673
|
+
// token the exchange gave, as the usage endpoint does. A live Qoder CN
|
|
1674
|
+
// PAT account answered 200 to the list and CLAIMED to a claim, so the
|
|
1675
|
+
// check-in works there as it does on a device-token account. That path
|
|
1676
|
+
// has no second token to rotate, so a refusal is the answer.
|
|
1677
|
+
const patToken = cred.patAuth && !cred.deviceToken ? cred.access : ""
|
|
1662
1678
|
const call = async (url, method, body) => {
|
|
1663
|
-
let res = await campaignCall(url, method, body, cred.deviceToken, machine)
|
|
1679
|
+
let res = await campaignCall(url, method, body, patToken || cred.deviceToken, machine)
|
|
1680
|
+
if (patToken) return res
|
|
1664
1681
|
if (res.status === 401 || res.status === 403) {
|
|
1665
1682
|
const token = await deviceToken(getAuth, cred.deviceToken)
|
|
1666
1683
|
const current = latest(await getAuth())
|
|
@@ -1809,38 +1826,35 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1809
1826
|
{ type: "oauth", label: `Sign in with ${site.name}`, authorize: () => deviceSignIn(site) },
|
|
1810
1827
|
// PAT sign-in: exchange a Personal Access Token for a job token,
|
|
1811
1828
|
// bypassing the OAuth device flow. PATs last up to 1 year.
|
|
1812
|
-
//
|
|
1813
|
-
//
|
|
1814
|
-
// the chat
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
return patSignIn(site, pat)
|
|
1838
|
-
},
|
|
1839
|
-
}
|
|
1840
|
-
},
|
|
1829
|
+
// Offered on both sites: Qoder CN's /api/v1/jobToken/exchange takes
|
|
1830
|
+
// a PAT too (verified: HTTP 200 and a jt- token), and that job token
|
|
1831
|
+
// signs the model list and chat calls with the COSY envelope as on
|
|
1832
|
+
// qoder.com, so the exchange gives a token the chat endpoint accepts.
|
|
1833
|
+
{
|
|
1834
|
+
type: "oauth",
|
|
1835
|
+
label: `Sign in with ${site.name} PAT`,
|
|
1836
|
+
prompts: [
|
|
1837
|
+
{
|
|
1838
|
+
type: "password",
|
|
1839
|
+
key: "pat",
|
|
1840
|
+
message: `Your ${site.name} Personal Access Token (from ${site.web}/account/integrations)`,
|
|
1841
|
+
placeholder: "pt-...",
|
|
1842
|
+
validate: (v) => (String(v ?? "").trim().startsWith("pt-") ? undefined : "PAT must start with pt-"),
|
|
1843
|
+
},
|
|
1844
|
+
],
|
|
1845
|
+
authorize: (inputs) => {
|
|
1846
|
+
const pat = String(inputs?.pat ?? "").trim()
|
|
1847
|
+
if (!pat) throw new Error("enter your Personal Access Token")
|
|
1848
|
+
return {
|
|
1849
|
+
url: `${site.web}/account/integrations`,
|
|
1850
|
+
instructions: `Create a PAT at ${site.web}/account/integrations if you don't have one`,
|
|
1851
|
+
method: "auto",
|
|
1852
|
+
async callback() {
|
|
1853
|
+
return patSignIn(site, pat)
|
|
1841
1854
|
},
|
|
1842
|
-
|
|
1843
|
-
|
|
1855
|
+
}
|
|
1856
|
+
},
|
|
1857
|
+
},
|
|
1844
1858
|
// an enterprise VPC instance (Qoder CN), a method of its own so
|
|
1845
1859
|
// that the sign-in above asks nothing new
|
|
1846
1860
|
...(site.vpcDomain
|
package/package.json
CHANGED