@magpie-community/opencode-zcode-auth 0.1.1 → 0.1.3

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 +7 -1
  2. package/index.mjs +158 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -20,6 +20,11 @@ over Anthropic's Messages API. Provider id: `zcode`.
20
20
  with ZCode's session token and ZCode's own headers.
21
21
  - The Start Plan's token can't be refreshed. When it runs out, sign in
22
22
  again.
23
+ - **ZCode app's sign-in**
24
+ - Uses the account the ZCode app on this computer is signed in to. For
25
+ every request it reads ZCode's own credentials (`~/.zcode/v2`), so it
26
+ follows ZCode as it signs in again, renews its session or switches to a
27
+ team's plan.
23
28
  - **GLM Coding Plan API key**
24
29
  - Paste a key (`<id>.<secret>`) and say which site it is from.
25
30
 
@@ -33,7 +38,8 @@ In OpenCode's `auth.json` (magpie: `plugin-auth.json`), under `zcode`:
33
38
  token, the team project, the plan and a device id of this plugin's own.
34
39
  - **A pasted key** is kept as an `api` entry, with the site in `metadata`.
35
40
 
36
- The plugin never reads or writes ZCode's own credential store.
41
+ The plugin never writes ZCode's own credential store. It reads it only for
42
+ the ZCode app's sign-in (`refresh` then holds `"source": "zcode"`).
37
43
 
38
44
  ## How requests are routed
39
45
 
package/index.mjs CHANGED
@@ -16,8 +16,9 @@
16
16
  // `refresh` the rest of it as JSON (site, base, key, jwt, team project,
17
17
  // plan, device id), `accountId` the account's email.
18
18
 
19
+ import { createDecipheriv, createHash } from "node:crypto"
19
20
  import { existsSync, readFileSync, readdirSync } from "node:fs"
20
- import { homedir } from "node:os"
21
+ import { homedir, userInfo } from "node:os"
21
22
  import { join } from "node:path"
22
23
 
23
24
  const PROVIDER = "zcode"
@@ -38,7 +39,6 @@ const MODELS = [
38
39
  { id: "GLM-5-Turbo", context: 200_000, output: 64_000, efforts: ["none", "high"] },
39
40
  ]
40
41
  const START_MODELS = MODELS.filter((m) => m.id !== "GLM-5.3")
41
- const OUTPUT = 131_072 // when ZCode's config names none
42
42
 
43
43
  const SITES = {
44
44
  zai: { name: "Z.ai", api: ZAI_API, base: ZAI_BASE, subscribe: "z.ai/subscribe" },
@@ -661,7 +661,7 @@ async function authorize(site, log) {
661
661
  instructions: `Sign in to ${SITES[site].name} in the browser; this finishes on its own.`,
662
662
  method: "auto",
663
663
  async callback() {
664
- const fail = (msg) => (log(msg), { type: "failed" })
664
+ const fail = (msg) => (log(msg), { type: "failed", error: msg })
665
665
  for (;;) {
666
666
  await sleep(interval)
667
667
  if (Date.now() > deadline) return fail("the sign-in expired; start again")
@@ -696,6 +696,136 @@ async function authorize(site, log) {
696
696
  }
697
697
  }
698
698
 
699
+ // ---- ZCode's own sign-in ------------------------------------------------------------
700
+ // ZCode keeps its sign-in in ~/.zcode/v2/credentials.json, each value
701
+ // encrypted (AES-256-GCM, "enc:v1:<iv>.<tag>.<data>") with a key made from
702
+ // the machine's user; ~/.zcode/v2/setting.json says whether it is on a
703
+ // team's plan. An account signed in "as ZCode" reads them for every
704
+ // request, so it follows ZCode's own sign-in, its renewed session and its
705
+ // switches between plans, as magpie's built-in did (zcode.go zcodeOwn).
706
+
707
+ const zcodeDir = () => join(homedir(), ".zcode", "v2")
708
+
709
+ // secret is the key ZCode encrypts its credentials with.
710
+ function secret() {
711
+ let seed = process.env.ZCODE_CREDENTIAL_SECRET
712
+ if (!seed) {
713
+ let name = ""
714
+ try {
715
+ name = userInfo().username
716
+ } catch {}
717
+ name = name.slice(name.lastIndexOf("\\") + 1) // DOMAIN\user
718
+ seed = `zcode-credential-fallback:${process.platform}:${homedir()}:${name}`
719
+ }
720
+ return createHash("sha256").update(seed).digest()
721
+ }
722
+
723
+ function decrypt(key, v) {
724
+ if (typeof v !== "string" || !v.startsWith("enc:v1:")) return null
725
+ const parts = v.slice(7).split(".")
726
+ if (parts.length !== 3) return null
727
+ try {
728
+ const [iv, tag, data] = parts.map((p) => Buffer.from(p.replace(/=+$/, ""), "base64url"))
729
+ const d = createDecipheriv("aes-256-gcm", key, iv)
730
+ d.setAuthTag(tag)
731
+ return Buffer.concat([d.update(data), d.final()]).toString("utf8")
732
+ } catch {
733
+ return null
734
+ }
735
+ }
736
+
737
+ function readJSON(path) {
738
+ try {
739
+ return JSON.parse(readFileSync(path, "utf8"))
740
+ } catch {
741
+ return null
742
+ }
743
+ }
744
+
745
+ // ownTeam is the team plan ZCode switched to, when it did.
746
+ function ownTeam(store, key) {
747
+ const set = readJSON(join(zcodeDir(), "setting.json"))
748
+ if (!set) return null
749
+ const families = set.providerFamilyDomain === "zai" || set.providerFamilyDomain === "bigmodel" ? [set.providerFamilyDomain] : ["zai", "bigmodel"]
750
+ for (const fam of families) {
751
+ let org = ""
752
+ let project = ""
753
+ const sel = set.providerFamilyConnectionSelections?.[fam]
754
+ if (sel) {
755
+ if (sel.kind === "team-coding-plan") [org, project] = [first(sel.organizationId), first(sel.projectId)]
756
+ } else {
757
+ // before providerFamilyConnectionSelections
758
+ const legacy = first(set.modelProviderFamilySelectedKeys?.[fam])
759
+ const pre = `team-plan:builtin:${fam}-coding-plan:`
760
+ if (legacy.startsWith(pre)) {
761
+ const ps = legacy.slice(pre.length).split(":")
762
+ if (ps.length === 3) {
763
+ try {
764
+ ;[org, project] = [decodeURIComponent(ps[1]), decodeURIComponent(ps[2])]
765
+ } catch {}
766
+ }
767
+ }
768
+ }
769
+ if (!org || !project) continue
770
+ const tok = first(decrypt(key, store[`oauth:${fam}:access_token`]))
771
+ if (!tok) continue
772
+ // a stale token ZCode won't use either
773
+ if (fam === "bigmodel" && first(decrypt(key, store.zcodejwttoken)) === tok) continue
774
+ return { site: fam, base: SITES[fam].base, key: "", token: tok, org, project }
775
+ }
776
+ return null
777
+ }
778
+
779
+ // ownSignIn is what ZCode is signed in to now: the account, its coding
780
+ // plan key, ZCode's session token (the Start Plan's key) and the team plan
781
+ // it is switched to; null when it has none of them.
782
+ function ownSignIn() {
783
+ const store = readJSON(join(zcodeDir(), "credentials.json"))
784
+ if (!store || typeof store !== "object") return null
785
+ const key = secret()
786
+ let s = { key: "", base: "" }
787
+ for (const [name, v] of Object.entries(store)) {
788
+ // account-provider:coding-plan:account:zai-individual-coding-plan:account:<uuid>:api-key
789
+ if (!name.includes(":coding-plan:") || !name.endsWith(":api-key")) continue
790
+ const k = decrypt(key, v)
791
+ if (!k || !k.includes(".")) continue
792
+ const base = name.includes(":bigmodel-") ? BIGMODEL_BASE : ZAI_BASE
793
+ // Z.ai's first, as ZCode lists it
794
+ if (!s.key || (base === ZAI_BASE && s.base !== ZAI_BASE)) s = { key: k, base }
795
+ }
796
+ const jwt = first(first(decrypt(key, store.zcodejwttoken)).replace(/^Bearer /, ""))
797
+ const team = ownTeam(store, key)
798
+ if (team) s = { ...team, jwt }
799
+ else s = { ...s, jwt, base: s.base || ZAI_BASE, site: s.base === BIGMODEL_BASE ? "bigmodel" : "zai" }
800
+ if (!s.key && !s.jwt && !isTeam(s)) return null
801
+ let info = {}
802
+ for (const name of ["oauth:zai:user_info", "oauth:bigmodel:user_info"]) {
803
+ const v = decrypt(key, store[name])
804
+ if (v && !first(info.email, info.name, info.user_id)) {
805
+ try {
806
+ info = JSON.parse(v)
807
+ } catch {}
808
+ }
809
+ }
810
+ return { ...s, user: who(info) }
811
+ }
812
+
813
+ // asZCode is the sign-in "as ZCode": what it reads is ZCode's own.
814
+ function asZCode() {
815
+ return {
816
+ url: "",
817
+ instructions: "Uses the account the ZCode app is signed in to, and follows it.",
818
+ method: "auto",
819
+ async callback() {
820
+ const own = ownSignIn()
821
+ if (!own) return { type: "failed", error: "ZCode isn't signed in on this computer; sign in to ZCode, or sign in to Z.ai or BigModel here" }
822
+ const { user, ...s } = own
823
+ const state = { source: "zcode", device: anyDevice, ...s }
824
+ return { type: "success", refresh: JSON.stringify(state), access: s.key || s.jwt || "", expires: 0, accountId: user }
825
+ },
826
+ }
827
+ }
828
+
699
829
  // stateOf is a stored sign-in as the requests need it.
700
830
  function stateOf(auth) {
701
831
  if (auth?.type === "oauth") {
@@ -703,6 +833,15 @@ function stateOf(auth) {
703
833
  try {
704
834
  s = JSON.parse(auth.refresh)
705
835
  } catch {}
836
+ if (s.source === "zcode") {
837
+ // ZCode's own, read now: what it was when this was signed in, if
838
+ // ZCode has since signed out
839
+ const own = ownSignIn()
840
+ if (own) {
841
+ const { user, ...now } = own
842
+ s = { source: "zcode", device: s.device, plan: s.plan, ...now }
843
+ }
844
+ }
706
845
  const site = siteOf(s.site)
707
846
  return { ...s, site, base: s.base || SITES[site].base, key: s.key ?? (s.jwt ? "" : auth.access), device: s.device || anyDevice }
708
847
  }
@@ -835,7 +974,8 @@ const entry = (m) => ({
835
974
  tool_call: true,
836
975
  attachment: !!m.image,
837
976
  modalities: { input: m.image ? ["text", "image"] : ["text"], output: ["text"] },
838
- limit: { context: m.context || 200_000, output: m.output || OUTPUT },
977
+ // what ZCode's config names, else 0 (unknown), as magpie's built-in
978
+ limit: { context: m.context || 0, output: m.output || 0 },
839
979
  cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
840
980
  variants: variants(m.efforts),
841
981
  })
@@ -962,6 +1102,14 @@ export async function ZCodeAuthPlugin({ client }) {
962
1102
  const auth = await getAuth()
963
1103
  const s = stateOf(auth)
964
1104
  if (!s) return { error: "not signed in" }
1105
+ // every card starts from the plan the sign-in saved, as magpie's
1106
+ // built-in did: a reply without a level, a team detail that
1107
+ // failed or an error keeps it
1108
+ const saved = (out) => {
1109
+ const plan = s.plan || auth?.plan
1110
+ if (plan && !out.plan) out.plan = plan
1111
+ return out
1112
+ }
965
1113
  try {
966
1114
  if (isTeam(s)) {
967
1115
  let key = s.key
@@ -972,17 +1120,18 @@ export async function ZCodeAuthPlugin({ client }) {
972
1120
  await client?.auth?.set?.({ path: { id: provider?.id ?? PROVIDER }, body: next }).catch?.(() => {})
973
1121
  }
974
1122
  }
975
- return await teamUsage(s, key)
1123
+ return saved(await teamUsage(s, key))
976
1124
  }
977
- if (await onStart(s)) return await startUsage(s)
978
- return await codingUsage(s)
1125
+ if (await onStart(s)) return saved(await startUsage(s))
1126
+ return saved(await codingUsage(s))
979
1127
  } catch (e) {
980
- return { error: e?.message ?? String(e) }
1128
+ return saved({ error: e?.message ?? String(e) })
981
1129
  }
982
1130
  },
983
1131
  methods: [
984
1132
  oauth("zai"),
985
1133
  oauth("bigmodel"),
1134
+ { type: "oauth", label: "ZCode app's sign-in", authorize: async () => asZCode() },
986
1135
  {
987
1136
  type: "api",
988
1137
  label: "GLM Coding Plan API key",
@@ -1004,4 +1153,4 @@ export async function ZCodeAuthPlugin({ client }) {
1004
1153
  }
1005
1154
 
1006
1155
  // for tests
1007
- export const _internal = { limitWindows, termOf, startUsage, routes, teamKeys }
1156
+ export const _internal = { entry, limitWindows, termOf, startUsage, routes, teamKeys, ownSignIn, stateOf }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-zcode-auth",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",