@magpie-community/opencode-workbuddy-auth 0.1.6 → 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 +81 -13
- 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)
|
|
@@ -571,6 +632,13 @@ function makePlugin(site) {
|
|
|
571
632
|
},
|
|
572
633
|
auth: {
|
|
573
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),
|
|
574
642
|
async loader(getAuth) {
|
|
575
643
|
const auth = await getAuth()
|
|
576
644
|
if (auth?.type !== "oauth") return {}
|
|
@@ -676,4 +744,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
676
744
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
677
745
|
|
|
678
746
|
// for tests
|
|
679
|
-
export const _internal = { withSystem, usageOf, desktopHeld, explained, REFUSED_HINT, freeCredits, creditsOf, 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
|
}
|