@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.
- package/README.md +7 -1
- package/index.mjs +158 -9
- 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
|
|
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
|
-
|
|
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.
|
|
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",
|