@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.
Files changed (3) hide show
  1. package/README.md +11 -4
  2. package/index.mjs +108 -46
  3. 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. It is
33
- also renewed once when the server turns a request away: a 401, or a Xiaomi
34
- sign-in page where an answer was expected. If the passToken no longer works,
35
- sign in again.
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
- res = await fetch(url, { headers, redirect: "manual", signal })
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
- // fresh is the account with a live session, signed on again when it is
338
- // older than the app lets one get, or when force is set (the server
339
- // turned the last one away)
340
- const fresh = async (getAuth, force) => {
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
- try {
349
- const s = await session(a.creds, a.creds.base)
350
- const issued = Date.now()
351
- await save(toAuth(a.creds, s.cookies, issued))
352
- return { creds: a.creds, cookies: s.cookies, expires: issued + RENEW_AFTER }
353
- } catch (e) {
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
- const page = async (getAuth, path) => {
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) throw new SignInGone(`${fromAuth(await getAuth())?.creds.userId}: the Xiaomi MiMo sign-in has expired — sign in again`)
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
- const send = async (force) => {
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
- const res = await send(false)
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 account's
489
- // own 401, in the built-in's words (mimoLapse)
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: "authentication_error", code: null } }), { status: 401, headers: { "content-type": "application/json" } })
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.4",
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
  }