@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.
Files changed (2) hide show
  1. package/index.mjs +107 -41
  2. 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
- class SignInGone extends Error {}
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) throw new SignInGone("Qoder: the sign-in has expired — sign in again")
224
- if (r.status !== 200) throw new Error(`Qoder job token refresh: status ${r.status}`)
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("Qoder job token refresh: incomplete token pair")
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
- const d = typeof j?.details === "string" ? JSON.parse(j.details) : null
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
- // only Qoder's 401 says the job token itself is refused; a 403 is kept as
494
- // what it is, as magpie marks the account lapsed on a 401
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
- const errorResponse = ({ status, message }) =>
501
- new Response(JSON.stringify({ error: { message, type: "qoder_error", code: status } }), { status, headers: { "Content-Type": "application/json" } })
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({ status: e instanceof SignInGone ? 401 : 502, message: String(e?.message ?? e).replace(/^Qoder: /, "") })
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
- let m
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(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-qoder-auth",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Qoder subscription in OpenCode and magpie",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",