@magpie-community/opencode-workbuddy-auth 0.1.29 → 0.1.31

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 +21 -2
  2. package/index.mjs +213 -22
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,6 +56,22 @@ Desktop's sign-in is refreshed only when the app hasn't refreshed it
56
56
  itself, and the new token is kept in memory, never written back, as magpie's
57
57
  built-in WorkBuddy does.
58
58
 
59
+ A chat or usage read WorkBuddy answers 401 (since 0.1.31) is renewed once,
60
+ whatever the token's time, and sent once more. A token another process
61
+ saved since is taken first instead of spending the refresh token again. A
62
+ desktop sign-in is never renewed for it: the app's file is read again, and
63
+ its token is used if the app signed in again. A 401 again, or no new
64
+ token, ends in a 401 that says to sign in again (Providers → WorkBuddy →
65
+ this account's Sign in again in magpie; WorkBuddy desktop for a desktop
66
+ sign-in). The plugin marks the account expired in magpie
67
+ (`X-Magpie-Sign-In: expired`, usage's `signIn`) only on WorkBuddy's word
68
+ that the sign-in is gone: the refresh token refused (401, `invalid_grant`),
69
+ or the access token refused (`WWW-Authenticate: … error="invalid_token"`)
70
+ with no refresh token left to renew it with or just after renewing. A
71
+ failed renewal for another reason, or a bare 401 without `invalid_token`,
72
+ leaves the account as it is; a desktop sign-in is never marked. A 401
73
+ after a renewal takes a mark off (`renewed`).
74
+
59
75
  ## Requests
60
76
 
61
77
  Chats are chat completions (`@ai-sdk/openai-compatible`) at `<API>/chat/completions`.
@@ -92,8 +108,11 @@ WorkBuddy's default system prompt in its place: WorkBuddy checks only the
92
108
  system and assistant messages, and the line it matched is then in neither.
93
109
  A system message later in the chat becomes a user message where it stands.
94
110
  The plugin remembers each refused system prompt while it is loaded, so that
95
- agent's next chats go moved at once. A refusal that stands after the move
96
- (words in an assistant message) ends in a hint that names no one client
111
+ agent's next chats go moved at once. The move can't reach an assistant
112
+ message, so words the model quotes back into one keep that chat refused
113
+ until they are in the list reworded in every message (since 0.1.30, the "Web GUI whose URL the runtime
114
+ context names" line, #291); otherwise only a new chat clears it. A refusal
115
+ that stands after the move ends in a hint that names no one client
97
116
  (since 0.1.28): WorkBuddy refused words in this client's prompt; use it from
98
117
  Hermes, OpenCode or Pi, or add another provider to the group. Neither
99
118
  refusal rests the account.
package/index.mjs CHANGED
@@ -467,6 +467,11 @@ const FLAGGED = [
467
467
  // WorkBuddy refuses as 11128 (magpie-community/plugins#279; the same
468
468
  // refusal traced in smart-open/TraeWorkAssistant#125).
469
469
  [/You are an AI agent powered by DeepSeek Harness\.?/gi, "You are an AI agent that helps the user with programming tasks."],
470
+ // A line WorkBuddy refuses in a long DeepSeek Harness chat, measured 400
471
+ // then 200 with this wording by tearslee (magpie-community/plugins#291). The
472
+ // model can quote it back into an assistant message, which the move to
473
+ // the user's instructions can't reach, so it is reworded in every message.
474
+ [/Web GUI whose URL the runtime context names\.\s+When the user refers/gi, "Web GUI whose URL the runtime context names. The interface, when the user refers"],
470
475
  ]
471
476
 
472
477
  function unflagged(text) {
@@ -1118,6 +1123,8 @@ function readDesktop(site) {
1118
1123
  uid: acct.uid,
1119
1124
  ...(typeof acct.enterpriseId === "string" && acct.enterpriseId.trim() ? { enterpriseId: acct.enterpriseId.trim() } : {}),
1120
1125
  name: acct.nickname || acct.phoneNumber || acct.uid,
1126
+ // what the errors below say to do goes by it: sign in to the app again
1127
+ source: "desktop",
1121
1128
  }
1122
1129
  } catch {
1123
1130
  return null
@@ -1232,23 +1239,143 @@ async function unanswered(site, a, body, err, signal) {
1232
1239
  const size = byteLength(body)
1233
1240
  if (size <= EDGE_READS) throw err
1234
1241
  let status = 0
1242
+ let challenge = ""
1235
1243
  try {
1236
1244
  const res = await meterAsked(site, a, "/billing/meter/get-user-resource-summary", {}, AbortSignal.timeout(20000))
1237
1245
  status = res.status
1246
+ challenge = res.headers.get("www-authenticate") ?? ""
1238
1247
  await res.body?.cancel()
1239
1248
  } catch {
1240
1249
  throw err
1241
1250
  }
1242
1251
  if (status !== 401) throw err
1243
1252
  const mb = (size / (1024 * 1024)).toFixed(1)
1253
+ const note = `It closes the connection on a refused request over 2 MiB (this one was ${mb} MiB) instead of answering, which is why it read as ECONNRESET.`
1244
1254
  const message =
1245
- `${site.name} refused this account's sign-in (401). It closes the connection on a refused request over 2 MiB ` +
1246
- `(this one was ${mb} MiB) instead of answering, which is why it read as ECONNRESET. ` +
1255
+ `${site.name} refused this account's sign-in (401). ${note} ` +
1247
1256
  (a.source === "desktop" ? `Sign in to ${site.name} desktop again.` : `Sign in to ${site.name} again in magpie.`)
1257
+ // the billing API's challenge goes with it: it says whether the token
1258
+ // itself was refused (invalidToken), as the chat's own 401 would have
1259
+ const headers = { "content-type": "application/json", ...(challenge ? { "www-authenticate": challenge } : {}) }
1260
+ const res = new Response(JSON.stringify({ error: { message, type: "authentication_error", code: 401 } }), {
1261
+ status: 401,
1262
+ statusText: "Unauthorized",
1263
+ headers,
1264
+ })
1265
+ edgeNotes.set(res, note)
1266
+ return res
1267
+ }
1268
+
1269
+ // edgeNotes is what unanswered's 401s say of the reset, for signedOut to
1270
+ // say again when the sign-in can't be renewed.
1271
+ const edgeNotes = new WeakMap()
1272
+
1273
+ // ---- a refused sign-in ----------------------------------------------------------
1274
+
1275
+ // invalidToken says res is WorkBuddy refusing the access token itself. Its
1276
+ // front door (APISIX, on both sites) answers a token it doesn't take with
1277
+ // 401 and WWW-Authenticate: Bearer realm="copilot", error="invalid_token",
1278
+ // on a chat (streamed or not) and on the billing API alike; measured on
1279
+ // www.workbuddy.ai on 2026-10-23 with a real account's token cut short,
1280
+ // and with "garbage". (Its /v3/config answers 200 to any token, so the
1281
+ // models list never meets a 401.) A 401 without it is not taken as the
1282
+ // sign-in's: WorkBuddy answers some paths with a bare 401 to an accepted
1283
+ // sign-in (magpie's check-in, モモコ on Discord).
1284
+ const invalidToken = (res) => res?.status === 401 && /invalid_token/i.test(res.headers.get("www-authenticate") ?? "")
1285
+
1286
+ // refreshRefused says e is WorkBuddy turning the refresh token itself
1287
+ // away: HTTP 401 with code 12153, "refresh token failed:400 Bad Request:
1288
+ // invalid_grant: Invalid refresh token" for a spent or unknown one, or
1289
+ // "10000:token format error" (measured 2026-10-23). Anything else (a 5xx,
1290
+ // a timeout) says nothing of the sign-in.
1291
+ const refreshRefused = (e) => e instanceof WBError && (e.status === 401 || /invalid_grant/i.test(e.message))
1292
+
1293
+ // reissued is the sign-in to send a request WorkBuddy refused a's access
1294
+ // token for (401) once more with, before that token's time: WorkBuddy can
1295
+ // refuse one long before it ends (a browser sign-in's lasts as long as
1296
+ // its refresh token, about 340 days), and renewing only by time left such
1297
+ // an account refused on every request for months. It is
1298
+ // { a }: a sign-in with another access token, to send the request with
1299
+ // once more;
1300
+ // { why, kind }: none to be had, why, and of what kind: "refresh
1301
+ // refused" (WorkBuddy turned the refresh token away too), "nothing to
1302
+ // renew with" (no refresh token, or one past its end), or "unknown"
1303
+ // (the refresh failed for another reason, the desktop's token is the
1304
+ // one refused).
1305
+ // began is the access token the request read at its start: a stored one
1306
+ // that isn't it was saved since by someone else (magpie's renewal,
1307
+ // another of its processes), and is taken rather than renewing again.
1308
+ //
1309
+ // A desktop sign-in is the app's: its file is read again, and what was
1310
+ // renewed here (desktopHeld) is let go, but its refresh token is never
1311
+ // spent for a refusal, which would sign the app itself out (see
1312
+ // desktopSignIn).
1313
+ async function reissued(site, client, getAuth, a, began) {
1314
+ if (a.source === "desktop") {
1315
+ desktopHeld.delete(site.id + "|" + a.uid)
1316
+ const d = readDesktop(site)
1317
+ if (!d) return { why: notDesktop(site), kind: "unknown" }
1318
+ if (d.access !== a.access) {
1319
+ desktopHeld.set(site.id + "|" + d.uid, d)
1320
+ return { a: d }
1321
+ }
1322
+ return { why: `${site.name} desktop's own sign-in is the one refused`, kind: "unknown" }
1323
+ }
1324
+ const newer = async () => {
1325
+ const now = await getAuth().catch(() => null)
1326
+ return now?.type === "oauth" && now.source !== "desktop" && now.access && now.access !== began && now.access !== a.access ? now : null
1327
+ }
1328
+ const saved = await newer()
1329
+ if (saved) return { a: saved }
1330
+ if (!refreshable(a)) return { why: a.refresh ? "its refresh token has ended" : "there is no refresh token to renew it with", kind: "nothing to renew with" }
1331
+ try {
1332
+ const next = await renew(site, a, async (n) => {
1333
+ try {
1334
+ await client?.auth?.set?.({ path: { id: site.id }, body: n })
1335
+ } catch {}
1336
+ })
1337
+ return next.access !== a.access ? { a: next } : { why: "renewing it gave the same token", kind: "unknown" }
1338
+ } catch (e) {
1339
+ if (refreshRefused(e)) {
1340
+ // spent by someone else, who saved what it gave
1341
+ const saved = await newer()
1342
+ if (saved) return { a: saved }
1343
+ return { why: `it refused the refresh token too (${e.said ?? e.message})`, kind: "refresh refused" }
1344
+ }
1345
+ return { why: `renewing it failed: ${e?.said ?? e?.message ?? e}`, kind: "unknown" }
1346
+ }
1347
+ }
1348
+
1349
+ // gone says whether a sign-in reissued (r) couldn't renew is gone, on
1350
+ // WorkBuddy's word: it turned the refresh token away, or refused the access
1351
+ // token itself (tokenRefused, invalidToken) with none to renew it with. A
1352
+ // desktop sign-in never is: the app's file is read on each request, and
1353
+ // signing in to the app again is all it takes.
1354
+ const gone = (a, r, tokenRefused) =>
1355
+ a.source !== "desktop" && (r.kind === "refresh refused" || (r.kind === "nothing to renew with" && !!tokenRefused))
1356
+
1357
+ // refusedText says WorkBuddy refused a's sign-in, why it wasn't renewed,
1358
+ // and what to do.
1359
+ function refusedText(site, a, why, note) {
1360
+ return (
1361
+ `${site.name} refused this account's sign-in (401)${why ? "; " + why : ""}. ${note ? note + " " : ""}` +
1362
+ (a.source === "desktop"
1363
+ ? `Sign in to ${site.name} desktop again; magpie reads its sign-in on the next request.`
1364
+ : `Sign in to it again in magpie: Providers → ${site.name} → this account's Sign in again.`)
1365
+ )
1366
+ }
1367
+
1368
+ // signedOut is the answer to a request WorkBuddy refused this account's
1369
+ // sign-in for, renewed or not: a 401 that says so and what to do, which
1370
+ // WorkBuddy's own (an HTML page, its {code, msg}) doesn't. expired marks
1371
+ // the account in magpie (X-Magpie-Sign-In), out of routing and "sign in
1372
+ // again" on its row, till it is signed in again; else it is kept.
1373
+ function signedOut(site, a, why, expired, note) {
1374
+ const message = refusedText(site, a, why, note)
1248
1375
  return new Response(JSON.stringify({ error: { message, type: "authentication_error", code: 401 } }), {
1249
1376
  status: 401,
1250
1377
  statusText: "Unauthorized",
1251
- headers: { "content-type": "application/json" },
1378
+ headers: { "content-type": "application/json", "X-Magpie-Sign-In": expired ? "expired" : "kept" },
1252
1379
  })
1253
1380
  }
1254
1381
 
@@ -1262,9 +1389,12 @@ async function meter(site, a, path, body = {}) {
1262
1389
  } catch {}
1263
1390
  const code = Number.parseInt(env.code, 10) || 0
1264
1391
  if (!res.ok) {
1265
- if (code) throw new Error(env.msg || `error ${code}`)
1266
- if (env.msg) throw new Error(env.msg)
1267
- throw new Error(statusText(res.status))
1392
+ // the status and whether the token itself was refused go with it, for
1393
+ // usage's one renewal on a 401 (reissued)
1394
+ const said = { status: res.status, invalidToken: invalidToken(res) }
1395
+ if (code) throw Object.assign(new Error(env.msg || `error ${code}`), said)
1396
+ if (env.msg) throw Object.assign(new Error(env.msg), said)
1397
+ throw Object.assign(new Error(statusText(res.status)), said)
1268
1398
  }
1269
1399
  if (code) throw new Error(env.msg || `error ${code}`)
1270
1400
  return env.data ?? null
@@ -1666,9 +1796,9 @@ function makePlugin(site) {
1666
1796
  baseURL: api(site),
1667
1797
  apiKey: "",
1668
1798
  async fetch(input, init) {
1669
- let a = await getAuth()
1670
- if (a?.type !== "oauth") throw new Error(`${site.name} isn't signed in`)
1671
- a = await current(site, client, a)
1799
+ const stored = await getAuth()
1800
+ if (stored?.type !== "oauth") throw new Error(`${site.name} isn't signed in`)
1801
+ let a = await current(site, client, stored)
1672
1802
  const req = input instanceof Request ? input : null
1673
1803
  const headers = new Headers(init?.headers ?? req?.headers)
1674
1804
  headers.delete("authorization")
@@ -1682,8 +1812,44 @@ function makePlugin(site) {
1682
1812
  const fit = fitted(body)
1683
1813
  body = normalizeToolChoice(fit.body)
1684
1814
  attend(headers, body, session)
1685
- const send = (b) =>
1815
+ const post = (b) =>
1686
1816
  fetch(req ? req.url : input, { ...init, method: init?.method ?? req?.method, headers, body: b }).catch((e) => unanswered(site, a, b, e, init?.signal))
1817
+ // a 401 is answered once, on whichever try meets it: by a
1818
+ // sign-in with another access token (reissued), the request
1819
+ // sent once more with it, never again after that; else, or
1820
+ // when that is refused too, by signedOut. The body is a
1821
+ // string here (or bytes), which goes again as it is; one that
1822
+ // can't be read twice isn't sent again.
1823
+ //
1824
+ // The account is marked (expired) only on WorkBuddy's word
1825
+ // that the sign-in is gone: it turned the refresh token away,
1826
+ // or refused the access token itself (invalidToken) with none
1827
+ // to renew it with, or refused even the token just renewed. A
1828
+ // desktop sign-in is never marked: the app's file is read on
1829
+ // each request, so signing in to the app again is enough.
1830
+ let renewed = false
1831
+ const ours = new WeakSet() // the answers signedOut gave, passed on as they are
1832
+ const out = (why, expired, res) => {
1833
+ const r = signedOut(site, a, why, expired && a.source !== "desktop", edgeNotes.get(res))
1834
+ ours.add(r)
1835
+ return r
1836
+ }
1837
+ const resendable = (b) => b == null || typeof b === "string" || b instanceof ArrayBuffer || ArrayBuffer.isView(b)
1838
+ const send = async (b) => {
1839
+ const res = await post(b)
1840
+ if (res.status !== 401) return res
1841
+ if (renewed) return out("it refused the token renewed for it too", invalidToken(res), res)
1842
+ if (!resendable(b)) return res
1843
+ await res.body?.cancel().catch(() => {})
1844
+ const r = await reissued(site, client, getAuth, a, stored.access)
1845
+ if (!r.a) return out(r.why, gone(a, r, invalidToken(res)), res)
1846
+ renewed = true
1847
+ a = r.a
1848
+ sign(site, a, headers)
1849
+ const again = await post(b)
1850
+ if (again.status !== 401) return again
1851
+ return out("it refused the token renewed for it too", invalidToken(again), again)
1852
+ }
1687
1853
  // a model known to turn tool results then the user's message away
1688
1854
  // gets the empty reply between them up front (see endsAfterTools)
1689
1855
  const model = chatOf(body)?.model
@@ -1711,20 +1877,43 @@ function makePlugin(site) {
1711
1877
  res = new Response(text, { status: res.status, statusText: res.statusText, headers: res.headers })
1712
1878
  }
1713
1879
  }
1714
- return res.status >= 400 ? explained(res) : kept(await renamed(res, fit.names))
1880
+ if (ours.has(res)) return res
1881
+ // renewed here: a mark the account had comes off
1882
+ return res.status >= 400 ? explained(res) : kept(await renamed(res, fit.names), renewed ? "renewed" : "kept")
1715
1883
  },
1716
1884
  }
1717
1885
  },
1718
1886
  // the account's credits, as magpie's built-in showed them; each
1719
1887
  // read keeps the sign-in (signIn), as the built-in's neither marked
1720
- // an account lapsed nor cleared it
1888
+ // an account lapsed nor cleared it, but for a 401 (gone, below)
1721
1889
  async usage(getAuth) {
1722
1890
  const auth = await getAuth()
1723
1891
  if (auth?.type !== "oauth" || !(auth.access || auth.source)) return { error: "not signed in", signIn: "kept" }
1724
1892
  try {
1725
- const a = await current(site, client, auth)
1893
+ let a = await current(site, client, auth)
1726
1894
  // the meter has no /v2 prefix, on either site
1727
- const sum = await meter(site, a, "/billing/meter/get-user-resource-summary")
1895
+ const SUMMARY = "/billing/meter/get-user-resource-summary"
1896
+ // a 401 is answered as a chat's is: once, by a sign-in with
1897
+ // another access token, else the card says WorkBuddy refused
1898
+ // the sign-in and what to do; renewed, a mark comes off
1899
+ let said = "kept"
1900
+ let sum
1901
+ try {
1902
+ sum = await meter(site, a, SUMMARY)
1903
+ } catch (e) {
1904
+ if (e?.status !== 401) throw e
1905
+ const r = await reissued(site, client, getAuth, a, auth.access)
1906
+ if (!r.a) return { error: refusedText(site, a, r.why), signIn: gone(a, r, e.invalidToken) ? "expired" : "kept" }
1907
+ a = r.a
1908
+ try {
1909
+ sum = await meter(site, a, SUMMARY)
1910
+ } catch (e) {
1911
+ if (e?.status !== 401) throw e
1912
+ const why = "it refused the token renewed for it too"
1913
+ return { error: refusedText(site, a, why), signIn: a.source !== "desktop" && e.invalidToken ? "expired" : "kept" }
1914
+ }
1915
+ said = "renewed"
1916
+ }
1728
1917
  const out = usageOf(sum, auth.plan)
1729
1918
  // an enterprise account's summary has no packages: its credits
1730
1919
  // are the enterprise meter's (#1904). A personal account's
@@ -1735,7 +1924,7 @@ function makePlugin(site) {
1735
1924
  try {
1736
1925
  ent = enterpriseUsageOf(await meter(site, a, "/billing/meter/get-enterprise-user-usage"))
1737
1926
  } catch {}
1738
- if (ent) return { plan: auth.plan || "Enterprise", windows: await spareFree(site, a, [ent]), signIn: "kept" }
1927
+ if (ent) return { plan: auth.plan || "Enterprise", windows: await spareFree(site, a, [ent]), signIn: said }
1739
1928
  }
1740
1929
  // when the credits left run out, for routing to spend them first;
1741
1930
  // a list that can't be read leaves it unknown, the card stands
@@ -1746,7 +1935,7 @@ function makePlugin(site) {
1746
1935
  } catch {}
1747
1936
  }
1748
1937
  await spareFree(site, a, out.windows)
1749
- return { ...out, signIn: "kept" }
1938
+ return { ...out, signIn: said }
1750
1939
  } catch (e) {
1751
1940
  return { error: e?.message ?? String(e), signIn: "kept" }
1752
1941
  }
@@ -1801,17 +1990,19 @@ const EXHAUSTED_HINT =
1801
1990
  // another error, and insufficient_quota would rest the account as out of credit.
1802
1991
  const EXHAUSTED_TEXT = /credits exhausted|额度已用尽|["']code["']\s*:\s*["']?14018(?![\d.])/i
1803
1992
 
1804
- // kept is an answer that went through, saying the sign-in is kept: the
1805
- // built-in never cleared a WorkBuddy account's mark, as it never set one.
1806
- function kept(res) {
1993
+ // kept is an answer that went through, saying the sign-in is kept (the
1994
+ // built-in never cleared a WorkBuddy account's mark, as it never set one),
1995
+ // or "renewed" when it went through on a sign-in renewed for a 401, which
1996
+ // takes a mark off (see signedOut).
1997
+ function kept(res, said = "kept") {
1807
1998
  const headers = new Headers(res.headers)
1808
- headers.set("X-Magpie-Sign-In", "kept")
1999
+ headers.set("X-Magpie-Sign-In", said)
1809
2000
  return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
1810
2001
  }
1811
2002
 
1812
2003
  // explained is res with REFUSED_HINT, RESTRICTED_HINT or EXHAUSTED_HINT added to that refusal's message.
1813
- // Each refusal says the sign-in is kept: the built-in passed WorkBuddy's
1814
- // 401 on and never marked a WorkBuddy account lapsed.
2004
+ // Each refusal says the sign-in is kept: a 401 never comes here, it is
2005
+ // signedOut's.
1815
2006
  async function explained(res) {
1816
2007
  const text = await res.text()
1817
2008
  const again = (b) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-workbuddy-auth",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
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",