@magpie-community/opencode-workbuddy-auth 0.1.36 → 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 +82 -5
- 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
|
|
@@ -2007,6 +2056,17 @@ function makePlugin(site) {
|
|
|
2007
2056
|
// the account's own list, when it's signed in and WorkBuddy answers;
|
|
2008
2057
|
// else the list above
|
|
2009
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
|
+
}
|
|
2010
2070
|
if (auth?.type !== "oauth" || !(auth.access || auth.source)) return provider.models
|
|
2011
2071
|
try {
|
|
2012
2072
|
const a = await current(site, client, auth)
|
|
@@ -2029,7 +2089,7 @@ function makePlugin(site) {
|
|
|
2029
2089
|
refresh: (auth) => refreshed(site, auth),
|
|
2030
2090
|
async loader(getAuth) {
|
|
2031
2091
|
const auth = await getAuth()
|
|
2032
|
-
if (auth?.type !== "oauth") return {}
|
|
2092
|
+
if (auth?.type !== "oauth" && auth?.type !== "api") return {}
|
|
2033
2093
|
return {
|
|
2034
2094
|
// no baseURL: a model's own api.url says where it is served —
|
|
2035
2095
|
// <endpoint>/v2 for a chat, <endpoint>/v1 for a Claude model on
|
|
@@ -2038,8 +2098,9 @@ function makePlugin(site) {
|
|
|
2038
2098
|
apiKey: "",
|
|
2039
2099
|
async fetch(input, init) {
|
|
2040
2100
|
const stored = await getAuth()
|
|
2041
|
-
|
|
2042
|
-
|
|
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))
|
|
2043
2104
|
const req = input instanceof Request ? input : null
|
|
2044
2105
|
const headers = new Headers(init?.headers ?? req?.headers)
|
|
2045
2106
|
headers.delete("authorization")
|
|
@@ -2096,6 +2157,12 @@ function makePlugin(site) {
|
|
|
2096
2157
|
const res = await post(b)
|
|
2097
2158
|
if (res.status !== 401) return res
|
|
2098
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
|
+
}
|
|
2099
2166
|
if (!resendable(b)) return res
|
|
2100
2167
|
await res.body?.cancel().catch(() => {})
|
|
2101
2168
|
const r = await reissued(site, client, getAuth, a, stored.access)
|
|
@@ -2160,6 +2227,9 @@ function makePlugin(site) {
|
|
|
2160
2227
|
// an account lapsed nor cleared it, but for a 401 (gone, below)
|
|
2161
2228
|
async usage(getAuth) {
|
|
2162
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" }
|
|
2163
2233
|
if (auth?.type !== "oauth" || !(auth.access || auth.source)) return { error: "not signed in", signIn: "kept" }
|
|
2164
2234
|
try {
|
|
2165
2235
|
let a = await current(site, client, auth)
|
|
@@ -2225,6 +2295,13 @@ function makePlugin(site) {
|
|
|
2225
2295
|
label: `${site.name} desktop's sign-in`,
|
|
2226
2296
|
authorize: async () => desktopSignIn(site),
|
|
2227
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
|
+
},
|
|
2228
2305
|
],
|
|
2229
2306
|
},
|
|
2230
2307
|
})
|
|
@@ -2332,4 +2409,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
2332
2409
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
2333
2410
|
|
|
2334
2411
|
// for tests
|
|
2335
|
-
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",
|