@magpie-community/opencode-workbuddy-auth 0.1.35 → 0.1.37
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 +9 -0
- package/index.mjs +105 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,15 @@ The package exports `WorkBuddyAuthPlugin` (`workbuddy`) and
|
|
|
26
26
|
|
|
27
27
|
WorkBuddy 5.6 and later keep the sign-in encrypted, which only the app
|
|
28
28
|
opens, so this way fails there and says so: use the browser sign-in.
|
|
29
|
+
- **API key** (since 0.1.37). A key made on the build's own keys page
|
|
30
|
+
(`https://www.workbuddy.cn/profile/keys` for `workbuddy`,
|
|
31
|
+
`https://www.workbuddy.ai/profile/keys` for `workbuddy-ai`), sent as
|
|
32
|
+
WorkBuddy's own CLI sends one: `X-API-Key` and `Authorization: Bearer`,
|
|
33
|
+
both the key, and `X-User-Id: anonymous_<the key's last 8 characters>`,
|
|
34
|
+
to the same API as the other sign-ins. A key isn't renewed; a refused one
|
|
35
|
+
says to check it. A key's credits are unknown (WorkBuddy's own client reads
|
|
36
|
+
none with a key), and its models are `/v3/config`'s only for `ck_`/`pt_`
|
|
37
|
+
keys, as the CLI asks; any other key keeps the list below.
|
|
29
38
|
|
|
30
39
|
A browser sign-in is kept as an OpenCode `oauth` auth: the access and
|
|
31
40
|
refresh tokens, their expiry, the user id and the domain; desktop's as a
|
package/index.mjs
CHANGED
|
@@ -67,6 +67,8 @@ const SITES = {
|
|
|
67
67
|
authID: "workbuddy-desktop",
|
|
68
68
|
platform: "workbuddy",
|
|
69
69
|
ai: false,
|
|
70
|
+
// where this build's API keys are made (see keyAccount)
|
|
71
|
+
keys: "https://www.workbuddy.cn/profile/keys",
|
|
70
72
|
models: CN_MODELS,
|
|
71
73
|
// it serves the Claude models on Anthropic's Messages API too (see
|
|
72
74
|
// nativeOf)
|
|
@@ -79,6 +81,7 @@ const SITES = {
|
|
|
79
81
|
authID: "workbuddy-desktop-ai",
|
|
80
82
|
platform: "workbuddy-ai",
|
|
81
83
|
ai: true,
|
|
84
|
+
keys: "https://www.workbuddy.ai/profile/keys",
|
|
82
85
|
models: AI_MODELS,
|
|
83
86
|
// models whose images WorkBuddy AI drops though its config says they
|
|
84
87
|
// take them (see textOnly)
|
|
@@ -299,7 +302,14 @@ async function refreshed(site, auth) {
|
|
|
299
302
|
function sign(site, a, headers) {
|
|
300
303
|
headers.set("Authorization", "Bearer " + a.access)
|
|
301
304
|
headers.set("X-User-Id", a.uid ?? "")
|
|
302
|
-
|
|
305
|
+
if (a.source === "key") {
|
|
306
|
+
// the key's own header, and no X-Domain: a key's session has none
|
|
307
|
+
headers.set("X-API-Key", a.key)
|
|
308
|
+
headers.delete("X-Domain")
|
|
309
|
+
} else {
|
|
310
|
+
headers.delete("X-API-Key")
|
|
311
|
+
headers.set("X-Domain", domainOf(site, a))
|
|
312
|
+
}
|
|
303
313
|
headers.set("X-Product", "SaaS")
|
|
304
314
|
headers.set("X-IDE-Type", "WorkBuddy")
|
|
305
315
|
headers.set("User-Agent", "WorkBuddy/" + UA_VERSION)
|
|
@@ -313,6 +323,31 @@ function sign(site, a, headers) {
|
|
|
313
323
|
headers.set("X-Agent-Type", "main")
|
|
314
324
|
}
|
|
315
325
|
|
|
326
|
+
// keyAccount is the account an API key (OpenCode's {type: "api", key}) is
|
|
327
|
+
// sent as, the way WorkBuddy's own CLI sends one (@tencent-ai/codebuddy-code
|
|
328
|
+
// 2.177.0, CODEBUDDY_API_KEY): its API-key sign-in is a session whose access
|
|
329
|
+
// token is the key and whose uid is "anonymous_" and the key's last eight
|
|
330
|
+
// characters (ACC_USER_ID unset), personal, with no domain and no
|
|
331
|
+
// enterprise; and its model requests carry the key twice, X-API-Key and
|
|
332
|
+
// Authorization: Bearer (getCustomHeaders). Its requests go to the build's
|
|
333
|
+
// own host, the one its other sign-ins use: for the China build that is
|
|
334
|
+
// copilot.tencent.com, the CLI's own endpoint for that build
|
|
335
|
+
// (CODEBUDDY_INTERNET_ENVIRONMENT=internal, product.internal.json), and the
|
|
336
|
+
// same API as www.workbuddy.cn, where the keys are made (the CLI's
|
|
337
|
+
// CN_SAAS_CORE_API_FRONT_DOORS); for the international build,
|
|
338
|
+
// www.workbuddy.ai (the CLI's own default, www.codebuddy.ai, is CodeBuddy's,
|
|
339
|
+
// not WorkBuddy's). Nothing renews a key.
|
|
340
|
+
function keyAccount(auth) {
|
|
341
|
+
const key = typeof auth?.key === "string" ? auth.key.trim() : ""
|
|
342
|
+
if (!key) return null
|
|
343
|
+
return { key, access: key, uid: "anonymous_" + key.slice(-8), source: "key" }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// remoteConfig says whether WorkBuddy's own CLI asks /v3/config for the
|
|
347
|
+
// account's models with this key: only for keys that start ck_ or pt_
|
|
348
|
+
// (shouldLoadRemoteConfig); any other key keeps the built-in list.
|
|
349
|
+
const remoteConfig = (key) => /^(ck_|pt_)/.test(key)
|
|
350
|
+
|
|
316
351
|
// enterpriseHeaders names the enterprise (team) an account signed in as, as
|
|
317
352
|
// WorkBuddy's own client does on every request it makes for that account
|
|
318
353
|
// (its auth interceptor: X-Enterprise-Id and X-Tenant-Id, both the
|
|
@@ -1419,6 +1454,8 @@ async function unanswered(site, a, body, err, signal) {
|
|
|
1419
1454
|
if (signal?.aborted || err?.name === "AbortError" || err?.name === "TimeoutError") throw err
|
|
1420
1455
|
const size = byteLength(body)
|
|
1421
1456
|
if (size <= EDGE_READS) throw err
|
|
1457
|
+
// the billing API isn't asked with a key: what it answers one is unknown
|
|
1458
|
+
if (a.source === "key") throw err
|
|
1422
1459
|
let status = 0
|
|
1423
1460
|
let challenge = ""
|
|
1424
1461
|
try {
|
|
@@ -1546,6 +1583,18 @@ function refusedText(site, a, why, note) {
|
|
|
1546
1583
|
)
|
|
1547
1584
|
}
|
|
1548
1585
|
|
|
1586
|
+
// keyRefused is the answer to a request WorkBuddy refused an API key for
|
|
1587
|
+
// (401): a key isn't renewed, so it says to check the key. Marked expired
|
|
1588
|
+
// only on WorkBuddy's word that the token itself is refused (invalidToken).
|
|
1589
|
+
function keyRefused(site, expired, note) {
|
|
1590
|
+
const message = `${site.name} refused this API key (401). ${note ? note + " " : ""}Check the key at ${site.keys}, then add it again in magpie: Providers → ${site.name}.`
|
|
1591
|
+
return new Response(JSON.stringify({ error: { message, type: "authentication_error", code: 401 } }), {
|
|
1592
|
+
status: 401,
|
|
1593
|
+
statusText: "Unauthorized",
|
|
1594
|
+
headers: { "content-type": "application/json", "X-Magpie-Sign-In": expired ? "expired" : "kept" },
|
|
1595
|
+
})
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1549
1598
|
// signedOut is the answer to a request WorkBuddy refused this account's
|
|
1550
1599
|
// sign-in for, renewed or not: a 401 that says so and what to do, which
|
|
1551
1600
|
// WorkBuddy's own (an HTML page, its {code, msg}) doesn't. expired marks
|
|
@@ -1728,14 +1777,33 @@ function runsOutOf(sum, packages, now = Date.now()) {
|
|
|
1728
1777
|
// is over.
|
|
1729
1778
|
function cycleOf(sum, packages, now = Date.now()) {
|
|
1730
1779
|
const code = sum?.SubscriptionPackageCode
|
|
1731
|
-
if (!code) return null
|
|
1732
1780
|
const at = (v) => (typeof v === "string" && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(v.trim()) ? Date.parse(v.trim().replace(" ", "T") + "+08:00") : 0)
|
|
1733
|
-
|
|
1734
|
-
if (p?.PackageCode !== code) continue
|
|
1781
|
+
const of = (p) => {
|
|
1735
1782
|
const end = at(p?.CycleEndTime), start = at(p?.CycleStartTime)
|
|
1736
|
-
if (!(end > now))
|
|
1783
|
+
if (!(end > now)) return null
|
|
1737
1784
|
return { resetsAt: new Date(end).toISOString(), ...(start > 0 && start < end ? { span: (end - start) / 1000 } : {}) }
|
|
1738
1785
|
}
|
|
1786
|
+
// A summary that does name one goes by it alone: named and not listed, or
|
|
1787
|
+
// its cycle over, is unknown — a paid plan's cycle is its subscription's,
|
|
1788
|
+
// never the slice's (yetone, #415).
|
|
1789
|
+
if (code) {
|
|
1790
|
+
for (const p of packages ?? []) if (p?.PackageCode === code) return of(p)
|
|
1791
|
+
return null
|
|
1792
|
+
}
|
|
1793
|
+
// A free account's summary names no subscription at all: both sites answer
|
|
1794
|
+
// SubscriptionPackageCode "" (copilot.tencent.com and www.workbuddy.ai,
|
|
1795
|
+
// measured 2026-10-26 on eight CN accounts and one AI one), so this
|
|
1796
|
+
// returned null and the card of an account whose credits are being spent
|
|
1797
|
+
// said nothing of when they come back — the one thing cycleOf is for.
|
|
1798
|
+
//
|
|
1799
|
+
// Its plan's own credits are the slice (CapacityType 4): packagesOf's
|
|
1800
|
+
// free list returns a month's plan_base with it, 00:00:00 to 23:59:59
|
|
1801
|
+
// UTC+8, the same cycle on every account on both sites (500 credits on
|
|
1802
|
+
// the CN site, 100 on the international), and its CycleEndTime is when
|
|
1803
|
+
// the plan gives the next month's back. A slice tells no account apart,
|
|
1804
|
+
// which is why runsOutOf keeps it out: which account to spend first is a
|
|
1805
|
+
// different question from when the plan refills.
|
|
1806
|
+
for (const p of packages ?? []) if (String(p?.CapacityType) === SLICE) return of(p)
|
|
1739
1807
|
return null
|
|
1740
1808
|
}
|
|
1741
1809
|
|
|
@@ -1988,6 +2056,17 @@ function makePlugin(site) {
|
|
|
1988
2056
|
// the account's own list, when it's signed in and WorkBuddy answers;
|
|
1989
2057
|
// else the list above
|
|
1990
2058
|
async models(provider, { auth }) {
|
|
2059
|
+
if (auth?.type === "api") {
|
|
2060
|
+
// asked as WorkBuddy's own CLI asks with a key (remoteConfig)
|
|
2061
|
+
const a = keyAccount(auth)
|
|
2062
|
+
if (!a || !remoteConfig(a.key)) return provider.models
|
|
2063
|
+
try {
|
|
2064
|
+
const ms = await liveModels(site, a)
|
|
2065
|
+
return ms.length ? Object.fromEntries(ms.map((m) => [m.id, modelOf(site, provider, m)])) : provider.models
|
|
2066
|
+
} catch {
|
|
2067
|
+
return provider.models
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
1991
2070
|
if (auth?.type !== "oauth" || !(auth.access || auth.source)) return provider.models
|
|
1992
2071
|
try {
|
|
1993
2072
|
const a = await current(site, client, auth)
|
|
@@ -2010,7 +2089,7 @@ function makePlugin(site) {
|
|
|
2010
2089
|
refresh: (auth) => refreshed(site, auth),
|
|
2011
2090
|
async loader(getAuth) {
|
|
2012
2091
|
const auth = await getAuth()
|
|
2013
|
-
if (auth?.type !== "oauth") return {}
|
|
2092
|
+
if (auth?.type !== "oauth" && auth?.type !== "api") return {}
|
|
2014
2093
|
return {
|
|
2015
2094
|
// no baseURL: a model's own api.url says where it is served —
|
|
2016
2095
|
// <endpoint>/v2 for a chat, <endpoint>/v1 for a Claude model on
|
|
@@ -2019,8 +2098,9 @@ function makePlugin(site) {
|
|
|
2019
2098
|
apiKey: "",
|
|
2020
2099
|
async fetch(input, init) {
|
|
2021
2100
|
const stored = await getAuth()
|
|
2022
|
-
|
|
2023
|
-
|
|
2101
|
+
const keyed = stored?.type === "api" ? keyAccount(stored) : null
|
|
2102
|
+
if (stored?.type !== "oauth" && !keyed) throw new Error(`${site.name} isn't signed in`)
|
|
2103
|
+
let a = keyed ?? (await current(site, client, stored))
|
|
2024
2104
|
const req = input instanceof Request ? input : null
|
|
2025
2105
|
const headers = new Headers(init?.headers ?? req?.headers)
|
|
2026
2106
|
headers.delete("authorization")
|
|
@@ -2077,6 +2157,12 @@ function makePlugin(site) {
|
|
|
2077
2157
|
const res = await post(b)
|
|
2078
2158
|
if (res.status !== 401) return res
|
|
2079
2159
|
if (renewed) return out("it refused the token renewed for it too", invalidToken(res), res)
|
|
2160
|
+
// a key isn't renewed: the 401 says the key was refused
|
|
2161
|
+
if (a.source === "key") {
|
|
2162
|
+
const r = keyRefused(site, invalidToken(res), edgeNotes.get(res))
|
|
2163
|
+
ours.add(r)
|
|
2164
|
+
return r
|
|
2165
|
+
}
|
|
2080
2166
|
if (!resendable(b)) return res
|
|
2081
2167
|
await res.body?.cancel().catch(() => {})
|
|
2082
2168
|
const r = await reissued(site, client, getAuth, a, stored.access)
|
|
@@ -2141,6 +2227,9 @@ function makePlugin(site) {
|
|
|
2141
2227
|
// an account lapsed nor cleared it, but for a 401 (gone, below)
|
|
2142
2228
|
async usage(getAuth) {
|
|
2143
2229
|
const auth = await getAuth()
|
|
2230
|
+
// WorkBuddy's own CLI reads no credits with a key, and what the
|
|
2231
|
+
// billing API answers one is unknown, so a key's card says so
|
|
2232
|
+
if (auth?.type === "api") return { error: `credits unknown with an API key (WorkBuddy's own client reads none with one); see ${site.keys.replace(/\/profile\/keys$/, "")}`, signIn: "kept" }
|
|
2144
2233
|
if (auth?.type !== "oauth" || !(auth.access || auth.source)) return { error: "not signed in", signIn: "kept" }
|
|
2145
2234
|
try {
|
|
2146
2235
|
let a = await current(site, client, auth)
|
|
@@ -2206,6 +2295,13 @@ function makePlugin(site) {
|
|
|
2206
2295
|
label: `${site.name} desktop's sign-in`,
|
|
2207
2296
|
authorize: async () => desktopSignIn(site),
|
|
2208
2297
|
},
|
|
2298
|
+
{
|
|
2299
|
+
// a key made at site.keys, sent as WorkBuddy's CLI sends one
|
|
2300
|
+
// (keyAccount)
|
|
2301
|
+
type: "api",
|
|
2302
|
+
label: `${site.name} API key`,
|
|
2303
|
+
placeholder: `from ${site.keys.replace(/^https:\/\//, "")}`,
|
|
2304
|
+
},
|
|
2209
2305
|
],
|
|
2210
2306
|
},
|
|
2211
2307
|
})
|
|
@@ -2313,4 +2409,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
2313
2409
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
2314
2410
|
|
|
2315
2411
|
// for tests
|
|
2316
|
-
export const _internal = { cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, answeredBefore, nativeOf, modelOf, configModels, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
|
|
2412
|
+
export const _internal = { keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, answeredBefore, nativeOf, modelOf, configModels, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-workbuddy-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
4
4
|
"description": "OpenCode provider plugins for WorkBuddy's plans: WorkBuddy (CodeBuddy, copilot.tencent.com) and WorkBuddy AI (www.workbuddy.ai)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|