@magpie-community/opencode-mimo-auth 0.1.4 → 0.1.5

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 +5 -4
  2. package/index.mjs +53 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -29,10 +29,11 @@ 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. A model
33
+ request the server turns away (a 401) is answered as the server answered it,
34
+ as magpie's built-in MiMo account did; reading usage renews the session once
35
+ when the server turns it away. If the passToken no longer works, sign in
36
+ again.
36
37
 
37
38
  ## Where the sign-in is kept
38
39
 
package/index.mjs CHANGED
@@ -117,7 +117,11 @@ async function session(creds, base) {
117
117
  const headers = appHeaders()
118
118
  const cookie = jar.header(url)
119
119
  if (cookie) headers.Cookie = cookie
120
- res = await fetch(url, { headers, redirect: "manual", signal })
120
+ try {
121
+ res = await fetch(url, { headers, redirect: "manual", signal })
122
+ } catch (e) {
123
+ throw new Error("Xiaomi MiMo sign-in: " + (e?.message ?? e))
124
+ }
121
125
  jar.take(res, url)
122
126
  const loc = res.headers.get("location")
123
127
  if (res.status < 300 || res.status >= 400 || !loc) break
@@ -184,6 +188,15 @@ function fromAuth(auth) {
184
188
  }
185
189
  }
186
190
 
191
+ // said is an answer with magpie's X-Magpie-Sign-In, which tells what it
192
+ // means for the account's sign-in whatever its status: "expired" marks it
193
+ // lapsed, "kept" leaves it be. magpie takes it off before the agent sees it.
194
+ function said(res, v) {
195
+ const h = new Headers(res.headers)
196
+ h.set("X-Magpie-Sign-In", v)
197
+ return new Response(res.body, { status: res.status, statusText: res.statusText, headers: h })
198
+ }
199
+
187
200
  // ---- the sign-in ------------------------------------------------------------
188
201
 
189
202
  // serviceOf is the sid and callback the MiMo server sends a browser with
@@ -336,7 +349,8 @@ export const MimoAuthPlugin = async ({ client }) => {
336
349
 
337
350
  // fresh is the account with a live session, signed on again when it is
338
351
  // older than the app lets one get, or when force is set (the server
339
- // turned the last one away)
352
+ // turned the last one away); renewed when it was signed on again, which
353
+ // took the built-in's lapse mark off
340
354
  const fresh = async (getAuth, force) => {
341
355
  const a = fromAuth(await getAuth())
342
356
  if (!a) throw new Error("Xiaomi MiMo: not signed in")
@@ -349,7 +363,7 @@ export const MimoAuthPlugin = async ({ client }) => {
349
363
  const s = await session(a.creds, a.creds.base)
350
364
  const issued = Date.now()
351
365
  await save(toAuth(a.creds, s.cookies, issued))
352
- return { creds: a.creds, cookies: s.cookies, expires: issued + RENEW_AFTER }
366
+ return { creds: a.creds, cookies: s.cookies, expires: issued + RENEW_AFTER, renewed: true }
353
367
  } catch (e) {
354
368
  // a hiccup: the session in hand may still do
355
369
  if (!(e instanceof Lapsed) && !force && Object.keys(a.cookies).length) return a
@@ -364,16 +378,22 @@ export const MimoAuthPlugin = async ({ client }) => {
364
378
  }
365
379
 
366
380
  // 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) => {
381
+ // again once when it is turned away, and gives its data. read.signIn is
382
+ // what that did to the built-in's lapse mark: signed on again took it
383
+ // off, a sign-in Xiaomi no longer takes (mimoLapse) put it on.
384
+ const page = async (getAuth, path, read) => {
369
385
  for (let t = 0; ; t++) {
370
386
  let s
371
387
  try {
372
388
  s = await fresh(getAuth, t > 0)
373
389
  } 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`)
390
+ if (e instanceof Lapsed) {
391
+ read.signIn = "expired"
392
+ throw new SignInGone(`${fromAuth(await getAuth())?.creds.userId}: the Xiaomi MiMo sign-in has expired — sign in again`)
393
+ }
375
394
  throw e
376
395
  }
396
+ if (s.renewed) read.signIn = "renewed"
377
397
  const res = await fetch(s.creds.base.replace(/\/+$/, "") + path, {
378
398
  headers: { Cookie: cookieHeader(s.cookies), ...appHeaders() },
379
399
  signal: AbortSignal.timeout(20_000),
@@ -387,6 +407,7 @@ export const MimoAuthPlugin = async ({ client }) => {
387
407
  if (res.status === 401 || (res.status === 200 && bad)) {
388
408
  // a session gone stale, or a redirect to Xiaomi's sign-in
389
409
  if (t === 0) continue
410
+ read.signIn = "expired"
390
411
  throw new SignInGone(`${s.creds.userId}: the Xiaomi MiMo sign-in has expired — sign in again`)
391
412
  }
392
413
  if (res.status !== 200 || bad) throw new Error(`Xiaomi MiMo ${path}: ${vendorError(text, statusLine(res.status))}`)
@@ -395,13 +416,16 @@ export const MimoAuthPlugin = async ({ client }) => {
395
416
  }
396
417
  }
397
418
 
398
- // usage is the plan and the week's allowance, magpie's own hook
419
+ // usage is the plan and the week's allowance, magpie's own hook; signIn
420
+ // is the lapse mark as the built-in's reads left it, a clean read with
421
+ // no signing on again leaving it be
399
422
  const usage = async (getAuth) => {
423
+ const read = { signIn: "kept" }
400
424
  let self
401
425
  try {
402
- self = await page(getAuth, "/user/xiaomi/subscription/self")
426
+ self = await page(getAuth, "/user/xiaomi/subscription/self", read)
403
427
  } catch (e) {
404
- return { error: e?.message ?? String(e) }
428
+ return { error: e?.message ?? String(e), signIn: read.signIn }
405
429
  }
406
430
  const c = self?.current
407
431
  const out = { plan: "Free" }
@@ -415,12 +439,13 @@ export const MimoAuthPlugin = async ({ client }) => {
415
439
  }
416
440
  let use
417
441
  try {
418
- use = await page(getAuth, "/user/usage")
442
+ use = await page(getAuth, "/user/usage", read)
419
443
  } catch (e) {
420
444
  if (e instanceof SignInGone) out.error = e.message
421
445
  // the plan alone, when the week's allowance can't be read
422
- return out
446
+ return { ...out, signIn: read.signIn }
423
447
  }
448
+ out.signIn = read.signIn
424
449
  const percent = use?.percent
425
450
  const reset = use?.resetDate
426
451
  // no plan: the free offer shows no allowance
@@ -467,31 +492,29 @@ export const MimoAuthPlugin = async ({ client }) => {
467
492
  if (j?.model === "mimo-auto") body = JSON.stringify({ ...j, model: "mimo-pro" })
468
493
  } catch {}
469
494
  }
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
- }
495
+ let s
480
496
  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)
497
+ s = await fresh(getAuth, false)
487
498
  } catch (e) {
488
- // the passToken no longer signs the account on: the account's
489
- // own 401, in the built-in's words (mimoLapse)
499
+ // the passToken no longer signs the account on: the built-in
500
+ // failed the request (magpie's 502) in mimoLapse's words and
501
+ // marked the account lapsed
490
502
  if (!(e instanceof Lapsed)) throw e
491
503
  const who = fromAuth(await getAuth())?.creds.userId ?? a.creds.userId
492
504
  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" } })
505
+ return said(new Response(JSON.stringify({ error: { message, type: "api_error", code: null } }), { status: 502, headers: { "content-type": "application/json" } }), "expired")
494
506
  }
507
+ const headers = new Headers(init.headers ?? req?.headers)
508
+ headers.delete("authorization")
509
+ headers.set("Cookie", cookieHeader(s.cookies))
510
+ headers.set("X-Mimo-Source", SOURCE)
511
+ headers.set("User-Agent", UA)
512
+ headers.set("X-Client-Version", APP_VERSION)
513
+ // the server's answer goes through as it is, a 401 too, as the
514
+ // built-in's did: it took the lapse off only when it signed on
515
+ // again, whatever the server then answered
516
+ const res = await fetch(url, { ...init, method: init.method ?? req?.method ?? "POST", headers, body })
517
+ return said(res, s.renewed ? "renewed" : "kept")
495
518
  },
496
519
  }
497
520
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-mimo-auth",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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",