ds-harness-remote 0.4.0 → 0.4.1

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.
package/dist/index.js CHANGED
@@ -4111,9 +4111,22 @@ function normalizeSdpMLineIndex(value) {
4111
4111
  return null;
4112
4112
  return value;
4113
4113
  }
4114
+ function selectCapabilities(offered, supported) {
4115
+ const offeredCapabilities = new Set(offered);
4116
+ return [...new Set(supported)].filter((capability) => offeredCapabilities.has(capability));
4117
+ }
4118
+ function acceptNegotiatedCapabilities(offered, negotiated) {
4119
+ const accepted = negotiated ?? ["transport.relay"];
4120
+ if (selectCapabilities(accepted, offered).length !== accepted.length) {
4121
+ throw new Error("Server selected a capability that the peer did not offer.");
4122
+ }
4123
+ return [...accepted];
4124
+ }
4114
4125
  var rpcMethodSchema = external_exports.enum(rpcMethods);
4115
4126
  var messageTypeSchema = external_exports.enum(messageTypes);
4116
4127
  var controlFrameTypeSchema = external_exports.enum(controlFrameTypes);
4128
+ var uniqueStrings = (values) => new Set(values).size === values.length;
4129
+ var uniqueNumbers = (values) => new Set(values).size === values.length;
4117
4130
  var remoteMessageSchema = external_exports.object({
4118
4131
  v: external_exports.literal(PROTOCOL_VERSION),
4119
4132
  id: external_exports.string().min(1),
@@ -4134,8 +4147,8 @@ var helloPayloadSchema = external_exports.object({
4134
4147
  role: external_exports.enum(["host", "client"]),
4135
4148
  deviceId: external_exports.string().min(1),
4136
4149
  accessToken: external_exports.string().min(1),
4137
- protocols: external_exports.array(external_exports.number().int()).min(1),
4138
- capabilities: external_exports.array(external_exports.string()),
4150
+ protocols: external_exports.array(external_exports.number().int().nonnegative().safe()).min(1).refine(uniqueNumbers),
4151
+ capabilities: external_exports.array(external_exports.string().min(1)).refine(uniqueStrings),
4139
4152
  clientVersion: external_exports.string().optional(),
4140
4153
  harnessVersion: external_exports.string().optional()
4141
4154
  });
@@ -4146,6 +4159,7 @@ var helloAckPayloadSchema = external_exports.object({
4146
4159
  heartbeatIntervalMs: external_exports.number().int().positive(),
4147
4160
  maxControlFrameBytes: external_exports.number().int().positive(),
4148
4161
  maxRelayFrameBytes: external_exports.number().int().positive(),
4162
+ capabilities: external_exports.array(external_exports.string().min(1)).refine(uniqueStrings).optional(),
4149
4163
  webrtcEnabled: external_exports.boolean().optional(),
4150
4164
  webrtcFallbackTimeoutMs: external_exports.number().int().positive().optional()
4151
4165
  });
@@ -4177,7 +4191,7 @@ var secureHandshakePayloadSchema = external_exports.object({
4177
4191
  var relayPayloadSchema = external_exports.object({
4178
4192
  connectionId: external_exports.string().min(1),
4179
4193
  targetDeviceId: external_exports.string().min(1),
4180
- counter: external_exports.number().int().nonnegative(),
4194
+ counter: external_exports.number().int().nonnegative().safe(),
4181
4195
  ciphertext: external_exports.string().min(1)
4182
4196
  });
4183
4197
  var signalPayloadSchema = external_exports.object({
@@ -4384,6 +4398,9 @@ function cryptoRandomId() {
4384
4398
  return `msg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
4385
4399
  }
4386
4400
 
4401
+ // ../client-core/dist/remote-gateway.js
4402
+ var DIRECT_REMOTE_CALL_BYTES = 2 * 1024 * 1024;
4403
+
4387
4404
  // ../client-core/dist/index.js
4388
4405
  var RemoteClientError = class extends Error {
4389
4406
  code;
@@ -4925,6 +4942,8 @@ function isChunk(frame) {
4925
4942
 
4926
4943
  // ../webrtc/dist/rtc-data-channel.js
4927
4944
  var DEFAULT_NEGOTIATE_TIMEOUT_MS = 8e3;
4945
+ var SELECTED_PATH_RETRY_COUNT = 5;
4946
+ var SELECTED_PATH_RETRY_DELAY_MS = 50;
4928
4947
  var RtcDataChannelTransport = class {
4929
4948
  pc;
4930
4949
  role;
@@ -5254,7 +5273,11 @@ var RtcDataChannelTransport = class {
5254
5273
  }
5255
5274
  async resolveSelectedTransport() {
5256
5275
  try {
5257
- const selected = await this.refreshStatsDiagnostics();
5276
+ let selected = await this.refreshStatsDiagnostics();
5277
+ for (let attempt = 0; attempt < SELECTED_PATH_RETRY_COUNT && !this.closed && (selected.transport === void 0 || selected.mode === void 0); attempt += 1) {
5278
+ await sleep(SELECTED_PATH_RETRY_DELAY_MS);
5279
+ selected = await this.refreshStatsDiagnostics();
5280
+ }
5258
5281
  this.selected = selected?.transport;
5259
5282
  this.selectedMode = selected?.mode;
5260
5283
  this.selectedPathTelemetry = selectedPathTelemetry(selected);
@@ -5418,6 +5441,8 @@ function inspectSelectedPath(stats) {
5418
5441
  const remote = candidates.get(String(pair.remoteCandidateId));
5419
5442
  const localType = local?.candidateType;
5420
5443
  const remoteType = remote?.candidateType;
5444
+ const localAddressScope = summarizeAddress(candidateAddressValue(local)).addressScope;
5445
+ const remoteAddressScope = summarizeAddress(candidateAddressValue(remote)).addressScope;
5421
5446
  let selected;
5422
5447
  if (localType === "relay" || remoteType === "relay")
5423
5448
  selected = { transport: "turn", mode: "TURN" };
@@ -5425,13 +5450,15 @@ function inspectSelectedPath(stats) {
5425
5450
  continue;
5426
5451
  selected ??= {
5427
5452
  transport: "p2p",
5428
- mode: localType === "host" && remoteType === "host" ? "LAN" : "P2P"
5453
+ mode: isLanCandidatePair(localType, remoteType, localAddressScope, remoteAddressScope) ? "LAN" : "P2P"
5429
5454
  };
5430
5455
  const currentRoundTripTime = numberStat(pair, "currentRoundTripTime");
5431
5456
  return {
5432
5457
  ...selected,
5433
5458
  localCandidateType: stringStat(local, "candidateType"),
5434
5459
  remoteCandidateType: stringStat(remote, "candidateType"),
5460
+ localAddressScope,
5461
+ remoteAddressScope,
5435
5462
  localAddress: candidateAddress(local),
5436
5463
  remoteAddress: candidateAddress(remote),
5437
5464
  protocol: stringStat(local, "protocol") ?? stringStat(remote, "protocol"),
@@ -5556,6 +5583,19 @@ function candidateTypeStat(candidate) {
5556
5583
  function candidateAddressValue(candidate) {
5557
5584
  return stringStat(candidate, "address") ?? stringStat(candidate, "ip");
5558
5585
  }
5586
+ function isLanCandidatePair(localType, remoteType, localScope, remoteScope) {
5587
+ if (localType === "host" && remoteType === "host")
5588
+ return true;
5589
+ const directTypes = /* @__PURE__ */ new Set(["host", "prflx"]);
5590
+ if (!directTypes.has(String(localType)) || !directTypes.has(String(remoteType)))
5591
+ return false;
5592
+ if (isLocalNetworkScope(localScope) && isLocalNetworkScope(remoteScope))
5593
+ return true;
5594
+ return localType === "host" && isLocalNetworkScope(localScope) && remoteType === "prflx" && remoteScope === "unknown" || remoteType === "host" && isLocalNetworkScope(remoteScope) && localType === "prflx" && localScope === "unknown";
5595
+ }
5596
+ function isLocalNetworkScope(scope) {
5597
+ return scope === "private" || scope === "link-local";
5598
+ }
5559
5599
  function increment(map, key) {
5560
5600
  map[key] = (map[key] ?? 0) + 1;
5561
5601
  }
@@ -5565,6 +5605,8 @@ function selectedPathTelemetry(details) {
5565
5605
  ...details.mode === void 0 ? {} : { mode: details.mode },
5566
5606
  ...details.localCandidateType === void 0 ? {} : { localCandidateType: details.localCandidateType },
5567
5607
  ...details.remoteCandidateType === void 0 ? {} : { remoteCandidateType: details.remoteCandidateType },
5608
+ ...details.localAddressScope === void 0 ? {} : { localAddressScope: details.localAddressScope },
5609
+ ...details.remoteAddressScope === void 0 ? {} : { remoteAddressScope: details.remoteAddressScope },
5568
5610
  ...details.protocol === void 0 ? {} : { protocol: details.protocol },
5569
5611
  ...details.relayProtocol === void 0 ? {} : { relayProtocol: details.relayProtocol },
5570
5612
  ...details.currentRoundTripTimeMs === void 0 ? {} : { currentRoundTripTimeMs: details.currentRoundTripTimeMs },
@@ -5609,6 +5651,7 @@ var AdaptiveTransport = class extends BaseTransport {
5609
5651
  handshakeTimer;
5610
5652
  webrtcEnabled = true;
5611
5653
  serverNegotiateTimeoutMs;
5654
+ negotiatedCapabilities = ["transport.relay"];
5612
5655
  connectedAt;
5613
5656
  lastRtcDiagnostics;
5614
5657
  constructor(url, options) {
@@ -5687,6 +5730,8 @@ var AdaptiveTransport = class extends BaseTransport {
5687
5730
  controlChannelUrl: this.url,
5688
5731
  controlChannelState: socketState(this.socket?.readyState),
5689
5732
  preferredTransports: this.options.forceRelay === true ? ["relay"] : [...this.options.preferredTransports ?? DEFAULT_PREFERRED_TRANSPORTS],
5733
+ negotiatedCapabilities: [...this.negotiatedCapabilities],
5734
+ webRtcEnabled: this.webrtcEnabled,
5690
5735
  ...this.rtc === void 0 ? {} : { webRtc: await this.rtc.connectionDetails() }
5691
5736
  };
5692
5737
  }
@@ -5733,6 +5778,12 @@ var AdaptiveTransport = class extends BaseTransport {
5733
5778
  const payload = frame.payload;
5734
5779
  if (payload.protocol !== PROTOCOL_VERSION)
5735
5780
  throw new Error("Server selected an unsupported protocol version");
5781
+ const offered = this.options.capabilities ?? DEFAULT_CAPABILITIES;
5782
+ this.negotiatedCapabilities = acceptNegotiatedCapabilities(offered, payload.capabilities);
5783
+ const preferredTransports = this.negotiatedTransports();
5784
+ if (preferredTransports.length === 0) {
5785
+ throw new Error("Server did not negotiate a supported transport capability");
5786
+ }
5736
5787
  if (payload.webrtcEnabled === false)
5737
5788
  this.webrtcEnabled = false;
5738
5789
  if (typeof payload.webrtcFallbackTimeoutMs === "number" && Number.isSafeInteger(payload.webrtcFallbackTimeoutMs) && payload.webrtcFallbackTimeoutMs > 0) {
@@ -5740,7 +5791,7 @@ var AdaptiveTransport = class extends BaseTransport {
5740
5791
  }
5741
5792
  this.sendControl("connect.request", {
5742
5793
  hostDeviceId: this.options.targetDeviceId,
5743
- preferredTransports: this.options.forceRelay === true ? ["relay"] : this.options.preferredTransports ?? [...DEFAULT_PREFERRED_TRANSPORTS]
5794
+ preferredTransports
5744
5795
  });
5745
5796
  return;
5746
5797
  }
@@ -5750,7 +5801,9 @@ var AdaptiveTransport = class extends BaseTransport {
5750
5801
  throw new Error("connect.accepted did not include a connectionId");
5751
5802
  }
5752
5803
  this.connectionId = payload.connectionId;
5753
- void this.negotiate();
5804
+ void this.negotiate().catch((error) => {
5805
+ this.failConnection(error instanceof Error ? error : new Error("Adaptive transport negotiation failed"));
5806
+ });
5754
5807
  return;
5755
5808
  }
5756
5809
  if (frame.type === "connect.rejected" || frame.type === "error") {
@@ -5807,15 +5860,25 @@ var AdaptiveTransport = class extends BaseTransport {
5807
5860
  async negotiate() {
5808
5861
  if (this.connectionId === void 0)
5809
5862
  return;
5810
- const preferred = this.options.preferredTransports ?? [...DEFAULT_PREFERRED_TRANSPORTS];
5863
+ const preferred = this.negotiatedTransports();
5811
5864
  const wantWebRtc = !this.options.forceRelay && this.webrtcEnabled && (preferred.includes("lan") || preferred.includes("p2p") || preferred.includes("turn"));
5812
5865
  let selected = "relay";
5813
5866
  this.lastRtcDiagnostics = void 0;
5814
5867
  if (wantWebRtc) {
5815
5868
  try {
5816
5869
  const rtcSelected = await this.tryWebRtc();
5817
- this.dataMode = "webrtc";
5818
- selected = rtcSelected;
5870
+ if (!preferred.includes(rtcSelected)) {
5871
+ await this.rtc?.close();
5872
+ this.rtc = void 0;
5873
+ if (!preferred.includes("relay")) {
5874
+ throw new Error(`WebRTC selected unnegotiated transport: ${rtcSelected}`);
5875
+ }
5876
+ this.dataMode = "relay";
5877
+ selected = "relay";
5878
+ } else {
5879
+ this.dataMode = "webrtc";
5880
+ selected = rtcSelected;
5881
+ }
5819
5882
  } catch (error) {
5820
5883
  const reason = error instanceof Error ? error : new Error("WebRTC negotiation failed.");
5821
5884
  try {
@@ -5824,10 +5887,14 @@ var AdaptiveTransport = class extends BaseTransport {
5824
5887
  }
5825
5888
  await this.rtc?.close();
5826
5889
  this.rtc = void 0;
5890
+ if (!preferred.includes("relay"))
5891
+ throw reason;
5827
5892
  this.dataMode = "relay";
5828
5893
  selected = "relay";
5829
5894
  }
5830
5895
  } else {
5896
+ if (!preferred.includes("relay"))
5897
+ throw new Error("No negotiated transport is available");
5831
5898
  this.dataMode = "relay";
5832
5899
  selected = "relay";
5833
5900
  }
@@ -5839,6 +5906,14 @@ var AdaptiveTransport = class extends BaseTransport {
5839
5906
  });
5840
5907
  this.finishConnection();
5841
5908
  }
5909
+ negotiatedTransports() {
5910
+ const preferred = this.options.forceRelay === true ? ["relay"] : this.options.preferredTransports ?? DEFAULT_PREFERRED_TRANSPORTS;
5911
+ return preferred.filter((transport) => {
5912
+ if (transport === "lan" || transport === "p2p")
5913
+ return this.negotiatedCapabilities.includes("transport.p2p");
5914
+ return this.negotiatedCapabilities.includes(`transport.${transport}`);
5915
+ });
5916
+ }
5842
5917
  async tryWebRtc() {
5843
5918
  const connectionId = this.connectionId;
5844
5919
  if (connectionId === void 0)
@@ -13470,8 +13545,8 @@ function base64ToBytes(value) {
13470
13545
  }
13471
13546
 
13472
13547
  // src/remote-typert-gateway.ts
13473
- var DIRECT_REMOTE_CALL_BYTES = 2 * 1024 * 1024;
13474
- var RemoteTypertGateway = class {
13548
+ var DIRECT_REMOTE_CALL_BYTES2 = 2 * 1024 * 1024;
13549
+ var RemoteTypertGateway2 = class {
13475
13550
  constructor(client) {
13476
13551
  this.client = client;
13477
13552
  }
@@ -13488,7 +13563,7 @@ var RemoteTypertGateway = class {
13488
13563
  const request = { endpoint, payload };
13489
13564
  const encoded = new TextEncoder().encode(JSON.stringify(request));
13490
13565
  let response;
13491
- if (encoded.byteLength > DIRECT_REMOTE_CALL_BYTES) {
13566
+ if (encoded.byteLength > DIRECT_REMOTE_CALL_BYTES2) {
13492
13567
  response = await this.callTransferred(encoded, signal);
13493
13568
  } else {
13494
13569
  try {
@@ -13770,7 +13845,7 @@ function normalizeServerUrl(value) {
13770
13845
  }
13771
13846
 
13772
13847
  // src/version.ts
13773
- var PLUGIN_VERSION = "0.4.0";
13848
+ var PLUGIN_VERSION = "0.4.1";
13774
13849
 
13775
13850
  // src/server-api.ts
13776
13851
  var HostServerApi = class {
@@ -14169,6 +14244,7 @@ function requireRecord(value, name2) {
14169
14244
 
14170
14245
  // src/typert-gateway-switch.ts
14171
14246
  var REMOTE_COMMAND_METHODS = ["execute", "list"];
14247
+ var LOCAL_ONLY_NAMESPACES = /* @__PURE__ */ new Set(["dynamicCordisRunner"]);
14172
14248
  var ALL_REMOTE_COMMANDS = { execute: true, list: true };
14173
14249
  var TypertGatewaySwitch = class {
14174
14250
  runtime;
@@ -14229,13 +14305,13 @@ var TypertGatewaySwitch = class {
14229
14305
  if (this.installed) return;
14230
14306
  this.runtime.invoke = (request) => this.selectInvoke(request);
14231
14307
  if (this.originalStream !== void 0) {
14232
- this.runtime.stream = (request) => this.remoteTarget === void 0 ? this.localStream(request) : this.remoteTarget.open(endpointOf(request), { args: request.args }, request.signal ?? new AbortController().signal);
14308
+ this.runtime.stream = (request) => this.remoteTarget === void 0 || isLocalOnlyEndpoint(endpointOf(request)) ? this.localStream(request) : this.remoteTarget.open(endpointOf(request), { args: request.args }, request.signal ?? new AbortController().signal);
14233
14309
  }
14234
14310
  if (this.originalDispatch !== void 0) {
14235
- this.runtime.dispatchRpc = (endpoint, payload, signal) => this.remoteTarget === void 0 ? this.localDispatch(endpoint, payload, signal) : this.remoteTarget.dispatch(endpoint, payload, signal);
14311
+ this.runtime.dispatchRpc = (endpoint, payload, signal) => this.remoteTarget === void 0 || isLocalOnlyEndpoint(endpoint) ? this.localDispatch(endpoint, payload, signal) : this.remoteTarget.dispatch(endpoint, payload, signal);
14236
14312
  }
14237
14313
  if (this.originalOpen !== void 0) {
14238
- this.runtime.openWireStream = (endpoint, payload, signal) => this.remoteTarget === void 0 ? this.localOpen(endpoint, payload, signal) : this.remoteTarget.open(endpoint, payload, signal);
14314
+ this.runtime.openWireStream = (endpoint, payload, signal) => this.remoteTarget === void 0 || isLocalOnlyEndpoint(endpoint) ? this.localOpen(endpoint, payload, signal) : this.remoteTarget.open(endpoint, payload, signal);
14239
14315
  }
14240
14316
  this.installed = true;
14241
14317
  }
@@ -14262,6 +14338,7 @@ var TypertGatewaySwitch = class {
14262
14338
  this.installed = false;
14263
14339
  }
14264
14340
  selectInvoke(request) {
14341
+ if (isLocalOnlyEndpoint(endpointOf(request))) return this.localInvoke(request);
14265
14342
  if (this.remoteTarget !== void 0) return this.remoteTarget.invoke(request);
14266
14343
  if (request.namespace !== "commands" || !isRemoteCommandMethod(request.method) || this.remoteInvoke === void 0) {
14267
14344
  return this.localInvoke(request);
@@ -14292,6 +14369,10 @@ function requestFromCarrier(endpoint, payload, signal) {
14292
14369
  function endpointOf(request) {
14293
14370
  return `${request.namespace}/${request.method}`;
14294
14371
  }
14372
+ function isLocalOnlyEndpoint(endpoint) {
14373
+ const separator = endpoint.indexOf("/");
14374
+ return separator > 0 && LOCAL_ONLY_NAMESPACES.has(endpoint.slice(0, separator));
14375
+ }
14295
14376
  function isRemoteCommandMethod(method) {
14296
14377
  return REMOTE_COMMAND_METHODS.includes(method);
14297
14378
  }
@@ -14305,7 +14386,7 @@ import { networkInterfaces } from "node:os";
14305
14386
 
14306
14387
  // src/native-rtc-helper.ts
14307
14388
  import { spawn, spawnSync } from "node:child_process";
14308
- import { existsSync, readdirSync, statSync } from "node:fs";
14389
+ import { existsSync, readdirSync, realpathSync, statSync } from "node:fs";
14309
14390
  import { delimiter, dirname, join } from "node:path";
14310
14391
  import { fileURLToPath } from "node:url";
14311
14392
  var cachedExternalFactory;
@@ -14321,17 +14402,25 @@ async function loadExternalNativeRtcFactory() {
14321
14402
  cachedExternalFactory = buildExternalNativeRtcFactory(nodeBinary);
14322
14403
  return cachedExternalFactory;
14323
14404
  }
14324
- function buildExternalNativeRtcFactory(nodeBinary, requireFrom = fileURLToPath(import.meta.url)) {
14405
+ function buildExternalNativeRtcFactory(nodeBinary, requireFrom = resolveNativeRtcRequireFrom()) {
14325
14406
  return {
14326
14407
  create(configuration) {
14327
14408
  return new ExternalNativePeerConnection(nodeBinary, requireFrom, configuration);
14328
14409
  }
14329
14410
  };
14330
14411
  }
14412
+ function resolveNativeRtcRequireFrom(moduleUrl = import.meta.url) {
14413
+ const modulePath = fileURLToPath(moduleUrl);
14414
+ try {
14415
+ return realpathSync(modulePath);
14416
+ } catch {
14417
+ return modulePath;
14418
+ }
14419
+ }
14331
14420
  function resolveExternalNodeBinaryForRtc() {
14332
14421
  if (cachedNodeBinaryResolved) return cachedNodeBinary;
14333
14422
  cachedNodeBinaryResolved = true;
14334
- const requireFrom = fileURLToPath(import.meta.url);
14423
+ const requireFrom = resolveNativeRtcRequireFrom();
14335
14424
  for (const candidate of nodeBinaryCandidates()) {
14336
14425
  if (isUsableExternalNode(candidate, requireFrom)) {
14337
14426
  cachedNodeBinary = candidate;
@@ -15520,7 +15609,7 @@ var ClientModeRuntime = class {
15520
15609
  async listRemoteDirectory(targetDeviceId, path, signal) {
15521
15610
  const remote = await this.ensureConnected(targetDeviceId, signal);
15522
15611
  if (remote.features.remoteGateway) {
15523
- const value = await new RemoteTypertGateway(remote.client).invoke({
15612
+ const value = await new RemoteTypertGateway2(remote.client).invoke({
15524
15613
  namespace: "directoryPicker",
15525
15614
  method: "list",
15526
15615
  args: path === void 0 ? {} : { path },
@@ -15538,7 +15627,7 @@ var ClientModeRuntime = class {
15538
15627
  async listRemoteWorkspaces(targetDeviceId, signal) {
15539
15628
  const remote = await this.ensureConnected(targetDeviceId, signal);
15540
15629
  if (remote.features.remoteGateway) {
15541
- return readRemoteWorkspaceBaseline(new RemoteTypertGateway(remote.client), signal);
15630
+ return readRemoteWorkspaceBaseline(new RemoteTypertGateway2(remote.client), signal);
15542
15631
  }
15543
15632
  const api = new RemoteHarnessApiProxy(remote.client).api;
15544
15633
  const response = await api.workspace.list({
@@ -15554,7 +15643,7 @@ var ClientModeRuntime = class {
15554
15643
  this.assertRemoteCompatible(remote);
15555
15644
  let workspace;
15556
15645
  if (remote.features.remoteGateway) {
15557
- workspace = await new RemoteTypertGateway(remote.client).invoke({
15646
+ workspace = await new RemoteTypertGateway2(remote.client).invoke({
15558
15647
  namespace: "workspace",
15559
15648
  method: "create",
15560
15649
  args: { request: { path } },
@@ -15605,7 +15694,7 @@ var ClientModeRuntime = class {
15605
15694
  selectRemoteTarget(remote) {
15606
15695
  const target = { deviceId: remote.target.deviceId, name: remote.target.name };
15607
15696
  if (this.gatewaySwitch.supportsCarrier()) {
15608
- this.gatewaySwitch.selectRemote(new RemoteTypertGateway(remote.client), void 0, target);
15697
+ this.gatewaySwitch.selectRemote(new RemoteTypertGateway2(remote.client), void 0, target);
15609
15698
  return;
15610
15699
  }
15611
15700
  this.proxySwitch.selectRemote(new RemoteHarnessApiProxy(remote.client).api, target);
@@ -15634,6 +15723,11 @@ var ClientModeRuntime = class {
15634
15723
  if (!presence.online) throw new ClientModeError("HOST_OFFLINE", "The selected Host is offline.", true);
15635
15724
  const credentials = await this.server.authenticate(identity);
15636
15725
  const rtcFactory = this.config.forceRelay ? void 0 : await this.rtcFactoryProvider({ routeTargets: [this.server.baseUrl] }).catch(() => void 0);
15726
+ if (!this.config.forceRelay && rtcFactory === void 0) {
15727
+ this.logger.warn("remote Harness WebRTC backend unavailable; using relay", {
15728
+ targetDeviceId: shortId(target.deviceId)
15729
+ });
15730
+ }
15637
15731
  let webRtcFallback = false;
15638
15732
  const createTransport = (attempt) => new AdaptiveTransport(
15639
15733
  websocketUrl(this.server.baseUrl),
@@ -15698,6 +15792,11 @@ var ClientModeRuntime = class {
15698
15792
  this.logger.info("remote Harness transport ready", {
15699
15793
  targetDeviceId: shortId(target.deviceId),
15700
15794
  transport: connectedClient.getStats().mode,
15795
+ ...connectionDetails === void 0 ? {} : {
15796
+ preferredTransports: connectionDetails.preferredTransports,
15797
+ negotiatedCapabilities: connectionDetails.negotiatedCapabilities,
15798
+ webRtcEnabled: connectionDetails.webRtcEnabled
15799
+ },
15701
15800
  ...webrtcDiagnosticsLogFields(connectionDetails?.webRtc?.diagnostics)
15702
15801
  });
15703
15802
  const features = await probeRemoteHostFeatures(connectedClient, serverDevice.clientVersion);
@@ -17026,6 +17125,7 @@ var HostServerConnection = class {
17026
17125
  reconnectRequested = false;
17027
17126
  resumeQueued = false;
17028
17127
  rtcFactory;
17128
+ negotiatedCapabilities = ["transport.relay"];
17029
17129
  start() {
17030
17130
  if (this.running !== void 0) return;
17031
17131
  this.stopped = false;
@@ -17115,6 +17215,7 @@ var HostServerConnection = class {
17115
17215
  async connectOnce() {
17116
17216
  const credentials = await this.api.authenticate(this.identity);
17117
17217
  if (this.stopped) return;
17218
+ const offeredCapabilities = this.rtcFactoryProvider === void 0 || this.config.forceRelay ? ["transport.relay", ...this.hostCapabilities()] : ["transport.p2p", "transport.turn", "transport.relay", ...this.hostCapabilities()];
17118
17219
  const socket = this.createWebSocket(websocketUrl2(this.api.baseUrl));
17119
17220
  this.socket = socket;
17120
17221
  let acknowledged = false;
@@ -17138,7 +17239,7 @@ var HostServerConnection = class {
17138
17239
  protocols: [PROTOCOL_VERSION],
17139
17240
  clientVersion: PLUGIN_VERSION,
17140
17241
  ...this.harnessVersion === void 0 ? {} : { harnessVersion: this.harnessVersion },
17141
- capabilities: this.rtcFactoryProvider === void 0 || this.config.forceRelay ? ["transport.relay", ...this.hostCapabilities()] : ["transport.p2p", "transport.turn", "transport.relay", ...this.hostCapabilities()]
17242
+ capabilities: offeredCapabilities
17142
17243
  });
17143
17244
  };
17144
17245
  socket.onmessage = (event) => {
@@ -17147,6 +17248,10 @@ var HostServerConnection = class {
17147
17248
  this.lastActiveAt = Date.now();
17148
17249
  if (frame.type === "hello.ack") {
17149
17250
  const payload = requireHelloAck(frame.payload);
17251
+ this.negotiatedCapabilities = acceptNegotiatedCapabilities(offeredCapabilities, payload.capabilities);
17252
+ if (!this.negotiatedCapabilities.some((capability) => capability === "transport.relay" || capability === "transport.p2p" || capability === "transport.turn")) {
17253
+ throw new ControlConnectionError("INVALID_MESSAGE", "Server did not negotiate a transport capability.");
17254
+ }
17150
17255
  acknowledged = true;
17151
17256
  clearTimeout(helloTimer);
17152
17257
  this.online = true;
@@ -17222,7 +17327,10 @@ var HostServerConnection = class {
17222
17327
  this.handleSignalIce(requireSignalIce(frame.payload));
17223
17328
  return;
17224
17329
  }
17225
- if (frame.type === "transport.selected") return;
17330
+ if (frame.type === "transport.selected") {
17331
+ await this.handleTransportSelected(requireTransportSelected(frame.payload));
17332
+ return;
17333
+ }
17226
17334
  if (frame.type === "signal.answer") return;
17227
17335
  if (frame.type === "error") {
17228
17336
  const payload = requireControlError(frame.payload);
@@ -17309,6 +17417,17 @@ var HostServerConnection = class {
17309
17417
  if (tunnel === void 0 || payload.targetDeviceId !== this.identity.deviceId || payload.step !== 1) {
17310
17418
  throw new ControlConnectionError("SECURE_CHANNEL_FAILED", "Noise IK handshake is not valid for this connection.");
17311
17419
  }
17420
+ if (tunnel.transport === "negotiating" && tunnel.rtc !== void 0) {
17421
+ if (tunnel.pendingHandshake === void 0) tunnel.pendingHandshake = payload;
17422
+ else this.logger.warn("duplicate pending secure handshake ignored", {
17423
+ connectionId: shortId3(tunnel.connectionId),
17424
+ peerDeviceId: shortId3(tunnel.peer.deviceId)
17425
+ });
17426
+ return;
17427
+ }
17428
+ await this.completeHandshake(tunnel, payload);
17429
+ }
17430
+ async completeHandshake(tunnel, payload) {
17312
17431
  tunnel.noise.readHandshake(fromBase64Url2(payload.data));
17313
17432
  const reply = tunnel.noise.writeHandshake();
17314
17433
  if (!tunnel.noise.complete) throw new ControlConnectionError("SECURE_CHANNEL_FAILED", "Noise IK handshake did not complete.");
@@ -17333,7 +17452,16 @@ var HostServerConnection = class {
17333
17452
  transport: mode
17334
17453
  });
17335
17454
  }
17455
+ async resumePendingHandshake(tunnel) {
17456
+ const payload = tunnel.pendingHandshake;
17457
+ if (payload === void 0 || tunnel.channel !== void 0 || tunnel.transport === "negotiating") return;
17458
+ tunnel.pendingHandshake = void 0;
17459
+ await this.completeHandshake(tunnel, payload);
17460
+ }
17336
17461
  async handleRelay(payload) {
17462
+ if (!this.negotiatedCapabilities.includes("transport.relay")) {
17463
+ throw new ControlConnectionError("INVALID_MESSAGE", "Server forwarded Relay without negotiating it.");
17464
+ }
17337
17465
  if (payload.targetDeviceId !== this.identity.deviceId) {
17338
17466
  throw new ControlConnectionError("INVALID_MESSAGE", "Relay frame target does not match this Host.");
17339
17467
  }
@@ -17352,6 +17480,9 @@ var HostServerConnection = class {
17352
17480
  }
17353
17481
  }
17354
17482
  async handleSignalOffer(payload) {
17483
+ if (!this.canUseWebRtc()) {
17484
+ throw new ControlConnectionError("INVALID_MESSAGE", "Server forwarded WebRTC signaling without negotiating it.");
17485
+ }
17355
17486
  const tunnel = this.tunnels.get(payload.connectionId);
17356
17487
  if (tunnel === void 0 || payload.targetDeviceId !== this.identity.deviceId) {
17357
17488
  this.logger.warn("stale webrtc offer ignored", { connectionId: shortId3(payload.connectionId) });
@@ -17413,10 +17544,54 @@ var HostServerConnection = class {
17413
17544
  rtc.handleSignal({ type: "offer", sdp: payload.sdp });
17414
17545
  }
17415
17546
  handleSignalIce(payload) {
17547
+ if (!this.canUseWebRtc()) {
17548
+ throw new ControlConnectionError("INVALID_MESSAGE", "Server forwarded WebRTC signaling without negotiating it.");
17549
+ }
17416
17550
  const tunnel = this.tunnels.get(payload.connectionId);
17417
17551
  if (tunnel === void 0 || payload.targetDeviceId !== this.identity.deviceId) return;
17418
17552
  tunnel.rtc?.handleSignal({ type: "ice", candidate: payload.candidate });
17419
17553
  }
17554
+ async handleTransportSelected(payload) {
17555
+ const tunnel = this.tunnels.get(payload.connectionId);
17556
+ if (tunnel === void 0 || payload.targetDeviceId !== this.identity.deviceId) {
17557
+ this.logger.warn("stale transport selection ignored", { connectionId: shortId3(payload.connectionId) });
17558
+ return;
17559
+ }
17560
+ if (tunnel.channel !== void 0) {
17561
+ if (tunnel.transport !== payload.transport) {
17562
+ this.logger.warn("late transport selection ignored", {
17563
+ connectionId: shortId3(tunnel.connectionId),
17564
+ selected: payload.transport
17565
+ });
17566
+ }
17567
+ return;
17568
+ }
17569
+ const requiredCapability = `transport.${payload.transport}`;
17570
+ if (!this.negotiatedCapabilities.includes(requiredCapability)) {
17571
+ throw new ControlConnectionError(
17572
+ "INVALID_MESSAGE",
17573
+ `Client selected unnegotiated transport: ${payload.transport}`
17574
+ );
17575
+ }
17576
+ if (payload.transport === "relay") {
17577
+ const rtc = tunnel.rtc;
17578
+ tunnel.rtc = void 0;
17579
+ tunnel.transport = "relay";
17580
+ tunnel.transportMode = void 0;
17581
+ await rtc?.close();
17582
+ await this.resumePendingHandshake(tunnel);
17583
+ return;
17584
+ }
17585
+ if (tunnel.rtc === void 0) {
17586
+ throw new ControlConnectionError("INVALID_MESSAGE", "Client selected WebRTC before creating a data channel.");
17587
+ }
17588
+ tunnel.transport = payload.transport;
17589
+ tunnel.transportMode = tunnel.rtc.selectedPathMode();
17590
+ await this.resumePendingHandshake(tunnel);
17591
+ }
17592
+ canUseWebRtc() {
17593
+ return this.negotiatedCapabilities.includes("transport.p2p") || this.negotiatedCapabilities.includes("transport.turn");
17594
+ }
17420
17595
  sendRtcSignal(tunnel, signal) {
17421
17596
  if (signal.type === "answer") {
17422
17597
  this.sendControl("signal.answer", {
@@ -17434,6 +17609,20 @@ var HostServerConnection = class {
17434
17609
  }
17435
17610
  handleRtcOpened(tunnel, selected) {
17436
17611
  if (this.tunnels.get(tunnel.connectionId) !== tunnel || tunnel.rtc === void 0) return;
17612
+ const requiredCapability = selected === "turn" ? "transport.turn" : "transport.p2p";
17613
+ if (!this.negotiatedCapabilities.includes(requiredCapability)) {
17614
+ const error = new Error(`WebRTC selected unnegotiated transport: ${selected}`);
17615
+ if (!this.negotiatedCapabilities.includes("transport.relay")) {
17616
+ void this.dropTunnel(tunnel.connectionId, "CONNECTION_FAILED");
17617
+ return;
17618
+ }
17619
+ void this.handleRtcFailed(
17620
+ tunnel,
17621
+ tunnel.rtc,
17622
+ error
17623
+ );
17624
+ return;
17625
+ }
17437
17626
  tunnel.transport = selected;
17438
17627
  tunnel.transportMode = tunnel.rtc.selectedPathMode();
17439
17628
  this.sendTransportSelected(tunnel, selected);
@@ -17443,6 +17632,13 @@ var HostServerConnection = class {
17443
17632
  transport: tunnel.transportMode ?? selected,
17444
17633
  ...webrtcDiagnosticsLogFields2(rtcDiagnostics(tunnel.rtc))
17445
17634
  });
17635
+ void this.resumePendingHandshake(tunnel).catch((error) => {
17636
+ this.logger.warn("pending secure handshake failed", {
17637
+ connectionId: shortId3(tunnel.connectionId),
17638
+ reason: diagnosticReason3(error)
17639
+ });
17640
+ void this.dropTunnel(tunnel.connectionId, "SECURE_CHANNEL_FAILED");
17641
+ });
17446
17642
  }
17447
17643
  async handleRtcFailed(tunnel, rtc, error) {
17448
17644
  if (this.tunnels.get(tunnel.connectionId) !== tunnel || tunnel.rtc !== rtc) return;
@@ -17463,6 +17659,7 @@ var HostServerConnection = class {
17463
17659
  reason: diagnosticReason3(error),
17464
17660
  ...webrtcDiagnosticsLogFields2(rtcDiagnostics(rtc))
17465
17661
  });
17662
+ await this.resumePendingHandshake(tunnel);
17466
17663
  }
17467
17664
  sendTransportSelected(tunnel, transport) {
17468
17665
  this.sendControl("transport.selected", {
@@ -17662,6 +17859,13 @@ function requireSignalIce(value) {
17662
17859
  }
17663
17860
  return payload;
17664
17861
  }
17862
+ function requireTransportSelected(value) {
17863
+ const payload = requireObject(value);
17864
+ if (typeof payload.connectionId !== "string" || typeof payload.targetDeviceId !== "string" || payload.transport !== "p2p" && payload.transport !== "turn" && payload.transport !== "relay") {
17865
+ throw new ControlConnectionError("INVALID_MESSAGE", "transport.selected payload is invalid.");
17866
+ }
17867
+ return payload;
17868
+ }
17665
17869
  function requireControlError(value) {
17666
17870
  const payload = requireObject(value);
17667
17871
  if (typeof payload.code !== "string" || typeof payload.message !== "string" || payload.connectionId !== void 0 && (typeof payload.connectionId !== "string" || payload.connectionId === "")) {
@@ -19249,7 +19453,7 @@ export {
19249
19453
  PluginControlRuntime,
19250
19454
  RemoteFileViewerBridge,
19251
19455
  RemoteHarnessApiProxy,
19252
- RemoteTypertGateway,
19456
+ RemoteTypertGateway2 as RemoteTypertGateway,
19253
19457
  RpcError,
19254
19458
  RpcRouter,
19255
19459
  ServerApiError,