@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.
Files changed (3) hide show
  1. package/README.md +25 -0
  2. package/index.mjs +48 -34
  3. 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
- let res = await campaignCall(url, method, body, cred.deviceToken, machine, init.signal)
1640
- if (res.status === 401 || res.status === 403) res = await campaignCall(url, method, body, await deviceToken(getAuth, cred.deviceToken), machine, init.signal)
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
- // Not offered on Qoder CN: its CLI uses device chat directly and
1813
- // never makes a job token, so a PAT exchange may not produce one
1814
- // the chat endpoint accepts.
1815
- ...(site.id !== "qoder-cn"
1816
- ? [
1817
- {
1818
- type: "oauth",
1819
- label: `Sign in with ${site.name} PAT`,
1820
- prompts: [
1821
- {
1822
- type: "password",
1823
- key: "pat",
1824
- message: "Your Qoder Personal Access Token (from qoder.com/account/integrations)",
1825
- placeholder: "pt-...",
1826
- validate: (v) => (String(v ?? "").trim().startsWith("pt-") ? undefined : "PAT must start with pt-"),
1827
- },
1828
- ],
1829
- authorize: (inputs) => {
1830
- const pat = String(inputs?.pat ?? "").trim()
1831
- if (!pat) throw new Error("enter your Personal Access Token")
1832
- return {
1833
- url: `${site.web}/account/integrations`,
1834
- instructions: `Create a PAT at ${site.web}/account/integrations if you don't have one`,
1835
- method: "auto",
1836
- async callback() {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-qoder-auth",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "description": "Qoder subscriptions in OpenCode and magpie: Qoder (qoder.com) and Qoder CN (qoder.cn)",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",