ds-harness-remote 0.3.16 → 0.3.17

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 (40) hide show
  1. package/README.md +29 -10
  2. package/dist/client-runtime.d.ts +5 -1
  3. package/dist/client-runtime.d.ts.map +1 -1
  4. package/dist/client-runtime.js +83 -7
  5. package/dist/client-runtime.js.map +1 -1
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.github.js +227 -9
  8. package/dist/client.js +227 -9
  9. package/dist/client.js.map +1 -1
  10. package/dist/file-viewer-bridge.d.ts +30 -0
  11. package/dist/file-viewer-bridge.d.ts.map +1 -0
  12. package/dist/file-viewer-bridge.js +128 -0
  13. package/dist/file-viewer-bridge.js.map +1 -0
  14. package/dist/file-viewer-contract.d.ts +5 -0
  15. package/dist/file-viewer-contract.d.ts.map +1 -0
  16. package/dist/file-viewer-contract.js +4 -0
  17. package/dist/file-viewer-contract.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +624 -259
  21. package/dist/index.js.map +4 -4
  22. package/dist/remote-file-content-provider.d.ts +28 -0
  23. package/dist/remote-file-content-provider.d.ts.map +1 -0
  24. package/dist/remote-file-content-provider.js +69 -0
  25. package/dist/remote-file-content-provider.js.map +1 -0
  26. package/dist/rpc-router.d.ts +4 -2
  27. package/dist/rpc-router.d.ts.map +1 -1
  28. package/dist/rpc-router.js +11 -2
  29. package/dist/rpc-router.js.map +1 -1
  30. package/dist/server-connection.d.ts +2 -1
  31. package/dist/server-connection.d.ts.map +1 -1
  32. package/dist/server-connection.js +9 -6
  33. package/dist/server-connection.js.map +1 -1
  34. package/dist/service.d.ts +3 -1
  35. package/dist/service.d.ts.map +1 -1
  36. package/dist/service.js +9 -3
  37. package/dist/service.js.map +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4083,7 +4083,8 @@ var rpcMethods = [
4083
4083
  "harness.api.call",
4084
4084
  "harness.api.respond",
4085
4085
  "harness.api.stream.open",
4086
- "harness.api.stream.close"
4086
+ "harness.api.stream.close",
4087
+ "fileviewer.call"
4087
4088
  ];
4088
4089
  var rpcMethodSchema = external_exports.enum(rpcMethods);
4089
4090
  var messageTypeSchema = external_exports.enum(messageTypes);
@@ -4803,6 +4804,7 @@ var RtcDataChannelTransport = class {
4803
4804
  bytesSent = 0;
4804
4805
  bytesReceived = 0;
4805
4806
  selected;
4807
+ selectedMode;
4806
4808
  lastBufferedAmount = 0;
4807
4809
  constructor(options) {
4808
4810
  this.role = options.role;
@@ -4879,10 +4881,27 @@ var RtcDataChannelTransport = class {
4879
4881
  selectedTransport() {
4880
4882
  return this.selected;
4881
4883
  }
4884
+ selectedPathMode() {
4885
+ return this.selectedMode;
4886
+ }
4887
+ async connectionDetails() {
4888
+ let statsDetails = {};
4889
+ try {
4890
+ statsDetails = inspectSelectedPath(await this.pc.getStats());
4891
+ } catch {
4892
+ }
4893
+ return {
4894
+ ...statsDetails,
4895
+ mode: statsDetails.mode ?? this.selectedMode,
4896
+ connectionState: this.pc.connectionState,
4897
+ iceConnectionState: this.pc.iceConnectionState,
4898
+ dataChannelState: this.channel?.readyState
4899
+ };
4900
+ }
4882
4901
  getStats() {
4883
4902
  const connected = this.channel?.readyState === "open";
4884
4903
  return {
4885
- mode: !connected ? "Disconnected" : this.selected === "turn" ? "TURN" : "P2P",
4904
+ mode: !connected ? "Disconnected" : this.selectedMode ?? (this.selected === "turn" ? "TURN" : "P2P"),
4886
4905
  connected,
4887
4906
  bytesSent: this.bytesSent,
4888
4907
  bytesReceived: this.bytesReceived
@@ -5043,9 +5062,12 @@ var RtcDataChannelTransport = class {
5043
5062
  }
5044
5063
  async resolveSelectedTransport() {
5045
5064
  try {
5046
- this.selected = detectSelectedTransport(await this.pc.getStats());
5065
+ const selected = inspectSelectedPath(await this.pc.getStats());
5066
+ this.selected = selected?.transport;
5067
+ this.selectedMode = selected?.mode;
5047
5068
  } catch {
5048
5069
  this.selected = void 0;
5070
+ this.selectedMode = void 0;
5049
5071
  }
5050
5072
  }
5051
5073
  checkConnectionState() {
@@ -5119,28 +5141,73 @@ var RtcConnectError = class extends Error {
5119
5141
  this.code = code;
5120
5142
  }
5121
5143
  };
5122
- function detectSelectedTransport(stats) {
5123
- const candidateTypes = /* @__PURE__ */ new Map();
5144
+ function inspectSelectedPath(stats) {
5145
+ const candidates = /* @__PURE__ */ new Map();
5146
+ const candidatePairs = /* @__PURE__ */ new Map();
5124
5147
  const selectedPairs = [];
5125
- for (const [, entry] of stats) {
5148
+ const selectedPairIds = /* @__PURE__ */ new Set();
5149
+ for (const [reportId, entry] of stats) {
5126
5150
  if (entry.type === "local-candidate" || entry.type === "remote-candidate") {
5127
5151
  if (typeof entry.candidateType === "string" && entry.id !== void 0) {
5128
- candidateTypes.set(String(entry.id), entry.candidateType);
5152
+ candidates.set(String(entry.id), entry);
5129
5153
  }
5130
- } else if (entry.type === "candidate-pair" || entry.type === "transport") {
5154
+ } else if (entry.type === "candidate-pair") {
5155
+ candidatePairs.set(String(entry.id ?? reportId), entry);
5131
5156
  if (entry.selected === true || entry.nominated === true)
5132
5157
  selectedPairs.push(entry);
5133
- }
5134
- }
5135
- for (const pair of selectedPairs) {
5136
- const local = candidateTypes.get(String(pair.localCandidateId));
5137
- const remote = candidateTypes.get(String(pair.remoteCandidateId));
5138
- if (local === "relay" || remote === "relay")
5139
- return "turn";
5140
- if (local !== void 0 || remote !== void 0)
5141
- return "p2p";
5158
+ } else if (entry.type === "transport" && typeof entry.selectedCandidatePairId === "string") {
5159
+ selectedPairIds.add(entry.selectedCandidatePairId);
5160
+ }
5161
+ }
5162
+ const pairs = [
5163
+ ...[...selectedPairIds].map((id) => candidatePairs.get(id)).filter((pair) => pair !== void 0),
5164
+ ...selectedPairs
5165
+ ];
5166
+ for (const pair of pairs) {
5167
+ const local = candidates.get(String(pair.localCandidateId));
5168
+ const remote = candidates.get(String(pair.remoteCandidateId));
5169
+ const localType = local?.candidateType;
5170
+ const remoteType = remote?.candidateType;
5171
+ let selected;
5172
+ if (localType === "relay" || remoteType === "relay")
5173
+ selected = { transport: "turn", mode: "TURN" };
5174
+ if (local === void 0 && remote === void 0)
5175
+ continue;
5176
+ selected ??= {
5177
+ transport: "p2p",
5178
+ mode: localType === "host" && remoteType === "host" ? "LAN" : "P2P"
5179
+ };
5180
+ const currentRoundTripTime = numberStat(pair, "currentRoundTripTime");
5181
+ return {
5182
+ ...selected,
5183
+ localCandidateType: stringStat(local, "candidateType"),
5184
+ remoteCandidateType: stringStat(remote, "candidateType"),
5185
+ localAddress: candidateAddress(local),
5186
+ remoteAddress: candidateAddress(remote),
5187
+ protocol: stringStat(local, "protocol") ?? stringStat(remote, "protocol"),
5188
+ relayProtocol: stringStat(local, "relayProtocol") ?? stringStat(remote, "relayProtocol"),
5189
+ currentRoundTripTimeMs: currentRoundTripTime === void 0 ? void 0 : Math.round(currentRoundTripTime * 1e3),
5190
+ availableOutgoingBitrate: numberStat(pair, "availableOutgoingBitrate"),
5191
+ bytesSent: numberStat(pair, "bytesSent"),
5192
+ bytesReceived: numberStat(pair, "bytesReceived")
5193
+ };
5142
5194
  }
5143
- return void 0;
5195
+ return {};
5196
+ }
5197
+ function candidateAddress(candidate) {
5198
+ const address = stringStat(candidate, "address") ?? stringStat(candidate, "ip");
5199
+ if (address === void 0)
5200
+ return void 0;
5201
+ const port = numberStat(candidate, "port");
5202
+ return port === void 0 ? address : `${address}:${port}`;
5203
+ }
5204
+ function stringStat(entry, key) {
5205
+ const value = entry?.[key];
5206
+ return typeof value === "string" && value.length > 0 ? value : void 0;
5207
+ }
5208
+ function numberStat(entry, key) {
5209
+ const value = entry?.[key];
5210
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
5144
5211
  }
5145
5212
  function requireSdp(description) {
5146
5213
  if (typeof description.sdp !== "string" || description.sdp.length === 0) {
@@ -5159,7 +5226,7 @@ function asError(error) {
5159
5226
 
5160
5227
  // ../webrtc/dist/adaptive-transport.js
5161
5228
  var DEFAULT_CAPABILITIES = ["transport.p2p", "transport.turn", "transport.relay", "harness.api.v1"];
5162
- var DEFAULT_PREFERRED_TRANSPORTS = ["p2p", "turn", "relay"];
5229
+ var DEFAULT_PREFERRED_TRANSPORTS = ["lan", "p2p", "turn", "relay"];
5163
5230
  var AdaptiveTransport = class extends BaseTransport {
5164
5231
  url;
5165
5232
  options;
@@ -5177,6 +5244,7 @@ var AdaptiveTransport = class extends BaseTransport {
5177
5244
  handshakeTimer;
5178
5245
  webrtcEnabled = true;
5179
5246
  serverNegotiateTimeoutMs;
5247
+ connectedAt;
5180
5248
  constructor(url, options) {
5181
5249
  super();
5182
5250
  this.url = url;
@@ -5246,6 +5314,16 @@ var AdaptiveTransport = class extends BaseTransport {
5246
5314
  remoteDeviceId: this.options.targetDeviceId
5247
5315
  };
5248
5316
  }
5317
+ async connectionDetails() {
5318
+ return {
5319
+ ...this.connectionId === void 0 ? {} : { connectionId: this.connectionId },
5320
+ ...this.connectedAt === void 0 ? {} : { connectedAt: this.connectedAt },
5321
+ controlChannelUrl: this.url,
5322
+ controlChannelState: socketState(this.socket?.readyState),
5323
+ preferredTransports: this.options.forceRelay === true ? ["relay"] : [...this.options.preferredTransports ?? DEFAULT_PREFERRED_TRANSPORTS],
5324
+ ...this.rtc === void 0 ? {} : { webRtc: await this.rtc.connectionDetails() }
5325
+ };
5326
+ }
5249
5327
  async sendHandshake(step, data) {
5250
5328
  if (this.socket?.readyState !== WebSocket.OPEN || this.connectionId === void 0) {
5251
5329
  throw new Error("adaptive transport has not been authorized");
@@ -5276,10 +5354,9 @@ var AdaptiveTransport = class extends BaseTransport {
5276
5354
  let mode = "Disconnected";
5277
5355
  if (this.selected === "relay")
5278
5356
  mode = relayConnected ? "Relay" : "Disconnected";
5279
- else if (this.selected === "turn")
5280
- mode = webrtcConnected ? "TURN" : "Disconnected";
5281
- else if (this.selected === "p2p")
5282
- mode = webrtcConnected ? "P2P" : "Disconnected";
5357
+ else if (this.selected === "turn" || this.selected === "p2p") {
5358
+ mode = webrtcConnected ? this.rtc.getStats().mode : "Disconnected";
5359
+ }
5283
5360
  return { mode, connected, bytesSent: this.bytesSent, bytesReceived: this.bytesReceived };
5284
5361
  }
5285
5362
  async handleSocketMessage(raw) {
@@ -5365,14 +5442,19 @@ var AdaptiveTransport = class extends BaseTransport {
5365
5442
  if (this.connectionId === void 0)
5366
5443
  return;
5367
5444
  const preferred = this.options.preferredTransports ?? [...DEFAULT_PREFERRED_TRANSPORTS];
5368
- const wantWebRtc = !this.options.forceRelay && this.webrtcEnabled && (preferred.includes("p2p") || preferred.includes("turn"));
5445
+ const wantWebRtc = !this.options.forceRelay && this.webrtcEnabled && (preferred.includes("lan") || preferred.includes("p2p") || preferred.includes("turn"));
5369
5446
  let selected = "relay";
5370
5447
  if (wantWebRtc) {
5371
5448
  try {
5372
5449
  const rtcSelected = await this.tryWebRtc();
5373
5450
  this.dataMode = "webrtc";
5374
5451
  selected = rtcSelected;
5375
- } catch {
5452
+ } catch (error) {
5453
+ const reason = error instanceof Error ? error : new Error("WebRTC negotiation failed.");
5454
+ try {
5455
+ this.options.onWebRtcFallback?.(reason);
5456
+ } catch {
5457
+ }
5376
5458
  await this.rtc?.close();
5377
5459
  this.rtc = void 0;
5378
5460
  this.dataMode = "relay";
@@ -5457,6 +5539,7 @@ var AdaptiveTransport = class extends BaseTransport {
5457
5539
  }
5458
5540
  finishConnection() {
5459
5541
  this.clearHandshake();
5542
+ this.connectedAt ??= Date.now();
5460
5543
  this.readyResolve?.();
5461
5544
  this.readyResolve = void 0;
5462
5545
  this.readyReject = void 0;
@@ -5487,6 +5570,15 @@ function adaptiveControlCloseError(event) {
5487
5570
  function isObject(value) {
5488
5571
  return typeof value === "object" && value !== null && !Array.isArray(value);
5489
5572
  }
5573
+ function socketState(readyState) {
5574
+ if (readyState === 0)
5575
+ return "connecting";
5576
+ if (readyState === 1)
5577
+ return "open";
5578
+ if (readyState === 2)
5579
+ return "closing";
5580
+ return "closed";
5581
+ }
5490
5582
 
5491
5583
  // src/api-proxy-switch.ts
5492
5584
  var SWITCHED_DOMAINS = [
@@ -12973,7 +13065,7 @@ function normalizeServerUrl(value) {
12973
13065
  }
12974
13066
 
12975
13067
  // src/version.ts
12976
- var PLUGIN_VERSION = "0.3.16";
13068
+ var PLUGIN_VERSION = "0.3.17";
12977
13069
 
12978
13070
  // src/server-api.ts
12979
13071
  var HostServerApi = class {
@@ -13396,14 +13488,208 @@ function isRemoteCommandMethod(method) {
13396
13488
  return REMOTE_COMMAND_METHODS.includes(method);
13397
13489
  }
13398
13490
 
13491
+ // src/werift-rtc.ts
13492
+ import { networkInterfaces } from "node:os";
13493
+ var cachedFactory;
13494
+ async function loadWeriftFactory() {
13495
+ if (cachedFactory !== void 0) return cachedFactory;
13496
+ try {
13497
+ const werift = await import("werift");
13498
+ cachedFactory = buildWeriftFactory(werift);
13499
+ return cachedFactory;
13500
+ } catch {
13501
+ return void 0;
13502
+ }
13503
+ }
13504
+ function buildWeriftFactory(werift) {
13505
+ return {
13506
+ create(configuration) {
13507
+ const hostIpv4 = detectHostIpv4();
13508
+ const raw = new werift.RTCPeerConnection({
13509
+ iceServers: configuration.iceServers ?? [],
13510
+ iceUseIpv6: false,
13511
+ iceUseLinkLocalAddress: false,
13512
+ ...hostIpv4 === void 0 ? {} : { iceInterfaceAddresses: { udp4: hostIpv4 } }
13513
+ });
13514
+ let onIceCandidate = null;
13515
+ let onDataChannel = null;
13516
+ raw.onicecandidate = (event) => {
13517
+ if (onIceCandidate === null) return;
13518
+ onIceCandidate({ candidate: event.candidate === void 0 ? null : event.candidate.toJSON() });
13519
+ };
13520
+ raw.ondatachannel = (event) => {
13521
+ if (onDataChannel === null) return;
13522
+ onDataChannel({ channel: adaptDataChannel2(event.channel) });
13523
+ };
13524
+ const pc = {
13525
+ get connectionState() {
13526
+ return raw.connectionState;
13527
+ },
13528
+ get iceConnectionState() {
13529
+ return raw.iceConnectionState;
13530
+ },
13531
+ get iceGatheringState() {
13532
+ return raw.iceGatheringState;
13533
+ },
13534
+ get signalingState() {
13535
+ return raw.signalingState;
13536
+ },
13537
+ set onconnectionstatechange(value) {
13538
+ raw.onconnectionstatechange = value;
13539
+ },
13540
+ get onconnectionstatechange() {
13541
+ return raw.onconnectionstatechange;
13542
+ },
13543
+ set oniceconnectionstatechange(value) {
13544
+ raw.oniceconnectionstatechange = value;
13545
+ },
13546
+ get oniceconnectionstatechange() {
13547
+ return raw.oniceconnectionstatechange;
13548
+ },
13549
+ set onicegatheringstatechange(value) {
13550
+ raw.onicegatheringstatechange = value;
13551
+ },
13552
+ get onicegatheringstatechange() {
13553
+ return raw.onicegatheringstatechange;
13554
+ },
13555
+ set onicecandidate(value) {
13556
+ onIceCandidate = value;
13557
+ },
13558
+ get onicecandidate() {
13559
+ return onIceCandidate;
13560
+ },
13561
+ set ondatachannel(value) {
13562
+ onDataChannel = value;
13563
+ },
13564
+ get ondatachannel() {
13565
+ return onDataChannel;
13566
+ },
13567
+ createDataChannel(label, options) {
13568
+ return adaptDataChannel2(raw.createDataChannel(label, { ordered: options?.ordered ?? true }));
13569
+ },
13570
+ createOffer() {
13571
+ return raw.createOffer();
13572
+ },
13573
+ createAnswer() {
13574
+ return raw.createAnswer();
13575
+ },
13576
+ setLocalDescription(description) {
13577
+ return raw.setLocalDescription(description).then(() => void 0);
13578
+ },
13579
+ setRemoteDescription(description) {
13580
+ return raw.setRemoteDescription(description);
13581
+ },
13582
+ addIceCandidate(candidate) {
13583
+ return raw.addIceCandidate(candidate);
13584
+ },
13585
+ getStats() {
13586
+ return raw.getStats();
13587
+ },
13588
+ close() {
13589
+ void raw.close().catch(() => void 0);
13590
+ }
13591
+ };
13592
+ return pc;
13593
+ }
13594
+ };
13595
+ }
13596
+ function adaptDataChannel2(raw) {
13597
+ return {
13598
+ get label() {
13599
+ return raw.label;
13600
+ },
13601
+ get ordered() {
13602
+ return raw.ordered;
13603
+ },
13604
+ get readyState() {
13605
+ return raw.readyState;
13606
+ },
13607
+ get bufferedAmount() {
13608
+ return raw.bufferedAmount;
13609
+ },
13610
+ binaryType: "arraybuffer",
13611
+ set onopen(value) {
13612
+ raw.onopen = value;
13613
+ },
13614
+ get onopen() {
13615
+ return raw.onopen;
13616
+ },
13617
+ set onmessage(value) {
13618
+ raw.onmessage = value === null ? null : (event) => value({ data: toArrayBuffer2(event.data) });
13619
+ },
13620
+ get onmessage() {
13621
+ return null;
13622
+ },
13623
+ set onclose(value) {
13624
+ raw.onclose = value;
13625
+ },
13626
+ get onclose() {
13627
+ return raw.onclose;
13628
+ },
13629
+ set onerror(value) {
13630
+ raw.onerror = value;
13631
+ },
13632
+ get onerror() {
13633
+ return raw.onerror;
13634
+ },
13635
+ onbufferedamountlow: null,
13636
+ send(data) {
13637
+ const bytes = typeof data === "string" ? Buffer.byteLength(data) : data.byteLength;
13638
+ try {
13639
+ raw.send(typeof data === "string" ? data : Buffer.from(data));
13640
+ } catch (error) {
13641
+ console.error("[werift-send-error] bytes=" + bytes, error instanceof Error ? error.message : error);
13642
+ throw error;
13643
+ }
13644
+ },
13645
+ close() {
13646
+ raw.close();
13647
+ }
13648
+ };
13649
+ }
13650
+ function toArrayBuffer2(data) {
13651
+ if (typeof data === "string") return data;
13652
+ return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
13653
+ }
13654
+ function detectHostIpv4() {
13655
+ const preferred = [];
13656
+ const fallback = [];
13657
+ for (const [name2, addresses] of Object.entries(networkInterfaces())) {
13658
+ if (isVirtualInterface(name2)) continue;
13659
+ for (const address of addresses ?? []) {
13660
+ if (address.internal || address.family !== "IPv4") continue;
13661
+ const ip = address.address;
13662
+ if (ip.startsWith("127.") || ip.startsWith("169.254.") || isCgnat(ip)) continue;
13663
+ if (isPrivate(ip)) preferred.push(ip);
13664
+ else fallback.push(ip);
13665
+ }
13666
+ }
13667
+ return preferred[0] ?? fallback[0];
13668
+ }
13669
+ function isVirtualInterface(name2) {
13670
+ return /^(utun|ppp|bridge|awdl|llw|gif|stf|anpi|ap\d|en[1-9]\d*$)/i.test(name2);
13671
+ }
13672
+ function isCgnat(ip) {
13673
+ const parts = ip.split(".").map(Number);
13674
+ return parts.length === 4 && parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127;
13675
+ }
13676
+ function isPrivate(ip) {
13677
+ const parts = ip.split(".").map(Number);
13678
+ if (parts.length !== 4) return false;
13679
+ if (parts[0] === 10) return true;
13680
+ if (parts[0] === 192 && parts[1] === 168) return true;
13681
+ return parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31;
13682
+ }
13683
+
13399
13684
  // src/client-runtime.ts
13400
13685
  var ClientModeRuntime = class {
13401
- constructor(config, identities, server, apiProxy, typertGateway, logger, host) {
13686
+ constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadWeriftFactory) {
13402
13687
  this.config = config;
13403
13688
  this.identities = identities;
13404
13689
  this.server = server;
13405
13690
  this.logger = logger;
13406
13691
  this.host = host;
13692
+ this.rtcFactoryProvider = rtcFactoryProvider;
13407
13693
  this.proxySwitch = new ApiProxySwitch(apiProxy);
13408
13694
  this.gatewaySwitch = new TypertGatewaySwitch(typertGateway);
13409
13695
  }
@@ -13441,6 +13727,28 @@ var ClientModeRuntime = class {
13441
13727
  ...this.host === void 0 ? {} : { host: this.host.hostStatus() }
13442
13728
  };
13443
13729
  }
13730
+ async detailedStatus() {
13731
+ const connected = this.connected;
13732
+ if (connected === void 0 || this.identity === void 0) return this.status();
13733
+ const details = await connected.transport.connectionDetails();
13734
+ if (this.connected !== connected) return this.status();
13735
+ return {
13736
+ ...this.status(),
13737
+ network: {
13738
+ ...details,
13739
+ local: {
13740
+ deviceId: this.identity.deviceId,
13741
+ name: this.identity.name,
13742
+ platform: process.platform
13743
+ },
13744
+ remote: {
13745
+ deviceId: connected.target.deviceId,
13746
+ name: connected.target.name,
13747
+ platform: connected.target.platform
13748
+ }
13749
+ }
13750
+ };
13751
+ }
13444
13752
  async devices() {
13445
13753
  this.requireIdentity();
13446
13754
  const serverDevices = await this.server.listDevices();
@@ -13566,6 +13874,13 @@ var ClientModeRuntime = class {
13566
13874
  this.proxySwitch.restore();
13567
13875
  this.gatewaySwitch.restore();
13568
13876
  }
13877
+ async callRemoteFileViewer(endpoint, payload, signal) {
13878
+ const remote = this.connected;
13879
+ if (remote === void 0 || this.proxySwitch.status().mode !== "remote") {
13880
+ throw new ClientModeError("REMOTE_NOT_CONNECTED", "No Remote Host is selected.", true);
13881
+ }
13882
+ return remote.client.rpc("fileviewer.call", { endpoint, payload }, signal);
13883
+ }
13569
13884
  async connect(targetDeviceId, signal) {
13570
13885
  signal?.throwIfAborted();
13571
13886
  const identity = this.requireIdentity();
@@ -13577,19 +13892,43 @@ var ClientModeRuntime = class {
13577
13892
  const presence = await this.server.presenceFor(targetDeviceId);
13578
13893
  if (!presence.online) throw new ClientModeError("HOST_OFFLINE", "The selected Host is offline.", true);
13579
13894
  const credentials = await this.server.authenticate(identity);
13580
- const transport = new AdaptiveTransport(websocketUrl(this.server.baseUrl), {
13581
- role: "client",
13582
- deviceId: identity.deviceId,
13583
- accessToken: credentials.accessToken,
13584
- targetDeviceId,
13585
- forceRelay: this.config.forceRelay,
13586
- preferredTransports: this.config.forceRelay ? ["relay"] : ["p2p", "turn", "relay"],
13587
- fetchIceServers: async (connectionId) => this.server.turnCredentials(connectionId)
13588
- });
13589
- const client = new RemoteClientCore(new ClientSecureTransport(transport, identity, target), 6e4);
13895
+ const rtcFactory = this.config.forceRelay ? void 0 : await this.rtcFactoryProvider().catch(() => void 0);
13896
+ let webRtcFallback = false;
13897
+ const createTransport = (relayOnly) => new AdaptiveTransport(
13898
+ websocketUrl(this.server.baseUrl),
13899
+ {
13900
+ role: "client",
13901
+ deviceId: identity.deviceId,
13902
+ accessToken: credentials.accessToken,
13903
+ targetDeviceId,
13904
+ forceRelay: this.config.forceRelay || relayOnly,
13905
+ preferredTransports: this.config.forceRelay || relayOnly ? ["relay"] : ["lan", "p2p", "turn", "relay"],
13906
+ ...rtcFactory === void 0 || relayOnly ? {} : { rtcFactory },
13907
+ fetchIceServers: async (connectionId) => this.server.turnCredentials(connectionId),
13908
+ onWebRtcFallback: (error) => {
13909
+ webRtcFallback = true;
13910
+ this.logger.warn("remote Harness WebRTC failed; using relay", {
13911
+ targetDeviceId: shortId(target.deviceId),
13912
+ reason: diagnosticReason(error)
13913
+ });
13914
+ }
13915
+ }
13916
+ );
13917
+ let transport = createTransport(false);
13918
+ let client = new RemoteClientCore(new ClientSecureTransport(transport, identity, target), 6e4);
13590
13919
  try {
13591
13920
  await client.connect();
13592
13921
  signal?.throwIfAborted();
13922
+ if (webRtcFallback) {
13923
+ await client.close();
13924
+ transport = createTransport(true);
13925
+ client = new RemoteClientCore(new ClientSecureTransport(transport, identity, target), 6e4);
13926
+ await client.connect();
13927
+ signal?.throwIfAborted();
13928
+ this.logger.info("remote Harness relay fallback re-established", {
13929
+ targetDeviceId: shortId(target.deviceId)
13930
+ });
13931
+ }
13593
13932
  client.onClose(() => {
13594
13933
  if (this.connected?.client !== client) return;
13595
13934
  this.connected = void 0;
@@ -13600,7 +13939,11 @@ var ClientModeRuntime = class {
13600
13939
  targetDeviceId: shortId(target.deviceId)
13601
13940
  });
13602
13941
  });
13603
- return { client, target };
13942
+ this.logger.info("remote Harness transport ready", {
13943
+ targetDeviceId: shortId(target.deviceId),
13944
+ transport: client.getStats().mode
13945
+ });
13946
+ return { client, target, transport };
13604
13947
  } catch (error) {
13605
13948
  await client.close().catch(() => void 0);
13606
13949
  throw error;
@@ -13616,7 +13959,7 @@ var ClientModeRuntime = class {
13616
13959
  }
13617
13960
  async handleControl(endpoint, payload, signal) {
13618
13961
  try {
13619
- if (endpoint === "status") return ok(this.status());
13962
+ if (endpoint === "status") return ok(await this.detailedStatus());
13620
13963
  if (endpoint === "devices") return ok(await this.devices());
13621
13964
  if (endpoint === "client.account.login") {
13622
13965
  const value = record(payload);
@@ -13654,6 +13997,10 @@ var ClientModeRuntime = class {
13654
13997
  }
13655
13998
  return ok(await this.openRemoteWorkspace(value.targetDeviceId, value.path, signal));
13656
13999
  }
14000
+ if (endpoint === "fileviewer.stat" || endpoint === "fileviewer.readRange" || endpoint === "fileviewer.list") {
14001
+ const method = endpoint === "fileviewer.stat" ? "stat" : endpoint === "fileviewer.readRange" ? "readRange" : "list";
14002
+ return ok(await this.callRemoteFileViewer(method, payload, signal));
14003
+ }
13657
14004
  if (endpoint === "host.account.login") {
13658
14005
  if (this.host === void 0) throw new ClientModeError("METHOD_NOT_ALLOWED", "This plugin is not running as a Host.");
13659
14006
  const value = record(payload);
@@ -13776,6 +14123,11 @@ function fail(error) {
13776
14123
  function shortId(value) {
13777
14124
  return value.length <= 12 ? value : `${value.slice(0, 8)}\u2026${value.slice(-4)}`;
13778
14125
  }
14126
+ function diagnosticReason(error) {
14127
+ const code = "code" in error && typeof error.code === "string" ? error.code : void 0;
14128
+ const message = error.message.replace(/[\r\n\t]+/g, " ").slice(0, 240);
14129
+ return code === void 0 ? message : `${code}: ${message}`;
14130
+ }
13779
14131
 
13780
14132
  // src/control-runtime.ts
13781
14133
  import { hostname as hostname2 } from "node:os";
@@ -14396,7 +14748,7 @@ var ConnectionController = class {
14396
14748
  } catch (error) {
14397
14749
  this.logger?.warn("peer message handling failed; disconnecting", {
14398
14750
  peerDeviceId: shortId2(connection.channel.peerDeviceId),
14399
- reason: diagnosticReason(error)
14751
+ reason: diagnosticReason2(error)
14400
14752
  });
14401
14753
  await this.disconnect(connection);
14402
14754
  }
@@ -14414,7 +14766,7 @@ var ConnectionController = class {
14414
14766
  this.logger?.warn("peer send failed; disconnecting", {
14415
14767
  peerDeviceId: shortId2(connection.channel.peerDeviceId),
14416
14768
  messageType: message.type,
14417
- reason: diagnosticReason(error)
14769
+ reason: diagnosticReason2(error)
14418
14770
  });
14419
14771
  await this.disconnect(connection);
14420
14772
  }
@@ -14444,7 +14796,7 @@ var ConnectionRejectedError = class extends Error {
14444
14796
  this.code = code;
14445
14797
  }
14446
14798
  };
14447
- function diagnosticReason(error) {
14799
+ function diagnosticReason2(error) {
14448
14800
  const message = error instanceof Error ? error.message : String(error);
14449
14801
  return message.replace(/[\r\n]+/g, " ").slice(0, 160) || "Unknown peer connection failure.";
14450
14802
  }
@@ -14458,14 +14810,16 @@ var apiMethods = /* @__PURE__ */ new Set([
14458
14810
  "harness.api.call",
14459
14811
  "harness.api.respond",
14460
14812
  "harness.api.stream.open",
14461
- "harness.api.stream.close"
14813
+ "harness.api.stream.close",
14814
+ "fileviewer.call"
14462
14815
  ]);
14463
- var HOST_CAPABILITIES = ["harness.api.v1"];
14816
+ var HOST_CAPABILITIES = ["harness.api.v1", "fileviewer.read.v1"];
14464
14817
  var RpcRouter = class {
14465
- constructor(harnessApi, maxPending = 128, logger) {
14818
+ constructor(harnessApi, maxPending = 128, logger, fileViewer) {
14466
14819
  this.harnessApi = harnessApi;
14467
14820
  this.maxPending = maxPending;
14468
14821
  this.logger = logger;
14822
+ this.fileViewer = fileViewer;
14469
14823
  }
14470
14824
  active = 0;
14471
14825
  closePeerStreams() {
@@ -14500,7 +14854,7 @@ var RpcRouter = class {
14500
14854
  durationMs: Math.round(performance.now() - startedAt),
14501
14855
  code: response.payload.code,
14502
14856
  retryable: response.payload.retryable,
14503
- reason: diagnosticReason2(error)
14857
+ reason: diagnosticReason3(error)
14504
14858
  });
14505
14859
  return response;
14506
14860
  } finally {
@@ -14517,6 +14871,12 @@ var RpcRouter = class {
14517
14871
  return this.harnessApi.openStream(params);
14518
14872
  case "harness.api.stream.close":
14519
14873
  return this.harnessApi.closeStream(params);
14874
+ case "fileviewer.call": {
14875
+ if (this.fileViewer === void 0) {
14876
+ throw new RpcError("FILE_VIEWER_UNAVAILABLE", "The Remote Host does not have DSH File Viewer available.");
14877
+ }
14878
+ return this.fileViewer.call(params);
14879
+ }
14520
14880
  default:
14521
14881
  throw new RpcError("METHOD_NOT_FOUND", "The requested method does not exist.");
14522
14882
  }
@@ -14535,15 +14895,135 @@ function errorResponse(requestId, error) {
14535
14895
  if (error instanceof external_exports.ZodError) return createRpcError(requestId, "INVALID_MESSAGE", "The RPC parameters are invalid.");
14536
14896
  return createRpcError(requestId, "INTERNAL_ERROR", "The Host could not complete the request.");
14537
14897
  }
14538
- function diagnosticReason2(error) {
14898
+ function diagnosticReason3(error) {
14539
14899
  const message = error instanceof Error ? error.message : String(error);
14540
14900
  return message.replace(/[\r\n]+/g, " ").slice(0, 160) || "Unknown Host request failure.";
14541
14901
  }
14542
14902
 
14903
+ // src/file-viewer-contract.ts
14904
+ var REMOTE_FILE_CHUNK_BYTES = 512 * 1024;
14905
+ var MAX_REMOTE_FILE_LOCATOR_CHARS = 4096;
14906
+ var MAX_REMOTE_DIRECTORY_ENTRIES = 1e3;
14907
+
14908
+ // src/file-viewer-bridge.ts
14909
+ var locatorSchema = external_exports.string().min(1).max(MAX_REMOTE_FILE_LOCATOR_CHARS);
14910
+ var callSchema = external_exports.discriminatedUnion("endpoint", [
14911
+ external_exports.object({
14912
+ endpoint: external_exports.literal("stat"),
14913
+ payload: external_exports.object({ path: locatorSchema }).strict()
14914
+ }).strict(),
14915
+ external_exports.object({
14916
+ endpoint: external_exports.literal("readRange"),
14917
+ payload: external_exports.object({
14918
+ path: locatorSchema,
14919
+ offset: external_exports.number().int().nonnegative().safe(),
14920
+ length: external_exports.number().int().positive().max(REMOTE_FILE_CHUNK_BYTES)
14921
+ }).strict()
14922
+ }).strict(),
14923
+ external_exports.object({
14924
+ endpoint: external_exports.literal("list"),
14925
+ payload: external_exports.object({ path: locatorSchema }).strict()
14926
+ }).strict()
14927
+ ]);
14928
+ var statResultSchema = external_exports.object({
14929
+ path: locatorSchema,
14930
+ name: external_exports.string().min(1).max(1024),
14931
+ ext: external_exports.string().max(128),
14932
+ mime: external_exports.string().min(1).max(256),
14933
+ size: external_exports.number().int().nonnegative().safe(),
14934
+ mtimeMs: external_exports.number().nonnegative().optional(),
14935
+ isDirectory: external_exports.boolean(),
14936
+ exists: external_exports.boolean()
14937
+ }).strict();
14938
+ var rangeResultSchema = external_exports.object({
14939
+ data: external_exports.string().max(Math.ceil(REMOTE_FILE_CHUNK_BYTES / 3) * 4 + 4),
14940
+ offset: external_exports.number().int().nonnegative().safe(),
14941
+ size: external_exports.number().int().nonnegative().safe(),
14942
+ eof: external_exports.boolean()
14943
+ }).strict();
14944
+ var directoryEntrySchema = external_exports.object({
14945
+ name: external_exports.string().min(1).max(1024),
14946
+ path: locatorSchema,
14947
+ isDirectory: external_exports.boolean(),
14948
+ size: external_exports.number().int().nonnegative().safe().optional(),
14949
+ mtimeMs: external_exports.number().nonnegative().optional()
14950
+ }).strict();
14951
+ var listResultSchema = external_exports.object({
14952
+ path: locatorSchema,
14953
+ entries: external_exports.array(directoryEntrySchema).max(MAX_REMOTE_DIRECTORY_ENTRIES)
14954
+ }).strict();
14955
+ var RemoteFileViewerBridge = class {
14956
+ constructor(service, logger) {
14957
+ this.service = service;
14958
+ this.logger = logger;
14959
+ }
14960
+ async call(input) {
14961
+ const params = callSchema.parse(input);
14962
+ const service = this.service();
14963
+ if (service === void 0) {
14964
+ throw new RpcError("FILE_VIEWER_UNAVAILABLE", "The Remote Host does not have DSH File Viewer available.");
14965
+ }
14966
+ const startedAt = performance.now();
14967
+ const signal = AbortSignal.timeout(3e4);
14968
+ let raw;
14969
+ try {
14970
+ raw = await service.handle(params.endpoint, params.payload, signal);
14971
+ } catch {
14972
+ this.logger?.warn("remote file viewer service failed", { endpoint: params.endpoint });
14973
+ throw new RpcError("FILE_VIEWER_ERROR", "The Remote File Viewer could not complete the request.");
14974
+ }
14975
+ const result = parseHostResult(raw);
14976
+ if (!result.ok) throw hostFailure(result);
14977
+ try {
14978
+ const value = params.endpoint === "stat" ? statResultSchema.parse(result.value) : params.endpoint === "readRange" ? rangeResultSchema.parse(result.value) : listResultSchema.parse(result.value);
14979
+ this.logger?.debug("remote file viewer call ok", {
14980
+ endpoint: params.endpoint,
14981
+ durationMs: Math.round(performance.now() - startedAt)
14982
+ });
14983
+ return value;
14984
+ } catch (error) {
14985
+ if (error instanceof external_exports.ZodError && params.endpoint === "list" && isOversizedListing(result.value)) {
14986
+ throw new RpcError(
14987
+ "RESPONSE_TOO_LARGE",
14988
+ `The remote directory contains more than ${MAX_REMOTE_DIRECTORY_ENTRIES} entries.`,
14989
+ { maxEntries: MAX_REMOTE_DIRECTORY_ENTRIES },
14990
+ true
14991
+ );
14992
+ }
14993
+ throw error;
14994
+ }
14995
+ }
14996
+ };
14997
+ function parseHostResult(input) {
14998
+ if (typeof input !== "object" || input === null || !("ok" in input) || typeof input.ok !== "boolean") {
14999
+ throw new RpcError("FILE_VIEWER_INVALID_RESPONSE", "The Remote Host File Viewer returned an invalid response.");
15000
+ }
15001
+ return input;
15002
+ }
15003
+ function hostFailure(result) {
15004
+ const message = result.error?.message ?? "";
15005
+ if (/access denied/i.test(message)) {
15006
+ return new RpcError("ACCESS_DENIED", "The Remote File Viewer denied access to this locator.");
15007
+ }
15008
+ if (/does not exist|not exist|not found/i.test(message)) {
15009
+ return new RpcError("NOT_FOUND", "The requested remote file does not exist.");
15010
+ }
15011
+ if (result.error?.code === "cancelled") {
15012
+ return new RpcError("CANCELLED", "The Remote File Viewer request was cancelled.", void 0, true);
15013
+ }
15014
+ if (result.error?.code === "bad-request") {
15015
+ return new RpcError("INVALID_MESSAGE", "The Remote File Viewer request is invalid.");
15016
+ }
15017
+ return new RpcError("FILE_VIEWER_ERROR", "The Remote File Viewer could not complete the request.");
15018
+ }
15019
+ function isOversizedListing(value) {
15020
+ return typeof value === "object" && value !== null && "entries" in value && Array.isArray(value.entries) && value.entries.length > MAX_REMOTE_DIRECTORY_ENTRIES;
15021
+ }
15022
+
14543
15023
  // src/server-connection.ts
14544
15024
  var DEFAULT_WEBRTC_NEGOTIATE_TIMEOUT_MS = 8e3;
14545
15025
  var HostServerConnection = class {
14546
- constructor(config, identity, identities, api, connections, logger, createWebSocket = (url) => new WebSocket(url), rtcFactoryProvider) {
15026
+ constructor(config, identity, identities, api, connections, logger, createWebSocket = (url) => new WebSocket(url), rtcFactoryProvider, hostCapabilities = () => ["harness.api.v1"]) {
14547
15027
  this.config = config;
14548
15028
  this.identity = identity;
14549
15029
  this.identities = identities;
@@ -14552,6 +15032,7 @@ var HostServerConnection = class {
14552
15032
  this.logger = logger;
14553
15033
  this.createWebSocket = createWebSocket;
14554
15034
  this.rtcFactoryProvider = rtcFactoryProvider;
15035
+ this.hostCapabilities = hostCapabilities;
14555
15036
  }
14556
15037
  socket;
14557
15038
  running;
@@ -14675,7 +15156,7 @@ var HostServerConnection = class {
14675
15156
  accessToken: credentials.accessToken,
14676
15157
  protocols: [PROTOCOL_VERSION],
14677
15158
  clientVersion: PLUGIN_VERSION,
14678
- capabilities: this.rtcFactoryProvider === void 0 || this.config.forceRelay ? ["transport.relay", "harness.api.v1"] : ["transport.p2p", "transport.turn", "transport.relay", "harness.api.v1"]
15159
+ capabilities: this.rtcFactoryProvider === void 0 || this.config.forceRelay ? ["transport.relay", ...this.hostCapabilities()] : ["transport.p2p", "transport.turn", "transport.relay", ...this.hostCapabilities()]
14679
15160
  });
14680
15161
  };
14681
15162
  socket.onmessage = (event) => {
@@ -14701,7 +15182,7 @@ var HostServerConnection = class {
14701
15182
  this.terminalError = code;
14702
15183
  this.logger.error("server control frame failed", {
14703
15184
  code,
14704
- reason: diagnosticReason3(error)
15185
+ reason: diagnosticReason4(error)
14705
15186
  });
14706
15187
  socket.close(4008, "invalid control frame");
14707
15188
  });
@@ -14850,7 +15331,7 @@ var HostServerConnection = class {
14850
15331
  if (!tunnel.noise.complete) throw new ControlConnectionError("SECURE_CHANNEL_FAILED", "Noise IK handshake did not complete.");
14851
15332
  const viaWebRtc = tunnel.rtc !== void 0 && (tunnel.transport === "p2p" || tunnel.transport === "turn");
14852
15333
  if (!viaWebRtc && tunnel.transport === "negotiating") tunnel.transport = "relay";
14853
- const mode = viaWebRtc ? tunnel.transport === "turn" ? "TURN" : "P2P" : "Relay";
15334
+ const mode = viaWebRtc ? tunnel.transportMode ?? (tunnel.transport === "turn" ? "TURN" : "P2P") : "Relay";
14854
15335
  const transmit = viaWebRtc ? (ciphertext) => tunnel.rtc.send(ciphertext) : (ciphertext) => this.sendRelay(tunnel, ciphertext);
14855
15336
  const channel = new ServerNoiseChannel(tunnel, transmit, () => {
14856
15337
  if (this.tunnels.get(tunnel.connectionId) === tunnel) this.tunnels.delete(tunnel.connectionId);
@@ -14960,19 +15441,20 @@ var HostServerConnection = class {
14960
15441
  handleRtcOpened(tunnel, selected) {
14961
15442
  if (this.tunnels.get(tunnel.connectionId) !== tunnel || tunnel.rtc === void 0) return;
14962
15443
  tunnel.transport = selected;
15444
+ tunnel.transportMode = tunnel.rtc.selectedPathMode();
14963
15445
  this.sendTransportSelected(tunnel, selected);
14964
15446
  this.logger.info("webrtc data channel ready", {
14965
15447
  connectionId: shortId3(tunnel.connectionId),
14966
15448
  peerDeviceId: shortId3(tunnel.peer.deviceId),
14967
- transport: selected
15449
+ transport: tunnel.transportMode ?? selected
14968
15450
  });
14969
15451
  }
14970
15452
  async handleRtcFailed(tunnel, rtc, error) {
14971
15453
  if (this.tunnels.get(tunnel.connectionId) !== tunnel || tunnel.rtc !== rtc) return;
14972
- if (tunnel.channel !== void 0 || tunnel.transport === "p2p" || tunnel.transport === "turn") {
15454
+ if (tunnel.transport === "p2p" || tunnel.transport === "turn") {
14973
15455
  this.logger.warn("webrtc data channel failed; disconnecting peer", {
14974
15456
  connectionId: shortId3(tunnel.connectionId),
14975
- reason: diagnosticReason3(error)
15457
+ reason: diagnosticReason4(error)
14976
15458
  });
14977
15459
  await this.dropTunnel(tunnel.connectionId, "CONNECTION_FAILED");
14978
15460
  return;
@@ -14982,7 +15464,7 @@ var HostServerConnection = class {
14982
15464
  await rtc.close();
14983
15465
  this.logger.warn("webrtc negotiation failed; falling back to relay", {
14984
15466
  connectionId: shortId3(tunnel.connectionId),
14985
- reason: diagnosticReason3(error)
15467
+ reason: diagnosticReason4(error)
14986
15468
  });
14987
15469
  }
14988
15470
  sendTransportSelected(tunnel, transport) {
@@ -15026,7 +15508,7 @@ var HostServerConnection = class {
15026
15508
  } catch (error) {
15027
15509
  this.logger.warn("remote connection RTC cleanup failed", {
15028
15510
  connectionId: shortId3(connectionId),
15029
- reason: diagnosticReason3(error)
15511
+ reason: diagnosticReason4(error)
15030
15512
  });
15031
15513
  }
15032
15514
  if (tunnel.channel !== void 0) {
@@ -15037,7 +15519,7 @@ var HostServerConnection = class {
15037
15519
  await tunnel.channel.close(code).catch(() => void 0);
15038
15520
  this.logger.warn("remote connection channel cleanup failed", {
15039
15521
  connectionId: shortId3(connectionId),
15040
- reason: diagnosticReason3(error)
15522
+ reason: diagnosticReason4(error)
15041
15523
  });
15042
15524
  }
15043
15525
  } else {
@@ -15205,7 +15687,7 @@ function shortId3(value) {
15205
15687
  function asError2(error) {
15206
15688
  return error instanceof Error ? error : new Error("Unknown Server connection error.");
15207
15689
  }
15208
- function diagnosticReason3(error) {
15690
+ function diagnosticReason4(error) {
15209
15691
  const message = asError2(error).message.replace(/[\r\n]+/g, " ").slice(0, 160);
15210
15692
  return message || "Unknown Server connection error.";
15211
15693
  }
@@ -15269,7 +15751,7 @@ function crumbs(path) {
15269
15751
  }
15270
15752
 
15271
15753
  // src/harness-api-bridge.ts
15272
- var callSchema = external_exports.object({
15754
+ var callSchema2 = external_exports.object({
15273
15755
  method: external_exports.string().min(1).max(80),
15274
15756
  rpcId: external_exports.string().min(1).max(128),
15275
15757
  payload: external_exports.unknown()
@@ -15361,7 +15843,7 @@ var HarnessApiBridge = class {
15361
15843
  host;
15362
15844
  answer;
15363
15845
  async call(input) {
15364
- const params = callSchema.parse(input);
15846
+ const params = callSchema2.parse(input);
15365
15847
  const method = this.methods.get(params.method);
15366
15848
  if (method === void 0) throw deniedMethod(params.method);
15367
15849
  const startedAt = performance.now();
@@ -15395,7 +15877,7 @@ var HarnessApiBridge = class {
15395
15877
  method: params.method,
15396
15878
  durationMs,
15397
15879
  timedOut: signal.aborted,
15398
- reason: diagnosticReason4(error)
15880
+ reason: diagnosticReason5(error)
15399
15881
  });
15400
15882
  throw error;
15401
15883
  }
@@ -15539,7 +16021,7 @@ function domainProperty(wireDomain) {
15539
16021
  function deniedMethod(method) {
15540
16022
  return new RpcError("METHOD_NOT_ALLOWED", `Harness API method ${JSON.stringify(method)} is not available in remote mode.`);
15541
16023
  }
15542
- function diagnosticReason4(error) {
16024
+ function diagnosticReason5(error) {
15543
16025
  const message = error instanceof Error ? error.message : String(error);
15544
16026
  return message.replace(/[\r\n]+/g, " ").slice(0, 160) || "Unknown Harness API failure.";
15545
16027
  }
@@ -15618,205 +16100,13 @@ function shortId4(value) {
15618
16100
  return value.length <= 12 ? value : `${value.slice(0, 8)}\u2026${value.slice(-4)}`;
15619
16101
  }
15620
16102
 
15621
- // src/werift-rtc.ts
15622
- import { networkInterfaces } from "node:os";
15623
- var cachedFactory;
15624
- async function loadWeriftFactory() {
15625
- if (cachedFactory !== void 0) return cachedFactory;
15626
- try {
15627
- const werift = await import("werift");
15628
- cachedFactory = buildWeriftFactory(werift);
15629
- return cachedFactory;
15630
- } catch {
15631
- return void 0;
15632
- }
15633
- }
15634
- function buildWeriftFactory(werift) {
15635
- return {
15636
- create(configuration) {
15637
- const hostIpv4 = detectHostIpv4();
15638
- const raw = new werift.RTCPeerConnection({
15639
- iceServers: configuration.iceServers ?? [],
15640
- iceUseIpv6: false,
15641
- iceUseLinkLocalAddress: false,
15642
- ...hostIpv4 === void 0 ? {} : { iceInterfaceAddresses: { udp4: hostIpv4 } }
15643
- });
15644
- let onIceCandidate = null;
15645
- let onDataChannel = null;
15646
- raw.onicecandidate = (event) => {
15647
- if (onIceCandidate === null) return;
15648
- onIceCandidate({ candidate: event.candidate === void 0 ? null : event.candidate.toJSON() });
15649
- };
15650
- raw.ondatachannel = (event) => {
15651
- if (onDataChannel === null) return;
15652
- onDataChannel({ channel: adaptDataChannel2(event.channel) });
15653
- };
15654
- const pc = {
15655
- get connectionState() {
15656
- return raw.connectionState;
15657
- },
15658
- get iceConnectionState() {
15659
- return raw.iceConnectionState;
15660
- },
15661
- get iceGatheringState() {
15662
- return raw.iceGatheringState;
15663
- },
15664
- get signalingState() {
15665
- return raw.signalingState;
15666
- },
15667
- set onconnectionstatechange(value) {
15668
- raw.onconnectionstatechange = value;
15669
- },
15670
- get onconnectionstatechange() {
15671
- return raw.onconnectionstatechange;
15672
- },
15673
- set oniceconnectionstatechange(value) {
15674
- raw.oniceconnectionstatechange = value;
15675
- },
15676
- get oniceconnectionstatechange() {
15677
- return raw.oniceconnectionstatechange;
15678
- },
15679
- set onicegatheringstatechange(value) {
15680
- raw.onicegatheringstatechange = value;
15681
- },
15682
- get onicegatheringstatechange() {
15683
- return raw.onicegatheringstatechange;
15684
- },
15685
- set onicecandidate(value) {
15686
- onIceCandidate = value;
15687
- },
15688
- get onicecandidate() {
15689
- return onIceCandidate;
15690
- },
15691
- set ondatachannel(value) {
15692
- onDataChannel = value;
15693
- },
15694
- get ondatachannel() {
15695
- return onDataChannel;
15696
- },
15697
- createDataChannel(label, options) {
15698
- return adaptDataChannel2(raw.createDataChannel(label, { ordered: options?.ordered ?? true }));
15699
- },
15700
- createOffer() {
15701
- return raw.createOffer();
15702
- },
15703
- createAnswer() {
15704
- return raw.createAnswer();
15705
- },
15706
- setLocalDescription(description) {
15707
- return raw.setLocalDescription(description).then(() => void 0);
15708
- },
15709
- setRemoteDescription(description) {
15710
- return raw.setRemoteDescription(description);
15711
- },
15712
- addIceCandidate(candidate) {
15713
- return raw.addIceCandidate(candidate);
15714
- },
15715
- getStats() {
15716
- return raw.getStats();
15717
- },
15718
- close() {
15719
- void raw.close().catch(() => void 0);
15720
- }
15721
- };
15722
- return pc;
15723
- }
15724
- };
15725
- }
15726
- function adaptDataChannel2(raw) {
15727
- return {
15728
- get label() {
15729
- return raw.label;
15730
- },
15731
- get ordered() {
15732
- return raw.ordered;
15733
- },
15734
- get readyState() {
15735
- return raw.readyState;
15736
- },
15737
- get bufferedAmount() {
15738
- return raw.bufferedAmount;
15739
- },
15740
- binaryType: "arraybuffer",
15741
- set onopen(value) {
15742
- raw.onopen = value;
15743
- },
15744
- get onopen() {
15745
- return raw.onopen;
15746
- },
15747
- set onmessage(value) {
15748
- raw.onmessage = value === null ? null : (event) => value({ data: toArrayBuffer2(event.data) });
15749
- },
15750
- get onmessage() {
15751
- return null;
15752
- },
15753
- set onclose(value) {
15754
- raw.onclose = value;
15755
- },
15756
- get onclose() {
15757
- return raw.onclose;
15758
- },
15759
- set onerror(value) {
15760
- raw.onerror = value;
15761
- },
15762
- get onerror() {
15763
- return raw.onerror;
15764
- },
15765
- onbufferedamountlow: null,
15766
- send(data) {
15767
- const bytes = typeof data === "string" ? Buffer.byteLength(data) : data.byteLength;
15768
- try {
15769
- raw.send(typeof data === "string" ? data : Buffer.from(data));
15770
- } catch (error) {
15771
- console.error("[werift-send-error] bytes=" + bytes, error instanceof Error ? error.message : error);
15772
- throw error;
15773
- }
15774
- },
15775
- close() {
15776
- raw.close();
15777
- }
15778
- };
15779
- }
15780
- function toArrayBuffer2(data) {
15781
- if (typeof data === "string") return data;
15782
- return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
15783
- }
15784
- function detectHostIpv4() {
15785
- const preferred = [];
15786
- const fallback = [];
15787
- for (const [name2, addresses] of Object.entries(networkInterfaces())) {
15788
- if (isVirtualInterface(name2)) continue;
15789
- for (const address of addresses ?? []) {
15790
- if (address.internal || address.family !== "IPv4") continue;
15791
- const ip = address.address;
15792
- if (ip.startsWith("127.") || ip.startsWith("169.254.") || isCgnat(ip)) continue;
15793
- if (isPrivate(ip)) preferred.push(ip);
15794
- else fallback.push(ip);
15795
- }
15796
- }
15797
- return preferred[0] ?? fallback[0];
15798
- }
15799
- function isVirtualInterface(name2) {
15800
- return /^(utun|ppp|bridge|awdl|llw|gif|stf|anpi|ap\d|en[1-9]\d*$)/i.test(name2);
15801
- }
15802
- function isCgnat(ip) {
15803
- const parts = ip.split(".").map(Number);
15804
- return parts.length === 4 && parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127;
15805
- }
15806
- function isPrivate(ip) {
15807
- const parts = ip.split(".").map(Number);
15808
- if (parts.length !== 4) return false;
15809
- if (parts[0] === 10) return true;
15810
- if (parts[0] === 192 && parts[1] === 168) return true;
15811
- return parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31;
15812
- }
15813
-
15814
16103
  // src/service.ts
15815
16104
  var HostPluginRuntime = class {
15816
- constructor(config, identities, apiProxy, logger, typertGateway) {
16105
+ constructor(config, identities, apiProxy, logger, typertGateway, fileViewerHost) {
15817
16106
  this.config = config;
15818
16107
  this.identities = identities;
15819
16108
  this.logger = logger;
16109
+ this.fileViewerHost = fileViewerHost;
15820
16110
  this.connections = new ConnectionController(this.identities, (_context, send) => {
15821
16111
  const harnessApi = new HarnessApiBridge(
15822
16112
  apiProxy,
@@ -15825,7 +16115,11 @@ var HostPluginRuntime = class {
15825
16115
  this.logger,
15826
16116
  typertGateway?.()
15827
16117
  );
15828
- return new RpcRouter(harnessApi, void 0, this.logger);
16118
+ const fileViewer = new RemoteFileViewerBridge(
16119
+ () => this.fileViewerHost?.(),
16120
+ this.logger
16121
+ );
16122
+ return new RpcRouter(harnessApi, void 0, this.logger, fileViewer);
15829
16123
  }, this.logger);
15830
16124
  if (config.serverUrl !== void 0) {
15831
16125
  this.serverApi = new HostServerApi(config.serverUrl, new ServerCredentialStore(identities.directory));
@@ -15962,7 +16256,8 @@ var HostPluginRuntime = class {
15962
16256
  this.connections,
15963
16257
  this.logger,
15964
16258
  void 0,
15965
- this.config.forceRelay ? void 0 : loadWeriftFactory
16259
+ this.config.forceRelay ? void 0 : loadWeriftFactory,
16260
+ () => this.fileViewerHost?.() === void 0 ? ["harness.api.v1"] : ["harness.api.v1", "fileviewer.read.v1"]
15966
16261
  );
15967
16262
  }
15968
16263
  };
@@ -15970,6 +16265,67 @@ function shortId5(value) {
15970
16265
  return value.length <= 12 ? value : `${value.slice(0, 8)}\u2026${value.slice(-4)}`;
15971
16266
  }
15972
16267
 
16268
+ // src/remote-file-content-provider.ts
16269
+ function createRemoteFileContentProvider(call) {
16270
+ return {
16271
+ id: "dsh-remote-files",
16272
+ priority: 1e4,
16273
+ supports: () => true,
16274
+ async stat(locator, signal) {
16275
+ const value = await call("fileviewer.stat", { path: locator }, signal);
16276
+ if (!value.exists) return void 0;
16277
+ return {
16278
+ name: value.name,
16279
+ size: value.isDirectory ? 0 : value.size,
16280
+ mime: value.mime,
16281
+ mtimeMs: value.mtimeMs,
16282
+ isDirectory: value.isDirectory
16283
+ };
16284
+ },
16285
+ async read(locator, request) {
16286
+ if (!Number.isInteger(request.offset) || request.offset < 0) throw new Error("A non-negative integer offset is required.");
16287
+ if (!Number.isInteger(request.length) || request.length <= 0) throw new Error("A positive integer length is required.");
16288
+ const chunks = [];
16289
+ let received = 0;
16290
+ while (received < request.length) {
16291
+ request.signal.throwIfAborted();
16292
+ const length = Math.min(REMOTE_FILE_CHUNK_BYTES, request.length - received);
16293
+ const offset = request.offset + received;
16294
+ const range = await call("fileviewer.readRange", { path: locator, offset, length }, request.signal);
16295
+ if (range.offset !== offset) throw new Error("The Remote Host returned a mismatched file range.");
16296
+ const bytes = decodeBase64(range.data);
16297
+ if (bytes.byteLength > length) throw new Error("The Remote Host returned more file bytes than requested.");
16298
+ chunks.push(bytes);
16299
+ received += bytes.byteLength;
16300
+ if (range.eof || bytes.byteLength === 0) break;
16301
+ }
16302
+ const merged = new Uint8Array(received);
16303
+ let cursor = 0;
16304
+ for (const chunk of chunks) {
16305
+ merged.set(chunk, cursor);
16306
+ cursor += chunk.byteLength;
16307
+ }
16308
+ return merged;
16309
+ },
16310
+ async list(locator, signal) {
16311
+ const value = await call("fileviewer.list", { path: locator }, signal);
16312
+ return value.entries.map((entry) => ({
16313
+ locator: entry.path,
16314
+ name: entry.name,
16315
+ size: entry.isDirectory ? 0 : entry.size ?? 0,
16316
+ mtimeMs: entry.mtimeMs,
16317
+ isDirectory: entry.isDirectory
16318
+ }));
16319
+ }
16320
+ };
16321
+ }
16322
+ function decodeBase64(value) {
16323
+ const binary = atob(value);
16324
+ const bytes = new Uint8Array(binary.length);
16325
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
16326
+ return bytes;
16327
+ }
16328
+
15973
16329
  // src/index.ts
15974
16330
  var name = "dsh-remote";
15975
16331
  function apply(ctx, input = {}) {
@@ -16012,7 +16368,14 @@ async function activate(ctx, input) {
16012
16368
  const connection = ctx.get("connection");
16013
16369
  const nativeTypertGateway = ctx.get("typertGateway");
16014
16370
  const localTypertGateway = new TypertGatewaySwitch(nativeTypertGateway).local();
16015
- const runtime = new HostPluginRuntime(config, hostIdentities, apiProxy, logger, () => localTypertGateway);
16371
+ const runtime = new HostPluginRuntime(
16372
+ config,
16373
+ hostIdentities,
16374
+ apiProxy,
16375
+ logger,
16376
+ () => localTypertGateway,
16377
+ () => ctx.get("fileViewerHost")
16378
+ );
16016
16379
  let clientRuntime;
16017
16380
  const hostControl = runtime;
16018
16381
  if (config.serverUrl !== void 0 && apiProxy !== void 0 && connection !== void 0) {
@@ -16076,6 +16439,7 @@ export {
16076
16439
  IdentityInvalidError,
16077
16440
  IdentityStore,
16078
16441
  PluginControlRuntime,
16442
+ RemoteFileViewerBridge,
16079
16443
  RemoteHarnessApiProxy,
16080
16444
  RpcError,
16081
16445
  RpcRouter,
@@ -16084,6 +16448,7 @@ export {
16084
16448
  ServerCredentialsInvalidError,
16085
16449
  TypertGatewaySwitch,
16086
16450
  apply,
16451
+ createRemoteFileContentProvider,
16087
16452
  fingerprint,
16088
16453
  name,
16089
16454
  resolveConfig,