@magpie-community/opencode-qoder-auth 0.1.2 → 0.1.4
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 +107 -41
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// from magpie's internal/qoder, which ported it from CLIProxyAPI's qoder
|
|
17
17
|
// support (https://github.com/ufec/CLIProxyAPI, MIT).
|
|
18
18
|
import { createCipheriv, createHash, publicEncrypt, randomBytes, randomUUID, constants } from "node:crypto"
|
|
19
|
+
import { STATUS_CODES } from "node:http"
|
|
19
20
|
|
|
20
21
|
const ID = "qoder"
|
|
21
22
|
const CLIENT_ID = "732aef47-9cf2-46a2-95fe-4cebb5d0d1fa"
|
|
@@ -213,17 +214,27 @@ async function deviceSignIn() {
|
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
|
|
217
|
+
// SignInGone is a sign-in that can't sign a request: a 401. Only Qoder
|
|
218
|
+
// refusing the refresh token marks the account lapsed (expired), as the
|
|
219
|
+
// built-in's qoderRefreshFailed did; no refresh token, or no account,
|
|
220
|
+
// answered 401 there without marking one.
|
|
221
|
+
class SignInGone extends Error {
|
|
222
|
+
constructor(message, expired = false) {
|
|
223
|
+
super(message)
|
|
224
|
+
this.expired = expired
|
|
225
|
+
}
|
|
226
|
+
}
|
|
217
227
|
|
|
218
228
|
// refreshJob trades the job token's refresh token for a new pair; the old
|
|
219
229
|
// one is spent.
|
|
220
|
-
async function refreshJob(refresh) {
|
|
230
|
+
async function refreshJob(refresh, user) {
|
|
221
231
|
if (!String(refresh ?? "").trim()) throw new SignInGone("Qoder: the sign-in lapsed; sign in again")
|
|
222
232
|
const r = await openapi("/api/v1/jobToken/refresh", { method: "POST", body: { refresh_token: refresh } })
|
|
223
|
-
if (r.status === 401 || r.status === 403)
|
|
224
|
-
|
|
233
|
+
if (r.status === 401 || r.status === 403)
|
|
234
|
+
throw new SignInGone(`${user}'s Qoder sign-in has expired — sign in again (qoder job token refresh: status ${r.status})`, true)
|
|
235
|
+
if (r.status !== 200) throw new Error(`qoder job token refresh: status ${r.status}`)
|
|
225
236
|
if (!String(r.json?.token ?? "").trim() || !String(r.json?.refresh_token ?? "").trim())
|
|
226
|
-
throw new Error("
|
|
237
|
+
throw new Error("qoder job token refresh: incomplete token pair")
|
|
227
238
|
return r.json
|
|
228
239
|
}
|
|
229
240
|
|
|
@@ -486,19 +497,26 @@ function failure(status, text) {
|
|
|
486
497
|
try {
|
|
487
498
|
const j = JSON.parse(text)
|
|
488
499
|
if (j?.message) msg = j.message
|
|
489
|
-
|
|
500
|
+
// details is a JSON string, or the object itself (gjson reads either)
|
|
501
|
+
const d = typeof j?.details === "string" ? JSON.parse(j.details) : j?.details
|
|
490
502
|
if (d?.error?.message) msg += ": " + d.error.message
|
|
491
503
|
} catch {}
|
|
492
|
-
msg ||= `HTTP ${status}`
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
if (status === 401 || status === 403) return { status, message: "the sign-in lapsed — sign in again" }
|
|
504
|
+
msg ||= STATUS_CODES[status] ?? `HTTP ${status}`
|
|
505
|
+
// a refused chat is the built-in's 401, which never marked the account
|
|
506
|
+
// lapsed: only a refused refresh did (errorResponse says kept)
|
|
507
|
+
if (status === 401 || status === 403) return { status: 401, message: "the sign-in lapsed — sign in again" }
|
|
496
508
|
if (status === 429 || msg.toLowerCase().includes("quota")) return { status: 429, message: "usage limit reached: " + msg }
|
|
497
509
|
return { status, message: msg }
|
|
498
510
|
}
|
|
499
511
|
|
|
500
|
-
|
|
501
|
-
|
|
512
|
+
// errorResponse is a failure as OpenAI's API gives it. signIn is what it
|
|
513
|
+
// means for the account (magpie's X-Magpie-Sign-In): a 401 keeps it unless
|
|
514
|
+
// told expired, since the built-in marked only a refused refresh.
|
|
515
|
+
const errorResponse = ({ status, message, signIn = status === 401 ? "kept" : undefined }) =>
|
|
516
|
+
new Response(JSON.stringify({ error: { message, type: "qoder_error", code: status } }), {
|
|
517
|
+
status,
|
|
518
|
+
headers: { "Content-Type": "application/json", ...(signIn ? { "X-Magpie-Sign-In": signIn } : {}) },
|
|
519
|
+
})
|
|
502
520
|
|
|
503
521
|
// ---- the reply ----------------------------------------------------------------
|
|
504
522
|
|
|
@@ -741,6 +759,17 @@ async function bodyText(input, init) {
|
|
|
741
759
|
return typeof b === "string" ? b : new TextDecoder().decode(b)
|
|
742
760
|
}
|
|
743
761
|
|
|
762
|
+
// signed is res saying what it means for the sign-in, as the built-in's
|
|
763
|
+
// answer did: a renewed job token cleared the mark whatever came of the
|
|
764
|
+
// request; without one, a success left the mark as it was.
|
|
765
|
+
function signed(res, renewed) {
|
|
766
|
+
const said = renewed ? "renewed" : res.ok ? "kept" : null
|
|
767
|
+
if (!said) return res
|
|
768
|
+
const headers = new Headers(res.headers)
|
|
769
|
+
headers.set("X-Magpie-Sign-In", said)
|
|
770
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
771
|
+
}
|
|
772
|
+
|
|
744
773
|
// ---- usage --------------------------------------------------------------------
|
|
745
774
|
//
|
|
746
775
|
// The account's allowance as magpie's built-in Qoder account shows it
|
|
@@ -862,6 +891,17 @@ export async function QoderAuthPlugin({ client }) {
|
|
|
862
891
|
}
|
|
863
892
|
// each account's listing, the model configs a request carries
|
|
864
893
|
const listings = new Map()
|
|
894
|
+
// the accounts fresh renewed: the built-in took the lapse mark off on a
|
|
895
|
+
// renewed job token (qoderPersist), whatever the request then met
|
|
896
|
+
const renewals = new WeakSet()
|
|
897
|
+
// the accounts the models hook renewed: that hook has no way to say so
|
|
898
|
+
// and still answer the list, so the account's next usage read or answer
|
|
899
|
+
// says "renewed" for it
|
|
900
|
+
const unsaid = new Set()
|
|
901
|
+
const renewed = (cred) => {
|
|
902
|
+
const was = unsaid.delete(cred.uid)
|
|
903
|
+
return renewals.has(cred) || was
|
|
904
|
+
}
|
|
865
905
|
|
|
866
906
|
// fresh is the account with a live job token, refreshed and saved near
|
|
867
907
|
// its end.
|
|
@@ -870,9 +910,10 @@ export async function QoderAuthPlugin({ client }) {
|
|
|
870
910
|
const a = await getAuth()
|
|
871
911
|
if (a?.type !== "oauth" || !a.access || !a.uid) throw new SignInGone("Qoder: not signed in")
|
|
872
912
|
if (a.expires - Date.now() > REFRESH_LEAD) return a
|
|
873
|
-
const jt = await refreshJob(a.refresh)
|
|
913
|
+
const jt = await refreshJob(a.refresh, a.accountId || a.email || a.uid)
|
|
874
914
|
const next = { ...a, access: jt.token, refresh: jt.refresh_token, expires: expiresAt(jt) }
|
|
875
915
|
await client.auth.set({ path: { id: ID }, body: next })
|
|
916
|
+
renewals.add(next)
|
|
876
917
|
return next
|
|
877
918
|
})
|
|
878
919
|
|
|
@@ -896,10 +937,15 @@ export async function QoderAuthPlugin({ client }) {
|
|
|
896
937
|
return dt.token
|
|
897
938
|
})
|
|
898
939
|
|
|
899
|
-
// usage is the account's allowance, magpie's own hook
|
|
940
|
+
// usage is the account's allowance, magpie's own hook. signIn is what the read means for the sign-in, as the built-in's did:
|
|
941
|
+
// a refused job refresh marks it, a renewed one clears it, and nothing
|
|
942
|
+
// else does — not a clean read, nor a refused device token, which serves
|
|
943
|
+
// only the account pages
|
|
900
944
|
const usage = async (getAuth) => {
|
|
945
|
+
let signIn = "kept"
|
|
901
946
|
try {
|
|
902
947
|
const cred = await fresh(getAuth)
|
|
948
|
+
if (renewed(cred)) signIn = "renewed"
|
|
903
949
|
let env
|
|
904
950
|
try {
|
|
905
951
|
env = await fetchUsage(cred.deviceToken)
|
|
@@ -907,14 +953,47 @@ export async function QoderAuthPlugin({ client }) {
|
|
|
907
953
|
if (!(e instanceof UsageStatus) || (e.status !== 401 && e.status !== 403)) throw e
|
|
908
954
|
env = await fetchUsage(await deviceToken(getAuth, cred.deviceToken))
|
|
909
955
|
}
|
|
910
|
-
return parseUsage(env)
|
|
956
|
+
return { ...parseUsage(env), signIn }
|
|
911
957
|
} catch (e) {
|
|
912
|
-
return { error: e?.message ?? String(e) }
|
|
958
|
+
return { error: e?.message ?? String(e), signIn: e?.expired ? "expired" : signIn }
|
|
913
959
|
}
|
|
914
960
|
}
|
|
915
961
|
|
|
916
962
|
const signedInError = (e) =>
|
|
917
|
-
errorResponse({
|
|
963
|
+
errorResponse({
|
|
964
|
+
status: e instanceof SignInGone ? 401 : 502,
|
|
965
|
+
message: String(e?.message ?? e).replace(/^Qoder: /, ""),
|
|
966
|
+
signIn: e?.expired ? "expired" : undefined,
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
// ask is the answer to a chat completion on the live account cred.
|
|
970
|
+
const ask = async (chat, cred, init) => {
|
|
971
|
+
let m
|
|
972
|
+
try {
|
|
973
|
+
m = (await models(cred)).find((x) => x.key === chat.model) ?? (await models(cred, true)).find((x) => x.key === chat.model)
|
|
974
|
+
} catch (e) {
|
|
975
|
+
// the built-in answered any failure to read the list 400
|
|
976
|
+
// (QoderModelOf), a refused one included, marking nothing
|
|
977
|
+
return errorResponse({ status: 400, message: e.message })
|
|
978
|
+
}
|
|
979
|
+
if (!m) return errorResponse({ status: 400, message: `unknown or disabled model "${chat.model}"` })
|
|
980
|
+
const wire = encodeBody(JSON.stringify(qoderBody(chat, m)))
|
|
981
|
+
const headers = {
|
|
982
|
+
...cosyHeaders(CHAT_URL, cred, wire),
|
|
983
|
+
Accept: "text/event-stream",
|
|
984
|
+
"Cache-Control": "no-cache",
|
|
985
|
+
"X-Model-Key": m.key,
|
|
986
|
+
"X-Model-Source": m.source,
|
|
987
|
+
}
|
|
988
|
+
let res
|
|
989
|
+
try {
|
|
990
|
+
res = await fetch(CHAT_URL, { method: "POST", headers, body: wire, signal: init.signal })
|
|
991
|
+
} catch (e) {
|
|
992
|
+
return errorResponse({ status: 502, message: e.message })
|
|
993
|
+
}
|
|
994
|
+
if (!res.ok) return errorResponse(failure(res.status, (await res.text()).slice(0, 1 << 20)))
|
|
995
|
+
return answer(res, chat)
|
|
996
|
+
}
|
|
918
997
|
|
|
919
998
|
return {
|
|
920
999
|
auth: {
|
|
@@ -942,29 +1021,7 @@ export async function QoderAuthPlugin({ client }) {
|
|
|
942
1021
|
} catch (e) {
|
|
943
1022
|
return signedInError(e)
|
|
944
1023
|
}
|
|
945
|
-
|
|
946
|
-
try {
|
|
947
|
-
m = (await models(cred)).find((x) => x.key === chat.model) ?? (await models(cred, true)).find((x) => x.key === chat.model)
|
|
948
|
-
} catch (e) {
|
|
949
|
-
return errorResponse({ status: 502, message: e.message })
|
|
950
|
-
}
|
|
951
|
-
if (!m) return errorResponse({ status: 400, message: `unknown or disabled model "${chat.model}"` })
|
|
952
|
-
const wire = encodeBody(JSON.stringify(qoderBody(chat, m)))
|
|
953
|
-
const headers = {
|
|
954
|
-
...cosyHeaders(CHAT_URL, cred, wire),
|
|
955
|
-
Accept: "text/event-stream",
|
|
956
|
-
"Cache-Control": "no-cache",
|
|
957
|
-
"X-Model-Key": m.key,
|
|
958
|
-
"X-Model-Source": m.source,
|
|
959
|
-
}
|
|
960
|
-
let res
|
|
961
|
-
try {
|
|
962
|
-
res = await fetch(CHAT_URL, { method: "POST", headers, body: wire, signal: init.signal })
|
|
963
|
-
} catch (e) {
|
|
964
|
-
return errorResponse({ status: 502, message: e.message })
|
|
965
|
-
}
|
|
966
|
-
if (!res.ok) return errorResponse(failure(res.status, (await res.text()).slice(0, 1 << 20)))
|
|
967
|
-
return answer(res, chat)
|
|
1024
|
+
return signed(await ask(chat, cred, init), renewed(cred))
|
|
968
1025
|
},
|
|
969
1026
|
}
|
|
970
1027
|
},
|
|
@@ -987,8 +1044,17 @@ export async function QoderAuthPlugin({ client }) {
|
|
|
987
1044
|
id: ID,
|
|
988
1045
|
async models(provider, { auth } = {}) {
|
|
989
1046
|
if (auth?.type !== "oauth") return provider.models
|
|
1047
|
+
let cred
|
|
1048
|
+
try {
|
|
1049
|
+
cred = await fresh(async () => auth)
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
// the built-in listed through QoderCredentialOf: a refused job
|
|
1052
|
+
// refresh marked the account there; anything else marked nothing
|
|
1053
|
+
if (e?.expired) throw Object.assign(new Error(e.message), { signIn: "expired" })
|
|
1054
|
+
return provider.models
|
|
1055
|
+
}
|
|
1056
|
+
if (renewals.has(cred)) unsaid.add(cred.uid)
|
|
990
1057
|
try {
|
|
991
|
-
const cred = await fresh(async () => auth)
|
|
992
1058
|
const ms = await models(cred, true)
|
|
993
1059
|
if (!ms.length) return provider.models
|
|
994
1060
|
return Object.fromEntries(
|