creditdaddy 0.9.5 → 1.2.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.
@@ -1,307 +1,386 @@
1
- /**
2
- * mirasim(原生 AI 编程开发环境)本机凭据与切号引擎。
3
- *
4
- * 凭据存储:
5
- * ~/.mirasim/setting.json auth: { token, refreshToken, userId, name, exp }(mrs1: 加密)
6
- * ~/.mirasim/secret.key DPAPI 加密的 master key(Windows 上是 hex 字符串)
7
- *
8
- * 加密格式:
9
- * mrs1:<base64(12B IV + 16B tag + ciphertext)>,AES-256-GCM,
10
- * key = DPAPI Unprotect(secret.key) → utf16le 字符串(64 字符 hex)→ Buffer
11
- *
12
- * 账号切换:
13
- * - mirasim 正在运行时可提示或强制退出
14
- * - 切换前先把当前登录同步进 CreditDaddy 账号库(防丢号)
15
- * - 将目标账号的 auth 替换写回 setting.json(保留其他所有配置,如 models、workspaces 等)
16
- * - 桌面端切号成功后可一键拉起 Mirasim.exe
17
- */
18
-
19
- import fs from 'node:fs';
20
- import os from 'node:os';
21
- import path from 'node:path';
22
- import crypto from 'node:crypto';
23
- import { execFile, execSync } from 'node:child_process';
24
-
25
- const ENC_PREFIX = 'mrs1:';
26
-
27
- export function mirasimHome() {
28
- return process.env.MIRASIM_HOME || path.join(os.homedir(), '.mirasim');
29
- }
30
-
31
- export function mirasimPaths() {
32
- const h = mirasimHome();
33
- return {
34
- home: h,
35
- setting: path.join(h, 'setting.json'),
36
- secretKey: path.join(h, 'secret.key'),
37
- insights: path.join(h, 'insights'),
38
- };
39
- }
40
-
41
- function readJson(file) {
42
- try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
43
- }
44
-
45
- function atomicWriteJson(file, obj) {
46
- fs.mkdirSync(path.dirname(file), { recursive: true });
47
- const tmp = `${file}.tmp-${crypto.randomUUID()}`;
48
- fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', { mode: 0o600 });
49
- fs.renameSync(tmp, file);
50
- }
51
-
52
- // ── DPAPI 解密 master key ──
53
-
54
- let masterKeyCache = null;
55
-
56
- function dpapiUnprotect(buf) {
57
- const script = 'Add-Type -AssemblyName System.Security;'
58
- + '$b=[Convert]::FromBase64String([Console]::In.ReadToEnd());'
59
- + "[Convert]::ToBase64String([System.Security.Cryptography.ProtectedData]::Unprotect($b,$null,'CurrentUser'))";
60
- // 使用 -EncodedCommand 传入 UTF-16LE Base64,避免命令行参数中包含敏感 API 关键字被杀毒软件误报
61
- const encoded = Buffer.from(script, 'utf16le').toString('base64');
62
- return new Promise((resolve, reject) => {
63
- const child = execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded],
64
- { windowsHide: true, timeout: 15_000 }, (err, stdout) => {
65
- if (err) return reject(new Error('DPAPI 解密失败:' + err.message));
66
- resolve(Buffer.from(String(stdout).trim(), 'base64'));
67
- });
68
- child.stdin.end(buf.toString('base64'));
69
- });
70
- }
71
-
72
- /** 取得解开后的 32 字节 master key Buffer(未配置或解密失败返回 null) */
73
- export async function getMasterKey() {
74
- if (masterKeyCache) return masterKeyCache;
75
- const p = mirasimPaths();
76
- if (!fs.existsSync(p.secretKey)) return null;
77
-
78
- try {
79
- const raw = fs.readFileSync(p.secretKey, 'utf8').trim();
80
- if (process.platform === 'win32') {
81
- const bin = /^[0-9a-f]+$/i.test(raw) ? Buffer.from(raw, 'hex') : Buffer.from(raw, 'base64');
82
- const unprotected = await dpapiUnprotect(bin);
83
- // Windows 存放的是 utf16le 编码的 64 字符 hex 密钥
84
- const hex = unprotected.toString('utf16le').trim();
85
- if (/^[0-9a-f]{64}$/i.test(hex)) {
86
- masterKeyCache = Buffer.from(hex, 'hex');
87
- return masterKeyCache;
88
- }
89
- if (/^[0-9a-f]{64}$/i.test(unprotected.toString('utf8').trim())) {
90
- masterKeyCache = Buffer.from(unprotected.toString('utf8').trim(), 'hex');
91
- return masterKeyCache;
92
- }
93
- } else {
94
- // macOS / Linux 下如果有直接存 hex 或钥匙串导出
95
- if (/^[0-9a-f]{64}$/i.test(raw)) {
96
- masterKeyCache = Buffer.from(raw, 'hex');
97
- return masterKeyCache;
98
- }
99
- }
100
- } catch {}
101
- return null;
102
- }
103
-
104
- export function isEncrypted(v) {
105
- return typeof v === 'string' && v.startsWith(ENC_PREFIX);
106
- }
107
-
108
- /** 解密 mrs1: 字符串 */
109
- export function decryptWithKey(value, key) {
110
- if (!isEncrypted(value)) return value;
111
- if (!key) throw new Error('缺少 mirasim master key,无法解密');
112
- const raw = Buffer.from(value.slice(ENC_PREFIX.length), 'base64');
113
- if (raw.length < 28) throw new Error('mrs1 格式长度不正确');
114
- const iv = raw.subarray(0, 12);
115
- const tag = raw.subarray(12, 28);
116
- const ct = raw.subarray(28);
117
- const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
118
- decipher.setAuthTag(tag);
119
- return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
120
- }
121
-
122
- /** 加密成 mrs1: 字符串(切号写回时保证与客户端格式一致) */
123
- export function encryptWithKey(plain, key) {
124
- if (!key) return plain;
125
- const iv = crypto.randomBytes(12);
126
- const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
127
- const ct = Buffer.concat([cipher.update(String(plain), 'utf8'), cipher.final()]);
128
- const tag = cipher.getAuthTag();
129
- return ENC_PREFIX + Buffer.concat([iv, tag, ct]).toString('base64');
130
- }
131
-
132
- // ── 本机探测 ──
133
-
134
- export function detectMirasim() {
135
- const p = mirasimPaths();
136
- const exists = fs.existsSync(p.setting);
137
- let signedIn = false;
138
- if (exists) {
139
- const s = readJson(p.setting);
140
- signedIn = Boolean(s && s.auth && (s.auth.token || s.auth.userId));
141
- }
142
- const clientPath = candidateClientPath();
143
- return {
144
- dataDir: p.home,
145
- exists,
146
- signedIn,
147
- clientInstalled: Boolean(clientPath),
148
- clientPath,
149
- running: mirasimRunning(),
150
- };
151
- }
152
-
153
- export function candidateClientPath() {
154
- const home = os.homedir();
155
- if (process.platform === 'win32') {
156
- const local = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
157
- const p1 = path.join(local, 'Programs', '@mirasimdesktop', 'Mirasim.exe');
158
- if (fs.existsSync(p1)) return p1;
159
- const p2 = path.join(process.env.ProgramFiles || 'C:\\Program Files', '@mirasimdesktop', 'Mirasim.exe');
160
- if (fs.existsSync(p2)) return p2;
161
- } else if (process.platform === 'darwin') {
162
- const p = '/Applications/Mirasim.app/Contents/MacOS/Mirasim';
163
- if (fs.existsSync(p)) return p;
164
- }
165
- return null;
166
- }
167
-
168
- export function mirasimRunning() {
169
- if (process.platform !== 'win32') return false;
170
- try {
171
- const out = execSync('tasklist /FI "IMAGENAME eq Mirasim.exe" /NH', { windowsHide: true, encoding: 'utf8' });
172
- return /mirasim\.exe/i.test(out);
173
- } catch { return false; }
174
- }
175
-
176
- const nap = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
177
-
178
- export function terminateMirasim({ timeoutMs = 8000 } = {}) {
179
- if (process.platform !== 'win32') return { closed: false, supported: false };
180
- if (!mirasimRunning()) return { closed: false, running: false };
181
- try { execSync('taskkill /IM Mirasim.exe /T', { windowsHide: true, stdio: 'ignore' }); } catch {}
182
- let deadline = Date.now() + timeoutMs;
183
- while (Date.now() < deadline && mirasimRunning()) nap(250);
184
- if (mirasimRunning()) {
185
- try { execSync('taskkill /IM Mirasim.exe /T /F', { windowsHide: true, stdio: 'ignore' }); } catch {}
186
- deadline = Date.now() + 3000;
187
- while (Date.now() < deadline && mirasimRunning()) nap(250);
188
- }
189
- return { closed: !mirasimRunning(), running: mirasimRunning() };
190
- }
191
-
192
- // ── 当前会话读取与切换 ──
193
-
194
- /** 读取当前登录的 mirasim 账号,返回标准化账号对象(供 addAccount / scan) */
195
- export async function liveToAccount() {
196
- const p = mirasimPaths();
197
- const s = readJson(p.setting);
198
- if (!s || !s.auth || !s.auth.token) return null;
199
- const key = await getMasterKey();
200
- let token = s.auth.token;
201
- let refreshToken = s.auth.refreshToken;
202
- if (isEncrypted(token)) {
203
- if (!key) return null; // 无法解密
204
- try {
205
- token = decryptWithKey(token, key);
206
- refreshToken = refreshToken ? decryptWithKey(refreshToken, key) : null;
207
- } catch { return null; }
208
- }
209
-
210
- const userId = s.auth.userId || (token.includes('.') ? (() => {
211
- try { return JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8')).sub; } catch { return null; }
212
- })() : null);
213
-
214
- const expiresAt = s.auth.exp ? new Date(s.auth.exp * 1000).toISOString() : null;
215
-
216
- return {
217
- provider: 'mirasim',
218
- token,
219
- refreshToken,
220
- uid: userId,
221
- name: s.auth.name || null,
222
- expiresAt,
223
- source: 'local-app',
224
- meta: {
225
- encryptedAuth: s.auth, // 原始密文备份,切换时可原样写回
226
- capturedAt: new Date().toISOString(),
227
- },
228
- };
229
- }
230
-
231
- export function currentMirasimUid() {
232
- const p = mirasimPaths();
233
- const s = readJson(p.setting);
234
- return s?.auth?.userId || null;
235
- }
236
-
237
- /**
238
- * 把刷新后的凭据写回 ~/.mirasim/setting.json(仅当该账号正是客户端当前登录时调用,
239
- * 避免 mirasim 客户端拿着被轮换掉的旧 token 掉线)。保留 setting.json 其余字段原样。
240
- * @returns {boolean} 是否发生了写回
241
- */
242
- export async function writeMirasimAuth(account) {
243
- const p = mirasimPaths();
244
- const s = readJson(p.setting);
245
- if (!s || !s.auth) return false;
246
- if (!account?.uid || s.auth.userId !== account.uid) return false;
247
- const key = await getMasterKey();
248
- const enc = (v) => (key ? encryptWithKey(String(v), key) : String(v));
249
- s.auth = {
250
- ...s.auth,
251
- token: enc(account.token),
252
- refreshToken: account.refreshToken ? enc(account.refreshToken) : s.auth.refreshToken,
253
- exp: account.expiresAt ? Math.floor(new Date(account.expiresAt).getTime() / 1000) : s.auth.exp,
254
- };
255
- atomicWriteJson(p.setting, s);
256
- return true;
257
- }
258
-
259
- /**
260
- * 切换 mirasim 当前登录账号
261
- * @param {object} account 目标账号
262
- * @param {{ force?: boolean }} opts
263
- */
264
- export async function switchTo(account, { force = false } = {}) {
265
- const p = mirasimPaths();
266
- const s = readJson(p.setting) || {};
267
- const currentUid = s.auth?.userId || null;
268
- if (account.uid && currentUid && account.uid === currentUid) {
269
- return { switched: false, alreadyActive: true };
270
- }
271
-
272
- if (!force && mirasimRunning()) {
273
- const err = new Error('Mirasim 客户端正在运行,请先退出后再切换(或强制切换,切换后将自动重新拉起客户端)');
274
- err.mirasimRunning = true;
275
- throw err;
276
- }
277
-
278
- const key = await getMasterKey();
279
- let authToWrite = null;
280
-
281
- if (account.meta?.encryptedAuth) {
282
- // 优先原样使用抓取到的加密密文
283
- authToWrite = account.meta.encryptedAuth;
284
- } else if (key) {
285
- // 新增或从外部导入的账号:使用当前 master key 加密写回
286
- authToWrite = {
287
- token: encryptWithKey(account.token, key),
288
- refreshToken: account.refreshToken ? encryptWithKey(account.refreshToken, key) : null,
289
- userId: account.uid,
290
- name: account.name || null,
291
- exp: account.expiresAt ? Math.floor(new Date(account.expiresAt).getTime() / 1000) : null,
292
- };
293
- } else {
294
- // 没有 master key 时明文写入(mirasim 支持 unencrypted fallback)
295
- authToWrite = {
296
- token: account.token,
297
- refreshToken: account.refreshToken || null,
298
- userId: account.uid,
299
- name: account.name || null,
300
- exp: account.expiresAt ? Math.floor(new Date(account.expiresAt).getTime() / 1000) : null,
301
- };
302
- }
303
-
304
- s.auth = authToWrite;
305
- atomicWriteJson(p.setting, s);
306
- return { switched: true, alreadyActive: false };
307
- }
1
+ /**
2
+ * mirasim(原生 AI 编程开发环境)本机凭据与切号引擎。
3
+ *
4
+ * 凭据存储:
5
+ * ~/.mirasim/setting.json auth: { token, refreshToken, userId, name, exp }(mrs1: 加密)
6
+ * ~/.mirasim/secret.key DPAPI 加密的 master key(Windows 上是 hex 字符串;旧版)
7
+ * %APPDATA%\@mirasim\desktop\secret-key.enc
8
+ * 新版桌面端:Electron safeStorage 密文(v10 + AES-256-GCM,
9
+ * 密钥 = 同目录 Local State 的 os_crypt.encrypted_key 经 DPAPI 解开),
10
+ * 明文即 64 字符 hex master key
11
+ * MIRASIM_SECRET_KEY / MIRASIM_APP_SECRET_KEY 环境变量(64 字符 hex,客户端也会注入给子进程)
12
+ *
13
+ * 加密格式:
14
+ * mrs1:<base64(12B IV + 16B tag + ciphertext)>,AES-256-GCM,
15
+ * key = DPAPI Unprotect(secret.key) → utf16le 字符串(64 字符 hex)→ Buffer
16
+ *
17
+ * 账号切换:
18
+ * - mirasim 正在运行时可提示或强制退出
19
+ * - 切换前先把当前登录同步进 CreditDaddy 账号库(防丢号)
20
+ * - 将目标账号的 auth 替换写回 setting.json(保留其他所有配置,如 models、workspaces 等)
21
+ * - 桌面端切号成功后可一键拉起 Mirasim.exe
22
+ */
23
+
24
+ import fs from 'node:fs';
25
+ import os from 'node:os';
26
+ import path from 'node:path';
27
+ import crypto from 'node:crypto';
28
+ import { execFile, execFileSync, execSync } from 'node:child_process';
29
+
30
+ const ENC_PREFIX = 'mrs1:';
31
+
32
+ export function mirasimHome() {
33
+ return process.env.MIRASIM_HOME || path.join(os.homedir(), '.mirasim');
34
+ }
35
+
36
+ /** 新版桌面端(Electron)userData 目录 */
37
+ export function mirasimDesktopDataDir() {
38
+ if (process.env.MIRASIM_DESKTOP_DATA_DIR) return process.env.MIRASIM_DESKTOP_DATA_DIR;
39
+ const roaming = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
40
+ return path.join(roaming, '@mirasim', 'desktop');
41
+ }
42
+
43
+ export function mirasimPaths() {
44
+ const h = mirasimHome();
45
+ const desktop = mirasimDesktopDataDir();
46
+ return {
47
+ home: h,
48
+ setting: path.join(h, 'setting.json'),
49
+ secretKey: path.join(h, 'secret.key'),
50
+ secretKeyEnc: path.join(desktop, 'secret-key.enc'),
51
+ localState: path.join(desktop, 'Local State'),
52
+ insights: path.join(h, 'insights'),
53
+ };
54
+ }
55
+
56
+ function readJson(file) {
57
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
58
+ }
59
+
60
+ function atomicWriteJson(file, obj) {
61
+ fs.mkdirSync(path.dirname(file), { recursive: true });
62
+ const tmp = `${file}.tmp-${crypto.randomUUID()}`;
63
+ fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', { mode: 0o600 });
64
+ fs.renameSync(tmp, file);
65
+ }
66
+
67
+ // ── DPAPI 解密 master key ──
68
+
69
+ let masterKeyCache = null;
70
+ let masterKeyFromSecretFile = false; // 只有取自 ~/.mirasim/secret.key 的密钥才回写该文件
71
+
72
+ function persistMasterKey(paths, key) {
73
+ try {
74
+ fs.mkdirSync(paths.home, { recursive: true, mode: 0o700 });
75
+ if (process.platform === 'win32') {
76
+ const script = 'Add-Type -AssemblyName System.Security;'
77
+ + '$b=[Convert]::FromBase64String([Console]::In.ReadToEnd());'
78
+ + "[Convert]::ToBase64String([System.Security.Cryptography.ProtectedData]::Protect($b,$null,'CurrentUser'))";
79
+ const encoded = Buffer.from(script, 'utf16le').toString('base64');
80
+ const encrypted = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], {
81
+ input: Buffer.from(key.toString('hex'), 'utf16le').toString('base64'), windowsHide: true, timeout: 15_000, encoding: 'utf8',
82
+ }).trim();
83
+ fs.writeFileSync(paths.secretKey, Buffer.from(encrypted, 'base64').toString('hex'), { mode: 0o600 });
84
+ } else {
85
+ fs.writeFileSync(paths.secretKey, key.toString('hex'), { mode: 0o600 });
86
+ }
87
+ return true;
88
+ } catch { return false; }
89
+ }
90
+
91
+ function readEnvironmentMasterKey() {
92
+ const value = process.env.MIRASIM_SECRET_KEY || process.env.MIRASIM_APP_SECRET_KEY;
93
+ if (!value) return null;
94
+ const hex = value.trim();
95
+ if (!/^[0-9a-f]{64}$/i.test(hex)) return null;
96
+ return Buffer.from(hex, 'hex');
97
+ }
98
+
99
+ function dpapiUnprotect(buf) {
100
+ const script = 'Add-Type -AssemblyName System.Security;'
101
+ + '$b=[Convert]::FromBase64String([Console]::In.ReadToEnd());'
102
+ + "[Convert]::ToBase64String([System.Security.Cryptography.ProtectedData]::Unprotect($b,$null,'CurrentUser'))";
103
+ // 使用 -EncodedCommand 传入 UTF-16LE Base64,避免命令行参数中包含敏感 API 关键字被杀毒软件误报
104
+ const encoded = Buffer.from(script, 'utf16le').toString('base64');
105
+ return new Promise((resolve, reject) => {
106
+ const child = execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded],
107
+ { windowsHide: true, timeout: 15_000 }, (err, stdout) => {
108
+ if (err) return reject(new Error('DPAPI 解密失败:' + err.message));
109
+ resolve(Buffer.from(String(stdout).trim(), 'base64'));
110
+ });
111
+ child.stdin.end(buf.toString('base64'));
112
+ });
113
+ }
114
+
115
+ /** 新版桌面端 secret-key.enc(Electron safeStorage,仅 Windows)→ master key;读不到返回 null */
116
+ async function readSafeStorageMasterKey(p) {
117
+ if (process.platform !== 'win32' || !fs.existsSync(p.secretKeyEnc)) return null;
118
+ try {
119
+ const enc = fs.readFileSync(p.secretKeyEnc);
120
+ const ek = Buffer.from(readJson(p.localState)?.os_crypt?.encrypted_key || '', 'base64');
121
+ if (enc.length < 3 + 12 + 16 || enc.subarray(0, 3).toString('latin1') !== 'v10') return null;
122
+ if (ek.subarray(0, 5).toString('latin1') !== 'DPAPI') return null;
123
+ const aesKey = await dpapiUnprotect(ek.subarray(5));
124
+ const decipher = crypto.createDecipheriv('aes-256-gcm', aesKey, enc.subarray(3, 15));
125
+ decipher.setAuthTag(enc.subarray(enc.length - 16));
126
+ const hex = Buffer.concat([decipher.update(enc.subarray(15, enc.length - 16)), decipher.final()]).toString('utf8').trim();
127
+ return /^[0-9a-f]{64}$/i.test(hex) ? Buffer.from(hex, 'hex') : null;
128
+ } catch { return null; }
129
+ }
130
+
131
+ /**
132
+ * 取得解开后的 32 字节 master key Buffer(未配置或解密失败返回 null)。
133
+ * 依次尝试:~/.mirasim/secret.key(旧版)→ 桌面端 secret-key.enc(新版)→ 环境变量。
134
+ */
135
+ export async function getMasterKey() {
136
+ const p = mirasimPaths();
137
+ // 旧版 secret.key 读过后被删:用缓存解密,并原样写回修复
138
+ if (masterKeyCache) {
139
+ if (masterKeyFromSecretFile && !fs.existsSync(p.secretKey)) persistMasterKey(p, masterKeyCache);
140
+ return masterKeyCache;
141
+ }
142
+ const fromFile = await readSecretKeyFile(p);
143
+ if (fromFile) { masterKeyFromSecretFile = true; return (masterKeyCache = fromFile); }
144
+ const fromDesktop = await readSafeStorageMasterKey(p);
145
+ if (fromDesktop) return (masterKeyCache = fromDesktop);
146
+ const fromEnv = readEnvironmentMasterKey();
147
+ if (fromEnv) return (masterKeyCache = fromEnv);
148
+ return null;
149
+ }
150
+
151
+ /** 旧版 ~/.mirasim/secret.key → master key;没有或解不开返回 null */
152
+ async function readSecretKeyFile(p) {
153
+ if (!fs.existsSync(p.secretKey)) return null;
154
+ try {
155
+ const raw = fs.readFileSync(p.secretKey, 'utf8').trim();
156
+ if (process.platform === 'win32') {
157
+ const bin = /^[0-9a-f]+$/i.test(raw) ? Buffer.from(raw, 'hex') : Buffer.from(raw, 'base64');
158
+ const unprotected = await dpapiUnprotect(bin);
159
+ // Windows 存放的是 utf16le 编码的 64 字符 hex 密钥
160
+ const hex = unprotected.toString('utf16le').trim();
161
+ if (/^[0-9a-f]{64}$/i.test(hex)) return Buffer.from(hex, 'hex');
162
+ const utf8 = unprotected.toString('utf8').trim();
163
+ if (/^[0-9a-f]{64}$/i.test(utf8)) return Buffer.from(utf8, 'hex');
164
+ } else {
165
+ // macOS / Linux 下如果有直接存 hex 或钥匙串导出
166
+ if (/^[0-9a-f]{64}$/i.test(raw)) return Buffer.from(raw, 'hex');
167
+ }
168
+ } catch {}
169
+ return null;
170
+ }
171
+
172
+ export function isEncrypted(v) {
173
+ return typeof v === 'string' && v.startsWith(ENC_PREFIX);
174
+ }
175
+
176
+ /** 解密 mrs1: 字符串 */
177
+ export function decryptWithKey(value, key) {
178
+ if (!isEncrypted(value)) return value;
179
+ if (!key) throw new Error('缺少 mirasim master key,无法解密');
180
+ const raw = Buffer.from(value.slice(ENC_PREFIX.length), 'base64');
181
+ if (raw.length < 28) throw new Error('mrs1 格式长度不正确');
182
+ const iv = raw.subarray(0, 12);
183
+ const tag = raw.subarray(12, 28);
184
+ const ct = raw.subarray(28);
185
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
186
+ decipher.setAuthTag(tag);
187
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
188
+ }
189
+
190
+ /** 加密成 mrs1: 字符串(切号写回时保证与客户端格式一致) */
191
+ export function encryptWithKey(plain, key) {
192
+ if (!key) return plain;
193
+ const iv = crypto.randomBytes(12);
194
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
195
+ const ct = Buffer.concat([cipher.update(String(plain), 'utf8'), cipher.final()]);
196
+ const tag = cipher.getAuthTag();
197
+ return ENC_PREFIX + Buffer.concat([iv, tag, ct]).toString('base64');
198
+ }
199
+
200
+ // ── 本机探测 ──
201
+
202
+ export function detectMirasim() {
203
+ const p = mirasimPaths();
204
+ const exists = fs.existsSync(p.setting);
205
+ let signedIn = false;
206
+ if (exists) {
207
+ const s = readJson(p.setting);
208
+ signedIn = Boolean(s && s.auth && (s.auth.token || s.auth.userId));
209
+ }
210
+ const clientPath = candidateClientPath();
211
+ return {
212
+ dataDir: p.home,
213
+ exists,
214
+ signedIn,
215
+ clientInstalled: Boolean(clientPath),
216
+ clientPath,
217
+ running: mirasimRunning(),
218
+ };
219
+ }
220
+
221
+ export function candidateClientPath() {
222
+ const home = os.homedir();
223
+ if (process.platform === 'win32') {
224
+ const local = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
225
+ const p1 = path.join(local, 'Programs', '@mirasimdesktop', 'Mirasim.exe');
226
+ if (fs.existsSync(p1)) return p1;
227
+ const p2 = path.join(process.env.ProgramFiles || 'C:\\Program Files', '@mirasimdesktop', 'Mirasim.exe');
228
+ if (fs.existsSync(p2)) return p2;
229
+ } else if (process.platform === 'darwin') {
230
+ const p = '/Applications/Mirasim.app/Contents/MacOS/Mirasim';
231
+ if (fs.existsSync(p)) return p;
232
+ }
233
+ return null;
234
+ }
235
+
236
+ export function mirasimRunning() {
237
+ if (process.platform !== 'win32') return false;
238
+ try {
239
+ const out = execSync('tasklist /FI "IMAGENAME eq Mirasim.exe" /NH', { windowsHide: true, encoding: 'utf8' });
240
+ return /mirasim\.exe/i.test(out);
241
+ } catch { return false; }
242
+ }
243
+
244
+ const nap = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
245
+
246
+ export function terminateMirasim({ timeoutMs = 8000 } = {}) {
247
+ if (process.platform !== 'win32') return { closed: false, supported: false };
248
+ if (!mirasimRunning()) return { closed: false, running: false };
249
+ try { execSync('taskkill /IM Mirasim.exe /T', { windowsHide: true, stdio: 'ignore' }); } catch {}
250
+ let deadline = Date.now() + timeoutMs;
251
+ while (Date.now() < deadline && mirasimRunning()) nap(250);
252
+ if (mirasimRunning()) {
253
+ try { execSync('taskkill /IM Mirasim.exe /T /F', { windowsHide: true, stdio: 'ignore' }); } catch {}
254
+ deadline = Date.now() + 3000;
255
+ while (Date.now() < deadline && mirasimRunning()) nap(250);
256
+ }
257
+ return { closed: !mirasimRunning(), running: mirasimRunning() };
258
+ }
259
+
260
+ // ── 当前会话读取与切换 ──
261
+
262
+ /** 读取当前登录的 mirasim 账号,返回标准化账号对象(供 addAccount / scan) */
263
+ export async function liveToAccount() {
264
+ const p = mirasimPaths();
265
+ const s = readJson(p.setting);
266
+ if (!s || !s.auth || !s.auth.token) return null;
267
+
268
+ const key = await getMasterKey();
269
+ let token = s.auth.token;
270
+ let refreshToken = s.auth.refreshToken;
271
+ if (isEncrypted(token)) {
272
+ if (!key) {
273
+ // 明确区分「已登录但缺密钥」「解密失败」「未登录(返回 null)」
274
+ const where = process.platform === 'win32' ? `${p.secretKey} / ${p.secretKeyEnc}` : p.secretKey;
275
+ const err = new Error(`已登录但读不到 mirasim 密钥(已检查 ${where} 与环境变量 MIRASIM_SECRET_KEY / MIRASIM_APP_SECRET_KEY)。请完全退出全部 Mirasim 进程后重启并登录,再重新扫描`);
276
+ err.code = 'MISSING_SECRET_KEY';
277
+ throw err;
278
+ }
279
+ try {
280
+ token = decryptWithKey(token, key);
281
+ refreshToken = refreshToken ? decryptWithKey(refreshToken, key) : null;
282
+ } catch (e) {
283
+ const err = new Error(`解密凭据失败:${e.message}`);
284
+ err.code = 'DECRYPT_FAILED';
285
+ throw err;
286
+ }
287
+ }
288
+
289
+ const userId = s.auth.userId || (token.includes('.') ? (() => {
290
+ try { return JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8')).sub; } catch { return null; }
291
+ })() : null);
292
+
293
+ const expiresAt = s.auth.exp ? new Date(s.auth.exp * 1000).toISOString() : null;
294
+
295
+ return {
296
+ provider: 'mirasim',
297
+ token,
298
+ refreshToken,
299
+ uid: userId,
300
+ name: s.auth.name || null,
301
+ expiresAt,
302
+ source: 'local-app',
303
+ meta: {
304
+ encryptedAuth: s.auth, // 原始密文备份,切换时可原样写回
305
+ capturedAt: new Date().toISOString(),
306
+ },
307
+ };
308
+ }
309
+
310
+ export function currentMirasimUid() {
311
+ const p = mirasimPaths();
312
+ const s = readJson(p.setting);
313
+ return s?.auth?.userId || null;
314
+ }
315
+
316
+ /**
317
+ * 把刷新后的凭据写回 ~/.mirasim/setting.json(仅当该账号正是客户端当前登录时调用,
318
+ * 避免 mirasim 客户端拿着被轮换掉的旧 token 掉线)。保留 setting.json 其余字段原样。
319
+ * @returns {boolean} 是否发生了写回
320
+ */
321
+ export async function writeMirasimAuth(account) {
322
+ const p = mirasimPaths();
323
+ const s = readJson(p.setting);
324
+ if (!s || !s.auth) return false;
325
+ if (!account?.uid || s.auth.userId !== account.uid) return false;
326
+ const key = await getMasterKey();
327
+ const enc = (v) => (key ? encryptWithKey(String(v), key) : String(v));
328
+ s.auth = {
329
+ ...s.auth,
330
+ token: enc(account.token),
331
+ refreshToken: account.refreshToken ? enc(account.refreshToken) : s.auth.refreshToken,
332
+ exp: account.expiresAt ? Math.floor(new Date(account.expiresAt).getTime() / 1000) : s.auth.exp,
333
+ };
334
+ atomicWriteJson(p.setting, s);
335
+ return true;
336
+ }
337
+
338
+ /**
339
+ * 切换 mirasim 当前登录账号
340
+ * @param {object} account 目标账号
341
+ * @param {{ force?: boolean }} opts
342
+ */
343
+ export async function switchTo(account, { force = false } = {}) {
344
+ const p = mirasimPaths();
345
+ const s = readJson(p.setting) || {};
346
+ const currentUid = s.auth?.userId || null;
347
+ if (account.uid && currentUid && account.uid === currentUid) {
348
+ return { switched: false, alreadyActive: true };
349
+ }
350
+
351
+ if (!force && mirasimRunning()) {
352
+ const err = new Error('Mirasim 客户端正在运行,请先退出后再切换(或强制切换,切换后将自动重新拉起客户端)');
353
+ err.mirasimRunning = true;
354
+ throw err;
355
+ }
356
+
357
+ const key = await getMasterKey();
358
+ let authToWrite = null;
359
+
360
+ if (account.meta?.encryptedAuth) {
361
+ // 优先原样使用抓取到的加密密文
362
+ authToWrite = account.meta.encryptedAuth;
363
+ } else if (key) {
364
+ // 新增或从外部导入的账号:使用当前 master key 加密写回
365
+ authToWrite = {
366
+ token: encryptWithKey(account.token, key),
367
+ refreshToken: account.refreshToken ? encryptWithKey(account.refreshToken, key) : null,
368
+ userId: account.uid,
369
+ name: account.name || null,
370
+ exp: account.expiresAt ? Math.floor(new Date(account.expiresAt).getTime() / 1000) : null,
371
+ };
372
+ } else {
373
+ // 没有 master key 时明文写入(mirasim 支持 unencrypted fallback)
374
+ authToWrite = {
375
+ token: account.token,
376
+ refreshToken: account.refreshToken || null,
377
+ userId: account.uid,
378
+ name: account.name || null,
379
+ exp: account.expiresAt ? Math.floor(new Date(account.expiresAt).getTime() / 1000) : null,
380
+ };
381
+ }
382
+
383
+ s.auth = authToWrite;
384
+ atomicWriteJson(p.setting, s);
385
+ return { switched: true, alreadyActive: false };
386
+ }