@afterrealism/dendri-client 2.3.7 → 2.5.0

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.
@@ -3536,7 +3536,7 @@ var dendri = (() => {
3536
3536
  var util = new Util();
3537
3537
 
3538
3538
  // src/api.ts
3539
- var version = "2.3.7";
3539
+ var version = "2.5.0";
3540
3540
  var API = class _API {
3541
3541
  constructor(_options) {
3542
3542
  this._options = _options;
@@ -3549,6 +3549,7 @@ var dendri = (() => {
3549
3549
  const url = new URL(`${protocol}://${host}:${port}${path}${key}/${method}`);
3550
3550
  url.searchParams.set("ts", `${Date.now()}${Math.random()}`);
3551
3551
  url.searchParams.set("version", version);
3552
+ if (this._options.apiKey) url.searchParams.set("api_key", this._options.apiKey);
3552
3553
  const controller = new AbortController();
3553
3554
  const timeoutId = setTimeout(() => controller.abort(), _API.FETCH_TIMEOUT);
3554
3555
  return fetch(url.href, {
@@ -3573,15 +3574,16 @@ var dendri = (() => {
3573
3574
  throw new Error(`Could not get an ID from the server.${pathError}`);
3574
3575
  }
3575
3576
  }
3576
- /** Fetch TURN credentials from the signaling server's GET /turn endpoint. */
3577
+ /** Fetch TURN credentials from the signaling server's turn-credentials endpoint. */
3577
3578
  async getTurnCredentials() {
3578
3579
  const protocol = this._options.secure ? "https" : "http";
3579
- const { host, port } = this._options;
3580
- const url = `${protocol}://${host}:${port}/turn`;
3580
+ const { host, port, path, key } = this._options;
3581
+ const url = new URL(`${protocol}://${host}:${port}${path}${key}/turn-credentials`);
3582
+ if (this._options.apiKey) url.searchParams.set("api_key", this._options.apiKey);
3581
3583
  try {
3582
3584
  const controller = new AbortController();
3583
3585
  const timeoutId = setTimeout(() => controller.abort(), _API.FETCH_TIMEOUT);
3584
- const response = await fetch(url, {
3586
+ const response = await fetch(url.href, {
3585
3587
  referrerPolicy: this._options.referrerPolicy,
3586
3588
  signal: controller.signal
3587
3589
  }).finally(() => clearTimeout(timeoutId));
@@ -3706,6 +3708,7 @@ var dendri = (() => {
3706
3708
  }
3707
3709
  connection;
3708
3710
  _pendingCandidates = [];
3711
+ _iceCandidateFilter = null;
3709
3712
  /** Returns a PeerConnection object set up correctly (for data, media). */
3710
3713
  startConnection(options) {
3711
3714
  const peerConnection = this._startPeerConnection();
@@ -3728,6 +3731,13 @@ var dendri = (() => {
3728
3731
  _startPeerConnection() {
3729
3732
  logger_default.log("Creating RTCPeerConnection.");
3730
3733
  const peerConnection = new RTCPeerConnection(this.connection.provider?.options.config);
3734
+ if (this.connection.provider?.options.ipPolicy === "public") {
3735
+ const isPublicCandidate = (c) => {
3736
+ const sdp2 = c.candidate ?? "";
3737
+ return !sdp2.includes("typ host");
3738
+ };
3739
+ this._iceCandidateFilter = isPublicCandidate;
3740
+ }
3731
3741
  this._setupListeners(peerConnection);
3732
3742
  return peerConnection;
3733
3743
  }
@@ -3740,6 +3750,9 @@ var dendri = (() => {
3740
3750
  logger_default.log("Listening for ICE candidates.");
3741
3751
  peerConnection.onicecandidate = (evt) => {
3742
3752
  if (!evt.candidate?.candidate) return;
3753
+ if (this._iceCandidateFilter && !this._iceCandidateFilter(evt.candidate)) {
3754
+ return;
3755
+ }
3743
3756
  logger_default.log(`Received ICE candidates for ${peerId}:`, evt.candidate);
3744
3757
  provider.socket.send({
3745
3758
  type: "CANDIDATE" /* Candidate */,
@@ -4078,6 +4091,7 @@ var dendri = (() => {
4078
4091
  this.dataChannel.onopen = () => {
4079
4092
  logger_default.log(`DC#${this.connectionId} dc connection success`);
4080
4093
  this._open = true;
4094
+ this._applyAdaptiveBuffer(dc);
4081
4095
  this.emit("open");
4082
4096
  };
4083
4097
  this.dataChannel.onclose = () => {
@@ -4085,6 +4099,24 @@ var dendri = (() => {
4085
4099
  this.close();
4086
4100
  };
4087
4101
  }
4102
+ _applyAdaptiveBuffer(dc) {
4103
+ const pc = this.peerConnection;
4104
+ if (!pc || typeof pc.getStats !== "function") return;
4105
+ pc.getStats().then((stats) => {
4106
+ let rtt = null;
4107
+ stats.forEach((report) => {
4108
+ if (report.type === "candidate-pair" && report.state === "succeeded" && report.currentRoundTripTime) {
4109
+ rtt = report.currentRoundTripTime * 1e3;
4110
+ }
4111
+ });
4112
+ if (rtt !== null) {
4113
+ const bdp = 12.5 * 1024 * 1024 * (rtt / 1e3);
4114
+ const optimal = Math.max(1 * 1024 * 1024, Math.min(32 * 1024 * 1024, Math.ceil(bdp)));
4115
+ dc.bufferedAmountLowThreshold = optimal;
4116
+ }
4117
+ }).catch(() => {
4118
+ });
4119
+ }
4088
4120
  /**
4089
4121
  * Exposed functionality for users.
4090
4122
  */
@@ -4619,7 +4651,17 @@ var dendri = (() => {
4619
4651
  if (this._closed) {
4620
4652
  return;
4621
4653
  }
4622
- this._attemptWebRTC();
4654
+ logger_default.log(
4655
+ `HybridConnection: start peer=${this.peer} iceTimeout=${this._options.iceTimeout ?? 1e4}ms encryptRelay=${this._encryptRelay}`
4656
+ );
4657
+ if (typeof this._provider?.on !== "function") {
4658
+ this._attemptWebRTC();
4659
+ return;
4660
+ }
4661
+ this._tryConnectionReversal().then((direct) => {
4662
+ if (direct) return;
4663
+ this._attemptWebRTC();
4664
+ });
4623
4665
  }
4624
4666
  /** Send data through the best available transport, optionally tagged with a topic. */
4625
4667
  send(data, options) {
@@ -4909,10 +4951,16 @@ var dendri = (() => {
4909
4951
  this._dataConnection = dc;
4910
4952
  this._iceTimer = setTimeout(() => {
4911
4953
  if (this._mode !== "webrtc" /* WebRTC */) {
4954
+ logger_default.warn(
4955
+ `HybridConnection: ICE timeout after ${iceTimeout}ms for ${this.peer}, falling back to relay`
4956
+ );
4912
4957
  this._fallbackToRelay();
4913
4958
  }
4914
4959
  }, iceTimeout);
4915
4960
  this._dataConnection.on("open", () => {
4961
+ logger_default.log(
4962
+ `HybridConnection: WebRTC opened to ${this.peer} (attempt ${this._upgradeAttempts + 1})`
4963
+ );
4916
4964
  this._clearIceTimer();
4917
4965
  this._clearUpgradeTimer();
4918
4966
  this._upgradeAttempts = 0;
@@ -4953,6 +5001,7 @@ var dendri = (() => {
4953
5001
  /** Update the transport mode and emit if changed. */
4954
5002
  _setMode(mode) {
4955
5003
  if (this._mode !== mode) {
5004
+ logger_default.log(`HybridConnection: transport ${this._mode} -> ${mode} for ${this.peer}`);
4956
5005
  this._mode = mode;
4957
5006
  this.emit("transportChanged", mode);
4958
5007
  }
@@ -4993,6 +5042,117 @@ var dendri = (() => {
4993
5042
  this._attemptWebRTC();
4994
5043
  }, interval);
4995
5044
  }
5045
+ async _tryConnectionReversal() {
5046
+ if (typeof this._provider?.on !== "function") return false;
5047
+ try {
5048
+ const resp = await new Promise((resolve, reject) => {
5049
+ const timer = setTimeout(() => reject(new Error("timeout")), 3e3);
5050
+ const handler = (data) => {
5051
+ clearTimeout(timer);
5052
+ this._provider.off("CONNECT-REQUEST" /* ConnectRequest */, handler);
5053
+ resolve(data);
5054
+ };
5055
+ this._provider.on("CONNECT-REQUEST" /* ConnectRequest */, handler);
5056
+ this._provider.socket.send({
5057
+ type: "CONNECT-REQUEST" /* ConnectRequest */,
5058
+ payload: { peer: this.peer }
5059
+ });
5060
+ });
5061
+ const addr = resp?.address;
5062
+ if (!addr) return false;
5063
+ const pc = new RTCPeerConnection(this._provider.options.config);
5064
+ const dc = pc.createDataChannel("probe", { id: 0 });
5065
+ await new Promise((resolve, reject) => {
5066
+ const timer = setTimeout(() => {
5067
+ pc.close();
5068
+ reject(new Error("direct-dial-timeout"));
5069
+ }, 2500);
5070
+ dc.onopen = () => {
5071
+ clearTimeout(timer);
5072
+ resolve();
5073
+ };
5074
+ dc.onerror = () => {
5075
+ clearTimeout(timer);
5076
+ pc.close();
5077
+ reject(new Error("dc-error"));
5078
+ };
5079
+ });
5080
+ this._dataConnection = void 0;
5081
+ this._setMode("webrtc" /* WebRTC */);
5082
+ pc.close();
5083
+ return true;
5084
+ } catch {
5085
+ return false;
5086
+ }
5087
+ }
5088
+ async _gatherSrflxCandidates() {
5089
+ const pc = new RTCPeerConnection({ iceServers: this._provider.options.config?.iceServers });
5090
+ pc.createDataChannel("probe");
5091
+ const offer = await pc.createOffer();
5092
+ await pc.setLocalDescription(offer);
5093
+ const candidates = [];
5094
+ await new Promise((resolve) => {
5095
+ const timer = setTimeout(resolve, 2e3);
5096
+ pc.onicecandidate = (evt) => {
5097
+ if (!evt.candidate) {
5098
+ clearTimeout(timer);
5099
+ resolve();
5100
+ return;
5101
+ }
5102
+ if (!evt.candidate.candidate.includes("typ host")) {
5103
+ candidates.push(evt.candidate.candidate);
5104
+ }
5105
+ };
5106
+ });
5107
+ pc.close();
5108
+ return candidates;
5109
+ }
5110
+ async _dcutrHolePunch() {
5111
+ try {
5112
+ const localAddrs = await this._gatherSrflxCandidates();
5113
+ const t0 = performance.now();
5114
+ const peerAddrs = await new Promise((resolve, reject) => {
5115
+ const timer = setTimeout(() => reject(new Error("dcutr-timeout")), 8e3);
5116
+ const handler = (data) => {
5117
+ clearTimeout(timer);
5118
+ this._provider.off("DCUTR-CONNECT" /* DcutrConnect */, handler);
5119
+ resolve(data?.addresses ?? []);
5120
+ };
5121
+ this._provider.on("DCUTR-CONNECT" /* DcutrConnect */, handler);
5122
+ this._provider.socket.send({
5123
+ type: "DCUTR-CONNECT" /* DcutrConnect */,
5124
+ payload: { addresses: localAddrs }
5125
+ });
5126
+ });
5127
+ const relayRtt = performance.now() - t0;
5128
+ this._provider.socket.send({ type: "DCUTR-SYNC" /* DcutrSync */, payload: {} });
5129
+ await new Promise((r) => setTimeout(r, relayRtt / 2));
5130
+ for (let i = 0; i < Math.min(peerAddrs.length, 4); i++) {
5131
+ try {
5132
+ const pc = new RTCPeerConnection(this._provider.options.config);
5133
+ await new Promise((resolve, reject) => {
5134
+ const timer = setTimeout(() => {
5135
+ pc.close();
5136
+ reject(new Error("dc-dial-timeout"));
5137
+ }, 5e3);
5138
+ const dc = pc.createDataChannel("dcutr");
5139
+ dc.onopen = () => {
5140
+ clearTimeout(timer);
5141
+ resolve();
5142
+ };
5143
+ });
5144
+ this._dataConnection = void 0;
5145
+ this._setMode("webrtc" /* WebRTC */);
5146
+ pc.close();
5147
+ return true;
5148
+ } catch {
5149
+ }
5150
+ }
5151
+ return false;
5152
+ } catch {
5153
+ return false;
5154
+ }
5155
+ }
4996
5156
  };
4997
5157
 
4998
5158
  // src/mediaconnection.ts
@@ -5149,17 +5309,29 @@ var dendri = (() => {
5149
5309
  _heartbeatTimer;
5150
5310
  _lastSeq = 0;
5151
5311
  _baseUrl;
5312
+ _key;
5313
+ _jwt;
5314
+ _apiKey;
5152
5315
  _pingInterval;
5153
5316
  _abortController;
5154
5317
  /** Backoff schedule base delays in milliseconds. */
5155
5318
  static BACKOFF_SCHEDULE = [0, 1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
5156
5319
  /** Random jitter range in milliseconds (applied as +/-). */
5157
5320
  static BACKOFF_JITTER = 500;
5158
- constructor(secure, host, port, path, _key, pingInterval = 5e3, _jwt) {
5321
+ constructor(secure, host, port, path, key, pingInterval = 5e3, jwt, apiKey) {
5159
5322
  super();
5160
5323
  const protocol = secure ? "https://" : "http://";
5161
5324
  this._baseUrl = `${protocol + host}:${port}${path}`;
5162
5325
  this._pingInterval = pingInterval;
5326
+ this._key = key;
5327
+ this._jwt = jwt;
5328
+ this._apiKey = apiKey;
5329
+ }
5330
+ /** Append the shared auth params (key, jwt, api_key) so every HTTP call authenticates like the WS transport. */
5331
+ _applyAuthParams(params) {
5332
+ params.set("key", this._key);
5333
+ if (this._jwt) params.set("jwt", this._jwt);
5334
+ if (this._apiKey) params.set("api_key", this._apiKey);
5163
5335
  }
5164
5336
  get reconnectAttempt() {
5165
5337
  return this._reconnectAttempt;
@@ -5187,6 +5359,7 @@ var dendri = (() => {
5187
5359
  id: this._id,
5188
5360
  token: this._token
5189
5361
  });
5362
+ this._applyAuthParams(params);
5190
5363
  if (this._lastSeq > 0) params.set("last_seq", String(this._lastSeq));
5191
5364
  const response = await fetch(`${this._baseUrl}http/poll?${params}`, {
5192
5365
  signal: this._abortController.signal
@@ -5225,6 +5398,7 @@ var dendri = (() => {
5225
5398
  id: this._id,
5226
5399
  token: this._token
5227
5400
  });
5401
+ this._applyAuthParams(params);
5228
5402
  try {
5229
5403
  await fetch(`${this._baseUrl}http/send?${params}`, {
5230
5404
  method: "POST",
@@ -5289,13 +5463,16 @@ var dendri = (() => {
5289
5463
  };
5290
5464
 
5291
5465
  // src/socket.ts
5292
- var version2 = "2.3.7";
5466
+ var version2 = "2.5.0";
5293
5467
  var Socket = class _Socket extends SignalingTransport {
5294
- constructor(secure, host, port, path, key, pingInterval = 5e3, jwt) {
5468
+ constructor(secure, host, port, path, key, pingInterval = 5e3, jwt, apiKey) {
5295
5469
  super();
5296
5470
  this.pingInterval = pingInterval;
5297
5471
  const wsProtocol = secure ? "wss://" : "ws://";
5298
5472
  this._baseUrl = `${wsProtocol + host}:${port}${path}dendri?key=${key}`;
5473
+ if (apiKey) {
5474
+ this._baseUrl += `&api_key=${encodeURIComponent(apiKey)}`;
5475
+ }
5299
5476
  this._jwt = jwt;
5300
5477
  }
5301
5478
  pingInterval;
@@ -5331,7 +5508,7 @@ var dendri = (() => {
5331
5508
  if (this._jwt) {
5332
5509
  wsUrl += `&jwt=${encodeURIComponent(this._jwt)}`;
5333
5510
  }
5334
- if (!!this._socket || !this._disconnected) {
5511
+ if (this._socket || !this._disconnected) {
5335
5512
  return;
5336
5513
  }
5337
5514
  this._socket = new WebSocket(`${wsUrl}&version=${version2}`);
@@ -5601,18 +5778,28 @@ var dendri = (() => {
5601
5778
  _heartbeatTimer;
5602
5779
  _lastSeq = 0;
5603
5780
  _baseUrl;
5781
+ _key;
5604
5782
  _jwt;
5783
+ _apiKey;
5605
5784
  _pingInterval;
5606
5785
  /** Backoff schedule base delays in milliseconds. */
5607
5786
  static BACKOFF_SCHEDULE = [0, 1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
5608
5787
  /** Random jitter range in milliseconds (applied as +/-). */
5609
5788
  static BACKOFF_JITTER = 500;
5610
- constructor(secure, host, port, path, _key, pingInterval = 5e3, jwt) {
5789
+ constructor(secure, host, port, path, key, pingInterval = 5e3, jwt, apiKey) {
5611
5790
  super();
5612
5791
  const protocol = secure ? "https://" : "http://";
5613
5792
  this._baseUrl = `${protocol + host}:${port}${path}`;
5614
5793
  this._pingInterval = pingInterval;
5794
+ this._key = key;
5615
5795
  this._jwt = jwt;
5796
+ this._apiKey = apiKey;
5797
+ }
5798
+ /** Append the shared auth params (key, jwt, api_key) so every HTTP call authenticates like the WS transport. */
5799
+ _applyAuthParams(params) {
5800
+ params.set("key", this._key);
5801
+ if (this._jwt) params.set("jwt", this._jwt);
5802
+ if (this._apiKey) params.set("api_key", this._apiKey);
5616
5803
  }
5617
5804
  get reconnectAttempt() {
5618
5805
  return this._reconnectAttempt;
@@ -5630,10 +5817,9 @@ var dendri = (() => {
5630
5817
  if (this._disconnected) return;
5631
5818
  const params = new URLSearchParams({
5632
5819
  id: this._id,
5633
- token: this._token,
5634
- key: "dendri"
5820
+ token: this._token
5635
5821
  });
5636
- if (this._jwt) params.set("jwt", this._jwt);
5822
+ this._applyAuthParams(params);
5637
5823
  if (this._lastSeq > 0) params.set("last_seq", String(this._lastSeq));
5638
5824
  const url = `${this._baseUrl}http/sse?${params}`;
5639
5825
  try {
@@ -5707,6 +5893,7 @@ var dendri = (() => {
5707
5893
  id: this._id,
5708
5894
  token: this._token
5709
5895
  });
5896
+ this._applyAuthParams(params);
5710
5897
  try {
5711
5898
  await fetch(`${this._baseUrl}http/send?${params}`, {
5712
5899
  method: "POST",
@@ -5779,6 +5966,28 @@ var dendri = (() => {
5779
5966
  };
5780
5967
 
5781
5968
  // src/dendri.ts
5969
+ function parseServerUrl(url) {
5970
+ let parsed;
5971
+ try {
5972
+ parsed = new URL(url);
5973
+ } catch {
5974
+ throw new Error(
5975
+ `Invalid Dendri "url" option: "${url}". Expected a full URL like "wss://signal.example.com".`
5976
+ );
5977
+ }
5978
+ const secure = parsed.protocol === "wss:" || parsed.protocol === "https:";
5979
+ if (!secure && parsed.protocol !== "ws:" && parsed.protocol !== "http:") {
5980
+ throw new Error(
5981
+ `Invalid Dendri "url" protocol: "${parsed.protocol}". Use wss://, ws://, https://, or http://.`
5982
+ );
5983
+ }
5984
+ return {
5985
+ host: parsed.hostname,
5986
+ port: parsed.port ? Number(parsed.port) : secure ? 443 : 80,
5987
+ secure,
5988
+ path: parsed.pathname || "/"
5989
+ };
5990
+ }
5782
5991
  var Dendri = class _Dendri extends EventEmitterWithError {
5783
5992
  static DEFAULT_KEY = "dendri";
5784
5993
  _serializers = {
@@ -5869,6 +6078,9 @@ var dendri = (() => {
5869
6078
  } else if (id) {
5870
6079
  userId = id.toString();
5871
6080
  }
6081
+ if (providedOptions?.url) {
6082
+ providedOptions = { ...parseServerUrl(providedOptions.url), ...providedOptions };
6083
+ }
5872
6084
  const normalizedOptions = {
5873
6085
  debug: 0,
5874
6086
  // 1: Errors, 2: Warnings, 3: All logs
@@ -5922,7 +6134,7 @@ var dendri = (() => {
5922
6134
  return;
5923
6135
  }
5924
6136
  }
5925
- if (!!userId && !util.validateId(userId)) {
6137
+ if (userId && !util.validateId(userId)) {
5926
6138
  this._delayedAbort("invalid-id" /* InvalidID */, `ID "${userId}" is invalid`);
5927
6139
  return;
5928
6140
  }
@@ -5987,7 +6199,8 @@ var dendri = (() => {
5987
6199
  this._options.path,
5988
6200
  this._options.key,
5989
6201
  this._options.pingInterval,
5990
- this._options.jwt
6202
+ this._options.jwt,
6203
+ this._options.apiKey
5991
6204
  ) : transport === "polling" ? new PollingTransport(
5992
6205
  this._options.secure ?? false,
5993
6206
  this._options.host,
@@ -5995,7 +6208,8 @@ var dendri = (() => {
5995
6208
  this._options.path,
5996
6209
  this._options.key,
5997
6210
  this._options.pingInterval,
5998
- this._options.jwt
6211
+ this._options.jwt,
6212
+ this._options.apiKey
5999
6213
  ) : new Socket(
6000
6214
  this._options.secure ?? false,
6001
6215
  this._options.host,
@@ -6003,7 +6217,8 @@ var dendri = (() => {
6003
6217
  this._options.path,
6004
6218
  this._options.key,
6005
6219
  this._options.pingInterval,
6006
- this._options.jwt
6220
+ this._options.jwt,
6221
+ this._options.apiKey
6007
6222
  );
6008
6223
  socket.on("message" /* Message */, (data) => {
6009
6224
  this._handleMessage(data);