ds-harness-remote 0.3.32 → 0.3.34

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 (42) hide show
  1. package/README.md +3 -3
  2. package/dist/client-runtime.d.ts +2 -1
  3. package/dist/client-runtime.d.ts.map +1 -1
  4. package/dist/client-runtime.js +73 -31
  5. package/dist/client-runtime.js.map +1 -1
  6. package/dist/client.d.ts +1 -0
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.github.js +88 -54
  9. package/dist/client.js +88 -54
  10. package/dist/client.js.map +1 -1
  11. package/dist/harness-api-bridge.d.ts.map +1 -1
  12. package/dist/harness-api-bridge.js +2 -5
  13. package/dist/harness-api-bridge.js.map +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1384 -113
  16. package/dist/index.js.map +4 -4
  17. package/dist/native-rtc-helper.d.ts +4 -0
  18. package/dist/native-rtc-helper.d.ts.map +1 -0
  19. package/dist/native-rtc-helper.js +539 -0
  20. package/dist/native-rtc-helper.js.map +1 -0
  21. package/dist/rpc-router.d.ts +1 -6
  22. package/dist/rpc-router.d.ts.map +1 -1
  23. package/dist/rpc-router.js +5 -20
  24. package/dist/rpc-router.js.map +1 -1
  25. package/dist/safe-error.d.ts +8 -0
  26. package/dist/safe-error.d.ts.map +1 -0
  27. package/dist/safe-error.js +20 -0
  28. package/dist/safe-error.js.map +1 -0
  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 +4 -2
  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
  42. package/public.d.ts +1 -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.32";
13537
+ var PLUGIN_VERSION = "0.3.34";
13216
13538
 
13217
13539
  // src/server-api.ts
13218
13540
  var HostServerApi = class {
@@ -13651,33 +13973,662 @@ function isRemoteCommandMethod(method) {
13651
13973
  }
13652
13974
 
13653
13975
  // src/werift-rtc.ts
13976
+ import { createSocket } from "node:dgram";
13654
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
13655
14476
  var cachedFactory;
13656
- async function loadWeriftFactory() {
13657
- 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;
13658
14486
  try {
13659
- const werift = await import("werift");
13660
- cachedFactory = buildWeriftFactory(werift);
13661
- 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;
13662
14493
  } catch {
13663
14494
  return void 0;
13664
14495
  }
13665
14496
  }
13666
- 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 = {}) {
13667
14591
  return {
13668
14592
  create(configuration) {
13669
- const hostIpv4 = detectHostIpv4();
14593
+ const hostIpv4Candidates = detectHostIpv4Candidates(
14594
+ options.interfaces ?? networkInterfaces(),
14595
+ options.preferredHostIpv4Candidates
14596
+ );
14597
+ const allowedHostIpv4 = new Set(hostIpv4Candidates);
13670
14598
  const raw = new werift.RTCPeerConnection({
13671
- iceServers: configuration.iceServers ?? [],
14599
+ iceServers: orderIceServersForWerift(configuration.iceServers ?? []),
14600
+ iceUseIpv4: true,
13672
14601
  iceUseIpv6: false,
13673
14602
  iceUseLinkLocalAddress: false,
13674
- ...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
+ }
13675
14617
  });
13676
14618
  let onIceCandidate = null;
13677
14619
  let onDataChannel = null;
13678
14620
  raw.onicecandidate = (event) => {
13679
14621
  if (onIceCandidate === null) return;
13680
- 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 });
13681
14632
  };
13682
14633
  raw.ondatachannel = (event) => {
13683
14634
  if (onDataChannel === null) return;
@@ -13726,8 +14677,8 @@ function buildWeriftFactory(werift) {
13726
14677
  get ondatachannel() {
13727
14678
  return onDataChannel;
13728
14679
  },
13729
- createDataChannel(label, options) {
13730
- return adaptDataChannel2(raw.createDataChannel(label, { ordered: options?.ordered ?? true }));
14680
+ createDataChannel(label, options2) {
14681
+ return adaptDataChannel2(raw.createDataChannel(label, { ordered: options2?.ordered ?? true }));
13731
14682
  },
13732
14683
  createOffer() {
13733
14684
  return raw.createOffer();
@@ -13809,27 +14760,245 @@ function adaptDataChannel2(raw) {
13809
14760
  }
13810
14761
  };
13811
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
+ }
13812
14823
  function toArrayBuffer2(data) {
13813
14824
  if (typeof data === "string") return data;
13814
14825
  return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
13815
14826
  }
13816
- function detectHostIpv4() {
13817
- const preferred = [];
13818
- const fallback = [];
13819
- for (const [name2, addresses] of Object.entries(networkInterfaces())) {
13820
- 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;
13821
14850
  for (const address of addresses ?? []) {
13822
- if (address.internal || address.family !== "IPv4") continue;
14851
+ if (address.internal || !isIpv4Family(address.family)) continue;
13823
14852
  const ip = address.address;
13824
- if (ip.startsWith("127.") || ip.startsWith("169.254.") || isCgnat(ip)) continue;
13825
- if (isPrivate(ip)) preferred.push(ip);
13826
- 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
+ });
13827
14859
  }
13828
14860
  }
13829
- 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
+ }
13830
14938
  }
13831
- function isVirtualInterface(name2) {
13832
- return /^(utun|ppp|bridge|awdl|llw|gif|stf|anpi|ap\d|en[1-9]\d*$)/i.test(name2);
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;
14995
+ }
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);
13833
15002
  }
13834
15003
  function isCgnat(ip) {
13835
15004
  const parts = ip.split(".").map(Number);
@@ -13846,8 +15015,9 @@ function isPrivate(ip) {
13846
15015
  // src/client-runtime.ts
13847
15016
  var REMOTE_COMMAND_LIST_MIN_VERSION = [0, 3, 16];
13848
15017
  var REMOTE_FILE_VIEWER_MIN_VERSION = [0, 3, 17];
15018
+ var DIRECT_WEBRTC_NEGOTIATE_TIMEOUT_MS = 12e3;
13849
15019
  var ClientModeRuntime = class {
13850
- constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadWeriftFactory) {
15020
+ constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory) {
13851
15021
  this.config = config;
13852
15022
  this.identities = identities;
13853
15023
  this.server = server;
@@ -14083,61 +15253,76 @@ var ClientModeRuntime = class {
14083
15253
  const presence = await this.server.presenceFor(targetDeviceId);
14084
15254
  if (!presence.online) throw new ClientModeError("HOST_OFFLINE", "The selected Host is offline.", true);
14085
15255
  const credentials = await this.server.authenticate(identity);
14086
- 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);
14087
15257
  let webRtcFallback = false;
14088
- const createTransport = (relayOnly) => new AdaptiveTransport(
15258
+ const createTransport = (attempt) => new AdaptiveTransport(
14089
15259
  websocketUrl(this.server.baseUrl),
14090
15260
  {
14091
15261
  role: "client",
14092
15262
  deviceId: identity.deviceId,
14093
15263
  accessToken: credentials.accessToken,
14094
15264
  targetDeviceId,
14095
- forceRelay: this.config.forceRelay || relayOnly,
14096
- preferredTransports: this.config.forceRelay || relayOnly ? ["relay"] : ["lan", "p2p", "turn", "relay"],
14097
- ...rtcFactory === void 0 || relayOnly ? {} : { rtcFactory },
14098
- fetchIceServers: async (connectionId) => this.server.turnCredentials(connectionId),
14099
- 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) => {
14100
15271
  webRtcFallback = true;
14101
- 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", {
14102
15273
  targetDeviceId: shortId(target.deviceId),
14103
- reason: diagnosticReason(error)
15274
+ attempt,
15275
+ reason: diagnosticReason(error),
15276
+ ...webrtcDiagnosticsLogFields(diagnostics)
14104
15277
  });
14105
15278
  }
14106
15279
  }
14107
15280
  );
14108
- let transport = createTransport(false);
14109
- 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;
14110
15284
  try {
14111
- await client.connect();
14112
- signal?.throwIfAborted();
14113
- if (webRtcFallback) {
14114
- await client.close();
14115
- transport = createTransport(true);
15285
+ for (const attempt of attempts) {
15286
+ webRtcFallback = false;
15287
+ transport = createTransport(attempt);
14116
15288
  client = new RemoteClientCore(new ClientSecureTransport(transport, identity, target), 6e4);
14117
15289
  await client.connect();
14118
15290
  signal?.throwIfAborted();
14119
- this.logger.info("remote Harness relay fallback re-established", {
14120
- targetDeviceId: shortId(target.deviceId)
14121
- });
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
+ }
14122
15300
  }
14123
- client.onClose(() => {
14124
- 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;
14125
15308
  this.connected = void 0;
14126
15309
  this.pendingWorkspaceSelection = void 0;
14127
15310
  this.proxySwitch.selectLocal();
14128
15311
  this.gatewaySwitch.selectLocal();
14129
- void client.close().catch(() => void 0);
15312
+ void connectedClient.close().catch(() => void 0);
14130
15313
  this.logger.warn("remote Harness transport closed; falling back to local mode", {
14131
15314
  targetDeviceId: shortId(target.deviceId)
14132
15315
  });
14133
15316
  });
15317
+ const connectionDetails = await connectedTransport.connectionDetails().catch(() => void 0);
14134
15318
  this.logger.info("remote Harness transport ready", {
14135
15319
  targetDeviceId: shortId(target.deviceId),
14136
- transport: client.getStats().mode
15320
+ transport: connectedClient.getStats().mode,
15321
+ ...webrtcDiagnosticsLogFields(connectionDetails?.webRtc?.diagnostics)
14137
15322
  });
14138
- return { client, target, transport, features: remoteHostFeatures(serverDevice.clientVersion) };
15323
+ return { client: connectedClient, target, transport: connectedTransport, features: remoteHostFeatures(serverDevice.clientVersion) };
14139
15324
  } catch (error) {
14140
- await client.close().catch(() => void 0);
15325
+ await client?.close().catch(() => void 0);
14141
15326
  throw error;
14142
15327
  }
14143
15328
  }
@@ -14284,6 +15469,20 @@ function websocketUrl(baseUrl) {
14284
15469
  url.pathname = `${url.pathname.replace(/\/$/, "")}/ws/v1/connect`;
14285
15470
  return url.toString();
14286
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
+ }
14287
15486
  function record(value) {
14288
15487
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
14289
15488
  throw new ClientModeError("INVALID_MESSAGE", "The control request payload is invalid.");
@@ -14361,6 +15560,14 @@ function diagnosticReason(error) {
14361
15560
  const message = error.message.replace(/[\r\n\t]+/g, " ").slice(0, 240);
14362
15561
  return code === void 0 ? message : `${code}: ${message}`;
14363
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
+ }
14364
15571
 
14365
15572
  // src/control-runtime.ts
14366
15573
  import { hostname as hostname2 } from "node:os";
@@ -14369,7 +15576,7 @@ import { hostname as hostname2 } from "node:os";
14369
15576
  import { createHash } from "node:crypto";
14370
15577
  import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
14371
15578
  import { homedir } from "node:os";
14372
- import { dirname, join } from "node:path";
15579
+ import { dirname as dirname2, join as join2 } from "node:path";
14373
15580
  var identitySchema = external_exports.object({
14374
15581
  schemaVersion: external_exports.literal(1),
14375
15582
  deviceId: external_exports.string().uuid(),
@@ -14398,14 +15605,14 @@ var IdentityStore = class {
14398
15605
  peers = /* @__PURE__ */ new Map();
14399
15606
  constructor(options = {}) {
14400
15607
  const env = options.env ?? process.env;
14401
- const dshHome = env.DSH_HOME || join(options.homeDirectory ?? homedir(), ".dsh");
14402
- 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");
14403
15610
  }
14404
15611
  async loadOrCreate(deviceName) {
14405
15612
  await mkdir(this.directory, { recursive: true, mode: 448 });
14406
15613
  await chmod(this.directory, 448);
14407
- const devicePath = join(this.directory, "device.json");
14408
- const keyPath = join(this.directory, "device.key");
15614
+ const devicePath = join2(this.directory, "device.json");
15615
+ const keyPath = join2(this.directory, "device.key");
14409
15616
  const [hasDevice, hasKey] = await Promise.all([exists(devicePath), exists(keyPath)]);
14410
15617
  if (hasDevice !== hasKey) {
14411
15618
  throw new IdentityInvalidError("device identity is incomplete; repair it explicitly before reconnecting");
@@ -14474,7 +15681,7 @@ var IdentityStore = class {
14474
15681
  return removed;
14475
15682
  }
14476
15683
  async loadPeers() {
14477
- const path = join(this.directory, "trusted-peers.json");
15684
+ const path = join2(this.directory, "trusted-peers.json");
14478
15685
  if (!await exists(path)) {
14479
15686
  await atomicJsonWrite(path, { schemaVersion: 1, peers: [] }, 384);
14480
15687
  }
@@ -14490,7 +15697,7 @@ var IdentityStore = class {
14490
15697
  this.peers = peers;
14491
15698
  }
14492
15699
  async savePeers() {
14493
- await atomicJsonWrite(join(this.directory, "trusted-peers.json"), {
15700
+ await atomicJsonWrite(join2(this.directory, "trusted-peers.json"), {
14494
15701
  schemaVersion: 1,
14495
15702
  peers: [...this.peers.values()]
14496
15703
  }, 384);
@@ -14499,7 +15706,7 @@ var IdentityStore = class {
14499
15706
  function serverStorageDirectory(root, serverUrl, role) {
14500
15707
  const origin = new URL(serverUrl).origin;
14501
15708
  const scope = createHash("sha256").update(origin).digest("hex").slice(0, 24);
14502
- return join(root, "servers", scope, role);
15709
+ return join2(root, "servers", scope, role);
14503
15710
  }
14504
15711
  function fingerprint(publicKey) {
14505
15712
  const compact = createHash("sha256").update(fromBase64Url2(publicKey)).digest("hex").slice(0, 12).toUpperCase();
@@ -14517,7 +15724,7 @@ async function atomicJsonWrite(path, value, mode) {
14517
15724
  `, mode);
14518
15725
  }
14519
15726
  async function atomicTextWrite(path, value, mode) {
14520
- await mkdir(dirname(path), { recursive: true, mode: 448 });
15727
+ await mkdir(dirname2(path), { recursive: true, mode: 448 });
14521
15728
  const temporary = `${path}.${process.pid}.${uuidV7()}.tmp`;
14522
15729
  await writeFile(temporary, value, { encoding: "utf8", mode, flag: "wx" });
14523
15730
  await chmod(temporary, mode);
@@ -14542,7 +15749,7 @@ function safeErrorMessage(error) {
14542
15749
 
14543
15750
  // src/server-credentials.ts
14544
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";
14545
- import { dirname as dirname2, join as join2 } from "node:path";
15752
+ import { dirname as dirname3, join as join3 } from "node:path";
14546
15753
  var credentialSchema = external_exports.object({
14547
15754
  schemaVersion: external_exports.literal(1),
14548
15755
  serverUrl: external_exports.string().url(),
@@ -14557,7 +15764,7 @@ var credentialSchema = external_exports.object({
14557
15764
  var ServerCredentialStore = class {
14558
15765
  path;
14559
15766
  constructor(directory) {
14560
- this.path = join2(directory, "server-credentials.json");
15767
+ this.path = join3(directory, "server-credentials.json");
14561
15768
  }
14562
15769
  async load(serverUrl, deviceId) {
14563
15770
  if (!await exists2(this.path)) return void 0;
@@ -14584,7 +15791,7 @@ var ServerCredentialsInvalidError = class extends Error {
14584
15791
  code = "SERVER_CREDENTIALS_INVALID";
14585
15792
  };
14586
15793
  async function atomicWrite(path, contents) {
14587
- await mkdir2(dirname2(path), { recursive: true, mode: 448 });
15794
+ await mkdir2(dirname3(path), { recursive: true, mode: 448 });
14588
15795
  const temporary = `${path}.${process.pid}.${uuidV7()}.tmp`;
14589
15796
  await writeFile2(temporary, contents, { encoding: "utf8", mode: 384, flag: "wx" });
14590
15797
  await chmod2(temporary, 384);
@@ -15044,7 +16251,7 @@ function shortId2(value) {
15044
16251
 
15045
16252
  // src/harness-version.ts
15046
16253
  import { readFile as readFile3 } from "node:fs/promises";
15047
- import { dirname as dirname3, isAbsolute, join as join3 } from "node:path";
16254
+ import { dirname as dirname4, isAbsolute, join as join4 } from "node:path";
15048
16255
  var LEGACY_PLACEHOLDER_VERSION = "0.0.1";
15049
16256
  function normalizeHarnessVersion(value) {
15050
16257
  if (typeof value !== "string") return void 0;
@@ -15058,20 +16265,35 @@ function selectHarnessVersion(reportedVersion, distributionVersion) {
15058
16265
  }
15059
16266
  async function readHarnessDistributionVersion(entrypoint = process.argv[1]) {
15060
16267
  if (entrypoint === void 0 || !isAbsolute(entrypoint)) return void 0;
15061
- let directory = dirname3(entrypoint);
16268
+ let directory = dirname4(entrypoint);
15062
16269
  for (let depth = 0; depth < 8; depth += 1) {
15063
16270
  try {
15064
- const manifest = JSON.parse(await readFile3(join3(directory, "package.json"), "utf8"));
16271
+ const manifest = JSON.parse(await readFile3(join4(directory, "package.json"), "utf8"));
15065
16272
  if (manifest.name === "@deepseek-ai/dsh") return normalizeHarnessVersion(manifest.version);
15066
16273
  } catch {
15067
16274
  }
15068
- const parent = dirname3(directory);
16275
+ const parent = dirname4(directory);
15069
16276
  if (parent === directory) break;
15070
16277
  directory = parent;
15071
16278
  }
15072
16279
  return void 0;
15073
16280
  }
15074
16281
 
16282
+ // src/safe-error.ts
16283
+ var RpcError = class extends Error {
16284
+ constructor(code, message, details, retryable = false) {
16285
+ super(message);
16286
+ this.code = code;
16287
+ this.details = details;
16288
+ this.retryable = retryable;
16289
+ }
16290
+ };
16291
+ function safeErrorCode(error) {
16292
+ if (error instanceof RpcError) return error.code;
16293
+ if (error instanceof external_exports.ZodError) return "INVALID_MESSAGE";
16294
+ return "INTERNAL_ERROR";
16295
+ }
16296
+
15075
16297
  // src/rpc-router.ts
15076
16298
  var wireRequestSchema = external_exports.object({ method: external_exports.string().min(1), params: external_exports.unknown() }).strict();
15077
16299
  var apiMethods = /* @__PURE__ */ new Set([
@@ -15126,8 +16348,7 @@ var RpcRouter = class {
15126
16348
  method: request.payload.method,
15127
16349
  durationMs: Math.round(performance.now() - startedAt),
15128
16350
  code: response.payload.code,
15129
- retryable: response.payload.retryable,
15130
- reason: diagnosticReason3(error)
16351
+ retryable: response.payload.retryable
15131
16352
  });
15132
16353
  return response;
15133
16354
  } finally {
@@ -15165,22 +16386,14 @@ var RpcRouter = class {
15165
16386
  }
15166
16387
  }
15167
16388
  };
15168
- var RpcError = class extends Error {
15169
- constructor(code, message, details, retryable = false) {
15170
- super(message);
15171
- this.code = code;
15172
- this.details = details;
15173
- this.retryable = retryable;
15174
- }
15175
- };
15176
16389
  function errorResponse(requestId, error) {
15177
- if (error instanceof RpcError) return createRpcError(requestId, error.code, error.message, error.details, error.retryable);
15178
- if (error instanceof external_exports.ZodError) return createRpcError(requestId, "INVALID_MESSAGE", "The RPC parameters are invalid.");
15179
- return createRpcError(requestId, "INTERNAL_ERROR", "The Host could not complete the request.");
15180
- }
15181
- function diagnosticReason3(error) {
15182
- const message = error instanceof Error ? error.message : String(error);
15183
- return message.replace(/[\r\n]+/g, " ").slice(0, 160) || "Unknown Host request failure.";
16390
+ const code = safeErrorCode(error);
16391
+ if (error instanceof RpcError) return createRpcError(requestId, code, error.message, error.details, error.retryable);
16392
+ return createRpcError(
16393
+ requestId,
16394
+ code,
16395
+ code === "INVALID_MESSAGE" ? "The RPC parameters are invalid." : "The Host could not complete the request."
16396
+ );
15184
16397
  }
15185
16398
 
15186
16399
  // src/file-viewer-contract.ts
@@ -15304,7 +16517,7 @@ function isOversizedListing(value) {
15304
16517
  }
15305
16518
 
15306
16519
  // src/server-connection.ts
15307
- var DEFAULT_WEBRTC_NEGOTIATE_TIMEOUT_MS = 8e3;
16520
+ var DEFAULT_WEBRTC_NEGOTIATE_TIMEOUT_MS = 12e3;
15308
16521
  var HostServerConnection = class {
15309
16522
  constructor(config, identity, identities, api, connections, logger, createWebSocket = (url) => new WebSocket(url), rtcFactoryProvider, hostCapabilities = () => ["harness.api.v1"], harnessVersion) {
15310
16523
  this.config = config;
@@ -15467,7 +16680,7 @@ var HostServerConnection = class {
15467
16680
  this.terminalError = code;
15468
16681
  this.logger.error("server control frame failed", {
15469
16682
  code,
15470
- reason: diagnosticReason4(error)
16683
+ reason: diagnosticReason3(error)
15471
16684
  });
15472
16685
  socket.close(4008, "invalid control frame");
15473
16686
  });
@@ -15594,6 +16807,7 @@ var HostServerConnection = class {
15594
16807
  connectionId: payload.connectionId,
15595
16808
  membershipId: descriptor.membershipId,
15596
16809
  peer,
16810
+ preferredTransports: payload.preferredTransports,
15597
16811
  noise,
15598
16812
  transport: "negotiating"
15599
16813
  });
@@ -15683,13 +16897,24 @@ var HostServerConnection = class {
15683
16897
  code: errorCode(error)
15684
16898
  });
15685
16899
  }
16900
+ if (!tunnel.preferredTransports.includes("turn")) iceServers = stunOnlyIceServers(iceServers);
15686
16901
  const rtc = new RtcDataChannelTransport({
15687
16902
  role: "responder",
15688
16903
  factory: this.rtcFactory,
15689
16904
  iceServers,
15690
16905
  onSignal: (signal) => this.sendRtcSignal(tunnel, signal),
15691
16906
  negotiateTimeoutMs: DEFAULT_WEBRTC_NEGOTIATE_TIMEOUT_MS,
15692
- label: `host<-${tunnel.peer.deviceId}`
16907
+ label: `host<-${tunnel.peer.deviceId}`,
16908
+ onDiagnostic: (event) => {
16909
+ if (event.type !== "local-candidate-filtered" && event.type !== "candidate-pair-filtered") return;
16910
+ this.logger.debug("webrtc candidate filtered", {
16911
+ connectionId: shortId3(tunnel.connectionId),
16912
+ peerDeviceId: shortId3(tunnel.peer.deviceId),
16913
+ type: event.type,
16914
+ reason: event.reason,
16915
+ candidate: event.type === "local-candidate-filtered" ? event.candidate : event.localCandidate
16916
+ });
16917
+ }
15693
16918
  });
15694
16919
  tunnel.rtc = rtc;
15695
16920
  rtc.onMessage((data) => tunnel.channel?.receive(void 0, data));
@@ -15731,7 +16956,8 @@ var HostServerConnection = class {
15731
16956
  this.logger.info("webrtc data channel ready", {
15732
16957
  connectionId: shortId3(tunnel.connectionId),
15733
16958
  peerDeviceId: shortId3(tunnel.peer.deviceId),
15734
- transport: tunnel.transportMode ?? selected
16959
+ transport: tunnel.transportMode ?? selected,
16960
+ ...webrtcDiagnosticsLogFields2(rtcDiagnostics(tunnel.rtc))
15735
16961
  });
15736
16962
  }
15737
16963
  async handleRtcFailed(tunnel, rtc, error) {
@@ -15739,7 +16965,8 @@ var HostServerConnection = class {
15739
16965
  if (tunnel.transport === "p2p" || tunnel.transport === "turn") {
15740
16966
  this.logger.warn("webrtc data channel failed; disconnecting peer", {
15741
16967
  connectionId: shortId3(tunnel.connectionId),
15742
- reason: diagnosticReason4(error)
16968
+ reason: diagnosticReason3(error),
16969
+ ...webrtcDiagnosticsLogFields2(rtcDiagnostics(rtc))
15743
16970
  });
15744
16971
  await this.dropTunnel(tunnel.connectionId, "CONNECTION_FAILED");
15745
16972
  return;
@@ -15749,7 +16976,8 @@ var HostServerConnection = class {
15749
16976
  await rtc.close();
15750
16977
  this.logger.warn("webrtc negotiation failed; falling back to relay", {
15751
16978
  connectionId: shortId3(tunnel.connectionId),
15752
- reason: diagnosticReason4(error)
16979
+ reason: diagnosticReason3(error),
16980
+ ...webrtcDiagnosticsLogFields2(rtcDiagnostics(rtc))
15753
16981
  });
15754
16982
  }
15755
16983
  sendTransportSelected(tunnel, transport) {
@@ -15793,7 +17021,7 @@ var HostServerConnection = class {
15793
17021
  } catch (error) {
15794
17022
  this.logger.warn("remote connection RTC cleanup failed", {
15795
17023
  connectionId: shortId3(connectionId),
15796
- reason: diagnosticReason4(error)
17024
+ reason: diagnosticReason3(error)
15797
17025
  });
15798
17026
  }
15799
17027
  if (tunnel.channel !== void 0) {
@@ -15804,7 +17032,7 @@ var HostServerConnection = class {
15804
17032
  await tunnel.channel.close(code).catch(() => void 0);
15805
17033
  this.logger.warn("remote connection channel cleanup failed", {
15806
17034
  connectionId: shortId3(connectionId),
15807
- reason: diagnosticReason4(error)
17035
+ reason: diagnosticReason3(error)
15808
17036
  });
15809
17037
  }
15810
17038
  } else {
@@ -15972,10 +17200,32 @@ function shortId3(value) {
15972
17200
  function asError2(error) {
15973
17201
  return error instanceof Error ? error : new Error("Unknown Server connection error.");
15974
17202
  }
15975
- function diagnosticReason4(error) {
17203
+ function diagnosticReason3(error) {
15976
17204
  const message = asError2(error).message.replace(/[\r\n]+/g, " ").slice(0, 160);
15977
17205
  return message || "Unknown Server connection error.";
15978
17206
  }
17207
+ function webrtcDiagnosticsLogFields2(diagnostics) {
17208
+ if (diagnostics === void 0) return {};
17209
+ return {
17210
+ rtcConnectionState: diagnostics.connectionState,
17211
+ rtcIceConnectionState: diagnostics.iceConnectionState,
17212
+ rtcIceGatheringState: diagnostics.iceGatheringState,
17213
+ rtcLocalCandidates: diagnostics.localCandidates,
17214
+ rtcRemoteCandidates: diagnostics.remoteCandidates,
17215
+ rtcCandidatePairs: diagnostics.candidatePairs,
17216
+ rtcFilteredLocalCandidates: diagnostics.filteredLocalCandidates,
17217
+ rtcFilteredCandidatePairs: diagnostics.filteredCandidatePairs,
17218
+ ...diagnostics.selectedPath === void 0 ? {} : { rtcSelectedPath: diagnostics.selectedPath }
17219
+ };
17220
+ }
17221
+ function rtcDiagnostics(rtc) {
17222
+ try {
17223
+ const candidate = rtc;
17224
+ return typeof candidate.diagnostics === "function" ? candidate.diagnostics() : void 0;
17225
+ } catch {
17226
+ return void 0;
17227
+ }
17228
+ }
15979
17229
  function errorCode(error) {
15980
17230
  return error instanceof ServerApiError || error instanceof ControlConnectionError ? error.code : "CONNECTION_FAILED";
15981
17231
  }
@@ -15993,7 +17243,7 @@ function closeCode(code) {
15993
17243
  // src/remote-directory-browser.ts
15994
17244
  import { readdir, stat as stat3 } from "node:fs/promises";
15995
17245
  import { homedir as homedir2, platform as platform2 } from "node:os";
15996
- import { basename, dirname as dirname4, isAbsolute as isAbsolute2, parse, resolve } from "node:path";
17246
+ import { basename, dirname as dirname5, isAbsolute as isAbsolute2, parse, resolve } from "node:path";
15997
17247
  var MAX_ENTRIES = 500;
15998
17248
  async function listRemoteDirectory(path, signal) {
15999
17249
  signal?.throwIfAborted();
@@ -16026,7 +17276,7 @@ function crumbs(path) {
16026
17276
  let current = path;
16027
17277
  while (current !== root) {
16028
17278
  segments.unshift(basename(current));
16029
- current = dirname4(current);
17279
+ current = dirname5(current);
16030
17280
  }
16031
17281
  for (const segment of segments) {
16032
17282
  current = resolve(current, segment);
@@ -16249,7 +17499,7 @@ var HarnessApiBridge = class {
16249
17499
  method: params.method,
16250
17500
  durationMs,
16251
17501
  timedOut: signal.aborted,
16252
- reason: diagnosticReason5(error)
17502
+ code: safeErrorCode(error)
16253
17503
  });
16254
17504
  throw error;
16255
17505
  }
@@ -16698,10 +17948,6 @@ function deniedSettingsNamespace(ns) {
16698
17948
  function deniedMethod(method) {
16699
17949
  return new RpcError("METHOD_NOT_ALLOWED", `Harness API method ${JSON.stringify(method)} is not available in remote mode.`);
16700
17950
  }
16701
- function diagnosticReason5(error) {
16702
- const message = error instanceof Error ? error.message : String(error);
16703
- return message.replace(/[\r\n]+/g, " ").slice(0, 160) || "Unknown Harness API failure.";
16704
- }
16705
17951
  function frameSessionId(frame) {
16706
17952
  const payload = frame.payload;
16707
17953
  return typeof payload.sessionId === "string" && payload.sessionId.length > 0 ? payload.sessionId : void 0;
@@ -16954,7 +18200,7 @@ var HostPluginRuntime = class {
16954
18200
  this.connections,
16955
18201
  this.logger,
16956
18202
  void 0,
16957
- this.config.forceRelay ? void 0 : loadWeriftFactory,
18203
+ this.config.forceRelay ? void 0 : () => loadNodeRtcFactory({ routeTargets: this.config.serverUrl === void 0 ? [] : [this.config.serverUrl] }),
16958
18204
  () => this.fileViewerHost?.() === void 0 ? ["harness.api.v1"] : ["harness.api.v1", "fileviewer.read.v1"],
16959
18205
  this.harnessVersion
16960
18206
  );
@@ -17053,6 +18299,7 @@ function decodeBase64(value) {
17053
18299
 
17054
18300
  // src/index.ts
17055
18301
  var name = "ds-harness-remote";
18302
+ var legacyLoaderModuleNames = /* @__PURE__ */ new Set(["dsh-remote", "@dsh-remote/plugin"]);
17056
18303
  function apply(ctx, input = {}) {
17057
18304
  ctx.inject(["settings", "apiProxy", "connection", "typertGateway"], (runtimeContext) => activate(runtimeContext, input));
17058
18305
  }
@@ -17085,6 +18332,7 @@ async function activate(ctx, input) {
17085
18332
  console.error(message);
17086
18333
  }
17087
18334
  }, config.logLevel);
18335
+ await disableLegacyLoaderEntries(ctx, logger);
17088
18336
  const defaultIdentityDirectory = new IdentityStore().directory;
17089
18337
  const hostIdentities = new IdentityStore({
17090
18338
  directory: config.serverUrl === void 0 ? defaultIdentityDirectory : serverStorageDirectory(defaultIdentityDirectory, config.serverUrl, "host")
@@ -17146,6 +18394,29 @@ async function activate(ctx, input) {
17146
18394
  };
17147
18395
  }, "dsh-remote lifecycle");
17148
18396
  }
18397
+ async function disableLegacyLoaderEntries(ctx, logger) {
18398
+ const loader = ctx.get("loader");
18399
+ if (!isLoaderLike(loader)) return;
18400
+ const currentEntryId = loader.locate?.(ctx.fiber);
18401
+ for (const entry of loader.entries()) {
18402
+ const moduleName = entry.options.name;
18403
+ if (moduleName === void 0 || !legacyLoaderModuleNames.has(moduleName)) continue;
18404
+ if (entry.id === currentEntryId || entry.options.disabled === true) continue;
18405
+ try {
18406
+ await loader.update(entry.id, { disabled: true });
18407
+ logger.warn("disabled legacy loader entry", { entryId: entry.id, moduleName });
18408
+ } catch {
18409
+ logger.warn("failed to disable legacy loader entry", {
18410
+ entryId: entry.id,
18411
+ moduleName,
18412
+ code: "LOADER_UPDATE_FAILED"
18413
+ });
18414
+ }
18415
+ }
18416
+ }
18417
+ function isLoaderLike(value) {
18418
+ return typeof value === "object" && value !== null && typeof value.entries === "function" && typeof value.update === "function";
18419
+ }
17149
18420
  export {
17150
18421
  ApiProxySwitch,
17151
18422
  ClientModeError,