@magpie-community/opencode-qoder-auth 0.2.16 → 0.2.18

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 +4 -1
  2. package/index.mjs +82 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -201,7 +201,10 @@ The hosts and reply shapes come from a real VPC account's report
201
201
  Qoder gives credits for a daily claim (the Qoder client's campaign
202
202
  `CLAIM_BENEFIT`, 100 Credits a day as reported), on both sites. magpie can
203
203
  claim it for each account when its Qoder check-in switch is on
204
- (Settings). It asks these pages through this plugin's fetch (since 0.2.7),
204
+ (Settings), through `auth.checkin`: an already claimed campaign is `done`,
205
+ a campaign not available is `inactive`, and a claimable one is `claimed`
206
+ with its benefit amount. It asks these pages through this plugin's own
207
+ campaigns path (since 0.2.7),
205
208
  which sends them as the account, on the device token (`Bearer`,
206
209
  `Cosy-ClientType: 10`) as usage is read; a refused device token is
207
210
  rotated once and the page asked again.
package/index.mjs CHANGED
@@ -799,16 +799,26 @@ function failure(status, text) {
799
799
  let code = ""
800
800
  let queue
801
801
  try {
802
- const j = JSON.parse(text)
802
+ let j = JSON.parse(msg)
803
+ // #167: an HTTP/message wrapper can hold the actual 10605 body.
804
+ // Unwrap only that known queue code; a real auth/credit/request code
805
+ // on the outer body remains authoritative.
806
+ const outerCode = j?.code == null ? "" : String(j.code)
807
+ if (["", "401", "403"].includes(outerCode) && typeof j?.message === "string") {
808
+ try {
809
+ const inner = JSON.parse(j.message.trim())
810
+ if (inner && !Array.isArray(inner) && String(inner.code) === QUEUED) j = inner
811
+ } catch {}
812
+ }
803
813
  code = j?.code == null ? "" : String(j.code)
804
814
  if (j?.message) msg = String(j.message)
805
815
  // details is a JSON string, or the object itself (gjson reads either)
806
- const d = typeof j?.details === "string" ? JSON.parse(j.details) : j?.details
816
+ const d = typeof j?.details === "string" ? JSON.parse(j.details.trim()) : j?.details
807
817
  if (d?.error?.message) msg += ": " + d.error.message
808
818
  // the queue's state: the message itself, as a JSON string, or a field
809
819
  queue = j?.queue && typeof j.queue === "object" ? j.queue : undefined
810
820
  if (!queue && typeof j?.message === "string" && j.message.trim().startsWith("{")) {
811
- const q = JSON.parse(j.message)
821
+ const q = JSON.parse(j.message.trim())
812
822
  if (q && typeof q === "object" && ("isQueued" in q || "serviceAvailable" in q || "retryAfterSeconds" in q)) queue = q
813
823
  }
814
824
  } catch {}
@@ -1029,7 +1039,7 @@ async function* events(body) {
1029
1039
  if (inner.trim() === "[DONE]") break
1030
1040
  let chunk
1031
1041
  try {
1032
- chunk = JSON.parse(inner)
1042
+ chunk = JSON.parse(inner.trim())
1033
1043
  } catch {
1034
1044
  continue
1035
1045
  }
@@ -1618,6 +1628,73 @@ const makePlugin = (site) => async ({ client }) => {
1618
1628
  }
1619
1629
  }
1620
1630
 
1631
+ // checkin claims the daily credits through the same campaigns page as
1632
+ // Qoder's client. A campaign already claimed is done; one not available
1633
+ // is inactive, never an error that magpie would retry all day.
1634
+ const checkin = async (getAuth) => {
1635
+ let cred
1636
+ try {
1637
+ cred = await fresh(getAuth)
1638
+ } catch (e) {
1639
+ return { outcome: "failed", message: e?.message ?? String(e), ...(e?.expired ? { signIn: "expired" } : {}) }
1640
+ }
1641
+ const base = campaignTarget(site, siteOf(site, cred), site.openapi + CAMPAIGNS)
1642
+ if (!base) return { outcome: "inactive", message: "no campaigns page for this account" }
1643
+ try {
1644
+ const machine = site.id === "qoder" ? await machineForCampaign(cred.uid) : {}
1645
+ const call = async (url, method, body) => {
1646
+ let res = await campaignCall(url, method, body, cred.deviceToken, machine)
1647
+ if (res.status === 401 || res.status === 403) {
1648
+ const token = await deviceToken(getAuth, cred.deviceToken)
1649
+ const current = latest(await getAuth())
1650
+ cred = { ...current, deviceToken: token }
1651
+ res = await campaignCall(url, method, body, token, machine)
1652
+ }
1653
+ return res
1654
+ }
1655
+ const listRes = await call(base, "GET")
1656
+ let list = {}
1657
+ let listRaw = ""
1658
+ try {
1659
+ listRaw = await listRes.text()
1660
+ list = JSON.parse(listRaw) ?? {}
1661
+ } catch {
1662
+ return { outcome: "failed", message: `campaigns: unreadable reply ${listRaw.trim().slice(0, 120) || "(empty)"}` }
1663
+ }
1664
+ if (listRes.status !== 200) return { outcome: "failed", message: `campaigns: HTTP ${listRes.status}` }
1665
+ const now = Date.now()
1666
+ const active = (c) => (!c.startAt || c.startAt * 1000 <= now) && (!c.endAt || c.endAt * 1000 > now)
1667
+ const campaigns = (Array.isArray(list.campaigns) ? list.campaigns : []).filter((x) => x?.actionType === "CLAIM_BENEFIT")
1668
+ const campaign = campaigns.find((x) => x.claimStatus === "CLAIMABLE")
1669
+ if (!campaign) {
1670
+ const done = campaigns.find((x) => x.claimStatus === "CLAIMED" && active(x))
1671
+ if (done) {
1672
+ const amount = Number(done.benefit?.amount)
1673
+ return { outcome: "done", ...(Number.isFinite(amount) && amount > 0 ? { credit: amount } : {}) }
1674
+ }
1675
+ return campaigns.length ? { outcome: "failed", message: "campaigns: today's credits aren't offered yet" } : { outcome: "inactive" }
1676
+ }
1677
+ const claimRes = await call(base.replace(/\/$/, "") + "/" + encodeURIComponent(campaign.campaignId) + "/claim", "POST", "{}")
1678
+ let claim = {}
1679
+ let claimRaw = ""
1680
+ try {
1681
+ claimRaw = await claimRes.text()
1682
+ claim = JSON.parse(claimRaw) ?? {}
1683
+ } catch {
1684
+ return { outcome: "failed", message: `claim: unreadable reply ${claimRaw.trim().slice(0, 120) || "(empty)"}` }
1685
+ }
1686
+ const said = claim?.status ?? claim?.data?.status
1687
+ if (claimRes.status !== 200) return { outcome: "failed", message: `claim: HTTP ${claimRes.status}` }
1688
+ if (said === "CLAIMED") {
1689
+ const amount = Number(claim?.benefit?.amount ?? claim?.data?.benefit?.amount ?? campaign.benefit?.amount)
1690
+ return { outcome: "claimed", ...(Number.isFinite(amount) && amount > 0 ? { credit: amount } : {}) }
1691
+ }
1692
+ return { outcome: "failed", message: `claim: ${said || "unknown answer"}` }
1693
+ } catch (e) {
1694
+ return { outcome: "failed", message: e?.message ?? String(e), ...(e?.expired ? { signIn: "expired" } : {}) }
1695
+ }
1696
+ }
1697
+
1621
1698
  const signedInError = (e) =>
1622
1699
  errorResponse({
1623
1700
  status: e instanceof SignInGone ? 401 : 502,
@@ -1774,6 +1851,7 @@ const makePlugin = (site) => async ({ client }) => {
1774
1851
  : []),
1775
1852
  ],
1776
1853
  usage,
1854
+ checkin,
1777
1855
  },
1778
1856
  async config(config) {
1779
1857
  config.provider ??= {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-qoder-auth",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
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",