cicy-desktop 2.1.325 → 2.1.327

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cicy-desktop",
3
- "version": "2.1.325",
3
+ "version": "2.1.327",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -144,10 +144,10 @@
144
144
  "//optionalDependencies": "(2026-06 回调): mac/linux 改回 native cicy-code(:8008,colima 在 16G mac 上压垮内存)→ 重新内置 cicy-code-<plat> / cicy-mihomo-<plat>,localbin.fromBundle 零网络 seed(npm 仅作更新通道,带 npmmirror→npmjs 回退)。这些由 scripts/sync-runtime-deps.cjs 在 tag-push 时同步到最新版。npm 的 os/cpu 字段保证每个平台只装自己那份。Windows 仍走 docker(WSL :8009),它那份 cicy-code-windows 用不到但 bundle 着无害。",
145
145
  "optionalDependencies": {
146
146
  "electron": "41.0.3",
147
- "cicy-code-darwin-x64": "2.3.593",
148
- "cicy-code-darwin-arm64": "2.3.593",
149
- "cicy-code-linux-x64": "2.3.593",
150
- "cicy-code-linux-arm64": "2.3.593",
147
+ "cicy-code-darwin-x64": "2.3.595",
148
+ "cicy-code-darwin-arm64": "2.3.595",
149
+ "cicy-code-linux-x64": "2.3.595",
150
+ "cicy-code-linux-arm64": "2.3.595",
151
151
  "cicy-code-windows-x64": "2.3.193",
152
152
  "cicy-mihomo-darwin-x64": "1.10.4",
153
153
  "cicy-mihomo-darwin-arm64": "1.10.4",
@@ -22,6 +22,7 @@ const path = require("path");
22
22
  const crypto = require("crypto");
23
23
  const log = require("electron-log");
24
24
  const { readGlobalConfig, updateGlobalConfig } = require("../utils/global-json");
25
+ const hubTrust = require("../utils/hub-trust");
25
26
 
26
27
  const GLOBAL_JSON = path.join(os.homedir(), "cicy-ai", "global.json");
27
28
  const DEFAULT_ORIGIN = "https://ws.cicy-ai.com";
@@ -48,17 +49,26 @@ function readAuth() {
48
49
  const c = readGlobalConfig(GLOBAL_JSON);
49
50
  const a = c && c.hubAuth;
50
51
  return a && a.token ? a : null;
51
- } catch { return null; }
52
+ } catch {
53
+ return null;
54
+ }
52
55
  }
53
56
 
54
57
  // Stable per-machine hub instance id for this desktop (hub requires `code-` +
55
58
  // 16..96 [A-Za-z0-9_-]).
56
59
  function desktopInstanceId() {
57
60
  let id = "";
58
- try { id = String(readGlobalConfig(GLOBAL_JSON)?.hubDesktopInstanceId || ""); } catch {}
61
+ try {
62
+ id = String(readGlobalConfig(GLOBAL_JSON)?.hubDesktopInstanceId || "");
63
+ } catch {}
59
64
  if (/^code-[A-Za-z0-9_-]{16,96}$/.test(id)) return id;
60
65
  id = "code-desktop-" + crypto.randomBytes(12).toString("hex");
61
- try { updateGlobalConfig(GLOBAL_JSON, (c) => { c.hubDesktopInstanceId = id; return c; }); } catch {}
66
+ try {
67
+ updateGlobalConfig(GLOBAL_JSON, (c) => {
68
+ c.hubDesktopInstanceId = id;
69
+ return c;
70
+ });
71
+ } catch {}
62
72
  return id;
63
73
  }
64
74
 
@@ -67,7 +77,10 @@ function desktopInstanceId() {
67
77
  // fetch ignores them, which showed up as "fetch failed" on PCs that can only
68
78
  // reach the hub through a proxy. Falls back to global fetch outside Electron.
69
79
  function pickFetch() {
70
- try { const { net, app } = require("electron"); if (net && typeof net.fetch === "function" && app && app.isReady()) return net.fetch.bind(net); } catch {}
80
+ try {
81
+ const { net, app } = require("electron");
82
+ if (net && typeof net.fetch === "function" && app && app.isReady()) return net.fetch.bind(net);
83
+ } catch {}
71
84
  return fetch;
72
85
  }
73
86
 
@@ -80,19 +93,33 @@ async function hubFetch(route, { method = "GET", token = "", body = null, tries
80
93
  const headers = { accept: "application/json" };
81
94
  if (token) headers.authorization = "Bearer " + token;
82
95
  if (body != null) headers["content-type"] = "application/json";
83
- const r = await pickFetch()(hubOrigin() + route, { method, headers, body: body == null ? undefined : JSON.stringify(body), signal: ctrl.signal, cache: "no-store" });
96
+ const r = await pickFetch()(hubOrigin() + route, {
97
+ method,
98
+ headers,
99
+ body: body == null ? undefined : JSON.stringify(body),
100
+ signal: ctrl.signal,
101
+ cache: "no-store",
102
+ });
84
103
  const text = await r.text();
85
104
  let json = null;
86
- try { json = text ? JSON.parse(text) : null; } catch {}
105
+ try {
106
+ json = text ? JSON.parse(text) : null;
107
+ } catch {}
87
108
  return { status: r.status, ok: r.ok, json, text };
88
109
  } catch (e) {
89
110
  lastErr = e;
90
111
  log.warn(`[hub] ${method} ${route} failed (${attempt + 1}/${tries}): ${e.message}`);
91
112
  if (attempt + 1 < tries) await new Promise((r) => setTimeout(r, 1500));
92
- } finally { clearTimeout(t); }
113
+ } finally {
114
+ clearTimeout(t);
115
+ }
93
116
  }
94
117
  const msg = String((lastErr && lastErr.message) || lastErr || "fetch failed");
95
- throw new Error(/fetch failed|ECONN|ENOTFOUND|abort/i.test(msg) ? `hub unreachable (${hubOrigin()}): ${msg}` : msg);
118
+ throw new Error(
119
+ /fetch failed|ECONN|ENOTFOUND|abort/i.test(msg)
120
+ ? `hub unreachable (${hubOrigin()}): ${msg}`
121
+ : msg
122
+ );
96
123
  }
97
124
 
98
125
  // ── fallback: the local cicy-code sidecar ───────────────────────────────────
@@ -106,9 +133,17 @@ let _sidecarTok = { value: "", at: 0 };
106
133
  async function sidecarToken() {
107
134
  if (_sidecarTok.value && Date.now() - _sidecarTok.at < 5 * 60 * 1000) return _sidecarTok.value;
108
135
  let tok = "";
109
- try { tok = String(readGlobalConfig(GLOBAL_JSON)?.api_token || "").trim(); } catch {}
136
+ try {
137
+ tok = String(readGlobalConfig(GLOBAL_JSON)?.api_token || "").trim();
138
+ } catch {}
110
139
  if (!tok && process.platform === "win32") {
111
- try { tok = String(await require("../sidecar/wsl-docker").readContainerToken(SIDECAR_PORT) || "").trim(); } catch (e) { log.warn(`[hub] sidecar token: ${e.message}`); }
140
+ try {
141
+ tok = String(
142
+ (await require("../sidecar/wsl-docker").readContainerToken(SIDECAR_PORT)) || ""
143
+ ).trim();
144
+ } catch (e) {
145
+ log.warn(`[hub] sidecar token: ${e.message}`);
146
+ }
112
147
  }
113
148
  if (tok) _sidecarTok = { value: tok, at: Date.now() };
114
149
  return tok;
@@ -121,16 +156,32 @@ async function sidecarFetch(route, { method = "GET", body = null } = {}) {
121
156
  try {
122
157
  const headers = { accept: "application/json", authorization: "Bearer " + tok };
123
158
  if (body != null) headers["content-type"] = "application/json";
124
- const r = await fetch(`http://127.0.0.1:${SIDECAR_PORT}${route}`, { method, headers, body: body == null ? undefined : JSON.stringify(body), signal: ctrl.signal, cache: "no-store" });
159
+ const r = await fetch(`http://127.0.0.1:${SIDECAR_PORT}${route}`, {
160
+ method,
161
+ headers,
162
+ body: body == null ? undefined : JSON.stringify(body),
163
+ signal: ctrl.signal,
164
+ cache: "no-store",
165
+ });
125
166
  const text = await r.text();
126
- let json = null; try { json = text ? JSON.parse(text) : null; } catch {}
167
+ let json = null;
168
+ try {
169
+ json = text ? JSON.parse(text) : null;
170
+ } catch {}
127
171
  return { status: r.status, ok: r.ok, json, text };
128
- } finally { clearTimeout(t); }
172
+ } finally {
173
+ clearTimeout(t);
174
+ }
129
175
  }
130
- const isNetErr = (e) => /unreachable|fetch failed|ECONN|ENOTFOUND|abort/i.test(String((e && e.message) || e));
176
+ const isNetErr = (e) =>
177
+ /unreachable|fetch failed|ECONN|ENOTFOUND|abort/i.test(String((e && e.message) || e));
131
178
 
132
179
  function errorOf(res, fallback) {
133
- return (res && res.json && (res.json.error || res.json.message)) || fallback || `HTTP ${res && res.status}`;
180
+ return (
181
+ (res && res.json && (res.json.error || res.json.message)) ||
182
+ fallback ||
183
+ `HTTP ${res && res.status}`
184
+ );
134
185
  }
135
186
 
136
187
  function stopPending(reason) {
@@ -140,7 +191,9 @@ function stopPending(reason) {
140
191
  }
141
192
 
142
193
  function fire(payload) {
143
- try { _onResult && _onResult(payload); } catch {}
194
+ try {
195
+ _onResult && _onResult(payload);
196
+ } catch {}
144
197
  }
145
198
 
146
199
  function status() {
@@ -154,14 +207,19 @@ function status() {
154
207
  }
155
208
 
156
209
  async function loginStart({ email, onResult } = {}) {
157
- const addr = String(email || "").trim().toLowerCase();
210
+ const addr = String(email || "")
211
+ .trim()
212
+ .toLowerCase();
158
213
  if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(addr)) throw new Error("invalid_email");
159
214
  stopPending("new login");
160
215
  _onResult = onResult;
161
216
  const instanceId = desktopInstanceId();
162
217
  const host = (os.hostname() || "desktop").split(".")[0];
163
218
  const name = `desktop-${host}-${instanceId.slice(-4)}`;
164
- const res = await hubFetch("/api/login/start", { method: "POST", body: { email: addr, instanceId, name, platform: "desktop-" + process.platform } });
219
+ const res = await hubFetch("/api/login/start", {
220
+ method: "POST",
221
+ body: { email: addr, instanceId, name, platform: "desktop-" + process.platform },
222
+ });
165
223
  if (!res.ok || !res.json || !res.json.state) throw new Error(errorOf(res, "login_start_failed"));
166
224
  const state = res.json.state;
167
225
  _pending = { state, email: addr, startedAt: Date.now(), timer: null };
@@ -177,19 +235,36 @@ function schedulePoll(state, delay = POLL_EVERY_MS) {
177
235
 
178
236
  async function pollOnce(state) {
179
237
  if (!_pending || _pending.state !== state) return;
180
- if (Date.now() - _pending.startedAt > LOGIN_TIMEOUT_MS) { stopPending("timeout"); fire({ error: "timeout" }); return; }
238
+ if (Date.now() - _pending.startedAt > LOGIN_TIMEOUT_MS) {
239
+ stopPending("timeout");
240
+ fire({ error: "timeout" });
241
+ return;
242
+ }
181
243
  try {
182
244
  const res = await hubFetch("/api/login/poll?state=" + encodeURIComponent(state));
183
245
  const j = res.json || {};
184
246
  if (j.status === "ready" && j.token) {
185
- const auth = { origin: hubOrigin(), token: j.token, owner: j.owner || _pending.email, instanceId: j.instanceId || desktopInstanceId(), savedAt: Date.now() };
186
- updateGlobalConfig(GLOBAL_JSON, (c) => { c.hubAuth = auth; return c; });
247
+ const auth = {
248
+ origin: hubOrigin(),
249
+ token: j.token,
250
+ owner: j.owner || _pending.email,
251
+ instanceId: j.instanceId || desktopInstanceId(),
252
+ savedAt: Date.now(),
253
+ };
254
+ updateGlobalConfig(GLOBAL_JSON, (c) => {
255
+ c.hubAuth = auth;
256
+ return c;
257
+ });
187
258
  stopPending("ready");
188
259
  log.info(`[hub] signed in as ${auth.owner}`);
189
260
  fire({ ok: true, owner: auth.owner });
190
261
  return;
191
262
  }
192
- if (j.status === "expired") { stopPending("expired"); fire({ error: "expired" }); return; }
263
+ if (j.status === "expired") {
264
+ stopPending("expired");
265
+ fire({ error: "expired" });
266
+ return;
267
+ }
193
268
  } catch (e) {
194
269
  log.warn(`[hub] poll error (retrying): ${e.message}`);
195
270
  }
@@ -200,7 +275,10 @@ async function loginCode({ code } = {}) {
200
275
  if (!_pending) throw new Error("no_pending_login");
201
276
  const c = String(code || "").replace(/\D/g, "");
202
277
  if (c.length !== 6) throw new Error("invalid_code");
203
- const res = await hubFetch("/api/login/code", { method: "POST", body: { state: _pending.state, code: c } });
278
+ const res = await hubFetch("/api/login/code", {
279
+ method: "POST",
280
+ body: { state: _pending.state, code: c },
281
+ });
204
282
  if (!res.ok) throw new Error(errorOf(res, "invalid_code"));
205
283
  // approved → the very next poll hands over the token
206
284
  if (_pending.timer) clearTimeout(_pending.timer);
@@ -209,27 +287,48 @@ async function loginCode({ code } = {}) {
209
287
  }
210
288
 
211
289
  function cancel() {
212
- if (_pending) { stopPending("cancelled"); fire({ error: "cancelled" }); }
290
+ if (_pending) {
291
+ stopPending("cancelled");
292
+ fire({ error: "cancelled" });
293
+ }
213
294
  return { ok: true };
214
295
  }
215
296
 
216
297
  function clearAuth() {
217
- try { updateGlobalConfig(GLOBAL_JSON, (c) => { delete c.hubAuth; return c; }); } catch {}
298
+ try {
299
+ updateGlobalConfig(GLOBAL_JSON, (c) => {
300
+ delete c.hubAuth;
301
+ return c;
302
+ });
303
+ } catch {}
304
+ try {
305
+ hubTrust.clearOwnerHubHost();
306
+ } catch {}
218
307
  }
219
308
 
220
309
  async function instances() {
221
310
  const a = readAuth();
222
311
  if (!a) return { ok: false, error: "not_logged_in", instances: [] };
223
- let res, viaSidecar = false;
224
- try { res = await hubFetch("/api/instances", { token: a.token }); }
225
- catch (e) {
312
+ let res,
313
+ viaSidecar = false;
314
+ try {
315
+ res = await hubFetch("/api/instances", { token: a.token });
316
+ } catch (e) {
226
317
  if (!isNetErr(e)) return { ok: false, error: e.message, instances: [] };
227
318
  log.warn(`[hub] direct instance list failed (${e.message}); trying the local cicy-code`);
228
- try { res = await sidecarFetch("/api/im/cicy-cloud/instances"); viaSidecar = true; }
229
- catch (e2) { return { ok: false, error: `${e.message}; sidecar: ${e2.message}`, instances: [] }; }
319
+ try {
320
+ res = await sidecarFetch("/api/im/cicy-cloud/instances");
321
+ viaSidecar = true;
322
+ } catch (e2) {
323
+ return { ok: false, error: `${e.message}; sidecar: ${e2.message}`, instances: [] };
324
+ }
230
325
  }
231
- if (res.status === 401 && !viaSidecar) { clearAuth(); return { ok: false, error: "unauthorized", instances: [] }; }
232
- if (!res.ok || !res.json) return { ok: false, error: errorOf(res, "instances_failed"), instances: [] };
326
+ if (res.status === 401 && !viaSidecar) {
327
+ clearAuth();
328
+ return { ok: false, error: "unauthorized", instances: [] };
329
+ }
330
+ if (!res.ok || !res.json)
331
+ return { ok: false, error: errorOf(res, "instances_failed"), instances: [] };
233
332
  const list = Array.isArray(res.json.instances) ? res.json.instances : [];
234
333
  const out = list
235
334
  // direct: `self` is this desktop's hidden pseudo-instance; via sidecar: `self` is the
@@ -261,22 +360,45 @@ async function grantUrl({ id, port = 0, next = "/" } = {}) {
261
360
  const a = readAuth();
262
361
  if (!a) throw new Error("not_logged_in");
263
362
  let res;
264
- try { res = await hubFetch("/api/gateway/grant", { method: "POST", token: a.token, body: { instanceId: String(id || ""), port: Number(port) || 0, next: String(next || "/") } }); }
265
- catch (e) {
363
+ try {
364
+ res = await hubFetch("/api/gateway/grant", {
365
+ method: "POST",
366
+ token: a.token,
367
+ body: { instanceId: String(id || ""), port: Number(port) || 0, next: String(next || "/") },
368
+ });
369
+ } catch (e) {
266
370
  if (!isNetErr(e)) throw e;
267
371
  log.warn(`[hub] direct grant failed (${e.message}); trying the local cicy-code`);
268
- res = await sidecarFetch("/api/im/cicy-cloud/open", { method: "POST", body: { instance_id: String(id || ""), port: Number(port) || 0, next: String(next || "/") } });
372
+ res = await sidecarFetch("/api/im/cicy-cloud/open", {
373
+ method: "POST",
374
+ body: { instance_id: String(id || ""), port: Number(port) || 0, next: String(next || "/") },
375
+ });
269
376
  if (!res.ok || !res.json || !res.json.url) throw new Error(errorOf(res, "grant_failed"));
377
+ try {
378
+ hubTrust.recordOwnerHubHost(res.json.host);
379
+ } catch {}
270
380
  return { url: res.json.url, host: res.json.host };
271
381
  }
272
- if (res.status === 401) { clearAuth(); throw new Error("unauthorized"); }
382
+ if (res.status === 401) {
383
+ clearAuth();
384
+ throw new Error("unauthorized");
385
+ }
273
386
  if (!res.ok || !res.json || !res.json.url) throw new Error(errorOf(res, "grant_failed"));
387
+ // The grant was minted with THIS desktop's own hubAuth token, so its host
388
+ // provably belongs to the owner email — trust it for owner-scoped dangerous RPC.
389
+ try {
390
+ hubTrust.recordOwnerHubHost(res.json.host);
391
+ } catch {}
274
392
  return { url: res.json.url, host: res.json.host };
275
393
  }
276
394
 
277
395
  async function logout() {
278
396
  const a = readAuth();
279
- if (a) { try { await hubFetch("/api/logout", { method: "POST", token: a.token }); } catch {} }
397
+ if (a) {
398
+ try {
399
+ await hubFetch("/api/logout", { method: "POST", token: a.token });
400
+ } catch {}
401
+ }
280
402
  clearAuth();
281
403
  stopPending("logout");
282
404
  return { ok: true };