ds-harness-remote 0.3.31 → 0.3.33

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.
Files changed (41) hide show
  1. package/README.md +5 -5
  2. package/cordis.patch.yml +1 -1
  3. package/dist/client-runtime.d.ts +2 -1
  4. package/dist/client-runtime.d.ts.map +1 -1
  5. package/dist/client-runtime.js +74 -31
  6. package/dist/client-runtime.js.map +1 -1
  7. package/dist/client.github.js +90 -39
  8. package/dist/client.js +89 -38
  9. package/dist/client.js.map +1 -1
  10. package/dist/control-runtime.d.ts.map +1 -1
  11. package/dist/control-runtime.js +1 -0
  12. package/dist/control-runtime.js.map +1 -1
  13. package/dist/harness-api-bridge.d.ts +2 -1
  14. package/dist/harness-api-bridge.d.ts.map +1 -1
  15. package/dist/harness-api-bridge.js +30 -2
  16. package/dist/harness-api-bridge.js.map +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1372 -94
  20. package/dist/index.js.map +4 -4
  21. package/dist/native-rtc-helper.d.ts +4 -0
  22. package/dist/native-rtc-helper.d.ts.map +1 -0
  23. package/dist/native-rtc-helper.js +539 -0
  24. package/dist/native-rtc-helper.js.map +1 -0
  25. package/dist/server-api.d.ts +2 -0
  26. package/dist/server-api.d.ts.map +1 -1
  27. package/dist/server-api.js +3 -0
  28. package/dist/server-api.js.map +1 -1
  29. package/dist/server-connection.d.ts.map +1 -1
  30. package/dist/server-connection.js +43 -2
  31. package/dist/server-connection.js.map +1 -1
  32. package/dist/service.d.ts.map +1 -1
  33. package/dist/service.js +6 -3
  34. package/dist/service.js.map +1 -1
  35. package/dist/version.d.ts +1 -1
  36. package/dist/version.js +1 -1
  37. package/dist/werift-rtc.d.ts +45 -9
  38. package/dist/werift-rtc.d.ts.map +1 -1
  39. package/dist/werift-rtc.js +356 -35
  40. package/dist/werift-rtc.js.map +1 -1
  41. package/package.json +4 -1
package/dist/index.js CHANGED
@@ -4574,7 +4574,7 @@ var RTC_DATA_CHANNEL_OPTIONS = { ordered: true };
4574
4574
  function browserRtcFactory() {
4575
4575
  return {
4576
4576
  create(configuration) {
4577
- const raw = new RTCPeerConnection(configuration);
4577
+ const raw = new RTCPeerConnection({ iceServers: configuration.iceServers });
4578
4578
  return {
4579
4579
  get connectionState() {
4580
4580
  return raw.connectionState;
@@ -4646,6 +4646,97 @@ function browserRtcFactory() {
4646
4646
  }
4647
4647
  };
4648
4648
  }
4649
+ function summarizeIceCandidate(candidate) {
4650
+ const value = candidate?.candidate?.trim();
4651
+ if (value === void 0 || value.length === 0)
4652
+ return emptyCandidateSummary();
4653
+ const parts = value.replace(/^candidate:/, "").split(/\s+/);
4654
+ const typeIndex = parts.indexOf("typ");
4655
+ const relatedAddressIndex = parts.indexOf("raddr");
4656
+ return {
4657
+ candidateType: parseCandidateType(parts[typeIndex + 1]),
4658
+ protocol: normalizeProtocol(parts[2]),
4659
+ ...summarizeAddress(parts[4]),
4660
+ ...relatedAddressIndex < 0 ? {} : relatedAddressSummary(parts[relatedAddressIndex + 1])
4661
+ };
4662
+ }
4663
+ function summarizeAddress(address) {
4664
+ if (address === void 0 || address.length === 0)
4665
+ return { addressFamily: "unknown", addressScope: "unknown" };
4666
+ if (address.includes(":"))
4667
+ return { addressFamily: "ipv6", addressScope: summarizeIpv6Scope(address) };
4668
+ const parts = address.split(".").map(Number);
4669
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
4670
+ return { addressFamily: "unknown", addressScope: "unknown" };
4671
+ }
4672
+ return { addressFamily: "ipv4", addressScope: summarizeIpv4Scope(parts) };
4673
+ }
4674
+ function stunOnlyIceServers(iceServers) {
4675
+ const direct = [];
4676
+ for (const server of iceServers) {
4677
+ const sourceUrls = Array.isArray(server.urls) ? server.urls : [server.urls];
4678
+ const urls = sourceUrls.filter(isStunUrl);
4679
+ if (urls.length === 0)
4680
+ continue;
4681
+ direct.push({ urls: Array.isArray(server.urls) ? urls : urls[0] });
4682
+ }
4683
+ return direct;
4684
+ }
4685
+ function isStunUrl(url) {
4686
+ const value = url.trim().toLowerCase();
4687
+ return value.startsWith("stun:") || value.startsWith("stuns:");
4688
+ }
4689
+ function emptyCandidateSummary() {
4690
+ return { candidateType: "unknown", addressFamily: "unknown", addressScope: "unknown" };
4691
+ }
4692
+ function parseCandidateType(value) {
4693
+ if (value === "host" || value === "srflx" || value === "prflx" || value === "relay")
4694
+ return value;
4695
+ return "unknown";
4696
+ }
4697
+ function normalizeProtocol(value) {
4698
+ if (value === void 0 || value.length === 0)
4699
+ return void 0;
4700
+ return value.toLowerCase();
4701
+ }
4702
+ function relatedAddressSummary(address) {
4703
+ const summary = summarizeAddress(address);
4704
+ return {
4705
+ relatedAddressFamily: summary.addressFamily,
4706
+ relatedAddressScope: summary.addressScope
4707
+ };
4708
+ }
4709
+ function summarizeIpv4Scope(parts) {
4710
+ if (parts[0] === 0 || parts[0] >= 240)
4711
+ return "reserved";
4712
+ if (parts[0] === 127)
4713
+ return "loopback";
4714
+ if (parts[0] === 169 && parts[1] === 254)
4715
+ return "link-local";
4716
+ if (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127)
4717
+ return "cgnat";
4718
+ if (parts[0] >= 224)
4719
+ return "reserved";
4720
+ if (parts[0] === 10)
4721
+ return "private";
4722
+ if (parts[0] === 192 && parts[1] === 168)
4723
+ return "private";
4724
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
4725
+ return "private";
4726
+ return "public";
4727
+ }
4728
+ function summarizeIpv6Scope(address) {
4729
+ const value = address.toLowerCase();
4730
+ if (value === "::1")
4731
+ return "loopback";
4732
+ if (value.startsWith("fe80:"))
4733
+ return "link-local";
4734
+ if (value.startsWith("fc") || value.startsWith("fd"))
4735
+ return "private";
4736
+ if (value.startsWith("ff"))
4737
+ return "reserved";
4738
+ return "public";
4739
+ }
4649
4740
  function adaptDataChannel(raw) {
4650
4741
  return {
4651
4742
  get label() {
@@ -4823,6 +4914,8 @@ var RtcDataChannelTransport = class {
4823
4914
  negotiateTimeoutMs;
4824
4915
  channelLabel;
4825
4916
  sendTimeoutMs;
4917
+ label;
4918
+ onDiagnostic;
4826
4919
  channel;
4827
4920
  remoteCandidates = [];
4828
4921
  remoteDescriptionSet = false;
@@ -4843,6 +4936,12 @@ var RtcDataChannelTransport = class {
4843
4936
  bytesReceived = 0;
4844
4937
  selected;
4845
4938
  selectedMode;
4939
+ selectedPathTelemetry;
4940
+ localCandidateTelemetry = emptyCandidateTelemetry();
4941
+ remoteCandidateTelemetry = emptyCandidateTelemetry();
4942
+ candidatePairTelemetry = emptyCandidatePairTelemetry();
4943
+ filteredLocalCandidateTelemetry = emptyCandidateTelemetry();
4944
+ filteredCandidatePairTelemetry = emptyCandidateTelemetry();
4846
4945
  lastBufferedAmount = 0;
4847
4946
  constructor(options) {
4848
4947
  this.role = options.role;
@@ -4850,15 +4949,30 @@ var RtcDataChannelTransport = class {
4850
4949
  this.negotiateTimeoutMs = options.negotiateTimeoutMs ?? DEFAULT_NEGOTIATE_TIMEOUT_MS;
4851
4950
  this.channelLabel = options.channelLabel ?? RTC_DATA_CHANNEL_LABEL;
4852
4951
  this.sendTimeoutMs = options.sendTimeoutMs;
4853
- this.pc = options.factory.create({ iceServers: options.iceServers });
4952
+ this.label = options.label;
4953
+ this.onDiagnostic = options.onDiagnostic;
4954
+ this.pc = options.factory.create({
4955
+ iceServers: options.iceServers,
4956
+ onDiagnostic: (event) => this.handlePeerDiagnostic(event)
4957
+ });
4854
4958
  this.pc.ondatachannel = (event) => this.adoptChannel(event.channel);
4855
4959
  this.pc.onicecandidate = (event) => {
4856
4960
  if (event.candidate !== null && !this.closed) {
4961
+ const candidate = summarizeIceCandidate(event.candidate);
4962
+ recordCandidate(this.localCandidateTelemetry, candidate);
4963
+ this.emitDiagnostic({ type: "local-candidate", candidate, diagnostics: this.diagnostics() });
4857
4964
  this.onSignal({ type: "ice", candidate: event.candidate });
4858
4965
  }
4859
4966
  };
4860
- this.pc.onconnectionstatechange = () => this.checkConnectionState();
4861
- this.pc.oniceconnectionstatechange = () => this.checkConnectionState();
4967
+ this.pc.onconnectionstatechange = () => {
4968
+ this.emitStateChange();
4969
+ this.checkConnectionState();
4970
+ };
4971
+ this.pc.oniceconnectionstatechange = () => {
4972
+ this.emitStateChange();
4973
+ this.checkConnectionState();
4974
+ };
4975
+ this.pc.onicegatheringstatechange = () => this.emitStateChange();
4862
4976
  }
4863
4977
  /** Begin negotiation; resolves when the DataChannel is open. */
4864
4978
  connect(signal) {
@@ -4871,7 +4985,7 @@ var RtcDataChannelTransport = class {
4871
4985
  this.openResolve = resolve2;
4872
4986
  this.openReject = reject;
4873
4987
  this.negotiateTimer = setTimeout(() => {
4874
- this.failOpen(new RtcConnectError("RTC_CONNECT_TIMEOUT", `WebRTC negotiation timed out after ${this.negotiateTimeoutMs}ms.`));
4988
+ void this.failOpenAfterStats(new RtcConnectError("RTC_CONNECT_TIMEOUT", `WebRTC negotiation timed out after ${this.negotiateTimeoutMs}ms.`));
4875
4989
  }, this.negotiateTimeoutMs);
4876
4990
  if (this.role === "initiator") {
4877
4991
  void this.startInitiator().catch((error) => this.failOpen(asError(error)));
@@ -4922,10 +5036,25 @@ var RtcDataChannelTransport = class {
4922
5036
  selectedPathMode() {
4923
5037
  return this.selectedMode;
4924
5038
  }
5039
+ diagnostics() {
5040
+ return {
5041
+ role: this.role,
5042
+ ...this.label === void 0 ? {} : { label: this.label },
5043
+ connectionState: this.pc.connectionState,
5044
+ iceConnectionState: this.pc.iceConnectionState,
5045
+ iceGatheringState: this.pc.iceGatheringState,
5046
+ localCandidates: cloneCandidateTelemetry(this.localCandidateTelemetry),
5047
+ remoteCandidates: cloneCandidateTelemetry(this.remoteCandidateTelemetry),
5048
+ candidatePairs: cloneCandidatePairTelemetry(this.candidatePairTelemetry),
5049
+ filteredLocalCandidates: cloneCandidateTelemetry(this.filteredLocalCandidateTelemetry),
5050
+ filteredCandidatePairs: cloneCandidateTelemetry(this.filteredCandidatePairTelemetry),
5051
+ ...this.selectedPathTelemetry === void 0 ? {} : { selectedPath: { ...this.selectedPathTelemetry } }
5052
+ };
5053
+ }
4925
5054
  async connectionDetails() {
4926
5055
  let statsDetails = {};
4927
5056
  try {
4928
- statsDetails = inspectSelectedPath(await this.pc.getStats());
5057
+ statsDetails = await this.refreshStatsDiagnostics();
4929
5058
  } catch {
4930
5059
  }
4931
5060
  return {
@@ -4933,7 +5062,8 @@ var RtcDataChannelTransport = class {
4933
5062
  mode: statsDetails.mode ?? this.selectedMode,
4934
5063
  connectionState: this.pc.connectionState,
4935
5064
  iceConnectionState: this.pc.iceConnectionState,
4936
- dataChannelState: this.channel?.readyState
5065
+ dataChannelState: this.channel?.readyState,
5066
+ diagnostics: this.diagnostics()
4937
5067
  };
4938
5068
  }
4939
5069
  getStats() {
@@ -5029,13 +5159,19 @@ var RtcDataChannelTransport = class {
5029
5159
  }
5030
5160
  }
5031
5161
  async handleIce(candidate) {
5162
+ const summary = summarizeIceCandidate(candidate);
5032
5163
  if (!this.remoteDescriptionSet) {
5164
+ recordCandidate(this.remoteCandidateTelemetry, summary);
5165
+ this.emitDiagnostic({ type: "remote-candidate", candidate: summary, action: "buffered", diagnostics: this.diagnostics() });
5033
5166
  this.remoteCandidates.push(candidate);
5034
5167
  return;
5035
5168
  }
5036
5169
  try {
5037
5170
  await this.pc.addIceCandidate(candidate);
5171
+ recordCandidate(this.remoteCandidateTelemetry, summary);
5172
+ this.emitDiagnostic({ type: "remote-candidate", candidate: summary, action: "added", diagnostics: this.diagnostics() });
5038
5173
  } catch {
5174
+ this.emitDiagnostic({ type: "remote-candidate", candidate: summary, action: "ignored", diagnostics: this.diagnostics() });
5039
5175
  }
5040
5176
  }
5041
5177
  async flushRemoteCandidates() {
@@ -5100,9 +5236,15 @@ var RtcDataChannelTransport = class {
5100
5236
  }
5101
5237
  async resolveSelectedTransport() {
5102
5238
  try {
5103
- const selected = inspectSelectedPath(await this.pc.getStats());
5239
+ const selected = await this.refreshStatsDiagnostics();
5104
5240
  this.selected = selected?.transport;
5105
5241
  this.selectedMode = selected?.mode;
5242
+ this.selectedPathTelemetry = selectedPathTelemetry(selected);
5243
+ this.emitDiagnostic({
5244
+ type: "selected-path",
5245
+ selectedPath: this.selectedPathTelemetry,
5246
+ diagnostics: this.diagnostics()
5247
+ });
5106
5248
  } catch {
5107
5249
  this.selected = void 0;
5108
5250
  this.selectedMode = void 0;
@@ -5112,9 +5254,16 @@ var RtcDataChannelTransport = class {
5112
5254
  if (this.closed || this.opened)
5113
5255
  return;
5114
5256
  if (this.pc.connectionState === "failed" || this.pc.iceConnectionState === "failed" || this.pc.connectionState === "closed" || this.pc.iceConnectionState === "closed") {
5115
- this.failOpen(new RtcConnectError("RTC_FAILED", "WebRTC peer connection failed before the data channel opened."));
5257
+ void this.failOpenAfterStats(new RtcConnectError("RTC_FAILED", "WebRTC peer connection failed before the data channel opened."));
5116
5258
  }
5117
5259
  }
5260
+ async failOpenAfterStats(error) {
5261
+ await Promise.race([
5262
+ this.refreshStatsDiagnostics().catch(() => void 0),
5263
+ sleep(250)
5264
+ ]);
5265
+ this.failOpen(error);
5266
+ }
5118
5267
  failOpen(error) {
5119
5268
  if (this.closed || this.opened || this.openReject === void 0)
5120
5269
  return;
@@ -5176,6 +5325,46 @@ var RtcDataChannelTransport = class {
5176
5325
  this.watchdogTimer = void 0;
5177
5326
  this.lastBufferedAmount = 0;
5178
5327
  }
5328
+ async refreshStatsDiagnostics() {
5329
+ const stats = [...await this.pc.getStats()];
5330
+ this.candidatePairTelemetry = inspectCandidatePairs(stats);
5331
+ return inspectSelectedPath(stats);
5332
+ }
5333
+ handlePeerDiagnostic(event) {
5334
+ if (event.type === "local-candidate-filtered") {
5335
+ recordCandidate(this.filteredLocalCandidateTelemetry, event.candidate);
5336
+ this.emitDiagnostic({
5337
+ type: event.type,
5338
+ candidate: event.candidate,
5339
+ reason: event.reason,
5340
+ diagnostics: this.diagnostics()
5341
+ });
5342
+ return;
5343
+ }
5344
+ if (event.localCandidate !== void 0)
5345
+ recordCandidate(this.filteredCandidatePairTelemetry, event.localCandidate);
5346
+ this.emitDiagnostic({
5347
+ type: event.type,
5348
+ localCandidate: event.localCandidate,
5349
+ reason: event.reason,
5350
+ diagnostics: this.diagnostics()
5351
+ });
5352
+ }
5353
+ emitStateChange() {
5354
+ this.emitDiagnostic({
5355
+ type: "state-change",
5356
+ connectionState: this.pc.connectionState,
5357
+ iceConnectionState: this.pc.iceConnectionState,
5358
+ iceGatheringState: this.pc.iceGatheringState,
5359
+ diagnostics: this.diagnostics()
5360
+ });
5361
+ }
5362
+ emitDiagnostic(event) {
5363
+ try {
5364
+ this.onDiagnostic?.(event);
5365
+ } catch {
5366
+ }
5367
+ }
5179
5368
  };
5180
5369
  var RtcConnectError = class extends Error {
5181
5370
  code;
@@ -5237,6 +5426,33 @@ function inspectSelectedPath(stats) {
5237
5426
  }
5238
5427
  return {};
5239
5428
  }
5429
+ function inspectCandidatePairs(stats) {
5430
+ const candidates = /* @__PURE__ */ new Map();
5431
+ const telemetry = emptyCandidatePairTelemetry();
5432
+ const pairs = [];
5433
+ for (const [reportId, entry] of stats) {
5434
+ if (entry.type === "local-candidate" || entry.type === "remote-candidate") {
5435
+ if (typeof entry.candidateType === "string" && entry.id !== void 0) {
5436
+ candidates.set(String(entry.id), entry);
5437
+ }
5438
+ } else if (entry.type === "candidate-pair") {
5439
+ pairs.push({ ...entry, id: entry.id ?? reportId });
5440
+ }
5441
+ }
5442
+ for (const pair of pairs) {
5443
+ telemetry.total += 1;
5444
+ if (pair.nominated === true)
5445
+ telemetry.nominated += 1;
5446
+ if (pair.selected === true)
5447
+ telemetry.selected += 1;
5448
+ increment(telemetry.byState, stringStat(pair, "state") ?? (pair.selected === true ? "selected" : "unknown"));
5449
+ const local = candidates.get(String(pair.localCandidateId));
5450
+ const remote = candidates.get(String(pair.remoteCandidateId));
5451
+ recordCandidateStats(telemetry.byLocalType, telemetry.byLocalFamily, telemetry.byLocalScope, local);
5452
+ recordCandidateStats(telemetry.byRemoteType, telemetry.byRemoteFamily, telemetry.byRemoteScope, remote);
5453
+ }
5454
+ return telemetry;
5455
+ }
5240
5456
  function candidateAddress(candidate) {
5241
5457
  const address = stringStat(candidate, "address") ?? stringStat(candidate, "ip");
5242
5458
  if (address === void 0)
@@ -5252,6 +5468,91 @@ function numberStat(entry, key) {
5252
5468
  const value = entry?.[key];
5253
5469
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
5254
5470
  }
5471
+ function emptyCandidateTelemetry() {
5472
+ return {
5473
+ total: 0,
5474
+ byType: {},
5475
+ byFamily: {},
5476
+ byScope: {},
5477
+ byProtocol: {}
5478
+ };
5479
+ }
5480
+ function cloneCandidateTelemetry(value) {
5481
+ return {
5482
+ total: value.total,
5483
+ byType: { ...value.byType },
5484
+ byFamily: { ...value.byFamily },
5485
+ byScope: { ...value.byScope },
5486
+ byProtocol: { ...value.byProtocol }
5487
+ };
5488
+ }
5489
+ function emptyCandidatePairTelemetry() {
5490
+ return {
5491
+ total: 0,
5492
+ nominated: 0,
5493
+ selected: 0,
5494
+ byState: {},
5495
+ byLocalType: {},
5496
+ byRemoteType: {},
5497
+ byLocalFamily: {},
5498
+ byRemoteFamily: {},
5499
+ byLocalScope: {},
5500
+ byRemoteScope: {}
5501
+ };
5502
+ }
5503
+ function cloneCandidatePairTelemetry(value) {
5504
+ return {
5505
+ total: value.total,
5506
+ nominated: value.nominated,
5507
+ selected: value.selected,
5508
+ byState: { ...value.byState },
5509
+ byLocalType: { ...value.byLocalType },
5510
+ byRemoteType: { ...value.byRemoteType },
5511
+ byLocalFamily: { ...value.byLocalFamily },
5512
+ byRemoteFamily: { ...value.byRemoteFamily },
5513
+ byLocalScope: { ...value.byLocalScope },
5514
+ byRemoteScope: { ...value.byRemoteScope }
5515
+ };
5516
+ }
5517
+ function recordCandidate(telemetry, candidate) {
5518
+ telemetry.total += 1;
5519
+ increment(telemetry.byType, candidate.candidateType);
5520
+ increment(telemetry.byFamily, candidate.addressFamily);
5521
+ increment(telemetry.byScope, candidate.addressScope);
5522
+ if (candidate.protocol !== void 0)
5523
+ increment(telemetry.byProtocol, candidate.protocol);
5524
+ }
5525
+ function recordCandidateStats(byType, byFamily, byScope, candidate) {
5526
+ const candidateType = candidateTypeStat(candidate);
5527
+ const address = summarizeAddress(candidateAddressValue(candidate));
5528
+ increment(byType, candidateType);
5529
+ increment(byFamily, address.addressFamily);
5530
+ increment(byScope, address.addressScope);
5531
+ }
5532
+ function candidateTypeStat(candidate) {
5533
+ const value = stringStat(candidate, "candidateType");
5534
+ if (value === "host" || value === "srflx" || value === "prflx" || value === "relay")
5535
+ return value;
5536
+ return "unknown";
5537
+ }
5538
+ function candidateAddressValue(candidate) {
5539
+ return stringStat(candidate, "address") ?? stringStat(candidate, "ip");
5540
+ }
5541
+ function increment(map, key) {
5542
+ map[key] = (map[key] ?? 0) + 1;
5543
+ }
5544
+ function selectedPathTelemetry(details) {
5545
+ return {
5546
+ ...details.transport === void 0 ? {} : { transport: details.transport },
5547
+ ...details.mode === void 0 ? {} : { mode: details.mode },
5548
+ ...details.localCandidateType === void 0 ? {} : { localCandidateType: details.localCandidateType },
5549
+ ...details.remoteCandidateType === void 0 ? {} : { remoteCandidateType: details.remoteCandidateType },
5550
+ ...details.protocol === void 0 ? {} : { protocol: details.protocol },
5551
+ ...details.relayProtocol === void 0 ? {} : { relayProtocol: details.relayProtocol },
5552
+ ...details.currentRoundTripTimeMs === void 0 ? {} : { currentRoundTripTimeMs: details.currentRoundTripTimeMs },
5553
+ ...details.availableOutgoingBitrate === void 0 ? {} : { availableOutgoingBitrate: details.availableOutgoingBitrate }
5554
+ };
5555
+ }
5255
5556
  function requireSdp(description) {
5256
5557
  if (typeof description.sdp !== "string" || description.sdp.length === 0) {
5257
5558
  throw new RtcConnectError("RTC_FAILED", "The RTC backend produced an empty session description.");
@@ -5266,6 +5567,9 @@ function toArrayBuffer(data) {
5266
5567
  function asError(error) {
5267
5568
  return error instanceof Error ? error : new RtcConnectError("RTC_FAILED", "WebRTC negotiation failed.");
5268
5569
  }
5570
+ function sleep(ms) {
5571
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
5572
+ }
5269
5573
 
5270
5574
  // ../webrtc/dist/adaptive-transport.js
5271
5575
  var DEFAULT_CAPABILITIES = ["transport.p2p", "transport.turn", "transport.relay", "harness.api.v1"];
@@ -5288,6 +5592,7 @@ var AdaptiveTransport = class extends BaseTransport {
5288
5592
  webrtcEnabled = true;
5289
5593
  serverNegotiateTimeoutMs;
5290
5594
  connectedAt;
5595
+ lastRtcDiagnostics;
5291
5596
  constructor(url, options) {
5292
5597
  super();
5293
5598
  this.url = url;
@@ -5487,6 +5792,7 @@ var AdaptiveTransport = class extends BaseTransport {
5487
5792
  const preferred = this.options.preferredTransports ?? [...DEFAULT_PREFERRED_TRANSPORTS];
5488
5793
  const wantWebRtc = !this.options.forceRelay && this.webrtcEnabled && (preferred.includes("lan") || preferred.includes("p2p") || preferred.includes("turn"));
5489
5794
  let selected = "relay";
5795
+ this.lastRtcDiagnostics = void 0;
5490
5796
  if (wantWebRtc) {
5491
5797
  try {
5492
5798
  const rtcSelected = await this.tryWebRtc();
@@ -5495,7 +5801,7 @@ var AdaptiveTransport = class extends BaseTransport {
5495
5801
  } catch (error) {
5496
5802
  const reason = error instanceof Error ? error : new Error("WebRTC negotiation failed.");
5497
5803
  try {
5498
- this.options.onWebRtcFallback?.(reason);
5804
+ this.options.onWebRtcFallback?.(reason, this.lastRtcDiagnostics);
5499
5805
  } catch {
5500
5806
  }
5501
5807
  await this.rtc?.close();
@@ -5534,8 +5840,15 @@ var AdaptiveTransport = class extends BaseTransport {
5534
5840
  factory,
5535
5841
  iceServers,
5536
5842
  onSignal: (signal) => this.sendRtcSignal(signal),
5537
- negotiateTimeoutMs: this.serverNegotiateTimeoutMs ?? this.options.negotiateTimeoutMs,
5538
- label: `client->${this.options.targetDeviceId}`
5843
+ negotiateTimeoutMs: negotiateTimeout(this.serverNegotiateTimeoutMs, this.options.negotiateTimeoutMs),
5844
+ label: `client->${this.options.targetDeviceId}`,
5845
+ onDiagnostic: (event) => {
5846
+ this.lastRtcDiagnostics = event.diagnostics;
5847
+ try {
5848
+ this.options.onWebRtcDiagnostic?.(event);
5849
+ } catch {
5850
+ }
5851
+ }
5539
5852
  });
5540
5853
  this.rtc = rtc;
5541
5854
  rtc.onMessage((data) => {
@@ -5549,10 +5862,12 @@ var AdaptiveTransport = class extends BaseTransport {
5549
5862
  try {
5550
5863
  await rtc.connect();
5551
5864
  } catch (error) {
5865
+ this.lastRtcDiagnostics = rtc.diagnostics();
5552
5866
  await rtc.close();
5553
5867
  this.rtc = void 0;
5554
5868
  throw error;
5555
5869
  }
5870
+ this.lastRtcDiagnostics = rtc.diagnostics();
5556
5871
  return rtc.selectedTransport() ?? "p2p";
5557
5872
  }
5558
5873
  sendRtcSignal(signal) {
@@ -5625,6 +5940,13 @@ function socketState(readyState) {
5625
5940
  return "closing";
5626
5941
  return "closed";
5627
5942
  }
5943
+ function negotiateTimeout(serverMs, localMs) {
5944
+ if (serverMs === void 0)
5945
+ return localMs;
5946
+ if (localMs === void 0)
5947
+ return serverMs;
5948
+ return Math.max(serverMs, localMs);
5949
+ }
5628
5950
 
5629
5951
  // src/api-proxy-switch.ts
5630
5952
  var SWITCHED_DOMAINS = [
@@ -13212,7 +13534,7 @@ function normalizeServerUrl(value) {
13212
13534
  }
13213
13535
 
13214
13536
  // src/version.ts
13215
- var PLUGIN_VERSION = "0.3.31";
13537
+ var PLUGIN_VERSION = "0.3.33";
13216
13538
 
13217
13539
  // src/server-api.ts
13218
13540
  var HostServerApi = class {
@@ -13226,9 +13548,13 @@ var HostServerApi = class {
13226
13548
  identity;
13227
13549
  credentials;
13228
13550
  credentialsPromise;
13551
+ harnessVersion;
13229
13552
  bindIdentity(identity) {
13230
13553
  this.identity = identity;
13231
13554
  }
13555
+ setHarnessVersion(version) {
13556
+ this.harnessVersion = version;
13557
+ }
13232
13558
  currentAuthorization() {
13233
13559
  if (this.credentials === void 0) return void 0;
13234
13560
  return {
@@ -13435,7 +13761,8 @@ var HostServerApi = class {
13435
13761
  role: this.role,
13436
13762
  platform: platform(),
13437
13763
  identityKey: identity.publicKey,
13438
- clientVersion: PLUGIN_VERSION
13764
+ clientVersion: PLUGIN_VERSION,
13765
+ ...this.role === "host" && this.harnessVersion !== void 0 ? { harnessVersion: this.harnessVersion } : {}
13439
13766
  };
13440
13767
  }
13441
13768
  saveTokens(identity, tokens, authorization) {
@@ -13646,33 +13973,662 @@ function isRemoteCommandMethod(method) {
13646
13973
  }
13647
13974
 
13648
13975
  // src/werift-rtc.ts
13976
+ import { createSocket } from "node:dgram";
13649
13977
  import { networkInterfaces } from "node:os";
13978
+
13979
+ // src/native-rtc-helper.ts
13980
+ import { spawn, spawnSync } from "node:child_process";
13981
+ import { existsSync, readdirSync, statSync } from "node:fs";
13982
+ import { delimiter, dirname, join } from "node:path";
13983
+ import { fileURLToPath } from "node:url";
13984
+ var cachedExternalFactory;
13985
+ var cachedExternalFactoryResolved = false;
13986
+ var cachedNodeBinary;
13987
+ var cachedNodeBinaryResolved = false;
13988
+ async function loadExternalNativeRtcFactory() {
13989
+ if (cachedExternalFactory !== void 0) return cachedExternalFactory;
13990
+ if (cachedExternalFactoryResolved) return void 0;
13991
+ cachedExternalFactoryResolved = true;
13992
+ const nodeBinary = resolveExternalNodeBinaryForRtc();
13993
+ if (nodeBinary === void 0) return void 0;
13994
+ cachedExternalFactory = buildExternalNativeRtcFactory(nodeBinary);
13995
+ return cachedExternalFactory;
13996
+ }
13997
+ function buildExternalNativeRtcFactory(nodeBinary, requireFrom = fileURLToPath(import.meta.url)) {
13998
+ return {
13999
+ create(configuration) {
14000
+ return new ExternalNativePeerConnection(nodeBinary, requireFrom, configuration);
14001
+ }
14002
+ };
14003
+ }
14004
+ function resolveExternalNodeBinaryForRtc() {
14005
+ if (cachedNodeBinaryResolved) return cachedNodeBinary;
14006
+ cachedNodeBinaryResolved = true;
14007
+ const requireFrom = fileURLToPath(import.meta.url);
14008
+ for (const candidate of nodeBinaryCandidates()) {
14009
+ if (isUsableExternalNode(candidate, requireFrom)) {
14010
+ cachedNodeBinary = candidate;
14011
+ return candidate;
14012
+ }
14013
+ }
14014
+ return void 0;
14015
+ }
14016
+ function nodeBinaryCandidates() {
14017
+ const candidates = [];
14018
+ const add3 = (value) => {
14019
+ if (value === void 0 || value.trim() === "") return;
14020
+ if (!candidates.includes(value)) candidates.push(value);
14021
+ };
14022
+ add3(process.env.DSH_REMOTE_NODE);
14023
+ add3(process.env.NODE);
14024
+ add3(process.execPath);
14025
+ for (const part of (process.env.PATH ?? "").split(delimiter)) add3(join(part, process.platform === "win32" ? "node.exe" : "node"));
14026
+ add3("/opt/homebrew/bin/node");
14027
+ add3("/usr/local/bin/node");
14028
+ add3("/usr/bin/node");
14029
+ add3(join(process.env.HOME ?? "", ".volta", "bin", process.platform === "win32" ? "node.exe" : "node"));
14030
+ add3(join(process.env.HOME ?? "", ".asdf", "shims", process.platform === "win32" ? "node.exe" : "node"));
14031
+ add3(join(process.env.HOME ?? "", ".local", "bin", process.platform === "win32" ? "node.exe" : "node"));
14032
+ for (const nvmNode of nvmNodeCandidates()) add3(nvmNode);
14033
+ return candidates;
14034
+ }
14035
+ function nvmNodeCandidates() {
14036
+ const root = join(process.env.HOME ?? "", ".nvm", "versions", "node");
14037
+ if (root === "" || !existsSync(root)) return [];
14038
+ try {
14039
+ return readdirSync(root).map((version) => join(root, version, "bin", process.platform === "win32" ? "node.exe" : "node")).sort((left, right) => right.localeCompare(left, "en", { numeric: true }));
14040
+ } catch {
14041
+ return [];
14042
+ }
14043
+ }
14044
+ function isUsableExternalNode(candidate, requireFrom) {
14045
+ if (!isExecutableFile(candidate)) return false;
14046
+ const env = { ...process.env, DSH_REMOTE_RTC_HELPER_REQUIRE_FROM: requireFrom };
14047
+ delete env.ELECTRON_RUN_AS_NODE;
14048
+ const probe = spawnSync(candidate, [
14049
+ "--input-type=module",
14050
+ "--eval",
14051
+ [
14052
+ "import { createRequire } from 'node:module';",
14053
+ "const require = createRequire(process.env.DSH_REMOTE_RTC_HELPER_REQUIRE_FROM);",
14054
+ "if (process.versions.electron) throw new Error('electron-node');",
14055
+ "const wrtc = require('@roamhq/wrtc');",
14056
+ "const RTCPeerConnection = wrtc.RTCPeerConnection ?? wrtc.default?.RTCPeerConnection;",
14057
+ "if (typeof RTCPeerConnection !== 'function') throw new Error('missing-rtc');",
14058
+ "console.log(`node:${process.versions.node}`);"
14059
+ ].join("")
14060
+ ], {
14061
+ cwd: dirname(requireFrom),
14062
+ env,
14063
+ encoding: "utf8",
14064
+ timeout: 3e3
14065
+ });
14066
+ return probe.status === 0 && probe.stdout.startsWith("node:");
14067
+ }
14068
+ function isExecutableFile(candidate) {
14069
+ try {
14070
+ const stat4 = statSync(candidate);
14071
+ if (!stat4.isFile()) return false;
14072
+ if (process.platform === "win32") return true;
14073
+ return (stat4.mode & 73) !== 0;
14074
+ } catch {
14075
+ return false;
14076
+ }
14077
+ }
14078
+ var ExternalNativePeerConnection = class {
14079
+ constructor(nodeBinary, requireFrom, configuration) {
14080
+ this.nodeBinary = nodeBinary;
14081
+ this.requireFrom = requireFrom;
14082
+ this.child = spawn(this.nodeBinary, ["--input-type=module", "--eval", HELPER_SOURCE], {
14083
+ cwd: dirname(this.requireFrom),
14084
+ env: { ...process.env, DSH_REMOTE_RTC_HELPER_REQUIRE_FROM: this.requireFrom },
14085
+ stdio: ["pipe", "pipe", "pipe"]
14086
+ });
14087
+ this.child.stdout.setEncoding("utf8");
14088
+ this.child.stdout.on("data", (chunk) => this.handleStdout(String(chunk)));
14089
+ this.child.stderr.on("data", () => void 0);
14090
+ this.child.on("exit", () => this.handleExit());
14091
+ this.child.on("error", (error) => this.handleExit(error));
14092
+ this.child.stdin.on("error", (error) => this.handleExit(error));
14093
+ this.ready = this.request("init", { iceServers: configuration.iceServers ?? [] }).then(() => void 0);
14094
+ }
14095
+ connectionState = "new";
14096
+ iceConnectionState = "new";
14097
+ iceGatheringState = "new";
14098
+ signalingState = "stable";
14099
+ onconnectionstatechange = null;
14100
+ oniceconnectionstatechange = null;
14101
+ onicegatheringstatechange = null;
14102
+ onicecandidate = null;
14103
+ ondatachannel = null;
14104
+ child;
14105
+ pending = /* @__PURE__ */ new Map();
14106
+ channels = /* @__PURE__ */ new Map();
14107
+ nextRequestId = 1;
14108
+ nextChannelId = 1;
14109
+ stdout = "";
14110
+ closed = false;
14111
+ ready;
14112
+ createDataChannel(label, options) {
14113
+ const channelId = this.nextChannelId++;
14114
+ const channel = new ExternalNativeDataChannel(this, channelId, label, options?.ordered ?? true);
14115
+ this.channels.set(channelId, channel);
14116
+ this.notify("createDataChannel", { channelId, label, options: { ordered: options?.ordered ?? true } });
14117
+ return channel;
14118
+ }
14119
+ async createOffer() {
14120
+ await this.ready;
14121
+ return this.request("createOffer", {});
14122
+ }
14123
+ async createAnswer() {
14124
+ await this.ready;
14125
+ return this.request("createAnswer", {});
14126
+ }
14127
+ async setLocalDescription(description) {
14128
+ await this.ready;
14129
+ await this.request("setLocalDescription", { description });
14130
+ }
14131
+ async setRemoteDescription(description) {
14132
+ await this.ready;
14133
+ await this.request("setRemoteDescription", { description });
14134
+ }
14135
+ async addIceCandidate(candidate) {
14136
+ await this.ready;
14137
+ await this.request("addIceCandidate", { candidate: candidate ?? null });
14138
+ }
14139
+ async getStats() {
14140
+ await this.ready;
14141
+ return this.request("getStats", {});
14142
+ }
14143
+ close() {
14144
+ if (this.closed) return;
14145
+ this.closed = true;
14146
+ this.notify("close", {});
14147
+ const timer = setTimeout(() => {
14148
+ if (this.child.exitCode === null && this.child.signalCode === null) this.child.kill();
14149
+ }, 1e3);
14150
+ timer.unref?.();
14151
+ }
14152
+ sendChannelData(channelId, data) {
14153
+ const payload = typeof data === "string" ? { channelId, text: data } : { channelId, base64: Buffer.from(data).toString("base64") };
14154
+ this.notify("channelSend", payload);
14155
+ }
14156
+ closeChannel(channelId) {
14157
+ this.notify("channelClose", { channelId });
14158
+ }
14159
+ request(method, payload) {
14160
+ if (this.closed) return Promise.reject(new Error("native rtc helper is closed"));
14161
+ const id = this.nextRequestId++;
14162
+ const promise = new Promise((resolve2, reject) => {
14163
+ this.pending.set(id, { resolve: resolve2, reject });
14164
+ });
14165
+ this.write({ id, method, payload });
14166
+ return promise;
14167
+ }
14168
+ notify(method, payload) {
14169
+ if (this.closed && method !== "close") return;
14170
+ this.write({ method, payload });
14171
+ }
14172
+ write(message) {
14173
+ try {
14174
+ this.child.stdin.write(`${JSON.stringify(message)}
14175
+ `);
14176
+ } catch {
14177
+ this.handleExit(new Error("native rtc helper stdin is closed"));
14178
+ }
14179
+ }
14180
+ handleStdout(chunk) {
14181
+ this.stdout += chunk;
14182
+ for (; ; ) {
14183
+ const newline = this.stdout.indexOf("\n");
14184
+ if (newline < 0) return;
14185
+ const line = this.stdout.slice(0, newline);
14186
+ this.stdout = this.stdout.slice(newline + 1);
14187
+ if (line.trim() === "") continue;
14188
+ try {
14189
+ this.handleMessage(JSON.parse(line));
14190
+ } catch {
14191
+ this.handleExit(new Error("native rtc helper sent invalid JSON"));
14192
+ }
14193
+ }
14194
+ }
14195
+ handleMessage(message) {
14196
+ if ("id" in message) {
14197
+ const pending = this.pending.get(message.id);
14198
+ if (pending === void 0) return;
14199
+ this.pending.delete(message.id);
14200
+ if (message.ok) pending.resolve(message.value);
14201
+ else pending.reject(new Error(message.error ?? "native rtc helper command failed"));
14202
+ return;
14203
+ }
14204
+ if (message.event === "state") {
14205
+ this.connectionState = message.connectionState;
14206
+ this.iceConnectionState = message.iceConnectionState;
14207
+ this.iceGatheringState = message.iceGatheringState;
14208
+ this.signalingState = message.signalingState;
14209
+ this.onconnectionstatechange?.();
14210
+ this.oniceconnectionstatechange?.();
14211
+ this.onicegatheringstatechange?.();
14212
+ return;
14213
+ }
14214
+ if (message.event === "icecandidate") {
14215
+ this.onicecandidate?.({ candidate: message.candidate });
14216
+ return;
14217
+ }
14218
+ if (message.event === "datachannel") {
14219
+ const channel = this.channelFor(message.channelId, message.label, message.ordered);
14220
+ this.ondatachannel?.({ channel });
14221
+ channel.updateState(message.readyState, message.bufferedAmount);
14222
+ return;
14223
+ }
14224
+ if (message.event === "channel-state") {
14225
+ this.channels.get(message.channelId)?.updateState(message.readyState, message.bufferedAmount);
14226
+ return;
14227
+ }
14228
+ if (message.event === "channel-message") {
14229
+ const channel = this.channels.get(message.channelId);
14230
+ if (channel === void 0) return;
14231
+ if (message.text !== void 0) channel.emitMessage(message.text);
14232
+ else if (message.base64 !== void 0) channel.emitMessage(bufferToArrayBuffer(Buffer.from(message.base64, "base64")));
14233
+ return;
14234
+ }
14235
+ if (message.event === "channel-error") {
14236
+ this.channels.get(message.channelId)?.emitError();
14237
+ }
14238
+ }
14239
+ channelFor(channelId, label, ordered) {
14240
+ const existing = this.channels.get(channelId);
14241
+ if (existing !== void 0) return existing;
14242
+ const channel = new ExternalNativeDataChannel(this, channelId, label, ordered);
14243
+ this.channels.set(channelId, channel);
14244
+ return channel;
14245
+ }
14246
+ handleExit(error) {
14247
+ if (!this.closed) this.closed = true;
14248
+ const reason = error ?? new Error("native rtc helper exited");
14249
+ for (const pending of this.pending.values()) pending.reject(reason);
14250
+ this.pending.clear();
14251
+ for (const channel of this.channels.values()) channel.updateState("closed", 0);
14252
+ }
14253
+ };
14254
+ var ExternalNativeDataChannel = class {
14255
+ constructor(owner, channelId, label, ordered) {
14256
+ this.owner = owner;
14257
+ this.channelId = channelId;
14258
+ this.label = label;
14259
+ this.ordered = ordered;
14260
+ }
14261
+ readyState = "connecting";
14262
+ bufferedAmount = 0;
14263
+ binaryType = "arraybuffer";
14264
+ onopen = null;
14265
+ onmessage = null;
14266
+ onclose = null;
14267
+ onerror = null;
14268
+ onbufferedamountlow = null;
14269
+ send(data) {
14270
+ this.owner.sendChannelData(this.channelId, data);
14271
+ }
14272
+ close() {
14273
+ this.owner.closeChannel(this.channelId);
14274
+ }
14275
+ updateState(readyState, bufferedAmount) {
14276
+ const previous = this.readyState;
14277
+ this.readyState = readyState;
14278
+ this.bufferedAmount = bufferedAmount;
14279
+ if (previous !== "open" && readyState === "open") this.onopen?.();
14280
+ if (previous !== "closed" && readyState === "closed") this.onclose?.();
14281
+ if (bufferedAmount === 0) this.onbufferedamountlow?.();
14282
+ }
14283
+ emitMessage(data) {
14284
+ this.onmessage?.({ data });
14285
+ }
14286
+ emitError() {
14287
+ this.onerror?.();
14288
+ }
14289
+ };
14290
+ function bufferToArrayBuffer(buffer) {
14291
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
14292
+ }
14293
+ var HELPER_SOURCE = String.raw`
14294
+ import { createRequire } from 'node:module'
14295
+ import { createInterface } from 'node:readline'
14296
+
14297
+ const require = createRequire(process.env.DSH_REMOTE_RTC_HELPER_REQUIRE_FROM || (process.cwd() + '/package.json'))
14298
+ const wrtc = require('@roamhq/wrtc')
14299
+ const RTCPeerConnection = wrtc.RTCPeerConnection ?? wrtc.default?.RTCPeerConnection
14300
+ if (typeof RTCPeerConnection !== 'function') throw new Error('missing RTCPeerConnection')
14301
+
14302
+ let pc
14303
+ let nextRemoteChannelId = 100000
14304
+ const channels = new Map()
14305
+ let chain = Promise.resolve()
14306
+
14307
+ function send(message) {
14308
+ process.stdout.write(JSON.stringify(message) + '\n')
14309
+ }
14310
+
14311
+ function respond(id, ok, value, error) {
14312
+ if (id === undefined) return
14313
+ send(ok ? { id, ok: true, value } : { id, ok: false, error: String(error?.message ?? error ?? 'command failed') })
14314
+ }
14315
+
14316
+ function state() {
14317
+ return {
14318
+ event: 'state',
14319
+ connectionState: pc?.connectionState ?? 'closed',
14320
+ iceConnectionState: pc?.iceConnectionState ?? 'closed',
14321
+ iceGatheringState: pc?.iceGatheringState ?? 'complete',
14322
+ signalingState: pc?.signalingState ?? 'closed',
14323
+ }
14324
+ }
14325
+
14326
+ function emitState() {
14327
+ send(state())
14328
+ }
14329
+
14330
+ function normalizeCandidate(candidate) {
14331
+ if (candidate === null || candidate === undefined) return null
14332
+ if (typeof candidate.toJSON === 'function') return candidate.toJSON()
14333
+ return {
14334
+ candidate: candidate.candidate,
14335
+ sdpMid: candidate.sdpMid,
14336
+ sdpMLineIndex: candidate.sdpMLineIndex,
14337
+ usernameFragment: candidate.usernameFragment,
14338
+ }
14339
+ }
14340
+
14341
+ function attachChannel(channelId, channel) {
14342
+ channels.set(channelId, channel)
14343
+ try { channel.binaryType = 'arraybuffer' } catch {}
14344
+ const emitChannel = () => send({
14345
+ event: 'channel-state',
14346
+ channelId,
14347
+ readyState: channel.readyState,
14348
+ bufferedAmount: channel.bufferedAmount ?? 0,
14349
+ })
14350
+ channel.onopen = emitChannel
14351
+ channel.onclose = emitChannel
14352
+ channel.onerror = () => send({ event: 'channel-error', channelId })
14353
+ channel.onmessage = event => {
14354
+ const data = event.data
14355
+ if (typeof data === 'string') {
14356
+ send({ event: 'channel-message', channelId, text: data })
14357
+ return
14358
+ }
14359
+ send({ event: 'channel-message', channelId, base64: Buffer.from(data).toString('base64') })
14360
+ }
14361
+ send({
14362
+ event: 'datachannel',
14363
+ channelId,
14364
+ label: channel.label,
14365
+ ordered: channel.ordered,
14366
+ readyState: channel.readyState,
14367
+ bufferedAmount: channel.bufferedAmount ?? 0,
14368
+ })
14369
+ }
14370
+
14371
+ function serializeStats(stats) {
14372
+ const entries = []
14373
+ const push = (key, value) => {
14374
+ const copy = {}
14375
+ for (const [entryKey, entryValue] of Object.entries(value ?? {})) {
14376
+ if (typeof entryValue !== 'function') copy[entryKey] = entryValue
14377
+ }
14378
+ entries.push([String(key), copy])
14379
+ }
14380
+ if (typeof stats?.forEach === 'function') stats.forEach((value, key) => push(key, value))
14381
+ else for (const [key, value] of stats ?? []) push(key, value)
14382
+ return entries
14383
+ }
14384
+
14385
+ async function handle(command) {
14386
+ const { id, method, payload = {} } = command
14387
+ try {
14388
+ if (method === 'init') {
14389
+ pc = new RTCPeerConnection({ iceServers: payload.iceServers ?? [] })
14390
+ pc.onconnectionstatechange = emitState
14391
+ pc.oniceconnectionstatechange = emitState
14392
+ pc.onicegatheringstatechange = emitState
14393
+ pc.onsignalingstatechange = emitState
14394
+ pc.onicecandidate = event => send({ event: 'icecandidate', candidate: normalizeCandidate(event.candidate) })
14395
+ pc.ondatachannel = event => attachChannel(nextRemoteChannelId++, event.channel)
14396
+ emitState()
14397
+ respond(id, true, {})
14398
+ return
14399
+ }
14400
+ if (method === 'createDataChannel') {
14401
+ const channel = pc.createDataChannel(payload.label, payload.options)
14402
+ attachChannel(payload.channelId, channel)
14403
+ respond(id, true, {})
14404
+ return
14405
+ }
14406
+ if (method === 'createOffer') {
14407
+ respond(id, true, await pc.createOffer())
14408
+ return
14409
+ }
14410
+ if (method === 'createAnswer') {
14411
+ respond(id, true, await pc.createAnswer())
14412
+ return
14413
+ }
14414
+ if (method === 'setLocalDescription') {
14415
+ await pc.setLocalDescription(payload.description)
14416
+ emitState()
14417
+ respond(id, true, {})
14418
+ return
14419
+ }
14420
+ if (method === 'setRemoteDescription') {
14421
+ await pc.setRemoteDescription(payload.description)
14422
+ emitState()
14423
+ respond(id, true, {})
14424
+ return
14425
+ }
14426
+ if (method === 'addIceCandidate') {
14427
+ await pc.addIceCandidate(payload.candidate)
14428
+ respond(id, true, {})
14429
+ return
14430
+ }
14431
+ if (method === 'getStats') {
14432
+ respond(id, true, serializeStats(await pc.getStats()))
14433
+ return
14434
+ }
14435
+ if (method === 'channelSend') {
14436
+ const channel = channels.get(payload.channelId)
14437
+ if (channel === undefined) throw new Error('unknown data channel')
14438
+ channel.send(payload.text ?? Buffer.from(payload.base64, 'base64'))
14439
+ respond(id, true, {})
14440
+ return
14441
+ }
14442
+ if (method === 'channelClose') {
14443
+ channels.get(payload.channelId)?.close()
14444
+ respond(id, true, {})
14445
+ return
14446
+ }
14447
+ if (method === 'close') {
14448
+ for (const channel of channels.values()) {
14449
+ try { channel.close() } catch {}
14450
+ }
14451
+ channels.clear()
14452
+ try { pc?.close() } catch {}
14453
+ respond(id, true, {})
14454
+ setTimeout(() => process.exit(0), 0)
14455
+ return
14456
+ }
14457
+ throw new Error('unknown method ' + method)
14458
+ } catch (error) {
14459
+ respond(id, false, undefined, error)
14460
+ }
14461
+ }
14462
+
14463
+ const rl = createInterface({ input: process.stdin })
14464
+ rl.on('line', line => {
14465
+ chain = chain.then(() => handle(JSON.parse(line))).catch(error => {
14466
+ send({ id: -1, ok: false, error: String(error?.message ?? error) })
14467
+ })
14468
+ })
14469
+ process.stdin.on('end', () => {
14470
+ try { pc?.close() } catch {}
14471
+ process.exit(0)
14472
+ })
14473
+ `;
14474
+
14475
+ // src/werift-rtc.ts
13650
14476
  var cachedFactory;
13651
- async function loadWeriftFactory() {
13652
- if (cachedFactory !== void 0) return cachedFactory;
14477
+ var cachedWerift;
14478
+ var cachedNativeFactory;
14479
+ async function loadNodeRtcFactory(options = {}) {
14480
+ const nativeFactory = isElectronRuntime() ? await loadExternalNativeRtcFactory().catch(() => void 0) : await loadNativeRtcFactory().catch(() => void 0);
14481
+ return nativeFactory ?? await loadWeriftFactory(options);
14482
+ }
14483
+ async function loadWeriftFactory(options = {}) {
14484
+ const cacheable = isCacheableFactoryOptions(options);
14485
+ if (cacheable && cachedFactory !== void 0) return cachedFactory;
13653
14486
  try {
13654
- const werift = await import("werift");
13655
- cachedFactory = buildWeriftFactory(werift);
13656
- return cachedFactory;
14487
+ const werift = cachedWerift ?? await import("werift");
14488
+ cachedWerift = werift;
14489
+ const routeCandidates = options.preferredHostIpv4Candidates ?? await detectRouteHostIpv4Candidates(options.routeTargets ?? [], options.routeProbeTimeoutMs);
14490
+ const factory = buildWeriftFactory(werift, { ...options, preferredHostIpv4Candidates: routeCandidates });
14491
+ if (cacheable) cachedFactory = factory;
14492
+ return factory;
13657
14493
  } catch {
13658
14494
  return void 0;
13659
14495
  }
13660
14496
  }
13661
- function buildWeriftFactory(werift) {
14497
+ async function loadNativeRtcFactory() {
14498
+ if (cachedNativeFactory !== void 0) return cachedNativeFactory;
14499
+ try {
14500
+ const wrtc = await import("@roamhq/wrtc");
14501
+ const RTCPeerConnection2 = wrtc.RTCPeerConnection ?? wrtc.default?.RTCPeerConnection;
14502
+ if (RTCPeerConnection2 === void 0) return void 0;
14503
+ cachedNativeFactory = buildNativeRtcFactory(RTCPeerConnection2);
14504
+ return cachedNativeFactory;
14505
+ } catch {
14506
+ return void 0;
14507
+ }
14508
+ }
14509
+ function isElectronRuntime() {
14510
+ const versions = process.versions;
14511
+ return typeof versions.electron === "string" || process.env.ELECTRON_RUN_AS_NODE === "1";
14512
+ }
14513
+ function buildNativeRtcFactory(RTCPeerConnection2) {
14514
+ return {
14515
+ create(configuration) {
14516
+ const raw = new RTCPeerConnection2({ iceServers: configuration.iceServers });
14517
+ return {
14518
+ get connectionState() {
14519
+ return raw.connectionState;
14520
+ },
14521
+ get iceConnectionState() {
14522
+ return raw.iceConnectionState;
14523
+ },
14524
+ get iceGatheringState() {
14525
+ return raw.iceGatheringState;
14526
+ },
14527
+ get signalingState() {
14528
+ return raw.signalingState;
14529
+ },
14530
+ set onconnectionstatechange(value) {
14531
+ raw.onconnectionstatechange = value;
14532
+ },
14533
+ get onconnectionstatechange() {
14534
+ return raw.onconnectionstatechange;
14535
+ },
14536
+ set oniceconnectionstatechange(value) {
14537
+ raw.oniceconnectionstatechange = value;
14538
+ },
14539
+ get oniceconnectionstatechange() {
14540
+ return raw.oniceconnectionstatechange;
14541
+ },
14542
+ set onicegatheringstatechange(value) {
14543
+ raw.onicegatheringstatechange = value;
14544
+ },
14545
+ get onicegatheringstatechange() {
14546
+ return raw.onicegatheringstatechange;
14547
+ },
14548
+ set onicecandidate(value) {
14549
+ raw.onicecandidate = value === null ? null : (event) => value({
14550
+ candidate: event.candidate === null ? null : normalizeNativeCandidate(event.candidate)
14551
+ });
14552
+ },
14553
+ get onicecandidate() {
14554
+ return raw.onicecandidate;
14555
+ },
14556
+ set ondatachannel(value) {
14557
+ raw.ondatachannel = value === null ? null : (event) => value({ channel: adaptNativeDataChannel(event.channel) });
14558
+ },
14559
+ get ondatachannel() {
14560
+ return raw.ondatachannel;
14561
+ },
14562
+ createDataChannel(label, options) {
14563
+ return adaptNativeDataChannel(raw.createDataChannel(label, options));
14564
+ },
14565
+ createOffer() {
14566
+ return raw.createOffer();
14567
+ },
14568
+ createAnswer() {
14569
+ return raw.createAnswer();
14570
+ },
14571
+ setLocalDescription(description) {
14572
+ return raw.setLocalDescription(description);
14573
+ },
14574
+ setRemoteDescription(description) {
14575
+ return raw.setRemoteDescription(description);
14576
+ },
14577
+ addIceCandidate(candidate) {
14578
+ return raw.addIceCandidate(candidate);
14579
+ },
14580
+ getStats() {
14581
+ return raw.getStats();
14582
+ },
14583
+ close() {
14584
+ raw.close();
14585
+ }
14586
+ };
14587
+ }
14588
+ };
14589
+ }
14590
+ function buildWeriftFactory(werift, options = {}) {
13662
14591
  return {
13663
14592
  create(configuration) {
13664
- const hostIpv4 = detectHostIpv4();
14593
+ const hostIpv4Candidates = detectHostIpv4Candidates(
14594
+ options.interfaces ?? networkInterfaces(),
14595
+ options.preferredHostIpv4Candidates
14596
+ );
14597
+ const allowedHostIpv4 = new Set(hostIpv4Candidates);
13665
14598
  const raw = new werift.RTCPeerConnection({
13666
- iceServers: configuration.iceServers ?? [],
14599
+ iceServers: orderIceServersForWerift(configuration.iceServers ?? []),
14600
+ iceUseIpv4: true,
13667
14601
  iceUseIpv6: false,
13668
14602
  iceUseLinkLocalAddress: false,
13669
- ...hostIpv4 === void 0 ? {} : { iceInterfaceAddresses: { udp4: hostIpv4 } }
14603
+ ...hostIpv4Candidates.length === 0 ? {} : {
14604
+ iceAdditionalHostAddresses: hostIpv4Candidates,
14605
+ iceFilterCandidatePair: (pair) => {
14606
+ const allowed = shouldUseCandidatePair(pair, allowedHostIpv4);
14607
+ if (!allowed) {
14608
+ configuration.onDiagnostic?.({
14609
+ type: "candidate-pair-filtered",
14610
+ localCandidate: summarizeWeriftCandidate(localCandidate(pair)),
14611
+ reason: "local-host-not-allowed"
14612
+ });
14613
+ }
14614
+ return allowed;
14615
+ }
14616
+ }
13670
14617
  });
13671
14618
  let onIceCandidate = null;
13672
14619
  let onDataChannel = null;
13673
14620
  raw.onicecandidate = (event) => {
13674
14621
  if (onIceCandidate === null) return;
13675
- onIceCandidate({ candidate: event.candidate === void 0 ? null : event.candidate.toJSON() });
14622
+ const candidate = event.candidate === void 0 ? null : event.candidate.toJSON();
14623
+ if (candidate !== null && !shouldAdvertiseCandidate(candidate, allowedHostIpv4)) {
14624
+ configuration.onDiagnostic?.({
14625
+ type: "local-candidate-filtered",
14626
+ candidate: summarizeIceCandidate(candidate),
14627
+ reason: "local-host-not-allowed"
14628
+ });
14629
+ return;
14630
+ }
14631
+ onIceCandidate({ candidate });
13676
14632
  };
13677
14633
  raw.ondatachannel = (event) => {
13678
14634
  if (onDataChannel === null) return;
@@ -13721,8 +14677,8 @@ function buildWeriftFactory(werift) {
13721
14677
  get ondatachannel() {
13722
14678
  return onDataChannel;
13723
14679
  },
13724
- createDataChannel(label, options) {
13725
- return adaptDataChannel2(raw.createDataChannel(label, { ordered: options?.ordered ?? true }));
14680
+ createDataChannel(label, options2) {
14681
+ return adaptDataChannel2(raw.createDataChannel(label, { ordered: options2?.ordered ?? true }));
13726
14682
  },
13727
14683
  createOffer() {
13728
14684
  return raw.createOffer();
@@ -13804,27 +14760,245 @@ function adaptDataChannel2(raw) {
13804
14760
  }
13805
14761
  };
13806
14762
  }
14763
+ function adaptNativeDataChannel(raw) {
14764
+ let onmessage = null;
14765
+ return {
14766
+ get label() {
14767
+ return raw.label;
14768
+ },
14769
+ get ordered() {
14770
+ return raw.ordered;
14771
+ },
14772
+ get readyState() {
14773
+ return raw.readyState;
14774
+ },
14775
+ get bufferedAmount() {
14776
+ return raw.bufferedAmount;
14777
+ },
14778
+ get binaryType() {
14779
+ return raw.binaryType;
14780
+ },
14781
+ set binaryType(value) {
14782
+ raw.binaryType = value;
14783
+ },
14784
+ set onopen(value) {
14785
+ raw.onopen = value;
14786
+ },
14787
+ get onopen() {
14788
+ return raw.onopen;
14789
+ },
14790
+ set onmessage(value) {
14791
+ onmessage = value;
14792
+ raw.onmessage = value === null ? null : (event) => value({ data: normalizeNativeMessageData(event.data) });
14793
+ },
14794
+ get onmessage() {
14795
+ return onmessage;
14796
+ },
14797
+ set onclose(value) {
14798
+ raw.onclose = value;
14799
+ },
14800
+ get onclose() {
14801
+ return raw.onclose;
14802
+ },
14803
+ set onerror(value) {
14804
+ raw.onerror = value;
14805
+ },
14806
+ get onerror() {
14807
+ return raw.onerror;
14808
+ },
14809
+ set onbufferedamountlow(value) {
14810
+ raw.onbufferedamountlow = value;
14811
+ },
14812
+ get onbufferedamountlow() {
14813
+ return raw.onbufferedamountlow;
14814
+ },
14815
+ send(data) {
14816
+ raw.send(typeof data === "string" ? data : Buffer.from(data));
14817
+ },
14818
+ close() {
14819
+ raw.close();
14820
+ }
14821
+ };
14822
+ }
13807
14823
  function toArrayBuffer2(data) {
13808
14824
  if (typeof data === "string") return data;
13809
14825
  return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
13810
14826
  }
13811
- function detectHostIpv4() {
13812
- const preferred = [];
13813
- const fallback = [];
13814
- for (const [name2, addresses] of Object.entries(networkInterfaces())) {
13815
- if (isVirtualInterface(name2)) continue;
14827
+ function normalizeNativeCandidate(candidate) {
14828
+ if (typeof candidate.toJSON === "function") return candidate.toJSON();
14829
+ return {
14830
+ candidate: candidate.candidate,
14831
+ sdpMid: candidate.sdpMid,
14832
+ sdpMLineIndex: candidate.sdpMLineIndex,
14833
+ usernameFragment: candidate.usernameFragment
14834
+ };
14835
+ }
14836
+ function normalizeNativeMessageData(data) {
14837
+ if (typeof data === "string") return data;
14838
+ if (data instanceof ArrayBuffer) return data;
14839
+ if (ArrayBuffer.isView(data)) {
14840
+ return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
14841
+ }
14842
+ return new Uint8Array().buffer;
14843
+ }
14844
+ function detectHostIpv4Candidates(interfaces, preferredHostIpv4Candidates = []) {
14845
+ const candidates = [];
14846
+ const routePreference = new Map(preferredHostIpv4Candidates.map((ip, index) => [ip, 1e4 - index]));
14847
+ for (const [name2, addresses] of Object.entries(interfaces)) {
14848
+ const interfaceScore = physicalInterfaceScore(name2);
14849
+ if (interfaceScore === void 0) continue;
13816
14850
  for (const address of addresses ?? []) {
13817
- if (address.internal || address.family !== "IPv4") continue;
14851
+ if (address.internal || !isIpv4Family(address.family)) continue;
13818
14852
  const ip = address.address;
13819
- if (ip.startsWith("127.") || ip.startsWith("169.254.") || isCgnat(ip)) continue;
13820
- if (isPrivate(ip)) preferred.push(ip);
13821
- else fallback.push(ip);
14853
+ if (!isUsableIpv4(ip)) continue;
14854
+ candidates.push({
14855
+ name: name2,
14856
+ ip,
14857
+ score: interfaceScore + (isPrivate(ip) ? 1e3 : 0) + (routePreference.get(ip) ?? 0)
14858
+ });
13822
14859
  }
13823
14860
  }
13824
- return preferred[0] ?? fallback[0];
14861
+ candidates.sort((left, right) => {
14862
+ const score = right.score - left.score;
14863
+ if (score !== 0) return score;
14864
+ const name2 = left.name.localeCompare(right.name, "en");
14865
+ return name2 === 0 ? left.ip.localeCompare(right.ip, "en", { numeric: true }) : name2;
14866
+ });
14867
+ return [...new Set(candidates.map((candidate) => candidate.ip))];
14868
+ }
14869
+ async function detectRouteHostIpv4Candidates(targets, timeoutMs = 500) {
14870
+ const detected = await Promise.all(targets.map(async (target) => {
14871
+ const routeTarget = parseRouteTarget(target);
14872
+ return routeTarget === void 0 ? void 0 : await detectRouteHostIpv4(routeTarget, timeoutMs).catch(() => void 0);
14873
+ }));
14874
+ return [...new Set(detected.filter((ip) => ip !== void 0 && isUsableIpv4(ip)))];
14875
+ }
14876
+ function shouldUseCandidatePair(pair, allowedHostIpv4) {
14877
+ if (allowedHostIpv4.size === 0) return true;
14878
+ const candidate = localCandidate(pair);
14879
+ if (candidate === void 0) return true;
14880
+ return shouldUseLocalCandidate(candidate, allowedHostIpv4);
14881
+ }
14882
+ function shouldAdvertiseCandidate(candidate, allowedHostIpv4) {
14883
+ if (allowedHostIpv4.size === 0) return true;
14884
+ const parsed = parseIceCandidate(candidate);
14885
+ if (parsed === void 0) return true;
14886
+ return shouldUseLocalCandidate(parsed, allowedHostIpv4);
14887
+ }
14888
+ function shouldUseLocalCandidate(candidate, allowedHostIpv4) {
14889
+ if (candidate.type === "host") return candidate.host !== void 0 && allowedHostIpv4.has(candidate.host);
14890
+ if (candidate.type === "srflx" && candidate.relatedAddress !== void 0) {
14891
+ return allowedHostIpv4.has(candidate.relatedAddress);
14892
+ }
14893
+ return true;
14894
+ }
14895
+ function localCandidate(pair) {
14896
+ try {
14897
+ return pair.localCandidate;
14898
+ } catch {
14899
+ return void 0;
14900
+ }
14901
+ }
14902
+ function summarizeWeriftCandidate(candidate) {
14903
+ if (candidate === void 0) return void 0;
14904
+ const address = summarizeAddress(candidate.host);
14905
+ const related = summarizeAddress(candidate.relatedAddress);
14906
+ return {
14907
+ candidateType: candidate.type === "host" || candidate.type === "srflx" || candidate.type === "prflx" || candidate.type === "relay" ? candidate.type : "unknown",
14908
+ ...address,
14909
+ ...candidate.relatedAddress === void 0 ? {} : { relatedAddressFamily: related.addressFamily, relatedAddressScope: related.addressScope }
14910
+ };
14911
+ }
14912
+ function orderIceServersForWerift(iceServers) {
14913
+ return iceServers.map((server) => {
14914
+ const urls = Array.isArray(server.urls) ? [...server.urls] : [server.urls];
14915
+ urls.sort((left, right) => iceUrlScore(left) - iceUrlScore(right));
14916
+ return { ...server, urls: Array.isArray(server.urls) ? urls : urls[0] ?? server.urls };
14917
+ });
14918
+ }
14919
+ function iceUrlScore(url) {
14920
+ const value = url.trim().toLowerCase();
14921
+ if (value.startsWith("stun:") || value.startsWith("stuns:")) return 0;
14922
+ if (value.startsWith("turn:") && value.includes("transport=tcp")) return 10;
14923
+ if (value.startsWith("turns:") && value.includes("transport=tcp")) return 20;
14924
+ if (value.startsWith("turn:") && value.includes("transport=udp")) return 30;
14925
+ if (value.startsWith("turns:")) return 40;
14926
+ if (value.startsWith("turn:")) return 50;
14927
+ return 100;
14928
+ }
14929
+ function parseRouteTarget(value) {
14930
+ try {
14931
+ const url = new URL(value);
14932
+ const port = Number(url.port || (url.protocol === "http:" ? "80" : "443"));
14933
+ if (!Number.isSafeInteger(port) || port <= 0 || port > 65535 || url.hostname.length === 0) return void 0;
14934
+ return { host: url.hostname, port };
14935
+ } catch {
14936
+ return void 0;
14937
+ }
14938
+ }
14939
+ async function detectRouteHostIpv4(target, timeoutMs) {
14940
+ const socket = createSocket("udp4");
14941
+ try {
14942
+ return await new Promise((resolve2, reject) => {
14943
+ const timer = setTimeout(() => {
14944
+ cleanup();
14945
+ resolve2(void 0);
14946
+ }, timeoutMs);
14947
+ const cleanup = () => {
14948
+ clearTimeout(timer);
14949
+ socket.off("error", onError);
14950
+ };
14951
+ const onError = (error) => {
14952
+ cleanup();
14953
+ reject(error);
14954
+ };
14955
+ socket.once("error", onError);
14956
+ socket.connect(target.port, target.host, () => {
14957
+ cleanup();
14958
+ const address = socket.address();
14959
+ resolve2(typeof address === "string" ? void 0 : address.address);
14960
+ });
14961
+ });
14962
+ } finally {
14963
+ socket.close();
14964
+ }
14965
+ }
14966
+ function isCacheableFactoryOptions(options) {
14967
+ return options.interfaces === void 0 && options.preferredHostIpv4Candidates === void 0 && options.routeTargets === void 0;
14968
+ }
14969
+ function parseIceCandidate(candidate) {
14970
+ const value = candidate.candidate?.trim();
14971
+ if (value === void 0 || value.length === 0) return void 0;
14972
+ const parts = value.replace(/^candidate:/, "").split(/\s+/);
14973
+ const typeIndex = parts.indexOf("typ");
14974
+ if (typeIndex < 0) return void 0;
14975
+ const host = parts[4];
14976
+ const type = parts[typeIndex + 1];
14977
+ const relatedAddressIndex = parts.indexOf("raddr");
14978
+ return {
14979
+ ...host === void 0 ? {} : { host },
14980
+ ...type === void 0 ? {} : { type },
14981
+ ...relatedAddressIndex < 0 || parts[relatedAddressIndex + 1] === void 0 ? {} : { relatedAddress: parts[relatedAddressIndex + 1] }
14982
+ };
14983
+ }
14984
+ function physicalInterfaceScore(name2) {
14985
+ const value = name2.toLowerCase();
14986
+ if (/^(utun|tun|tap|ppp|bridge|awdl|llw|gif|stf|anpi|ap\d|vmnet|veth|docker|br-|vboxnet|lo)/.test(value)) {
14987
+ return void 0;
14988
+ }
14989
+ if (/^(eth|eno|ens|enp)/.test(value)) return 500;
14990
+ if (/^(en|wlan|wl|wifi|wi-fi)/.test(value)) return 450;
14991
+ return 300;
14992
+ }
14993
+ function isIpv4Family(family) {
14994
+ return family === "IPv4" || family === 4;
13825
14995
  }
13826
- function isVirtualInterface(name2) {
13827
- return /^(utun|ppp|bridge|awdl|llw|gif|stf|anpi|ap\d|en[1-9]\d*$)/i.test(name2);
14996
+ function isUsableIpv4(ip) {
14997
+ const parts = ip.split(".").map(Number);
14998
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
14999
+ if (parts[0] === 0 || parts[0] === 127 || parts[0] >= 224) return false;
15000
+ if (parts[0] === 169 && parts[1] === 254) return false;
15001
+ return !isCgnat(ip);
13828
15002
  }
13829
15003
  function isCgnat(ip) {
13830
15004
  const parts = ip.split(".").map(Number);
@@ -13841,8 +15015,9 @@ function isPrivate(ip) {
13841
15015
  // src/client-runtime.ts
13842
15016
  var REMOTE_COMMAND_LIST_MIN_VERSION = [0, 3, 16];
13843
15017
  var REMOTE_FILE_VIEWER_MIN_VERSION = [0, 3, 17];
15018
+ var DIRECT_WEBRTC_NEGOTIATE_TIMEOUT_MS = 12e3;
13844
15019
  var ClientModeRuntime = class {
13845
- constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadWeriftFactory) {
15020
+ constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory) {
13846
15021
  this.config = config;
13847
15022
  this.identities = identities;
13848
15023
  this.server = server;
@@ -13879,6 +15054,7 @@ var ClientModeRuntime = class {
13879
15054
  available: this.config.serverUrl !== void 0,
13880
15055
  identityReady: this.identity !== void 0,
13881
15056
  deviceId: this.identity?.deviceId,
15057
+ deviceName: this.identity?.name,
13882
15058
  serverUrl: this.config.serverUrl,
13883
15059
  ...this.proxySwitch.status(),
13884
15060
  connected: this.connected !== void 0,
@@ -14077,61 +15253,76 @@ var ClientModeRuntime = class {
14077
15253
  const presence = await this.server.presenceFor(targetDeviceId);
14078
15254
  if (!presence.online) throw new ClientModeError("HOST_OFFLINE", "The selected Host is offline.", true);
14079
15255
  const credentials = await this.server.authenticate(identity);
14080
- const rtcFactory = this.config.forceRelay ? void 0 : await this.rtcFactoryProvider().catch(() => void 0);
15256
+ const rtcFactory = this.config.forceRelay ? void 0 : await this.rtcFactoryProvider({ routeTargets: [this.server.baseUrl] }).catch(() => void 0);
14081
15257
  let webRtcFallback = false;
14082
- const createTransport = (relayOnly) => new AdaptiveTransport(
15258
+ const createTransport = (attempt) => new AdaptiveTransport(
14083
15259
  websocketUrl(this.server.baseUrl),
14084
15260
  {
14085
15261
  role: "client",
14086
15262
  deviceId: identity.deviceId,
14087
15263
  accessToken: credentials.accessToken,
14088
15264
  targetDeviceId,
14089
- forceRelay: this.config.forceRelay || relayOnly,
14090
- preferredTransports: this.config.forceRelay || relayOnly ? ["relay"] : ["lan", "p2p", "turn", "relay"],
14091
- ...rtcFactory === void 0 || relayOnly ? {} : { rtcFactory },
14092
- fetchIceServers: async (connectionId) => this.server.turnCredentials(connectionId),
14093
- onWebRtcFallback: (error) => {
15265
+ forceRelay: this.config.forceRelay || attempt === "relay",
15266
+ preferredTransports: preferredTransportsForAttempt(attempt),
15267
+ negotiateTimeoutMs: attempt === "direct" ? DIRECT_WEBRTC_NEGOTIATE_TIMEOUT_MS : void 0,
15268
+ ...rtcFactory === void 0 || attempt === "relay" ? {} : { rtcFactory },
15269
+ fetchIceServers: async (connectionId) => iceServersForAttempt(attempt, await this.server.turnCredentials(connectionId)),
15270
+ onWebRtcFallback: (error, diagnostics) => {
14094
15271
  webRtcFallback = true;
14095
- this.logger.warn("remote Harness WebRTC failed; using relay", {
15272
+ this.logger.warn(attempt === "direct" ? "remote Harness direct WebRTC failed; trying TURN" : "remote Harness TURN WebRTC failed; using relay", {
14096
15273
  targetDeviceId: shortId(target.deviceId),
14097
- reason: diagnosticReason(error)
15274
+ attempt,
15275
+ reason: diagnosticReason(error),
15276
+ ...webrtcDiagnosticsLogFields(diagnostics)
14098
15277
  });
14099
15278
  }
14100
15279
  }
14101
15280
  );
14102
- let transport = createTransport(false);
14103
- let client = new RemoteClientCore(new ClientSecureTransport(transport, identity, target), 6e4);
15281
+ const attempts = this.config.forceRelay || rtcFactory === void 0 ? ["relay"] : ["direct", "turn", "relay"];
15282
+ let transport;
15283
+ let client;
14104
15284
  try {
14105
- await client.connect();
14106
- signal?.throwIfAborted();
14107
- if (webRtcFallback) {
14108
- await client.close();
14109
- transport = createTransport(true);
15285
+ for (const attempt of attempts) {
15286
+ webRtcFallback = false;
15287
+ transport = createTransport(attempt);
14110
15288
  client = new RemoteClientCore(new ClientSecureTransport(transport, identity, target), 6e4);
14111
15289
  await client.connect();
14112
15290
  signal?.throwIfAborted();
14113
- this.logger.info("remote Harness relay fallback re-established", {
14114
- targetDeviceId: shortId(target.deviceId)
14115
- });
15291
+ if (attempt === "relay" || !webRtcFallback) break;
15292
+ await client.close();
15293
+ client = void 0;
15294
+ transport = void 0;
15295
+ if (attempt === "turn") {
15296
+ this.logger.info("remote Harness relay fallback re-established", {
15297
+ targetDeviceId: shortId(target.deviceId)
15298
+ });
15299
+ }
14116
15300
  }
14117
- client.onClose(() => {
14118
- if (this.connected?.client !== client) return;
15301
+ if (client === void 0 || transport === void 0) {
15302
+ throw new ClientModeError("CONNECTION_FAILED", "Unable to establish a remote transport.", true);
15303
+ }
15304
+ const connectedClient = client;
15305
+ const connectedTransport = transport;
15306
+ connectedClient.onClose(() => {
15307
+ if (this.connected?.client !== connectedClient) return;
14119
15308
  this.connected = void 0;
14120
15309
  this.pendingWorkspaceSelection = void 0;
14121
15310
  this.proxySwitch.selectLocal();
14122
15311
  this.gatewaySwitch.selectLocal();
14123
- void client.close().catch(() => void 0);
15312
+ void connectedClient.close().catch(() => void 0);
14124
15313
  this.logger.warn("remote Harness transport closed; falling back to local mode", {
14125
15314
  targetDeviceId: shortId(target.deviceId)
14126
15315
  });
14127
15316
  });
15317
+ const connectionDetails = await connectedTransport.connectionDetails().catch(() => void 0);
14128
15318
  this.logger.info("remote Harness transport ready", {
14129
15319
  targetDeviceId: shortId(target.deviceId),
14130
- transport: client.getStats().mode
15320
+ transport: connectedClient.getStats().mode,
15321
+ ...webrtcDiagnosticsLogFields(connectionDetails?.webRtc?.diagnostics)
14131
15322
  });
14132
- return { client, target, transport, features: remoteHostFeatures(serverDevice.clientVersion) };
15323
+ return { client: connectedClient, target, transport: connectedTransport, features: remoteHostFeatures(serverDevice.clientVersion) };
14133
15324
  } catch (error) {
14134
- await client.close().catch(() => void 0);
15325
+ await client?.close().catch(() => void 0);
14135
15326
  throw error;
14136
15327
  }
14137
15328
  }
@@ -14278,6 +15469,20 @@ function websocketUrl(baseUrl) {
14278
15469
  url.pathname = `${url.pathname.replace(/\/$/, "")}/ws/v1/connect`;
14279
15470
  return url.toString();
14280
15471
  }
15472
+ function webrtcDiagnosticsLogFields(diagnostics) {
15473
+ if (diagnostics === void 0) return {};
15474
+ return {
15475
+ rtcConnectionState: diagnostics.connectionState,
15476
+ rtcIceConnectionState: diagnostics.iceConnectionState,
15477
+ rtcIceGatheringState: diagnostics.iceGatheringState,
15478
+ rtcLocalCandidates: diagnostics.localCandidates,
15479
+ rtcRemoteCandidates: diagnostics.remoteCandidates,
15480
+ rtcCandidatePairs: diagnostics.candidatePairs,
15481
+ rtcFilteredLocalCandidates: diagnostics.filteredLocalCandidates,
15482
+ rtcFilteredCandidatePairs: diagnostics.filteredCandidatePairs,
15483
+ ...diagnostics.selectedPath === void 0 ? {} : { rtcSelectedPath: diagnostics.selectedPath }
15484
+ };
15485
+ }
14281
15486
  function record(value) {
14282
15487
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
14283
15488
  throw new ClientModeError("INVALID_MESSAGE", "The control request payload is invalid.");
@@ -14355,6 +15560,14 @@ function diagnosticReason(error) {
14355
15560
  const message = error.message.replace(/[\r\n\t]+/g, " ").slice(0, 240);
14356
15561
  return code === void 0 ? message : `${code}: ${message}`;
14357
15562
  }
15563
+ function preferredTransportsForAttempt(attempt) {
15564
+ if (attempt === "direct") return ["lan", "p2p", "relay"];
15565
+ if (attempt === "turn") return ["turn", "relay"];
15566
+ return ["relay"];
15567
+ }
15568
+ function iceServersForAttempt(attempt, iceServers) {
15569
+ return attempt === "direct" ? stunOnlyIceServers(iceServers) : iceServers;
15570
+ }
14358
15571
 
14359
15572
  // src/control-runtime.ts
14360
15573
  import { hostname as hostname2 } from "node:os";
@@ -14363,7 +15576,7 @@ import { hostname as hostname2 } from "node:os";
14363
15576
  import { createHash } from "node:crypto";
14364
15577
  import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
14365
15578
  import { homedir } from "node:os";
14366
- import { dirname, join } from "node:path";
15579
+ import { dirname as dirname2, join as join2 } from "node:path";
14367
15580
  var identitySchema = external_exports.object({
14368
15581
  schemaVersion: external_exports.literal(1),
14369
15582
  deviceId: external_exports.string().uuid(),
@@ -14392,14 +15605,14 @@ var IdentityStore = class {
14392
15605
  peers = /* @__PURE__ */ new Map();
14393
15606
  constructor(options = {}) {
14394
15607
  const env = options.env ?? process.env;
14395
- const dshHome = env.DSH_HOME || join(options.homeDirectory ?? homedir(), ".dsh");
14396
- this.directory = options.directory ?? join(dshHome, "remote");
15608
+ const dshHome = env.DSH_HOME || join2(options.homeDirectory ?? homedir(), ".dsh");
15609
+ this.directory = options.directory ?? join2(dshHome, "remote");
14397
15610
  }
14398
15611
  async loadOrCreate(deviceName) {
14399
15612
  await mkdir(this.directory, { recursive: true, mode: 448 });
14400
15613
  await chmod(this.directory, 448);
14401
- const devicePath = join(this.directory, "device.json");
14402
- const keyPath = join(this.directory, "device.key");
15614
+ const devicePath = join2(this.directory, "device.json");
15615
+ const keyPath = join2(this.directory, "device.key");
14403
15616
  const [hasDevice, hasKey] = await Promise.all([exists(devicePath), exists(keyPath)]);
14404
15617
  if (hasDevice !== hasKey) {
14405
15618
  throw new IdentityInvalidError("device identity is incomplete; repair it explicitly before reconnecting");
@@ -14468,7 +15681,7 @@ var IdentityStore = class {
14468
15681
  return removed;
14469
15682
  }
14470
15683
  async loadPeers() {
14471
- const path = join(this.directory, "trusted-peers.json");
15684
+ const path = join2(this.directory, "trusted-peers.json");
14472
15685
  if (!await exists(path)) {
14473
15686
  await atomicJsonWrite(path, { schemaVersion: 1, peers: [] }, 384);
14474
15687
  }
@@ -14484,7 +15697,7 @@ var IdentityStore = class {
14484
15697
  this.peers = peers;
14485
15698
  }
14486
15699
  async savePeers() {
14487
- await atomicJsonWrite(join(this.directory, "trusted-peers.json"), {
15700
+ await atomicJsonWrite(join2(this.directory, "trusted-peers.json"), {
14488
15701
  schemaVersion: 1,
14489
15702
  peers: [...this.peers.values()]
14490
15703
  }, 384);
@@ -14493,7 +15706,7 @@ var IdentityStore = class {
14493
15706
  function serverStorageDirectory(root, serverUrl, role) {
14494
15707
  const origin = new URL(serverUrl).origin;
14495
15708
  const scope = createHash("sha256").update(origin).digest("hex").slice(0, 24);
14496
- return join(root, "servers", scope, role);
15709
+ return join2(root, "servers", scope, role);
14497
15710
  }
14498
15711
  function fingerprint(publicKey) {
14499
15712
  const compact = createHash("sha256").update(fromBase64Url2(publicKey)).digest("hex").slice(0, 12).toUpperCase();
@@ -14511,7 +15724,7 @@ async function atomicJsonWrite(path, value, mode) {
14511
15724
  `, mode);
14512
15725
  }
14513
15726
  async function atomicTextWrite(path, value, mode) {
14514
- await mkdir(dirname(path), { recursive: true, mode: 448 });
15727
+ await mkdir(dirname2(path), { recursive: true, mode: 448 });
14515
15728
  const temporary = `${path}.${process.pid}.${uuidV7()}.tmp`;
14516
15729
  await writeFile(temporary, value, { encoding: "utf8", mode, flag: "wx" });
14517
15730
  await chmod(temporary, mode);
@@ -14536,7 +15749,7 @@ function safeErrorMessage(error) {
14536
15749
 
14537
15750
  // src/server-credentials.ts
14538
15751
  import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
14539
- import { dirname as dirname2, join as join2 } from "node:path";
15752
+ import { dirname as dirname3, join as join3 } from "node:path";
14540
15753
  var credentialSchema = external_exports.object({
14541
15754
  schemaVersion: external_exports.literal(1),
14542
15755
  serverUrl: external_exports.string().url(),
@@ -14551,7 +15764,7 @@ var credentialSchema = external_exports.object({
14551
15764
  var ServerCredentialStore = class {
14552
15765
  path;
14553
15766
  constructor(directory) {
14554
- this.path = join2(directory, "server-credentials.json");
15767
+ this.path = join3(directory, "server-credentials.json");
14555
15768
  }
14556
15769
  async load(serverUrl, deviceId) {
14557
15770
  if (!await exists2(this.path)) return void 0;
@@ -14578,7 +15791,7 @@ var ServerCredentialsInvalidError = class extends Error {
14578
15791
  code = "SERVER_CREDENTIALS_INVALID";
14579
15792
  };
14580
15793
  async function atomicWrite(path, contents) {
14581
- await mkdir2(dirname2(path), { recursive: true, mode: 448 });
15794
+ await mkdir2(dirname3(path), { recursive: true, mode: 448 });
14582
15795
  const temporary = `${path}.${process.pid}.${uuidV7()}.tmp`;
14583
15796
  await writeFile2(temporary, contents, { encoding: "utf8", mode: 384, flag: "wx" });
14584
15797
  await chmod2(temporary, 384);
@@ -14788,6 +16001,7 @@ var PluginControlRuntime = class {
14788
16001
  return {
14789
16002
  mode: "local",
14790
16003
  available: false,
16004
+ deviceName: hostname2(),
14791
16005
  hostAuthorizationAvailable: this.host !== void 0,
14792
16006
  ...this.host === void 0 ? {} : { host: this.host.hostStatus() }
14793
16007
  };
@@ -15037,7 +16251,7 @@ function shortId2(value) {
15037
16251
 
15038
16252
  // src/harness-version.ts
15039
16253
  import { readFile as readFile3 } from "node:fs/promises";
15040
- import { dirname as dirname3, isAbsolute, join as join3 } from "node:path";
16254
+ import { dirname as dirname4, isAbsolute, join as join4 } from "node:path";
15041
16255
  var LEGACY_PLACEHOLDER_VERSION = "0.0.1";
15042
16256
  function normalizeHarnessVersion(value) {
15043
16257
  if (typeof value !== "string") return void 0;
@@ -15051,14 +16265,14 @@ function selectHarnessVersion(reportedVersion, distributionVersion) {
15051
16265
  }
15052
16266
  async function readHarnessDistributionVersion(entrypoint = process.argv[1]) {
15053
16267
  if (entrypoint === void 0 || !isAbsolute(entrypoint)) return void 0;
15054
- let directory = dirname3(entrypoint);
16268
+ let directory = dirname4(entrypoint);
15055
16269
  for (let depth = 0; depth < 8; depth += 1) {
15056
16270
  try {
15057
- const manifest = JSON.parse(await readFile3(join3(directory, "package.json"), "utf8"));
16271
+ const manifest = JSON.parse(await readFile3(join4(directory, "package.json"), "utf8"));
15058
16272
  if (manifest.name === "@deepseek-ai/dsh") return normalizeHarnessVersion(manifest.version);
15059
16273
  } catch {
15060
16274
  }
15061
- const parent = dirname3(directory);
16275
+ const parent = dirname4(directory);
15062
16276
  if (parent === directory) break;
15063
16277
  directory = parent;
15064
16278
  }
@@ -15297,7 +16511,7 @@ function isOversizedListing(value) {
15297
16511
  }
15298
16512
 
15299
16513
  // src/server-connection.ts
15300
- var DEFAULT_WEBRTC_NEGOTIATE_TIMEOUT_MS = 8e3;
16514
+ var DEFAULT_WEBRTC_NEGOTIATE_TIMEOUT_MS = 12e3;
15301
16515
  var HostServerConnection = class {
15302
16516
  constructor(config, identity, identities, api, connections, logger, createWebSocket = (url) => new WebSocket(url), rtcFactoryProvider, hostCapabilities = () => ["harness.api.v1"], harnessVersion) {
15303
16517
  this.config = config;
@@ -15587,6 +16801,7 @@ var HostServerConnection = class {
15587
16801
  connectionId: payload.connectionId,
15588
16802
  membershipId: descriptor.membershipId,
15589
16803
  peer,
16804
+ preferredTransports: payload.preferredTransports,
15590
16805
  noise,
15591
16806
  transport: "negotiating"
15592
16807
  });
@@ -15676,13 +16891,24 @@ var HostServerConnection = class {
15676
16891
  code: errorCode(error)
15677
16892
  });
15678
16893
  }
16894
+ if (!tunnel.preferredTransports.includes("turn")) iceServers = stunOnlyIceServers(iceServers);
15679
16895
  const rtc = new RtcDataChannelTransport({
15680
16896
  role: "responder",
15681
16897
  factory: this.rtcFactory,
15682
16898
  iceServers,
15683
16899
  onSignal: (signal) => this.sendRtcSignal(tunnel, signal),
15684
16900
  negotiateTimeoutMs: DEFAULT_WEBRTC_NEGOTIATE_TIMEOUT_MS,
15685
- label: `host<-${tunnel.peer.deviceId}`
16901
+ label: `host<-${tunnel.peer.deviceId}`,
16902
+ onDiagnostic: (event) => {
16903
+ if (event.type !== "local-candidate-filtered" && event.type !== "candidate-pair-filtered") return;
16904
+ this.logger.debug("webrtc candidate filtered", {
16905
+ connectionId: shortId3(tunnel.connectionId),
16906
+ peerDeviceId: shortId3(tunnel.peer.deviceId),
16907
+ type: event.type,
16908
+ reason: event.reason,
16909
+ candidate: event.type === "local-candidate-filtered" ? event.candidate : event.localCandidate
16910
+ });
16911
+ }
15686
16912
  });
15687
16913
  tunnel.rtc = rtc;
15688
16914
  rtc.onMessage((data) => tunnel.channel?.receive(void 0, data));
@@ -15724,7 +16950,8 @@ var HostServerConnection = class {
15724
16950
  this.logger.info("webrtc data channel ready", {
15725
16951
  connectionId: shortId3(tunnel.connectionId),
15726
16952
  peerDeviceId: shortId3(tunnel.peer.deviceId),
15727
- transport: tunnel.transportMode ?? selected
16953
+ transport: tunnel.transportMode ?? selected,
16954
+ ...webrtcDiagnosticsLogFields2(rtcDiagnostics(tunnel.rtc))
15728
16955
  });
15729
16956
  }
15730
16957
  async handleRtcFailed(tunnel, rtc, error) {
@@ -15732,7 +16959,8 @@ var HostServerConnection = class {
15732
16959
  if (tunnel.transport === "p2p" || tunnel.transport === "turn") {
15733
16960
  this.logger.warn("webrtc data channel failed; disconnecting peer", {
15734
16961
  connectionId: shortId3(tunnel.connectionId),
15735
- reason: diagnosticReason4(error)
16962
+ reason: diagnosticReason4(error),
16963
+ ...webrtcDiagnosticsLogFields2(rtcDiagnostics(rtc))
15736
16964
  });
15737
16965
  await this.dropTunnel(tunnel.connectionId, "CONNECTION_FAILED");
15738
16966
  return;
@@ -15742,7 +16970,8 @@ var HostServerConnection = class {
15742
16970
  await rtc.close();
15743
16971
  this.logger.warn("webrtc negotiation failed; falling back to relay", {
15744
16972
  connectionId: shortId3(tunnel.connectionId),
15745
- reason: diagnosticReason4(error)
16973
+ reason: diagnosticReason4(error),
16974
+ ...webrtcDiagnosticsLogFields2(rtcDiagnostics(rtc))
15746
16975
  });
15747
16976
  }
15748
16977
  sendTransportSelected(tunnel, transport) {
@@ -15969,6 +17198,28 @@ function diagnosticReason4(error) {
15969
17198
  const message = asError2(error).message.replace(/[\r\n]+/g, " ").slice(0, 160);
15970
17199
  return message || "Unknown Server connection error.";
15971
17200
  }
17201
+ function webrtcDiagnosticsLogFields2(diagnostics) {
17202
+ if (diagnostics === void 0) return {};
17203
+ return {
17204
+ rtcConnectionState: diagnostics.connectionState,
17205
+ rtcIceConnectionState: diagnostics.iceConnectionState,
17206
+ rtcIceGatheringState: diagnostics.iceGatheringState,
17207
+ rtcLocalCandidates: diagnostics.localCandidates,
17208
+ rtcRemoteCandidates: diagnostics.remoteCandidates,
17209
+ rtcCandidatePairs: diagnostics.candidatePairs,
17210
+ rtcFilteredLocalCandidates: diagnostics.filteredLocalCandidates,
17211
+ rtcFilteredCandidatePairs: diagnostics.filteredCandidatePairs,
17212
+ ...diagnostics.selectedPath === void 0 ? {} : { rtcSelectedPath: diagnostics.selectedPath }
17213
+ };
17214
+ }
17215
+ function rtcDiagnostics(rtc) {
17216
+ try {
17217
+ const candidate = rtc;
17218
+ return typeof candidate.diagnostics === "function" ? candidate.diagnostics() : void 0;
17219
+ } catch {
17220
+ return void 0;
17221
+ }
17222
+ }
15972
17223
  function errorCode(error) {
15973
17224
  return error instanceof ServerApiError || error instanceof ControlConnectionError ? error.code : "CONNECTION_FAILED";
15974
17225
  }
@@ -15986,7 +17237,7 @@ function closeCode(code) {
15986
17237
  // src/remote-directory-browser.ts
15987
17238
  import { readdir, stat as stat3 } from "node:fs/promises";
15988
17239
  import { homedir as homedir2, platform as platform2 } from "node:os";
15989
- import { basename, dirname as dirname4, isAbsolute as isAbsolute2, parse, resolve } from "node:path";
17240
+ import { basename, dirname as dirname5, isAbsolute as isAbsolute2, parse, resolve } from "node:path";
15990
17241
  var MAX_ENTRIES = 500;
15991
17242
  async function listRemoteDirectory(path, signal) {
15992
17243
  signal?.throwIfAborted();
@@ -16019,7 +17270,7 @@ function crumbs(path) {
16019
17270
  let current = path;
16020
17271
  while (current !== root) {
16021
17272
  segments.unshift(basename(current));
16022
- current = dirname4(current);
17273
+ current = dirname5(current);
16023
17274
  }
16024
17275
  for (const segment of segments) {
16025
17276
  current = resolve(current, segment);
@@ -16070,7 +17321,11 @@ var transferReadSchema = external_exports.object({ transferId: transferIdSchema,
16070
17321
  var transferCloseSchema = external_exports.object({ transferId: transferIdSchema }).strict();
16071
17322
  var commandExecuteSchema = external_exports.object({
16072
17323
  agentId: external_exports.string().min(1).max(128),
16073
- line: external_exports.string().min(1).max(2048)
17324
+ line: external_exports.string().min(1).max(2048),
17325
+ // The official Harness command endpoint requires the `images` wire field.
17326
+ // Remote command execution keeps attachments out of scope, so only an empty
17327
+ // list is accepted and legacy clients that omit it are normalized below.
17328
+ images: external_exports.array(external_exports.never()).length(0).optional()
16074
17329
  }).strict();
16075
17330
  var commandListSchema = external_exports.object({
16076
17331
  agentId: external_exports.string().min(1).max(128)
@@ -16183,11 +17438,12 @@ var API_TRANSFER_IDLE_MS = 2 * 6e4;
16183
17438
  var INLINE_TRANSFER_RESPONSE_BYTES = 2 * 1024 * 1024;
16184
17439
  var SESSION_HISTORY_PAGE_SIZES = [50, 30, 20, 12, 6, 3, 1];
16185
17440
  var HarnessApiBridge = class {
16186
- constructor(api, publish, maxStreams = 8, logger, typertGateway) {
17441
+ constructor(api, publish, maxStreams = 8, logger, typertGateway, harnessVersion) {
16187
17442
  this.api = api;
16188
17443
  this.publish = publish;
16189
17444
  this.maxStreams = maxStreams;
16190
17445
  this.logger = logger;
17446
+ this.harnessVersion = harnessVersion;
16191
17447
  this.methods = createMethodMap(api, typertGateway);
16192
17448
  this.mux = api.events.mux.bind(api.events);
16193
17449
  this.host = api.events.host.bind(api.events);
@@ -16225,6 +17481,7 @@ var HarnessApiBridge = class {
16225
17481
  const value = await listRemoteDirectory(typeof payload.path === "string" ? payload.path : void 0, signal);
16226
17482
  response = { rpcId: params.rpcId, result: { ok: true, value } };
16227
17483
  }
17484
+ if (params.method === "host.describe") response = normalizeHostDescribeVersion(response, this.harnessVersion);
16228
17485
  this.logger?.debug("harness api call ok", {
16229
17486
  method: params.method,
16230
17487
  durationMs: Math.round(performance.now() - startedAt)
@@ -16455,6 +17712,24 @@ function needsRemoteDirectoryFallback(response) {
16455
17712
  const result = response.result;
16456
17713
  return typeof result === "object" && result !== null && "ok" in result && result.ok === false && "error" in result && typeof result.error === "object" && result.error !== null && "code" in result.error && result.error.code === "directory-picker-unavailable";
16457
17714
  }
17715
+ function normalizeHostDescribeVersion(response, harnessVersion) {
17716
+ if (!response.result.ok || harnessVersion === void 0) return response;
17717
+ const value = response.result.value;
17718
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return response;
17719
+ const description = value;
17720
+ const selectedVersion = selectHarnessVersion(
17721
+ normalizeHarnessVersion(description.version),
17722
+ harnessVersion
17723
+ );
17724
+ if (selectedVersion === void 0 || description.version === selectedVersion) return response;
17725
+ return {
17726
+ ...response,
17727
+ result: {
17728
+ ...response.result,
17729
+ value: { ...description, version: selectedVersion }
17730
+ }
17731
+ };
17732
+ }
16458
17733
  function createMethodMap(api, typertGateway) {
16459
17734
  const domains = api;
16460
17735
  const methods = /* @__PURE__ */ new Map();
@@ -16464,7 +17739,8 @@ function createMethodMap(api, typertGateway) {
16464
17739
  const [namespace, commandMethod] = method.split(".");
16465
17740
  const implementation2 = async (request, signal) => {
16466
17741
  if (commandMethod === "execute") {
16467
- const args2 = commandExecuteSchema.parse(request.payload);
17742
+ const payload = commandExecuteSchema.parse(request.payload);
17743
+ const args2 = { ...payload, images: payload.images ?? [] };
16468
17744
  const value2 = await typertGateway.invoke({
16469
17745
  namespace,
16470
17746
  method: "execute",
@@ -16775,7 +18051,8 @@ var HostPluginRuntime = class {
16775
18051
  (event, data) => send(createEvent(event, data)),
16776
18052
  void 0,
16777
18053
  this.logger,
16778
- typertGateway?.()
18054
+ typertGateway?.(),
18055
+ this.harnessVersion
16779
18056
  );
16780
18057
  const fileViewer = new RemoteFileViewerBridge(
16781
18058
  () => this.fileViewerHost?.(),
@@ -16803,6 +18080,7 @@ var HostPluginRuntime = class {
16803
18080
  });
16804
18081
  if (this.serverApi !== void 0) {
16805
18082
  this.harnessVersion = await this.readHarnessVersion();
18083
+ this.serverApi.setHarnessVersion(this.harnessVersion);
16806
18084
  this.serverApi.bindIdentity(this.identity);
16807
18085
  this.serverConnection = this.createServerConnection(this.identity);
16808
18086
  this.serverConnection.start();
@@ -16920,7 +18198,7 @@ var HostPluginRuntime = class {
16920
18198
  this.connections,
16921
18199
  this.logger,
16922
18200
  void 0,
16923
- this.config.forceRelay ? void 0 : loadWeriftFactory,
18201
+ this.config.forceRelay ? void 0 : () => loadNodeRtcFactory({ routeTargets: this.config.serverUrl === void 0 ? [] : [this.config.serverUrl] }),
16924
18202
  () => this.fileViewerHost?.() === void 0 ? ["harness.api.v1"] : ["harness.api.v1", "fileviewer.read.v1"],
16925
18203
  this.harnessVersion
16926
18204
  );
@@ -17018,13 +18296,13 @@ function decodeBase64(value) {
17018
18296
  }
17019
18297
 
17020
18298
  // src/index.ts
17021
- var name = "dsh-remote";
18299
+ var name = "ds-harness-remote";
17022
18300
  function apply(ctx, input = {}) {
17023
18301
  ctx.inject(["settings", "apiProxy", "connection", "typertGateway"], (runtimeContext) => activate(runtimeContext, input));
17024
18302
  }
17025
18303
  async function activate(ctx, input) {
17026
18304
  const settings = ctx.get("settings");
17027
- const settingsScope = settings?.register(settingsNamespace("dsh-remote"), Config, {
18305
+ const settingsScope = settings?.register(settingsNamespace("ds-harness-remote"), Config, {
17028
18306
  base: input,
17029
18307
  applies: "restart",
17030
18308
  validate: (value) => {