@magpie-community/opencode-mimo-auth 0.1.0
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 +51 -0
- package/index.mjs +381 -0
- package/package.json +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @magpie-community/opencode-mimo-auth
|
|
2
|
+
|
|
3
|
+
Signs in to **Xiaomi MiMo** with a Xiaomi account, the way the MiMo desktop
|
|
4
|
+
app does, and makes its model requests. Provider id: `mimo-app`.
|
|
5
|
+
|
|
6
|
+
## Sign-in
|
|
7
|
+
|
|
8
|
+
One way, **Xiaomi account**. The plugin opens Xiaomi's sign-in page
|
|
9
|
+
(account.xiaomi.com's long-poll ticket). Sign in there, or scan its QR code
|
|
10
|
+
with the Xiaomi phone app. The plugin waits on the ticket until Xiaomi says
|
|
11
|
+
the account is in, so there's nothing to paste back.
|
|
12
|
+
|
|
13
|
+
Xiaomi's passToken then signs the account on at the MiMo server. The server
|
|
14
|
+
sends the browser through account.xiaomi.com and back through its `/sts`,
|
|
15
|
+
which sets the session cookies (`serviceToken`, `userId`, `cUserId`). An
|
|
16
|
+
account from Russia or India is moved to that region's server
|
|
17
|
+
(`mimo-server-ru` / `mimo-server-in`). Everyone else stays on
|
|
18
|
+
`mimo-server-sgp`.
|
|
19
|
+
|
|
20
|
+
## Requests
|
|
21
|
+
|
|
22
|
+
Chat completions go to `https://mimo-server-<region>.xiaomimimo.com/api/route/chat/completions`
|
|
23
|
+
(`@ai-sdk/openai-compatible`). Each request carries:
|
|
24
|
+
|
|
25
|
+
- the session cookies, with no `Authorization` header
|
|
26
|
+
- `X-Mimo-Source: mimocode-cli-free`
|
|
27
|
+
- `X-Client-Version: 26.929.292248`
|
|
28
|
+
- the app's User-Agent
|
|
29
|
+
|
|
30
|
+
`mimo-auto`, the app's default model, is requested as `mimo-pro`.
|
|
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.
|
|
36
|
+
|
|
37
|
+
## Where the sign-in is kept
|
|
38
|
+
|
|
39
|
+
In OpenCode's `auth.json`, or magpie's `plugin-auth.json`, as an OAuth sign-in:
|
|
40
|
+
|
|
41
|
+
- `refresh`: the Xiaomi account as JSON (`userId`, `cUserId`, `passToken`, `deviceId`, `region`, `base`)
|
|
42
|
+
- `access`: the session cookies as JSON
|
|
43
|
+
- `expires`: when the session is renewed
|
|
44
|
+
- `accountId`: the Xiaomi user id
|
|
45
|
+
|
|
46
|
+
## Models
|
|
47
|
+
|
|
48
|
+
| Model | Name | Context | Output | Input |
|
|
49
|
+
|---|---|---|---|---|
|
|
50
|
+
| `mimo-pro` | MiMo Pro | 1,000,000 | 128,000 | text, image |
|
|
51
|
+
| `mimo-flash` | MiMo Flash | 1,000,000 | 128,000 | text, image |
|
package/index.mjs
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// Xiaomi MiMo, as the MiMo desktop app signs in to it: a Xiaomi account
|
|
2
|
+
// signed in on account.xiaomi.com's long-poll page (a QR code for the
|
|
3
|
+
// Xiaomi phone app, or the password), whose passToken signs it on at the
|
|
4
|
+
// MiMo server, which answers with session cookies. Model requests are
|
|
5
|
+
// chat completions at the server's /route, carrying those cookies.
|
|
6
|
+
import { randomBytes } from "node:crypto"
|
|
7
|
+
|
|
8
|
+
const ID = "mimo-app"
|
|
9
|
+
const HOSTS = {
|
|
10
|
+
SGP: "https://mimo-server-sgp.xiaomimimo.com/api",
|
|
11
|
+
RU: "https://mimo-server-ru.xiaomimimo.com/api",
|
|
12
|
+
IN: "https://mimo-server-in.xiaomimimo.com/api",
|
|
13
|
+
}
|
|
14
|
+
const REGION = "SGP"
|
|
15
|
+
const ACCOUNT = "https://account.xiaomi.com"
|
|
16
|
+
const APP_VERSION = "26.929.292248"
|
|
17
|
+
const SOURCE = "mimocode-cli-free"
|
|
18
|
+
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"
|
|
19
|
+
const RENEW_AFTER = 24 * 60 * 60 * 1000 // the app signs on again after a day
|
|
20
|
+
const SIGN_IN_LIFE = 10 * 60 * 1000
|
|
21
|
+
|
|
22
|
+
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"] } }
|
|
23
|
+
const MODELS = {
|
|
24
|
+
"mimo-pro": { name: "MiMo Pro", ...MODEL },
|
|
25
|
+
"mimo-flash": { name: "MiMo Flash", ...MODEL },
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const baseOf = (region) => HOSTS[String(region ?? "").trim().toUpperCase()] ?? ""
|
|
29
|
+
const deviceId = () => "pc_" + randomBytes(16).toString("hex")
|
|
30
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
31
|
+
|
|
32
|
+
// the account server's JSON comes after a "&&&START&&&" guard
|
|
33
|
+
function accountJSON(text) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(text.trim().replace(/^&&&START&&&/, ""))
|
|
36
|
+
} catch {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---- a cookie jar, enough for the sign-on's redirects -----------------------
|
|
42
|
+
|
|
43
|
+
class Jar {
|
|
44
|
+
constructor() {
|
|
45
|
+
this.cookies = []
|
|
46
|
+
}
|
|
47
|
+
put(c) {
|
|
48
|
+
this.cookies = this.cookies.filter((o) => !(o.name === c.name && o.domain === c.domain && o.path === c.path))
|
|
49
|
+
if (!c.gone) this.cookies.push(c)
|
|
50
|
+
}
|
|
51
|
+
seed(url, name, value) {
|
|
52
|
+
this.put({ name, value, domain: new URL(url).hostname.toLowerCase(), hostOnly: false, path: "/", secure: false })
|
|
53
|
+
}
|
|
54
|
+
take(res, url) {
|
|
55
|
+
const u = new URL(url)
|
|
56
|
+
const lines = typeof res.headers.getSetCookie === "function" ? res.headers.getSetCookie() : []
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
const [pair, ...attrs] = line.split(";")
|
|
59
|
+
const eq = pair.indexOf("=")
|
|
60
|
+
if (eq <= 0) continue
|
|
61
|
+
const c = { name: pair.slice(0, eq).trim(), value: pair.slice(eq + 1).trim(), domain: u.hostname.toLowerCase(), hostOnly: true, secure: false }
|
|
62
|
+
const dir = u.pathname.lastIndexOf("/")
|
|
63
|
+
c.path = dir > 0 ? u.pathname.slice(0, dir) : "/"
|
|
64
|
+
for (const a of attrs) {
|
|
65
|
+
const i = a.indexOf("=")
|
|
66
|
+
const k = (i < 0 ? a : a.slice(0, i)).trim().toLowerCase()
|
|
67
|
+
const v = i < 0 ? "" : a.slice(i + 1).trim()
|
|
68
|
+
if (k === "domain" && v) {
|
|
69
|
+
const d = v.replace(/^\./, "").toLowerCase()
|
|
70
|
+
if (c.domain === d || c.domain.endsWith("." + d)) Object.assign(c, { domain: d, hostOnly: false })
|
|
71
|
+
} else if (k === "path" && v.startsWith("/")) c.path = v
|
|
72
|
+
else if (k === "secure") c.secure = true
|
|
73
|
+
else if (k === "max-age" && Number(v) <= 0) c.gone = true
|
|
74
|
+
else if (k === "expires" && Date.parse(v) <= Date.now()) c.gone = true
|
|
75
|
+
}
|
|
76
|
+
this.put(c)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for(url) {
|
|
80
|
+
const u = new URL(url)
|
|
81
|
+
const host = u.hostname.toLowerCase()
|
|
82
|
+
return this.cookies.filter((c) => {
|
|
83
|
+
if (c.secure && u.protocol !== "https:") return false
|
|
84
|
+
if (c.hostOnly ? host !== c.domain : host !== c.domain && !host.endsWith("." + c.domain)) return false
|
|
85
|
+
return u.pathname === c.path || u.pathname.startsWith(c.path.endsWith("/") ? c.path : c.path + "/") || c.path === "/"
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
header(url) {
|
|
89
|
+
return this.for(url).map((c) => `${c.name}=${c.value}`).join("; ")
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---- the session ------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
class Lapsed extends Error {}
|
|
96
|
+
|
|
97
|
+
const appHeaders = () => ({ "User-Agent": UA, "X-Client-Version": APP_VERSION })
|
|
98
|
+
|
|
99
|
+
// session signs the account on at a MiMo server as the app's window does:
|
|
100
|
+
// the server's /user/xiaomi/me, asked with no session, sends the browser to
|
|
101
|
+
// account.xiaomi.com's serviceLogin, which (holding the passToken) sends it
|
|
102
|
+
// back through the server's /sts, where the session cookies are set, to
|
|
103
|
+
// /user/xiaomi/me again. The server's cookies are the session.
|
|
104
|
+
async function session(creds, base) {
|
|
105
|
+
const jar = new Jar()
|
|
106
|
+
for (const [k, v] of [["userId", creds.userId], ["passToken", creds.passToken], ["cUserId", creds.cUserId],
|
|
107
|
+
["deviceId", creds.deviceId], ["pass_ua", "pc"], ["uLocale", "zh_CN"]]) {
|
|
108
|
+
if (v) jar.seed(ACCOUNT, k, v)
|
|
109
|
+
}
|
|
110
|
+
const bu = new URL(base)
|
|
111
|
+
const signal = AbortSignal.timeout(30_000)
|
|
112
|
+
let url = base.replace(/\/+$/, "") + "/user/xiaomi/me"
|
|
113
|
+
let res
|
|
114
|
+
for (let hop = 0; ; hop++) {
|
|
115
|
+
if (hop > 10) throw new Error("Xiaomi MiMo sign-in: too many redirects")
|
|
116
|
+
const headers = appHeaders()
|
|
117
|
+
const cookie = jar.header(url)
|
|
118
|
+
if (cookie) headers.Cookie = cookie
|
|
119
|
+
res = await fetch(url, { headers, redirect: "manual", signal })
|
|
120
|
+
jar.take(res, url)
|
|
121
|
+
const loc = res.headers.get("location")
|
|
122
|
+
if (res.status < 300 || res.status >= 400 || !loc) break
|
|
123
|
+
await res.arrayBuffer().catch(() => {})
|
|
124
|
+
const next = new URL(loc, url)
|
|
125
|
+
// the server's followup is written http://; its session cookies are
|
|
126
|
+
// only sent back over https
|
|
127
|
+
if (next.protocol === "http:" && bu.protocol === "https:" && next.host.toLowerCase() === bu.host.toLowerCase()) next.protocol = "https:"
|
|
128
|
+
url = next.href
|
|
129
|
+
}
|
|
130
|
+
const text = await res.text()
|
|
131
|
+
let me
|
|
132
|
+
try {
|
|
133
|
+
me = JSON.parse(text)
|
|
134
|
+
} catch {
|
|
135
|
+
// Xiaomi's sign-in page, not the server's answer: the passToken no
|
|
136
|
+
// longer signs the account on
|
|
137
|
+
throw new Lapsed("Xiaomi MiMo: the Xiaomi sign-in has lapsed; sign in again")
|
|
138
|
+
}
|
|
139
|
+
if (res.status === 403 || me?.code === 403 || me?.code === 46109) {
|
|
140
|
+
throw new Error("Xiaomi MiMo doesn't serve this Xiaomi account (its region isn't served here)")
|
|
141
|
+
}
|
|
142
|
+
if (me?.code !== 0 || !String(me?.data?.userId ?? "")) throw new Lapsed("Xiaomi MiMo: the Xiaomi sign-in has lapsed; sign in again")
|
|
143
|
+
const cookies = {}
|
|
144
|
+
for (const c of jar.for(bu.href)) cookies[c.name] = c.value
|
|
145
|
+
if (!Object.keys(cookies).length) throw new Error("Xiaomi MiMo sign-in: the server set no session")
|
|
146
|
+
return { cookies, me: me.data }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// cookieHeader is a session as a Cookie header, in a steady order
|
|
150
|
+
function cookieHeader(cookies) {
|
|
151
|
+
const first = ["serviceToken", "userId", "cUserId"]
|
|
152
|
+
const parts = first.filter((k) => k in cookies).map((k) => `${k}=${cookies[k]}`)
|
|
153
|
+
for (const [k, v] of Object.entries(cookies)) if (!first.includes(k)) parts.push(`${k}=${v}`)
|
|
154
|
+
return parts.join("; ")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The sign-in is kept as OpenCode keeps an OAuth one: refresh is the
|
|
158
|
+
// Xiaomi account (what signs it on again), access the session's cookies,
|
|
159
|
+
// expires when the app would sign on again.
|
|
160
|
+
function toAuth(creds, cookies, issued) {
|
|
161
|
+
return {
|
|
162
|
+
type: "oauth",
|
|
163
|
+
refresh: JSON.stringify(creds),
|
|
164
|
+
access: JSON.stringify(cookies),
|
|
165
|
+
expires: issued + RENEW_AFTER,
|
|
166
|
+
accountId: creds.userId,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function fromAuth(auth) {
|
|
171
|
+
if (auth?.type !== "oauth") return null
|
|
172
|
+
try {
|
|
173
|
+
const creds = JSON.parse(auth.refresh)
|
|
174
|
+
let cookies = {}
|
|
175
|
+
try {
|
|
176
|
+
cookies = JSON.parse(auth.access || "{}") ?? {}
|
|
177
|
+
} catch {}
|
|
178
|
+
if (!creds?.passToken || !creds?.userId) return null
|
|
179
|
+
creds.base ||= baseOf(creds.region) || HOSTS[REGION]
|
|
180
|
+
return { creds, cookies, expires: Number(auth.expires) || 0 }
|
|
181
|
+
} catch {
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---- the sign-in ------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
// serviceOf is the sid and callback the MiMo server sends a browser with
|
|
189
|
+
// no session to account.xiaomi.com with
|
|
190
|
+
async function serviceOf(base) {
|
|
191
|
+
let sid = "mimosgp"
|
|
192
|
+
let callback = base.replace(/\/+$/, "") + "/sts"
|
|
193
|
+
try {
|
|
194
|
+
const res = await fetch(base.replace(/\/+$/, "") + "/user/xiaomi/me", { headers: appHeaders(), redirect: "manual", signal: AbortSignal.timeout(15_000) })
|
|
195
|
+
await res.arrayBuffer().catch(() => {})
|
|
196
|
+
const loc = res.headers.get("location")
|
|
197
|
+
if (loc) {
|
|
198
|
+
const q = new URL(loc, base).searchParams
|
|
199
|
+
sid = q.get("sid") || sid
|
|
200
|
+
callback = q.get("callback") || callback
|
|
201
|
+
}
|
|
202
|
+
} catch {}
|
|
203
|
+
return { sid, callback }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function accountGet(url, device, signal) {
|
|
207
|
+
const res = await fetch(url, { headers: { "User-Agent": UA, Cookie: `deviceId=${device}; pass_ua=pc; uLocale=zh_CN` }, signal })
|
|
208
|
+
return { status: res.status, text: await res.text() }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function startSignIn() {
|
|
212
|
+
const base = HOSTS[REGION]
|
|
213
|
+
const device = deviceId()
|
|
214
|
+
const { sid, callback } = await serviceOf(base)
|
|
215
|
+
const q = new URLSearchParams({
|
|
216
|
+
_group: "DEFAULT",
|
|
217
|
+
_qrsize: "240",
|
|
218
|
+
qs: "%3Fsid%3D" + encodeURIComponent(sid) + "%26_json%3Dtrue",
|
|
219
|
+
callback,
|
|
220
|
+
_hasLogo: "false",
|
|
221
|
+
sid,
|
|
222
|
+
serviceParam: "",
|
|
223
|
+
_locale: "en_US",
|
|
224
|
+
})
|
|
225
|
+
const { status, text } = await accountGet(`${ACCOUNT}/longPolling/loginUrl?${q}`, device, AbortSignal.timeout(20_000))
|
|
226
|
+
const lp = accountJSON(text)
|
|
227
|
+
if (!lp || lp.code !== 0 || !lp.loginUrl || !lp.lp) {
|
|
228
|
+
throw new Error(`Xiaomi MiMo sign-in: account.xiaomi.com answered ${status} ${lp?.desc || text.trim().slice(0, 200)}`)
|
|
229
|
+
}
|
|
230
|
+
const life = lp.timeout > 0 ? Math.min(lp.timeout * 1000, SIGN_IN_LIFE) : SIGN_IN_LIFE
|
|
231
|
+
return { url: lp.loginUrl, lp: lp.lp, device, until: Date.now() + life }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// waitPoll asks the ticket's long poll until Xiaomi says the account is in
|
|
235
|
+
async function waitPoll(t) {
|
|
236
|
+
while (Date.now() < t.until) {
|
|
237
|
+
try {
|
|
238
|
+
const left = t.until - Date.now()
|
|
239
|
+
const { status, text } = await accountGet(t.lp, t.device, AbortSignal.timeout(Math.min(70_000, Math.max(left, 1))))
|
|
240
|
+
const p = status === 200 ? accountJSON(text) : null
|
|
241
|
+
if (p?.passToken && String(p.userId ?? "")) return p
|
|
242
|
+
} catch {}
|
|
243
|
+
await sleep(1000)
|
|
244
|
+
}
|
|
245
|
+
throw new Error("the Xiaomi sign-in page expired before the sign-in finished; start it again")
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function signedInWith(p, device) {
|
|
249
|
+
let creds = { userId: String(p.userId), cUserId: p.cUserId ?? "", passToken: p.passToken, deviceId: device, region: REGION, base: HOSTS[REGION] }
|
|
250
|
+
let s
|
|
251
|
+
try {
|
|
252
|
+
s = await session(creds, creds.base)
|
|
253
|
+
} catch (e) {
|
|
254
|
+
if (e instanceof Lapsed) throw new Error("Xiaomi signed the account in, but the MiMo server didn't take it; try again")
|
|
255
|
+
throw e
|
|
256
|
+
}
|
|
257
|
+
// an account of another region is served by that region's server
|
|
258
|
+
const r = String(s.me?.region ?? "").trim().toUpperCase()
|
|
259
|
+
const nb = baseOf(r)
|
|
260
|
+
if (r && r !== creds.region && nb && nb !== creds.base) {
|
|
261
|
+
try {
|
|
262
|
+
s = await session(creds, nb)
|
|
263
|
+
creds = { ...creds, region: r, base: nb }
|
|
264
|
+
} catch {}
|
|
265
|
+
}
|
|
266
|
+
return { creds: { ...creds, userId: String(s.me?.userId ?? "") || creds.userId }, cookies: s.cookies }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---- the plugin -------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
export const MimoAuthPlugin = async ({ client }) => {
|
|
272
|
+
let renewing = null
|
|
273
|
+
|
|
274
|
+
const save = async (auth) => {
|
|
275
|
+
try {
|
|
276
|
+
await client?.auth?.set?.({ path: { id: ID }, body: auth })
|
|
277
|
+
} catch {}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// fresh is the account with a live session, signed on again when it is
|
|
281
|
+
// older than the app lets one get, or when force is set (the server
|
|
282
|
+
// turned the last one away)
|
|
283
|
+
const fresh = async (getAuth, force) => {
|
|
284
|
+
const a = fromAuth(await getAuth())
|
|
285
|
+
if (!a) throw new Error("Xiaomi MiMo: not signed in")
|
|
286
|
+
if (!force && Object.keys(a.cookies).length && Date.now() < a.expires) return a
|
|
287
|
+
if (!renewing) {
|
|
288
|
+
renewing = (async () => {
|
|
289
|
+
try {
|
|
290
|
+
const s = await session(a.creds, a.creds.base)
|
|
291
|
+
const issued = Date.now()
|
|
292
|
+
await save(toAuth(a.creds, s.cookies, issued))
|
|
293
|
+
return { creds: a.creds, cookies: s.cookies, expires: issued + RENEW_AFTER }
|
|
294
|
+
} catch (e) {
|
|
295
|
+
// a hiccup: the session in hand may still do
|
|
296
|
+
if (!(e instanceof Lapsed) && !force && Object.keys(a.cookies).length) return a
|
|
297
|
+
throw e
|
|
298
|
+
}
|
|
299
|
+
})().finally(() => {
|
|
300
|
+
renewing = null
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
return renewing
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
config: async (config) => {
|
|
308
|
+
config.provider ??= {}
|
|
309
|
+
const was = config.provider[ID] ?? {}
|
|
310
|
+
config.provider[ID] = {
|
|
311
|
+
name: "Xiaomi MiMo",
|
|
312
|
+
npm: "@ai-sdk/openai-compatible",
|
|
313
|
+
api: HOSTS[REGION] + "/route",
|
|
314
|
+
...was,
|
|
315
|
+
models: { ...MODELS, ...(was.models ?? {}) },
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
auth: {
|
|
319
|
+
provider: ID,
|
|
320
|
+
loader: async (getAuth) => {
|
|
321
|
+
const a = fromAuth(await getAuth())
|
|
322
|
+
if (!a) return {}
|
|
323
|
+
return {
|
|
324
|
+
baseURL: a.creds.base.replace(/\/+$/, "") + "/route",
|
|
325
|
+
apiKey: "mimo", // the engine's placeholder; the app strips it
|
|
326
|
+
async fetch(input, init = {}) {
|
|
327
|
+
const req = input instanceof Request ? input : null
|
|
328
|
+
const url = req ? req.url : String(input)
|
|
329
|
+
let body = init.body ?? (req ? await req.text() : undefined)
|
|
330
|
+
// mimo-auto, the app's default, is asked as the model it stands for
|
|
331
|
+
if (typeof body === "string" && body.includes('"mimo-auto"')) {
|
|
332
|
+
try {
|
|
333
|
+
const j = JSON.parse(body)
|
|
334
|
+
if (j?.model === "mimo-auto") body = JSON.stringify({ ...j, model: "mimo-pro" })
|
|
335
|
+
} catch {}
|
|
336
|
+
}
|
|
337
|
+
const send = async (force) => {
|
|
338
|
+
const s = await fresh(getAuth, force)
|
|
339
|
+
const headers = new Headers(init.headers ?? req?.headers)
|
|
340
|
+
headers.delete("authorization")
|
|
341
|
+
headers.set("Cookie", cookieHeader(s.cookies))
|
|
342
|
+
headers.set("X-Mimo-Source", SOURCE)
|
|
343
|
+
headers.set("User-Agent", UA)
|
|
344
|
+
headers.set("X-Client-Version", APP_VERSION)
|
|
345
|
+
return fetch(url, { ...init, method: init.method ?? req?.method ?? "POST", headers, body })
|
|
346
|
+
}
|
|
347
|
+
const res = await send(false)
|
|
348
|
+
// a session gone stale, or a redirect to Xiaomi's sign-in page
|
|
349
|
+
const stale = res.status === 401 || (res.status === 200 && /text\/html/i.test(res.headers.get("content-type") ?? ""))
|
|
350
|
+
if (!stale || (body != null && typeof body !== "string")) return res
|
|
351
|
+
await res.arrayBuffer().catch(() => {})
|
|
352
|
+
return send(true)
|
|
353
|
+
},
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
methods: [
|
|
357
|
+
{
|
|
358
|
+
type: "oauth",
|
|
359
|
+
label: "Xiaomi account",
|
|
360
|
+
authorize: async () => {
|
|
361
|
+
const t = await startSignIn()
|
|
362
|
+
return {
|
|
363
|
+
url: t.url,
|
|
364
|
+
instructions: "Sign in to your Xiaomi account on the page that opens (or scan its QR code with the Xiaomi app). The sign-in finishes here by itself.",
|
|
365
|
+
method: "auto",
|
|
366
|
+
callback: async () => {
|
|
367
|
+
try {
|
|
368
|
+
const p = await waitPoll(t)
|
|
369
|
+
const { creds, cookies } = await signedInWith(p, t.device)
|
|
370
|
+
return { ...toAuth(creds, cookies, Date.now()), type: "success" }
|
|
371
|
+
} catch {
|
|
372
|
+
return { type: "failed" }
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@magpie-community/opencode-mimo-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode provider plugin for Xiaomi MiMo: the MiMo app's Xiaomi-account sign-in, MiMo Pro and MiMo Flash",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.mjs",
|
|
7
|
+
"exports": "./index.mjs",
|
|
8
|
+
"files": ["index.mjs", "README.md"],
|
|
9
|
+
"keywords": ["opencode", "opencode-plugin", "xiaomi", "mimo"],
|
|
10
|
+
"license": "MIT"
|
|
11
|
+
}
|