@magpie-community/opencode-workbuddy-auth 0.1.5 → 0.1.7
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 +10 -0
- package/index.mjs +100 -17
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -33,6 +33,16 @@ marker, `{"type": "oauth", "source": "desktop", "accountId", "uid"}`. OpenCode k
|
|
|
33
33
|
refreshed a minute before it ends, and the new one is saved. If the refresh
|
|
34
34
|
fails, the old token is used while it lasts.
|
|
35
35
|
|
|
36
|
+
In magpie (0.1.684 or later) the plugin also gives `auth.refresh` with
|
|
37
|
+
`refreshLead` of 5 minutes: magpie renews a browser sign-in (an hour's
|
|
38
|
+
token) 5 minutes before it ends, once for the account, before its
|
|
39
|
+
requests, models and usage ask, and saves what it gives. It goes through
|
|
40
|
+
the same one-refresh-at-a-time as a request's refresh, so a refresh token
|
|
41
|
+
is never spent twice. A failed renewal, a refresh token past its end
|
|
42
|
+
included, doesn't mark the account, as the built-in never did; magpie
|
|
43
|
+
tries again later, and the old token is used meanwhile.
|
|
44
|
+
OpenCode doesn't call `auth.refresh`; the check before each request stays.
|
|
45
|
+
|
|
36
46
|
Desktop's sign-in is refreshed only when the app hasn't refreshed it
|
|
37
47
|
itself, and the new token is kept in memory, never written back, as magpie's
|
|
38
48
|
built-in WorkBuddy does.
|
package/index.mjs
CHANGED
|
@@ -17,7 +17,9 @@ const APP_VERSION = "2.0.0" // the version WorkBuddy's sign-in page is opened wi
|
|
|
17
17
|
const UA_VERSION = "5.5.6" // WorkBuddy/<this> is the User-Agent the API takes
|
|
18
18
|
const POLL_MS = 1000
|
|
19
19
|
const SIGN_IN_MS = 5 * 60 * 1000
|
|
20
|
-
const EARLY_MS = 60 * 1000 // a token this close to its end is refreshed
|
|
20
|
+
const EARLY_MS = 60 * 1000 // a token this close to its end is refreshed before a request
|
|
21
|
+
const LEAD_MS = 5 * 60 * 1000 // and this close, magpie renews it ahead of time (auth.refresh)
|
|
22
|
+
const KEEP_RENEWED_MS = 60 * 1000 // how long a refresh's result answers for the old refresh token
|
|
21
23
|
|
|
22
24
|
// [id, name, context, efforts]
|
|
23
25
|
const CN_MODELS = [
|
|
@@ -146,27 +148,66 @@ function stale(a) {
|
|
|
146
148
|
return !!a.expires && Date.now() >= a.expires - EARLY_MS
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
// renewing is each refresh under way, by site and the refresh token it
|
|
152
|
+
// spends; renewed is what each gave, kept a minute so one that read the
|
|
153
|
+
// old refresh token after it was spent (magpie's auth.refresh, a request)
|
|
154
|
+
// takes that rather than spending it again.
|
|
155
|
+
const renewing = new Map()
|
|
156
|
+
const renewed = new Map()
|
|
157
|
+
|
|
158
|
+
function refreshable(a) {
|
|
159
|
+
return !!a.refresh && !(a.refreshExpiresAt && Date.now() >= a.refreshExpiresAt)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// renew spends a's refresh token, once: those who come while it runs, or
|
|
163
|
+
// within KEEP_RENEWED_MS of it, get what it gave. save keeps the result.
|
|
164
|
+
function renew(site, a, save) {
|
|
165
|
+
const key = site.id + "\n" + a.refresh
|
|
166
|
+
const done = renewed.get(key)
|
|
167
|
+
if (done && Date.now() - done.at < KEEP_RENEWED_MS) return Promise.resolve(merged(a, done.next))
|
|
168
|
+
let r = renewing.get(key)
|
|
169
|
+
if (!r) {
|
|
170
|
+
r = (async () => {
|
|
171
|
+
const got = await call(site, "POST", "/v2/plugin/auth/token/refresh", {
|
|
172
|
+
"X-Refresh-Token": a.refresh,
|
|
173
|
+
"X-Auth-Refresh-Source": "plugin",
|
|
174
|
+
"X-Domain": domainOf(site, a),
|
|
175
|
+
}, {})
|
|
176
|
+
if (!got?.accessToken) throw Object.assign(new Error("WorkBuddy gave no refreshed token"), { bare: true })
|
|
177
|
+
const next = merge(a, got)
|
|
178
|
+
renewed.set(key, { next, at: Date.now() })
|
|
179
|
+
await save?.(next)
|
|
180
|
+
return next
|
|
181
|
+
})().finally(() => renewing.delete(key))
|
|
182
|
+
renewing.set(key, r)
|
|
183
|
+
}
|
|
184
|
+
return r.then((next) => merged(a, next))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// merged is a with the tokens another's refresh gave over it.
|
|
188
|
+
function merged(a, next) {
|
|
189
|
+
const out = { ...a }
|
|
190
|
+
for (const k of TOKEN_FIELDS) if (k in next) out[k] = next[k]
|
|
191
|
+
return out
|
|
192
|
+
}
|
|
193
|
+
const TOKEN_FIELDS = ["access", "refresh", "expires", "refreshExpiresAt", "domain", "tokenType"]
|
|
194
|
+
|
|
149
195
|
// fresh is the sign-in with an access token that isn't about to end:
|
|
150
196
|
// refreshed (and saved) when it is. A refresh that fails, or a refresh
|
|
151
197
|
// token that has ended, leaves the access token there is. Its failures
|
|
152
198
|
// are worded as the built-in's (wbFresh), "WorkBuddy" on both sites.
|
|
153
199
|
async function fresh(site, client, a) {
|
|
154
200
|
if (!stale(a)) return a
|
|
155
|
-
|
|
156
|
-
if (!refreshable) {
|
|
201
|
+
if (!refreshable(a)) {
|
|
157
202
|
if (a.access) return a
|
|
158
203
|
throw new Error("this WorkBuddy account is signed out; sign in again")
|
|
159
204
|
}
|
|
160
205
|
try {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
if (!got?.accessToken) throw Object.assign(new Error("WorkBuddy gave no refreshed token"), { bare: true })
|
|
167
|
-
const next = merge(a, got)
|
|
168
|
-
await client?.auth?.set?.({ path: { id: site.id }, body: next })
|
|
169
|
-
return next
|
|
206
|
+
return await renew(site, a, async (next) => {
|
|
207
|
+
try {
|
|
208
|
+
await client?.auth?.set?.({ path: { id: site.id }, body: next })
|
|
209
|
+
} catch {}
|
|
210
|
+
})
|
|
170
211
|
} catch (e) {
|
|
171
212
|
if (a.access) return a
|
|
172
213
|
if (e?.bare) throw e
|
|
@@ -174,6 +215,26 @@ async function fresh(site, client, a) {
|
|
|
174
215
|
}
|
|
175
216
|
}
|
|
176
217
|
|
|
218
|
+
// refreshed is what magpie's auth.refresh gives for a browser sign-in:
|
|
219
|
+
// the fields a refresh changed, nothing when there's nothing to renew.
|
|
220
|
+
// magpie saves them itself.
|
|
221
|
+
async function refreshed(site, auth) {
|
|
222
|
+
if (auth?.type !== "oauth" || auth.source === "desktop" || !auth.access || !auth.refresh) return undefined
|
|
223
|
+
// a refresh token that has ended is said plainly, never as signIn
|
|
224
|
+
// "expired": the built-in never marked a WorkBuddy account
|
|
225
|
+
if (!refreshable(auth)) throw new Error("this WorkBuddy account is signed out; sign in again")
|
|
226
|
+
let next
|
|
227
|
+
try {
|
|
228
|
+
next = await renew(site, auth)
|
|
229
|
+
} catch (e) {
|
|
230
|
+
if (e?.bare) throw e
|
|
231
|
+
throw new Error(`WorkBuddy token refresh: ${e?.said ?? e?.message ?? e}`)
|
|
232
|
+
}
|
|
233
|
+
const out = {}
|
|
234
|
+
for (const k of TOKEN_FIELDS) if (k in next && next[k] !== auth[k]) out[k] = next[k]
|
|
235
|
+
return Object.keys(out).length ? out : undefined
|
|
236
|
+
}
|
|
237
|
+
|
|
177
238
|
// sign sets on headers what WorkBuddy's desktop app sends with a chat.
|
|
178
239
|
function sign(site, a, headers) {
|
|
179
240
|
headers.set("Authorization", "Bearer " + a.access)
|
|
@@ -465,18 +526,29 @@ function modelOf(site, provider, m) {
|
|
|
465
526
|
variants: variants(m.efforts),
|
|
466
527
|
// magpie's own: served at no cost to the plan's credits
|
|
467
528
|
free: !!m.free,
|
|
529
|
+
// magpie's own: the credits a request costs, as a multiple (0: none)
|
|
530
|
+
rate: m.rate ?? 0,
|
|
468
531
|
}
|
|
469
532
|
}
|
|
470
533
|
|
|
471
534
|
// liveModels asks WorkBuddy's product config for the plan's models: the
|
|
472
535
|
// "cli" agent's list, each with its details. The CLI User-Agent picks
|
|
473
536
|
// WorkBuddy's config (a bare WorkBuddy/<v> gets CodeBuddy IDE's, no cli).
|
|
537
|
+
// creditsOf reads a model's credits, the multiple WorkBuddy's picker shows
|
|
538
|
+
// by it: "x0.03", "0" or 0; "x0.00" is free. A rate it can't read (or
|
|
539
|
+
// none, or an infinite one) is undefined: the model is neither free nor
|
|
540
|
+
// rated. The same rule as magpie's built-in (wbCredits).
|
|
541
|
+
function creditsOf(c) {
|
|
542
|
+
if (c == null || c === "") return undefined
|
|
543
|
+
const s = String(c).trim().toLowerCase().replace(/^x/, "")
|
|
544
|
+
const n = Number(s)
|
|
545
|
+
return s !== "" && Number.isFinite(n) && n >= 0 ? n : undefined
|
|
546
|
+
}
|
|
547
|
+
|
|
474
548
|
// freeCredits says whether a model's credits are none: "x0.00", "0" or 0.
|
|
475
549
|
// A rate it can't read is not free.
|
|
476
550
|
function freeCredits(c) {
|
|
477
|
-
|
|
478
|
-
const s = String(c).trim().toLowerCase().replace(/^x/, "")
|
|
479
|
-
return s !== "" && Number.isFinite(Number(s)) && Number(s) === 0
|
|
551
|
+
return creditsOf(c) === 0
|
|
480
552
|
}
|
|
481
553
|
|
|
482
554
|
async function liveModels(site, a) {
|
|
@@ -500,7 +572,11 @@ async function liveModels(site, a) {
|
|
|
500
572
|
m.context = d.maxInputTokens
|
|
501
573
|
m.output = d.maxOutputTokens
|
|
502
574
|
m.images = d.supportsImages === true
|
|
503
|
-
|
|
575
|
+
const r = creditsOf(d.credits)
|
|
576
|
+
if (r !== undefined) {
|
|
577
|
+
m.free = r === 0
|
|
578
|
+
m.rate = r
|
|
579
|
+
}
|
|
504
580
|
const es = d.reasoning?.supportedEfforts ?? []
|
|
505
581
|
if (es.length) {
|
|
506
582
|
const canOff = d.reasoning?.canDisableThinking
|
|
@@ -556,6 +632,13 @@ function makePlugin(site) {
|
|
|
556
632
|
},
|
|
557
633
|
auth: {
|
|
558
634
|
provider: site.id,
|
|
635
|
+
// magpie renews a browser sign-in LEAD_MS before its end (an hour's
|
|
636
|
+
// token), once for the account, before its requests, models and
|
|
637
|
+
// usage ask; the check before each request stays for OpenCode,
|
|
638
|
+
// which doesn't call this. Desktop's sign-in has no expiry stored
|
|
639
|
+
// and is renewed as before.
|
|
640
|
+
refreshLead: LEAD_MS,
|
|
641
|
+
refresh: (auth) => refreshed(site, auth),
|
|
559
642
|
async loader(getAuth) {
|
|
560
643
|
const auth = await getAuth()
|
|
561
644
|
if (auth?.type !== "oauth") return {}
|
|
@@ -661,4 +744,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
661
744
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
662
745
|
|
|
663
746
|
// for tests
|
|
664
|
-
export const _internal = { withSystem, usageOf, desktopHeld, explained, REFUSED_HINT, freeCredits, fresh, SITES }
|
|
747
|
+
export const _internal = { withSystem, usageOf, desktopHeld, explained, REFUSED_HINT, freeCredits, creditsOf, fresh, SITES, renewing, renewed }
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-workbuddy-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
|
7
7
|
"exports": "./index.mjs",
|
|
8
8
|
"files": ["index.mjs", "README.md"],
|
|
9
9
|
"keywords": ["opencode", "opencode-plugin", "workbuddy", "codebuddy"],
|
|
10
|
-
"license": "MIT"
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": { "type": "git", "url": "git+https://github.com/magpie-community/plugins.git", "directory": "packages/workbuddy" }
|
|
11
12
|
}
|