@mrrisega/dsh-remote 0.6.9-beta.3 → 0.6.9

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.
@@ -529,14 +529,27 @@
529
529
  };
530
530
  SeE2eeWs.prototype.send = function (data) {
531
531
  var self = this;
532
- var p;
533
- if (typeof data === "string") p = this._sealSend(data, 0);
534
- else if (typeof Blob !== "undefined" && data instanceof Blob) p = data.arrayBuffer().then(function (ab) { return self._sealSend(new Uint8Array(ab), 1); });
535
- else if (data instanceof ArrayBuffer) p = this._sealSend(new Uint8Array(data), 1);
536
- else if (ArrayBuffer.isView(data)) p = this._sealSend(new Uint8Array(data.buffer, data.byteOffset || 0, data.byteLength || data.length), 1);
537
- else p = Promise.reject(seErr("bad_ws_data", "e2ee: 不支持的 WS 消息类型"));
538
- this._sendQ = this._sendQ.then(function () { return p; })
539
- .catch(function (e) { seNotifyFail("⚠ 无法加密 WS 消息: " + (e && e.message || e)); });
532
+ /* ⚠️ 消息体在**调用时刻**归一化(同步完成,保持调用者看到的语义),
533
+ 但**加密与发送必须排进 `_sendQ` 队列里做**。
534
+ 2026-09-19 修:原先写成
535
+ if (typeof data === "string") p = this._sealSend(data, 0);
536
+ this._sendQ = this._sendQ.then(function () { return p; });
537
+ 队列只串了 **promise**,没有串住"开始加密"这个动作 —— 两次背靠背的 `send()`
538
+ 会**各自立刻**发起一次 seal(WebCrypto 走线程池,完成顺序无保证),而
539
+ `counter: this._p2b++` 是在 seal **之前**求值的,于是**计数 1 的信封可能先于
540
+ 计数 0 发出**。WebSocket 本该保证消息有序,这条路被破坏了。
541
+ (它是间歇性的:端到端用例 `e2ee-shim.test.mjs` 的 WS 顺序断言约 1/5 概率变红,
542
+ 一直被当成"flaky 测试",其实是它抓到了真竞态。)
543
+ 现在把 `_sealSend` 整体推迟到队列里执行,计数器与发送顺序严格一致。 */
544
+ var norm;
545
+ if (typeof data === "string") norm = Promise.resolve({ payload: data, t: 0 });
546
+ else if (typeof Blob !== "undefined" && data instanceof Blob) norm = data.arrayBuffer().then(function (ab) { return { payload: new Uint8Array(ab), t: 1 }; });
547
+ else if (data instanceof ArrayBuffer) norm = Promise.resolve({ payload: new Uint8Array(data), t: 1 });
548
+ else if (ArrayBuffer.isView(data)) norm = Promise.resolve({ payload: new Uint8Array(data.buffer, data.byteOffset || 0, data.byteLength || data.length), t: 1 });
549
+ else norm = Promise.reject(seErr("bad_ws_data", "e2ee: 不支持的 WS 消息类型"));
550
+ this._sendQ = this._sendQ.then(function () {
551
+ return norm.then(function (m) { return self._sealSend(m.payload, m.t); });
552
+ }).catch(function (e) { seNotifyFail("⚠ 无法加密 WS 消息: " + (e && e.message || e)); });
540
553
  };
541
554
  SeE2eeWs.prototype._sealSend = async function (payload, t) {
542
555
  var env = await this._meta.sess.seal({ kind: "w", dir: SE_DIR_P2B, counter: this._p2b++, data: payload, t: t, wsLabel: this._meta.wsLabel });
@@ -427,6 +427,54 @@ test("WS 包装:URL 追加 &e2ee=&w=,逐消息封包(计数递增)且桥端可
427
427
  ws.close(1000);
428
428
  });
429
429
 
430
+ test("WS 包装:连续 send 的计数与发送顺序严格一致(防并行的 seal 打乱顺序)", async () => {
431
+ /**
432
+ * 这条是上面那条的**确定性加强版**,用来稳定复现一个竞态:
433
+ *
434
+ * 旧实现里 `send()` 会**立刻**调用 `_sealSend()`(`counter: this._p2b++` 当场求值),
435
+ * 而 `_sendQ` 只串了 promise、**没有串住"开始加密"这一步**。于是两次背靠背的
436
+ * `send()` 各自并行发起一次 seal,谁先完成谁先发 —— **WebSocket 的消息有序性被破坏**。
437
+ * 旧代码下上面那条用例约 1/5 概率变红,长期被误当成"flaky 测试";其实是它抓到了真竞态。
438
+ *
439
+ * 上面那条依赖 WebCrypto 的完成顺序,**不够确定**(实测把消息加到 8 条也可能侥幸通过)。
440
+ * 这里改用**确定性手法**:把第一次 `seal` 故意拖慢 —— 只要实现还会并行发起 seal,
441
+ * 第一条就一定排在后面,用例必然失败;把"开始加密"排进队列后必然通过。
442
+ */
443
+ const seS = makeSeSession();
444
+ const peer = bridgePeer(seS.sessId, seS.shk, seS.saltH);
445
+ const ORIGIN = "http://relay.local";
446
+
447
+ let sealCalls = 0;
448
+ const realSeal = seS.seal.bind(seS);
449
+ seS.seal = async function (opt) {
450
+ sealCalls += 1;
451
+ if (sealCalls === 1) await new Promise((r) => setTimeout(r, 80)); // 只拖慢第一条
452
+ return realSeal(opt);
453
+ };
454
+
455
+ const patched = shim.seMakeWsCtor(FakeNative, { sess: seS, origin: ORIGIN });
456
+ const ws = patched(ORIGIN + "/api/events.mux?x=1");
457
+ const inner = ws._inner;
458
+
459
+ const N = 6;
460
+ for (let i = 0; i < N; i++) ws.send("msg-" + i);
461
+ await until(() => inner.sent.length === N);
462
+ assert.equal(inner.sent.length, N, `${N} 条都发出去了`);
463
+
464
+ const parts = shim.seWsLabelParts(inner.url.slice(inner.url.indexOf("/api")));
465
+ for (let i = 0; i < N; i++) {
466
+ const env = JSON.parse(inner.sent[i]);
467
+ assert.equal(
468
+ env.c,
469
+ i,
470
+ `第 ${i} 条信封的计数应为 ${i} —— 顺序被打乱了(说明 seal 又变成并行发起了)`
471
+ );
472
+ const opened = peer.open({ kind: "w", dir: "p2b", env, counter: env.c, wsLabel: parts.wsLabel });
473
+ assert.equal(Buffer.from(opened.data).toString("utf8"), "msg-" + i, `第 ${i} 条内容要对应`);
474
+ }
475
+ ws.close(1000);
476
+ });
477
+
430
478
  test("WS 包装:下行信封解密→原文事件;binaryType=arraybuffer;重放 → 1008 关闭", async () => {
431
479
  const seS = makeSeSession();
432
480
  const peer = bridgePeer(seS.sessId, seS.shk, seS.saltH);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrisega/dsh-remote",
3
- "version": "0.6.9-beta.3",
3
+ "version": "0.6.9",
4
4
  "description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -2441,7 +2441,7 @@ const PLUGIN_ID = "dsh-remote-web";
2441
2441
  const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
2442
2442
  const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
2443
2443
  /** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
2444
- const PLUGIN_VERSION = "0.6.9-beta.3";
2444
+ const PLUGIN_VERSION = "0.6.9";
2445
2445
  const UPDATE_LOG = ".dsh-update.log";
2446
2446
  const UPDATE_MARKER = ".dsh-update-running";
2447
2447
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-web",
3
- "version": "0.6.9-beta.3",
3
+ "version": "0.6.9",
4
4
  "description": "公网远程控制 DeepSeek Harness(dsh web):安装即得专属加密地址,人在外面也能用手机访问电脑上的 dsh——无需同一局域网/WiFi、无需公网 IP 与内网穿透,全程加密;手机端 100% 还原电脑体验(对话/工具/审批/设置)。技术用户可选自建服务,流量走自己的服务器。Remote control DeepSeek Harness (dsh web) from anywhere over the public internet — install, get an encrypted URL, use it from your phone on any network (dual-half cordis plugin: Settings panel + same-origin /dsh-remote routes). (2026-09 由 dsh-remote-ui 更名 / renamed from dsh-remote-ui; dsh-remote 的 dsh web 插件半,不是纯 UI/皮肤插件)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",