@magpie-community/opencode-workbuddy-auth 0.1.1 → 0.1.3
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/index.mjs +126 -5
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -243,7 +243,7 @@ async function browserSignIn(site) {
|
|
|
243
243
|
const deadline = Date.now() + SIGN_IN_MS
|
|
244
244
|
const q = encodeURIComponent(state.state)
|
|
245
245
|
const token = await poll(site, `/v2/plugin/auth/token?state=${q}`, {}, [11217], deadline)
|
|
246
|
-
if (!token.accessToken) return { type: "failed" }
|
|
246
|
+
if (!token.accessToken) return { type: "failed", error: `${site.name} sent back no token` }
|
|
247
247
|
let a = merge({}, token)
|
|
248
248
|
const who = await poll(site, `/v2/plugin/login/account?state=${q}`, {
|
|
249
249
|
Authorization: "Bearer " + a.access,
|
|
@@ -253,8 +253,8 @@ async function browserSignIn(site) {
|
|
|
253
253
|
}, [12151], deadline)
|
|
254
254
|
a.uid = who.uid ?? ""
|
|
255
255
|
return success(site, a, who.nickname || who.phoneNumber || who.uid)
|
|
256
|
-
} catch {
|
|
257
|
-
return { type: "failed" }
|
|
256
|
+
} catch (e) {
|
|
257
|
+
return { type: "failed", error: e.message }
|
|
258
258
|
}
|
|
259
259
|
},
|
|
260
260
|
}
|
|
@@ -319,7 +319,7 @@ function desktopSignIn(site) {
|
|
|
319
319
|
method: "auto",
|
|
320
320
|
async callback() {
|
|
321
321
|
const d = readDesktop(site)
|
|
322
|
-
if (!d) return { type: "failed" }
|
|
322
|
+
if (!d) return { type: "failed", error: `${site.name} desktop isn't signed in` }
|
|
323
323
|
return { type: "success", refresh: "", access: "", expires: 0, source: "desktop", accountId: d.name || site.name, uid: d.uid }
|
|
324
324
|
},
|
|
325
325
|
}
|
|
@@ -342,6 +342,65 @@ async function current(site, client, auth) {
|
|
|
342
342
|
return a
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
// ---- allowance ----------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
// STATUS_TEXT is Go's http.StatusText for the answers WorkBuddy gives.
|
|
348
|
+
const STATUS_TEXT = { 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed",
|
|
349
|
+
408: "Request Timeout", 429: "Too Many Requests", 500: "Internal Server Error", 502: "Bad Gateway", 503: "Service Unavailable",
|
|
350
|
+
504: "Gateway Timeout" }
|
|
351
|
+
|
|
352
|
+
// meter asks the billing API as magpie's built-in did, its errors said
|
|
353
|
+
// the same: WorkBuddy's message, else its code, else the HTTP status.
|
|
354
|
+
async function meter(site, a, path) {
|
|
355
|
+
const res = await fetch(site.endpoint + path, {
|
|
356
|
+
method: "POST",
|
|
357
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "WorkBuddy/" + UA_VERSION,
|
|
358
|
+
Authorization: "Bearer " + a.access, "X-User-Id": a.uid ?? "", "X-Domain": domainOf(site, a), "X-Product": "SaaS",
|
|
359
|
+
"X-IDE-Type": "WorkBuddy" },
|
|
360
|
+
body: "{}",
|
|
361
|
+
signal: AbortSignal.timeout(20000),
|
|
362
|
+
})
|
|
363
|
+
let env = {}
|
|
364
|
+
try {
|
|
365
|
+
env = JSON.parse(await res.text()) ?? {}
|
|
366
|
+
} catch {}
|
|
367
|
+
const code = Number.parseInt(env.code, 10) || 0
|
|
368
|
+
if (!res.ok) {
|
|
369
|
+
if (code) throw new Error(env.msg || `error ${code}`)
|
|
370
|
+
if (env.msg) throw new Error(env.msg)
|
|
371
|
+
throw new Error(STATUS_TEXT[res.status] ?? "")
|
|
372
|
+
}
|
|
373
|
+
if (code) throw new Error(env.msg || `error ${code}`)
|
|
374
|
+
return env.data ?? null
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// capacity is a capacity the billing API sends, as a string ("438.88")
|
|
378
|
+
// or now and then a number; empty is 0.
|
|
379
|
+
function capacity(v) {
|
|
380
|
+
if (v == null || v === "") return 0
|
|
381
|
+
if (typeof v === "number") return v
|
|
382
|
+
const s = String(v)
|
|
383
|
+
const n = Number(s)
|
|
384
|
+
if (s.trim() !== s || Number.isNaN(n)) throw new Error(`strconv.ParseFloat: parsing ${JSON.stringify(s)}: invalid syntax`)
|
|
385
|
+
return n
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// compact is a count as magpie says one: whole, else to two places.
|
|
389
|
+
const compact = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/0+$/, "").replace(/\.$/, ""))
|
|
390
|
+
|
|
391
|
+
// usageOf is the account's credits, from its resource summary: the plan's
|
|
392
|
+
// credits used against what the cycle grants, as magpie's built-in said.
|
|
393
|
+
function usageOf(sum, plan) {
|
|
394
|
+
const out = { plan: plan || (sum?.IsPaidUser ? "Pro" : "Free"), windows: [] }
|
|
395
|
+
let total = 0, used = 0
|
|
396
|
+
for (const p of sum?.Packages ?? []) {
|
|
397
|
+
total += capacity(p?.CycleTotalCapacity)
|
|
398
|
+
used += capacity(p?.CycleUsedCapacity)
|
|
399
|
+
}
|
|
400
|
+
if (total > 0) out.windows.push({ name: "Credits", used: (100 * used) / total, display: `${compact(used)} / ${compact(total)}` })
|
|
401
|
+
return out
|
|
402
|
+
}
|
|
403
|
+
|
|
345
404
|
// ---- models -----------------------------------------------------------------
|
|
346
405
|
|
|
347
406
|
// variants are the reasoning levels as OpenCode gives them to the AI SDK.
|
|
@@ -380,12 +439,22 @@ function modelOf(site, provider, m) {
|
|
|
380
439
|
},
|
|
381
440
|
release_date: "",
|
|
382
441
|
variants: variants(m.efforts),
|
|
442
|
+
// magpie's own: served at no cost to the plan's credits
|
|
443
|
+
free: !!m.free,
|
|
383
444
|
}
|
|
384
445
|
}
|
|
385
446
|
|
|
386
447
|
// liveModels asks WorkBuddy's product config for the plan's models: the
|
|
387
448
|
// "cli" agent's list, each with its details. The CLI User-Agent picks
|
|
388
449
|
// WorkBuddy's config (a bare WorkBuddy/<v> gets CodeBuddy IDE's, no cli).
|
|
450
|
+
// freeCredits says whether a model's credits are none: "x0.00", "0" or 0.
|
|
451
|
+
// A rate it can't read is not free.
|
|
452
|
+
function freeCredits(c) {
|
|
453
|
+
if (c == null || c === "") return false
|
|
454
|
+
const s = String(c).trim().toLowerCase().replace(/^x/, "")
|
|
455
|
+
return s !== "" && Number.isFinite(Number(s)) && Number(s) === 0
|
|
456
|
+
}
|
|
457
|
+
|
|
389
458
|
async function liveModels(site, a) {
|
|
390
459
|
const headers = new Headers()
|
|
391
460
|
sign(site, a, headers)
|
|
@@ -407,6 +476,7 @@ async function liveModels(site, a) {
|
|
|
407
476
|
m.context = d.maxInputTokens
|
|
408
477
|
m.output = d.maxOutputTokens
|
|
409
478
|
m.images = d.supportsImages === true
|
|
479
|
+
m.free = freeCredits(d.credits)
|
|
410
480
|
const es = d.reasoning?.supportedEfforts ?? []
|
|
411
481
|
if (es.length) {
|
|
412
482
|
const canOff = d.reasoning?.canDisableThinking
|
|
@@ -480,10 +550,23 @@ function makePlugin(site) {
|
|
|
480
550
|
sign(site, a, headers)
|
|
481
551
|
let body = init?.body
|
|
482
552
|
if (body === undefined && req) body = await req.clone().text()
|
|
483
|
-
|
|
553
|
+
const res = await fetch(req ? req.url : input, { ...init, method: init?.method ?? req?.method, headers, body: withSystem(body) })
|
|
554
|
+
return res.status >= 400 ? explained(res) : res
|
|
484
555
|
},
|
|
485
556
|
}
|
|
486
557
|
},
|
|
558
|
+
// the account's credits, as magpie's built-in showed them
|
|
559
|
+
async usage(getAuth) {
|
|
560
|
+
const auth = await getAuth()
|
|
561
|
+
if (auth?.type !== "oauth" || !(auth.access || auth.source)) return { error: "not signed in" }
|
|
562
|
+
try {
|
|
563
|
+
const a = await current(site, client, auth)
|
|
564
|
+
// the meter has no /v2 prefix, on either site
|
|
565
|
+
return usageOf(await meter(site, a, "/billing/meter/get-user-resource-summary"), auth.plan)
|
|
566
|
+
} catch (e) {
|
|
567
|
+
return { error: e?.message ?? String(e) }
|
|
568
|
+
}
|
|
569
|
+
},
|
|
487
570
|
methods: [
|
|
488
571
|
{
|
|
489
572
|
type: "oauth",
|
|
@@ -500,5 +583,43 @@ function makePlugin(site) {
|
|
|
500
583
|
})
|
|
501
584
|
}
|
|
502
585
|
|
|
586
|
+
// REFUSED_HINT is what the user can do about WorkBuddy's "Illegal API
|
|
587
|
+
// invocation from an unapproved channel": both builds answer it to a chat
|
|
588
|
+
// whose system prompt is Codex's or Claude Code's own (magpie #182),
|
|
589
|
+
// whatever the headers. It ends the error's message, as magpie's built-in
|
|
590
|
+
// put it (provider.WBRefusedHint), so magpie's routing page says it apart.
|
|
591
|
+
const REFUSED_HINT =
|
|
592
|
+
"WorkBuddy refuses chats from Codex and Claude Code (their system prompt); use it from Hermes, OpenCode or Pi, or add another provider to this group"
|
|
593
|
+
const REFUSED = /unapproved channel|illegal api invocation/i
|
|
594
|
+
|
|
595
|
+
// explained is res with REFUSED_HINT added to that refusal's message.
|
|
596
|
+
async function explained(res) {
|
|
597
|
+
const text = await res.text()
|
|
598
|
+
const again = (b) => {
|
|
599
|
+
const headers = new Headers(res.headers)
|
|
600
|
+
headers.delete("content-length")
|
|
601
|
+
headers.delete("content-encoding")
|
|
602
|
+
return new Response(b, { status: res.status, statusText: res.statusText, headers })
|
|
603
|
+
}
|
|
604
|
+
if (!REFUSED.test(text)) return again(text)
|
|
605
|
+
let msg = text.trim()
|
|
606
|
+
let v
|
|
607
|
+
try {
|
|
608
|
+
v = JSON.parse(text)
|
|
609
|
+
} catch {}
|
|
610
|
+
const at = v?.error && typeof v.error === "object" ? v.error : v && typeof v === "object" ? v : null
|
|
611
|
+
if (at && typeof at.message === "string") msg = at.message
|
|
612
|
+
else if (typeof v?.error === "string") msg = v.error
|
|
613
|
+
msg = `${msg} — ${REFUSED_HINT}`
|
|
614
|
+
const headers = new Headers(res.headers)
|
|
615
|
+
headers.set("content-type", "application/json")
|
|
616
|
+
headers.delete("content-length")
|
|
617
|
+
headers.delete("content-encoding")
|
|
618
|
+
return new Response(JSON.stringify({ error: { message: msg, type: "permission_error", code: null } }), { status: res.status, statusText: res.statusText, headers })
|
|
619
|
+
}
|
|
620
|
+
|
|
503
621
|
export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
504
622
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
623
|
+
|
|
624
|
+
// for tests
|
|
625
|
+
export const _internal = { usageOf, desktopHeld, explained, REFUSED_HINT, freeCredits }
|
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.3",
|
|
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",
|