@magpie-community/opencode-mimo-auth 0.1.4 → 0.1.6
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 +11 -4
- package/index.mjs +108 -46
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -29,10 +29,17 @@ Chat completions go to `https://mimo-server-<region>.xiaomimimo.com/api/route/ch
|
|
|
29
29
|
|
|
30
30
|
`mimo-auto`, the app's default model, is requested as `mimo-pro`.
|
|
31
31
|
|
|
32
|
-
The session is renewed with the passToken a day after it was issued.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
The session is renewed with the passToken a day after it was issued.
|
|
33
|
+
magpie (0.1.684 and later) does it ahead of time through `auth.refresh`, ten
|
|
34
|
+
minutes before that day is out (`refreshLead`), once for the account and
|
|
35
|
+
before its requests, models and usage ask for it; a passToken Xiaomi no
|
|
36
|
+
longer takes marks the account for a new sign-in. The check before each
|
|
37
|
+
request stays for OpenCode, which doesn't call `auth.refresh`, and one
|
|
38
|
+
sign-on at a time serves both. A model
|
|
39
|
+
request the server turns away (a 401) is answered as the server answered it,
|
|
40
|
+
as magpie's built-in MiMo account did; reading usage renews the session once
|
|
41
|
+
when the server turns it away. If the passToken no longer works, sign in
|
|
42
|
+
again.
|
|
36
43
|
|
|
37
44
|
## Where the sign-in is kept
|
|
38
45
|
|
package/index.mjs
CHANGED
|
@@ -18,6 +18,7 @@ const APP_VERSION = "26.929.292248"
|
|
|
18
18
|
const SOURCE = "mimocode-cli-free"
|
|
19
19
|
const UA = "miNative PC/Normal Windows_NT/10.0.26100 SDKV/1.0.0 DEVT/PC DEVS/Windows APP/miaccount_desktop APPV/0.1.0"
|
|
20
20
|
const RENEW_AFTER = 24 * 60 * 60 * 1000 // the app signs on again after a day
|
|
21
|
+
const LEAD_MS = 10 * 60 * 1000 // this close to it, magpie signs on again ahead of time (auth.refresh)
|
|
21
22
|
const SIGN_IN_LIFE = 10 * 60 * 1000
|
|
22
23
|
|
|
23
24
|
const MODEL = { release_date: "2026-07-01", attachment: true, tool_call: true, limit: { context: 1_000_000, output: 128_000 }, modalities: { input: ["text", "image"], output: ["text"] } }
|
|
@@ -117,7 +118,11 @@ async function session(creds, base) {
|
|
|
117
118
|
const headers = appHeaders()
|
|
118
119
|
const cookie = jar.header(url)
|
|
119
120
|
if (cookie) headers.Cookie = cookie
|
|
120
|
-
|
|
121
|
+
try {
|
|
122
|
+
res = await fetch(url, { headers, redirect: "manual", signal })
|
|
123
|
+
} catch (e) {
|
|
124
|
+
throw new Error("Xiaomi MiMo sign-in: " + (e?.message ?? e))
|
|
125
|
+
}
|
|
121
126
|
jar.take(res, url)
|
|
122
127
|
const loc = res.headers.get("location")
|
|
123
128
|
if (res.status < 300 || res.status >= 400 || !loc) break
|
|
@@ -184,6 +189,15 @@ function fromAuth(auth) {
|
|
|
184
189
|
}
|
|
185
190
|
}
|
|
186
191
|
|
|
192
|
+
// said is an answer with magpie's X-Magpie-Sign-In, which tells what it
|
|
193
|
+
// means for the account's sign-in whatever its status: "expired" marks it
|
|
194
|
+
// lapsed, "kept" leaves it be. magpie takes it off before the agent sees it.
|
|
195
|
+
function said(res, v) {
|
|
196
|
+
const h = new Headers(res.headers)
|
|
197
|
+
h.set("X-Magpie-Sign-In", v)
|
|
198
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers: h })
|
|
199
|
+
}
|
|
200
|
+
|
|
187
201
|
// ---- the sign-in ------------------------------------------------------------
|
|
188
202
|
|
|
189
203
|
// serviceOf is the sid and callback the MiMo server sends a browser with
|
|
@@ -325,8 +339,11 @@ function serverTime(v) {
|
|
|
325
339
|
|
|
326
340
|
export const MimoAuthPlugin = async ({ client }) => {
|
|
327
341
|
// renewals under way, one to an account: two accounts signing on at
|
|
328
|
-
// once each keep their own session
|
|
342
|
+
// once each keep their own session. renewed is the last session each
|
|
343
|
+
// account got here, so magpie's auth.refresh handed a sign-in from
|
|
344
|
+
// before it gives that one rather than signing on again.
|
|
329
345
|
const renewing = new Map()
|
|
346
|
+
const renewed = new Map()
|
|
330
347
|
|
|
331
348
|
const save = async (auth) => {
|
|
332
349
|
try {
|
|
@@ -334,27 +351,20 @@ export const MimoAuthPlugin = async ({ client }) => {
|
|
|
334
351
|
} catch {}
|
|
335
352
|
}
|
|
336
353
|
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
const
|
|
341
|
-
const a = fromAuth(await getAuth())
|
|
342
|
-
if (!a) throw new Error("Xiaomi MiMo: not signed in")
|
|
343
|
-
if (!force && Object.keys(a.cookies).length && Date.now() < a.expires) return a
|
|
354
|
+
// renew signs the account on again, once at a time: those who come
|
|
355
|
+
// while it runs wait for it. keep saves the new session (the request
|
|
356
|
+
// path); magpie's auth.refresh saves what it gives itself.
|
|
357
|
+
const renew = (a, keep) => {
|
|
344
358
|
const who = String(a.creds.userId ?? "")
|
|
345
359
|
let r = renewing.get(who)
|
|
346
360
|
if (!r) {
|
|
347
361
|
r = (async () => {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
// a hiccup: the session in hand may still do
|
|
355
|
-
if (!(e instanceof Lapsed) && !force && Object.keys(a.cookies).length) return a
|
|
356
|
-
throw e
|
|
357
|
-
}
|
|
362
|
+
const s = await session(a.creds, a.creds.base)
|
|
363
|
+
const issued = Date.now()
|
|
364
|
+
const got = { creds: a.creds, cookies: s.cookies, expires: issued + RENEW_AFTER, renewed: true }
|
|
365
|
+
renewed.set(who, got)
|
|
366
|
+
if (keep) await save(toAuth(a.creds, s.cookies, issued))
|
|
367
|
+
return got
|
|
358
368
|
})().finally(() => {
|
|
359
369
|
renewing.delete(who)
|
|
360
370
|
})
|
|
@@ -363,17 +373,61 @@ export const MimoAuthPlugin = async ({ client }) => {
|
|
|
363
373
|
return r
|
|
364
374
|
}
|
|
365
375
|
|
|
376
|
+
// fresh is the account with a live session, signed on again when it is
|
|
377
|
+
// older than the app lets one get, or when force is set (the server
|
|
378
|
+
// turned the last one away); renewed when it was signed on again, which
|
|
379
|
+
// took the built-in's lapse mark off
|
|
380
|
+
const fresh = async (getAuth, force) => {
|
|
381
|
+
const a = fromAuth(await getAuth())
|
|
382
|
+
if (!a) throw new Error("Xiaomi MiMo: not signed in")
|
|
383
|
+
if (!force && Object.keys(a.cookies).length && Date.now() < a.expires) return a
|
|
384
|
+
try {
|
|
385
|
+
return await renew(a, true)
|
|
386
|
+
} catch (e) {
|
|
387
|
+
// a hiccup: the session in hand may still do
|
|
388
|
+
if (!(e instanceof Lapsed) && !force && Object.keys(a.cookies).length) return a
|
|
389
|
+
throw e
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// refresh is magpie's auth.refresh: the session signed on again
|
|
394
|
+
// LEAD_MS before the app would, as the fields that changed (the Xiaomi
|
|
395
|
+
// account in refresh stays as it is). A passToken Xiaomi no longer takes
|
|
396
|
+
// is signIn "expired"; any other failure throws as it is.
|
|
397
|
+
const refresh = async (auth) => {
|
|
398
|
+
const a = fromAuth(auth)
|
|
399
|
+
if (!a) return undefined
|
|
400
|
+
const last = renewed.get(String(a.creds.userId ?? ""))
|
|
401
|
+
// signed on again here already, the store not yet saying so
|
|
402
|
+
if (last && last.expires > a.expires && Date.now() < last.expires - LEAD_MS) {
|
|
403
|
+
return { access: JSON.stringify(last.cookies), expires: last.expires }
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const s = await renew(a, false)
|
|
407
|
+
return { access: JSON.stringify(s.cookies), expires: s.expires }
|
|
408
|
+
} catch (e) {
|
|
409
|
+
if (e instanceof Lapsed) throw Object.assign(new Error(e.message), { signIn: "expired" })
|
|
410
|
+
throw e
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
366
414
|
// page asks the MiMo server for one of the account's pages, signing on
|
|
367
|
-
// again once when it is turned away, and gives its data.
|
|
368
|
-
|
|
415
|
+
// again once when it is turned away, and gives its data. read.signIn is
|
|
416
|
+
// what that did to the built-in's lapse mark: signed on again took it
|
|
417
|
+
// off, a sign-in Xiaomi no longer takes (mimoLapse) put it on.
|
|
418
|
+
const page = async (getAuth, path, read) => {
|
|
369
419
|
for (let t = 0; ; t++) {
|
|
370
420
|
let s
|
|
371
421
|
try {
|
|
372
422
|
s = await fresh(getAuth, t > 0)
|
|
373
423
|
} catch (e) {
|
|
374
|
-
if (e instanceof Lapsed)
|
|
424
|
+
if (e instanceof Lapsed) {
|
|
425
|
+
read.signIn = "expired"
|
|
426
|
+
throw new SignInGone(`${fromAuth(await getAuth())?.creds.userId}: the Xiaomi MiMo sign-in has expired — sign in again`)
|
|
427
|
+
}
|
|
375
428
|
throw e
|
|
376
429
|
}
|
|
430
|
+
if (s.renewed) read.signIn = "renewed"
|
|
377
431
|
const res = await fetch(s.creds.base.replace(/\/+$/, "") + path, {
|
|
378
432
|
headers: { Cookie: cookieHeader(s.cookies), ...appHeaders() },
|
|
379
433
|
signal: AbortSignal.timeout(20_000),
|
|
@@ -387,6 +441,7 @@ export const MimoAuthPlugin = async ({ client }) => {
|
|
|
387
441
|
if (res.status === 401 || (res.status === 200 && bad)) {
|
|
388
442
|
// a session gone stale, or a redirect to Xiaomi's sign-in
|
|
389
443
|
if (t === 0) continue
|
|
444
|
+
read.signIn = "expired"
|
|
390
445
|
throw new SignInGone(`${s.creds.userId}: the Xiaomi MiMo sign-in has expired — sign in again`)
|
|
391
446
|
}
|
|
392
447
|
if (res.status !== 200 || bad) throw new Error(`Xiaomi MiMo ${path}: ${vendorError(text, statusLine(res.status))}`)
|
|
@@ -395,13 +450,16 @@ export const MimoAuthPlugin = async ({ client }) => {
|
|
|
395
450
|
}
|
|
396
451
|
}
|
|
397
452
|
|
|
398
|
-
// usage is the plan and the week's allowance, magpie's own hook
|
|
453
|
+
// usage is the plan and the week's allowance, magpie's own hook; signIn
|
|
454
|
+
// is the lapse mark as the built-in's reads left it, a clean read with
|
|
455
|
+
// no signing on again leaving it be
|
|
399
456
|
const usage = async (getAuth) => {
|
|
457
|
+
const read = { signIn: "kept" }
|
|
400
458
|
let self
|
|
401
459
|
try {
|
|
402
|
-
self = await page(getAuth, "/user/xiaomi/subscription/self")
|
|
460
|
+
self = await page(getAuth, "/user/xiaomi/subscription/self", read)
|
|
403
461
|
} catch (e) {
|
|
404
|
-
return { error: e?.message ?? String(e) }
|
|
462
|
+
return { error: e?.message ?? String(e), signIn: read.signIn }
|
|
405
463
|
}
|
|
406
464
|
const c = self?.current
|
|
407
465
|
const out = { plan: "Free" }
|
|
@@ -415,12 +473,13 @@ export const MimoAuthPlugin = async ({ client }) => {
|
|
|
415
473
|
}
|
|
416
474
|
let use
|
|
417
475
|
try {
|
|
418
|
-
use = await page(getAuth, "/user/usage")
|
|
476
|
+
use = await page(getAuth, "/user/usage", read)
|
|
419
477
|
} catch (e) {
|
|
420
478
|
if (e instanceof SignInGone) out.error = e.message
|
|
421
479
|
// the plan alone, when the week's allowance can't be read
|
|
422
|
-
return out
|
|
480
|
+
return { ...out, signIn: read.signIn }
|
|
423
481
|
}
|
|
482
|
+
out.signIn = read.signIn
|
|
424
483
|
const percent = use?.percent
|
|
425
484
|
const reset = use?.resetDate
|
|
426
485
|
// no plan: the free offer shows no allowance
|
|
@@ -447,6 +506,11 @@ export const MimoAuthPlugin = async ({ client }) => {
|
|
|
447
506
|
auth: {
|
|
448
507
|
provider: ID,
|
|
449
508
|
usage,
|
|
509
|
+
// magpie signs the account on again LEAD_MS before the day is out,
|
|
510
|
+
// once, before its requests, models and usage ask; fresh's check
|
|
511
|
+
// before each request stays for OpenCode, which doesn't call this
|
|
512
|
+
refreshLead: LEAD_MS,
|
|
513
|
+
refresh,
|
|
450
514
|
loader: async (getAuth) => {
|
|
451
515
|
const a = fromAuth(await getAuth())
|
|
452
516
|
if (!a) return {}
|
|
@@ -467,31 +531,29 @@ export const MimoAuthPlugin = async ({ client }) => {
|
|
|
467
531
|
if (j?.model === "mimo-auto") body = JSON.stringify({ ...j, model: "mimo-pro" })
|
|
468
532
|
} catch {}
|
|
469
533
|
}
|
|
470
|
-
|
|
471
|
-
const s = await fresh(getAuth, force)
|
|
472
|
-
const headers = new Headers(init.headers ?? req?.headers)
|
|
473
|
-
headers.delete("authorization")
|
|
474
|
-
headers.set("Cookie", cookieHeader(s.cookies))
|
|
475
|
-
headers.set("X-Mimo-Source", SOURCE)
|
|
476
|
-
headers.set("User-Agent", UA)
|
|
477
|
-
headers.set("X-Client-Version", APP_VERSION)
|
|
478
|
-
return fetch(url, { ...init, method: init.method ?? req?.method ?? "POST", headers, body })
|
|
479
|
-
}
|
|
534
|
+
let s
|
|
480
535
|
try {
|
|
481
|
-
|
|
482
|
-
// a session gone stale, or a redirect to Xiaomi's sign-in page
|
|
483
|
-
const stale = res.status === 401 || (res.status === 200 && /text\/html/i.test(res.headers.get("content-type") ?? ""))
|
|
484
|
-
if (!stale || (body != null && typeof body !== "string")) return res
|
|
485
|
-
await res.arrayBuffer().catch(() => {})
|
|
486
|
-
return await send(true)
|
|
536
|
+
s = await fresh(getAuth, false)
|
|
487
537
|
} catch (e) {
|
|
488
|
-
// the passToken no longer signs the account on: the
|
|
489
|
-
//
|
|
538
|
+
// the passToken no longer signs the account on: the built-in
|
|
539
|
+
// failed the request (magpie's 502) in mimoLapse's words and
|
|
540
|
+
// marked the account lapsed
|
|
490
541
|
if (!(e instanceof Lapsed)) throw e
|
|
491
542
|
const who = fromAuth(await getAuth())?.creds.userId ?? a.creds.userId
|
|
492
543
|
const message = `${who}: the Xiaomi MiMo sign-in has expired — sign in again`
|
|
493
|
-
return new Response(JSON.stringify({ error: { message, type: "
|
|
544
|
+
return said(new Response(JSON.stringify({ error: { message, type: "api_error", code: null } }), { status: 502, headers: { "content-type": "application/json" } }), "expired")
|
|
494
545
|
}
|
|
546
|
+
const headers = new Headers(init.headers ?? req?.headers)
|
|
547
|
+
headers.delete("authorization")
|
|
548
|
+
headers.set("Cookie", cookieHeader(s.cookies))
|
|
549
|
+
headers.set("X-Mimo-Source", SOURCE)
|
|
550
|
+
headers.set("User-Agent", UA)
|
|
551
|
+
headers.set("X-Client-Version", APP_VERSION)
|
|
552
|
+
// the server's answer goes through as it is, a 401 too, as the
|
|
553
|
+
// built-in's did: it took the lapse off only when it signed on
|
|
554
|
+
// again, whatever the server then answered
|
|
555
|
+
const res = await fetch(url, { ...init, method: init.method ?? req?.method ?? "POST", headers, body })
|
|
556
|
+
return said(res, s.renewed ? "renewed" : "kept")
|
|
495
557
|
},
|
|
496
558
|
}
|
|
497
559
|
},
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-mimo-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "OpenCode provider plugin for Xiaomi MiMo: the MiMo app's Xiaomi-account sign-in, MiMo Pro and MiMo Flash",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
7
7
|
"exports": "./index.mjs",
|
|
8
8
|
"files": ["index.mjs", "README.md"],
|
|
9
9
|
"keywords": ["opencode", "opencode-plugin", "xiaomi", "mimo"],
|
|
10
|
-
"license": "MIT"
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": { "type": "git", "url": "git+https://github.com/magpie-community/plugins.git", "directory": "packages/mimo" }
|
|
11
12
|
}
|