@magpie-community/opencode-qoder-auth 0.2.2 → 0.2.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/README.md +15 -4
- package/index.mjs +71 -15
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -26,10 +26,21 @@ What is kept: the job token, its refresh token and expiry, the uid, the
|
|
|
26
26
|
device token and a machine id made for this sign-in. OpenCode keeps them in
|
|
27
27
|
`auth.json`; magpie keeps them in `plugin-auth.json`.
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
refresh token once, so the
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
Refreshing:
|
|
30
|
+
- Qoder spends a refresh token once, so the plugin never sends one twice.
|
|
31
|
+
- magpie renews the job token 10 minutes before it runs out, through the
|
|
32
|
+
plugin's `auth.refresh`, once for the account and before its requests,
|
|
33
|
+
models and usage need it; magpie saves the new pair. OpenCode doesn't
|
|
34
|
+
call that hook: there the job token is refreshed 5 minutes before it runs
|
|
35
|
+
out, before a request, and the new pair is saved straight away.
|
|
36
|
+
- Only one refresh runs at a time. A request that reads the sign-in before
|
|
37
|
+
magpie saved a renewal uses the renewed pair, and a renewal handed a pair
|
|
38
|
+
a request already spent gives what that request got.
|
|
39
|
+
- The device token, which reads usage, has no end on record: it is rotated
|
|
40
|
+
when Qoder refuses it. On a [Qoder CN](#qoder-cn) device-token account it
|
|
41
|
+
is the chat token too, and is renewed as above.
|
|
42
|
+
- When Qoder refuses a refresh (401 or 403), the account needs signing in
|
|
43
|
+
again. Any other failure is tried again later.
|
|
33
44
|
|
|
34
45
|
## Requests
|
|
35
46
|
|
package/index.mjs
CHANGED
|
@@ -32,7 +32,9 @@ const CHAT_PATH = "/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=
|
|
|
32
32
|
const MODELS_PATH = "/algo/api/v2/model/list?Encode=1"
|
|
33
33
|
const COSY_VERSION = "1.1.49"
|
|
34
34
|
const SIGN_IN_TIMEOUT = 15 * 60 * 1000
|
|
35
|
-
const REFRESH_LEAD = 5 * 60 * 1000
|
|
35
|
+
const REFRESH_LEAD = 5 * 60 * 1000 // a chat token this close to its end is refreshed before a request
|
|
36
|
+
const RENEW_LEAD = 10 * 60 * 1000 // and this close, magpie renews it ahead of time (auth.refresh)
|
|
37
|
+
const KEEP_SPENT_MS = 60 * 60 * 1000 // how long what a spent refresh token gave is remembered
|
|
36
38
|
const DAY = 24 * 60 * 60 * 1000
|
|
37
39
|
const CHAT = "@ai-sdk/openai-compatible"
|
|
38
40
|
|
|
@@ -842,6 +844,13 @@ function signed(res, renewed) {
|
|
|
842
844
|
return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
843
845
|
}
|
|
844
846
|
|
|
847
|
+
// changed is the fields of b that differ from a's: what auth.refresh gives.
|
|
848
|
+
function changed(a, b) {
|
|
849
|
+
const out = {}
|
|
850
|
+
for (const k of ["access", "refresh", "expires", "deviceToken", "deviceRefresh"]) if (b[k] !== a[k]) out[k] = b[k]
|
|
851
|
+
return out
|
|
852
|
+
}
|
|
853
|
+
|
|
845
854
|
// ---- usage --------------------------------------------------------------------
|
|
846
855
|
//
|
|
847
856
|
// The account's allowance as magpie's built-in Qoder account shows it
|
|
@@ -995,6 +1004,38 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
995
1004
|
lock = run.catch(() => {})
|
|
996
1005
|
return run
|
|
997
1006
|
}
|
|
1007
|
+
// spent is what each refresh token this process spent gave, by that
|
|
1008
|
+
// token: the chat pair it renewed. magpie saves what auth.refresh gives
|
|
1009
|
+
// only once the hook has returned, and a request may read the account in
|
|
1010
|
+
// between, or the hook be handed the account as it was before a request
|
|
1011
|
+
// renewed it: either follows spent rather than spending the token again.
|
|
1012
|
+
const spent = new Map()
|
|
1013
|
+
const latest = (a) => {
|
|
1014
|
+
for (let i = 0; i < 16 && a?.refresh; i++) {
|
|
1015
|
+
const s = spent.get(a.refresh)
|
|
1016
|
+
if (!s) break
|
|
1017
|
+
a = { ...a, ...s.got }
|
|
1018
|
+
}
|
|
1019
|
+
return a
|
|
1020
|
+
}
|
|
1021
|
+
const spend = (refresh, got) => {
|
|
1022
|
+
const now = Date.now()
|
|
1023
|
+
for (const [k, v] of spent) if (now - v.at > KEEP_SPENT_MS) spent.delete(k)
|
|
1024
|
+
spent.set(refresh, { got, at: now })
|
|
1025
|
+
return got
|
|
1026
|
+
}
|
|
1027
|
+
// renew spends a's chat refresh token, under the lock: the fields it
|
|
1028
|
+
// changes, recorded in spent.
|
|
1029
|
+
const renew = async (a) => {
|
|
1030
|
+
if (a.deviceChat) {
|
|
1031
|
+
// the device token is the chat token: renewed as a device token,
|
|
1032
|
+
// both pairs being the one pair
|
|
1033
|
+
const dt = await refreshDevice(site, a.refresh, true)
|
|
1034
|
+
return spend(a.refresh, { access: dt.token, refresh: dt.refresh, expires: dt.expires, deviceToken: dt.token, deviceRefresh: dt.refresh })
|
|
1035
|
+
}
|
|
1036
|
+
const jt = await refreshJob(site, a.refresh, a.accountId || a.email || a.uid)
|
|
1037
|
+
return spend(a.refresh, { access: jt.token, refresh: jt.refresh_token, expires: expiresAt(jt) })
|
|
1038
|
+
}
|
|
998
1039
|
// each account's listing, the model configs a request carries
|
|
999
1040
|
const listings = new Map()
|
|
1000
1041
|
// the accounts fresh renewed: the built-in took the lapse mark off on a
|
|
@@ -1013,19 +1054,12 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1013
1054
|
// its end.
|
|
1014
1055
|
const fresh = (getAuth) =>
|
|
1015
1056
|
locked(async () => {
|
|
1016
|
-
const
|
|
1017
|
-
if (
|
|
1057
|
+
const stored = await getAuth()
|
|
1058
|
+
if (stored?.type !== "oauth" || !stored.access || !stored.uid) throw new SignInGone(`${site.name}: not signed in`)
|
|
1059
|
+
// renewed already (by magpie's auth.refresh), the store not yet saying so
|
|
1060
|
+
const a = latest(stored)
|
|
1018
1061
|
if (a.expires - Date.now() > REFRESH_LEAD) return a
|
|
1019
|
-
|
|
1020
|
-
if (a.deviceChat) {
|
|
1021
|
-
// the device token is the chat token: renewed as a device token,
|
|
1022
|
-
// both pairs being the one pair
|
|
1023
|
-
const dt = await refreshDevice(site, a.refresh, true)
|
|
1024
|
-
next = { ...a, access: dt.token, refresh: dt.refresh, expires: dt.expires, deviceToken: dt.token, deviceRefresh: dt.refresh }
|
|
1025
|
-
} else {
|
|
1026
|
-
const jt = await refreshJob(site, a.refresh, a.accountId || a.email || a.uid)
|
|
1027
|
-
next = { ...a, access: jt.token, refresh: jt.refresh_token, expires: expiresAt(jt) }
|
|
1028
|
-
}
|
|
1062
|
+
const next = { ...a, ...(await renew(a)) }
|
|
1029
1063
|
await client.auth.set({ path: { id: ID }, body: next })
|
|
1030
1064
|
renewals.add(next)
|
|
1031
1065
|
return next
|
|
@@ -1044,12 +1078,12 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1044
1078
|
// the one saved since, else a rotated one, saved.
|
|
1045
1079
|
const deviceToken = (getAuth, attempted) =>
|
|
1046
1080
|
locked(async () => {
|
|
1047
|
-
const a = await getAuth()
|
|
1081
|
+
const a = latest(await getAuth())
|
|
1048
1082
|
if (a?.deviceToken !== attempted) return a?.deviceToken
|
|
1049
1083
|
const dt = await refreshDevice(site, a.deviceRefresh, !!a.deviceChat)
|
|
1050
1084
|
const next = { ...a, deviceToken: dt.token, deviceRefresh: dt.refresh }
|
|
1051
1085
|
// the pair it spent was the chat pair too
|
|
1052
|
-
if (a.deviceChat) Object.assign(next, { access: dt.token, refresh: dt.refresh, expires: dt.expires })
|
|
1086
|
+
if (a.deviceChat) Object.assign(next, spend(a.refresh, { access: dt.token, refresh: dt.refresh, expires: dt.expires, deviceToken: dt.token, deviceRefresh: dt.refresh }))
|
|
1053
1087
|
await client.auth.set({ path: { id: ID }, body: next })
|
|
1054
1088
|
return dt.token
|
|
1055
1089
|
})
|
|
@@ -1115,6 +1149,28 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1115
1149
|
return {
|
|
1116
1150
|
auth: {
|
|
1117
1151
|
provider: ID,
|
|
1152
|
+
// magpie renews the chat token RENEW_LEAD before its end, once for
|
|
1153
|
+
// the account, before its requests, models and usage ask for it; the
|
|
1154
|
+
// check before each request (REFRESH_LEAD) stays for OpenCode, which
|
|
1155
|
+
// doesn't call this. The device token of a job-token account has no
|
|
1156
|
+
// end on record: usage rotates it once Qoder refuses it, as before.
|
|
1157
|
+
refreshLead: RENEW_LEAD,
|
|
1158
|
+
async refresh(auth) {
|
|
1159
|
+
if (auth?.type !== "oauth" || !auth.access || !auth.uid || !String(auth.refresh ?? "").trim()) return undefined
|
|
1160
|
+
return locked(async () => {
|
|
1161
|
+
// a request renewed it meanwhile (or magpie hasn't saved this
|
|
1162
|
+
// hook's last answer): what that gave, not a second refresh
|
|
1163
|
+
const a = latest(auth)
|
|
1164
|
+
if (a.access !== auth.access && a.expires - Date.now() > RENEW_LEAD) return changed(auth, a)
|
|
1165
|
+
try {
|
|
1166
|
+
// magpie saves what this gives
|
|
1167
|
+
return changed(auth, { ...a, ...(await renew(a)) })
|
|
1168
|
+
} catch (e) {
|
|
1169
|
+
if (e?.expired) throw Object.assign(new Error(e.message), { signIn: "expired" })
|
|
1170
|
+
throw e
|
|
1171
|
+
}
|
|
1172
|
+
})
|
|
1173
|
+
},
|
|
1118
1174
|
async loader(getAuth) {
|
|
1119
1175
|
const a = await getAuth()
|
|
1120
1176
|
if (a?.type !== "oauth") return {}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-qoder-auth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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",
|
|
7
7
|
"exports": "./index.mjs",
|
|
8
8
|
"files": ["index.mjs", "README.md", "LICENSE-CLIProxyAPI"],
|
|
9
9
|
"keywords": ["opencode", "opencode-plugin", "magpie", "qoder", "qoder-cn"],
|
|
10
|
-
"license": "MIT"
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": { "type": "git", "url": "git+https://github.com/magpie-community/plugins.git", "directory": "packages/qoder" }
|
|
11
12
|
}
|