@magpie-community/opencode-zcode-auth 0.1.1 → 0.1.2
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 +144 -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
|
|
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"
|
|
@@ -661,7 +662,7 @@ async function authorize(site, log) {
|
|
|
661
662
|
instructions: `Sign in to ${SITES[site].name} in the browser; this finishes on its own.`,
|
|
662
663
|
method: "auto",
|
|
663
664
|
async callback() {
|
|
664
|
-
const fail = (msg) => (log(msg), { type: "failed" })
|
|
665
|
+
const fail = (msg) => (log(msg), { type: "failed", error: msg })
|
|
665
666
|
for (;;) {
|
|
666
667
|
await sleep(interval)
|
|
667
668
|
if (Date.now() > deadline) return fail("the sign-in expired; start again")
|
|
@@ -696,6 +697,136 @@ async function authorize(site, log) {
|
|
|
696
697
|
}
|
|
697
698
|
}
|
|
698
699
|
|
|
700
|
+
// ---- ZCode's own sign-in ------------------------------------------------------------
|
|
701
|
+
// ZCode keeps its sign-in in ~/.zcode/v2/credentials.json, each value
|
|
702
|
+
// encrypted (AES-256-GCM, "enc:v1:<iv>.<tag>.<data>") with a key made from
|
|
703
|
+
// the machine's user; ~/.zcode/v2/setting.json says whether it is on a
|
|
704
|
+
// team's plan. An account signed in "as ZCode" reads them for every
|
|
705
|
+
// request, so it follows ZCode's own sign-in, its renewed session and its
|
|
706
|
+
// switches between plans, as magpie's built-in did (zcode.go zcodeOwn).
|
|
707
|
+
|
|
708
|
+
const zcodeDir = () => join(homedir(), ".zcode", "v2")
|
|
709
|
+
|
|
710
|
+
// secret is the key ZCode encrypts its credentials with.
|
|
711
|
+
function secret() {
|
|
712
|
+
let seed = process.env.ZCODE_CREDENTIAL_SECRET
|
|
713
|
+
if (!seed) {
|
|
714
|
+
let name = ""
|
|
715
|
+
try {
|
|
716
|
+
name = userInfo().username
|
|
717
|
+
} catch {}
|
|
718
|
+
name = name.slice(name.lastIndexOf("\\") + 1) // DOMAIN\user
|
|
719
|
+
seed = `zcode-credential-fallback:${process.platform}:${homedir()}:${name}`
|
|
720
|
+
}
|
|
721
|
+
return createHash("sha256").update(seed).digest()
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function decrypt(key, v) {
|
|
725
|
+
if (typeof v !== "string" || !v.startsWith("enc:v1:")) return null
|
|
726
|
+
const parts = v.slice(7).split(".")
|
|
727
|
+
if (parts.length !== 3) return null
|
|
728
|
+
try {
|
|
729
|
+
const [iv, tag, data] = parts.map((p) => Buffer.from(p.replace(/=+$/, ""), "base64url"))
|
|
730
|
+
const d = createDecipheriv("aes-256-gcm", key, iv)
|
|
731
|
+
d.setAuthTag(tag)
|
|
732
|
+
return Buffer.concat([d.update(data), d.final()]).toString("utf8")
|
|
733
|
+
} catch {
|
|
734
|
+
return null
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function readJSON(path) {
|
|
739
|
+
try {
|
|
740
|
+
return JSON.parse(readFileSync(path, "utf8"))
|
|
741
|
+
} catch {
|
|
742
|
+
return null
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ownTeam is the team plan ZCode switched to, when it did.
|
|
747
|
+
function ownTeam(store, key) {
|
|
748
|
+
const set = readJSON(join(zcodeDir(), "setting.json"))
|
|
749
|
+
if (!set) return null
|
|
750
|
+
const families = set.providerFamilyDomain === "zai" || set.providerFamilyDomain === "bigmodel" ? [set.providerFamilyDomain] : ["zai", "bigmodel"]
|
|
751
|
+
for (const fam of families) {
|
|
752
|
+
let org = ""
|
|
753
|
+
let project = ""
|
|
754
|
+
const sel = set.providerFamilyConnectionSelections?.[fam]
|
|
755
|
+
if (sel) {
|
|
756
|
+
if (sel.kind === "team-coding-plan") [org, project] = [first(sel.organizationId), first(sel.projectId)]
|
|
757
|
+
} else {
|
|
758
|
+
// before providerFamilyConnectionSelections
|
|
759
|
+
const legacy = first(set.modelProviderFamilySelectedKeys?.[fam])
|
|
760
|
+
const pre = `team-plan:builtin:${fam}-coding-plan:`
|
|
761
|
+
if (legacy.startsWith(pre)) {
|
|
762
|
+
const ps = legacy.slice(pre.length).split(":")
|
|
763
|
+
if (ps.length === 3) {
|
|
764
|
+
try {
|
|
765
|
+
;[org, project] = [decodeURIComponent(ps[1]), decodeURIComponent(ps[2])]
|
|
766
|
+
} catch {}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (!org || !project) continue
|
|
771
|
+
const tok = first(decrypt(key, store[`oauth:${fam}:access_token`]))
|
|
772
|
+
if (!tok) continue
|
|
773
|
+
// a stale token ZCode won't use either
|
|
774
|
+
if (fam === "bigmodel" && first(decrypt(key, store.zcodejwttoken)) === tok) continue
|
|
775
|
+
return { site: fam, base: SITES[fam].base, key: "", token: tok, org, project }
|
|
776
|
+
}
|
|
777
|
+
return null
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// ownSignIn is what ZCode is signed in to now: the account, its coding
|
|
781
|
+
// plan key, ZCode's session token (the Start Plan's key) and the team plan
|
|
782
|
+
// it is switched to; null when it has none of them.
|
|
783
|
+
function ownSignIn() {
|
|
784
|
+
const store = readJSON(join(zcodeDir(), "credentials.json"))
|
|
785
|
+
if (!store || typeof store !== "object") return null
|
|
786
|
+
const key = secret()
|
|
787
|
+
let s = { key: "", base: "" }
|
|
788
|
+
for (const [name, v] of Object.entries(store)) {
|
|
789
|
+
// account-provider:coding-plan:account:zai-individual-coding-plan:account:<uuid>:api-key
|
|
790
|
+
if (!name.includes(":coding-plan:") || !name.endsWith(":api-key")) continue
|
|
791
|
+
const k = decrypt(key, v)
|
|
792
|
+
if (!k || !k.includes(".")) continue
|
|
793
|
+
const base = name.includes(":bigmodel-") ? BIGMODEL_BASE : ZAI_BASE
|
|
794
|
+
// Z.ai's first, as ZCode lists it
|
|
795
|
+
if (!s.key || (base === ZAI_BASE && s.base !== ZAI_BASE)) s = { key: k, base }
|
|
796
|
+
}
|
|
797
|
+
const jwt = first(first(decrypt(key, store.zcodejwttoken)).replace(/^Bearer /, ""))
|
|
798
|
+
const team = ownTeam(store, key)
|
|
799
|
+
if (team) s = { ...team, jwt }
|
|
800
|
+
else s = { ...s, jwt, base: s.base || ZAI_BASE, site: s.base === BIGMODEL_BASE ? "bigmodel" : "zai" }
|
|
801
|
+
if (!s.key && !s.jwt && !isTeam(s)) return null
|
|
802
|
+
let info = {}
|
|
803
|
+
for (const name of ["oauth:zai:user_info", "oauth:bigmodel:user_info"]) {
|
|
804
|
+
const v = decrypt(key, store[name])
|
|
805
|
+
if (v && !first(info.email, info.name, info.user_id)) {
|
|
806
|
+
try {
|
|
807
|
+
info = JSON.parse(v)
|
|
808
|
+
} catch {}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return { ...s, user: who(info) }
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// asZCode is the sign-in "as ZCode": what it reads is ZCode's own.
|
|
815
|
+
function asZCode() {
|
|
816
|
+
return {
|
|
817
|
+
url: "",
|
|
818
|
+
instructions: "Uses the account the ZCode app is signed in to, and follows it.",
|
|
819
|
+
method: "auto",
|
|
820
|
+
async callback() {
|
|
821
|
+
const own = ownSignIn()
|
|
822
|
+
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" }
|
|
823
|
+
const { user, ...s } = own
|
|
824
|
+
const state = { source: "zcode", device: anyDevice, ...s }
|
|
825
|
+
return { type: "success", refresh: JSON.stringify(state), access: s.key || s.jwt || "", expires: 0, accountId: user }
|
|
826
|
+
},
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
699
830
|
// stateOf is a stored sign-in as the requests need it.
|
|
700
831
|
function stateOf(auth) {
|
|
701
832
|
if (auth?.type === "oauth") {
|
|
@@ -703,6 +834,15 @@ function stateOf(auth) {
|
|
|
703
834
|
try {
|
|
704
835
|
s = JSON.parse(auth.refresh)
|
|
705
836
|
} catch {}
|
|
837
|
+
if (s.source === "zcode") {
|
|
838
|
+
// ZCode's own, read now: what it was when this was signed in, if
|
|
839
|
+
// ZCode has since signed out
|
|
840
|
+
const own = ownSignIn()
|
|
841
|
+
if (own) {
|
|
842
|
+
const { user, ...now } = own
|
|
843
|
+
s = { source: "zcode", device: s.device, plan: s.plan, ...now }
|
|
844
|
+
}
|
|
845
|
+
}
|
|
706
846
|
const site = siteOf(s.site)
|
|
707
847
|
return { ...s, site, base: s.base || SITES[site].base, key: s.key ?? (s.jwt ? "" : auth.access), device: s.device || anyDevice }
|
|
708
848
|
}
|
|
@@ -983,6 +1123,7 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
983
1123
|
methods: [
|
|
984
1124
|
oauth("zai"),
|
|
985
1125
|
oauth("bigmodel"),
|
|
1126
|
+
{ type: "oauth", label: "ZCode app's sign-in", authorize: async () => asZCode() },
|
|
986
1127
|
{
|
|
987
1128
|
type: "api",
|
|
988
1129
|
label: "GLM Coding Plan API key",
|
|
@@ -1004,4 +1145,4 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1004
1145
|
}
|
|
1005
1146
|
|
|
1006
1147
|
// for tests
|
|
1007
|
-
export const _internal = { limitWindows, termOf, startUsage, routes, teamKeys }
|
|
1148
|
+
export const _internal = { 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.2",
|
|
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",
|