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 +5 -5
- package/src/backends/hub-client.js +159 -37
- package/src/main.js +626 -263
- package/src/tools/desktop-snapshot-tools.js +13 -6
- package/src/tools/hub-team-tools.js +106 -0
- package/src/tools/index.js +1 -0
- package/src/utils/desktop-snapshot.js +24 -3
- package/src/utils/hub-trust.js +116 -0
- package/src/utils/rpc-guard.js +237 -42
- package/test/docker-preflight-and-always-allow.test.js +26 -11
- package/test/rpc-guard-owner-hub.test.js +146 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cicy-desktop",
|
|
3
|
-
"version": "2.1.
|
|
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.
|
|
148
|
-
"cicy-code-darwin-arm64": "2.3.
|
|
149
|
-
"cicy-code-linux-x64": "2.3.
|
|
150
|
-
"cicy-code-linux-arm64": "2.3.
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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, {
|
|
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 {
|
|
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 {
|
|
113
|
+
} finally {
|
|
114
|
+
clearTimeout(t);
|
|
115
|
+
}
|
|
93
116
|
}
|
|
94
117
|
const msg = String((lastErr && lastErr.message) || lastErr || "fetch failed");
|
|
95
|
-
throw new Error(
|
|
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 {
|
|
136
|
+
try {
|
|
137
|
+
tok = String(readGlobalConfig(GLOBAL_JSON)?.api_token || "").trim();
|
|
138
|
+
} catch {}
|
|
110
139
|
if (!tok && process.platform === "win32") {
|
|
111
|
-
try {
|
|
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}`, {
|
|
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;
|
|
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 {
|
|
172
|
+
} finally {
|
|
173
|
+
clearTimeout(t);
|
|
174
|
+
}
|
|
129
175
|
}
|
|
130
|
-
const isNetErr = (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 (
|
|
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 {
|
|
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 || "")
|
|
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", {
|
|
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) {
|
|
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 = {
|
|
186
|
-
|
|
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") {
|
|
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", {
|
|
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) {
|
|
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 {
|
|
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,
|
|
224
|
-
|
|
225
|
-
|
|
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 {
|
|
229
|
-
|
|
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) {
|
|
232
|
-
|
|
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 {
|
|
265
|
-
|
|
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", {
|
|
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) {
|
|
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) {
|
|
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 };
|