@angelky7/dsh-browser-control 1.7.7 → 1.7.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.
@@ -87,7 +87,12 @@ export class BridgeServer {
87
87
  connectedAt;
88
88
  pending = new Map();
89
89
  lastError;
90
- continuationRemainder = '';
90
+ /**
91
+ * Payloads of the message being reassembled. **Buffers, not strings**: decoding
92
+ * each fragment with `toString('utf8')` corrupts any multi-byte character that
93
+ * straddles a fragment boundary. Decode once, at FIN.
94
+ */
95
+ continuationChunks = [];
91
96
  continuationOpen = false;
92
97
  constructor(options) {
93
98
  this.options = options;
@@ -202,7 +207,10 @@ export class BridgeServer {
202
207
  entry.onAbort = () => {
203
208
  if (!this.pending.delete(id))
204
209
  return;
205
- entry.reject(new Error(`browser command cancelled: ${command}`));
210
+ // 监听器只可能在 socket.write 之后触发(execute 从注册到写出是同步的、中间
211
+ // 没有 await),所以这里必须说 after send:写命令的结果是未知,而不是
212
+ // 「取消 = 什么都没发生」。before send 的那条检查在函数开头,措辞不同。
213
+ entry.reject(new Error(`browser command cancelled after send: ${command}`));
206
214
  };
207
215
  signal.addEventListener('abort', entry.onAbort, { once: true });
208
216
  }
@@ -215,8 +223,9 @@ export class BridgeServer {
215
223
  catch (error) {
216
224
  // One choke point: everything above this line that can fail is a link or
217
225
  // extension failure, and the caller needs to know whether the page may
218
- // already have been changed. See src/errors.ts.
219
- throw classify(command, error);
226
+ // already have been changed. See src/errors.ts. `params` matters for wait(fn):
227
+ // 谓词是任意 JS,超时/取消时按写命令报 unknown。
228
+ throw classify(command, error, params);
220
229
  }
221
230
  finally {
222
231
  clearTimeout(entry.timer);
@@ -359,19 +368,33 @@ export class BridgeServer {
359
368
  this.respondJson(res, 400, { ok: false, error: `invalid JSON body: ${errorMessage(error)}` });
360
369
  return;
361
370
  }
362
- if (typeof parsed.command !== 'string' || parsed.command.length === 0) {
371
+ // `null` parses fine and is NOT an object: without this guard the property
372
+ // reads below throw a TypeError inside an event callback (uncaught).
373
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
374
+ this.respondJson(res, 400, { ok: false, error: 'body must be {"command": "...", "params": {...}}' });
375
+ return;
376
+ }
377
+ const body = parsed;
378
+ if (typeof body.command !== 'string' || body.command.length === 0) {
363
379
  this.respondJson(res, 400, { ok: false, error: 'body must be {"command": "...", "params": {...}}' });
364
380
  return;
365
381
  }
366
- const timeoutMs = typeof parsed.timeoutMs === 'number' ? parsed.timeoutMs : undefined;
367
- this.execute(parsed.command, (parsed.params ?? {}), timeoutMs === undefined ? {} : { timeoutMs }).then((result) => this.respondJson(res, 200, { ok: true, result }), (error) => {
382
+ const timeoutMs = typeof body.timeoutMs === 'number' ? body.timeoutMs : undefined;
383
+ const options = timeoutMs === undefined ? {} : { timeoutMs };
384
+ // The plugin's controller carries the read-only / script gates and the audit
385
+ // trail; the raw execute() below is only the fallback for bare users.
386
+ const run = this.options.httpCommand ?? ((command, params, opts) => this.execute(command, params, opts));
387
+ run(body.command, (body.params ?? {}), options).then((result) => this.respondJson(res, 200, { ok: true, result }), (error) => {
368
388
  if (error instanceof BridgeError) {
369
- // 503 = 现在根本没法工作;504 = 超时且结果未知;502 = 扩展报了具体失败
389
+ // 503 = 现在根本没法工作;504 = 超时且结果未知;403 = 本机策略拦下(零副作用);
390
+ // 502 = 扩展报了具体失败
370
391
  const status = error.code === 'extension_offline' || error.code === 'bridge_disabled' || error.code === 'not_configured'
371
392
  ? 503
372
393
  : error.code === 'command_timeout' || error.code === 'link_lost'
373
394
  ? 504
374
- : 502;
395
+ : error.code === 'blocked_by_policy'
396
+ ? 403
397
+ : 502;
375
398
  this.respondJson(res, status, error.toJSON());
376
399
  return;
377
400
  }
@@ -425,11 +448,19 @@ export class BridgeServer {
425
448
  this.clientSocket = socket;
426
449
  this.hello = undefined;
427
450
  this.connectedAt = new Date();
428
- this.continuationRemainder = '';
451
+ this.continuationChunks = [];
429
452
  this.continuationOpen = false;
430
453
  this.log(`extension connected from ${socket.remoteAddress}:${socket.remotePort}`);
431
454
  const reader = new FrameReader();
432
455
  socket.on('data', (chunk) => {
456
+ // A replaced connection can still deliver buffered frames. Without this
457
+ // guard its pong/frames would be applied to the NEW link, and its close
458
+ // event could tear the new socket down (the close handler already checks,
459
+ // the data handler did not).
460
+ if (this.clientSocket !== socket) {
461
+ socket.destroy();
462
+ return;
463
+ }
433
464
  reader.push(chunk);
434
465
  try {
435
466
  reader.drain((frame) => this.handleFrame(frame));
@@ -454,7 +485,7 @@ export class BridgeServer {
454
485
  switch (frame.opcode) {
455
486
  case 0x1:
456
487
  case 0x0:
457
- this.handleText(frame.payload.toString('utf8'), frame.fin, frame.opcode);
488
+ this.handleText(frame.payload, frame.fin, frame.opcode);
458
489
  return;
459
490
  case 0x8:
460
491
  this.closeClientSocket(1000, '');
@@ -466,17 +497,19 @@ export class BridgeServer {
466
497
  return; // pong or binary: nothing to do
467
498
  }
468
499
  }
469
- handleText(text, fin, opcode) {
500
+ handleText(payload, fin, opcode) {
470
501
  if (opcode === 0x0 && !this.continuationOpen)
471
502
  return; // stray continuation
472
503
  if (opcode === 0x1)
473
- this.continuationRemainder = '';
504
+ this.continuationChunks = [];
474
505
  this.continuationOpen = opcode === 0x0 || !fin;
475
- this.continuationRemainder += text;
506
+ this.continuationChunks.push(payload);
476
507
  if (!fin)
477
508
  return;
478
- const full = this.continuationRemainder;
479
- this.continuationRemainder = '';
509
+ // Decode only the complete message: per-fragment toString('utf8') splits
510
+ // multi-byte characters at fragment boundaries and corrupts the JSON.
511
+ const full = Buffer.concat(this.continuationChunks).toString('utf8');
512
+ this.continuationChunks = [];
480
513
  this.continuationOpen = false;
481
514
  let msg;
482
515
  try {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@angelky7/dsh-browser-control",
3
3
  "description": "Local WebSocket bridge between dsh and the DSH Browser Control browser extension, with model-facing browser_* tools and a Settings-managed on/off switch",
4
- "version": "1.7.7",
4
+ "version": "1.7.9",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -37,7 +37,7 @@
37
37
  "build": "tsc -p tsconfig.json && tsdown",
38
38
  "typecheck": "tsc -p tsconfig.json --noEmit",
39
39
  "check": "npm run typecheck && npm run build && publint",
40
- "verify": "node verify/verify_config_patch.mjs && node verify/verify_peers.mjs && node verify/verify_surface.mjs && node verify/verify_tab_groups.mjs && node verify/verify_tabs_close_many.mjs && node verify/verify_cdp_retry.mjs && node verify/verify_snapshot_ax.mjs && node verify/verify_hard_lock.mjs && node verify/verify_policy.mjs && node verify/verify_cookies.mjs && node verify/verify_extension_e2e.mjs && node verify/verify_plugin.mjs legacy && node verify/verify_plugin.mjs conflict && node verify/verify_plugin.mjs new && node verify/verify_http.mjs && node verify/verify_errors.mjs && node verify/verify_hold.mjs && node verify/verify_protocol.mjs && node verify/verify_bundle_http.mjs && node verify/verify_nav_report.mjs && node verify/verify_nav_landing.mjs && node verify/verify_render_contract.mjs && node verify/verify_press_events.mjs && node verify/verify_nav_timeout.mjs && node verify/verify_nav_waituntil.mjs && node verify/verify_archive.mjs && node verify/verify_windows.mjs && node verify/verify_history_nav.mjs && node verify/verify_metrics_throttle.mjs && node verify/verify_drag.mjs && node verify/verify_audit.mjs && node verify/verify_journal.mjs && node verify/verify_sessions.mjs && node verify/verify_snapshot_page.mjs && node verify/verify_compat_matrix.mjs"
40
+ "verify": "node verify/verify_config_patch.mjs && node verify/verify_peers.mjs && node verify/verify_surface.mjs && node verify/verify_tab_groups.mjs && node verify/verify_tabs_close_many.mjs && node verify/verify_cdp_retry.mjs && node verify/verify_snapshot_ax.mjs && node verify/verify_hard_lock.mjs && node verify/verify_policy.mjs && node verify/verify_cookies.mjs && node verify/verify_extension_e2e.mjs && node verify/verify_plugin.mjs legacy && node verify/verify_plugin.mjs conflict && node verify/verify_plugin.mjs new && node verify/verify_http.mjs && node verify/verify_errors.mjs && node verify/verify_server_hardening.mjs && node verify/verify_hold.mjs && node verify/verify_protocol.mjs && node verify/verify_bundle_http.mjs && node verify/verify_nav_report.mjs && node verify/verify_nav_landing.mjs && node verify/verify_render_contract.mjs && node verify/verify_press_events.mjs && node verify/verify_nav_timeout.mjs && node verify/verify_nav_waituntil.mjs && node verify/verify_archive.mjs && node verify/verify_windows.mjs && node verify/verify_history_nav.mjs && node verify/verify_metrics_throttle.mjs && node verify/verify_drag.mjs && node verify/verify_audit.mjs && node verify/verify_journal.mjs && node verify/verify_sessions.mjs && node verify/verify_snapshot_page.mjs && node verify/verify_compat_matrix.mjs"
41
41
  },
42
42
  "publishConfig": {
43
43
  "access": "public",