@magpie-community/opencode-mimo-auth 0.1.0 → 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/index.mjs +130 -2
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// MiMo server, which answers with session cookies. Model requests are
|
|
5
5
|
// chat completions at the server's /route, carrying those cookies.
|
|
6
6
|
import { randomBytes } from "node:crypto"
|
|
7
|
+
import { STATUS_CODES } from "node:http"
|
|
7
8
|
|
|
8
9
|
const ID = "mimo-app"
|
|
9
10
|
const HOSTS = {
|
|
@@ -266,6 +267,60 @@ async function signedInWith(p, device) {
|
|
|
266
267
|
return { creds: { ...creds, userId: String(s.me?.userId ?? "") || creds.userId }, cookies: s.cookies }
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
// ---- usage ------------------------------------------------------------------
|
|
271
|
+
//
|
|
272
|
+
// What the account says of its allowance, as magpie's built-in MiMo
|
|
273
|
+
// account shows it (internal/provider/mimo_usage.go), from the pages the
|
|
274
|
+
// app's "Usage & billing" reads: /user/usage, how much of the week's
|
|
275
|
+
// allowance is left (percent remaining, and the day it resets; no reset
|
|
276
|
+
// date is no plan), and /user/xiaomi/subscription/self, the plan in force
|
|
277
|
+
// and when it ends. An account with no plan is on MiMo's free offer.
|
|
278
|
+
|
|
279
|
+
// the app's names for its plans' tiers
|
|
280
|
+
const TIERS = { 1: "Starter", 2: "Plus", 3: "Pro", 4: "Ultra" }
|
|
281
|
+
|
|
282
|
+
// SignInGone is an account Xiaomi no longer signs in: it must be signed in
|
|
283
|
+
// again.
|
|
284
|
+
class SignInGone extends Error {}
|
|
285
|
+
|
|
286
|
+
// statusLine is a status as Go's HTTP client names it, "502 Bad Gateway".
|
|
287
|
+
const statusLine = (status) => `${status} ${STATUS_CODES[status] ?? ""}`.trim()
|
|
288
|
+
|
|
289
|
+
// vendorError is the message in an error body as magpie reads it: its
|
|
290
|
+
// {error: {message}} or {error}, {message}, {msg}, {detail}, else the body
|
|
291
|
+
// cut short after the status.
|
|
292
|
+
function vendorError(text, fallback) {
|
|
293
|
+
let v
|
|
294
|
+
try {
|
|
295
|
+
v = JSON.parse(text)
|
|
296
|
+
} catch {}
|
|
297
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
298
|
+
const err = v.detail !== undefined && v.detail !== null ? v.detail : v.error
|
|
299
|
+
if (Array.isArray(v.errors) && typeof v.errors[0]?.message === "string" && v.errors[0].message) return v.errors[0].message
|
|
300
|
+
if (err && typeof err === "object" && typeof err.message === "string" && err.message) return err.message
|
|
301
|
+
if (typeof err === "string" && err) return err
|
|
302
|
+
const m = (typeof v.message === "string" && v.message) || (typeof v.msg === "string" && v.msg)
|
|
303
|
+
if (m) return m
|
|
304
|
+
}
|
|
305
|
+
const s = String(text ?? "").split(/\s+/).filter(Boolean).join(" ")
|
|
306
|
+
if (!s || s.startsWith("<")) return fallback
|
|
307
|
+
const r = [...s]
|
|
308
|
+
return fallback + ": " + (r.length > 300 ? r.slice(0, 300).join("") + "…" : s)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// serverTime reads one of the server's times: "2026-10-01T00:00:00", or a
|
|
312
|
+
// day alone. They carry no zone, and the app shows them as they come,
|
|
313
|
+
// which is Beijing's (and Singapore's) time.
|
|
314
|
+
function serverTime(v) {
|
|
315
|
+
const s = String(v ?? "").trim()
|
|
316
|
+
let iso = ""
|
|
317
|
+
if (/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(Z|[+-]\d\d:\d\d)$/i.test(s)) iso = s
|
|
318
|
+
else if (/^\d{4}-\d\d-\d\d[T ]\d\d:\d\d:\d\d(\.\d+)?$/.test(s)) iso = s.replace(" ", "T") + "+08:00"
|
|
319
|
+
else if (/^\d{4}-\d\d-\d\d$/.test(s)) iso = s + "T00:00:00+08:00"
|
|
320
|
+
const t = iso ? Date.parse(iso) : NaN
|
|
321
|
+
return isNaN(t) ? undefined : new Date(t).toISOString()
|
|
322
|
+
}
|
|
323
|
+
|
|
269
324
|
// ---- the plugin -------------------------------------------------------------
|
|
270
325
|
|
|
271
326
|
export const MimoAuthPlugin = async ({ client }) => {
|
|
@@ -303,6 +358,75 @@ export const MimoAuthPlugin = async ({ client }) => {
|
|
|
303
358
|
return renewing
|
|
304
359
|
}
|
|
305
360
|
|
|
361
|
+
// page asks the MiMo server for one of the account's pages, signing on
|
|
362
|
+
// again once when it is turned away, and gives its data.
|
|
363
|
+
const page = async (getAuth, path) => {
|
|
364
|
+
for (let t = 0; ; t++) {
|
|
365
|
+
let s
|
|
366
|
+
try {
|
|
367
|
+
s = await fresh(getAuth, t > 0)
|
|
368
|
+
} catch (e) {
|
|
369
|
+
if (e instanceof Lapsed) throw new SignInGone(`${fromAuth(await getAuth())?.creds.userId}: the Xiaomi MiMo sign-in has expired — sign in again`)
|
|
370
|
+
throw e
|
|
371
|
+
}
|
|
372
|
+
const res = await fetch(s.creds.base.replace(/\/+$/, "") + path, {
|
|
373
|
+
headers: { Cookie: cookieHeader(s.cookies), ...appHeaders() },
|
|
374
|
+
signal: AbortSignal.timeout(20_000),
|
|
375
|
+
})
|
|
376
|
+
const text = await res.text()
|
|
377
|
+
let env
|
|
378
|
+
try {
|
|
379
|
+
env = JSON.parse(text)
|
|
380
|
+
} catch {}
|
|
381
|
+
const bad = !env || typeof env !== "object" || Array.isArray(env) || (env.code != null && !Number.isInteger(env.code)) || (env.msg != null && typeof env.msg !== "string")
|
|
382
|
+
if (res.status === 401 || (res.status === 200 && bad)) {
|
|
383
|
+
// a session gone stale, or a redirect to Xiaomi's sign-in
|
|
384
|
+
if (t === 0) continue
|
|
385
|
+
throw new SignInGone(`${s.creds.userId}: the Xiaomi MiMo sign-in has expired — sign in again`)
|
|
386
|
+
}
|
|
387
|
+
if (res.status !== 200 || bad) throw new Error(`Xiaomi MiMo ${path}: ${vendorError(text, statusLine(res.status))}`)
|
|
388
|
+
if ((env.code ?? 0) !== 0) throw new Error(`Xiaomi MiMo ${path}: code ${env.code} ${env.msg ?? ""}`)
|
|
389
|
+
return env.data
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// usage is the plan and the week's allowance, magpie's own hook
|
|
394
|
+
const usage = async (getAuth) => {
|
|
395
|
+
let self
|
|
396
|
+
try {
|
|
397
|
+
self = await page(getAuth, "/user/xiaomi/subscription/self")
|
|
398
|
+
} catch (e) {
|
|
399
|
+
return { error: e?.message ?? String(e) }
|
|
400
|
+
}
|
|
401
|
+
const c = self?.current
|
|
402
|
+
const out = { plan: "Free" }
|
|
403
|
+
if (c && typeof c === "object") {
|
|
404
|
+
const text = (v) => (typeof v === "string" ? v.trim() : "")
|
|
405
|
+
out.plan = text(c.title) || TIERS[c.planTier] || text(c.planCode) || "MiMo"
|
|
406
|
+
const until = serverTime(c.endTime)
|
|
407
|
+
if (until) out.until = until
|
|
408
|
+
if (c.renewalMode === "MONTHLY" || c.renewalMode === "YEARLY") out.renew = "auto"
|
|
409
|
+
else if (c.renewalMode === "ONE_TIME") out.renew = "off"
|
|
410
|
+
}
|
|
411
|
+
let use
|
|
412
|
+
try {
|
|
413
|
+
use = await page(getAuth, "/user/usage")
|
|
414
|
+
} catch (e) {
|
|
415
|
+
if (e instanceof SignInGone) out.error = e.message
|
|
416
|
+
// the plan alone, when the week's allowance can't be read
|
|
417
|
+
return out
|
|
418
|
+
}
|
|
419
|
+
const percent = use?.percent
|
|
420
|
+
const reset = use?.resetDate
|
|
421
|
+
// no plan: the free offer shows no allowance
|
|
422
|
+
if (typeof percent !== "number" || typeof reset !== "string") return out
|
|
423
|
+
const w = { name: "7 days", used: Math.max(0, Math.min(100, 100 - percent)), span: 7 * 24 * 3600 }
|
|
424
|
+
const at = serverTime(reset)
|
|
425
|
+
if (at) w.resetsAt = at
|
|
426
|
+
out.windows = [w]
|
|
427
|
+
return out
|
|
428
|
+
}
|
|
429
|
+
|
|
306
430
|
return {
|
|
307
431
|
config: async (config) => {
|
|
308
432
|
config.provider ??= {}
|
|
@@ -317,6 +441,7 @@ export const MimoAuthPlugin = async ({ client }) => {
|
|
|
317
441
|
},
|
|
318
442
|
auth: {
|
|
319
443
|
provider: ID,
|
|
444
|
+
usage,
|
|
320
445
|
loader: async (getAuth) => {
|
|
321
446
|
const a = fromAuth(await getAuth())
|
|
322
447
|
if (!a) return {}
|
|
@@ -368,8 +493,8 @@ export const MimoAuthPlugin = async ({ client }) => {
|
|
|
368
493
|
const p = await waitPoll(t)
|
|
369
494
|
const { creds, cookies } = await signedInWith(p, t.device)
|
|
370
495
|
return { ...toAuth(creds, cookies, Date.now()), type: "success" }
|
|
371
|
-
} catch {
|
|
372
|
-
return { type: "failed" }
|
|
496
|
+
} catch (e) {
|
|
497
|
+
return { type: "failed", error: e.message }
|
|
373
498
|
}
|
|
374
499
|
},
|
|
375
500
|
}
|
|
@@ -379,3 +504,6 @@ export const MimoAuthPlugin = async ({ client }) => {
|
|
|
379
504
|
},
|
|
380
505
|
}
|
|
381
506
|
}
|
|
507
|
+
|
|
508
|
+
// for tests
|
|
509
|
+
export const _internal = { serverTime, vendorError }
|
package/package.json
CHANGED