@magpie-community/opencode-workbuddy-auth 0.1.3 → 0.1.5
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 +57 -18
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -86,10 +86,11 @@ const api = (site) => site.endpoint + "/v2"
|
|
|
86
86
|
// ---- WorkBuddy's API ------------------------------------------------------
|
|
87
87
|
|
|
88
88
|
class WBError extends Error {
|
|
89
|
-
constructor(message, code, status) {
|
|
89
|
+
constructor(message, code, status, said) {
|
|
90
90
|
super(message)
|
|
91
91
|
this.code = code
|
|
92
92
|
this.status = status
|
|
93
|
+
this.said = said // as magpie's built-in (wbCall) worded it
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
|
|
@@ -112,11 +113,13 @@ async function call(site, method, path, headers = {}, body) {
|
|
|
112
113
|
try {
|
|
113
114
|
env = JSON.parse(text)
|
|
114
115
|
} catch {}
|
|
116
|
+
const code = Number.parseInt(env.code, 10) || 0
|
|
115
117
|
if (!res.ok) {
|
|
116
118
|
const why = env.msg || env.message || env.error?.message || text.slice(0, 200) || res.statusText
|
|
117
|
-
|
|
119
|
+
const said = code ? env.msg || `error ${code}` : env.msg || statusText(res.status)
|
|
120
|
+
throw new WBError(`${site.name}: ${why} (HTTP ${res.status}${env.code ? ", code " + env.code : ""})`, env.code, res.status, said)
|
|
118
121
|
}
|
|
119
|
-
if (env.code) throw new WBError(`${site.name}: ${env.msg || "error"} (${env.code})`, env.code, res.status)
|
|
122
|
+
if (env.code) throw new WBError(`${site.name}: ${env.msg || "error"} (${env.code})`, env.code, res.status, env.msg || `error ${code}`)
|
|
120
123
|
return env.data
|
|
121
124
|
}
|
|
122
125
|
|
|
@@ -145,13 +148,14 @@ function stale(a) {
|
|
|
145
148
|
|
|
146
149
|
// fresh is the sign-in with an access token that isn't about to end:
|
|
147
150
|
// refreshed (and saved) when it is. A refresh that fails, or a refresh
|
|
148
|
-
// token that has ended, leaves the access token there is.
|
|
151
|
+
// token that has ended, leaves the access token there is. Its failures
|
|
152
|
+
// are worded as the built-in's (wbFresh), "WorkBuddy" on both sites.
|
|
149
153
|
async function fresh(site, client, a) {
|
|
150
154
|
if (!stale(a)) return a
|
|
151
155
|
const refreshable = a.refresh && !(a.refreshExpiresAt && Date.now() >= a.refreshExpiresAt)
|
|
152
156
|
if (!refreshable) {
|
|
153
157
|
if (a.access) return a
|
|
154
|
-
throw new Error(
|
|
158
|
+
throw new Error("this WorkBuddy account is signed out; sign in again")
|
|
155
159
|
}
|
|
156
160
|
try {
|
|
157
161
|
const got = await call(site, "POST", "/v2/plugin/auth/token/refresh", {
|
|
@@ -159,13 +163,14 @@ async function fresh(site, client, a) {
|
|
|
159
163
|
"X-Auth-Refresh-Source": "plugin",
|
|
160
164
|
"X-Domain": domainOf(site, a),
|
|
161
165
|
}, {})
|
|
162
|
-
if (!got?.accessToken) throw new Error(
|
|
166
|
+
if (!got?.accessToken) throw Object.assign(new Error("WorkBuddy gave no refreshed token"), { bare: true })
|
|
163
167
|
const next = merge(a, got)
|
|
164
168
|
await client?.auth?.set?.({ path: { id: site.id }, body: next })
|
|
165
169
|
return next
|
|
166
170
|
} catch (e) {
|
|
167
171
|
if (a.access) return a
|
|
168
|
-
|
|
172
|
+
if (e?.bare) throw e
|
|
173
|
+
throw new Error(`WorkBuddy token refresh: ${e?.said ?? e?.message ?? e}`)
|
|
169
174
|
}
|
|
170
175
|
}
|
|
171
176
|
|
|
@@ -194,6 +199,8 @@ function sign(site, a, headers) {
|
|
|
194
199
|
// withSystem gives a chat that doesn't open with a system message
|
|
195
200
|
// WorkBuddy's default one, as its app does.
|
|
196
201
|
function withSystem(body) {
|
|
202
|
+
// a body magpie or OpenCode hands over as bytes is read as the text it is
|
|
203
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) body = new TextDecoder().decode(body)
|
|
197
204
|
if (typeof body !== "string") return body
|
|
198
205
|
try {
|
|
199
206
|
const b = JSON.parse(body)
|
|
@@ -344,10 +351,27 @@ async function current(site, client, auth) {
|
|
|
344
351
|
|
|
345
352
|
// ---- allowance ----------------------------------------------------------------
|
|
346
353
|
|
|
347
|
-
// STATUS_TEXT is Go's http.StatusText
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
354
|
+
// STATUS_TEXT is Go's http.StatusText, which magpie's built-in said a
|
|
355
|
+
// refused meter call with; a status Go has no words for is "HTTP <n>", so
|
|
356
|
+
// the card always has an error.
|
|
357
|
+
const STATUS_TEXT = {
|
|
358
|
+
100: "Continue", 101: "Switching Protocols", 102: "Processing", 103: "Early Hints",
|
|
359
|
+
200: "OK", 201: "Created", 202: "Accepted", 203: "Non-Authoritative Information", 204: "No Content", 205: "Reset Content",
|
|
360
|
+
206: "Partial Content", 207: "Multi-Status", 208: "Already Reported", 226: "IM Used",
|
|
361
|
+
300: "Multiple Choices", 301: "Moved Permanently", 302: "Found", 303: "See Other", 304: "Not Modified", 305: "Use Proxy",
|
|
362
|
+
307: "Temporary Redirect", 308: "Permanent Redirect",
|
|
363
|
+
400: "Bad Request", 401: "Unauthorized", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed",
|
|
364
|
+
406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone",
|
|
365
|
+
411: "Length Required", 412: "Precondition Failed", 413: "Request Entity Too Large", 414: "Request URI Too Long",
|
|
366
|
+
415: "Unsupported Media Type", 416: "Requested Range Not Satisfiable", 417: "Expectation Failed", 418: "I'm a teapot",
|
|
367
|
+
421: "Misdirected Request", 422: "Unprocessable Entity", 423: "Locked", 424: "Failed Dependency", 425: "Too Early",
|
|
368
|
+
426: "Upgrade Required", 428: "Precondition Required", 429: "Too Many Requests", 431: "Request Header Fields Too Large",
|
|
369
|
+
451: "Unavailable For Legal Reasons",
|
|
370
|
+
500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout",
|
|
371
|
+
505: "HTTP Version Not Supported", 506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected",
|
|
372
|
+
510: "Not Extended", 511: "Network Authentication Required",
|
|
373
|
+
}
|
|
374
|
+
const statusText = (n) => STATUS_TEXT[n] || `HTTP ${n}`
|
|
351
375
|
|
|
352
376
|
// meter asks the billing API as magpie's built-in did, its errors said
|
|
353
377
|
// the same: WorkBuddy's message, else its code, else the HTTP status.
|
|
@@ -368,7 +392,7 @@ async function meter(site, a, path) {
|
|
|
368
392
|
if (!res.ok) {
|
|
369
393
|
if (code) throw new Error(env.msg || `error ${code}`)
|
|
370
394
|
if (env.msg) throw new Error(env.msg)
|
|
371
|
-
throw new Error(
|
|
395
|
+
throw new Error(statusText(res.status))
|
|
372
396
|
}
|
|
373
397
|
if (code) throw new Error(env.msg || `error ${code}`)
|
|
374
398
|
return env.data ?? null
|
|
@@ -551,20 +575,22 @@ function makePlugin(site) {
|
|
|
551
575
|
let body = init?.body
|
|
552
576
|
if (body === undefined && req) body = await req.clone().text()
|
|
553
577
|
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
|
|
578
|
+
return res.status >= 400 ? explained(res) : kept(res)
|
|
555
579
|
},
|
|
556
580
|
}
|
|
557
581
|
},
|
|
558
|
-
// the account's credits, as magpie's built-in showed them
|
|
582
|
+
// the account's credits, as magpie's built-in showed them; each
|
|
583
|
+
// read keeps the sign-in (signIn), as the built-in's neither marked
|
|
584
|
+
// an account lapsed nor cleared it
|
|
559
585
|
async usage(getAuth) {
|
|
560
586
|
const auth = await getAuth()
|
|
561
|
-
if (auth?.type !== "oauth" || !(auth.access || auth.source)) return { error: "not signed in" }
|
|
587
|
+
if (auth?.type !== "oauth" || !(auth.access || auth.source)) return { error: "not signed in", signIn: "kept" }
|
|
562
588
|
try {
|
|
563
589
|
const a = await current(site, client, auth)
|
|
564
590
|
// the meter has no /v2 prefix, on either site
|
|
565
|
-
return usageOf(await meter(site, a, "/billing/meter/get-user-resource-summary"), auth.plan)
|
|
591
|
+
return { ...usageOf(await meter(site, a, "/billing/meter/get-user-resource-summary"), auth.plan), signIn: "kept" }
|
|
566
592
|
} catch (e) {
|
|
567
|
-
return { error: e?.message ?? String(e) }
|
|
593
|
+
return { error: e?.message ?? String(e), signIn: "kept" }
|
|
568
594
|
}
|
|
569
595
|
},
|
|
570
596
|
methods: [
|
|
@@ -592,13 +618,24 @@ const REFUSED_HINT =
|
|
|
592
618
|
"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
619
|
const REFUSED = /unapproved channel|illegal api invocation/i
|
|
594
620
|
|
|
621
|
+
// kept is an answer that went through, saying the sign-in is kept: the
|
|
622
|
+
// built-in never cleared a WorkBuddy account's mark, as it never set one.
|
|
623
|
+
function kept(res) {
|
|
624
|
+
const headers = new Headers(res.headers)
|
|
625
|
+
headers.set("X-Magpie-Sign-In", "kept")
|
|
626
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
627
|
+
}
|
|
628
|
+
|
|
595
629
|
// explained is res with REFUSED_HINT added to that refusal's message.
|
|
630
|
+
// Each refusal says the sign-in is kept: the built-in passed WorkBuddy's
|
|
631
|
+
// 401 on and never marked a WorkBuddy account lapsed.
|
|
596
632
|
async function explained(res) {
|
|
597
633
|
const text = await res.text()
|
|
598
634
|
const again = (b) => {
|
|
599
635
|
const headers = new Headers(res.headers)
|
|
600
636
|
headers.delete("content-length")
|
|
601
637
|
headers.delete("content-encoding")
|
|
638
|
+
headers.set("X-Magpie-Sign-In", "kept")
|
|
602
639
|
return new Response(b, { status: res.status, statusText: res.statusText, headers })
|
|
603
640
|
}
|
|
604
641
|
if (!REFUSED.test(text)) return again(text)
|
|
@@ -610,11 +647,13 @@ async function explained(res) {
|
|
|
610
647
|
const at = v?.error && typeof v.error === "object" ? v.error : v && typeof v === "object" ? v : null
|
|
611
648
|
if (at && typeof at.message === "string") msg = at.message
|
|
612
649
|
else if (typeof v?.error === "string") msg = v.error
|
|
650
|
+
else if (typeof v?.msg === "string" && v.msg) msg = v.msg // WorkBuddy's own {"code", "msg"}
|
|
613
651
|
msg = `${msg} — ${REFUSED_HINT}`
|
|
614
652
|
const headers = new Headers(res.headers)
|
|
615
653
|
headers.set("content-type", "application/json")
|
|
616
654
|
headers.delete("content-length")
|
|
617
655
|
headers.delete("content-encoding")
|
|
656
|
+
headers.set("X-Magpie-Sign-In", "kept")
|
|
618
657
|
return new Response(JSON.stringify({ error: { message: msg, type: "permission_error", code: null } }), { status: res.status, statusText: res.statusText, headers })
|
|
619
658
|
}
|
|
620
659
|
|
|
@@ -622,4 +661,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
622
661
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
623
662
|
|
|
624
663
|
// for tests
|
|
625
|
-
export const _internal = { usageOf, desktopHeld, explained, REFUSED_HINT, freeCredits }
|
|
664
|
+
export const _internal = { withSystem, usageOf, desktopHeld, explained, REFUSED_HINT, freeCredits, fresh, SITES }
|
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.5",
|
|
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",
|