@magpie-community/opencode-zcode-auth 0.1.0 → 0.1.1
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/index.mjs +297 -1
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -51,6 +51,12 @@ const uuid = () => crypto.randomUUID()
|
|
|
51
51
|
const anyDevice = uuid() // X-Device-Mid for calls made signed out (the model list)
|
|
52
52
|
const first = (...ss) => ss.map((s) => (typeof s === "string" ? s.trim() : "")).find((s) => s) ?? ""
|
|
53
53
|
|
|
54
|
+
// STATUS_TEXT is Go's http.StatusText, which magpie's built-in said a
|
|
55
|
+
// refusal with no message in.
|
|
56
|
+
const STATUS_TEXT = { 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed",
|
|
57
|
+
408: "Request Timeout", 429: "Too Many Requests", 500: "Internal Server Error", 502: "Bad Gateway", 503: "Service Unavailable",
|
|
58
|
+
504: "Gateway Timeout" }
|
|
59
|
+
|
|
54
60
|
class ZError extends Error {
|
|
55
61
|
constructor(message, status) {
|
|
56
62
|
super(message)
|
|
@@ -75,7 +81,7 @@ async function call(method, url, { auth, body, headers, device, signal } = {}) {
|
|
|
75
81
|
const code = env?.code == null ? "" : String(env.code)
|
|
76
82
|
if (!res.ok) {
|
|
77
83
|
if (env?.msg) throw new ZError(code && code !== "0" ? `${env.msg} (${res.status}, code ${code})` : `${env.msg} (${res.status})`, res.status)
|
|
78
|
-
throw new ZError(`${res.status} ${res.statusText}`.trim(), res.status)
|
|
84
|
+
throw new ZError(STATUS_TEXT[res.status] ?? `${res.status} ${res.statusText}`.trim(), res.status)
|
|
79
85
|
}
|
|
80
86
|
if (!env || typeof env !== "object") throw new ZError("not JSON", res.status)
|
|
81
87
|
if (code && code !== "0" && code !== "200") throw new ZError(env.msg || `error ${code}`, res.status)
|
|
@@ -236,6 +242,270 @@ async function teamKey(s) {
|
|
|
236
242
|
}
|
|
237
243
|
}
|
|
238
244
|
|
|
245
|
+
// ---- allowance ---------------------------------------------------------------------
|
|
246
|
+
// As magpie's built-in told it (internal/provider zcode.go, zcode_start.go,
|
|
247
|
+
// zcode_team.go): the Coding Plan's windows and term, the Start Plan's
|
|
248
|
+
// buckets, or a team seat's windows and its plan's end.
|
|
249
|
+
|
|
250
|
+
// bizRoot is the business API of the site a plan is served from.
|
|
251
|
+
function bizRoot(base) {
|
|
252
|
+
let h = ""
|
|
253
|
+
try {
|
|
254
|
+
h = new URL(base).host.toLowerCase()
|
|
255
|
+
} catch {}
|
|
256
|
+
return h === new URL(BIGMODEL_BASE).host || h.endsWith("bigmodel.cn") ? BIGMODEL_API : rootOf(base)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// spanOf reads a limit's window, in seconds: unit 3 counts hours, 6 weeks
|
|
260
|
+
// (and 1, 4, 5 minutes, days and months by the same count). A team's five
|
|
261
|
+
// hours come with no count.
|
|
262
|
+
function spanOf(unit, n) {
|
|
263
|
+
n = Math.trunc(Number(n) || 0)
|
|
264
|
+
if (unit === 3 && n <= 0) n = 5
|
|
265
|
+
n = Math.max(n, 1)
|
|
266
|
+
return { 1: 60, 3: 3600, 4: 86400, 5: 30 * 86400, 6: 7 * 86400 }[unit] * n || 0
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function windowName(span) {
|
|
270
|
+
if (span === 0) return "Credits"
|
|
271
|
+
if (span < 86400) return `${Math.trunc(span / 3600)} hours`
|
|
272
|
+
if (span === 7 * 86400) return "Weekly"
|
|
273
|
+
if (span >= 28 * 86400) return "Monthly"
|
|
274
|
+
return `${Math.trunc(span / 86400)} days`
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// compact is a count as magpie says one: whole, else to two places.
|
|
278
|
+
const compact = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/0+$/, "").replace(/\.$/, ""))
|
|
279
|
+
const isNum = (v) => typeof v === "number" && Number.isFinite(v)
|
|
280
|
+
|
|
281
|
+
// limitWindows are /api/monitor/usage/quota/limit's limits as windows: what
|
|
282
|
+
// is used of the whole when both are told (the whole less what remains, or
|
|
283
|
+
// the current value), else the percentage given.
|
|
284
|
+
function limitWindows(d) {
|
|
285
|
+
const out = []
|
|
286
|
+
for (const x of d?.limits ?? []) {
|
|
287
|
+
const span = spanOf(Math.trunc(Number(x?.unit) || 0), x?.number)
|
|
288
|
+
const w = { name: windowName(span), used: 0 }
|
|
289
|
+
if (isNum(x?.percentage)) w.used = x.percentage
|
|
290
|
+
if (isNum(x?.usage) && x.usage > 0) {
|
|
291
|
+
const total = x.usage
|
|
292
|
+
if (isNum(x.remaining)) {
|
|
293
|
+
const used = total - x.remaining
|
|
294
|
+
w.used = (100 * used) / total
|
|
295
|
+
w.display = `${compact(used)} / ${compact(total)}`
|
|
296
|
+
} else if (isNum(x.currentValue)) {
|
|
297
|
+
if (!isNum(x.percentage)) w.used = (100 * x.currentValue) / total
|
|
298
|
+
w.display = `${compact(x.currentValue)} / ${compact(total)}`
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const reset = Math.trunc(Number(x?.nextResetTime) || 0)
|
|
302
|
+
if (reset > 0) w.resetsAt = new Date(reset).toISOString()
|
|
303
|
+
if (span) w.span = span
|
|
304
|
+
out.push(w)
|
|
305
|
+
}
|
|
306
|
+
return out
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// beijing reads the plan's times, "2026-10-18 12:00:00" in Beijing: ms, or 0.
|
|
310
|
+
function beijing(s) {
|
|
311
|
+
s = String(s ?? "").trim().replace(" ", "T")
|
|
312
|
+
if (!/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?$/.test(s)) return 0
|
|
313
|
+
const t = Date.parse(s + (s.length === 10 ? "T00:00:00" : "") + "+08:00")
|
|
314
|
+
return Number.isFinite(t) ? t : 0
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// termOf is how long a GLM Coding plan is paid for: the valid
|
|
318
|
+
// subscription's next renewal, a charge when it renews itself, else the
|
|
319
|
+
// end of its time, the last date its "valid" span names.
|
|
320
|
+
function termOf(subs) {
|
|
321
|
+
for (const x of Array.isArray(subs) ? subs : []) {
|
|
322
|
+
if (String(x?.status ?? "").toUpperCase() !== "VALID") continue
|
|
323
|
+
const auto = x.autoRenew === true || x.autoRenew === 1
|
|
324
|
+
const t = beijing(typeof x.nextRenewTime === "string" ? x.nextRenewTime : "")
|
|
325
|
+
if (t) return { until: new Date(t).toISOString(), renew: auto ? "auto" : "off" }
|
|
326
|
+
const ds = String(typeof x.valid === "string" ? x.valid : "").match(/\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}:\d{2})?/g) ?? []
|
|
327
|
+
if (ds.length && !auto) {
|
|
328
|
+
const end = beijing(ds[ds.length - 1])
|
|
329
|
+
if (end) return { until: new Date(end).toISOString(), renew: "off" }
|
|
330
|
+
}
|
|
331
|
+
return {}
|
|
332
|
+
}
|
|
333
|
+
return {}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// codingUsage is a Coding Plan key's allowance: credits per five hours and
|
|
337
|
+
// per week, the plan's level and its term.
|
|
338
|
+
async function codingUsage(s) {
|
|
339
|
+
const root = rootOf(s.base)
|
|
340
|
+
const d = (await call("GET", root + "/api/monitor/usage/quota/limit", { auth: s.key })) ?? {}
|
|
341
|
+
const out = {}
|
|
342
|
+
const level = typeof d.level === "string" ? d.level : ""
|
|
343
|
+
if (level) out.plan = "GLM Coding " + level[0].toUpperCase() + level.slice(1)
|
|
344
|
+
out.windows = limitWindows(d)
|
|
345
|
+
try {
|
|
346
|
+
Object.assign(out, termOf(await call("GET", root + "/api/biz/subscription/list", { auth: s.key })))
|
|
347
|
+
} catch {}
|
|
348
|
+
return out
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// startNum reads a number the balance gives as a number or a string.
|
|
352
|
+
function startNum(v) {
|
|
353
|
+
if (isNum(v)) return v
|
|
354
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
355
|
+
const n = Number(v.trim())
|
|
356
|
+
if (!Number.isNaN(n)) return n
|
|
357
|
+
}
|
|
358
|
+
return undefined
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// startUsage is the Start Plan's allowance: a window for each of its
|
|
362
|
+
// buckets, a model's tokens for the day or for the plan's time. A plan
|
|
363
|
+
// still "active" past its end is over, and its buckets are left out.
|
|
364
|
+
async function startUsage(s) {
|
|
365
|
+
if (!s.jwt) throw new Error("not signed in to ZCode")
|
|
366
|
+
if (jwtExpired(s.jwt)) throw new Error(EXPIRED)
|
|
367
|
+
const b = (await call("GET", `${ZCODE}/api/v1/zcode-plan/billing/balance?app_version=${APP_VERSION}`, { auth: "Bearer " + s.jwt, device: s.device })) ?? {}
|
|
368
|
+
const str = (v) => (typeof v === "string" ? v : "")
|
|
369
|
+
const plans = (Array.isArray(b.plans) ? b.plans : []).map((p) => ({ ...p, status: str(p?.status), user_plan_id: str(p?.user_plan_id), plan_id: str(p?.plan_id) }))
|
|
370
|
+
const now = startNum(b.server_time) > 0 ? startNum(b.server_time) : Math.trunc(Date.now() / 1000)
|
|
371
|
+
const over = new Set()
|
|
372
|
+
for (const p of plans) {
|
|
373
|
+
const end = startNum(p.ends_at)
|
|
374
|
+
if (end > 0 && end <= now && p.status.trim().toLowerCase() === "active") p.status = "expired"
|
|
375
|
+
if (p.status.trim().toLowerCase() === "expired") over.add(p.user_plan_id + "\0" + p.plan_id)
|
|
376
|
+
}
|
|
377
|
+
const same = (x, p) => (x.user_plan_id && p.user_plan_id ? x.user_plan_id === p.user_plan_id : x.plan_id === p.plan_id)
|
|
378
|
+
const balances = (Array.isArray(b.balances) ? b.balances : [])
|
|
379
|
+
.map((x) => ({ ...x, user_plan_id: str(x?.user_plan_id), plan_id: str(x?.plan_id) }))
|
|
380
|
+
.filter((x) => {
|
|
381
|
+
let keep = true
|
|
382
|
+
for (const p of plans) {
|
|
383
|
+
if (!same(x, p)) continue
|
|
384
|
+
keep = !over.has(p.user_plan_id + "\0" + p.plan_id)
|
|
385
|
+
if (keep) break
|
|
386
|
+
}
|
|
387
|
+
return keep
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
let active = null
|
|
391
|
+
for (const p of plans) {
|
|
392
|
+
if (p.status.trim().toLowerCase() !== "active") continue
|
|
393
|
+
const id = p.plan_id.trim().toLowerCase(), n = str(p.name).trim().toLowerCase()
|
|
394
|
+
const isStart = (x) => x.includes("start-plan") || x.includes("start plan")
|
|
395
|
+
if ((id || n) && !isStart(id) && !isStart(n)) continue
|
|
396
|
+
const end = startNum(p.ends_at)
|
|
397
|
+
active = { name: first(str(p.name), "Start Plan"), until: end > 0 ? new Date(Math.trunc(end) * 1000).toISOString() : "" }
|
|
398
|
+
break
|
|
399
|
+
}
|
|
400
|
+
if (!active) return { error: "this account has no GLM Coding Plan, and ZCode's Start Plan has ended or was never started" }
|
|
401
|
+
const out = { plan: active.name, windows: [] }
|
|
402
|
+
if (active.until) Object.assign(out, { until: active.until, renew: "off" })
|
|
403
|
+
for (const x of balances) {
|
|
404
|
+
const total = startNum(x.total_units), left = startNum(x.remaining_units)
|
|
405
|
+
let used = startNum(x.used_units)
|
|
406
|
+
if (total === undefined && used === undefined && left === undefined) continue
|
|
407
|
+
const models = (Array.isArray(x.capabilities) ? x.capabilities : [])
|
|
408
|
+
.map((c) => String(c ?? "").trim().replace(/^model:/, "").trim()).filter(Boolean)
|
|
409
|
+
const w = { name: first(str(x.show_name), models.join(", "), "Credits"), used: 0 }
|
|
410
|
+
if (used === undefined) used = total !== undefined && left !== undefined ? total - left : 0
|
|
411
|
+
if (total > 0) {
|
|
412
|
+
w.used = (100 * used) / total
|
|
413
|
+
w.display = `${compact(used)} / ${compact(total)}`
|
|
414
|
+
}
|
|
415
|
+
const exp = startNum(x.expires_at)
|
|
416
|
+
if (exp > 0) w.resetsAt = new Date(Math.trunc(exp) * 1000).toISOString()
|
|
417
|
+
let span = 0
|
|
418
|
+
for (const p of plans) {
|
|
419
|
+
if (!same(x, p)) continue
|
|
420
|
+
for (const e of Array.isArray(p.entitlements) ? p.entitlements : []) {
|
|
421
|
+
if (str(e?.entitlement_id) === str(x.entitlement_id)) span = periodOf(str(e?.period))
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (!span) {
|
|
425
|
+
const a = startNum(x.period_start), z = startNum(x.period_end)
|
|
426
|
+
if (a !== undefined && z !== undefined && z > a) span = Math.trunc(z - a)
|
|
427
|
+
}
|
|
428
|
+
if (span) w.span = span
|
|
429
|
+
if (models.length) w.models = models
|
|
430
|
+
out.windows.push(w)
|
|
431
|
+
}
|
|
432
|
+
return out
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// periodOf reads an entitlement's period: "daily", "weekly", "monthly".
|
|
436
|
+
function periodOf(p) {
|
|
437
|
+
p = p.toLowerCase()
|
|
438
|
+
if (p.includes("day") || p.includes("daily")) return 86400
|
|
439
|
+
if (p.includes("week")) return 7 * 86400
|
|
440
|
+
if (p.includes("month")) return 30 * 86400
|
|
441
|
+
return 0
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// when is a team plan's end: a time in Beijing, or a Unix time in seconds
|
|
445
|
+
// or milliseconds; ms, or 0.
|
|
446
|
+
function when(v) {
|
|
447
|
+
if (typeof v === "string") {
|
|
448
|
+
const t = beijing(v)
|
|
449
|
+
if (t) return t
|
|
450
|
+
}
|
|
451
|
+
const n = startNum(v)
|
|
452
|
+
if (n > 0) return n > 1e12 ? Math.trunc(n) : Math.trunc(n) * 1000
|
|
453
|
+
return 0
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// teamUsage is a team seat's allowance: the team plan's five hours and
|
|
457
|
+
// week, and the plan's name and end.
|
|
458
|
+
async function teamUsage(s, key) {
|
|
459
|
+
const root = bizRoot(s.base)
|
|
460
|
+
const headers = teamHeaders(s.base, s.org, s.project)
|
|
461
|
+
const d = (await call("GET", root + "/api/monitor/usage/quota/limit?type=2", { auth: key, headers })) ?? {}
|
|
462
|
+
const out = { windows: limitWindows(d) }
|
|
463
|
+
if (!s.token) return out
|
|
464
|
+
const resets = teamResets(root, s.token, headers)
|
|
465
|
+
try {
|
|
466
|
+
const t = (await call("GET", root + "/api/biz/team/subscribe/product/querySubscribeDetail", { auth: s.token, headers })) ?? {}
|
|
467
|
+
const usable = (t.hasSubscription == null || t.hasSubscription === true) && String(t.status ?? "").toUpperCase() === "EFFECTIVE" &&
|
|
468
|
+
String(t.memberGrantStatus ?? "").toUpperCase() === "VALID"
|
|
469
|
+
if (usable) {
|
|
470
|
+
if (typeof t.productName === "string" && t.productName) out.plan = t.productName
|
|
471
|
+
const end = when(t.subscribeEndTime)
|
|
472
|
+
if (end) Object.assign(out, { until: new Date(end).toISOString(), renew: "off" })
|
|
473
|
+
}
|
|
474
|
+
} catch {}
|
|
475
|
+
const r = await resets
|
|
476
|
+
if (r) out.resets = r
|
|
477
|
+
return out
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// teamResets is how many resets a team member may spend, of the five hours
|
|
481
|
+
// and of the week: those of customer-package-reset/list still available.
|
|
482
|
+
// null when it can't be told or there are none.
|
|
483
|
+
async function teamResets(root, auth, headers) {
|
|
484
|
+
let d
|
|
485
|
+
try {
|
|
486
|
+
d = (await call("GET", root + "/api/biz/customer-package-reset/list?targetType=TEAM", { auth, headers })) ?? {}
|
|
487
|
+
} catch {
|
|
488
|
+
return null
|
|
489
|
+
}
|
|
490
|
+
let until = 0
|
|
491
|
+
const count = (rs) => {
|
|
492
|
+
let n = 0
|
|
493
|
+
for (const x of Array.isArray(rs) ? rs : []) {
|
|
494
|
+
if (x?.available !== true) continue
|
|
495
|
+
n++
|
|
496
|
+
const t = when(x.expireTime)
|
|
497
|
+
if (t && (!until || t < until)) until = t
|
|
498
|
+
}
|
|
499
|
+
return n
|
|
500
|
+
}
|
|
501
|
+
const fiveHour = count(d.fiveHourResets)
|
|
502
|
+
const weekly = count(d.weekResets)
|
|
503
|
+
if (fiveHour + weekly === 0) return null
|
|
504
|
+
const r = { count: fiveHour + weekly, byWindow: true, fiveHour, weekly }
|
|
505
|
+
if (until) r.until = new Date(until).toISOString()
|
|
506
|
+
return r
|
|
507
|
+
}
|
|
508
|
+
|
|
239
509
|
// ---- signing in ------------------------------------------------------------------
|
|
240
510
|
|
|
241
511
|
// bizAuth is the Authorization a sign-in gives the business API: Z.ai's
|
|
@@ -687,6 +957,29 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
687
957
|
},
|
|
688
958
|
}
|
|
689
959
|
},
|
|
960
|
+
// the plan's allowance, as magpie's built-in showed it
|
|
961
|
+
async usage(getAuth, provider) {
|
|
962
|
+
const auth = await getAuth()
|
|
963
|
+
const s = stateOf(auth)
|
|
964
|
+
if (!s) return { error: "not signed in" }
|
|
965
|
+
try {
|
|
966
|
+
if (isTeam(s)) {
|
|
967
|
+
let key = s.key
|
|
968
|
+
if (!key) {
|
|
969
|
+
key = await teamKey(s)
|
|
970
|
+
if (auth?.type === "oauth") {
|
|
971
|
+
const next = { ...auth, refresh: JSON.stringify({ ...s, key }), access: key }
|
|
972
|
+
await client?.auth?.set?.({ path: { id: provider?.id ?? PROVIDER }, body: next }).catch?.(() => {})
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return await teamUsage(s, key)
|
|
976
|
+
}
|
|
977
|
+
if (await onStart(s)) return await startUsage(s)
|
|
978
|
+
return await codingUsage(s)
|
|
979
|
+
} catch (e) {
|
|
980
|
+
return { error: e?.message ?? String(e) }
|
|
981
|
+
}
|
|
982
|
+
},
|
|
690
983
|
methods: [
|
|
691
984
|
oauth("zai"),
|
|
692
985
|
oauth("bigmodel"),
|
|
@@ -709,3 +1002,6 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
709
1002
|
},
|
|
710
1003
|
}
|
|
711
1004
|
}
|
|
1005
|
+
|
|
1006
|
+
// for tests
|
|
1007
|
+
export const _internal = { limitWindows, termOf, startUsage, routes, teamKeys }
|
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.1",
|
|
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",
|