@magpie-community/opencode-qoder-auth 0.2.28 → 0.2.30
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 +22 -2
- package/index.mjs +63 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -285,8 +285,28 @@ the account's credentials.
|
|
|
285
285
|
international account gets no daily claim until Qoder is installed.
|
|
286
286
|
- A helper that is there but can't be read, or gives an invalid identity,
|
|
287
287
|
is `failed` and asked again later, rather than a false "no activity"
|
|
288
|
-
result.
|
|
289
|
-
|
|
288
|
+
result.
|
|
289
|
+
|
|
290
|
+
Qoder CN (since 0.2.30) sends its own device identity the same way when
|
|
291
|
+
Qoder CN is installed: its desktop 0.4.9 and CLI (`qoderclicn`) both ask
|
|
292
|
+
Qoder CN's `runtime-info` for environment `0` (the international one is
|
|
293
|
+
`3`) and send it on the campaigns pages (yetone/magpie#2911, #304).
|
|
294
|
+
|
|
295
|
+
- The plugin finds Qoder CN desktop's `resources/umid` (`Qoder CN.app` on
|
|
296
|
+
macOS, `%LOCALAPPDATA%\Programs\Qoder CN` or `%ProgramFiles%\Qoder CN`
|
|
297
|
+
on Windows, `/opt/Qoder-CN` on Linux) or the runtime its CLI caches in
|
|
298
|
+
`~/.qoder-cn/.bin` (`QODERCN_CONFIG_DIR`, `QODERCN_CLI_HOME` and
|
|
299
|
+
`QODERCN_CONFIG_DIR_NAME` move it). `QODERCN_RUNTIME_INFO` names
|
|
300
|
+
another location; `QODER_RUNTIME_INFO` is only the international one's.
|
|
301
|
+
- Without it the requests go out as before, as Qoder CN's desktop does
|
|
302
|
+
when it can't read the identity, and a claim the list still offers is
|
|
303
|
+
made. When the list then offers no daily claim, the check-in is
|
|
304
|
+
`ineligible` for the day with a message to install Qoder CN desktop (or
|
|
305
|
+
sign in to its CLI); a helper that is there but can't be read makes it
|
|
306
|
+
`failed`, asked again later.
|
|
307
|
+
- Not tried with a real Qoder CN account: whether Qoder CN now hides the
|
|
308
|
+
daily claim from a request without the identity, as the international
|
|
309
|
+
site does, is inferred from its clients, not seen.
|
|
290
310
|
|
|
291
311
|
- **List:** `GET <openapi>/sash/api/v1/me/campaigns`: `campaigns[]` with
|
|
292
312
|
`campaignId`, `actionType`, `claimStatus` (`CLAIMABLE`, `CLAIMED`),
|
package/index.mjs
CHANGED
|
@@ -1526,23 +1526,45 @@ const CAMPAIGN_MACHINE_TTL = 60 * 60 * 1000 // Qoder's desktop client renews its
|
|
|
1526
1526
|
// (or .qoder): a file runtime-info-<platform>-<arch>-<hash> on macOS and
|
|
1527
1527
|
// Linux, a folder umid-win32-<arch>-<hash> holding runtime-info.exe on
|
|
1528
1528
|
// Windows.
|
|
1529
|
-
|
|
1530
|
-
|
|
1529
|
+
//
|
|
1530
|
+
// Qoder CN keeps its own (Qoder CN desktop 0.4.9's packages, qoderclicn
|
|
1531
|
+
// 1.1.77): Qoder CN.app on macOS (its installer's expectedBundleName),
|
|
1532
|
+
// /opt/Qoder-CN on Linux (the .deb), Qoder CN on Windows (the user
|
|
1533
|
+
// installer's "Uninstall Qoder CN.exe", electron-builder's
|
|
1534
|
+
// Programs\<productName>); its CLI's config is QODERCN_CONFIG_DIR, else
|
|
1535
|
+
// QODERCN_CLI_HOME (or home) joined with QODERCN_CONFIG_DIR_NAME (or
|
|
1536
|
+
// .qoder-cn). Both ask the helper for environment 0, the international
|
|
1537
|
+
// ones for 3 (the desktop's `regionCode === "global" ? 3 : 0`, the CLI's
|
|
1538
|
+
// UMID environment ONLINE = 0, US_ONLINE = 3).
|
|
1539
|
+
//
|
|
1540
|
+
// required: international Qoder lists the daily credits only on a request
|
|
1541
|
+
// carrying the identity. Qoder CN's desktop sends it when it can read it
|
|
1542
|
+
// and goes on without it when it can't, and a Qoder CN account was offered
|
|
1543
|
+
// and claimed the credits without one (0.2.21), so there it is sent when
|
|
1544
|
+
// Qoder CN is installed and left out when it isn't.
|
|
1545
|
+
const RUNTIMES = {
|
|
1546
|
+
qoder: { name: "Qoder", environment: "3", override: "QODER_RUNTIME_INFO", app: "Qoder", linux: "Qoder", cli: "QODER_", dir: ".qoder", required: true },
|
|
1547
|
+
"qoder-cn": { name: "Qoder CN", environment: "0", override: "QODERCN_RUNTIME_INFO", app: "Qoder CN", linux: "Qoder-CN", cli: "QODERCN_", dir: ".qoder-cn", required: false },
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
async function campaignRuntime({ home = homedir(), platform = process.platform, arch = process.arch, env = process.env, opt = "/opt", runtime = RUNTIMES.qoder } = {}) {
|
|
1551
|
+
if (env[runtime.override]) return resolve(env[runtime.override])
|
|
1531
1552
|
const name = platform === "win32" ? "runtime-info.exe" : "runtime-info"
|
|
1532
1553
|
const isFile = async (path) => (await stat(path).catch(() => null))?.isFile() ?? false
|
|
1533
1554
|
const paths = []
|
|
1534
1555
|
if (platform === "win32") {
|
|
1535
1556
|
const local = env.LOCALAPPDATA || join(home, "AppData", "Local")
|
|
1536
|
-
paths.push(join(local, "Programs",
|
|
1557
|
+
paths.push(join(local, "Programs", runtime.app, "resources", "umid", name))
|
|
1537
1558
|
// the machine-wide installer, as VS Code's system setup places it
|
|
1538
|
-
for (const dir of [env.ProgramFiles || env.PROGRAMFILES, env.ProgramW6432]) if (dir) paths.push(join(dir,
|
|
1559
|
+
for (const dir of [env.ProgramFiles || env.PROGRAMFILES, env.ProgramW6432]) if (dir) paths.push(join(dir, runtime.app, "resources", "umid", name))
|
|
1539
1560
|
} else if (platform === "darwin") {
|
|
1540
|
-
for (const dir of ["/Applications", join(home, "Applications")]) paths.push(join(dir, "
|
|
1561
|
+
for (const dir of ["/Applications", join(home, "Applications")]) paths.push(join(dir, runtime.app + ".app", "Contents", "Resources", "umid", name))
|
|
1541
1562
|
} else if (platform === "linux") {
|
|
1542
|
-
paths.push(join(opt,
|
|
1563
|
+
paths.push(join(opt, runtime.linux, "resources", "umid", name))
|
|
1543
1564
|
}
|
|
1544
1565
|
for (const path of paths) if (await isFile(path)) return path
|
|
1545
|
-
const
|
|
1566
|
+
const cli = (key) => env[runtime.cli + key]
|
|
1567
|
+
const config = cli("CONFIG_DIR") || join(cli("CLI_HOME") || env.GEMINI_CLI_HOME || home, cli("CONFIG_DIR_NAME") || runtime.dir)
|
|
1546
1568
|
const cache = join(config, ".bin")
|
|
1547
1569
|
const entries = await readdir(cache, { withFileTypes: true }).catch(() => [])
|
|
1548
1570
|
const found = []
|
|
@@ -1556,7 +1578,7 @@ async function campaignRuntime({ home = homedir(), platform = process.platform,
|
|
|
1556
1578
|
if (path && (await isFile(path))) found.push({ path, time: (await stat(path)).mtimeMs })
|
|
1557
1579
|
}
|
|
1558
1580
|
if (found.length) return found.sort((a, b) => b.time - a.time)[0].path
|
|
1559
|
-
throw new RuntimeMissing()
|
|
1581
|
+
throw new RuntimeMissing(runtime)
|
|
1560
1582
|
}
|
|
1561
1583
|
|
|
1562
1584
|
// RuntimeMissing: no Qoder desktop or CLI on this machine to read the
|
|
@@ -1566,19 +1588,21 @@ async function campaignRuntime({ home = homedir(), platform = process.platform,
|
|
|
1566
1588
|
// never makes up. Asking again won't change that until the user installs
|
|
1567
1589
|
// Qoder, so the check-in says what to do once instead of "tries again later".
|
|
1568
1590
|
class RuntimeMissing extends Error {
|
|
1569
|
-
constructor() {
|
|
1591
|
+
constructor(runtime = RUNTIMES.qoder) {
|
|
1592
|
+
const { name, override } = runtime
|
|
1570
1593
|
super(
|
|
1571
|
-
|
|
1594
|
+
`${name} check-in: device identity runtime not found. ${name} gives the daily credits only to a device its own app identifies, and this machine has neither ${name} desktop nor ${name} CLI to read that identity from. Install ${name} desktop (or install ${name} CLI and sign in to it once), or set ${override} to its runtime-info executable; magpie checks in again the next day`,
|
|
1572
1595
|
)
|
|
1573
1596
|
}
|
|
1574
1597
|
}
|
|
1575
1598
|
|
|
1576
|
-
// Read the SDK identity as Qoder does: environment 3 is international
|
|
1577
|
-
// Windows and macOS the uid goes over stdin, never into the
|
|
1578
|
-
|
|
1599
|
+
// Read the SDK identity as Qoder does: environment 3 is international, 0
|
|
1600
|
+
// Qoder CN; on Windows and macOS the uid goes over stdin, never into the
|
|
1601
|
+
// command line.
|
|
1602
|
+
async function campaignMachine(uid, runtime = RUNTIMES.qoder) {
|
|
1579
1603
|
if (!uid) throw new Error("Qoder check-in: device identity needs the account uid; sign in again")
|
|
1580
|
-
const file = await campaignRuntime()
|
|
1581
|
-
const args = process.platform === "linux" ? [
|
|
1604
|
+
const file = await campaignRuntime({ runtime })
|
|
1605
|
+
const args = process.platform === "linux" ? [runtime.environment] : [runtime.environment, "--account-stdin"]
|
|
1582
1606
|
const text = await new Promise((resolve, reject) => {
|
|
1583
1607
|
const child = execFile(file, args, { timeout: 20_000, windowsHide: true, maxBuffer: 1 << 20 }, (err, out) => {
|
|
1584
1608
|
if (err) reject(err)
|
|
@@ -1587,7 +1611,7 @@ async function campaignMachine(uid) {
|
|
|
1587
1611
|
child.stdin?.on("error", () => {})
|
|
1588
1612
|
child.stdin?.end(process.platform === "linux" ? undefined : JSON.stringify({ account: uid }) + "\n")
|
|
1589
1613
|
}).catch(() => {
|
|
1590
|
-
throw new Error(
|
|
1614
|
+
throw new Error(`${runtime.name} check-in: could not read device identity; restart or update ${runtime.name}, or check ${runtime.override}`)
|
|
1591
1615
|
})
|
|
1592
1616
|
try {
|
|
1593
1617
|
const identity = JSON.parse(text.trim().split(/\r?\n/, 1)[0])
|
|
@@ -1599,7 +1623,7 @@ async function campaignMachine(uid) {
|
|
|
1599
1623
|
}
|
|
1600
1624
|
return headers
|
|
1601
1625
|
} catch {
|
|
1602
|
-
throw new Error(
|
|
1626
|
+
throw new Error(`${runtime.name} check-in: invalid device identity from ${runtime.name}; restart or update ${runtime.name}`)
|
|
1603
1627
|
}
|
|
1604
1628
|
}
|
|
1605
1629
|
|
|
@@ -1754,12 +1778,13 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1754
1778
|
const ID = site.id
|
|
1755
1779
|
// The SDK identity is account-specific. Share an in-flight read, refresh
|
|
1756
1780
|
// after an hour, and leave a failed read available for the next attempt.
|
|
1781
|
+
const runtime = RUNTIMES[ID]
|
|
1757
1782
|
const campaignMachines = new Map()
|
|
1758
1783
|
const machineForCampaign = async (uid) => {
|
|
1759
1784
|
const now = Date.now()
|
|
1760
1785
|
let entry = campaignMachines.get(uid)
|
|
1761
1786
|
if (!entry || now - entry.at >= CAMPAIGN_MACHINE_TTL) {
|
|
1762
|
-
entry = { at: now, promise: campaignMachine(uid) }
|
|
1787
|
+
entry = { at: now, promise: campaignMachine(uid, runtime) }
|
|
1763
1788
|
campaignMachines.set(uid, entry)
|
|
1764
1789
|
}
|
|
1765
1790
|
try {
|
|
@@ -1769,6 +1794,16 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1769
1794
|
throw e
|
|
1770
1795
|
}
|
|
1771
1796
|
}
|
|
1797
|
+
// campaignIdentity: the headers a campaigns request carries, and why
|
|
1798
|
+
// there are none where the site goes on without them (Qoder CN).
|
|
1799
|
+
const campaignIdentity = async (uid) => {
|
|
1800
|
+
if (runtime.required) return { machine: await machineForCampaign(uid) }
|
|
1801
|
+
try {
|
|
1802
|
+
return { machine: await machineForCampaign(uid) }
|
|
1803
|
+
} catch (e) {
|
|
1804
|
+
return { machine: {}, unread: e }
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1772
1807
|
// serializes checking, rotating and saving tokens: a refresh token is
|
|
1773
1808
|
// spent once, so two refreshes would spend it twice
|
|
1774
1809
|
let lock = Promise.resolve()
|
|
@@ -1927,7 +1962,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1927
1962
|
if (!url) return errorResponse({ status: 404, message: "only chat completions are served" })
|
|
1928
1963
|
const method = String(init.method ?? input?.method ?? "GET").toUpperCase()
|
|
1929
1964
|
try {
|
|
1930
|
-
const machine =
|
|
1965
|
+
const { machine } = await campaignIdentity(cred.uid)
|
|
1931
1966
|
// A PAT account has no device token: its job token signs the campaigns
|
|
1932
1967
|
// pages too, as it does the usage read. Nothing is rotated on that
|
|
1933
1968
|
// path — the refusal is the answer.
|
|
@@ -1956,7 +1991,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1956
1991
|
const base = campaignTarget(site, siteOf(site, cred), site.openapi + CAMPAIGNS)
|
|
1957
1992
|
if (!base) return { outcome: "inactive", message: "no campaigns page for this account" }
|
|
1958
1993
|
try {
|
|
1959
|
-
const machine
|
|
1994
|
+
const { machine, unread } = await campaignIdentity(cred.uid)
|
|
1960
1995
|
// A PAT account has no device token: the campaigns pages take the job
|
|
1961
1996
|
// token the exchange gave, as the usage endpoint does. A live Qoder CN
|
|
1962
1997
|
// PAT account answered 200 to the list and CLAIMED to a claim, so the
|
|
@@ -1994,7 +2029,13 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1994
2029
|
const amount = Number(done.benefit?.amount)
|
|
1995
2030
|
return { outcome: "done", ...(Number.isFinite(amount) && amount > 0 ? { credit: amount } : {}) }
|
|
1996
2031
|
}
|
|
1997
|
-
|
|
2032
|
+
if (campaigns.length) return { outcome: "failed", message: "campaigns: today's credits aren't offered yet" }
|
|
2033
|
+
// Asked without Qoder CN's identity and offered nothing: what an
|
|
2034
|
+
// international list says to a request without one, so say why
|
|
2035
|
+
// (no Qoder CN on this machine) or ask again (it didn't answer).
|
|
2036
|
+
if (unread instanceof RuntimeMissing) return { outcome: "ineligible", message: unread.message }
|
|
2037
|
+
if (unread) return { outcome: "failed", message: unread.message }
|
|
2038
|
+
return { outcome: "inactive" }
|
|
1998
2039
|
}
|
|
1999
2040
|
const claimRes = await call(base.replace(/\/$/, "") + "/" + encodeURIComponent(campaign.campaignId) + "/claim", "POST", "{}")
|
|
2000
2041
|
let claim = {}
|
|
@@ -2233,4 +2274,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
|
|
|
2233
2274
|
export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
|
|
2234
2275
|
|
|
2235
2276
|
// for tests
|
|
2236
|
-
export const _internal = { campaignRuntime, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, qoderMessages, cacheMarked, sessionOf, SITES, decodeBody, events, QUEUE, queueState }
|
|
2277
|
+
export const _internal = { campaignRuntime, RUNTIMES, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, qoderMessages, cacheMarked, sessionOf, SITES, decodeBody, events, QUEUE, queueState }
|
package/package.json
CHANGED