@magpie-community/opencode-zcode-auth 0.1.18 → 0.1.20

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 +24 -1
  2. package/index.mjs +114 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -24,9 +24,23 @@ over Anthropic's Messages API. Provider id: `zcode`.
24
24
  ZCode's does: ZCode's headers, ZCode's system prompt ahead of the
25
25
  agent's, the day in a `<system-reminder>` turn before the agent's
26
26
  turns, and the sign-in's device id in `metadata.user_id`. GLM Coding
27
- Plan and team requests go as the agent sent them.
27
+ Plan and team requests go as the agent sent them. One it still turns
28
+ away (a 405, the block page or code 3012) comes back with what to do
29
+ about it, as magpie's built-in said: "ZCode's Start Plan still turned
30
+ this request away, though magpie sends it as the ZCode app does; …
31
+ Use an account with a GLM Coding Plan, or add another provider to this
32
+ group".
28
33
  - The Start Plan's token can't be refreshed. When it runs out, sign in
29
34
  again.
35
+ - **ZCode: a team's GLM Coding Plan**
36
+ - For an account with a plan of its own and a seat on a team's plan
37
+ (#285). The sign-ins above take the account's own plan, as ZCode does
38
+ by default; this one asks which site, signs in the same way, and
39
+ takes the team seat's `zcode-team-api-key`, as ZCode does when its
40
+ plan picker is switched to the team's plan.
41
+ - It is kept as an account of its own, named `<email> (team)`, beside
42
+ the account's own plan, so both can be used.
43
+ - With no seat on a team's plan, it says so and signs in to nothing.
30
44
  - **ZCode app's sign-in**
31
45
  - Uses the account the ZCode app on this computer is signed in to. For
32
46
  every request it reads ZCode's own credentials (`~/.zcode/v2`), so it
@@ -96,6 +110,15 @@ behind an error, so a gift hiccup must not black out a working Coding
96
110
  Plan. A gift that goes out of date while requests use it simply stops
97
111
  showing.
98
112
 
113
+ **Resets.** A GLM Coding Plan of the account's own, and a team seat,
114
+ show the resets the vendor's usage page lists
115
+ (`/api/biz/customer-package-reset/list`, `targetType=PERSONAL` with the
116
+ plan's key, `TEAM` with the team's sign-in): how many of the five hours
117
+ and of the week are still available, and when each runs out, soonest
118
+ first. A team's are marked a team's. They are only read; a reset is
119
+ spent on the vendor's own page. A list that can't be read, or has none
120
+ left, shows no count.
121
+
99
122
  **Gift plans ZCode holds but has not granted.** ZCode hands out one-off
100
123
  trust-build / gift plans (a day's ZCode Trust Build, say) that are not
101
124
  on the account until the ZCode app claims them. The plugin reads the
package/index.mjs CHANGED
@@ -436,9 +436,12 @@ async function codingUsage(s) {
436
436
  const level = typeof d.level === "string" ? d.level : ""
437
437
  if (level) out.plan = "GLM Coding " + level[0].toUpperCase() + level.slice(1)
438
438
  out.windows = limitWindows(d)
439
+ const resets = personalResets(root, s.key)
439
440
  try {
440
441
  Object.assign(out, termOf(await call("GET", root + "/api/biz/subscription/list", { auth: s.key })))
441
442
  } catch {}
443
+ const r = await resets
444
+ if (r) out.resets = r
442
445
  return out
443
446
  }
444
447
 
@@ -708,31 +711,56 @@ async function teamUsage(s, key) {
708
711
  }
709
712
 
710
713
  // teamResets is how many resets a team member may spend, of the five hours
711
- // and of the week: those of customer-package-reset/list still available.
712
- // null when it can't be told or there are none.
714
+ // and of the week, marked a team's (team: true), as the built-in's
715
+ // zhipuTeamResets did. null when it can't be told or there are none.
713
716
  async function teamResets(root, auth, headers) {
717
+ const r = await resetsOf(root + "/api/biz/customer-package-reset/list?targetType=TEAM", auth, headers)
718
+ if (r) r.team = true
719
+ return r
720
+ }
721
+
722
+ // personalResets is how many resets a GLM Coding Plan of a person's own
723
+ // holds, as bigmodel.cn's usage page lists them, asked with the plan's
724
+ // key (the built-in's zhipuPersonalResets). Only read: a reset is spent
725
+ // on the vendor's own page, never from here.
726
+ async function personalResets(root, key) {
727
+ return resetsOf(root + "/api/biz/customer-package-reset/list?targetType=PERSONAL", key)
728
+ }
729
+
730
+ // resetsOf reads customer-package-reset/list: the resets of the five
731
+ // hours and of the week, of which only those still available count (one
732
+ // run out stays listed a week, available false), each with its own end
733
+ // (each, soonest first, one with no end last) and the soonest as until.
734
+ // null when the list can't be read, so no count is shown rather than 0,
735
+ // and when it holds none.
736
+ async function resetsOf(url, auth, headers) {
714
737
  let d
715
738
  try {
716
- d = (await call("GET", root + "/api/biz/customer-package-reset/list?targetType=TEAM", { auth, headers })) ?? {}
739
+ d = (await call("GET", url, { auth, headers })) ?? {}
717
740
  } catch {
718
741
  return null
719
742
  }
720
743
  let until = 0
721
- const count = (rs) => {
744
+ const each = []
745
+ const count = (rs, window) => {
722
746
  let n = 0
723
747
  for (const x of Array.isArray(rs) ? rs : []) {
724
748
  if (x?.available !== true) continue
725
749
  n++
726
750
  const t = when(x.expireTime)
727
751
  if (t && (!until || t < until)) until = t
752
+ each.push(t ? { until: new Date(t).toISOString(), window } : { window })
728
753
  }
729
754
  return n
730
755
  }
731
- const fiveHour = count(d.fiveHourResets)
732
- const weekly = count(d.weekResets)
756
+ const fiveHour = count(d.fiveHourResets, "fiveHour")
757
+ const weekly = count(d.weekResets, "weekly")
733
758
  if (fiveHour + weekly === 0) return null
759
+ const end = (c) => (c.until ? Date.parse(c.until) : Infinity)
760
+ each.sort((a, b) => (end(a) === end(b) ? 0 : end(a) < end(b) ? -1 : 1))
734
761
  const r = { count: fiveHour + weekly, byWindow: true, fiveHour, weekly }
735
762
  if (until) r.until = new Date(until).toISOString()
763
+ r.each = each
736
764
  return r
737
765
  }
738
766
 
@@ -819,13 +847,29 @@ async function teamSignIn(site, root, auth, info) {
819
847
 
820
848
  // signedIn is what a sign-in gives: the account's coding plan key and plan;
821
849
  // or, with none of its own, a seat on a team's plan; or else ZCode's token
822
- // for its Start Plan while the account has one.
823
- async function signedIn(site, token, jwt, device) {
850
+ // for its Start Plan while the account has one. Asked for the team's plan
851
+ // (onTeam), it is the team seat alone, as ZCode's plan picker gives a team
852
+ // plan beside the account's own (a "team-coding-plan" selection): an
853
+ // account with both is signed in to each as an account of its own (#285).
854
+ async function signedIn(site, token, jwt, device, onTeam = false) {
824
855
  const name = SITES[site].name
825
856
  const root = SITES[site].api
826
857
  let s = null
827
858
  let err = null
828
859
  let team = ""
860
+ if (onTeam) {
861
+ const auth = await bizAuth(site, token)
862
+ let info
863
+ try {
864
+ info = await call("GET", root + "/api/biz/customer/getCustomerInfo", { auth })
865
+ } catch (e) {
866
+ throw new Error(`${name} account: ${e.message}`)
867
+ }
868
+ const t = await teamSignIn(site, root, auth, info)
869
+ if (t.s) return { ...t.s, jwt, plan: t.plan }
870
+ if (t.none) throw new Error(`this ${name} account has no seat on a team's GLM Coding Plan — sign in with "ZCode: ${name} GLM Coding Plan" for its own plan`)
871
+ throw new Error(t.why ?? t.err)
872
+ }
829
873
  try {
830
874
  const auth = await bizAuth(site, token)
831
875
  let info
@@ -863,11 +907,15 @@ async function signedIn(site, token, jwt, device) {
863
907
  )
864
908
  }
865
909
 
910
+ // TEAM marks a team seat's account name beside the account's own.
911
+ const TEAM = "team"
866
912
  const who = (u) => first(String(u?.email ?? "").replace(/@phone\.local$/, ""), u?.name, u?.user_id, "ZCode")
867
913
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
868
914
 
869
915
  // authorize opens ZCode's sign-in flow on site; its callback polls it.
870
- async function authorize(site, log) {
916
+ // onTeam signs in to the account's seat on a team's plan, named apart from
917
+ // the account's own plan (the host keeps one entry per account name).
918
+ async function authorize(site, log, onTeam = false) {
871
919
  const poll = "Bearer " + Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex")
872
920
  const device = uuid()
873
921
  let flow
@@ -908,7 +956,7 @@ async function authorize(site, log) {
908
956
  if (got.status !== "ready" || !token) return fail("ZCode sign-in: unexpected answer " + got.status)
909
957
  let s
910
958
  try {
911
- s = await signedIn(site, token, first(got.token), device)
959
+ s = await signedIn(site, token, first(got.token), device, onTeam)
912
960
  } catch (e) {
913
961
  return fail(e.message)
914
962
  }
@@ -919,7 +967,7 @@ async function authorize(site, log) {
919
967
  access: s.key || s.jwt || "",
920
968
  // the key doesn't run out; ZCode's token, when it is all there is, does
921
969
  expires: s.key || isTeam(s) ? 0 : jwtExpiry(s.jwt),
922
- accountId: who(got.user),
970
+ accountId: onTeam ? `${who(got.user)} (${TEAM})` : who(got.user),
923
971
  }
924
972
  }
925
973
  },
@@ -1311,6 +1359,36 @@ function model(m, url) {
1311
1359
  // the plan serves (ARNO's "Freeflow", provider zcode-start) sends it: its
1312
1360
  // headers (startHeaders) and its body (dress). The texts are prompt.mjs.
1313
1361
 
1362
+ // START_BLOCKED is what the Start Plan still turning a request away means,
1363
+ // word for word magpie's provider.ZCodeStartBlockedHint (#425), which
1364
+ // magpie's Routing page knows and says in the user's language. It is put
1365
+ // in place of the block page, whose words magpie would otherwise read as a
1366
+ // network firewall's (provider.BlockedHint).
1367
+ const START_BLOCKED = "ZCode's Start Plan still turned this request away, though magpie sends it as the ZCode app does; it can be a network block of this IP, or ZCode checking for something new. Use an account with a GLM Coding Plan, or add another provider to this group"
1368
+ const EDGE_BLOCKED = /request has been blocked|errors\.aliyun\.com/i
1369
+ const START_REFUSED = /unusual activity|"code"\s*:\s*"?3012\b/i
1370
+
1371
+ // startRefused says a Start Plan answer is that refusal, as the built-in's
1372
+ // zcodeStartExplain read it: a 405, a block page or code 3012.
1373
+ const startRefused = (status, text) => status >= 400 && (status === 405 || EDGE_BLOCKED.test(text) || START_REFUSED.test(text))
1374
+
1375
+ // startBlocked is the refusal's answer as the built-in passed it on: what
1376
+ // the vendor said (its status for a block page), then START_BLOCKED.
1377
+ function startBlocked(res, text) {
1378
+ let said = ""
1379
+ if (!EDGE_BLOCKED.test(text)) {
1380
+ try {
1381
+ const v = JSON.parse(text)
1382
+ said = first(v?.error?.message, typeof v?.error === "string" ? v.error : "", v?.message, v?.msg)
1383
+ } catch {}
1384
+ }
1385
+ said ||= `${res.status} ${STATUS_TEXT[res.status] ?? res.statusText}`.trim()
1386
+ const h = new Headers(res.headers)
1387
+ h.set("content-type", "application/json")
1388
+ const error = { type: "permission_error", message: `${said} — ${START_BLOCKED}` }
1389
+ return new Response(JSON.stringify({ type: "error", error }), { status: res.status, statusText: res.statusText, headers: h })
1390
+ }
1391
+
1314
1392
  const osVersion = () => `${process.platform} ${release()} ${arch()}`
1315
1393
  const osCategory = () => (process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux")
1316
1394
 
@@ -1594,6 +1672,13 @@ export async function ZCodeAuthPlugin({ client }) {
1594
1672
  h.set("Authorization", "Bearer " + key)
1595
1673
  }
1596
1674
  let res = await fetch(url, { ...opts, headers: h })
1675
+ // the Start Plan turning the request away says what to do about
1676
+ // it, as the built-in's zcodeStartExplain did
1677
+ if (start && res.status >= 400) {
1678
+ const text = await res.text()
1679
+ if (startRefused(res.status, text)) return kept(startBlocked(res, text))
1680
+ res = new Response(text, { status: res.status, statusText: res.statusText, headers: res.headers })
1681
+ }
1597
1682
  // a trial entry's gift answer that can't serve the request is
1598
1683
  // the agent's answer, not the coding plan's: the user picked the
1599
1684
  // gift, so a spent bucket says 429 (its quota error inside a 200
@@ -1668,6 +1753,24 @@ export async function ZCodeAuthPlugin({ client }) {
1668
1753
  methods: [
1669
1754
  oauth("zai"),
1670
1755
  oauth("bigmodel"),
1756
+ // a team's plan beside the account's own (#285): the sign-ins above
1757
+ // take the account's own plan when it has one
1758
+ {
1759
+ type: "oauth",
1760
+ label: "ZCode: a team's GLM Coding Plan",
1761
+ prompts: [
1762
+ {
1763
+ type: "select",
1764
+ key: "site",
1765
+ message: "Where is the team's plan?",
1766
+ options: [
1767
+ { label: "Z.ai", value: "zai", hint: "z.ai" },
1768
+ { label: "BigModel (智谱)", value: "bigmodel", hint: "bigmodel.cn" },
1769
+ ],
1770
+ },
1771
+ ],
1772
+ authorize: (inputs) => authorize(inputs?.site === "bigmodel" ? "bigmodel" : "zai", log, true),
1773
+ },
1671
1774
  { type: "oauth", label: "ZCode app's sign-in", authorize: async () => asZCode() },
1672
1775
  {
1673
1776
  type: "api",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-zcode-auth",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "OpenCode provider plugin: signs in to Z.ai's or BigModel's (智谱) GLM Coding Plan as ZCode does, including team seats and ZCode's Start Plan",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",