@magpie-community/opencode-qoder-auth 0.2.8 → 0.2.10
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 +29 -2
- package/index.mjs +102 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -186,12 +186,35 @@ The hosts and reply shapes come from a real VPC account's report
|
|
|
186
186
|
|
|
187
187
|
Qoder gives credits for a daily claim (the Qoder client's campaign
|
|
188
188
|
`CLAIM_BENEFIT`, 100 Credits a day as reported), on both sites. magpie can
|
|
189
|
-
|
|
189
|
+
claim it for each account when its Qoder check-in switch is on
|
|
190
190
|
(Settings). It asks these pages through this plugin's fetch (since 0.2.7),
|
|
191
191
|
which sends them as the account, on the device token (`Bearer`,
|
|
192
192
|
`Cosy-ClientType: 10`) as usage is read; a refused device token is
|
|
193
193
|
rotated once and the page asked again.
|
|
194
194
|
|
|
195
|
+
International campaigns also need Qoder's native device identity:
|
|
196
|
+
`Cosy-MachineToken` and `Cosy-MachineType`. Without them, the server can
|
|
197
|
+
return HTTP 200 but omit the daily claim. The plugin reads them from
|
|
198
|
+
Qoder's own `runtime-info` helper for the signed-in uid, sharing an
|
|
199
|
+
in-flight read and caching it in memory for an hour. The same identity
|
|
200
|
+
goes on the list, the claim and a device-token retry; it is not saved in
|
|
201
|
+
the account's credentials.
|
|
202
|
+
|
|
203
|
+
- Install Qoder desktop, or sign in to Qoder CLI once, to make its native
|
|
204
|
+
runtime available. The plugin finds the desktop's `resources/umid`
|
|
205
|
+
(`Qoder.app` on macOS, `%LOCALAPPDATA%\Programs\Qoder` on Windows,
|
|
206
|
+
`/opt/Qoder` on Linux) or the runtime the CLI caches in `~/.qoder/.bin`
|
|
207
|
+
(`runtime-info-<platform>-<arch>-*` on macOS and Linux,
|
|
208
|
+
`umid-win32-x64-*\runtime-info.exe` on Windows; `QODER_CONFIG_DIR`,
|
|
209
|
+
`QODER_CLI_HOME` and `QODER_CONFIG_DIR_NAME` move it as they move the
|
|
210
|
+
CLI's folder).
|
|
211
|
+
- For another installation location, set `QODER_RUNTIME_INFO` to the
|
|
212
|
+
absolute path of Qoder's installed `runtime-info` executable
|
|
213
|
+
(`runtime-info.exe` on Windows), with its accompanying SDK files.
|
|
214
|
+
- A missing helper or invalid identity is an error, rather than a false
|
|
215
|
+
"no activity" result. Qoder CN keeps its existing requests and does
|
|
216
|
+
not need this local helper.
|
|
217
|
+
|
|
195
218
|
- **List:** `GET <openapi>/sash/api/v1/me/campaigns`: `campaigns[]` with
|
|
196
219
|
`campaignId`, `actionType`, `claimStatus` (`CLAIMABLE`, `CLAIMED`),
|
|
197
220
|
`startAt`, `endAt`, `benefit.amount`.
|
|
@@ -201,7 +224,11 @@ rotated once and the page asked again.
|
|
|
201
224
|
(Qoder CN). Other pages on those hosts are still refused.
|
|
202
225
|
|
|
203
226
|
Taken from [wallechfox/qoder-checkin](https://github.com/wallechfox/qoder-checkin),
|
|
204
|
-
which a user reports works
|
|
227
|
+
which a user reports works, and Qoder desktop 0.4.3's campaign requests.
|
|
228
|
+
International campaign discovery was checked with a real account on
|
|
229
|
+
2026-10-08: the native identity exposed the claimable 100 credits, and
|
|
230
|
+
omitting either header hid them. This verification only queried the
|
|
231
|
+
activity; a live claim was not performed.
|
|
205
232
|
|
|
206
233
|
## Not here
|
|
207
234
|
|
package/index.mjs
CHANGED
|
@@ -35,7 +35,11 @@
|
|
|
35
35
|
// <instance>-gateway.vpc.qoder.com.cn. The instance is kept on the account,
|
|
36
36
|
// and an account with none is served as before.
|
|
37
37
|
import { createCipheriv, createHash, publicEncrypt, randomBytes, randomUUID, constants } from "node:crypto"
|
|
38
|
+
import { execFile } from "node:child_process"
|
|
39
|
+
import { readdir, stat } from "node:fs/promises"
|
|
38
40
|
import { STATUS_CODES } from "node:http"
|
|
41
|
+
import { homedir } from "node:os"
|
|
42
|
+
import { join, resolve } from "node:path"
|
|
39
43
|
|
|
40
44
|
const CHAT_PATH = "/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1"
|
|
41
45
|
const MODELS_PATH = "/algo/api/v2/model/list?Encode=1"
|
|
@@ -1028,6 +1032,81 @@ async function fetchUsage(site, deviceToken) {
|
|
|
1028
1032
|
// credits among them (actionType CLAIM_BENEFIT) — and claims one
|
|
1029
1033
|
// (POST …/{id}/claim), as Qoder's client does, on the device token.
|
|
1030
1034
|
const CAMPAIGNS = "/sash/api/v1/me/campaigns"
|
|
1035
|
+
const CAMPAIGN_MACHINE_TTL = 60 * 60 * 1000 // Qoder's desktop client renews its native identity hourly
|
|
1036
|
+
|
|
1037
|
+
// campaignRuntime uses Qoder's own native device identity helper, installed
|
|
1038
|
+
// by its desktop client or cached by its CLI. A made-up token (including the
|
|
1039
|
+
// chat envelope's machine id and type 5) doesn't reveal international claims.
|
|
1040
|
+
//
|
|
1041
|
+
// Where each one keeps it (Qoder desktop 0.4.3's packages, qodercli 1.1.66):
|
|
1042
|
+
// - desktop: <install>/resources/umid/runtime-info(.exe): Qoder.app on
|
|
1043
|
+
// macOS, %LOCALAPPDATA%\Programs\Qoder (the user installer) on Windows,
|
|
1044
|
+
// /opt/Qoder (the .deb and the .rpm) on Linux;
|
|
1045
|
+
// - CLI: <config>/.bin, where <config> is QODER_CONFIG_DIR, else
|
|
1046
|
+
// QODER_CLI_HOME (or the home folder) joined with QODER_CONFIG_DIR_NAME
|
|
1047
|
+
// (or .qoder): a file runtime-info-<platform>-<arch>-<hash> on macOS and
|
|
1048
|
+
// Linux, a folder umid-win32-<arch>-<hash> holding runtime-info.exe on
|
|
1049
|
+
// Windows.
|
|
1050
|
+
async function campaignRuntime({ home = homedir(), platform = process.platform, arch = process.arch, env = process.env, opt = "/opt" } = {}) {
|
|
1051
|
+
if (env.QODER_RUNTIME_INFO) return resolve(env.QODER_RUNTIME_INFO)
|
|
1052
|
+
const name = platform === "win32" ? "runtime-info.exe" : "runtime-info"
|
|
1053
|
+
const isFile = async (path) => (await stat(path).catch(() => null))?.isFile() ?? false
|
|
1054
|
+
const paths = []
|
|
1055
|
+
if (platform === "win32") {
|
|
1056
|
+
const local = env.LOCALAPPDATA || join(home, "AppData", "Local")
|
|
1057
|
+
paths.push(join(local, "Programs", "Qoder", "resources", "umid", name))
|
|
1058
|
+
} else if (platform === "darwin") {
|
|
1059
|
+
for (const dir of ["/Applications", join(home, "Applications")]) paths.push(join(dir, "Qoder.app", "Contents", "Resources", "umid", name))
|
|
1060
|
+
} else if (platform === "linux") {
|
|
1061
|
+
paths.push(join(opt, "Qoder", "resources", "umid", name))
|
|
1062
|
+
}
|
|
1063
|
+
for (const path of paths) if (await isFile(path)) return path
|
|
1064
|
+
const config = env.QODER_CONFIG_DIR || join(env.QODER_CLI_HOME || env.GEMINI_CLI_HOME || home, env.QODER_CONFIG_DIR_NAME || ".qoder")
|
|
1065
|
+
const cache = join(config, ".bin")
|
|
1066
|
+
const entries = await readdir(cache, { withFileTypes: true }).catch(() => [])
|
|
1067
|
+
const found = []
|
|
1068
|
+
for (const d of entries) {
|
|
1069
|
+
let path
|
|
1070
|
+
if (platform === "win32") {
|
|
1071
|
+
if (d.isDirectory() && d.name.startsWith(`umid-win32-${arch}-`)) path = join(cache, d.name, name)
|
|
1072
|
+
} else if (d.isFile() && d.name.startsWith(`runtime-info-${platform}-${arch}-`) && !d.name.endsWith(".tmp")) {
|
|
1073
|
+
path = join(cache, d.name)
|
|
1074
|
+
}
|
|
1075
|
+
if (path && (await isFile(path))) found.push({ path, time: (await stat(path)).mtimeMs })
|
|
1076
|
+
}
|
|
1077
|
+
if (found.length) return found.sort((a, b) => b.time - a.time)[0].path
|
|
1078
|
+
throw new Error("Qoder check-in: device identity runtime not found; install Qoder desktop or sign in to Qoder CLI once, or set QODER_RUNTIME_INFO to its runtime-info executable")
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Read the SDK identity as Qoder does: environment 3 is international; on
|
|
1082
|
+
// Windows and macOS the uid goes over stdin, never into the command line.
|
|
1083
|
+
async function campaignMachine(uid) {
|
|
1084
|
+
if (!uid) throw new Error("Qoder check-in: device identity needs the account uid; sign in again")
|
|
1085
|
+
const file = await campaignRuntime()
|
|
1086
|
+
const args = process.platform === "linux" ? ["3"] : ["3", "--account-stdin"]
|
|
1087
|
+
const text = await new Promise((resolve, reject) => {
|
|
1088
|
+
const child = execFile(file, args, { timeout: 20_000, windowsHide: true, maxBuffer: 1 << 20 }, (err, out) => {
|
|
1089
|
+
if (err) reject(err)
|
|
1090
|
+
else resolve(out)
|
|
1091
|
+
})
|
|
1092
|
+
child.stdin?.on("error", () => {})
|
|
1093
|
+
child.stdin?.end(process.platform === "linux" ? undefined : JSON.stringify({ account: uid }) + "\n")
|
|
1094
|
+
}).catch(() => {
|
|
1095
|
+
throw new Error("Qoder check-in: could not read device identity; restart or update Qoder, or check QODER_RUNTIME_INFO")
|
|
1096
|
+
})
|
|
1097
|
+
try {
|
|
1098
|
+
const identity = JSON.parse(text.trim().split(/\r?\n/, 1)[0])
|
|
1099
|
+
const headers = {}
|
|
1100
|
+
for (const [key, field] of [["Cosy-MachineToken", "machineToken"], ["Cosy-MachineType", "machineType"]]) {
|
|
1101
|
+
const value = identity?.[field]?.trim()
|
|
1102
|
+
if (!value || value.length > 4096 || !/^[\x20-\x7e]+$/.test(value)) throw new Error("invalid identity")
|
|
1103
|
+
headers[key] = value
|
|
1104
|
+
}
|
|
1105
|
+
return headers
|
|
1106
|
+
} catch {
|
|
1107
|
+
throw new Error("Qoder check-in: invalid device identity from Qoder; restart or update Qoder")
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1031
1110
|
|
|
1032
1111
|
// campaignPage: url is the site's campaigns page or a claim under it, which
|
|
1033
1112
|
// the account's fetch sends as the account rather than as a chat.
|
|
@@ -1057,10 +1136,10 @@ function campaignTarget(base, s, url) {
|
|
|
1057
1136
|
|
|
1058
1137
|
// campaignCall is url asked with the device token; Qoder's answer comes back
|
|
1059
1138
|
// as it is.
|
|
1060
|
-
async function campaignCall(url, method, body, deviceToken, signal) {
|
|
1139
|
+
async function campaignCall(url, method, body, deviceToken, machine, signal) {
|
|
1061
1140
|
return fetch(url, {
|
|
1062
1141
|
method,
|
|
1063
|
-
headers: { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${deviceToken}`, "Cosy-ClientType": "10", "User-Agent": "Qoder" },
|
|
1142
|
+
headers: { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${deviceToken}`, "Cosy-ClientType": "10", "User-Agent": "Qoder", ...machine },
|
|
1064
1143
|
body: method === "GET" ? undefined : body || "{}",
|
|
1065
1144
|
signal: signal ?? AbortSignal.timeout(20_000),
|
|
1066
1145
|
})
|
|
@@ -1175,6 +1254,23 @@ const SITES = {
|
|
|
1175
1254
|
|
|
1176
1255
|
const makePlugin = (site) => async ({ client }) => {
|
|
1177
1256
|
const ID = site.id
|
|
1257
|
+
// The SDK identity is account-specific. Share an in-flight read, refresh
|
|
1258
|
+
// after an hour, and leave a failed read available for the next attempt.
|
|
1259
|
+
const campaignMachines = new Map()
|
|
1260
|
+
const machineForCampaign = async (uid) => {
|
|
1261
|
+
const now = Date.now()
|
|
1262
|
+
let entry = campaignMachines.get(uid)
|
|
1263
|
+
if (!entry || now - entry.at >= CAMPAIGN_MACHINE_TTL) {
|
|
1264
|
+
entry = { at: now, promise: campaignMachine(uid) }
|
|
1265
|
+
campaignMachines.set(uid, entry)
|
|
1266
|
+
}
|
|
1267
|
+
try {
|
|
1268
|
+
return await entry.promise
|
|
1269
|
+
} catch (e) {
|
|
1270
|
+
if (campaignMachines.get(uid) === entry) campaignMachines.delete(uid)
|
|
1271
|
+
throw e
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1178
1274
|
// serializes checking, rotating and saving tokens: a refresh token is
|
|
1179
1275
|
// spent once, so two refreshes would spend it twice
|
|
1180
1276
|
let lock = Promise.resolve()
|
|
@@ -1305,8 +1401,9 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1305
1401
|
if (!url) return errorResponse({ status: 404, message: "only chat completions are served" })
|
|
1306
1402
|
const method = String(init.method ?? input?.method ?? "GET").toUpperCase()
|
|
1307
1403
|
try {
|
|
1308
|
-
|
|
1309
|
-
|
|
1404
|
+
const machine = site.id === "qoder" ? await machineForCampaign(cred.uid) : {}
|
|
1405
|
+
let res = await campaignCall(url, method, body, cred.deviceToken, machine, init.signal)
|
|
1406
|
+
if (res.status === 401 || res.status === 403) res = await campaignCall(url, method, body, await deviceToken(getAuth, cred.deviceToken), machine, init.signal)
|
|
1310
1407
|
return signed(res, renewed(cred))
|
|
1311
1408
|
} catch (e) {
|
|
1312
1409
|
if (e?.expired) return signedInError(e)
|
|
@@ -1482,4 +1579,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
|
|
|
1482
1579
|
export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
|
|
1483
1580
|
|
|
1484
1581
|
// for tests
|
|
1485
|
-
export const _internal = { campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, SITES, decodeBody, events }
|
|
1582
|
+
export const _internal = { campaignRuntime, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, SITES, decodeBody, events }
|
package/package.json
CHANGED