@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.
Files changed (3) hide show
  1. package/README.md +10 -0
  2. package/index.mjs +100 -17
  3. 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
- const refreshable = a.refresh && !(a.refreshExpiresAt && Date.now() >= a.refreshExpiresAt)
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
- const got = await call(site, "POST", "/v2/plugin/auth/token/refresh", {
162
- "X-Refresh-Token": a.refresh,
163
- "X-Auth-Refresh-Source": "plugin",
164
- "X-Domain": domainOf(site, a),
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
- if (c == null || c === "") return false
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
- m.free = freeCredits(d.credits)
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.5",
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
  }