@magpie-community/opencode-mimo-auth 0.1.2 → 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 +71 -31
  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
@@ -324,7 +337,9 @@ function serverTime(v) {
324
337
  // ---- the plugin -------------------------------------------------------------
325
338
 
326
339
  export const MimoAuthPlugin = async ({ client }) => {
327
- let renewing = null
340
+ // renewals under way, one to an account: two accounts signing on at
341
+ // once each keep their own session
342
+ const renewing = new Map()
328
343
 
329
344
  const save = async (auth) => {
330
345
  try {
@@ -334,41 +349,51 @@ export const MimoAuthPlugin = async ({ client }) => {
334
349
 
335
350
  // fresh is the account with a live session, signed on again when it is
336
351
  // older than the app lets one get, or when force is set (the server
337
- // 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
338
354
  const fresh = async (getAuth, force) => {
339
355
  const a = fromAuth(await getAuth())
340
356
  if (!a) throw new Error("Xiaomi MiMo: not signed in")
341
357
  if (!force && Object.keys(a.cookies).length && Date.now() < a.expires) return a
342
- if (!renewing) {
343
- renewing = (async () => {
358
+ const who = String(a.creds.userId ?? "")
359
+ let r = renewing.get(who)
360
+ if (!r) {
361
+ r = (async () => {
344
362
  try {
345
363
  const s = await session(a.creds, a.creds.base)
346
364
  const issued = Date.now()
347
365
  await save(toAuth(a.creds, s.cookies, issued))
348
- 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 }
349
367
  } catch (e) {
350
368
  // a hiccup: the session in hand may still do
351
369
  if (!(e instanceof Lapsed) && !force && Object.keys(a.cookies).length) return a
352
370
  throw e
353
371
  }
354
372
  })().finally(() => {
355
- renewing = null
373
+ renewing.delete(who)
356
374
  })
375
+ renewing.set(who, r)
357
376
  }
358
- return renewing
377
+ return r
359
378
  }
360
379
 
361
380
  // 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) => {
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) => {
364
385
  for (let t = 0; ; t++) {
365
386
  let s
366
387
  try {
367
388
  s = await fresh(getAuth, t > 0)
368
389
  } 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`)
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
+ }
370
394
  throw e
371
395
  }
396
+ if (s.renewed) read.signIn = "renewed"
372
397
  const res = await fetch(s.creds.base.replace(/\/+$/, "") + path, {
373
398
  headers: { Cookie: cookieHeader(s.cookies), ...appHeaders() },
374
399
  signal: AbortSignal.timeout(20_000),
@@ -382,6 +407,7 @@ export const MimoAuthPlugin = async ({ client }) => {
382
407
  if (res.status === 401 || (res.status === 200 && bad)) {
383
408
  // a session gone stale, or a redirect to Xiaomi's sign-in
384
409
  if (t === 0) continue
410
+ read.signIn = "expired"
385
411
  throw new SignInGone(`${s.creds.userId}: the Xiaomi MiMo sign-in has expired — sign in again`)
386
412
  }
387
413
  if (res.status !== 200 || bad) throw new Error(`Xiaomi MiMo ${path}: ${vendorError(text, statusLine(res.status))}`)
@@ -390,13 +416,16 @@ export const MimoAuthPlugin = async ({ client }) => {
390
416
  }
391
417
  }
392
418
 
393
- // 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
394
422
  const usage = async (getAuth) => {
423
+ const read = { signIn: "kept" }
395
424
  let self
396
425
  try {
397
- self = await page(getAuth, "/user/xiaomi/subscription/self")
426
+ self = await page(getAuth, "/user/xiaomi/subscription/self", read)
398
427
  } catch (e) {
399
- return { error: e?.message ?? String(e) }
428
+ return { error: e?.message ?? String(e), signIn: read.signIn }
400
429
  }
401
430
  const c = self?.current
402
431
  const out = { plan: "Free" }
@@ -410,12 +439,13 @@ export const MimoAuthPlugin = async ({ client }) => {
410
439
  }
411
440
  let use
412
441
  try {
413
- use = await page(getAuth, "/user/usage")
442
+ use = await page(getAuth, "/user/usage", read)
414
443
  } catch (e) {
415
444
  if (e instanceof SignInGone) out.error = e.message
416
445
  // the plan alone, when the week's allowance can't be read
417
- return out
446
+ return { ...out, signIn: read.signIn }
418
447
  }
448
+ out.signIn = read.signIn
419
449
  const percent = use?.percent
420
450
  const reset = use?.resetDate
421
451
  // no plan: the free offer shows no allowance
@@ -452,6 +482,9 @@ export const MimoAuthPlugin = async ({ client }) => {
452
482
  const req = input instanceof Request ? input : null
453
483
  const url = req ? req.url : String(input)
454
484
  let body = init.body ?? (req ? await req.text() : undefined)
485
+ // a body handed over as bytes is read as the string it is
486
+ if (body instanceof ArrayBuffer) body = new TextDecoder().decode(body)
487
+ else if (ArrayBuffer.isView(body)) body = new TextDecoder().decode(new Uint8Array(body.buffer, body.byteOffset, body.byteLength))
455
488
  // mimo-auto, the app's default, is asked as the model it stands for
456
489
  if (typeof body === "string" && body.includes('"mimo-auto"')) {
457
490
  try {
@@ -459,22 +492,29 @@ export const MimoAuthPlugin = async ({ client }) => {
459
492
  if (j?.model === "mimo-auto") body = JSON.stringify({ ...j, model: "mimo-pro" })
460
493
  } catch {}
461
494
  }
462
- const send = async (force) => {
463
- const s = await fresh(getAuth, force)
464
- const headers = new Headers(init.headers ?? req?.headers)
465
- headers.delete("authorization")
466
- headers.set("Cookie", cookieHeader(s.cookies))
467
- headers.set("X-Mimo-Source", SOURCE)
468
- headers.set("User-Agent", UA)
469
- headers.set("X-Client-Version", APP_VERSION)
470
- return fetch(url, { ...init, method: init.method ?? req?.method ?? "POST", headers, body })
495
+ let s
496
+ try {
497
+ s = await fresh(getAuth, false)
498
+ } catch (e) {
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
502
+ if (!(e instanceof Lapsed)) throw e
503
+ const who = fromAuth(await getAuth())?.creds.userId ?? a.creds.userId
504
+ const message = `${who}: the Xiaomi MiMo sign-in has expired — sign in again`
505
+ return said(new Response(JSON.stringify({ error: { message, type: "api_error", code: null } }), { status: 502, headers: { "content-type": "application/json" } }), "expired")
471
506
  }
472
- const res = await send(false)
473
- // a session gone stale, or a redirect to Xiaomi's sign-in page
474
- const stale = res.status === 401 || (res.status === 200 && /text\/html/i.test(res.headers.get("content-type") ?? ""))
475
- if (!stale || (body != null && typeof body !== "string")) return res
476
- await res.arrayBuffer().catch(() => {})
477
- return send(true)
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")
478
518
  },
479
519
  }
480
520
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-mimo-auth",
3
- "version": "0.1.2",
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",