@magpie-community/opencode-qoder-auth 0.2.6 → 0.2.7

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 +21 -0
  2. package/index.mjs +50 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -151,6 +151,27 @@ Neither magpie's built-in Qoder CN nor this plugin has been checked against
151
151
  every kind of qoder.cn account. If a sign-in or a chat fails, please open
152
152
  an issue with the error.
153
153
 
154
+ ## Daily check-in
155
+
156
+ Qoder gives credits for a daily claim (the Qoder client's campaign
157
+ `CLAIM_BENEFIT`, 100 Credits a day as reported), on both sites. magpie can
158
+ press it once a day for each account when its Qoder check-in switch is on
159
+ (Settings). It asks these pages through this plugin's fetch (since 0.2.7),
160
+ which sends them as the account, on the device token (`Bearer`,
161
+ `Cosy-ClientType: 10`) as usage is read; a refused device token is
162
+ rotated once and the page asked again.
163
+
164
+ - **List:** `GET <openapi>/sash/api/v1/me/campaigns`: `campaigns[]` with
165
+ `campaignId`, `actionType`, `claimStatus` (`CLAIMABLE`, `CLAIMED`),
166
+ `startAt`, `endAt`, `benefit.amount`.
167
+ - **Claim:** `POST <openapi>/sash/api/v1/me/campaigns/{id}/claim`, body
168
+ `{}`: `status` `CLAIMED` (or under `data`).
169
+ - `<openapi>` is `openapi.qoder.sh` (Qoder) or `openapi.qoder.com.cn`
170
+ (Qoder CN). Other pages on those hosts are still refused.
171
+
172
+ Taken from [wallechfox/qoder-checkin](https://github.com/wallechfox/qoder-checkin),
173
+ which a user reports works. Not yet tried here with a real Qoder account.
174
+
154
175
  ## Not here
155
176
 
156
177
  - Qoder's usage and quota display.
package/index.mjs CHANGED
@@ -953,6 +953,33 @@ async function fetchUsage(site, deviceToken) {
953
953
  return env
954
954
  }
955
955
 
956
+ // CAMPAIGNS is where Qoder lists the account's campaigns — the daily
957
+ // credits among them (actionType CLAIM_BENEFIT) — and claims one
958
+ // (POST …/{id}/claim), as Qoder's client does, on the device token.
959
+ const CAMPAIGNS = "/sash/api/v1/me/campaigns"
960
+
961
+ // campaignPage: url is the site's campaigns page or a claim under it, which
962
+ // the account's fetch sends as the account rather than as a chat.
963
+ function campaignPage(site, url) {
964
+ try {
965
+ const u = new URL(url)
966
+ return u.origin === new URL(site.openapi).origin && (u.pathname === CAMPAIGNS || /^\/sash\/api\/v1\/me\/campaigns\/[^/]+\/claim$/.test(u.pathname))
967
+ } catch {
968
+ return false
969
+ }
970
+ }
971
+
972
+ // campaignCall is url asked with the device token; Qoder's answer comes back
973
+ // as it is.
974
+ async function campaignCall(url, method, body, deviceToken, signal) {
975
+ return fetch(url, {
976
+ method,
977
+ headers: { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${deviceToken}`, "Cosy-ClientType": "10", "User-Agent": "Qoder" },
978
+ body: method === "GET" ? undefined : body || "{}",
979
+ signal: signal ?? AbortSignal.timeout(20_000),
980
+ })
981
+ }
982
+
956
983
  // refreshDevice trades the device refresh token for a new pair; it rotates
957
984
  // too, so the caller saves it.
958
985
  async function refreshDevice(site, refresh, chat = false) {
@@ -1174,6 +1201,27 @@ const makePlugin = (site) => async ({ client }) => {
1174
1201
  }
1175
1202
  }
1176
1203
 
1204
+ // campaign is a campaigns page (the daily check-in) asked as the account:
1205
+ // a refused device token is rotated once, as usage does, and asked again
1206
+ const campaign = async (getAuth, input, url, init) => {
1207
+ const body = await bodyText(input, init)
1208
+ let cred
1209
+ try {
1210
+ cred = await fresh(getAuth)
1211
+ } catch (e) {
1212
+ return signedInError(e)
1213
+ }
1214
+ const method = String(init.method ?? input?.method ?? "GET").toUpperCase()
1215
+ try {
1216
+ let res = await campaignCall(url, method, body, cred.deviceToken, init.signal)
1217
+ if (res.status === 401 || res.status === 403) res = await campaignCall(url, method, body, await deviceToken(getAuth, cred.deviceToken), init.signal)
1218
+ return signed(res, renewed(cred))
1219
+ } catch (e) {
1220
+ if (e?.expired) return signedInError(e)
1221
+ return errorResponse({ status: 502, message: e?.message ?? String(e) })
1222
+ }
1223
+ }
1224
+
1177
1225
  const signedInError = (e) =>
1178
1226
  errorResponse({
1179
1227
  status: e instanceof SignInGone ? 401 : 502,
@@ -1244,6 +1292,7 @@ const makePlugin = (site) => async ({ client }) => {
1244
1292
  // every chat completion written as Qoder's own request
1245
1293
  async fetch(input, init = {}) {
1246
1294
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
1295
+ if (campaignPage(site, url)) return campaign(getAuth, input, url, init)
1247
1296
  if (!/\/chat\/completions$/.test(new URL(url).pathname))
1248
1297
  return errorResponse({ status: 404, message: "only chat completions are served" })
1249
1298
  let chat
@@ -1309,4 +1358,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
1309
1358
  export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
1310
1359
 
1311
1360
  // for tests
1312
- export const _internal = { parseUsage, gfmt, when, failure, modelInfo, qoderBody, SITES, decodeBody, events }
1361
+ export const _internal = { campaignPage, parseUsage, gfmt, when, failure, modelInfo, qoderBody, SITES, decodeBody, events }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-qoder-auth",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
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",