@signalwire/js 4.0.0-beta.12 → 4.0.0-beta.13

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.
@@ -1,8 +1,8 @@
1
1
  (function(global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('node:crypto')) :
3
- typeof define === 'function' && define.amd ? define(['exports', 'node:crypto'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.SignalWire = {}), global.node_crypto));
5
- })(this, function(exports, node_crypto) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.SignalWire = {})));
5
+ })(this, function(exports) {
6
6
  //#region rolldown:runtime
7
7
  var __create = Object.create;
8
8
  var __defProp = Object.defineProperty;
@@ -9021,6 +9021,151 @@ var Destroyable = class {
9021
9021
  }
9022
9022
  };
9023
9023
 
9024
+ //#endregion
9025
+ //#region src/core/constants.ts
9026
+ const INVITE_VERSION = 1e3;
9027
+ const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
9028
+ const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
9029
+ const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
9030
+ const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
9031
+ const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
9032
+ const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
9033
+ const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
9034
+ const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
9035
+ const PREFERENCES_STORAGE_KEY = "sw:preferences";
9036
+ /** Scope value that enables automatic token refresh. */
9037
+ const SAT_REFRESH_SCOPE = "sat:refresh";
9038
+ /** API endpoints for device token operations. */
9039
+ const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
9040
+ const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
9041
+ /** Default device token TTL in seconds (15 minutes). */
9042
+ const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
9043
+ /** Buffer time in milliseconds before expiry to trigger refresh. */
9044
+ const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
9045
+ /** Maximum retry attempts for device token refresh on transient failure. */
9046
+ const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
9047
+ /** Base delay in milliseconds for exponential backoff on refresh retry. */
9048
+ const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
9049
+ /** Maximum retry attempts for developer credential refresh on transient failure. */
9050
+ const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
9051
+ /** Base delay in milliseconds for exponential backoff on credential refresh retry. */
9052
+ const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
9053
+ /** Maximum delay in milliseconds for credential refresh backoff. */
9054
+ const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
9055
+ /** Buffer in milliseconds before token expiry to trigger refresh. */
9056
+ const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
9057
+ /**
9058
+ * Clock-skew allowance (ms) for treating an in-memory credential as expired
9059
+ * when deciding whether a fresh (re)connect must re-mint the token before
9060
+ * authenticating. A token within this window of its `expiry_at` is treated as
9061
+ * stale so the reconnect re-mints via the credential provider instead of
9062
+ * replaying a dead token (which the server rejects with -32003).
9063
+ */
9064
+ const CREDENTIAL_EXPIRY_SKEW_MS = 3e4;
9065
+ /**
9066
+ * Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
9067
+ * to resolve before treating the activation as failed and falling back to
9068
+ * the developer-provided refresh path. Prevents a wedged HTTP layer from
9069
+ * leaving the session with no active refresh mechanism.
9070
+ */
9071
+ const CREDENTIAL_ACTIVATE_TIMEOUT_MS = 3e4;
9072
+ /** JSON-RPC error code for requester validation failure (corrupted auth state). */
9073
+ const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
9074
+ /** JSON-RPC error code for invalid params (e.g., missing authentication block). */
9075
+ const RPC_ERROR_INVALID_PARAMS = -32602;
9076
+ /** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
9077
+ const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
9078
+ /** Error names browsers use for a media permission denial (user or policy). */
9079
+ const MEDIA_ACCESS_DENIAL_NAMES = [
9080
+ "NotAllowedError",
9081
+ "SecurityError",
9082
+ "PermissionDeniedError"
9083
+ ];
9084
+ /** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
9085
+ const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
9086
+ /** Number of initial samples used to build a baseline for spike detection. */
9087
+ const DEFAULT_STATS_BASELINE_SAMPLES = 10;
9088
+ /** Duration in ms with no inbound audio packets before emitting a critical issue. */
9089
+ const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
9090
+ /** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
9091
+ const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
9092
+ /** Packet loss fraction (0-1) above which a warning is emitted. */
9093
+ const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
9094
+ /** Multiplier applied to baseline jitter to detect a jitter spike. */
9095
+ const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
9096
+ /** Number of seconds of metrics history to retain. */
9097
+ const DEFAULT_STATS_HISTORY_SIZE = 30;
9098
+ /** Maximum keyframe requests allowed within a single burst window. */
9099
+ const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
9100
+ /** Duration of the keyframe burst window in milliseconds. */
9101
+ const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
9102
+ /** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
9103
+ const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
9104
+ /** Minimum time between re-INVITE attempts in milliseconds. */
9105
+ const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
9106
+ /** Maximum number of re-INVITE attempts per call. */
9107
+ const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
9108
+ /** Timeout for a single re-INVITE attempt in milliseconds. */
9109
+ const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
9110
+ /** Debounce window in ms to collapse multiple detection signals into one trigger. */
9111
+ const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
9112
+ /** Cooldown period in ms between recovery attempts. */
9113
+ const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
9114
+ /** Grace period in ms before treating ICE 'disconnected' as a failure. */
9115
+ const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
9116
+ /** Timeout for a single ICE restart attempt in milliseconds. */
9117
+ const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
9118
+ /** Maximum recovery attempts before emitting 'max_attempts_reached'. */
9119
+ const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
9120
+ /** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
9121
+ const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
9122
+ /** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
9123
+ const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
9124
+ /** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
9125
+ const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
9126
+ /** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
9127
+ const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
9128
+ /** RMS level threshold (0..1) above which the local participant is considered speaking. */
9129
+ const VAD_THRESHOLD = .03;
9130
+ /** Hold window in ms below the threshold before speaking$ flips back to false. */
9131
+ const VAD_HOLD_MS = 250;
9132
+ /** Whether to persist device selections to storage by default. */
9133
+ const DEFAULT_PERSIST_DEVICE_SELECTION = true;
9134
+ /** Whether to auto-apply device changes to active calls by default. */
9135
+ const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
9136
+ /** Storage keys for persisted device selections. */
9137
+ const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
9138
+ const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
9139
+ const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
9140
+ /** Whether to auto-mute video when the tab becomes hidden. */
9141
+ const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
9142
+ /** Whether to re-enumerate devices when the page becomes visible. */
9143
+ const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
9144
+ /** Whether to check peer connection health when the page becomes visible. */
9145
+ const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
9146
+ /** Whether automatic video degradation on low bandwidth is enabled. */
9147
+ const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
9148
+ /** Bitrate in kbps below which video is automatically disabled. */
9149
+ const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
9150
+ /** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
9151
+ const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
9152
+ /** Whether relay-only escalation is enabled as a last-resort recovery tier. */
9153
+ const DEFAULT_ENABLE_RELAY_FALLBACK = true;
9154
+ /** Whether to listen for browser online/offline/connection events. */
9155
+ const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
9156
+ /** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
9157
+ const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
9158
+ /** Default video track constraints applied when video is enabled without explicit constraints. */
9159
+ const DEFAULT_VIDEO_CONSTRAINTS = {
9160
+ width: { ideal: 1280 },
9161
+ height: { ideal: 720 },
9162
+ aspectRatio: 16 / 9
9163
+ };
9164
+ /** Whether stereo Opus is enabled by default. */
9165
+ const DEFAULT_STEREO_AUDIO = false;
9166
+ /** Max average bitrate for stereo Opus in bits per second. */
9167
+ const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
9168
+
9024
9169
  //#endregion
9025
9170
  //#region src/core/errors.ts
9026
9171
  var UnexpectedError = class extends Error {
@@ -9150,6 +9295,20 @@ var CallCreateError = class extends Error {
9150
9295
  this.name = "CallCreateError";
9151
9296
  }
9152
9297
  };
9298
+ var CallNotReadyError = class extends Error {
9299
+ constructor(callId, options) {
9300
+ super(`Call "${callId}" has no self member context yet: selfId/nodeId have not been received from the server`, options);
9301
+ this.callId = callId;
9302
+ this.name = "CallNotReadyError";
9303
+ }
9304
+ };
9305
+ var ParticipantNotReadyError = class extends Error {
9306
+ constructor(memberId, options) {
9307
+ super(`Participant "${memberId}" has no call context yet: its member state (call_id/node_id) has not been received from the server`, options);
9308
+ this.memberId = memberId;
9309
+ this.name = "ParticipantNotReadyError";
9310
+ }
9311
+ };
9153
9312
  var JSONRPCError = class extends Error {
9154
9313
  constructor(code, message, data, options, requestId) {
9155
9314
  super(message, options);
@@ -9230,6 +9389,33 @@ var MediaTrackError = class extends Error {
9230
9389
  this.name = "MediaTrackError";
9231
9390
  }
9232
9391
  };
9392
+ /** True when a `getUserMedia`/`getDisplayMedia` rejection is a permission denial. */
9393
+ function isMediaAccessDenial(originalError) {
9394
+ return originalError instanceof Error && MEDIA_ACCESS_DENIAL_NAMES.includes(originalError.name);
9395
+ }
9396
+ /**
9397
+ * Failure to acquire local media (camera, microphone, or screen capture)
9398
+ * via `getUserMedia`/`getDisplayMedia`.
9399
+ *
9400
+ * Non-fatal by default: screenshare and additional-device failures never
9401
+ * end the call, and main-connection failures degrade to receive-only when
9402
+ * possible. The wrapping site sets `fatal` to `true` only when the call
9403
+ * cannot continue (receive-only fallback disabled or no receive intent).
9404
+ */
9405
+ var MediaAccessError = class extends Error {
9406
+ constructor(operation, media, originalError, fatal = false) {
9407
+ super(`Media access ${isMediaAccessDenial(originalError) ? "denied" : "failed"} for ${operation} (${media})`, { cause: originalError instanceof Error ? originalError : void 0 });
9408
+ this.operation = operation;
9409
+ this.media = media;
9410
+ this.originalError = originalError;
9411
+ this.fatal = fatal;
9412
+ this.name = "MediaAccessError";
9413
+ }
9414
+ /** True when the underlying failure is a permission denial (user or policy). */
9415
+ get denied() {
9416
+ return isMediaAccessDenial(this.originalError);
9417
+ }
9418
+ };
9233
9419
  var DPoPInitError = class extends Error {
9234
9420
  constructor(originalError, message = "Failed to initialize DPoP key pair") {
9235
9421
  super(message, { cause: originalError instanceof Error ? originalError : void 0 });
@@ -9470,9 +9656,9 @@ var require_loglevel = /* @__PURE__ */ __commonJSMin(((exports, module) => {
9470
9656
  defaultLogger$1 = new Logger();
9471
9657
  defaultLogger$1.getLogger = function getLogger$1(name) {
9472
9658
  if (typeof name !== "symbol" && typeof name !== "string" || name === "") throw new TypeError("You must supply a name when creating a logger.");
9473
- var logger$32 = _loggersByName[name];
9474
- if (!logger$32) logger$32 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
9475
- return logger$32;
9659
+ var logger$33 = _loggersByName[name];
9660
+ if (!logger$33) logger$33 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
9661
+ return logger$33;
9476
9662
  };
9477
9663
  var _log = typeof window !== undefinedType ? window.log : void 0;
9478
9664
  defaultLogger$1.noConflict = function() {
@@ -9508,8 +9694,8 @@ const defaultLoggerLevel = defaultLogger.levels.WARN;
9508
9694
  defaultLogger.setLevel(defaultLoggerLevel);
9509
9695
  let userLogger = null;
9510
9696
  /** Replace the built-in logger with a custom implementation. Pass `null` to restore defaults. */
9511
- const setLogger = (logger$32) => {
9512
- userLogger = logger$32;
9697
+ const setLogger = (logger$33) => {
9698
+ userLogger = logger$33;
9513
9699
  };
9514
9700
  let debugOptions = {};
9515
9701
  /** Configure debug options (e.g., `{ logWsTraffic: true }`). */
@@ -9553,8 +9739,8 @@ const wsTraffic = (options) => {
9553
9739
  loggerInstance.debug(`${options.type.toUpperCase()}: \n`, msg, "\n");
9554
9740
  };
9555
9741
  const getLogger = () => {
9556
- const logger$32 = getLoggerInstance();
9557
- return new Proxy(logger$32, { get(_target, prop, _receiver) {
9742
+ const logger$33 = getLoggerInstance();
9743
+ return new Proxy(logger$33, { get(_target, prop, _receiver) {
9558
9744
  if (prop === "wsTraffic") return wsTraffic;
9559
9745
  const instance = getLoggerInstance();
9560
9746
  const value = Reflect.get(instance, prop);
@@ -9606,7 +9792,7 @@ const asyncRetry = async ({ asyncCallable, maxRetries: retries = DEFAULT_MAX_RET
9606
9792
 
9607
9793
  //#endregion
9608
9794
  //#region src/controllers/HTTPRequestController.ts
9609
- const logger$31 = getLogger();
9795
+ const logger$32 = getLogger();
9610
9796
  const GET_PARAMS = {
9611
9797
  method: "GET",
9612
9798
  headers: { Accept: "application/json" }
@@ -9670,7 +9856,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9670
9856
  this._responses$.next(response);
9671
9857
  return response;
9672
9858
  } catch (error) {
9673
- logger$31.error("[HTTPRequestController] Request error:", error);
9859
+ logger$32.error("[HTTPRequestController] Request error:", error);
9674
9860
  this._status$.next("error");
9675
9861
  const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
9676
9862
  this._errors$.next(err);
@@ -9697,7 +9883,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9697
9883
  const url = this.buildURL(request.url);
9698
9884
  const headers = this.buildHeaders(request.headers);
9699
9885
  const timeout$5 = request.timeout ?? this.requestTimeout;
9700
- logger$31.debug("[HTTPRequestController] Executing request:", {
9886
+ logger$32.debug("[HTTPRequestController] Executing request:", {
9701
9887
  method: request.method,
9702
9888
  url,
9703
9889
  headers: Object.keys(headers).reduce((acc, key) => {
@@ -9717,7 +9903,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9717
9903
  });
9718
9904
  clearTimeout(timeoutId);
9719
9905
  const httpResponse = await this.convertResponse(response);
9720
- logger$31.debug("[HTTPRequestController] Response received:", {
9906
+ logger$32.debug("[HTTPRequestController] Response received:", {
9721
9907
  status: response.status,
9722
9908
  statusText: response.statusText,
9723
9909
  headers: [...response.headers.entries()],
@@ -9727,7 +9913,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9727
9913
  } catch (error) {
9728
9914
  clearTimeout(timeoutId);
9729
9915
  if (error instanceof Error && error.name === "AbortError") throw new RequestTimeoutError(`Request timeout after ${timeout$5}ms`, { cause: error });
9730
- logger$31.error("[HTTPRequestController] Request failed:", error);
9916
+ logger$32.error("[HTTPRequestController] Request failed:", error);
9731
9917
  throw error;
9732
9918
  }
9733
9919
  }
@@ -9741,8 +9927,8 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9741
9927
  const credential = this.getCredential();
9742
9928
  if (credential.token) {
9743
9929
  headers.Authorization = `Bearer ${credential.token}`;
9744
- logger$31.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
9745
- } else logger$31.warn("[HTTPRequestController] No credentials available for authentication");
9930
+ logger$32.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
9931
+ } else logger$32.warn("[HTTPRequestController] No credentials available for authentication");
9746
9932
  return headers;
9747
9933
  }
9748
9934
  /**
@@ -9879,130 +10065,6 @@ var DeviceHistoryManager = class {
9879
10065
  }
9880
10066
  };
9881
10067
 
9882
- //#endregion
9883
- //#region src/core/constants.ts
9884
- const INVITE_VERSION = 1e3;
9885
- const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
9886
- const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
9887
- const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
9888
- const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
9889
- const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
9890
- const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
9891
- const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
9892
- const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
9893
- const PREFERENCES_STORAGE_KEY = "sw:preferences";
9894
- /** Scope value that enables automatic token refresh. */
9895
- const SAT_REFRESH_SCOPE = "sat:refresh";
9896
- /** API endpoints for device token operations. */
9897
- const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
9898
- const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
9899
- /** Default device token TTL in seconds (15 minutes). */
9900
- const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
9901
- /** Buffer time in milliseconds before expiry to trigger refresh. */
9902
- const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
9903
- /** Maximum retry attempts for device token refresh on transient failure. */
9904
- const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
9905
- /** Base delay in milliseconds for exponential backoff on refresh retry. */
9906
- const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
9907
- /** Maximum retry attempts for developer credential refresh on transient failure. */
9908
- const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
9909
- /** Base delay in milliseconds for exponential backoff on credential refresh retry. */
9910
- const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
9911
- /** Maximum delay in milliseconds for credential refresh backoff. */
9912
- const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
9913
- /** Buffer in milliseconds before token expiry to trigger refresh. */
9914
- const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
9915
- /** JSON-RPC error code for requester validation failure (corrupted auth state). */
9916
- const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
9917
- /** JSON-RPC error code for invalid params (e.g., missing authentication block). */
9918
- const RPC_ERROR_INVALID_PARAMS = -32602;
9919
- /** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
9920
- const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
9921
- /** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
9922
- const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
9923
- /** Number of initial samples used to build a baseline for spike detection. */
9924
- const DEFAULT_STATS_BASELINE_SAMPLES = 10;
9925
- /** Duration in ms with no inbound audio packets before emitting a critical issue. */
9926
- const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
9927
- /** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
9928
- const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
9929
- /** Packet loss fraction (0-1) above which a warning is emitted. */
9930
- const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
9931
- /** Multiplier applied to baseline jitter to detect a jitter spike. */
9932
- const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
9933
- /** Number of seconds of metrics history to retain. */
9934
- const DEFAULT_STATS_HISTORY_SIZE = 30;
9935
- /** Maximum keyframe requests allowed within a single burst window. */
9936
- const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
9937
- /** Duration of the keyframe burst window in milliseconds. */
9938
- const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
9939
- /** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
9940
- const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
9941
- /** Minimum time between re-INVITE attempts in milliseconds. */
9942
- const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
9943
- /** Maximum number of re-INVITE attempts per call. */
9944
- const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
9945
- /** Timeout for a single re-INVITE attempt in milliseconds. */
9946
- const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
9947
- /** Debounce window in ms to collapse multiple detection signals into one trigger. */
9948
- const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
9949
- /** Cooldown period in ms between recovery attempts. */
9950
- const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
9951
- /** Grace period in ms before treating ICE 'disconnected' as a failure. */
9952
- const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
9953
- /** Timeout for a single ICE restart attempt in milliseconds. */
9954
- const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
9955
- /** Maximum recovery attempts before emitting 'max_attempts_reached'. */
9956
- const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
9957
- /** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
9958
- const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
9959
- /** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
9960
- const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
9961
- /** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
9962
- const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
9963
- /** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
9964
- const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
9965
- /** RMS level threshold (0..1) above which the local participant is considered speaking. */
9966
- const VAD_THRESHOLD = .03;
9967
- /** Hold window in ms below the threshold before speaking$ flips back to false. */
9968
- const VAD_HOLD_MS = 250;
9969
- /** Whether to persist device selections to storage by default. */
9970
- const DEFAULT_PERSIST_DEVICE_SELECTION = true;
9971
- /** Whether to auto-apply device changes to active calls by default. */
9972
- const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
9973
- /** Storage keys for persisted device selections. */
9974
- const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
9975
- const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
9976
- const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
9977
- /** Whether to auto-mute video when the tab becomes hidden. */
9978
- const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
9979
- /** Whether to re-enumerate devices when the page becomes visible. */
9980
- const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
9981
- /** Whether to check peer connection health when the page becomes visible. */
9982
- const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
9983
- /** Whether automatic video degradation on low bandwidth is enabled. */
9984
- const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
9985
- /** Bitrate in kbps below which video is automatically disabled. */
9986
- const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
9987
- /** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
9988
- const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
9989
- /** Whether relay-only escalation is enabled as a last-resort recovery tier. */
9990
- const DEFAULT_ENABLE_RELAY_FALLBACK = true;
9991
- /** Whether to listen for browser online/offline/connection events. */
9992
- const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
9993
- /** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
9994
- const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
9995
- /** Default video track constraints applied when video is enabled without explicit constraints. */
9996
- const DEFAULT_VIDEO_CONSTRAINTS = {
9997
- width: { ideal: 1280 },
9998
- height: { ideal: 720 },
9999
- aspectRatio: 16 / 9
10000
- };
10001
- /** Whether stereo Opus is enabled by default. */
10002
- const DEFAULT_STEREO_AUDIO = false;
10003
- /** Max average bitrate for stereo Opus in bits per second. */
10004
- const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
10005
-
10006
10068
  //#endregion
10007
10069
  //#region src/utils/time.ts
10008
10070
  function fromSecToMs(seconds) {
@@ -10014,7 +10076,7 @@ function fromMsToSec(milliseconds) {
10014
10076
 
10015
10077
  //#endregion
10016
10078
  //#region src/containers/PreferencesContainer.ts
10017
- const logger$30 = getLogger();
10079
+ const logger$31 = getLogger();
10018
10080
  var PreferencesContainer = class PreferencesContainer {
10019
10081
  static get instance() {
10020
10082
  this._instance ??= new PreferencesContainer();
@@ -10676,7 +10738,7 @@ var ClientPreferences = class {
10676
10738
  if (!this._storage) return;
10677
10739
  const data = collectStoredPreferences();
10678
10740
  this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
10679
- logger$30.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
10741
+ logger$31.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
10680
10742
  });
10681
10743
  }
10682
10744
  /** Loads preferences from storage and applies them to the container. */
@@ -10685,7 +10747,7 @@ var ClientPreferences = class {
10685
10747
  this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
10686
10748
  if (stored) applyStoredPreferences(stored);
10687
10749
  }).catch((error) => {
10688
- logger$30.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
10750
+ logger$31.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
10689
10751
  });
10690
10752
  }
10691
10753
  };
@@ -10707,7 +10769,7 @@ function toError(value) {
10707
10769
  //#endregion
10708
10770
  //#region src/controllers/NavigatorDeviceController.ts
10709
10771
  var import_cjs$29 = require_cjs();
10710
- const logger$29 = getLogger();
10772
+ const logger$30 = getLogger();
10711
10773
  /** Maps a device kind to its storage key. */
10712
10774
  const DEVICE_STORAGE_KEYS = {
10713
10775
  audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
@@ -10729,7 +10791,7 @@ var NavigatorDeviceController = class extends Destroyable {
10729
10791
  super();
10730
10792
  this.webRTCApiProvider = webRTCApiProvider;
10731
10793
  this.deviceChangeHandler = () => {
10732
- logger$29.debug("[DeviceController] Device change detected");
10794
+ logger$30.debug("[DeviceController] Device change detected");
10733
10795
  this.enumerateDevices();
10734
10796
  };
10735
10797
  this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
@@ -10794,13 +10856,13 @@ var NavigatorDeviceController = class extends Destroyable {
10794
10856
  return this.cachedObservable("videoInputDevices$", () => this._devicesState$.pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$)));
10795
10857
  }
10796
10858
  get selectedAudioInputDevice$() {
10797
- return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audioinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$29.debug("[DeviceController] Selected audio input device changed:", info))));
10859
+ return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audioinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$30.debug("[DeviceController] Selected audio input device changed:", info))));
10798
10860
  }
10799
10861
  get selectedAudioOutputDevice$() {
10800
- return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audiooutput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$29.debug("[DeviceController] Selected audio output device changed:", info))));
10862
+ return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audiooutput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$30.debug("[DeviceController] Selected audio output device changed:", info))));
10801
10863
  }
10802
10864
  get selectedVideoInputDevice$() {
10803
- return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$29.debug("[DeviceController] Selected video input device changed:", info))));
10865
+ return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$30.debug("[DeviceController] Selected video input device changed:", info))));
10804
10866
  }
10805
10867
  get selectedAudioInputDevice() {
10806
10868
  if (this._audioInputDisabled$.value) return null;
@@ -10875,7 +10937,7 @@ var NavigatorDeviceController = class extends Destroyable {
10875
10937
  if (device) this.persistDeviceSelection("audioinput", device);
10876
10938
  }
10877
10939
  selectVideoInputDevice(device) {
10878
- logger$29.debug("[DeviceController] Setting selected video input device:", device);
10940
+ logger$30.debug("[DeviceController] Setting selected video input device:", device);
10879
10941
  if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
10880
10942
  const previous = this._selectedDevicesState$.value.videoinput;
10881
10943
  if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
@@ -10932,7 +10994,7 @@ var NavigatorDeviceController = class extends Destroyable {
10932
10994
  }
10933
10995
  const fromHistory = this._deviceHistory.findInHistory(kind, devices);
10934
10996
  if (fromHistory) {
10935
- logger$29.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
10997
+ logger$30.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
10936
10998
  this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
10937
10999
  return fromHistory;
10938
11000
  }
@@ -10985,7 +11047,7 @@ var NavigatorDeviceController = class extends Destroyable {
10985
11047
  try {
10986
11048
  await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
10987
11049
  } catch (error) {
10988
- logger$29.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
11050
+ logger$30.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
10989
11051
  }
10990
11052
  }
10991
11053
  async loadPersistedDevices() {
@@ -11001,7 +11063,7 @@ var NavigatorDeviceController = class extends Destroyable {
11001
11063
  [kind]: stored
11002
11064
  };
11003
11065
  } catch (error) {
11004
- logger$29.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
11066
+ logger$30.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
11005
11067
  }
11006
11068
  }
11007
11069
  /** Clears device history, persisted selections, and re-enumerates devices. */
@@ -11019,7 +11081,7 @@ var NavigatorDeviceController = class extends Destroyable {
11019
11081
  this.disableDeviceMonitoring();
11020
11082
  this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
11021
11083
  if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = (0, import_cjs$29.interval)(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
11022
- logger$29.debug("[DeviceController] Polling devices due to interval");
11084
+ logger$30.debug("[DeviceController] Polling devices due to interval");
11023
11085
  this.enumerateDevices();
11024
11086
  });
11025
11087
  this.enumerateDevices();
@@ -11045,13 +11107,13 @@ var NavigatorDeviceController = class extends Destroyable {
11045
11107
  videoinput: []
11046
11108
  });
11047
11109
  this._devicesState$.next(devicesByKind);
11048
- logger$29.debug("[DeviceController] Devices enumerated:", {
11110
+ logger$30.debug("[DeviceController] Devices enumerated:", {
11049
11111
  audioInputs: devicesByKind.audioinput.length,
11050
11112
  audioOutputs: devicesByKind.audiooutput.length,
11051
11113
  videoInputs: devicesByKind.videoinput.length
11052
11114
  });
11053
11115
  } catch (error) {
11054
- logger$29.error("[DeviceController] Failed to enumerate devices:", error);
11116
+ logger$30.error("[DeviceController] Failed to enumerate devices:", error);
11055
11117
  this._errors$.next(toError(error));
11056
11118
  }
11057
11119
  }
@@ -11067,7 +11129,7 @@ var NavigatorDeviceController = class extends Destroyable {
11067
11129
  stream.getTracks().forEach((t) => t.stop());
11068
11130
  return capabilities;
11069
11131
  } catch (error) {
11070
- logger$29.error("[DeviceController] Failed to get device capabilities:", error);
11132
+ logger$30.error("[DeviceController] Failed to get device capabilities:", error);
11071
11133
  this._errors$.next(toError(error));
11072
11134
  throw error;
11073
11135
  }
@@ -11318,7 +11380,7 @@ var DependencyContainer = class {
11318
11380
 
11319
11381
  //#endregion
11320
11382
  //#region src/controllers/CryptoController.ts
11321
- const logger$28 = getLogger();
11383
+ const logger$29 = getLogger();
11322
11384
  const DPOP_DB_NAME = "sw-dpop";
11323
11385
  const DPOP_DB_VERSION = 1;
11324
11386
  const DPOP_STORE_NAME = "keys";
@@ -11377,7 +11439,7 @@ async function loadKeyPairFromDB() {
11377
11439
  tx.oncomplete = () => db.close();
11378
11440
  });
11379
11441
  } catch (error) {
11380
- logger$28.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
11442
+ logger$29.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
11381
11443
  return null;
11382
11444
  }
11383
11445
  }
@@ -11397,7 +11459,7 @@ async function saveKeyPairToDB(keyPair) {
11397
11459
  };
11398
11460
  });
11399
11461
  } catch (error) {
11400
- logger$28.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
11462
+ logger$29.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
11401
11463
  }
11402
11464
  }
11403
11465
  async function deleteKeyPairFromDB() {
@@ -11416,7 +11478,7 @@ async function deleteKeyPairFromDB() {
11416
11478
  };
11417
11479
  });
11418
11480
  } catch (error) {
11419
- logger$28.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
11481
+ logger$29.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
11420
11482
  }
11421
11483
  }
11422
11484
  /**
@@ -11476,13 +11538,13 @@ var CryptoController = class {
11476
11538
  this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
11477
11539
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
11478
11540
  this._initialized = true;
11479
- logger$28.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
11541
+ logger$29.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
11480
11542
  return this._fingerprint;
11481
11543
  } catch (error) {
11482
- logger$28.warn("[DPoP] Stored key pair unusable, generating new one:", error);
11544
+ logger$29.warn("[DPoP] Stored key pair unusable, generating new one:", error);
11483
11545
  await deleteKeyPairFromDB();
11484
11546
  }
11485
- logger$28.debug("[DPoP] Generating RSA key pair");
11547
+ logger$29.debug("[DPoP] Generating RSA key pair");
11486
11548
  this._keyPair = await crypto.subtle.generateKey({
11487
11549
  name: "RSASSA-PKCS1-v1_5",
11488
11550
  modulusLength: 2048,
@@ -11497,7 +11559,7 @@ var CryptoController = class {
11497
11559
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
11498
11560
  this._initialized = true;
11499
11561
  await saveKeyPairToDB(this._keyPair);
11500
- logger$28.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
11562
+ logger$29.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
11501
11563
  return this._fingerprint;
11502
11564
  }
11503
11565
  /**
@@ -11563,7 +11625,7 @@ var CryptoController = class {
11563
11625
  this._fingerprint = null;
11564
11626
  this._initialized = false;
11565
11627
  deleteKeyPairFromDB();
11566
- logger$28.debug("[DPoP] Controller destroyed");
11628
+ logger$29.debug("[DPoP] Controller destroyed");
11567
11629
  }
11568
11630
  get publicJwk() {
11569
11631
  if (!this._publicJwk) throw new DPoPInitError("CryptoController not initialized. Call init() first.");
@@ -11587,7 +11649,7 @@ var CryptoController = class {
11587
11649
  //#endregion
11588
11650
  //#region src/controllers/NetworkMonitor.ts
11589
11651
  var import_cjs$28 = require_cjs();
11590
- const logger$27 = getLogger();
11652
+ const logger$28 = getLogger();
11591
11653
  /**
11592
11654
  * Safely check whether we are running in a browser environment
11593
11655
  * with `window` and the relevant event targets.
@@ -11644,7 +11706,7 @@ var NetworkMonitor = class extends Destroyable {
11644
11706
  }
11645
11707
  attachListeners() {
11646
11708
  if (!hasBrowserNetworkEvents()) {
11647
- logger$27.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
11709
+ logger$28.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
11648
11710
  return;
11649
11711
  }
11650
11712
  window.addEventListener("online", this._onOnline);
@@ -11652,7 +11714,7 @@ var NetworkMonitor = class extends Destroyable {
11652
11714
  const connection = getNetworkConnection();
11653
11715
  if (connection) connection.addEventListener("change", this._onConnectionChange);
11654
11716
  this._listenersAttached = true;
11655
- logger$27.debug("NetworkMonitor: event listeners attached");
11717
+ logger$28.debug("NetworkMonitor: event listeners attached");
11656
11718
  }
11657
11719
  removeListeners() {
11658
11720
  if (!this._listenersAttached) return;
@@ -11663,10 +11725,10 @@ var NetworkMonitor = class extends Destroyable {
11663
11725
  if (connection) connection.removeEventListener("change", this._onConnectionChange);
11664
11726
  }
11665
11727
  this._listenersAttached = false;
11666
- logger$27.debug("NetworkMonitor: event listeners removed");
11728
+ logger$28.debug("NetworkMonitor: event listeners removed");
11667
11729
  }
11668
11730
  handleOnline() {
11669
- logger$27.info("NetworkMonitor: browser went online");
11731
+ logger$28.info("NetworkMonitor: browser went online");
11670
11732
  this._isOnline$.next(true);
11671
11733
  this._networkChange$.next({
11672
11734
  type: "online",
@@ -11675,7 +11737,7 @@ var NetworkMonitor = class extends Destroyable {
11675
11737
  });
11676
11738
  }
11677
11739
  handleOffline() {
11678
- logger$27.info("NetworkMonitor: browser went offline");
11740
+ logger$28.info("NetworkMonitor: browser went offline");
11679
11741
  this._isOnline$.next(false);
11680
11742
  this._networkChange$.next({
11681
11743
  type: "offline",
@@ -11684,7 +11746,7 @@ var NetworkMonitor = class extends Destroyable {
11684
11746
  }
11685
11747
  handleConnectionChange() {
11686
11748
  const networkType = getNetworkType();
11687
- logger$27.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
11749
+ logger$28.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
11688
11750
  this._networkChange$.next({
11689
11751
  type: "connection_change",
11690
11752
  timestamp: Date.now(),
@@ -11800,7 +11862,7 @@ function getNavigatorMediaDevices() {
11800
11862
  //#endregion
11801
11863
  //#region src/controllers/PreflightRunner.ts
11802
11864
  var import_cjs$27 = require_cjs();
11803
- const logger$26 = getLogger();
11865
+ const logger$27 = getLogger();
11804
11866
  const DEFAULT_MEDIA_TEST_DURATION_S = 10;
11805
11867
  const ICE_GATHERING_TIMEOUT_MS = 1e4;
11806
11868
  const SIGNALING_RTT_TIMEOUT_MS = 5e3;
@@ -11849,7 +11911,7 @@ var PreflightRunner = class extends Destroyable {
11849
11911
  if (!this._options.skipMediaTest) try {
11850
11912
  bandwidth = await this.testMediaBandwidth(destination);
11851
11913
  } catch (error) {
11852
- logger$26.warn("[PreflightRunner] Media bandwidth test failed:", error);
11914
+ logger$27.warn("[PreflightRunner] Media bandwidth test failed:", error);
11853
11915
  warnings.push("Media bandwidth test failed");
11854
11916
  }
11855
11917
  return {
@@ -11861,7 +11923,7 @@ var PreflightRunner = class extends Destroyable {
11861
11923
  warnings
11862
11924
  };
11863
11925
  } catch (error) {
11864
- logger$26.error("[PreflightRunner] Preflight test failed:", error);
11926
+ logger$27.error("[PreflightRunner] Preflight test failed:", error);
11865
11927
  throw new PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
11866
11928
  } finally {
11867
11929
  this.destroy();
@@ -11892,7 +11954,7 @@ var PreflightRunner = class extends Destroyable {
11892
11954
  if (track.kind === "video" && track.readyState === "live") videoWorking = true;
11893
11955
  }
11894
11956
  } catch (error) {
11895
- logger$26.warn("[PreflightRunner] Device test failed:", error);
11957
+ logger$27.warn("[PreflightRunner] Device test failed:", error);
11896
11958
  } finally {
11897
11959
  if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
11898
11960
  }
@@ -11950,7 +12012,7 @@ var PreflightRunner = class extends Destroyable {
11950
12012
  rttMs
11951
12013
  };
11952
12014
  } catch (error) {
11953
- logger$26.warn("[PreflightRunner] ICE connectivity test failed:", error);
12015
+ logger$27.warn("[PreflightRunner] ICE connectivity test failed:", error);
11954
12016
  return {
11955
12017
  type: "failed",
11956
12018
  turnReachable: false,
@@ -11998,7 +12060,7 @@ var PreflightRunner = class extends Destroyable {
11998
12060
  //#endregion
11999
12061
  //#region src/controllers/VisibilityController.ts
12000
12062
  var import_cjs$26 = require_cjs();
12001
- const logger$25 = getLogger();
12063
+ const logger$26 = getLogger();
12002
12064
  /**
12003
12065
  * Checks whether the document visibility API is available.
12004
12066
  */
@@ -12035,8 +12097,8 @@ var VisibilityController = class extends Destroyable {
12035
12097
  this._boundHandler = this._handleVisibilityChange.bind(this);
12036
12098
  if (this._hasVisibilityApi) {
12037
12099
  document.addEventListener("visibilitychange", this._boundHandler);
12038
- logger$25.debug("VisibilityController: listening for visibilitychange events");
12039
- } else logger$25.debug("VisibilityController: document visibility API not available, defaulting to visible");
12100
+ logger$26.debug("VisibilityController: listening for visibilitychange events");
12101
+ } else logger$26.debug("VisibilityController: document visibility API not available, defaulting to visible");
12040
12102
  }
12041
12103
  /**
12042
12104
  * Observable of the current visibility state.
@@ -12061,7 +12123,7 @@ var VisibilityController = class extends Destroyable {
12061
12123
  destroy() {
12062
12124
  if (this._hasVisibilityApi) {
12063
12125
  document.removeEventListener("visibilitychange", this._boundHandler);
12064
- logger$25.debug("VisibilityController: removed visibilitychange listener");
12126
+ logger$26.debug("VisibilityController: removed visibilitychange listener");
12065
12127
  }
12066
12128
  super.destroy();
12067
12129
  }
@@ -12079,7 +12141,7 @@ var VisibilityController = class extends Destroyable {
12079
12141
  timestamp: Date.now()
12080
12142
  };
12081
12143
  this._visibilityChange$.next(changeEvent);
12082
- logger$25.debug("VisibilityController: visibility changed", {
12144
+ logger$26.debug("VisibilityController: visibility changed", {
12083
12145
  from: previousState,
12084
12146
  to: newState
12085
12147
  });
@@ -12154,22 +12216,17 @@ function unsafeStringify(arr, offset = 0) {
12154
12216
 
12155
12217
  //#endregion
12156
12218
  //#region ../../node_modules/uuid/dist-node/rng.js
12157
- const rnds8Pool = new Uint8Array(256);
12158
- let poolPtr = rnds8Pool.length;
12219
+ const rnds8 = new Uint8Array(16);
12159
12220
  function rng() {
12160
- if (poolPtr > rnds8Pool.length - 16) {
12161
- (0, node_crypto.randomFillSync)(rnds8Pool);
12162
- poolPtr = 0;
12163
- }
12164
- return rnds8Pool.slice(poolPtr, poolPtr += 16);
12221
+ return crypto.getRandomValues(rnds8);
12165
12222
  }
12166
12223
 
12167
- //#endregion
12168
- //#region ../../node_modules/uuid/dist-node/native.js
12169
- var native_default = { randomUUID: node_crypto.randomUUID };
12170
-
12171
12224
  //#endregion
12172
12225
  //#region ../../node_modules/uuid/dist-node/v4.js
12226
+ function v4(options, buf, offset) {
12227
+ if (!buf && !options && crypto.randomUUID) return crypto.randomUUID();
12228
+ return _v4(options, buf, offset);
12229
+ }
12173
12230
  function _v4(options, buf, offset) {
12174
12231
  options = options || {};
12175
12232
  const rnds = options.random ?? options.rng?.() ?? rng();
@@ -12184,10 +12241,6 @@ function _v4(options, buf, offset) {
12184
12241
  }
12185
12242
  return unsafeStringify(rnds);
12186
12243
  }
12187
- function v4(options, buf, offset) {
12188
- if (native_default.randomUUID && !buf && !options) return native_default.randomUUID();
12189
- return _v4(options, buf, offset);
12190
- }
12191
12244
  var v4_default = v4;
12192
12245
 
12193
12246
  //#endregion
@@ -12329,7 +12382,7 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
12329
12382
 
12330
12383
  //#endregion
12331
12384
  //#region src/managers/AttachManager.ts
12332
- const logger$24 = getLogger();
12385
+ const logger$25 = getLogger();
12333
12386
  var AttachManager = class {
12334
12387
  constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
12335
12388
  this.storage = storage;
@@ -12350,7 +12403,7 @@ var AttachManager = class {
12350
12403
  try {
12351
12404
  return await this.storage.getItem(this.attachKey) ?? {};
12352
12405
  } catch (error) {
12353
- logger$24.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
12406
+ logger$25.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
12354
12407
  return {};
12355
12408
  }
12356
12409
  }
@@ -12358,7 +12411,7 @@ var AttachManager = class {
12358
12411
  try {
12359
12412
  await this.storage.setItem(this.attachKey, attached);
12360
12413
  } catch (error) {
12361
- logger$24.warn("[AttachManager] Failed to write attached calls to storage", error);
12414
+ logger$25.warn("[AttachManager] Failed to write attached calls to storage", error);
12362
12415
  }
12363
12416
  }
12364
12417
  /**
@@ -12377,7 +12430,7 @@ var AttachManager = class {
12377
12430
  }
12378
12431
  async attach(call) {
12379
12432
  if (!call.to) {
12380
- logger$24.warn("[AttachManager] Skip attach for calls with no destination");
12433
+ logger$25.warn("[AttachManager] Skip attach for calls with no destination");
12381
12434
  return;
12382
12435
  }
12383
12436
  const destination = call.to;
@@ -12430,15 +12483,15 @@ var AttachManager = class {
12430
12483
  callId,
12431
12484
  ...options
12432
12485
  });
12433
- logger$24.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
12486
+ logger$25.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
12434
12487
  succeeded = true;
12435
12488
  break;
12436
12489
  } catch (error) {
12437
- logger$24.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
12490
+ logger$25.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
12438
12491
  if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
12439
12492
  }
12440
12493
  if (!succeeded) {
12441
- logger$24.warn(`[AttachManager] Reattach failed after 3 attempts for call ${callId}, removing reference`);
12494
+ logger$25.warn(`[AttachManager] Reattach failed after 3 attempts for call ${callId}, removing reference`);
12442
12495
  await this.detach({
12443
12496
  id: callId,
12444
12497
  mediaDirections: attachment.mediaDirections
@@ -13375,7 +13428,9 @@ const DEFAULT_MEMBER_CAPABILITIES = {
13375
13428
  position: false,
13376
13429
  meta: false,
13377
13430
  remove: false,
13378
- audioFlags: false
13431
+ audioFlags: false,
13432
+ denoise: false,
13433
+ lowbitrate: false
13379
13434
  };
13380
13435
  /**
13381
13436
  * Default call capabilities with no permissions
@@ -13448,7 +13503,9 @@ function computeMemberCapabilities(flags, memberType) {
13448
13503
  position: hasBooleanCapability(flags, memberType, null, "position"),
13449
13504
  meta: hasBooleanCapability(flags, memberType, null, "meta"),
13450
13505
  remove: hasBooleanCapability(flags, memberType, null, "remove"),
13451
- audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags")
13506
+ audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags"),
13507
+ denoise: hasBooleanCapability(flags, memberType, null, "denoise"),
13508
+ lowbitrate: hasBooleanCapability(flags, memberType, null, "lowbitrate")
13452
13509
  };
13453
13510
  }
13454
13511
  /**
@@ -13605,7 +13662,7 @@ function toggleHandraiseMethod(is) {
13605
13662
 
13606
13663
  //#endregion
13607
13664
  //#region src/core/entities/Participant.ts
13608
- const logger$23 = getLogger();
13665
+ const logger$24 = getLogger();
13609
13666
  const initialState = {};
13610
13667
  /**
13611
13668
  * Represents a participant in a call.
@@ -13615,9 +13672,9 @@ const initialState = {};
13615
13672
  * the local participant with additional device control.
13616
13673
  */
13617
13674
  var Participant = class extends Destroyable {
13618
- constructor(id, executeMethod, deviceController) {
13675
+ constructor(id, callExecuteMethod, deviceController) {
13619
13676
  super();
13620
- this.executeMethod = executeMethod;
13677
+ this.callExecuteMethod = callExecuteMethod;
13621
13678
  this.deviceController = deviceController;
13622
13679
  this._state$ = this.createBehaviorSubject(initialState);
13623
13680
  this.id = id;
@@ -13831,26 +13888,63 @@ var Participant = class extends Destroyable {
13831
13888
  get nodeId() {
13832
13889
  return this._state$.value.node_id;
13833
13890
  }
13891
+ /** Call ID for this participant's leg, or `undefined` if not available. */
13892
+ get callId() {
13893
+ return this._state$.value.call_id;
13894
+ }
13834
13895
  /** @internal */
13835
13896
  get value() {
13836
13897
  return this._state$.value;
13837
13898
  }
13899
+ /**
13900
+ * Target triple for member RPCs, built from the participant's own state.
13901
+ * The backend locates the member's session by the target `call_id`/`node_id`,
13902
+ * so this must always be the participant's own call context — never the
13903
+ * local call's id (issue #19400).
13904
+ *
13905
+ * Reading it doubles as a readiness probe: it throws until the first full
13906
+ * member event (`member.joined`/`member.updated` or the `call.joined`
13907
+ * roster) arrives, and never regresses afterwards.
13908
+ *
13909
+ * @throws {ParticipantNotReadyError} If the member state has not been
13910
+ * received yet (e.g. a participant first seen via `member.talking`) — an
13911
+ * empty call context can never address the member, so fail fast instead of
13912
+ * sending a doomed RPC.
13913
+ */
13914
+ get target() {
13915
+ const { call_id, node_id } = this._state$.value;
13916
+ if (!call_id || !node_id) throw new ParticipantNotReadyError(this.id);
13917
+ return {
13918
+ member_id: this.id,
13919
+ call_id,
13920
+ node_id
13921
+ };
13922
+ }
13923
+ /**
13924
+ * Executes a member RPC against this participant, injecting its own
13925
+ * {@link target} as the target.
13926
+ *
13927
+ * @throws {ParticipantNotReadyError} Via {@link target}, when the
13928
+ * member state has not been received yet.
13929
+ */
13930
+ async executeMethod(method, args) {
13931
+ return this.callExecuteMethod(this.target, method, args);
13932
+ }
13838
13933
  /** Toggles the deafened state (mutes/unmutes incoming audio). */
13839
13934
  async toggleDeaf() {
13840
- const method = toggleDeafMethod(this.deaf);
13841
- await this.executeMethod(this.id, method, {});
13935
+ await this.executeMethod(toggleDeafMethod(this.deaf), {});
13842
13936
  }
13843
13937
  /** Toggles the hand-raised state. */
13844
13938
  async toggleHandraise() {
13845
- await this.executeMethod(this.id, toggleHandraiseMethod(this.handraised), {});
13939
+ await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
13846
13940
  }
13847
13941
  /** Mutes the participant's audio. */
13848
13942
  async mute() {
13849
- await this.executeMethod(this.id, "call.mute", { channels: ["audio"] });
13943
+ await this.executeMethod("call.mute", { channels: ["audio"] });
13850
13944
  }
13851
13945
  /** Unmutes the participant's audio. */
13852
13946
  async unmute() {
13853
- await this.executeMethod(this.id, "call.unmute", { channels: ["audio"] });
13947
+ await this.executeMethod("call.unmute", { channels: ["audio"] });
13854
13948
  }
13855
13949
  /** Toggles the participant's audio mute state. */
13856
13950
  async toggleMute() {
@@ -13858,11 +13952,11 @@ var Participant = class extends Destroyable {
13858
13952
  }
13859
13953
  /** Mutes the participant's video. */
13860
13954
  async muteVideo() {
13861
- await this.executeMethod(this.id, "call.mute", { channels: ["video"] });
13955
+ await this.executeMethod("call.mute", { channels: ["video"] });
13862
13956
  }
13863
13957
  /** Unmutes the participant's video. */
13864
13958
  async unmuteVideo() {
13865
- await this.executeMethod(this.id, "call.unmute", { channels: ["video"] });
13959
+ await this.executeMethod("call.unmute", { channels: ["video"] });
13866
13960
  }
13867
13961
  /** Toggles the participant's video mute state. */
13868
13962
  async toggleMuteVideo() {
@@ -13870,7 +13964,7 @@ var Participant = class extends Destroyable {
13870
13964
  }
13871
13965
  /** Toggles echo cancellation on the audio input. */
13872
13966
  async toggleEchoCancellation() {
13873
- await this.executeMethod(this.id, "call.audioflags.set", {
13967
+ await this.executeMethod("call.audioflags.set", {
13874
13968
  echo_cancellation: !this.echoCancellation,
13875
13969
  auto_gain: this.autoGain,
13876
13970
  noise_suppression: this.noiseSuppression
@@ -13878,7 +13972,7 @@ var Participant = class extends Destroyable {
13878
13972
  }
13879
13973
  /** Toggles automatic gain control on the audio input. */
13880
13974
  async toggleAudioInputAutoGain() {
13881
- await this.executeMethod(this.id, "call.audioflags.set", {
13975
+ await this.executeMethod("call.audioflags.set", {
13882
13976
  echo_cancellation: this.echoCancellation,
13883
13977
  auto_gain: !this.autoGain,
13884
13978
  noise_suppression: this.noiseSuppression
@@ -13886,14 +13980,15 @@ var Participant = class extends Destroyable {
13886
13980
  }
13887
13981
  /** Toggles noise suppression on the audio input. */
13888
13982
  async toggleNoiseSuppression() {
13889
- await this.executeMethod(this.id, "call.audioflags.set", {
13983
+ await this.executeMethod("call.audioflags.set", {
13890
13984
  echo_cancellation: this.echoCancellation,
13891
13985
  auto_gain: this.autoGain,
13892
13986
  noise_suppression: !this.noiseSuppression
13893
13987
  });
13894
13988
  }
13989
+ /** Toggles low-bitrate mode for this participant's media. */
13895
13990
  async toggleLowbitrate() {
13896
- throw new UnimplementedError();
13991
+ await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
13897
13992
  }
13898
13993
  /**
13899
13994
  * Adjusts the **conference-only** microphone energy gate / sensitivity level
@@ -13909,7 +14004,7 @@ var Participant = class extends Destroyable {
13909
14004
  * (integer, larger values are more sensitive).
13910
14005
  */
13911
14006
  async setAudioInputSensitivity(value) {
13912
- await this.executeMethod(this.id, "call.microphone.sensitivity.set", { sensitivity: value });
14007
+ await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
13913
14008
  }
13914
14009
  /**
13915
14010
  * Sets the **server-side** microphone volume on this participant's bridged
@@ -13922,7 +14017,7 @@ var Participant = class extends Destroyable {
13922
14017
  * @param value - Volume level (0-100).
13923
14018
  */
13924
14019
  async setAudioInputVolume(value) {
13925
- await this.executeMethod(this.id, "call.microphone.volume.set", { volume: value });
14020
+ await this.executeMethod("call.microphone.volume.set", { volume: value });
13926
14021
  }
13927
14022
  /**
13928
14023
  * Sets the **server-side** speaker volume on this participant's bridged call
@@ -13936,28 +14031,31 @@ var Participant = class extends Destroyable {
13936
14031
  * @param value - Volume level (0-100).
13937
14032
  */
13938
14033
  async setAudioOutputVolume(value) {
13939
- await this.executeMethod(this.id, "call.speaker.volume.set", { volume: value });
14034
+ await this.executeMethod("call.speaker.volume.set", { volume: value });
13940
14035
  }
13941
14036
  /**
13942
14037
  * Sets the participant's position in the video layout.
14038
+ *
14039
+ * Requires the `member.position` capability. The gateway requires a
14040
+ * `targets` array of `{ target, position }` entries (issue #19400). A
14041
+ * resolved promise does not guarantee a visible change: the backend silently
14042
+ * returns `200` (no-op) for non-conference targets.
14043
+ *
13943
14044
  * @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
13944
14045
  */
13945
14046
  async setPosition(value) {
13946
- await this.executeMethod(this.id, "call.member.position.set", { position: value });
14047
+ await this.executeMethod("call.member.position.set", { targets: [{
14048
+ target: this.target,
14049
+ position: value
14050
+ }] });
13947
14051
  }
13948
14052
  /** Removes this participant from the call. */
13949
14053
  async remove() {
13950
- const state = this._state$.value;
13951
- const target = {
13952
- member_id: this.id,
13953
- call_id: state.call_id ?? "",
13954
- node_id: state.node_id ?? ""
13955
- };
13956
- await this.executeMethod(target, "call.member.remove", {});
14054
+ await this.executeMethod("call.member.remove", { targets: [this.target] });
13957
14055
  }
13958
14056
  /** Ends the call for this participant. */
13959
14057
  async end() {
13960
- await this.executeMethod(this.id, "call.end", {});
14058
+ await this.executeMethod("call.end", {});
13961
14059
  }
13962
14060
  /**
13963
14061
  * Replaces custom metadata for this participant.
@@ -13977,7 +14075,7 @@ var Participant = class extends Destroyable {
13977
14075
  }
13978
14076
  /** Destroys the participant, releasing all subscriptions and references. */
13979
14077
  destroy() {
13980
- this.executeMethod = void 0;
14078
+ this.callExecuteMethod = void 0;
13981
14079
  super.destroy();
13982
14080
  }
13983
14081
  };
@@ -13989,8 +14087,8 @@ var Participant = class extends Destroyable {
13989
14087
  */
13990
14088
  var SelfParticipant = class extends Participant {
13991
14089
  /** @internal */
13992
- constructor(id, executeMethod, vertoManager, deviceController) {
13993
- super(id, executeMethod, deviceController);
14090
+ constructor(id, callExecuteMethod, vertoManager, deviceController) {
14091
+ super(id, callExecuteMethod, deviceController);
13994
14092
  this.vertoManager = vertoManager;
13995
14093
  this._studioAudio$ = this.createBehaviorSubject(false);
13996
14094
  this.capabilities = new SelfCapabilities();
@@ -14014,7 +14112,7 @@ var SelfParticipant = class extends Participant {
14014
14112
  async enableStudioAudio() {
14015
14113
  if (this._studioAudio$.value) return;
14016
14114
  this._studioAudio$.next(true);
14017
- await this.executeMethod(this.id, "call.audioflags.set", {
14115
+ await this.executeMethod("call.audioflags.set", {
14018
14116
  echo_cancellation: false,
14019
14117
  auto_gain: false,
14020
14118
  noise_suppression: false
@@ -14027,18 +14125,27 @@ var SelfParticipant = class extends Participant {
14027
14125
  async disableStudioAudio() {
14028
14126
  if (!this._studioAudio$.value) return;
14029
14127
  this._studioAudio$.next(false);
14030
- await this.executeMethod(this.id, "call.audioflags.set", {
14128
+ await this.executeMethod("call.audioflags.set", {
14031
14129
  echo_cancellation: true,
14032
14130
  auto_gain: true,
14033
14131
  noise_suppression: true
14034
14132
  });
14035
14133
  }
14036
- /** Starts sharing the local screen. */
14134
+ /**
14135
+ * Starts sharing the local screen.
14136
+ *
14137
+ * The call is unaffected when acquisition fails.
14138
+ *
14139
+ * @throws The raw `getDisplayMedia` error. A dismissed picker or a
14140
+ * permission denial rejects with a `NotAllowedError` `DOMException` —
14141
+ * inspect `error.name` to tell benign cancels apart from real failures.
14142
+ */
14037
14143
  async startScreenShare() {
14038
14144
  try {
14039
14145
  await this.vertoManager.addScreenMedia();
14040
14146
  } catch (error) {
14041
- logger$23.error("[Participant.startScreenShare] Screen share error:", error);
14147
+ logger$24.error("[Participant.startScreenShare] Screen share error:", error);
14148
+ throw error;
14042
14149
  }
14043
14150
  }
14044
14151
  /** Observable of the current screen share status. */
@@ -14053,12 +14160,20 @@ var SelfParticipant = class extends Participant {
14053
14160
  async stopScreenShare() {
14054
14161
  return this.vertoManager.removeScreenMedia();
14055
14162
  }
14056
- /** Adds an additional media input device to the call. */
14163
+ /**
14164
+ * Adds an additional media input device to the call.
14165
+ *
14166
+ * The call is unaffected when acquisition fails.
14167
+ *
14168
+ * @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
14169
+ * permission denial) — inspect `error.name` to decide how to react.
14170
+ */
14057
14171
  async addAdditionalDevice(options) {
14058
14172
  try {
14059
14173
  await this.vertoManager.addInputDevice(options);
14060
14174
  } catch (error) {
14061
- logger$23.error("[Participant.startScreenShare] Screen share error:", error);
14175
+ logger$24.error("[Participant.addAdditionalDevice] Additional device error:", error);
14176
+ throw error;
14062
14177
  }
14063
14178
  }
14064
14179
  /** Removes an additional media input device by ID. */
@@ -14120,7 +14235,7 @@ var SelfParticipant = class extends Participant {
14120
14235
  */
14121
14236
  exitStudioModeIfActive() {
14122
14237
  if (this._studioAudio$.value) {
14123
- logger$23.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
14238
+ logger$24.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
14124
14239
  this._studioAudio$.next(false);
14125
14240
  }
14126
14241
  }
@@ -14144,7 +14259,7 @@ var SelfParticipant = class extends Participant {
14144
14259
  try {
14145
14260
  await super.mute();
14146
14261
  } catch (error) {
14147
- logger$23.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
14262
+ logger$24.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
14148
14263
  } finally {
14149
14264
  this.vertoManager.muteMainAudioInputDevice();
14150
14265
  }
@@ -14154,7 +14269,7 @@ var SelfParticipant = class extends Participant {
14154
14269
  try {
14155
14270
  await super.unmute();
14156
14271
  } catch (error) {
14157
- logger$23.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
14272
+ logger$24.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
14158
14273
  } finally {
14159
14274
  await this.vertoManager.unmuteMainAudioInputDevice();
14160
14275
  }
@@ -14164,7 +14279,7 @@ var SelfParticipant = class extends Participant {
14164
14279
  try {
14165
14280
  await super.muteVideo();
14166
14281
  } catch (error) {
14167
- logger$23.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
14282
+ logger$24.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
14168
14283
  } finally {
14169
14284
  this.vertoManager.muteMainVideoInputDevice();
14170
14285
  }
@@ -14174,7 +14289,7 @@ var SelfParticipant = class extends Participant {
14174
14289
  try {
14175
14290
  await super.unmuteVideo();
14176
14291
  } catch (error) {
14177
- logger$23.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
14292
+ logger$24.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
14178
14293
  } finally {
14179
14294
  await this.vertoManager.unmuteMainVideoInputDevice();
14180
14295
  }
@@ -14379,7 +14494,7 @@ function filterAs(predicate, resultPath) {
14379
14494
  //#endregion
14380
14495
  //#region src/operators/throwOnRPCError.ts
14381
14496
  var import_cjs$21 = require_cjs();
14382
- const logger$22 = getLogger();
14497
+ const logger$23 = getLogger();
14383
14498
  /**
14384
14499
  * RxJS operator that throws a {@link JSONRPCError} when the RPC response contains an error.
14385
14500
  * Passes successful responses through unchanged.
@@ -14387,14 +14502,14 @@ const logger$22 = getLogger();
14387
14502
  function throwOnRPCError() {
14388
14503
  return (0, import_cjs$21.map)((response) => {
14389
14504
  if (response.error) {
14390
- logger$22.error("[throwOnRPCError] RPC error response:", {
14505
+ logger$23.error("[throwOnRPCError] RPC error response:", {
14391
14506
  code: response.error.code,
14392
14507
  message: response.error.message,
14393
14508
  data: response.error.data
14394
14509
  });
14395
14510
  throw new JSONRPCError(response.error.code, response.error.message, response.error.data);
14396
14511
  }
14397
- logger$22.debug("[throwOnRPCError] RPC successful response:", response);
14512
+ logger$23.debug("[throwOnRPCError] RPC successful response:", response);
14398
14513
  return response;
14399
14514
  });
14400
14515
  }
@@ -14402,7 +14517,7 @@ function throwOnRPCError() {
14402
14517
  //#endregion
14403
14518
  //#region src/managers/CallEventsManager.ts
14404
14519
  var import_cjs$20 = require_cjs();
14405
- const logger$21 = getLogger();
14520
+ const logger$22 = getLogger();
14406
14521
  const initialSessionState = {};
14407
14522
  /** @internal */
14408
14523
  var CallEventsManager = class extends Destroyable {
@@ -14506,7 +14621,7 @@ var CallEventsManager = class extends Destroyable {
14506
14621
  }
14507
14622
  initSubscriptions() {
14508
14623
  this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
14509
- logger$21.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
14624
+ logger$22.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
14510
14625
  callId: callJoinedEvent.call_id,
14511
14626
  roomSessionId: callJoinedEvent.room_session_id
14512
14627
  });
@@ -14533,19 +14648,19 @@ var CallEventsManager = class extends Destroyable {
14533
14648
  if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
14534
14649
  });
14535
14650
  this.subscribeTo(this.memberUpdates$, (member) => {
14536
- logger$21.debug("[CallEventsManager] Handling member update event for member ID:", member);
14651
+ logger$22.debug("[CallEventsManager] Handling member update event for member ID:", member);
14537
14652
  this.upsertParticipant(member);
14538
14653
  });
14539
14654
  this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
14540
- logger$21.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
14655
+ logger$22.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
14541
14656
  const participants = { ...this._participants$.value };
14542
14657
  if (memberLeftEvent.member.member_id in participants) {
14543
14658
  delete participants[memberLeftEvent.member.member_id];
14544
14659
  this._participants$.next(participants);
14545
- } else logger$21.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
14660
+ } else logger$22.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
14546
14661
  });
14547
14662
  this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
14548
- logger$21.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
14663
+ logger$22.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
14549
14664
  const roomSession = callUpdatedEvent.room_session;
14550
14665
  this._sessionState$.next({
14551
14666
  ...this._sessionState$.value,
@@ -14560,7 +14675,7 @@ var CallEventsManager = class extends Destroyable {
14560
14675
  });
14561
14676
  });
14562
14677
  this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
14563
- logger$21.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
14678
+ logger$22.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
14564
14679
  this._sessionState$.next({
14565
14680
  ...this._sessionState$.value,
14566
14681
  layout_name: layoutChangedEvent.id,
@@ -14570,10 +14685,10 @@ var CallEventsManager = class extends Destroyable {
14570
14685
  });
14571
14686
  }
14572
14687
  updateParticipantPositions(layoutChangedEvent) {
14573
- if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$21.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
14688
+ if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$22.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
14574
14689
  layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
14575
14690
  if (!(layer.member_id in this._participants$.value)) {
14576
- logger$21.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
14691
+ logger$22.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
14577
14692
  return false;
14578
14693
  }
14579
14694
  return true;
@@ -14596,7 +14711,7 @@ var CallEventsManager = class extends Destroyable {
14596
14711
  layouts: response.result.layouts
14597
14712
  });
14598
14713
  }).catch((error) => {
14599
- logger$21.error("[CallEventsManager] Error fetching layouts:", error);
14714
+ logger$22.error("[CallEventsManager] Error fetching layouts:", error);
14600
14715
  });
14601
14716
  }
14602
14717
  updateParticipants(members) {
@@ -14612,7 +14727,7 @@ var CallEventsManager = class extends Destroyable {
14612
14727
  }
14613
14728
  const participant = this._participants$.value[member.member_id];
14614
14729
  const oldValue = participant.value;
14615
- logger$21.debug("[CallEventsManager] Updating participant:", member.member_id, {
14730
+ logger$22.debug("[CallEventsManager] Updating participant:", member.member_id, {
14616
14731
  oldValue,
14617
14732
  newValue: member
14618
14733
  });
@@ -14625,17 +14740,17 @@ var CallEventsManager = class extends Destroyable {
14625
14740
  }
14626
14741
  get callJoinedEvent$() {
14627
14742
  return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, import_cjs$20.filter)(isCallJoinedPayload), (0, import_cjs$20.tap)((event) => {
14628
- logger$21.debug("[CallEventsManager] Call joined event:", event);
14743
+ logger$22.debug("[CallEventsManager] Call joined event:", event);
14629
14744
  })));
14630
14745
  }
14631
14746
  get layoutChangedEvent$() {
14632
14747
  return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filterAs(isLayoutChangedPayload, "layout"), (0, import_cjs$20.tap)((event) => {
14633
- logger$21.debug("[CallEventsManager] Layout changed event:", event);
14748
+ logger$22.debug("[CallEventsManager] Layout changed event:", event);
14634
14749
  })));
14635
14750
  }
14636
14751
  get memberUpdates$() {
14637
14752
  return this.cachedObservable("memberUpdates$", () => (0, import_cjs$20.merge)(this.webRtcCallSession.memberJoined$, this.webRtcCallSession.memberUpdated$, this.webRtcCallSession.memberTalking$).pipe((0, import_cjs$20.map)((event) => event.member), (0, import_cjs$20.tap)((event) => {
14638
- logger$21.debug("[CallEventsManager] Member update event:", event);
14753
+ logger$22.debug("[CallEventsManager] Member update event:", event);
14639
14754
  })));
14640
14755
  }
14641
14756
  destroy() {
@@ -14892,7 +15007,7 @@ function appendStereoParams(fmtpLine, maxBitrate) {
14892
15007
  //#endregion
14893
15008
  //#region src/controllers/ICEGatheringController.ts
14894
15009
  var import_cjs$19 = require_cjs();
14895
- const logger$20 = getLogger();
15010
+ const logger$21 = getLogger();
14896
15011
  var ICEGatheringController = class extends Destroyable {
14897
15012
  constructor(peerConnection, peerConnectionControllerNegotiating$, options = {}) {
14898
15013
  super();
@@ -14900,23 +15015,23 @@ var ICEGatheringController = class extends Destroyable {
14900
15015
  this.peerConnectionControllerNegotiating$ = peerConnectionControllerNegotiating$;
14901
15016
  this.onicegatheringstatechangeHandler = () => {
14902
15017
  const { iceGatheringState } = this.peerConnection;
14903
- logger$20.debug(`[ICEGatheringController] ICE gathering state changed to: ${iceGatheringState}`);
15018
+ logger$21.debug(`[ICEGatheringController] ICE gathering state changed to: ${iceGatheringState}`);
14904
15019
  if (iceGatheringState === "gathering") this._iceCandidatesState.next({
14905
15020
  state: "gathering",
14906
15021
  validSDP: false
14907
15022
  });
14908
15023
  };
14909
15024
  this.onicecandidateHandler = (event) => {
14910
- logger$20.debug("[ICEGatheringController] ICE candidate event received:", event.candidate);
15025
+ logger$21.debug("[ICEGatheringController] ICE candidate event received:", event.candidate);
14911
15026
  this.removeTimer("iceCandidateTimer");
14912
15027
  if (event.candidate) this.iceCandidateTimer = setTimeout(() => {
14913
15028
  if (this.peerConnection.iceGatheringState !== "complete") {
14914
- logger$20.warn("[ICEGatheringController] ICE candidate timeout, using current SDP");
15029
+ logger$21.warn("[ICEGatheringController] ICE candidate timeout, using current SDP");
14915
15030
  this.handleICECandidateTimeout();
14916
15031
  }
14917
15032
  }, this.iceCandidateTimeout);
14918
15033
  else {
14919
- logger$20.debug("[ICEGatheringController] ICE gathering completed: null candidate received");
15034
+ logger$21.debug("[ICEGatheringController] ICE gathering completed: null candidate received");
14920
15035
  this.removeTimer("iceGatheringTimer");
14921
15036
  this.handleICEGatheringComplete();
14922
15037
  }
@@ -14934,7 +15049,7 @@ var ICEGatheringController = class extends Destroyable {
14934
15049
  this.setupEventListeners();
14935
15050
  this.iceGatheringTimer = setTimeout(() => {
14936
15051
  if (this.peerConnection.iceGatheringState !== "complete") {
14937
- logger$20.warn("[ICEGatheringController] ICE gathering timeout, using current SDP");
15052
+ logger$21.warn("[ICEGatheringController] ICE gathering timeout, using current SDP");
14938
15053
  this.handleICEGatheringTimeout();
14939
15054
  }
14940
15055
  }, this.iceGatheringTimeout);
@@ -14961,9 +15076,9 @@ var ICEGatheringController = class extends Destroyable {
14961
15076
  this.relayOnly = value;
14962
15077
  }
14963
15078
  handleICEGatheringComplete() {
14964
- logger$20.debug("[ICEGatheringController] Handling ICE gathering complete");
14965
- logger$20.debug(`[ICEGatheringController] Checking ICE gathering state: ${this.peerConnection.iceGatheringState}`);
14966
- logger$20.debug("[ICEGatheringController] ICE gathering complete");
15079
+ logger$21.debug("[ICEGatheringController] Handling ICE gathering complete");
15080
+ logger$21.debug(`[ICEGatheringController] Checking ICE gathering state: ${this.peerConnection.iceGatheringState}`);
15081
+ logger$21.debug("[ICEGatheringController] ICE gathering complete");
14967
15082
  this._iceCandidatesState.next({
14968
15083
  state: "complete",
14969
15084
  validSDP: this.hasValidLocalDescriptionSDP
@@ -14979,21 +15094,21 @@ var ICEGatheringController = class extends Destroyable {
14979
15094
  this.removeTimer("iceGatheringTimer");
14980
15095
  const validSDP = this.hasValidLocalDescriptionSDP;
14981
15096
  if (validSDP) {
14982
- logger$20.debug("[ICEGatheringController] Local SDP is valid");
15097
+ logger$21.debug("[ICEGatheringController] Local SDP is valid");
14983
15098
  this._iceCandidatesState.next({
14984
15099
  state: "timeout",
14985
15100
  validSDP
14986
15101
  });
14987
15102
  this.stopGathering();
14988
- } else logger$20.debug("### ICE gathering timeout\n", this.peerConnection.localDescription?.sdp);
15103
+ } else logger$21.debug("### ICE gathering timeout\n", this.peerConnection.localDescription?.sdp);
14989
15104
  }
14990
15105
  handleICECandidateTimeout() {
14991
15106
  if (this.iceCandidateTimer) this.removeTimer("iceCandidateTimer");
14992
- logger$20.warn("[ICEGatheringController] ICE candidate timeout");
15107
+ logger$21.warn("[ICEGatheringController] ICE candidate timeout");
14993
15108
  const validSDP = this.hasValidLocalDescriptionSDP;
14994
15109
  if (!validSDP && !this.relayOnly) this.restartICEGatheringWithRelayOnly();
14995
15110
  else {
14996
- logger$20.debug("[ICEGatheringController] Using current SDP due to ICE candidate timeout");
15111
+ logger$21.debug("[ICEGatheringController] Using current SDP due to ICE candidate timeout");
14997
15112
  this._iceCandidatesState.next({
14998
15113
  state: "timeout",
14999
15114
  validSDP
@@ -15002,7 +15117,7 @@ var ICEGatheringController = class extends Destroyable {
15002
15117
  }
15003
15118
  }
15004
15119
  restartICEGatheringWithRelayOnly() {
15005
- logger$20.debug("[ICEGatheringController] Restarting ICE gathering with relay-only candidates");
15120
+ logger$21.debug("[ICEGatheringController] Restarting ICE gathering with relay-only candidates");
15006
15121
  this.relayOnly = true;
15007
15122
  this.peerConnection.setConfiguration({
15008
15123
  ...this.peerConnection.getConfiguration(),
@@ -15017,7 +15132,7 @@ var ICEGatheringController = class extends Destroyable {
15017
15132
  }
15018
15133
  }
15019
15134
  clearAllTimers() {
15020
- logger$20.debug("[ICEGatheringController] Clearing all timers");
15135
+ logger$21.debug("[ICEGatheringController] Clearing all timers");
15021
15136
  this.removeTimer("iceGatheringTimer");
15022
15137
  this.removeTimer("iceCandidateTimer");
15023
15138
  }
@@ -15026,7 +15141,7 @@ var ICEGatheringController = class extends Destroyable {
15026
15141
  this.peerConnection.removeEventListener("icecandidate", this.onicecandidateHandler);
15027
15142
  }
15028
15143
  destroy() {
15029
- logger$20.debug("[ICEGatheringController] Destroying ICEGatheringController");
15144
+ logger$21.debug("[ICEGatheringController] Destroying ICEGatheringController");
15030
15145
  this.clearAllTimers();
15031
15146
  this.removeEventListeners();
15032
15147
  super.destroy();
@@ -15036,7 +15151,7 @@ var ICEGatheringController = class extends Destroyable {
15036
15151
  //#endregion
15037
15152
  //#region src/controllers/LocalAudioPipeline.ts
15038
15153
  var import_cjs$18 = require_cjs();
15039
- const logger$19 = getLogger();
15154
+ const logger$20 = getLogger();
15040
15155
  /**
15041
15156
  * Web Audio pipeline for the local microphone stream.
15042
15157
  *
@@ -15148,7 +15263,7 @@ var LocalAudioPipeline = class extends Destroyable {
15148
15263
  try {
15149
15264
  this._inputSource.disconnect();
15150
15265
  } catch (error) {
15151
- logger$19.debug("[LocalAudioPipeline] input disconnect warning:", error);
15266
+ logger$20.debug("[LocalAudioPipeline] input disconnect warning:", error);
15152
15267
  }
15153
15268
  this._inputSource = null;
15154
15269
  }
@@ -15158,7 +15273,7 @@ var LocalAudioPipeline = class extends Destroyable {
15158
15273
  this._inputSource = this._audioContext.createMediaStreamSource(this._inputStream);
15159
15274
  this._inputSource.connect(this._gainNode);
15160
15275
  if (this._audioContext.state === "suspended") this._audioContext.resume().catch((error) => {
15161
- logger$19.warn("[LocalAudioPipeline] AudioContext resume failed:", error);
15276
+ logger$20.warn("[LocalAudioPipeline] AudioContext resume failed:", error);
15162
15277
  });
15163
15278
  }
15164
15279
  destroy() {
@@ -15173,7 +15288,7 @@ var LocalAudioPipeline = class extends Destroyable {
15173
15288
  this._analyser.disconnect();
15174
15289
  } catch {}
15175
15290
  this._audioContext.close().catch((error) => {
15176
- logger$19.debug("[LocalAudioPipeline] audio context close warning:", error);
15291
+ logger$20.debug("[LocalAudioPipeline] audio context close warning:", error);
15177
15292
  });
15178
15293
  super.destroy();
15179
15294
  }
@@ -15200,7 +15315,7 @@ var LocalAudioPipeline = class extends Destroyable {
15200
15315
  //#endregion
15201
15316
  //#region src/controllers/LocalStreamController.ts
15202
15317
  var import_cjs$17 = require_cjs();
15203
- const logger$18 = getLogger();
15318
+ const logger$19 = getLogger();
15204
15319
  var LocalStreamController = class extends Destroyable {
15205
15320
  constructor(options) {
15206
15321
  super();
@@ -15238,28 +15353,30 @@ var LocalStreamController = class extends Destroyable {
15238
15353
  * Build the local media stream based on the provided options.
15239
15354
  */
15240
15355
  async buildLocalStream() {
15241
- logger$18.debug("[LocalStreamController] Building local media stream.");
15356
+ logger$19.debug("[LocalStreamController] Building local media stream.");
15242
15357
  let stream;
15243
15358
  if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
15244
15359
  const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
15245
15360
  stream = new MediaStream(tracks);
15246
15361
  } else if (this.options.propose === "screenshare") {
15247
- logger$18.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
15362
+ logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
15248
15363
  stream = await this.options.getDisplayMedia({
15249
15364
  video: true,
15250
15365
  audio: Boolean(this.options.inputAudioDeviceConstraints)
15251
15366
  });
15252
- logger$18.debug("[LocalStreamController] Screen share media obtained:", stream);
15367
+ logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
15253
15368
  } else {
15254
15369
  const constraints = {
15255
15370
  audio: this.options.inputAudioDeviceConstraints,
15256
15371
  video: this.options.inputVideoDeviceConstraints
15257
15372
  };
15258
- logger$18.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
15373
+ logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
15259
15374
  stream = await this.options.getUserMedia(constraints);
15260
- logger$18.debug("[LocalStreamController] User media obtained:", stream);
15375
+ logger$19.debug("[LocalStreamController] User media obtained:", stream);
15261
15376
  }
15262
15377
  this._localStream$.next(stream);
15378
+ this._localAudioTracks$.next(stream.getAudioTracks());
15379
+ this._localVideoTracks$.next(stream.getVideoTracks());
15263
15380
  return stream;
15264
15381
  }
15265
15382
  /**
@@ -15274,7 +15391,7 @@ var LocalStreamController = class extends Destroyable {
15274
15391
  this._localStream$.next(localStream);
15275
15392
  if (track.kind === "video") this._localVideoTracks$.next(localStream.getVideoTracks());
15276
15393
  else this._localAudioTracks$.next(localStream.getAudioTracks());
15277
- logger$18.debug(`[LocalStreamController] ${track.kind} track added:`, track.id);
15394
+ logger$19.debug(`[LocalStreamController] ${track.kind} track added:`, track.id);
15278
15395
  return localStream;
15279
15396
  }
15280
15397
  /**
@@ -15286,7 +15403,7 @@ var LocalStreamController = class extends Destroyable {
15286
15403
  const stream = this._localStream$.value;
15287
15404
  const track = stream?.getTracks().find((t) => t.id === trackId);
15288
15405
  if (!track) {
15289
- logger$18.debug(`[LocalStreamController] track not found: ${trackId}`);
15406
+ logger$19.debug(`[LocalStreamController] track not found: ${trackId}`);
15290
15407
  return;
15291
15408
  }
15292
15409
  track.removeEventListener("ended", this.mediaTrackEndedHandler);
@@ -15295,7 +15412,7 @@ var LocalStreamController = class extends Destroyable {
15295
15412
  this._localStream$.next(stream);
15296
15413
  if (track.kind === "video") this._localVideoTracks$.next(stream?.getVideoTracks() ?? []);
15297
15414
  else this._localAudioTracks$.next(stream?.getAudioTracks() ?? []);
15298
- logger$18.debug(`[LocalStreamController] ${track.kind} track removed:`, trackId);
15415
+ logger$19.debug(`[LocalStreamController] ${track.kind} track removed:`, trackId);
15299
15416
  return track;
15300
15417
  }
15301
15418
  /**
@@ -15330,7 +15447,7 @@ var LocalStreamController = class extends Destroyable {
15330
15447
  */
15331
15448
  stopAllTracks() {
15332
15449
  this._localStream$.value?.getTracks().forEach((track) => {
15333
- logger$18.debug(`[LocalStreamController] Stopping local track: ${track.kind}`);
15450
+ logger$19.debug(`[LocalStreamController] Stopping local track: ${track.kind}`);
15334
15451
  track.removeEventListener("ended", this.mediaTrackEndedHandler);
15335
15452
  track.stop();
15336
15453
  });
@@ -15346,7 +15463,7 @@ var LocalStreamController = class extends Destroyable {
15346
15463
 
15347
15464
  //#endregion
15348
15465
  //#region src/controllers/TransceiverController.ts
15349
- const logger$17 = getLogger();
15466
+ const logger$18 = getLogger();
15350
15467
  const getDirection = (send, recv) => {
15351
15468
  if (send && recv) return "sendrecv";
15352
15469
  else if (send && !recv) return "sendonly";
@@ -15427,6 +15544,12 @@ var TransceiverController = class extends Destroyable {
15427
15544
  scaleResolutionDownBy: Number(rid) * 6 || 1
15428
15545
  }));
15429
15546
  }
15547
+ /**
15548
+ * Resolve the current MediaTrackConstraints for an input kind, normalising
15549
+ * boolean shorthand to an empty object. Public so the surrounding
15550
+ * RTCPeerConnectionController can drive its own pipeline-aware getUserMedia
15551
+ * call with the same effective constraints the transceiver would have used.
15552
+ */
15430
15553
  getConstraintsFor(kind) {
15431
15554
  const constraints = kind === "audio" ? this.inputAudioDeviceConstraints : this.inputVideoDeviceConstraints;
15432
15555
  return typeof constraints === "boolean" ? {} : constraints;
@@ -15448,7 +15571,7 @@ var TransceiverController = class extends Destroyable {
15448
15571
  sendEncodings: isAudio ? void 0 : this.sendEncodings,
15449
15572
  streams: direction === "recvonly" ? void 0 : [localStream]
15450
15573
  };
15451
- logger$17.debug(`[TransceiverController] Setting up transceiver sender for local ${track.kind} track:`, {
15574
+ logger$18.debug(`[TransceiverController] Setting up transceiver sender for local ${track.kind} track:`, {
15452
15575
  transceiver,
15453
15576
  transceiverParams
15454
15577
  });
@@ -15456,11 +15579,11 @@ var TransceiverController = class extends Destroyable {
15456
15579
  await transceiver.sender.replaceTrack(track);
15457
15580
  transceiver.direction = transceiverParams.direction;
15458
15581
  if (transceiverParams.streams?.some((stream) => Boolean(stream))) {
15459
- logger$17.debug(`[TransceiverController] Setting streams for transceiver sender for local ${track.kind} track:`, transceiverParams.streams);
15582
+ logger$18.debug(`[TransceiverController] Setting streams for transceiver sender for local ${track.kind} track:`, transceiverParams.streams);
15460
15583
  transceiver.sender.setStreams(...transceiverParams.streams);
15461
15584
  }
15462
15585
  } else {
15463
- logger$17.debug(`[TransceiverController] Adding new transceiver for local ${track.kind} track:`, track.id);
15586
+ logger$18.debug(`[TransceiverController] Adding new transceiver for local ${track.kind} track:`, track.id);
15464
15587
  this.peerConnection.addTransceiver(track, transceiverParams);
15465
15588
  }
15466
15589
  }
@@ -15474,13 +15597,13 @@ var TransceiverController = class extends Destroyable {
15474
15597
  if (options.updateTransceiverDirection) transceiver.direction = "inactive";
15475
15598
  }
15476
15599
  } catch (error) {
15477
- logger$17.error("[TransceiverController] stopTrackSender error", kind, error);
15600
+ logger$18.error("[TransceiverController] stopTrackSender error", kind, error);
15478
15601
  this.options.onError?.(new MediaTrackError("stopTrackSender", kind, error));
15479
15602
  }
15480
15603
  }
15481
15604
  async restoreTrackSender(kind) {
15482
15605
  try {
15483
- logger$17.debug("[TransceiverController] restoreTrackSender called", kind);
15606
+ logger$18.debug("[TransceiverController] restoreTrackSender called", kind);
15484
15607
  const constraints = {};
15485
15608
  const transceivers = this.transceiverByKind(kind);
15486
15609
  for (const transceiver of transceivers) {
@@ -15490,23 +15613,23 @@ var TransceiverController = class extends Destroyable {
15490
15613
  if (trackKind === "audio" || trackKind === "video") constraints[trackKind] = this.getConstraintsFor(trackKind);
15491
15614
  }
15492
15615
  }
15493
- logger$17.debug("[TransceiverController] restoreTrackSender constraints:", constraints);
15616
+ logger$18.debug("[TransceiverController] restoreTrackSender constraints:", constraints);
15494
15617
  if (Object.keys(constraints).length === 0) {
15495
- logger$17.warn("[TransceiverController] restoreTrackSender: no tracks need restoration", kind);
15618
+ logger$18.warn("[TransceiverController] restoreTrackSender: no tracks need restoration", kind);
15496
15619
  return;
15497
15620
  }
15498
15621
  const newTracks = (await this.options.getUserMedia(constraints)).getTracks();
15499
- logger$17.debug("[TransceiverController] restoreTrackSender new tracks:", newTracks);
15622
+ logger$18.debug("[TransceiverController] restoreTrackSender new tracks:", newTracks);
15500
15623
  for (const newTrack of newTracks) {
15501
15624
  this.options.localStreamController.addTrack(newTrack);
15502
15625
  const trackKind = newTrack.kind;
15503
15626
  const transceiverOfKind = this.transceiverByKind(trackKind)[0];
15504
15627
  transceiverOfKind.direction = trackKind === "audio" ? this.audioDirection : this.videoDirection;
15505
- logger$17.debug("[TransceiverController] restoreTrackSender setting direction for", trackKind, transceiverOfKind.direction);
15628
+ logger$18.debug("[TransceiverController] restoreTrackSender setting direction for", trackKind, transceiverOfKind.direction);
15506
15629
  await transceiverOfKind.sender.replaceTrack(newTrack);
15507
15630
  }
15508
15631
  } catch (error) {
15509
- logger$17.error("[TransceiverController] restoreTrackSender error", kind, error);
15632
+ logger$18.error("[TransceiverController] restoreTrackSender error", kind, error);
15510
15633
  this.options.onError?.(new MediaTrackError("restoreTrackSender", kind, error));
15511
15634
  }
15512
15635
  }
@@ -15547,14 +15670,14 @@ var TransceiverController = class extends Destroyable {
15547
15670
  };
15548
15671
  try {
15549
15672
  await track.applyConstraints(constraintsToApply);
15550
- logger$17.debug(`[TransceiverController] Updated ${kind} sender constraints:`, constraintsToApply);
15551
- logger$17.debug(`[TransceiverController] Updated ${kind} sender constraints:`, track.getConstraints());
15673
+ logger$18.debug(`[TransceiverController] Updated ${kind} sender constraints:`, constraintsToApply);
15674
+ logger$18.debug(`[TransceiverController] Updated ${kind} sender constraints:`, track.getConstraints());
15552
15675
  } catch (error) {
15553
- logger$17.warn(`[TransceiverController] applyConstraints failed for ${kind} track ${track.id}, attempting track replacement fallback:`, error);
15676
+ logger$18.warn(`[TransceiverController] applyConstraints failed for ${kind} track ${track.id}, attempting track replacement fallback:`, error);
15554
15677
  try {
15555
15678
  await this.replaceTrackFallback(sender, track, kind, constraintsToApply);
15556
15679
  } catch (fallbackError) {
15557
- logger$17.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
15680
+ logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
15558
15681
  this.options.onError?.(new MediaTrackError("updateSendersConstraints", kind, fallbackError));
15559
15682
  }
15560
15683
  }
@@ -15582,7 +15705,7 @@ var TransceiverController = class extends Destroyable {
15582
15705
  if (!newTrack) throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
15583
15706
  await sender.replaceTrack(newTrack);
15584
15707
  this.options.localStreamController.addTrack(newTrack);
15585
- logger$17.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
15708
+ logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
15586
15709
  }
15587
15710
  getMediaDirections() {
15588
15711
  if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
@@ -15613,7 +15736,7 @@ var TransceiverController = class extends Destroyable {
15613
15736
  //#endregion
15614
15737
  //#region src/controllers/RTCPeerConnectionController.ts
15615
15738
  var import_cjs$16 = require_cjs();
15616
- const logger$16 = getLogger();
15739
+ const logger$17 = getLogger();
15617
15740
  var RTCPeerConnectionController = class extends Destroyable {
15618
15741
  constructor(options = {}, remoteSessionDescription, deviceController) {
15619
15742
  super();
@@ -15629,43 +15752,43 @@ var RTCPeerConnectionController = class extends Destroyable {
15629
15752
  this.oniceconnectionstatechangeHandler = () => {
15630
15753
  if (this.peerConnection) {
15631
15754
  const { iceConnectionState } = this.peerConnection;
15632
- logger$16.debug(`[RTCPeerConnectionController] ICE connection state changed to: ${iceConnectionState}`);
15755
+ logger$17.debug(`[RTCPeerConnectionController] ICE connection state changed to: ${iceConnectionState}`);
15633
15756
  this._iceConnectionState$.next(this.peerConnection.iceConnectionState);
15634
15757
  }
15635
15758
  };
15636
15759
  this.onconnectionstatechangeHandler = () => {
15637
15760
  if (this.peerConnection) {
15638
15761
  const { connectionState } = this.peerConnection;
15639
- logger$16.debug(`[RTCPeerConnectionController] Connection state changed to: ${connectionState}`);
15762
+ logger$17.debug(`[RTCPeerConnectionController] Connection state changed to: ${connectionState}`);
15640
15763
  if (connectionState === "connected") this.removeConnectionTimer();
15641
15764
  this._connectionState$.next(this.peerConnection.connectionState);
15642
15765
  }
15643
15766
  };
15644
15767
  this.onsignalingstatechangeHandler = () => {
15645
- logger$16.debug(`[RTCPeerConnectionController] Signaling state changed to: ${this.peerConnection?.signalingState}`);
15768
+ logger$17.debug(`[RTCPeerConnectionController] Signaling state changed to: ${this.peerConnection?.signalingState}`);
15646
15769
  };
15647
15770
  this.onicegatheringstatechangeHandler = () => {
15648
15771
  if (this.peerConnection) this._iceGatheringState$.next(this.peerConnection.iceGatheringState);
15649
15772
  };
15650
15773
  this.onnegotiationneededHandler = (event) => {
15651
- logger$16.debug("[RTCPeerConnectionController] Negotiation needed event received.", event);
15774
+ logger$17.debug("[RTCPeerConnectionController] Negotiation needed event received.", event);
15652
15775
  this.negotiationNeeded$.next();
15653
15776
  };
15654
15777
  this.updateSelectedInputDevice = async (kind, deviceInfo) => {
15655
15778
  try {
15656
15779
  const { localStream } = this;
15657
15780
  if (!localStream) {
15658
- logger$16.warn("[RTCPeerConnectionController] No local stream available to update input device.");
15781
+ logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
15659
15782
  return;
15660
15783
  }
15661
- logger$16.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
15784
+ logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
15662
15785
  const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
15663
15786
  if (track) {
15664
15787
  this.transceiverController?.stopTrackSender(kind);
15665
15788
  this.localStreamController.removeTrack(track.id);
15666
- logger$16.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
15789
+ logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
15667
15790
  if (!deviceInfo) {
15668
- logger$16.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
15791
+ logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
15669
15792
  return;
15670
15793
  }
15671
15794
  const streamTrack = (await this.getUserMedia({ [kind]: {
@@ -15673,16 +15796,16 @@ var RTCPeerConnectionController = class extends Destroyable {
15673
15796
  ...this.deviceController.deviceInfoToConstraints(deviceInfo)
15674
15797
  } })).getTracks().find((t) => t.kind === kind);
15675
15798
  if (streamTrack) {
15676
- logger$16.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
15799
+ logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
15677
15800
  this.localStreamController.addTrack(streamTrack);
15678
15801
  await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
15679
- logger$16.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
15802
+ logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
15680
15803
  }
15681
15804
  }
15682
- logger$16.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
15805
+ logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
15683
15806
  } catch (error) {
15684
- logger$16.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
15685
- this._errors$.next(toError(error));
15807
+ logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
15808
+ this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
15686
15809
  throw error;
15687
15810
  }
15688
15811
  };
@@ -15753,7 +15876,19 @@ var RTCPeerConnectionController = class extends Destroyable {
15753
15876
  return this._memberId;
15754
15877
  }
15755
15878
  stopTrackSender(kind, options = { updateTransceiverDirection: false }) {
15756
- this.transceiverController?.stopTrackSender(kind, options);
15879
+ const audioCovered = kind === "audio" || kind === "both";
15880
+ if (audioCovered && this._localAudioPipeline) this.stopRawAudioInputForPipeline();
15881
+ if (!audioCovered) this.transceiverController?.stopTrackSender(kind, options);
15882
+ else if (kind === "both") this.transceiverController?.stopTrackSender("video", options);
15883
+ else if (!this._localAudioPipeline) this.transceiverController?.stopTrackSender(kind, options);
15884
+ }
15885
+ stopRawAudioInputForPipeline() {
15886
+ const rawTracks = this.localStreamController.localAudioTracks;
15887
+ for (const track of rawTracks) if (track.readyState === "live") {
15888
+ track.stop();
15889
+ this.localStreamController.removeTrack(track.id);
15890
+ }
15891
+ this._localAudioPipeline?.setInputTrack(null);
15757
15892
  }
15758
15893
  get isNegotiating$() {
15759
15894
  return this._isNegotiating$.asObservable();
@@ -15892,7 +16027,7 @@ var RTCPeerConnectionController = class extends Destroyable {
15892
16027
  case "main":
15893
16028
  default: return {
15894
16029
  ...options,
15895
- offerToReceiveAudio: true,
16030
+ offerToReceiveAudio: this.options.receiveAudio ?? true,
15896
16031
  offerToReceiveVideo: this.options.receiveVideo ?? Boolean(this.inputVideoDeviceConstraints)
15897
16032
  };
15898
16033
  }
@@ -15919,15 +16054,15 @@ var RTCPeerConnectionController = class extends Destroyable {
15919
16054
  this.setupPeerConnection();
15920
16055
  this.subscribeTo(this.negotiationNeeded$.pipe((0, import_cjs$16.auditTime)(0), (0, import_cjs$16.exhaustMap)(async () => this.startNegotiation())), {
15921
16056
  next: () => {
15922
- logger$16.debug("[RTCPeerConnectionController] Start Negotiation completed successfully");
16057
+ logger$17.debug("[RTCPeerConnectionController] Start Negotiation completed successfully");
15923
16058
  },
15924
16059
  error: (error) => {
15925
- logger$16.error("[RTCPeerConnectionController] Start Negotiation error:", error);
16060
+ logger$17.error("[RTCPeerConnectionController] Start Negotiation error:", error);
15926
16061
  this._errors$.next(toError(error));
15927
16062
  }
15928
16063
  });
15929
16064
  this.subscribeTo((0, import_cjs$16.merge)(this.deviceController.selectedAudioInputDevice$.pipe((0, import_cjs$16.map)((deviceInfo) => ["audio", deviceInfo])), this.deviceController.selectedVideoInputDevice$.pipe((0, import_cjs$16.map)((deviceInfo) => ["video", deviceInfo]))).pipe((0, import_cjs$16.skipWhile)(() => !this.localStreamController.localStream)), async ([kind, deviceInfo]) => {
15930
- logger$16.debug(`[RTCPeerConnectionController] Selected input device changed for:`, {
16065
+ logger$17.debug(`[RTCPeerConnectionController] Selected input device changed for:`, {
15931
16066
  kind,
15932
16067
  deviceInfo
15933
16068
  });
@@ -15944,7 +16079,7 @@ var RTCPeerConnectionController = class extends Destroyable {
15944
16079
  this._initialized$.next(true);
15945
16080
  }
15946
16081
  } catch (error) {
15947
- logger$16.error("[RTCPeerConnectionController] Initialization error:", error);
16082
+ logger$17.error("[RTCPeerConnectionController] Initialization error:", error);
15948
16083
  this._errors$.next(toError(error));
15949
16084
  this.destroy();
15950
16085
  }
@@ -15976,22 +16111,22 @@ var RTCPeerConnectionController = class extends Destroyable {
15976
16111
  }
15977
16112
  async startNegotiation() {
15978
16113
  if (this.isNegotiating) {
15979
- logger$16.debug("[RTCPeerConnectionController] Negotiation already in progress, skipping.");
16114
+ logger$17.debug("[RTCPeerConnectionController] Negotiation already in progress, skipping.");
15980
16115
  return;
15981
16116
  }
15982
16117
  this.setupEventListeners();
15983
16118
  if (this.type === "answer") {
15984
- logger$16.debug("[RTCPeerConnectionController] This is an answer type still, skipping offer creation.");
16119
+ logger$17.debug("[RTCPeerConnectionController] This is an answer type still, skipping offer creation.");
15985
16120
  return;
15986
16121
  }
15987
16122
  this._isNegotiating$.next(true);
15988
- logger$16.debug("[RTCPeerConnectionController] Starting negotiation.");
16123
+ logger$17.debug("[RTCPeerConnectionController] Starting negotiation.");
15989
16124
  try {
15990
16125
  const { offerOptions } = this;
15991
- logger$16.debug("[RTCPeerConnectionController] Creating offer with options:", offerOptions);
16126
+ logger$17.debug("[RTCPeerConnectionController] Creating offer with options:", offerOptions);
15992
16127
  await this.createOffer(offerOptions);
15993
16128
  } catch (error) {
15994
- logger$16.error("[RTCPeerConnectionController] Error during negotiation:", error);
16129
+ logger$17.error("[RTCPeerConnectionController] Error during negotiation:", error);
15995
16130
  this._errors$.next(toError(error));
15996
16131
  }
15997
16132
  }
@@ -16007,14 +16142,14 @@ var RTCPeerConnectionController = class extends Destroyable {
16007
16142
  let readyToConnect = status !== "failed";
16008
16143
  try {
16009
16144
  if (status === "received" && sdp) {
16010
- logger$16.debug("[RTCPeerConnectionController] Received answer SDP:", sdp);
16145
+ logger$17.debug("[RTCPeerConnectionController] Received answer SDP:", sdp);
16011
16146
  await this._setRemoteDescription({
16012
16147
  type: "answer",
16013
16148
  sdp
16014
16149
  });
16015
16150
  }
16016
16151
  } catch (error) {
16017
- logger$16.error("[RTCPeerConnectionController] Error updating answer status:", error);
16152
+ logger$17.error("[RTCPeerConnectionController] Error updating answer status:", error);
16018
16153
  this._errors$.next(toError(error));
16019
16154
  readyToConnect = false;
16020
16155
  } finally {
@@ -16033,7 +16168,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16033
16168
  await this.handleOfferReceived();
16034
16169
  break;
16035
16170
  case "failed":
16036
- logger$16.error("[RTCPeerConnectionController] Offer failed to be processed by remote.");
16171
+ logger$17.error("[RTCPeerConnectionController] Offer failed to be processed by remote.");
16037
16172
  break;
16038
16173
  case "sent":
16039
16174
  default:
@@ -16046,13 +16181,14 @@ var RTCPeerConnectionController = class extends Destroyable {
16046
16181
  */
16047
16182
  async acceptInbound(mediaOverrides) {
16048
16183
  if (mediaOverrides) {
16049
- const { audio, video, receiveAudio, receiveVideo } = mediaOverrides;
16184
+ const { audio, video, receiveAudio, receiveVideo, fallbackToReceiveOnly } = mediaOverrides;
16050
16185
  this.options = {
16051
16186
  ...this.options,
16052
16187
  ...audio !== void 0 ? { audio } : {},
16053
16188
  ...video !== void 0 ? { video } : {},
16054
16189
  ...receiveAudio !== void 0 ? { receiveAudio } : {},
16055
- ...receiveVideo !== void 0 ? { receiveVideo } : {}
16190
+ ...receiveVideo !== void 0 ? { receiveVideo } : {},
16191
+ ...fallbackToReceiveOnly !== void 0 ? { fallbackToReceiveOnly } : {}
16056
16192
  };
16057
16193
  this.transceiverController?.updateOptions({
16058
16194
  receiveAudio: this.receiveAudio,
@@ -16065,7 +16201,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16065
16201
  }
16066
16202
  await this.setupLocalTracks();
16067
16203
  const { answerOptions } = this;
16068
- logger$16.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
16204
+ logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
16069
16205
  await this.createAnswer(answerOptions);
16070
16206
  }
16071
16207
  async handleOfferReceived() {
@@ -16073,7 +16209,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16073
16209
  this._isNegotiating$.next(true);
16074
16210
  await this._setRemoteDescription(this.sdpInit);
16075
16211
  const { answerOptions } = this;
16076
- logger$16.debug("[RTCPeerConnectionController] Creating answer with options:", answerOptions);
16212
+ logger$17.debug("[RTCPeerConnectionController] Creating answer with options:", answerOptions);
16077
16213
  await this.createAnswer(answerOptions);
16078
16214
  }
16079
16215
  readyToConnect() {
@@ -16081,7 +16217,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16081
16217
  this.connectionTimer = setTimeout(() => {
16082
16218
  this.removeConnectionTimer();
16083
16219
  if (this.peerConnection?.connectionState !== "connected") {
16084
- logger$16.debug("[RTCPeerConnectionController] Connection timeout, restarting ICE gathering with relay only.");
16220
+ logger$17.debug("[RTCPeerConnectionController] Connection timeout, restarting ICE gathering with relay only.");
16085
16221
  this.iceGatheringController.restartICEGatheringWithRelayOnly();
16086
16222
  }
16087
16223
  }, this.connectionTimeout);
@@ -16103,14 +16239,14 @@ var RTCPeerConnectionController = class extends Destroyable {
16103
16239
  const stereo = this.options.stereo ?? PreferencesContainer.instance.stereoAudio;
16104
16240
  if (preferredAudioCodecs.length > 0 || preferredVideoCodecs.length > 0) {
16105
16241
  result = setCodecPreferences(result, preferredAudioCodecs, preferredVideoCodecs);
16106
- logger$16.debug("[RTCPeerConnectionController] Applied codec preferences to SDP", {
16242
+ logger$17.debug("[RTCPeerConnectionController] Applied codec preferences to SDP", {
16107
16243
  preferredAudioCodecs,
16108
16244
  preferredVideoCodecs
16109
16245
  });
16110
16246
  }
16111
16247
  if (stereo) {
16112
16248
  result = enableStereoOpus(result);
16113
- logger$16.debug("[RTCPeerConnectionController] Applied stereo Opus to SDP");
16249
+ logger$17.debug("[RTCPeerConnectionController] Applied stereo Opus to SDP");
16114
16250
  }
16115
16251
  return Promise.resolve(result);
16116
16252
  }
@@ -16166,25 +16302,25 @@ var RTCPeerConnectionController = class extends Destroyable {
16166
16302
  ...this.peerConnection.getConfiguration(),
16167
16303
  iceTransportPolicy: "relay"
16168
16304
  });
16169
- logger$16.debug("[RTCPeerConnectionController] ICE transport policy set to relay-only");
16305
+ logger$17.debug("[RTCPeerConnectionController] ICE transport policy set to relay-only");
16170
16306
  } catch (error) {
16171
- logger$16.warn("[RTCPeerConnectionController] Failed to set relay-only policy:", error);
16307
+ logger$17.warn("[RTCPeerConnectionController] Failed to set relay-only policy:", error);
16172
16308
  }
16173
16309
  this.setupEventListeners();
16174
16310
  this._isNegotiating$.next(true);
16175
- logger$16.debug(`[RTCPeerConnectionController] Triggering ICE restart${relayOnly ? " (relay-only)" : ""}.`);
16311
+ logger$17.debug(`[RTCPeerConnectionController] Triggering ICE restart${relayOnly ? " (relay-only)" : ""}.`);
16176
16312
  try {
16177
16313
  const offer = await this.peerConnection.createOffer({ iceRestart: true });
16178
16314
  await this.setLocalDescription(offer);
16179
16315
  } catch (error) {
16180
- logger$16.error("[RTCPeerConnectionController] ICE restart offer failed:", error);
16316
+ logger$17.error("[RTCPeerConnectionController] ICE restart offer failed:", error);
16181
16317
  this._errors$.next(toError(error));
16182
16318
  this.negotiationEnded();
16183
16319
  if (policyChanged) this.restoreIceTransportPolicy();
16184
16320
  throw error;
16185
16321
  }
16186
16322
  if (policyChanged) (0, import_cjs$16.firstValueFrom)((0, import_cjs$16.race)(this._iceGatheringState$.pipe((0, import_cjs$16.filter)((state) => state === "complete"), (0, import_cjs$16.take)(1)), (0, import_cjs$16.timer)(ICE_GATHERING_COMPLETE_TIMEOUT_MS).pipe((0, import_cjs$16.map)(() => "timeout")))).then(() => this.restoreIceTransportPolicy()).catch((error) => {
16187
- logger$16.warn("[RTCPeerConnectionController] Error waiting for ICE gathering to complete:", error);
16323
+ logger$17.warn("[RTCPeerConnectionController] Error waiting for ICE gathering to complete:", error);
16188
16324
  this.restoreIceTransportPolicy();
16189
16325
  });
16190
16326
  }
@@ -16194,9 +16330,9 @@ var RTCPeerConnectionController = class extends Destroyable {
16194
16330
  ...this.peerConnection.getConfiguration(),
16195
16331
  iceTransportPolicy: this.options.relayOnly ? "relay" : "all"
16196
16332
  });
16197
- logger$16.debug("[RTCPeerConnectionController] ICE transport policy restored");
16333
+ logger$17.debug("[RTCPeerConnectionController] ICE transport policy restored");
16198
16334
  } catch (error) {
16199
- logger$16.warn("[RTCPeerConnectionController] Failed to restore ICE transport policy:", error);
16335
+ logger$17.warn("[RTCPeerConnectionController] Failed to restore ICE transport policy:", error);
16200
16336
  }
16201
16337
  }
16202
16338
  /**
@@ -16208,13 +16344,25 @@ var RTCPeerConnectionController = class extends Destroyable {
16208
16344
  await this.setupRemoteTracks();
16209
16345
  }
16210
16346
  async setupLocalTracks() {
16211
- logger$16.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
16212
- const localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
16347
+ logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
16348
+ if (this.hasNoLocalMediaToSend()) {
16349
+ if (!this.receiveAudio && !this.receiveVideo) throw new InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
16350
+ logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
16351
+ this.setupReceiveOnlyTransceivers();
16352
+ return;
16353
+ }
16354
+ let localStream;
16355
+ try {
16356
+ localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
16357
+ } catch (error) {
16358
+ this.handleLocalMediaFailure(error);
16359
+ return;
16360
+ }
16213
16361
  if (this.transceiverController?.useAddStream ?? false) {
16214
- logger$16.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
16362
+ logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
16215
16363
  this.peerConnection?.addStream(localStream);
16216
16364
  if (!this.isNegotiating) {
16217
- logger$16.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
16365
+ logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
16218
16366
  this.negotiationNeeded$.next();
16219
16367
  }
16220
16368
  return;
@@ -16230,12 +16378,54 @@ var RTCPeerConnectionController = class extends Destroyable {
16230
16378
  const transceivers = (kind === "audio" ? this.transceiverController?.audioTransceivers : this.transceiverController?.videoTransceivers) ?? [];
16231
16379
  await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
16232
16380
  } else {
16233
- logger$16.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
16381
+ logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
16234
16382
  this.peerConnection?.addTrack(track, localStream);
16235
16383
  }
16236
16384
  }
16237
16385
  }
16238
16386
  }
16387
+ /** True for a main connection with no local media to send. */
16388
+ hasNoLocalMediaToSend() {
16389
+ const hasInputStreams = Boolean(this.options.inputAudioStream ?? this.options.inputVideoStream);
16390
+ return this.propose === "main" && !this.localStream && !hasInputStreams && !this.inputAudioDeviceConstraints && !this.inputVideoDeviceConstraints;
16391
+ }
16392
+ /** The media kinds this connection wants to send: 'audiovideo' | 'video' | 'audio'. */
16393
+ get requestedMediaKinds() {
16394
+ const wantsAudio = Boolean(this.inputAudioDeviceConstraints);
16395
+ const wantsVideo = Boolean(this.inputVideoDeviceConstraints);
16396
+ if (wantsAudio && wantsVideo) return "audiovideo";
16397
+ return wantsVideo ? "video" : "audio";
16398
+ }
16399
+ /**
16400
+ * Handle a local media acquisition failure with a typed, semantically
16401
+ * accurate MediaAccessError created at the acquisition site:
16402
+ * - Auxiliary connections (screenshare / additional-device) throw a
16403
+ * non-fatal error — VertoManager surfaces it and the call is unaffected.
16404
+ * - The main connection degrades to receive-only when allowed (default),
16405
+ * otherwise fails with a fatal error.
16406
+ */
16407
+ handleLocalMediaFailure(error) {
16408
+ if (this.propose === "screenshare") throw new MediaAccessError("startScreenShare", "screen", error, false);
16409
+ if (this.propose === "additional-device") throw new MediaAccessError("addInputDevice", this.requestedMediaKinds, error, false);
16410
+ const canReceive = this.receiveAudio || this.receiveVideo;
16411
+ if (!((this.options.fallbackToReceiveOnly ?? true) && canReceive)) throw new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, true);
16412
+ logger$17.warn("[RTCPeerConnectionController] Local media unavailable; continuing receive-only:", error);
16413
+ this._errors$.next(new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, false));
16414
+ this.setupReceiveOnlyTransceivers();
16415
+ }
16416
+ /**
16417
+ * Negotiate receive-only m-lines when there are no local tracks to send.
16418
+ * Only offer-type connections add transceivers — answer-type connections
16419
+ * reuse the transceivers created from the remote offer.
16420
+ */
16421
+ setupReceiveOnlyTransceivers() {
16422
+ if (this.type !== "offer") return;
16423
+ if (this.transceiverController?.useAddTransceivers ?? false) {
16424
+ this.peerConnection?.addTransceiver("audio", { direction: this.receiveAudio ? "recvonly" : "inactive" });
16425
+ this.peerConnection?.addTransceiver("video", { direction: this.receiveVideo ? "recvonly" : "inactive" });
16426
+ }
16427
+ if (!this.isNegotiating) this.negotiationNeeded$.next();
16428
+ }
16239
16429
  async getUserMedia(constraints) {
16240
16430
  return (this.options.webRTCApiProvider?.mediaDevices ?? navigator.mediaDevices).getUserMedia(constraints);
16241
16431
  }
@@ -16247,7 +16437,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16247
16437
  async setupRemoteTracks() {
16248
16438
  if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
16249
16439
  this.peerConnection.ontrack = (event) => {
16250
- logger$16.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
16440
+ logger$17.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
16251
16441
  if (event.streams[0]) this._remoteStream$.next(event.streams[0]);
16252
16442
  else {
16253
16443
  const existingTracks = this._remoteStream$.value?.getTracks() ?? [];
@@ -16258,8 +16448,27 @@ var RTCPeerConnectionController = class extends Destroyable {
16258
16448
  await this.transceiverController?.setupRemoteTransceivers(this.type);
16259
16449
  }
16260
16450
  async restoreTrackSender(kind) {
16261
- await this.transceiverController?.restoreTrackSender(kind);
16262
- if (kind !== "video" && this._localAudioPipeline) await this.applyLocalAudioPipelineToSender();
16451
+ const audioCovered = kind === "audio" || kind === "both";
16452
+ if (audioCovered && this._localAudioPipeline) await this.restoreRawAudioInputForPipeline();
16453
+ if (!audioCovered) await this.transceiverController?.restoreTrackSender(kind);
16454
+ else if (kind === "both") await this.transceiverController?.restoreTrackSender("video");
16455
+ else if (!this._localAudioPipeline) await this.transceiverController?.restoreTrackSender(kind);
16456
+ }
16457
+ async restoreRawAudioInputForPipeline() {
16458
+ if (!this._localAudioPipeline) return;
16459
+ const constraints = this.transceiverController?.getConstraintsFor("audio") ?? {};
16460
+ let stream;
16461
+ try {
16462
+ stream = await this.getUserMedia({ audio: constraints });
16463
+ } catch (error) {
16464
+ logger$17.error("[RTCPeerConnectionController] Failed to re-acquire mic for pipeline restore:", error);
16465
+ this._errors$.next(new MediaTrackError("restoreAudioPipelineInput", "audio", error));
16466
+ return;
16467
+ }
16468
+ const newTrack = stream.getAudioTracks().at(0);
16469
+ if (!newTrack) return;
16470
+ this.localStreamController.addTrack(newTrack);
16471
+ this._localAudioPipeline.setInputTrack(newTrack);
16263
16472
  }
16264
16473
  /**
16265
16474
  * Return the lazily-created {@link LocalAudioPipeline}, constructing it on
@@ -16274,7 +16483,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16274
16483
  try {
16275
16484
  this._localAudioPipeline = new LocalAudioPipeline();
16276
16485
  } catch (error) {
16277
- logger$16.warn("[RTCPeerConnectionController] Failed to create LocalAudioPipeline:", error);
16486
+ logger$17.warn("[RTCPeerConnectionController] Failed to create LocalAudioPipeline:", error);
16278
16487
  return null;
16279
16488
  }
16280
16489
  this.subscribeTo(this.localStreamController.localAudioTracks$, () => {
@@ -16296,7 +16505,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16296
16505
  try {
16297
16506
  await sender.replaceTrack(this._localAudioPipeline.outputTrack);
16298
16507
  } catch (error) {
16299
- logger$16.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
16508
+ logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
16300
16509
  }
16301
16510
  }
16302
16511
  /**
@@ -16312,10 +16521,10 @@ var RTCPeerConnectionController = class extends Destroyable {
16312
16521
  try {
16313
16522
  const localStream = this.localStreamController.addTrack(track);
16314
16523
  this.peerConnection.addTrack(track, localStream);
16315
- logger$16.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
16524
+ logger$17.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
16316
16525
  } catch (error) {
16317
- logger$16.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
16318
- this._errors$.next(toError(error));
16526
+ logger$17.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
16527
+ this._errors$.next(new MediaTrackError("addLocalTrack", track.kind, error));
16319
16528
  throw error;
16320
16529
  }
16321
16530
  }
@@ -16331,16 +16540,16 @@ var RTCPeerConnectionController = class extends Destroyable {
16331
16540
  }
16332
16541
  const sender = this.peerConnection.getSenders().find((sender$1) => sender$1.track?.id === trackId);
16333
16542
  if (!sender) {
16334
- logger$16.debug(`[RTCPeerConnectionController] track not found: ${trackId}`);
16543
+ logger$17.debug(`[RTCPeerConnectionController] track not found: ${trackId}`);
16335
16544
  return;
16336
16545
  }
16337
16546
  try {
16338
16547
  this.peerConnection.removeTrack(sender);
16339
16548
  this.localStreamController.removeTrack(trackId);
16340
- logger$16.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
16549
+ logger$17.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
16341
16550
  } catch (error) {
16342
- logger$16.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
16343
- this._errors$.next(toError(error));
16551
+ logger$17.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
16552
+ this._errors$.next(new MediaTrackError("removeLocalTrack", sender.track?.kind ?? "unknown", error));
16344
16553
  throw error;
16345
16554
  }
16346
16555
  }
@@ -16366,7 +16575,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16366
16575
  async replaceAudioTrackWithConstraints(constraints) {
16367
16576
  const senders = this.peerConnection?.getSenders().filter((s) => s.track?.kind === "audio" && s.track.readyState === "live");
16368
16577
  if (!senders || senders.length === 0) {
16369
- logger$16.warn("[RTCPeerConnectionController] No live audio sender to replace");
16578
+ logger$17.warn("[RTCPeerConnectionController] No live audio sender to replace");
16370
16579
  return;
16371
16580
  }
16372
16581
  for (const sender of senders) {
@@ -16384,7 +16593,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16384
16593
  const newTrack = (await this.getUserMedia({ audio: mergedConstraints })).getAudioTracks()[0];
16385
16594
  await sender.replaceTrack(newTrack);
16386
16595
  this.localStreamController.addTrack(newTrack);
16387
- logger$16.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
16596
+ logger$17.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
16388
16597
  }
16389
16598
  }
16390
16599
  /**
@@ -16392,7 +16601,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16392
16601
  * Completes all observables to prevent memory leaks.
16393
16602
  */
16394
16603
  destroy() {
16395
- logger$16.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
16604
+ logger$17.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
16396
16605
  this.removeConnectionTimer();
16397
16606
  this._iceGatheringController?.destroy();
16398
16607
  this._localAudioPipeline?.destroy();
@@ -16418,7 +16627,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16418
16627
  }
16419
16628
  stopRemoteTracks() {
16420
16629
  this._remoteStream$.value?.getTracks().forEach((track) => {
16421
- logger$16.debug(`[RTCPeerConnectionController] Stopping remote track: ${track.kind}`);
16630
+ logger$17.debug(`[RTCPeerConnectionController] Stopping remote track: ${track.kind}`);
16422
16631
  track.stop();
16423
16632
  });
16424
16633
  }
@@ -16435,7 +16644,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16435
16644
  ...params,
16436
16645
  sdp: finalRemote
16437
16646
  };
16438
- logger$16.debug("[RTCPeerConnectionController] Setting remote description:", answer);
16647
+ logger$17.debug("[RTCPeerConnectionController] Setting remote description:", answer);
16439
16648
  return this.peerConnection.setRemoteDescription(answer);
16440
16649
  }
16441
16650
  };
@@ -16450,7 +16659,11 @@ function isVertoInviteMessage(value) {
16450
16659
  const msg = value;
16451
16660
  return msg.method === "verto.invite" && isObject(msg.params) && hasProperty(msg.params, "sdp") && hasProperty(msg.params, "callID");
16452
16661
  }
16453
- function isVertoByeMessage(value) {
16662
+ /**
16663
+ * Guards inbound `verto.bye` frames (server → client). Outbound bye messages are
16664
+ * built locally and never type-guarded, so only the inbound shape is asserted.
16665
+ */
16666
+ function isVertoByeInboundMessage(value) {
16454
16667
  if (!isVertoMethodMessage(value)) return false;
16455
16668
  return value.method === "verto.bye";
16456
16669
  }
@@ -16470,11 +16683,19 @@ function isVertoMediaParamsInnerParams(value) {
16470
16683
  function isVertoPingInnerParams(value) {
16471
16684
  return isObject(value) && hasProperty(value, "jsonrpc") && value.jsonrpc === "2.0" && hasProperty(value, "method") && value.method === "verto.ping";
16472
16685
  }
16686
+ /**
16687
+ * Guards the params of an inbound `verto.bye` frame (top-level `callID`). Use
16688
+ * this to recognise the bye payload extracted by `vertoBye$`, e.g. when racing
16689
+ * it against a boolean answer/reject signal.
16690
+ */
16691
+ function isVertoByeInboundParamsGuard(value) {
16692
+ return isObject(value) && hasProperty(value, "callID") && typeof value.callID === "string";
16693
+ }
16473
16694
 
16474
16695
  //#endregion
16475
16696
  //#region src/managers/VertoManager.ts
16476
16697
  var import_cjs$15 = require_cjs();
16477
- const logger$15 = getLogger();
16698
+ const logger$16 = getLogger();
16478
16699
  /**
16479
16700
  * Decide what value goes on the `node_id` field of a `webrtc.verto` envelope.
16480
16701
  *
@@ -16530,7 +16751,7 @@ var WebRTCVertoManager = class extends VertoManager {
16530
16751
  try {
16531
16752
  await this.executeVerto(vertoModifyMessage);
16532
16753
  } catch (error) {
16533
- logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto hold:", error);
16754
+ logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto hold:", error);
16534
16755
  throw error;
16535
16756
  }
16536
16757
  }
@@ -16543,7 +16764,7 @@ var WebRTCVertoManager = class extends VertoManager {
16543
16764
  try {
16544
16765
  await this.executeVerto(vertoModifyMessage);
16545
16766
  } catch (error) {
16546
- logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto unhold:", error);
16767
+ logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto unhold:", error);
16547
16768
  throw error;
16548
16769
  }
16549
16770
  }
@@ -16596,25 +16817,25 @@ var WebRTCVertoManager = class extends VertoManager {
16596
16817
  if (event.member_id) this.setSelfIdIfNull(event.member_id);
16597
16818
  });
16598
16819
  this.subscribeTo(this.vertoMedia$, (event) => {
16599
- logger$15.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
16600
- this._signalingStatus$.next("ringing");
16820
+ logger$16.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
16601
16821
  const { sdp, callID } = event;
16822
+ this.emitMainSignalingStatus(callID, "ringing");
16602
16823
  this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
16603
16824
  status: "received",
16604
16825
  sdp
16605
16826
  });
16606
16827
  });
16607
16828
  this.subscribeTo(this.vertoAnswer$, (event) => {
16608
- logger$15.debug("[WebRTCManager] Received Verto answer event:", event);
16609
- this._signalingStatus$.next("connecting");
16829
+ logger$16.debug("[WebRTCManager] Received Verto answer event:", event);
16610
16830
  const { sdp, callID } = event;
16831
+ this.emitMainSignalingStatus(callID, "connecting");
16611
16832
  this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
16612
16833
  status: "received",
16613
16834
  sdp
16614
16835
  });
16615
16836
  });
16616
16837
  this.subscribeTo(this.vertoMediaParams$, (event) => {
16617
- logger$15.debug("[WebRTCManager] Received Verto mediaParams event:", event);
16838
+ logger$16.debug("[WebRTCManager] Received Verto mediaParams event:", event);
16618
16839
  const { mediaParams, callID } = event;
16619
16840
  const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
16620
16841
  const { audio, video } = mediaParams;
@@ -16628,7 +16849,7 @@ var WebRTCVertoManager = class extends VertoManager {
16628
16849
  timestamp: Date.now()
16629
16850
  });
16630
16851
  } catch (error) {
16631
- logger$15.warn("[WebRTCManager] Error applying server-pushed media params:", error);
16852
+ logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
16632
16853
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16633
16854
  }
16634
16855
  })();
@@ -16650,13 +16871,13 @@ var WebRTCVertoManager = class extends VertoManager {
16650
16871
  */
16651
16872
  setNodeIdIfNull(nodeId) {
16652
16873
  if (!this._nodeId$.value && nodeId) {
16653
- logger$15.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
16874
+ logger$16.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
16654
16875
  this._nodeId$.next(nodeId);
16655
16876
  }
16656
16877
  }
16657
16878
  setSelfIdIfNull(selfId) {
16658
16879
  if (!this._selfId$.value && selfId) {
16659
- logger$15.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
16880
+ logger$16.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
16660
16881
  this._selfId$.next(selfId);
16661
16882
  }
16662
16883
  }
@@ -16665,7 +16886,7 @@ var WebRTCVertoManager = class extends VertoManager {
16665
16886
  const vertoPongMessage = VertoPong({ ...vertoPing });
16666
16887
  await this.executeVerto(vertoPongMessage);
16667
16888
  } catch (error) {
16668
- logger$15.warn("[WebRTCManager] Call might disconnect, error sending Verto pong:", error);
16889
+ logger$16.warn("[WebRTCManager] Call might disconnect, error sending Verto pong:", error);
16669
16890
  this.onError?.(new VertoPongError(error));
16670
16891
  }
16671
16892
  }
@@ -16675,7 +16896,7 @@ var WebRTCVertoManager = class extends VertoManager {
16675
16896
  if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
16676
16897
  if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
16677
16898
  } catch (error) {
16678
- logger$15.warn("[WebRTCManager] Error updating media constraints:", error);
16899
+ logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
16679
16900
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16680
16901
  throw error;
16681
16902
  }
@@ -16705,20 +16926,20 @@ var WebRTCVertoManager = class extends VertoManager {
16705
16926
  try {
16706
16927
  const pc = this.mainPeerConnection.peerConnection;
16707
16928
  if (!pc) {
16708
- logger$15.warn("[WebRTCManager] No peer connection for keyframe request");
16929
+ logger$16.warn("[WebRTCManager] No peer connection for keyframe request");
16709
16930
  return;
16710
16931
  }
16711
16932
  const videoReceiver = pc.getReceivers().find((r) => r.track.kind === "video");
16712
16933
  if (!videoReceiver) {
16713
- logger$15.warn("[WebRTCManager] No video receiver for keyframe request");
16934
+ logger$16.warn("[WebRTCManager] No video receiver for keyframe request");
16714
16935
  return;
16715
16936
  }
16716
16937
  if (typeof videoReceiver.requestKeyFrame === "function") {
16717
16938
  videoReceiver.requestKeyFrame();
16718
- logger$15.debug("[WebRTCManager] Keyframe requested via RTCRtpReceiver.requestKeyFrame()");
16719
- } else logger$15.debug("[WebRTCManager] requestKeyFrame() not supported, skipping");
16939
+ logger$16.debug("[WebRTCManager] Keyframe requested via RTCRtpReceiver.requestKeyFrame()");
16940
+ } else logger$16.debug("[WebRTCManager] requestKeyFrame() not supported, skipping");
16720
16941
  } catch (error) {
16721
- logger$15.warn("[WebRTCManager] Keyframe request failed (non-fatal):", error);
16942
+ logger$16.warn("[WebRTCManager] Keyframe request failed (non-fatal):", error);
16722
16943
  }
16723
16944
  }
16724
16945
  /**
@@ -16736,13 +16957,13 @@ var WebRTCVertoManager = class extends VertoManager {
16736
16957
  try {
16737
16958
  const controller = this.mainPeerConnection;
16738
16959
  if (!controller.peerConnection) {
16739
- logger$15.warn("[WebRTCManager] No peer connection for ICE restart");
16960
+ logger$16.warn("[WebRTCManager] No peer connection for ICE restart");
16740
16961
  return;
16741
16962
  }
16742
16963
  await controller.triggerIceRestart(relayOnly);
16743
- logger$15.info(`[WebRTCManager] ICE restart initiated${relayOnly ? " (relay-only)" : ""}`);
16964
+ logger$16.info(`[WebRTCManager] ICE restart initiated${relayOnly ? " (relay-only)" : ""}`);
16744
16965
  } catch (error) {
16745
- logger$15.error("[WebRTCManager] ICE restart failed:", error);
16966
+ logger$16.error("[WebRTCManager] ICE restart failed:", error);
16746
16967
  throw error;
16747
16968
  }
16748
16969
  }
@@ -16760,13 +16981,13 @@ var WebRTCVertoManager = class extends VertoManager {
16760
16981
  const entries = Array.from(this._rtcPeerConnectionsMap.entries());
16761
16982
  for (const [id, controller] of entries) try {
16762
16983
  if (!controller.peerConnection) {
16763
- logger$15.debug(`[WebRTCManager] No peer connection for leg ${id}, skipping ICE restart`);
16984
+ logger$16.debug(`[WebRTCManager] No peer connection for leg ${id}, skipping ICE restart`);
16764
16985
  continue;
16765
16986
  }
16766
16987
  await controller.triggerIceRestart(relayOnly);
16767
- logger$15.info(`[WebRTCManager] ICE restart initiated for leg ${id}${relayOnly ? " (relay-only)" : ""}`);
16988
+ logger$16.info(`[WebRTCManager] ICE restart initiated for leg ${id}${relayOnly ? " (relay-only)" : ""}`);
16768
16989
  } catch (error) {
16769
- logger$15.warn(`[WebRTCManager] ICE restart failed for leg ${id}:`, error);
16990
+ logger$16.warn(`[WebRTCManager] ICE restart failed for leg ${id}:`, error);
16770
16991
  }
16771
16992
  }
16772
16993
  /**
@@ -16778,7 +16999,7 @@ var WebRTCVertoManager = class extends VertoManager {
16778
16999
  requestKeyframeAll() {
16779
17000
  for (const [id, controller] of this._rtcPeerConnectionsMap) {
16780
17001
  if (controller.isScreenShare) {
16781
- logger$15.debug(`[WebRTCManager] Skipping keyframe for send-only screen share leg ${id}`);
17002
+ logger$16.debug(`[WebRTCManager] Skipping keyframe for send-only screen share leg ${id}`);
16782
17003
  continue;
16783
17004
  }
16784
17005
  try {
@@ -16788,10 +17009,10 @@ var WebRTCVertoManager = class extends VertoManager {
16788
17009
  if (!videoReceiver) continue;
16789
17010
  if (typeof videoReceiver.requestKeyFrame === "function") {
16790
17011
  videoReceiver.requestKeyFrame();
16791
- logger$15.debug(`[WebRTCManager] Keyframe requested for leg ${id}`);
17012
+ logger$16.debug(`[WebRTCManager] Keyframe requested for leg ${id}`);
16792
17013
  }
16793
17014
  } catch (error) {
16794
- logger$15.warn(`[WebRTCManager] Keyframe request failed for leg ${id} (non-fatal):`, error);
17015
+ logger$16.warn(`[WebRTCManager] Keyframe request failed for leg ${id} (non-fatal):`, error);
16795
17016
  }
16796
17017
  }
16797
17018
  }
@@ -16808,7 +17029,7 @@ var WebRTCVertoManager = class extends VertoManager {
16808
17029
  return this.cachedObservable("vertoMediaParams$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoMediaParamsInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16809
17030
  }
16810
17031
  get vertoBye$() {
16811
- return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
17032
+ return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeInboundMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16812
17033
  }
16813
17034
  get vertoAttach$() {
16814
17035
  return this.cachedObservable("vertoAttach$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoAttachMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
@@ -16852,7 +17073,7 @@ var WebRTCVertoManager = class extends VertoManager {
16852
17073
  default:
16853
17074
  }
16854
17075
  } catch (error) {
16855
- logger$15.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
17076
+ logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
16856
17077
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16857
17078
  if (vertoMethod === "verto.modify") this.onModifyFailed?.();
16858
17079
  }
@@ -16867,19 +17088,33 @@ var WebRTCVertoManager = class extends VertoManager {
16867
17088
  sdp
16868
17089
  });
16869
17090
  } catch (error) {
16870
- logger$15.warn("[WebRTCManager] Error processing modify response:", error);
17091
+ logger$16.warn("[WebRTCManager] Error processing modify response:", error);
16871
17092
  const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
16872
17093
  this.onError?.(modifyError);
16873
17094
  }
16874
17095
  }
16875
17096
  }
17097
+ emitMainSignalingStatus(callId, status) {
17098
+ const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callId);
17099
+ if (!rtcPeerConnController) {
17100
+ const signalingError = new DependencyError(`Cannot emit signaling status, RTCPeerConnectionController not found for callID: ${callId}`);
17101
+ logger$16.error("[WebRTCManager] Failed to emit signaling status:", {
17102
+ callId,
17103
+ status,
17104
+ signalingError
17105
+ });
17106
+ this.onError?.(signalingError);
17107
+ return;
17108
+ }
17109
+ if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
17110
+ }
16876
17111
  processInviteResponse(response, rtcPeerConnController) {
16877
17112
  if (!response.error && getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
16878
- this._signalingStatus$.next("trying");
17113
+ this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
16879
17114
  this._nodeId$.next(getValueFrom(response, "result.node_id") ?? null);
16880
17115
  const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
16881
17116
  const callId = getValueFrom(response, "result.result.result.callID") ?? null;
16882
- logger$15.debug("[WebRTCManager] Verto invite response:", {
17117
+ logger$16.debug("[WebRTCManager] Verto invite response:", {
16883
17118
  callId,
16884
17119
  memberId,
16885
17120
  response
@@ -16889,14 +17124,14 @@ var WebRTCVertoManager = class extends VertoManager {
16889
17124
  if (callId) {
16890
17125
  this.webRtcCallSession.addCallId(callId);
16891
17126
  this.attachManager.attach(this.buildAttachableCall(callId));
16892
- } else logger$15.warn("[WebRTCManager] Cannot attach call, missing callId:", {
17127
+ } else logger$16.warn("[WebRTCManager] Cannot attach call, missing callId:", {
16893
17128
  nodeId: this.nodeId,
16894
17129
  callId
16895
17130
  });
16896
- logger$15.info("[WebRTCManager] Verto invite successful");
16897
- logger$15.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
17131
+ logger$16.info("[WebRTCManager] Verto invite successful");
17132
+ logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
16898
17133
  } else {
16899
- logger$15.error("[WebRTCManager] Verto invite failed:", response);
17134
+ logger$16.error("[WebRTCManager] Verto invite failed:", response);
16900
17135
  const inviteError = response.error ? new JSONRPCError(response.error.code, response.error.message, response.error.data) : /* @__PURE__ */ new Error("Verto invite failed: unexpected response");
16901
17136
  this.onError?.(inviteError);
16902
17137
  }
@@ -16923,6 +17158,7 @@ var WebRTCVertoManager = class extends VertoManager {
16923
17158
  inputVideoStream: options.inputVideoStream,
16924
17159
  receiveAudio: options.receiveAudio,
16925
17160
  receiveVideo: options.receiveVideo,
17161
+ fallbackToReceiveOnly: options.fallbackToReceiveOnly,
16926
17162
  webRTCApiProvider: this.webRTCApiProvider,
16927
17163
  preferredVideoCodecs: options.preferredVideoCodecs,
16928
17164
  preferredAudioCodecs: options.preferredAudioCodecs,
@@ -16941,17 +17177,17 @@ var WebRTCVertoManager = class extends VertoManager {
16941
17177
  if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
16942
17178
  }
16943
17179
  async handleInboundAnswer(rtcPeerConnController) {
16944
- logger$15.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
17180
+ logger$16.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
16945
17181
  const vertoByeOrAccepted = await (0, import_cjs$15.firstValueFrom)((0, import_cjs$15.race)(this.vertoBye$, this.webRtcCallSession.answered$).pipe((0, import_cjs$15.takeUntil)(this.destroyed$))).catch(() => null);
16946
17182
  if (vertoByeOrAccepted === null) {
16947
- logger$15.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
17183
+ logger$16.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
16948
17184
  return;
16949
17185
  }
16950
- if (isVertoByeMessage(vertoByeOrAccepted)) {
16951
- logger$15.info("[WebRTCManager] Inbound call ended by remote before answer.");
17186
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
17187
+ logger$16.info("[WebRTCManager] Inbound call ended by remote before answer.");
16952
17188
  this.callSession?.destroy();
16953
17189
  } else if (!vertoByeOrAccepted) {
16954
- logger$15.info("[WebRTCManager] Inbound call rejected by user.");
17190
+ logger$16.info("[WebRTCManager] Inbound call rejected by user.");
16955
17191
  try {
16956
17192
  await this.bye("USER_BUSY");
16957
17193
  } finally {
@@ -16959,19 +17195,19 @@ var WebRTCVertoManager = class extends VertoManager {
16959
17195
  this.callSession?.destroy();
16960
17196
  }
16961
17197
  } else {
16962
- logger$15.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
17198
+ logger$16.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
16963
17199
  const answerOptions = this.webRtcCallSession.answerMediaOptions;
16964
17200
  try {
16965
17201
  await rtcPeerConnController.acceptInbound(answerOptions);
16966
17202
  } catch (error) {
16967
- logger$15.error("[WebRTCManager] Error creating inbound answer:", error);
17203
+ logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
16968
17204
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16969
17205
  }
16970
17206
  }
16971
17207
  }
16972
17208
  setupVertoAttachHandler() {
16973
17209
  this.subscribeTo(this.vertoAttach$, async (vertoAttach) => {
16974
- logger$15.debug("[WebRTCManager] Received Verto attach event for existing call:", vertoAttach);
17210
+ logger$16.debug("[WebRTCManager] Received Verto attach event for existing call:", vertoAttach);
16975
17211
  const { callID } = vertoAttach;
16976
17212
  await this.attachManager.attach({
16977
17213
  nodeId: this.nodeId ?? void 0,
@@ -17043,17 +17279,17 @@ var WebRTCVertoManager = class extends VertoManager {
17043
17279
  };
17044
17280
  }
17045
17281
  async sendLocalDescriptionOnceAccepted(vertoMessageRequest, rtcPeerConnectionController) {
17046
- logger$15.debug("[WebRTCManager] Waiting for call to be accepted or ended before sending answer");
17282
+ logger$16.debug("[WebRTCManager] Waiting for call to be accepted or ended before sending answer");
17047
17283
  const vertoByeOrAccepted = await (0, import_cjs$15.firstValueFrom)((0, import_cjs$15.race)(this.vertoBye$, this.webRtcCallSession.answered$).pipe((0, import_cjs$15.takeUntil)(this.destroyed$))).catch(() => null);
17048
17284
  if (vertoByeOrAccepted === null) {
17049
- logger$15.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
17285
+ logger$16.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
17050
17286
  return;
17051
17287
  }
17052
- if (isVertoByeMessage(vertoByeOrAccepted)) {
17053
- logger$15.info("[WebRTCManager] Call ended before answer was sent.");
17288
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
17289
+ logger$16.info("[WebRTCManager] Call ended before answer was sent.");
17054
17290
  this.callSession?.destroy();
17055
17291
  } else if (!vertoByeOrAccepted) {
17056
- logger$15.info("[WebRTCManager] Call was not accepted, sending verto.bye.");
17292
+ logger$16.info("[WebRTCManager] Call was not accepted, sending verto.bye.");
17057
17293
  try {
17058
17294
  await this.bye("USER_BUSY");
17059
17295
  } finally {
@@ -17061,14 +17297,14 @@ var WebRTCVertoManager = class extends VertoManager {
17061
17297
  this.callSession?.destroy();
17062
17298
  }
17063
17299
  } else {
17064
- logger$15.debug("[WebRTCManager] Call accepted, sending answer");
17300
+ logger$16.debug("[WebRTCManager] Call accepted, sending answer");
17065
17301
  try {
17066
- this._signalingStatus$.next("connecting");
17302
+ this.emitMainSignalingStatus(rtcPeerConnectionController.id, "connecting");
17067
17303
  await this.sendLocalDescription(vertoMessageRequest, rtcPeerConnectionController);
17068
17304
  await rtcPeerConnectionController.updateAnswerStatus({ status: "sent" });
17069
17305
  await this.attachManager.attach(this.buildAttachableCall());
17070
17306
  } catch (error) {
17071
- logger$15.error("[WebRTCManager] Error sending Verto answer:", error);
17307
+ logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
17072
17308
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17073
17309
  await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
17074
17310
  }
@@ -17149,9 +17385,11 @@ var WebRTCVertoManager = class extends VertoManager {
17149
17385
  await this.initAdditionalPeerConnection("screenshare", options);
17150
17386
  }
17151
17387
  async initAdditionalPeerConnection(propose, options) {
17388
+ const isScreenShare = propose === "screenshare";
17389
+ let firstPeerConnectionError;
17152
17390
  let rtcPeerConnController = null;
17153
17391
  try {
17154
- this._screenShareStatus$.next("starting");
17392
+ if (isScreenShare) this._screenShareStatus$.next("starting");
17155
17393
  rtcPeerConnController = new RTCPeerConnectionController({
17156
17394
  ...options,
17157
17395
  ...this.RTCPeerConnectionConfig,
@@ -17159,21 +17397,27 @@ var WebRTCVertoManager = class extends VertoManager {
17159
17397
  webRTCApiProvider: this.webRTCApiProvider
17160
17398
  }, void 0, this.deviceController);
17161
17399
  this.setupLocalDescriptionHandler(rtcPeerConnController);
17162
- if (propose === "screenshare") this._screenShareId = rtcPeerConnController.id;
17400
+ if (isScreenShare) this._screenShareId = rtcPeerConnController.id;
17163
17401
  this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
17164
17402
  this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
17165
17403
  this.subscribeTo(rtcPeerConnController.errors$, (error) => {
17166
- this.onError?.(error);
17404
+ firstPeerConnectionError ??= error;
17405
+ this.onError?.(error, { fatal: false });
17167
17406
  });
17168
17407
  await (0, import_cjs$15.firstValueFrom)(rtcPeerConnController.connectionState$.pipe((0, import_cjs$15.filter)((state) => state === "connected"), (0, import_cjs$15.take)(1), (0, import_cjs$15.timeout)(this._screenShareTimeoutMs), (0, import_cjs$15.takeUntil)(this.destroyed$)));
17169
- this._screenShareStatus$.next("started");
17170
- logger$15.info("[WebRTCManager] Screen share started successfully.");
17408
+ if (isScreenShare) this._screenShareStatus$.next("started");
17409
+ logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
17171
17410
  return rtcPeerConnController.id;
17172
17411
  } catch (error) {
17173
- logger$15.warn("[WebRTCManager] Error initializing additional peer connection:", error);
17174
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17412
+ logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
17175
17413
  if (rtcPeerConnController) rtcPeerConnController.destroy();
17176
- this._screenShareStatus$.next("none");
17414
+ if (isScreenShare) this._screenShareStatus$.next("none");
17415
+ if (firstPeerConnectionError) throw firstPeerConnectionError instanceof MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
17416
+ if (error instanceof import_cjs$15.EmptyError) {
17417
+ logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
17418
+ return;
17419
+ }
17420
+ throw error instanceof Error ? error : new Error(String(error), { cause: error });
17177
17421
  }
17178
17422
  }
17179
17423
  async removeInputDevices(id) {
@@ -17189,9 +17433,9 @@ var WebRTCVertoManager = class extends VertoManager {
17189
17433
  if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
17190
17434
  }
17191
17435
  async removeScreenMedia() {
17192
- if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$15.warn("[WebRTCManager] No active screen share to stop.");
17436
+ if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$16.warn("[WebRTCManager] No active screen share to stop.");
17193
17437
  if (!this._screenShareId) {
17194
- logger$15.debug("[WebRTCManager] No screen share peer connection found.");
17438
+ logger$16.debug("[WebRTCManager] No screen share peer connection found.");
17195
17439
  return;
17196
17440
  }
17197
17441
  this._screenShareStatus$.next("stopping");
@@ -17220,7 +17464,7 @@ var WebRTCVertoManager = class extends VertoManager {
17220
17464
  dialogParams: this.dialogParams(rtcPeerConnController)
17221
17465
  }));
17222
17466
  } catch (error) {
17223
- logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
17467
+ logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
17224
17468
  throw error;
17225
17469
  }
17226
17470
  }
@@ -17238,7 +17482,7 @@ var WebRTCVertoManager = class extends VertoManager {
17238
17482
  try {
17239
17483
  await this.executeVerto(vertoInfoMessage);
17240
17484
  } catch (error) {
17241
- logger$15.warn("[WebRTCManager] Error sending DTMF digits:", error);
17485
+ logger$16.warn("[WebRTCManager] Error sending DTMF digits:", error);
17242
17486
  throw error;
17243
17487
  }
17244
17488
  }
@@ -17249,10 +17493,10 @@ var WebRTCVertoManager = class extends VertoManager {
17249
17493
  action: "transfer"
17250
17494
  });
17251
17495
  try {
17252
- logger$15.debug("[WebRTCManager] Transferring call with options:", options);
17496
+ logger$16.debug("[WebRTCManager] Transferring call with options:", options);
17253
17497
  await this.executeVerto(message);
17254
17498
  } catch (error) {
17255
- logger$15.error("[WebRTCManager] Error transferring call:", error);
17499
+ logger$16.error("[WebRTCManager] Error transferring call:", error);
17256
17500
  throw error;
17257
17501
  }
17258
17502
  }
@@ -17269,7 +17513,7 @@ var WebRTCVertoManager = class extends VertoManager {
17269
17513
  //#endregion
17270
17514
  //#region src/controllers/RemoteAudioMeter.ts
17271
17515
  var import_cjs$14 = require_cjs();
17272
- const logger$14 = getLogger();
17516
+ const logger$15 = getLogger();
17273
17517
  /**
17274
17518
  * Read-only audio level meter for a remote MediaStream. Attaches an
17275
17519
  * AnalyserNode to a MediaStreamAudioSourceNode so it observes the stream
@@ -17304,7 +17548,7 @@ var RemoteAudioMeter = class extends Destroyable {
17304
17548
  try {
17305
17549
  this._source.disconnect();
17306
17550
  } catch (error) {
17307
- logger$14.debug("[RemoteAudioMeter] source disconnect warning:", error);
17551
+ logger$15.debug("[RemoteAudioMeter] source disconnect warning:", error);
17308
17552
  }
17309
17553
  this._source = null;
17310
17554
  this._stream = null;
@@ -17321,7 +17565,7 @@ var RemoteAudioMeter = class extends Destroyable {
17321
17565
  this._source = null;
17322
17566
  }
17323
17567
  this._audioContext.close().catch((error) => {
17324
- logger$14.debug("[RemoteAudioMeter] audio context close warning:", error);
17568
+ logger$15.debug("[RemoteAudioMeter] audio context close warning:", error);
17325
17569
  });
17326
17570
  super.destroy();
17327
17571
  }
@@ -17340,7 +17584,7 @@ var RemoteAudioMeter = class extends Destroyable {
17340
17584
  //#endregion
17341
17585
  //#region src/controllers/RTCStatsMonitor.ts
17342
17586
  var import_cjs$13 = require_cjs();
17343
- const logger$13 = getLogger();
17587
+ const logger$14 = getLogger();
17344
17588
  const DEFAULT_POLLING_INTERVAL_MS = 1e3;
17345
17589
  const DEFAULT_BASELINE_SAMPLES = 10;
17346
17590
  const DEFAULT_NO_AUDIO_PACKET_THRESHOLD_MS = 2e3;
@@ -17430,9 +17674,9 @@ var RTCStatsMonitor = class extends Destroyable {
17430
17674
  const now = Date.now();
17431
17675
  this.lastAudioPacketChangeTime = now;
17432
17676
  this.lastVideoPacketChangeTime = now;
17433
- logger$13.debug("[RTCStatsMonitor] Starting stats monitoring");
17677
+ logger$14.debug("[RTCStatsMonitor] Starting stats monitoring");
17434
17678
  this.subscribeTo((0, import_cjs$13.interval)(this.pollingIntervalMs).pipe((0, import_cjs$13.filter)(() => this.running), (0, import_cjs$13.switchMap)(() => (0, import_cjs$13.from)(this.peerConnection.getStats()).pipe((0, import_cjs$13.catchError)((err) => {
17435
- logger$13.warn("[RTCStatsMonitor] Failed to get stats:", err);
17679
+ logger$14.warn("[RTCStatsMonitor] Failed to get stats:", err);
17436
17680
  return import_cjs$13.EMPTY;
17437
17681
  }))), (0, import_cjs$13.filter)(() => this.running), (0, import_cjs$13.map)((report) => this.extractSample(report))), (sample$1) => this._sample$.next(sample$1));
17438
17682
  this.subscribeTo(this._sample$.pipe((0, import_cjs$13.take)(this.baselineSampleCount), (0, import_cjs$13.toArray)(), (0, import_cjs$13.map)((samples) => ({
@@ -17440,7 +17684,7 @@ var RTCStatsMonitor = class extends Destroyable {
17440
17684
  jitter: samples.reduce((s, b) => s + b.audioJitter, 0) / samples.length,
17441
17685
  ready: true
17442
17686
  }))), (baseline) => {
17443
- logger$13.debug(`[RTCStatsMonitor] Baseline established: rtt=${baseline.rtt.toFixed(1)}ms, jitter=${baseline.jitter.toFixed(1)}ms`);
17687
+ logger$14.debug(`[RTCStatsMonitor] Baseline established: rtt=${baseline.rtt.toFixed(1)}ms, jitter=${baseline.jitter.toFixed(1)}ms`);
17444
17688
  this._baseline$.next(baseline);
17445
17689
  });
17446
17690
  this.subscribeTo(this._sample$.pipe((0, import_cjs$13.scan)((acc, sample$1) => ({
@@ -17477,10 +17721,10 @@ var RTCStatsMonitor = class extends Destroyable {
17477
17721
  stop() {
17478
17722
  if (!this.running) return;
17479
17723
  this.running = false;
17480
- logger$13.debug("[RTCStatsMonitor] Stopping stats monitoring");
17724
+ logger$14.debug("[RTCStatsMonitor] Stopping stats monitoring");
17481
17725
  }
17482
17726
  destroy() {
17483
- logger$13.debug("[RTCStatsMonitor] Destroying RTCStatsMonitor");
17727
+ logger$14.debug("[RTCStatsMonitor] Destroying RTCStatsMonitor");
17484
17728
  this.stop();
17485
17729
  super.destroy();
17486
17730
  }
@@ -17609,7 +17853,7 @@ var RTCStatsMonitor = class extends Destroyable {
17609
17853
  //#endregion
17610
17854
  //#region src/managers/CallRecoveryManager.ts
17611
17855
  var import_cjs$12 = require_cjs();
17612
- const logger$12 = getLogger();
17856
+ const logger$13 = getLogger();
17613
17857
  const DEFAULT_DEBOUNCE_TIME_MS = 2e3;
17614
17858
  const DEFAULT_COOLDOWN_MS = 1e4;
17615
17859
  const DEFAULT_ICE_GRACE_PERIOD_MS = 3e3;
@@ -17706,10 +17950,10 @@ var CallRecoveryManager = class extends Destroyable {
17706
17950
  */
17707
17951
  async requestIceRestart() {
17708
17952
  if (this._recoveryState$.value === "recovering") {
17709
- logger$12.info("CallRecoveryManager: manual ICE restart skipped — recovery already in progress");
17953
+ logger$13.info("CallRecoveryManager: manual ICE restart skipped — recovery already in progress");
17710
17954
  return;
17711
17955
  }
17712
- logger$12.info("CallRecoveryManager: manual ICE restart requested");
17956
+ logger$13.info("CallRecoveryManager: manual ICE restart requested");
17713
17957
  this.transitionTo("recovering");
17714
17958
  await this.executeIceRestart(false);
17715
17959
  this.startCooldown();
@@ -17725,7 +17969,7 @@ var CallRecoveryManager = class extends Destroyable {
17725
17969
  * WebSocket reconnect or call state recovers to 'connected'.
17726
17970
  */
17727
17971
  reset() {
17728
- logger$12.info("CallRecoveryManager: resetting counters");
17972
+ logger$13.info("CallRecoveryManager: resetting counters");
17729
17973
  this._attemptCount = 0;
17730
17974
  this._keyframeBurstCount = 0;
17731
17975
  this._keyframeBurstStart = 0;
@@ -17740,7 +17984,7 @@ var CallRecoveryManager = class extends Destroyable {
17740
17984
  */
17741
17985
  notifyModifyFailed() {
17742
17986
  if (this._recoveryState$.value === "cooldown" || this._recoveryState$.value === "idle") {
17743
- logger$12.info("CallRecoveryManager: verto.modify failed — re-entering recovery");
17987
+ logger$13.info("CallRecoveryManager: verto.modify failed — re-entering recovery");
17744
17988
  this._cooldownUntil = 0;
17745
17989
  this.transitionTo("idle");
17746
17990
  this.pushTrigger({
@@ -17764,7 +18008,7 @@ var CallRecoveryManager = class extends Destroyable {
17764
18008
  reason: `bandwidth ${bitrateKbps}kbps below threshold ${this._config.degradationBitrateThreshold}kbps`,
17765
18009
  timestamp: Date.now()
17766
18010
  });
17767
- logger$12.warn(`CallRecoveryManager: disabling video — bandwidth ${bitrateKbps}kbps < ${this._config.degradationBitrateThreshold}kbps`);
18011
+ logger$13.warn(`CallRecoveryManager: disabling video — bandwidth ${bitrateKbps}kbps < ${this._config.degradationBitrateThreshold}kbps`);
17768
18012
  } else if (wasConstrained && bitrateKbps >= this._config.degradationRecoveryThreshold) {
17769
18013
  this._bandwidthConstrained$.next(false);
17770
18014
  this._callbacks.enableVideo();
@@ -17773,7 +18017,7 @@ var CallRecoveryManager = class extends Destroyable {
17773
18017
  reason: `bandwidth ${bitrateKbps}kbps recovered above ${this._config.degradationRecoveryThreshold}kbps`,
17774
18018
  timestamp: Date.now()
17775
18019
  });
17776
- logger$12.info(`CallRecoveryManager: restoring video — bandwidth ${bitrateKbps}kbps >= ${this._config.degradationRecoveryThreshold}kbps`);
18020
+ logger$13.info(`CallRecoveryManager: restoring video — bandwidth ${bitrateKbps}kbps >= ${this._config.degradationRecoveryThreshold}kbps`);
17777
18021
  }
17778
18022
  }
17779
18023
  /**
@@ -17792,14 +18036,14 @@ var CallRecoveryManager = class extends Destroyable {
17792
18036
  handleWebSocketReconnect() {
17793
18037
  const pcState = this._callbacks.getPeerConnectionState();
17794
18038
  if (pcState === "connected" || pcState === "completed") {
17795
- logger$12.info("CallRecoveryManager: signal-only reconnect — peer connection still alive");
18039
+ logger$13.info("CallRecoveryManager: signal-only reconnect — peer connection still alive");
17796
18040
  this.emitEvent({
17797
18041
  action: "signal_reconnect",
17798
18042
  reason: "WebSocket reconnected, peer connection still connected",
17799
18043
  timestamp: Date.now()
17800
18044
  });
17801
18045
  } else {
17802
- logger$12.info("CallRecoveryManager: full reconnect — peer connection also down");
18046
+ logger$13.info("CallRecoveryManager: full reconnect — peer connection also down");
17803
18047
  this.emitEvent({
17804
18048
  action: "full_reconnect",
17805
18049
  reason: "WebSocket reconnected, peer connection not connected — ICE restart needed",
@@ -17823,7 +18067,7 @@ var CallRecoveryManager = class extends Destroyable {
17823
18067
  }), (0, import_cjs$12.debounceTime)(this._config.debounceTimeMs), (0, import_cjs$12.withLatestFrom)(this._inputs.signalingReady$), (0, import_cjs$12.filter)(([, signalingReady]) => this.passGateChecks(signalingReady)), (0, import_cjs$12.map)(([trigger]) => trigger), (0, import_cjs$12.exhaustMap)((trigger) => this.executeTieredRecovery(trigger)), (0, import_cjs$12.takeUntil)((0, import_cjs$12.merge)(this._destroyed$, this._pipelineStop$))), {
17824
18068
  next: () => {},
17825
18069
  error: (err) => {
17826
- logger$12.error("CallRecoveryManager: pipeline error", err);
18070
+ logger$13.error("CallRecoveryManager: pipeline error", err);
17827
18071
  this.transitionTo("idle");
17828
18072
  }
17829
18073
  });
@@ -17850,27 +18094,27 @@ var CallRecoveryManager = class extends Destroyable {
17850
18094
  reason: `no packet loss for ${this._config.packetLossRecoveryDelaySec}s — restoring video`,
17851
18095
  timestamp: Date.now()
17852
18096
  });
17853
- logger$12.info(`CallRecoveryManager: restoring video — no packet loss for ${this._config.packetLossRecoveryDelaySec}s`);
18097
+ logger$13.info(`CallRecoveryManager: restoring video — no packet loss for ${this._config.packetLossRecoveryDelaySec}s`);
17854
18098
  });
17855
18099
  }
17856
18100
  passGateChecks(signalingReady) {
17857
18101
  if (this._callbacks.isNegotiating()) {
17858
- logger$12.debug("CallRecoveryManager: gate blocked — negotiation in progress");
18102
+ logger$13.debug("CallRecoveryManager: gate blocked — negotiation in progress");
17859
18103
  this.transitionTo("idle");
17860
18104
  return false;
17861
18105
  }
17862
18106
  if (!signalingReady) {
17863
- logger$12.debug("CallRecoveryManager: gate blocked — signaling not ready");
18107
+ logger$13.debug("CallRecoveryManager: gate blocked — signaling not ready");
17864
18108
  this.transitionTo("idle");
17865
18109
  return false;
17866
18110
  }
17867
18111
  if (!this._callbacks.isCallConnected()) {
17868
- logger$12.debug("CallRecoveryManager: gate blocked — call not connected");
18112
+ logger$13.debug("CallRecoveryManager: gate blocked — call not connected");
17869
18113
  this.transitionTo("idle");
17870
18114
  return false;
17871
18115
  }
17872
18116
  if (this.isCooldownActive()) {
17873
- logger$12.debug("CallRecoveryManager: gate blocked — cooldown active");
18117
+ logger$13.debug("CallRecoveryManager: gate blocked — cooldown active");
17874
18118
  this.transitionTo("cooldown");
17875
18119
  return false;
17876
18120
  }
@@ -17881,9 +18125,9 @@ var CallRecoveryManager = class extends Destroyable {
17881
18125
  }
17882
18126
  executeTieredRecovery(trigger) {
17883
18127
  this.transitionTo("recovering");
17884
- logger$12.info(`CallRecoveryManager: starting tiered recovery — source=${trigger.source} detail=${trigger.detail}`);
18128
+ logger$13.info(`CallRecoveryManager: starting tiered recovery — source=${trigger.source} detail=${trigger.detail}`);
17885
18129
  return (0, import_cjs$12.from)(this.runTiers(trigger)).pipe((0, import_cjs$12.tap)(() => this.startCooldown()), (0, import_cjs$12.catchError)((err) => {
17886
- logger$12.error("CallRecoveryManager: tiered recovery failed", err);
18130
+ logger$13.error("CallRecoveryManager: tiered recovery failed", err);
17887
18131
  this.startCooldown();
17888
18132
  return import_cjs$12.EMPTY;
17889
18133
  }));
@@ -17891,7 +18135,7 @@ var CallRecoveryManager = class extends Destroyable {
17891
18135
  async runTiers(trigger) {
17892
18136
  this.executeKeyframe(trigger.detail);
17893
18137
  if (trigger.issueType && DEGRADATION_ONLY_ISSUES.has(trigger.issueType)) {
17894
- logger$12.debug(`CallRecoveryManager: degradation-only issue (${trigger.issueType}) — Tier 1 only, skipping ICE restart`);
18138
+ logger$13.debug(`CallRecoveryManager: degradation-only issue (${trigger.issueType}) — Tier 1 only, skipping ICE restart`);
17895
18139
  return;
17896
18140
  }
17897
18141
  if (this._attemptCount < this._config.maxAttempts) {
@@ -17908,13 +18152,13 @@ var CallRecoveryManager = class extends Destroyable {
17908
18152
  maxAttempts: this._config.maxAttempts,
17909
18153
  timestamp: Date.now()
17910
18154
  });
17911
- logger$12.warn("CallRecoveryManager: max recovery attempts reached");
18155
+ logger$13.warn("CallRecoveryManager: max recovery attempts reached");
17912
18156
  }
17913
18157
  }
17914
18158
  executeKeyframe(reason) {
17915
18159
  const now = Date.now();
17916
18160
  if (now < this._keyframeCooldownUntil) {
17917
- logger$12.debug("CallRecoveryManager: keyframe request skipped — cooldown active");
18161
+ logger$13.debug("CallRecoveryManager: keyframe request skipped — cooldown active");
17918
18162
  return;
17919
18163
  }
17920
18164
  if (now - this._keyframeBurstStart > this._config.keyframeBurstWindowMs) {
@@ -17923,7 +18167,7 @@ var CallRecoveryManager = class extends Destroyable {
17923
18167
  }
17924
18168
  if (this._keyframeBurstCount >= this._config.keyframeMaxBurst) {
17925
18169
  this._keyframeCooldownUntil = now + this._config.keyframeCooldownMs;
17926
- logger$12.debug(`CallRecoveryManager: keyframe burst limit reached (${this._config.keyframeMaxBurst}), cooldown until ${this._keyframeCooldownUntil}`);
18170
+ logger$13.debug(`CallRecoveryManager: keyframe burst limit reached (${this._config.keyframeMaxBurst}), cooldown until ${this._keyframeCooldownUntil}`);
17927
18171
  return;
17928
18172
  }
17929
18173
  this._keyframeBurstCount += 1;
@@ -17933,12 +18177,12 @@ var CallRecoveryManager = class extends Destroyable {
17933
18177
  reason,
17934
18178
  timestamp: now
17935
18179
  });
17936
- logger$12.debug(`CallRecoveryManager: keyframe requested (burst ${this._keyframeBurstCount}/${this._config.keyframeMaxBurst})`);
18180
+ logger$13.debug(`CallRecoveryManager: keyframe requested (burst ${this._keyframeBurstCount}/${this._config.keyframeMaxBurst})`);
17937
18181
  }
17938
18182
  async executeIceRestart(relayOnly) {
17939
18183
  this._attemptCount += 1;
17940
18184
  const tier = relayOnly ? "Tier 3 (relay-only)" : "Tier 2 (standard)";
17941
- logger$12.info(`CallRecoveryManager: ${tier} ICE restart — attempt ${this._attemptCount}/${this._config.maxAttempts}`);
18185
+ logger$13.info(`CallRecoveryManager: ${tier} ICE restart — attempt ${this._attemptCount}/${this._config.maxAttempts}`);
17942
18186
  this.emitEvent({
17943
18187
  action: "reinvite_started",
17944
18188
  reason: `${tier} ICE restart`,
@@ -17955,7 +18199,7 @@ var CallRecoveryManager = class extends Destroyable {
17955
18199
  maxAttempts: this._config.maxAttempts,
17956
18200
  timestamp: Date.now()
17957
18201
  });
17958
- logger$12.info(`CallRecoveryManager: ${tier} ICE restart succeeded`);
18202
+ logger$13.info(`CallRecoveryManager: ${tier} ICE restart succeeded`);
17959
18203
  this._attemptCount = 0;
17960
18204
  return true;
17961
18205
  }
@@ -17966,7 +18210,7 @@ var CallRecoveryManager = class extends Destroyable {
17966
18210
  maxAttempts: this._config.maxAttempts,
17967
18211
  timestamp: Date.now()
17968
18212
  });
17969
- logger$12.warn(`CallRecoveryManager: ${tier} ICE restart failed`);
18213
+ logger$13.warn(`CallRecoveryManager: ${tier} ICE restart failed`);
17970
18214
  return false;
17971
18215
  } catch {
17972
18216
  this.emitEvent({
@@ -17976,7 +18220,7 @@ var CallRecoveryManager = class extends Destroyable {
17976
18220
  maxAttempts: this._config.maxAttempts,
17977
18221
  timestamp: Date.now()
17978
18222
  });
17979
- logger$12.warn(`CallRecoveryManager: ${tier} ICE restart timed out`);
18223
+ logger$13.warn(`CallRecoveryManager: ${tier} ICE restart timed out`);
17980
18224
  return false;
17981
18225
  }
17982
18226
  }
@@ -17997,7 +18241,7 @@ var CallRecoveryManager = class extends Destroyable {
17997
18241
  transitionTo(state) {
17998
18242
  const prev = this._recoveryState$.value;
17999
18243
  if (prev !== state) {
18000
- logger$12.debug(`CallRecoveryManager: state ${prev} -> ${state}`);
18244
+ logger$13.debug(`CallRecoveryManager: state ${prev} -> ${state}`);
18001
18245
  this._recoveryState$.next(state);
18002
18246
  }
18003
18247
  }
@@ -18100,7 +18344,7 @@ function mosToQualityLevel(mos) {
18100
18344
  //#endregion
18101
18345
  //#region src/core/entities/Call.ts
18102
18346
  var import_cjs$11 = require_cjs();
18103
- const logger$11 = getLogger();
18347
+ const logger$12 = getLogger();
18104
18348
  /**
18105
18349
  * Ratio between the critical and warning RTT spike multipliers.
18106
18350
  * Warning threshold = baseline * warningMultiplier (default 3x)
@@ -18118,7 +18362,7 @@ const fromDestinationParams = (destination) => {
18118
18362
  });
18119
18363
  return params;
18120
18364
  } catch (error) {
18121
- logger$11.warn(`Failed to parse destination URI: ${destination}`, error);
18365
+ logger$12.warn(`Failed to parse destination URI: ${destination}`, error);
18122
18366
  return {};
18123
18367
  }
18124
18368
  };
@@ -18252,7 +18496,7 @@ var WebRTCCall = class extends Destroyable {
18252
18496
  /** Toggles the call lock state, preventing or allowing new participants from joining. */
18253
18497
  async toggleLock() {
18254
18498
  const method = this.locked ? "call.unlock" : "call.lock";
18255
- await this.executeMethod(this.selfId ?? "", method, {});
18499
+ await this.executeMethod(this.callSelf, method, {});
18256
18500
  }
18257
18501
  /**
18258
18502
  * Toggles the hold state of the call (pauses/resumes local media transmission).
@@ -18301,14 +18545,24 @@ var WebRTCCall = class extends Destroyable {
18301
18545
  *
18302
18546
  * Constructs call context (node_id, call_id, member_id) and sends the RPC request.
18303
18547
  *
18304
- * @param target - Target member ID string, or a {@link MemberTarget} object.
18548
+ * @param target - Target {@link MemberTarget} triple, or the local member's
18549
+ * ID string for self-operations (any other string is rejected — a bare
18550
+ * member id cannot carry the remote member's own call context).
18305
18551
  * @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
18306
18552
  * @param args - Parameters for the RPC method.
18307
18553
  * @returns The RPC response.
18554
+ * @throws {CallNotReadyError} If the call has no self member context yet.
18555
+ * @throws {InvalidParams} If a string target is not the local member's ID.
18308
18556
  * @throws {JSONRPCError} If the RPC call returns an error.
18309
18557
  */
18310
18558
  async executeMethod(target, method, args) {
18311
- const params = this.buildMethodParams(target, args);
18559
+ const self = this.callSelf;
18560
+ if (typeof target === "string" && target !== self.member_id) throw new InvalidParams(`Target member ID ${target} does not match call's self member ID ${self.member_id}`);
18561
+ const params = {
18562
+ ...args,
18563
+ self,
18564
+ target: typeof target === "string" ? self : target
18565
+ };
18312
18566
  const request = buildRPCRequest({
18313
18567
  method,
18314
18568
  params
@@ -18318,29 +18572,27 @@ var WebRTCCall = class extends Destroyable {
18318
18572
  if (isJSONRPCErrorResponse(response)) throw new JSONRPCError(parseInt(response.result?.code ?? "0"), `Error response from method ${method}: ${response.result?.code} ${response.result?.message}`, void 0, void 0, request.id);
18319
18573
  return response;
18320
18574
  } catch (error) {
18321
- logger$11.error(`[Call] Error executing method ${method} with params`, params, error);
18575
+ logger$12.error(`[Call] Error executing method ${method} with params`, params, error);
18322
18576
  throw error;
18323
18577
  }
18324
18578
  }
18325
- buildMethodParams(target, args) {
18326
- const self = {
18327
- node_id: this.nodeId ?? "",
18328
- call_id: this.id,
18329
- member_id: this.vertoManager.selfId ?? ""
18330
- };
18331
- if (typeof target === "object") return {
18332
- ...args,
18333
- self,
18334
- targets: [target]
18335
- };
18579
+ /**
18580
+ * The local leg's member triple — sent as `self` in every member RPC
18581
+ * envelope, and as the `target` of call-scoped self-operations (e.g. lock,
18582
+ * layout).
18583
+ *
18584
+ * @throws {CallNotReadyError} Before `call.joined` delivers the self member
18585
+ * context (`selfId`/`nodeId`) an RPC without it cannot be routed, so fail
18586
+ * fast instead of sending a doomed request.
18587
+ */
18588
+ get callSelf() {
18589
+ const node_id = this.nodeId;
18590
+ const member_id = this.vertoManager.selfId;
18591
+ if (!node_id || !member_id) throw new CallNotReadyError(this.id);
18336
18592
  return {
18337
- ...args,
18338
- self,
18339
- target: {
18340
- node_id: this.nodeId ?? "",
18341
- call_id: this.id,
18342
- member_id: target
18343
- }
18593
+ node_id,
18594
+ call_id: this.id,
18595
+ member_id
18344
18596
  };
18345
18597
  }
18346
18598
  /** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
@@ -18521,9 +18773,9 @@ var WebRTCCall = class extends Destroyable {
18521
18773
  */
18522
18774
  initResilienceSubsystems() {
18523
18775
  const pc = this.rtcPeerConnection;
18524
- logger$11.debug(`[Call] initResilienceSubsystems: pc=${pc ? "exists" : "undefined"}, connectionState=${pc?.connectionState}`);
18776
+ logger$12.debug(`[Call] initResilienceSubsystems: pc=${pc ? "exists" : "undefined"}, connectionState=${pc?.connectionState}`);
18525
18777
  if (!pc) {
18526
- logger$11.warn("[Call] No peer connection available, skipping resilience init");
18778
+ logger$12.warn("[Call] No peer connection available, skipping resilience init");
18527
18779
  return;
18528
18780
  }
18529
18781
  try {
@@ -18558,14 +18810,14 @@ var WebRTCCall = class extends Destroyable {
18558
18810
  disableVideo: () => {
18559
18811
  try {
18560
18812
  this.vertoManager.muteMainVideoInputDevice();
18561
- logger$11.debug("[Call] Recovery manager disabled video");
18813
+ logger$12.debug("[Call] Recovery manager disabled video");
18562
18814
  } catch {
18563
- logger$11.debug("[Call] Recovery manager failed to disable video");
18815
+ logger$12.debug("[Call] Recovery manager failed to disable video");
18564
18816
  }
18565
18817
  },
18566
18818
  enableVideo: () => {
18567
18819
  this.vertoManager.unmuteMainVideoInputDevice().catch(() => {
18568
- logger$11.debug("[Call] Recovery manager failed to enable video");
18820
+ logger$12.debug("[Call] Recovery manager failed to enable video");
18569
18821
  });
18570
18822
  },
18571
18823
  isNegotiating: () => this.vertoManager.mainPeerConnection.isNegotiating,
@@ -18615,7 +18867,7 @@ var WebRTCCall = class extends Destroyable {
18615
18867
  this.subscribeTo(this._recoveryManager.recoveryEvent$, (event) => {
18616
18868
  this._recoveryEvent$.next(event);
18617
18869
  if (event.action === "max_attempts_reached") {
18618
- logger$11.warn("[Call] All recovery attempts exhausted, terminating call");
18870
+ logger$12.warn("[Call] All recovery attempts exhausted, terminating call");
18619
18871
  this.emitError({
18620
18872
  kind: "network",
18621
18873
  fatal: true,
@@ -18635,13 +18887,13 @@ var WebRTCCall = class extends Destroyable {
18635
18887
  else if (event.type === "online") this._recoveryManager?.handleWebSocketReconnect();
18636
18888
  });
18637
18889
  this.subscribeTo(this.clientSession.authenticated$.pipe((0, import_cjs$11.skip)(1), (0, import_cjs$11.filter)(Boolean)), () => {
18638
- logger$11.debug("[Call] WebSocket reconnected — notifying recovery manager");
18890
+ logger$12.debug("[Call] WebSocket reconnected — notifying recovery manager");
18639
18891
  this._recoveryManager?.handleWebSocketReconnect();
18640
18892
  });
18641
18893
  this._statsMonitor.start();
18642
- logger$11.debug("[Call] Resilience subsystems initialized for call", this.id);
18894
+ logger$12.debug("[Call] Resilience subsystems initialized for call", this.id);
18643
18895
  } catch (error) {
18644
- logger$11.warn("[Call] Failed to initialize resilience subsystems:", error);
18896
+ logger$12.warn("[Call] Failed to initialize resilience subsystems:", error);
18645
18897
  }
18646
18898
  }
18647
18899
  /**
@@ -18724,19 +18976,19 @@ var WebRTCCall = class extends Destroyable {
18724
18976
  }
18725
18977
  isCallSessionEvent(event) {
18726
18978
  try {
18727
- logger$11.debug("[Call] Checking if event is for this call session:", event);
18979
+ logger$12.debug("[Call] Checking if event is for this call session:", event);
18728
18980
  const callId = getValueFrom(event, "params.params.callID") ?? getValueFrom(event, "params.call_id");
18729
18981
  const roomSessionId = getValueFrom(event, "params.room_session_id");
18730
- logger$11.debug(`[Call] Extracted session identifiers callID: ${callId} and roomSessionID: ${roomSessionId} from event:`);
18982
+ logger$12.debug(`[Call] Extracted session identifiers callID: ${callId} and roomSessionID: ${roomSessionId} from event:`);
18731
18983
  return callId === this.id || !!callId && this.callEventsManager.isCallIdValid(callId) || !!roomSessionId && this.callEventsManager.isRoomSessionIdValid(roomSessionId);
18732
18984
  } catch (error) {
18733
- logger$11.error("[Call] Error checking if event is for this call session:", error);
18985
+ logger$12.error("[Call] Error checking if event is for this call session:", error);
18734
18986
  return false;
18735
18987
  }
18736
18988
  }
18737
18989
  get callSessionEvents$() {
18738
18990
  return this.cachedObservable("callSessionEvents$", () => this.clientSession.signalingEvent$.pipe((0, import_cjs$11.filter)((event) => this.isCallSessionEvent(event)), (0, import_cjs$11.tap)((event) => {
18739
- logger$11.debug("[Call] Received call session event:", event);
18991
+ logger$12.debug("[Call] Received call session event:", event);
18740
18992
  }), (0, import_cjs$11.takeUntil)(this.destroyed$), (0, import_cjs$11.share)()));
18741
18993
  }
18742
18994
  /** Observable of call-updated events. */
@@ -18806,16 +19058,16 @@ var WebRTCCall = class extends Destroyable {
18806
19058
  this._customSubscriptions.set(eventType, filtered$);
18807
19059
  }, (error) => {
18808
19060
  this._customSubscriptions.delete(eventType);
18809
- logger$11.warn(`[Call] verto.subscribe for '${eventType}' failed, not caching:`, error);
19061
+ logger$12.warn(`[Call] verto.subscribe for '${eventType}' failed, not caching:`, error);
18810
19062
  });
18811
19063
  this._customSubscriptions.set(eventType, filtered$);
18812
19064
  return filtered$;
18813
19065
  }
18814
19066
  get webrtcMessages$() {
18815
- return this.cachedObservable("webrtcMessages$", () => this.callSessionEvents$.pipe(filterAs(isWebrtcMessageMetadata, "params"), (0, import_cjs$11.tap)((event) => logger$11.debug("[Call] Event is a WebRTC message event:", event)), (0, import_cjs$11.takeUntil)(this.destroyed$), (0, import_cjs$11.share)()));
19067
+ return this.cachedObservable("webrtcMessages$", () => this.callSessionEvents$.pipe(filterAs(isWebrtcMessageMetadata, "params"), (0, import_cjs$11.tap)((event) => logger$12.debug("[Call] Event is a WebRTC message event:", event)), (0, import_cjs$11.takeUntil)(this.destroyed$), (0, import_cjs$11.share)()));
18816
19068
  }
18817
19069
  get callEvent$() {
18818
- return this.cachedObservable("callEvent$", () => this.callSessionEvents$.pipe(filterAs(isSignalwireCallMetadata, "params"), (0, import_cjs$11.tap)((event) => logger$11.debug("[Call] Event is a call event:", event)), (0, import_cjs$11.takeUntil)(this.destroyed$), (0, import_cjs$11.share)()));
19070
+ return this.cachedObservable("callEvent$", () => this.callSessionEvents$.pipe(filterAs(isSignalwireCallMetadata, "params"), (0, import_cjs$11.tap)((event) => logger$12.debug("[Call] Event is a call event:", event)), (0, import_cjs$11.takeUntil)(this.destroyed$), (0, import_cjs$11.share)()));
18819
19071
  }
18820
19072
  get layoutEvent$() {
18821
19073
  return this.cachedObservable("layoutEvent$", () => this.callEvent$.pipe(filterAs(isLayoutChangedMetadata, "params")));
@@ -18893,11 +19145,27 @@ var WebRTCCall = class extends Destroyable {
18893
19145
  return this.deferEmission(this._answered$.asObservable());
18894
19146
  }
18895
19147
  /**
18896
- * Sets the call layout and participant positions.
19148
+ * Sets the call layout and, optionally, individual participant positions.
19149
+ *
19150
+ * The gateway `call.layout.set` DTO has **no** `positions` member, so when
19151
+ * `positions` is provided this method issues a `call.member.position.set`
19152
+ * request per member (via {@link Participant.setPosition}, which keys each
19153
+ * position by that member's own call context) alongside `call.layout.set`
19154
+ * (issue #19400, Flag #6).
19155
+ *
19156
+ * **These operations are NOT atomic.** The layout is applied first, then each
19157
+ * member position sequentially, so members may briefly flash into their
19158
+ * default slots before being moved to the requested positions. Targeted
19159
+ * members are validated upfront, though: when any of them has no
19160
+ * {@link Participant.target | member call context} yet, the whole call
19161
+ * rejects before any request is sent and the layout is left unchanged.
18897
19162
  *
18898
19163
  * @param layout - Layout name (must be one of {@link layouts}).
18899
- * @param positions - Map of member IDs to {@link VideoPosition} values.
19164
+ * @param positions - Optional map of member IDs to {@link VideoPosition} values.
19165
+ * When omitted or empty, only the layout is changed.
18900
19166
  * @throws {InvalidParams} If the layout is not in the available {@link layouts}.
19167
+ * @throws {ParticipantNotReadyError} If a targeted member's call context has
19168
+ * not been received yet — thrown before any request is sent.
18901
19169
  *
18902
19170
  * @example
18903
19171
  * ```ts
@@ -18908,11 +19176,19 @@ var WebRTCCall = class extends Destroyable {
18908
19176
  */
18909
19177
  async setLayout(layout, positions) {
18910
19178
  if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
19179
+ const targets = [];
19180
+ for (const [memberId, position] of Object.entries(positions ?? {})) {
19181
+ const participant = this.participants.find((p) => p.id === memberId);
19182
+ if (!participant) {
19183
+ logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
19184
+ continue;
19185
+ }
19186
+ participant.target;
19187
+ targets.push([participant, position]);
19188
+ }
18911
19189
  const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
18912
- await this.executeMethod(selfId, "call.layout.set", {
18913
- layout,
18914
- positions
18915
- });
19190
+ await this.executeMethod(selfId, "call.layout.set", { layout });
19191
+ for (const [participant, position] of targets) await participant.setPosition(position);
18916
19192
  }
18917
19193
  /**
18918
19194
  * Transfers the call to another destination.
@@ -18944,7 +19220,7 @@ var WebRTCCall = class extends Destroyable {
18944
19220
  setLocalMicrophoneGain(value) {
18945
19221
  const pipeline = this.vertoManager.ensureLocalAudioPipeline();
18946
19222
  if (!pipeline) {
18947
- logger$11.warn("[Call] setLocalMicrophoneGain: audio pipeline unavailable");
19223
+ logger$12.warn("[Call] setLocalMicrophoneGain: audio pipeline unavailable");
18948
19224
  return;
18949
19225
  }
18950
19226
  const percent = Math.max(0, Math.min(200, value));
@@ -18989,7 +19265,7 @@ var WebRTCCall = class extends Destroyable {
18989
19265
  enablePushToTalk() {
18990
19266
  const pipeline = this.vertoManager.ensureLocalAudioPipeline();
18991
19267
  if (!pipeline) {
18992
- logger$11.warn("[Call] enablePushToTalk: audio pipeline unavailable");
19268
+ logger$12.warn("[Call] enablePushToTalk: audio pipeline unavailable");
18993
19269
  return;
18994
19270
  }
18995
19271
  pipeline.setPTTActive(false);
@@ -19086,6 +19362,7 @@ function inferCallErrorKind(error) {
19086
19362
  if (error instanceof RPCTimeoutError) return "timeout";
19087
19363
  if (error instanceof JSONRPCError) return "signaling";
19088
19364
  if (error instanceof MediaTrackError) return "media";
19365
+ if (error instanceof MediaAccessError) return "media";
19089
19366
  if (error instanceof WebSocketConnectionError || error instanceof TransportConnectionError) return "network";
19090
19367
  return "internal";
19091
19368
  }
@@ -19102,6 +19379,7 @@ const RECOVERABLE_RPC_CODES = new Set([
19102
19379
  function isFatalError(error) {
19103
19380
  if (error instanceof VertoPongError) return false;
19104
19381
  if (error instanceof MediaTrackError) return false;
19382
+ if (error instanceof MediaAccessError) return error.fatal;
19105
19383
  if (error instanceof RPCTimeoutError) return false;
19106
19384
  if (error instanceof JSONRPCError && RECOVERABLE_RPC_CODES.has(error.code)) return false;
19107
19385
  return true;
@@ -19127,10 +19405,10 @@ var CallFactory = class {
19127
19405
  return {
19128
19406
  vertoManager: new WebRTCVertoManager(callInstance, this.attachManager, this.deviceController, this.webRTCApiProvider, {
19129
19407
  nodeId: options.nodeId,
19130
- onError: (error) => {
19408
+ onError: (error, options$1) => {
19131
19409
  const callError = {
19132
19410
  kind: inferCallErrorKind(error),
19133
- fatal: isFatalError(error),
19411
+ fatal: options$1?.fatal ?? isFatalError(error),
19134
19412
  error,
19135
19413
  callId: callInstance.id
19136
19414
  };
@@ -19152,7 +19430,7 @@ var CallFactory = class {
19152
19430
  //#endregion
19153
19431
  //#region src/behaviors/Collection.ts
19154
19432
  var import_cjs$10 = require_cjs();
19155
- const logger$10 = getLogger();
19433
+ const logger$11 = getLogger();
19156
19434
  var Fetcher = class {
19157
19435
  constructor(endpoint, params, http) {
19158
19436
  this.endpoint = endpoint;
@@ -19176,7 +19454,7 @@ var Fetcher = class {
19176
19454
  this.hasMore = !!this.nextUrl;
19177
19455
  return result.data.filter(this.filter).map(this.mapper);
19178
19456
  }
19179
- logger$10.error("Failed to fetch entity");
19457
+ logger$11.error("Failed to fetch entity");
19180
19458
  return [];
19181
19459
  }
19182
19460
  async id(v) {
@@ -19252,7 +19530,7 @@ var EntityCollection = class extends Destroyable {
19252
19530
  this._hasMore$.next(this.fetchController.hasMore ?? false);
19253
19531
  this._loading$.next(false);
19254
19532
  } catch (error) {
19255
- logger$10.error(`Failed to fetch initial collection data`, error);
19533
+ logger$11.error(`Failed to fetch initial collection data`, error);
19256
19534
  this._hasMore$.next(this.fetchController.hasMore ?? false);
19257
19535
  this._loading$.next(false);
19258
19536
  this.onError?.(new CollectionFetchError("fetchMore", error));
@@ -19266,7 +19544,7 @@ var EntityCollection = class extends Destroyable {
19266
19544
  if (data) this.upsertData(data);
19267
19545
  return data;
19268
19546
  } catch (error) {
19269
- logger$10.error(`Failed to fetch data for (${String(key)}:${String(value)}) :`, error);
19547
+ logger$11.error(`Failed to fetch data for (${String(key)}:${String(value)}) :`, error);
19270
19548
  this._loading$.next(false);
19271
19549
  this.onError?.(new CollectionFetchError(`tryFetch(${String(key)})`, error));
19272
19550
  }
@@ -19515,13 +19793,13 @@ var Address = class extends Destroyable {
19515
19793
  //#endregion
19516
19794
  //#region src/core/utils.ts
19517
19795
  var import_cjs$8 = require_cjs();
19518
- const logger$9 = getLogger();
19796
+ const logger$10 = getLogger();
19519
19797
  const isRPCConnectResult = (e) => {
19520
- logger$9.debug("isRPCConnectResult check:", e);
19798
+ logger$10.debug("isRPCConnectResult check:", e);
19521
19799
  if (!e || typeof e !== "object") return false;
19522
19800
  const result = e;
19523
19801
  const is = typeof result.identity === "string" && typeof result.protocol === "string" && typeof result.authorization === "object" && typeof result.authorization.jti === "string" && typeof result.authorization.project_id === "string" && typeof result.authorization.fabric_subscriber === "object";
19524
- logger$9.debug("isRPCConnectResult check result:", is);
19802
+ logger$10.debug("isRPCConnectResult check result:", is);
19525
19803
  return is;
19526
19804
  };
19527
19805
  var PendingRPC = class PendingRPC {
@@ -19530,7 +19808,7 @@ var PendingRPC = class PendingRPC {
19530
19808
  }
19531
19809
  constructor(request, responses$, options) {
19532
19810
  this.id = v4_default();
19533
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}: method:${request.method}] Creating PendingRPC`);
19811
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}: method:${request.method}] Creating PendingRPC`);
19534
19812
  this.request = request;
19535
19813
  const timeoutMs = options?.timeoutMs ?? PendingRPC.defaultTimeoutMs;
19536
19814
  const signal = options?.signal;
@@ -19556,22 +19834,22 @@ var PendingRPC = class PendingRPC {
19556
19834
  isSettled = true;
19557
19835
  if (response.error) {
19558
19836
  const rpcError = new JSONRPCError(response.error.code, response.error.message, response.error.data, void 0, request.id);
19559
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with RPC error:`, rpcError);
19837
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with RPC error:`, rpcError);
19560
19838
  reject(rpcError);
19561
19839
  } else {
19562
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Resolving promise with response:`, response);
19840
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Resolving promise with response:`, response);
19563
19841
  resolve(response);
19564
19842
  }
19565
19843
  subscription.unsubscribe();
19566
19844
  },
19567
19845
  error: (error) => {
19568
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with error:`, error);
19846
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with error:`, error);
19569
19847
  isSettled = true;
19570
19848
  reject(error);
19571
19849
  subscription.unsubscribe();
19572
19850
  },
19573
19851
  complete: () => {
19574
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Observable completed`);
19852
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Observable completed`);
19575
19853
  if (!isSettled) reject(new RPCTimeoutError(request.id, timeoutMs));
19576
19854
  subscription.unsubscribe();
19577
19855
  }
@@ -19592,7 +19870,18 @@ var PendingRPC = class PendingRPC {
19592
19870
  //#endregion
19593
19871
  //#region src/managers/ClientSessionManager.ts
19594
19872
  var import_cjs$7 = require_cjs();
19595
- const logger$8 = getLogger();
19873
+ const logger$9 = getLogger();
19874
+ /**
19875
+ * Decide whether an error emitted on `call.errors$` during dial should
19876
+ * abort the dial. A non-fatal MediaAccessError means the call degraded to
19877
+ * receive-only and still connects — everything else rejects `dial()` with
19878
+ * the real cause.
19879
+ *
19880
+ * Pure function — exported for unit testing.
19881
+ */
19882
+ function shouldAbortDial(callError) {
19883
+ return callError.fatal || !(callError.error instanceof MediaAccessError);
19884
+ }
19596
19885
  const getAddressSearchURI = (options) => {
19597
19886
  const to = options.to?.split("?")[0];
19598
19887
  const from$9 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
@@ -19690,7 +19979,7 @@ var ClientSessionManager = class extends Destroyable {
19690
19979
  try {
19691
19980
  return await this.transport.execute(request, options);
19692
19981
  } catch (error) {
19693
- logger$8.debug("[Session] Execute Error", error);
19982
+ logger$9.debug("[Session] Execute Error", error);
19694
19983
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
19695
19984
  throw error;
19696
19985
  }
@@ -19704,13 +19993,13 @@ var ClientSessionManager = class extends Destroyable {
19704
19993
  return true;
19705
19994
  }
19706
19995
  setupMessageHandlers() {
19707
- logger$8.debug("[Session] Setting up message handlers");
19996
+ logger$9.debug("[Session] Setting up message handlers");
19708
19997
  this.subscribeTo(this.authStateEvent$, async (authStateEvent) => {
19709
- logger$8.debug("[Session] Authorization state event received:", authStateEvent);
19998
+ logger$9.debug("[Session] Authorization state event received:", authStateEvent);
19710
19999
  try {
19711
20000
  await this.updateAuthorizationStateInStorage(authStateEvent.authorization_state);
19712
20001
  } catch (error) {
19713
- logger$8.error("[Session] Failed to handle authorization state update:", error);
20002
+ logger$9.error("[Session] Failed to handle authorization state update:", error);
19714
20003
  this._errors$.next(new AuthStateHandlerError(error));
19715
20004
  }
19716
20005
  });
@@ -19718,29 +20007,29 @@ var ClientSessionManager = class extends Destroyable {
19718
20007
  if (this._authState$.value.kind === "authenticated") this._authState$.next({ kind: "unauthenticated" });
19719
20008
  });
19720
20009
  this.subscribeTo(this.transport.connectionStatus$.pipe((0, import_cjs$7.filter)((status) => status === "connected"), (0, import_cjs$7.exhaustMap)(() => {
19721
- logger$8.debug("[Session] Connection established, initiating authentication");
20010
+ logger$9.debug("[Session] Connection established, initiating authentication");
19722
20011
  return (0, import_cjs$7.from)(this.authenticate()).pipe((0, import_cjs$7.catchError)((error) => {
19723
20012
  this.handleAuthenticationError(error).catch((err) => {
19724
- logger$8.error("[Session] Error handling authentication failure:", err);
20013
+ logger$9.error("[Session] Error handling authentication failure:", err);
19725
20014
  });
19726
20015
  return import_cjs$7.EMPTY;
19727
20016
  }));
19728
20017
  })), void 0);
19729
20018
  this.subscribeTo(this.vertoInvite$, async (invite) => {
19730
- logger$8.debug("[Session] Verto invite received:", invite);
20019
+ logger$9.debug("[Session] Verto invite received:", invite);
19731
20020
  try {
19732
20021
  await this.createInboundCall(invite);
19733
20022
  } catch (error) {
19734
- logger$8.error("[Session] Error handling Verto invite:", error);
20023
+ logger$9.error("[Session] Error handling Verto invite:", error);
19735
20024
  this._errors$.next(new VertoInviteHandlerError(error));
19736
20025
  }
19737
20026
  });
19738
20027
  this.subscribeTo(this.vertoAttach$, async (attach) => {
19739
- logger$8.debug("[Session] Verto attach received:", attach);
20028
+ logger$9.debug("[Session] Verto attach received:", attach);
19740
20029
  try {
19741
20030
  await this.handleVertoAttach(attach);
19742
20031
  } catch (error) {
19743
- logger$8.error("[Session] Error handling Verto attach:", error);
20032
+ logger$9.error("[Session] Error handling Verto attach:", error);
19744
20033
  this._errors$.next(new VertoAttachHandlerError(error));
19745
20034
  }
19746
20035
  });
@@ -19750,36 +20039,36 @@ var ClientSessionManager = class extends Destroyable {
19750
20039
  const storedState = await this.storage.getItem(this.authorizationStateKey);
19751
20040
  this.authorizationState$.next(storedState ?? void 0);
19752
20041
  } catch (error) {
19753
- logger$8.error("Failed to retrieve authorization state from storage:", error);
20042
+ logger$9.error("Failed to retrieve authorization state from storage:", error);
19754
20043
  this.authorizationState$.next(void 0);
19755
20044
  }
19756
20045
  }
19757
20046
  async updateAuthorizationStateInStorage(authorizationState) {
19758
20047
  if (!authorizationState) {
19759
- logger$8.debug("[Session] Removing authorization state from storage");
20048
+ logger$9.debug("[Session] Removing authorization state from storage");
19760
20049
  try {
19761
20050
  await this.storage.removeItem(this.authorizationStateKey);
19762
20051
  this.authorizationState$.next(void 0);
19763
20052
  } catch (error) {
19764
- logger$8.error("Failed to remove authorization state from storage:", error);
20053
+ logger$9.error("Failed to remove authorization state from storage:", error);
19765
20054
  throw error;
19766
20055
  }
19767
20056
  return;
19768
20057
  }
19769
20058
  try {
19770
- logger$8.debug("[Session] Updating authorization state in storage");
20059
+ logger$9.debug("[Session] Updating authorization state in storage");
19771
20060
  await this.storage.setItem(this.authorizationStateKey, authorizationState);
19772
20061
  this.authorizationState$.next(authorizationState);
19773
20062
  } catch (error) {
19774
- logger$8.error("Failed to retrieve authorization state from storage:", error);
20063
+ logger$9.error("Failed to retrieve authorization state from storage:", error);
19775
20064
  throw error;
19776
20065
  }
19777
20066
  }
19778
20067
  get authStateEvent$() {
19779
20068
  return this.cachedObservable("authStateEvent$", () => this.signalingEvent$.pipe((0, import_cjs$7.tap)((msg) => {
19780
- logger$8.debug("[Session] Received incoming message:", msg);
20069
+ logger$9.debug("[Session] Received incoming message:", msg);
19781
20070
  }), filterAs(isSignalwireAuthorizationStateMetadata, "params"), (0, import_cjs$7.tap)((event) => {
19782
- logger$8.debug("[Session] Authorization state event received:", event.authorization_state);
20071
+ logger$9.debug("[Session] Authorization state event received:", event.authorization_state);
19783
20072
  })));
19784
20073
  }
19785
20074
  get signalingEvent$() {
@@ -19817,42 +20106,72 @@ var ClientSessionManager = class extends Destroyable {
19817
20106
  await (0, import_cjs$7.firstValueFrom)(this.authenticated$.pipe((0, import_cjs$7.takeUntil)(this.destroyed$), (0, import_cjs$7.filter)(Boolean), (0, import_cjs$7.take)(1), (0, import_cjs$7.timeout)({ first: 15e3 })));
19818
20107
  }
19819
20108
  async handleAuthenticationError(error) {
19820
- logger$8.error("Authentication error:", error);
19821
- const isRecoverableAuthError = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
20109
+ logger$9.error("Authentication error:", error);
20110
+ const isRecoverableAuthError$1 = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
19822
20111
  const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
19823
- if (isRecoverableAuthError && hasStoredState) {
19824
- logger$8.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
20112
+ if (isRecoverableAuthError$1 && hasStoredState) {
20113
+ logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
19825
20114
  try {
19826
20115
  await this.cleanupStoredConnectionParams();
19827
20116
  } catch (cleanupError) {
19828
- logger$8.error("Failed to cleanup stored connection params:", cleanupError);
20117
+ logger$9.error("Failed to cleanup stored connection params:", cleanupError);
19829
20118
  } finally {
19830
20119
  this.transport.reconnect();
19831
20120
  }
19832
20121
  } else this._errors$.next(error);
19833
20122
  }
20123
+ /**
20124
+ * Clear the resume state (authorization_state + protocol) only.
20125
+ *
20126
+ * This is the stale-auth-state recovery helper used by handleAuthError:
20127
+ * the server rejected a reconnect, so the resume state is discarded and a
20128
+ * fresh connect follows. Attach records are deliberately preserved — the
20129
+ * session lives on through the reconnect and reattachCalls() needs the
20130
+ * stored call references afterwards. Do NOT add detachAll() here.
20131
+ *
20132
+ * For public teardown (disconnect/destroy), use {@link teardownSessionState}
20133
+ * instead, which clears the attach records as well.
20134
+ */
19834
20135
  async cleanupStoredConnectionParams() {
19835
20136
  await this.transport.setProtocol(void 0);
19836
20137
  await this.updateAuthorizationStateInStorage(void 0);
19837
20138
  this._authorization$.next(void 0);
19838
20139
  }
20140
+ /**
20141
+ * Public-teardown helper for disconnect()/destroy(). Clears the resume
20142
+ * state (authorization_state + protocol) AND the attach records as one
20143
+ * atomic unit.
20144
+ *
20145
+ * The two stores are coupled: the backend only honors attach records
20146
+ * within the session identified by the resume state, so ending the
20147
+ * session must clear both. Clearing one without the other strands records
20148
+ * no future session can honor (disconnect) or revives a session the
20149
+ * developer explicitly ended (destroy).
20150
+ *
20151
+ * Distinct from {@link cleanupStoredConnectionParams}, which keeps the
20152
+ * attach records for the stale-auth-state recovery path.
20153
+ */
20154
+ async teardownSessionState() {
20155
+ await this.cleanupStoredConnectionParams();
20156
+ await this.attachManager.detachAll();
20157
+ }
19839
20158
  async updateAuthState(authorization_state) {
19840
20159
  try {
19841
20160
  await this.storage.setItem(this.authorizationStateKey, authorization_state);
19842
20161
  } catch (error) {
19843
- logger$8.error("Failed to update authorization state in storage:", error);
20162
+ logger$9.error("Failed to update authorization state in storage:", error);
19844
20163
  this._errors$.next(new AuthStateHandlerError(error));
19845
20164
  }
19846
20165
  }
19847
20166
  async reauthenticate(token, dpopToken, options) {
19848
- logger$8.debug("[Session] Re-authenticating session");
20167
+ logger$9.debug("[Session] Re-authenticating session");
19849
20168
  try {
19850
20169
  let resolvedDpopToken = dpopToken;
19851
20170
  if (!resolvedDpopToken && this.dpopManager?.initialized) try {
19852
20171
  resolvedDpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
19853
20172
  } catch (error) {
19854
20173
  if (this.clientBound) throw error;
19855
- logger$8.warn("[Session] Failed to create DPoP proof for reauthenticate:", error);
20174
+ logger$9.warn("[Session] Failed to create DPoP proof for reauthenticate:", error);
19856
20175
  }
19857
20176
  const request = RPCReauthenticate({
19858
20177
  project: this._authorization$.value?.project_id ?? "",
@@ -19860,24 +20179,24 @@ var ClientSessionManager = class extends Destroyable {
19860
20179
  ...resolvedDpopToken ? { dpop_token: resolvedDpopToken } : {}
19861
20180
  });
19862
20181
  await (0, import_cjs$7.lastValueFrom)((0, import_cjs$7.from)(this.transport.execute(request)).pipe(throwOnRPCError(), (0, import_cjs$7.take)(1), (0, import_cjs$7.catchError)((err) => {
19863
- logger$8.error("[Session] Re-authentication RPC failed:", err);
20182
+ logger$9.error("[Session] Re-authentication RPC failed:", err);
19864
20183
  throw err;
19865
20184
  })));
19866
20185
  if (options?.clientBound) this._wasClientBound = true;
19867
- logger$8.debug("[Session] Re-authentication successful, updating stored auth state");
20186
+ logger$9.debug("[Session] Re-authentication successful, updating stored auth state");
19868
20187
  } catch (error) {
19869
- logger$8.error("[Session] Re-authentication failed:", error);
20188
+ logger$9.error("[Session] Re-authentication failed:", error);
19870
20189
  this._errors$.next(new AuthStateHandlerError(error));
19871
20190
  throw error;
19872
20191
  }
19873
20192
  }
19874
20193
  async authenticate() {
19875
- logger$8.debug("[Session] Starting authentication process");
20194
+ logger$9.debug("[Session] Starting authentication process");
19876
20195
  const persistedParams = await (0, import_cjs$7.firstValueFrom)((0, import_cjs$7.combineLatest)({
19877
20196
  protocol: this.transport.protocol$,
19878
20197
  authorization_state: this.authorizationState$
19879
20198
  }).pipe((0, import_cjs$7.take)(1)));
19880
- logger$8.debug("[Session] Persisted params:\n", {
20199
+ logger$9.debug("[Session] Persisted params:\n", {
19881
20200
  protocol: persistedParams.protocol,
19882
20201
  authStateLength: persistedParams.authorization_state?.length
19883
20202
  });
@@ -19885,16 +20204,20 @@ var ClientSessionManager = class extends Destroyable {
19885
20204
  const storedToken = this.getCredential().token;
19886
20205
  const isReconnect = hasReconnectState && storedToken;
19887
20206
  let dpopToken;
19888
- if (isReconnect) logger$8.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
19889
- else if (this.onBeforeReconnect && this.clientBound) {
19890
- logger$8.debug("[Session] Refreshing credentials before fresh connect");
19891
- await this.onBeforeReconnect();
20207
+ if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
20208
+ else {
20209
+ const credential = this.getCredential();
20210
+ const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + CREDENTIAL_EXPIRY_SKEW_MS;
20211
+ if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
20212
+ logger$9.debug("[Session] Refreshing credentials before fresh connect");
20213
+ await this.onBeforeReconnect();
20214
+ }
19892
20215
  }
19893
- if ((!isReconnect || this.clientBound) && this.dpopManager?.initialized) try {
20216
+ if (this.dpopManager?.initialized) try {
19894
20217
  dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
19895
20218
  } catch (error) {
19896
20219
  if (this.clientBound) throw error;
19897
- logger$8.warn("[Session] Failed to create DPoP proof for connect, proceeding without:", error);
20220
+ logger$9.warn("[Session] Failed to create DPoP proof for connect, proceeding without:", error);
19898
20221
  }
19899
20222
  const rpcConnectRequest = RPCConnect({
19900
20223
  authentication: isReconnect ? { jwt_token: storedToken } : this.authentication,
@@ -19911,26 +20234,27 @@ var ClientSessionManager = class extends Destroyable {
19911
20234
  } : {}
19912
20235
  });
19913
20236
  const response = await (0, import_cjs$7.lastValueFrom)((0, import_cjs$7.from)(this.transport.execute(rpcConnectRequest)).pipe(throwOnRPCError(), (0, import_cjs$7.map)((res) => res.result), (0, import_cjs$7.filter)(isRPCConnectResult), (0, import_cjs$7.tap)(() => {
19914
- logger$8.debug("[Session] Response passed filter, processing authentication result");
20237
+ logger$9.debug("[Session] Response passed filter, processing authentication result");
19915
20238
  }), (0, import_cjs$7.take)(1), (0, import_cjs$7.catchError)((err) => {
19916
- logger$8.error("[Session] Authentication RPC failed:", err);
20239
+ logger$9.error("[Session] Authentication RPC failed:", err);
19917
20240
  throw err;
19918
20241
  })));
19919
- logger$8.debug("[Session] Processing authentication result:", {
20242
+ logger$9.debug("[Session] Processing authentication result:", {
19920
20243
  hasProtocol: !!response.protocol,
19921
20244
  hasAuthorization: !!response.authorization,
19922
20245
  hasIceServers: !!response.ice_servers
19923
20246
  });
19924
20247
  if (response.protocol) await this.transport.setProtocol(response.protocol);
19925
20248
  this._authorization$.next(response.authorization);
20249
+ if (response.authorization.cnf?.jkt) this._wasClientBound = true;
19926
20250
  this._iceServers$.next(response.ice_servers ?? []);
19927
20251
  this._authState$.next({ kind: "authenticated" });
19928
- logger$8.debug("[Session] Authentication completed successfully");
20252
+ logger$9.debug("[Session] Authentication completed successfully");
19929
20253
  }
19930
20254
  async disconnect() {
19931
20255
  this.transport.disconnect();
19932
20256
  this._authState$.next({ kind: "unauthenticated" });
19933
- await this.cleanupStoredConnectionParams();
20257
+ await this.teardownSessionState();
19934
20258
  }
19935
20259
  async createInboundCall(invite) {
19936
20260
  const callSession = await this.createCall({
@@ -19961,11 +20285,11 @@ var ClientSessionManager = class extends Destroyable {
19961
20285
  async handleVertoAttach(attach) {
19962
20286
  const { callID } = attach;
19963
20287
  if (callID in this._calls$.value) {
19964
- logger$8.debug(`[Session] Verto attach for existing call ${callID}, deferring to per-call handler`);
20288
+ logger$9.debug(`[Session] Verto attach for existing call ${callID}, deferring to per-call handler`);
19965
20289
  return;
19966
20290
  }
19967
20291
  const storedOptions = await this.attachManager.consumePendingAttachment(callID);
19968
- logger$8.debug(`[Session] Creating reattached call for callID: ${callID}`);
20292
+ logger$9.debug(`[Session] Creating reattached call for callID: ${callID}`);
19969
20293
  const callSession = await this.createCall({
19970
20294
  nodeId: attach.node_id,
19971
20295
  callId: callID,
@@ -19989,14 +20313,14 @@ var ClientSessionManager = class extends Destroyable {
19989
20313
  to: destinationURI,
19990
20314
  ...options
19991
20315
  });
19992
- await (0, import_cjs$7.firstValueFrom)((0, import_cjs$7.race)(callSession.selfId$.pipe((0, import_cjs$7.filter)((id) => Boolean(id)), (0, import_cjs$7.take)(1), (0, import_cjs$7.timeout)(this.callCreateTimeout)), callSession.errors$.pipe((0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)((callError) => (0, import_cjs$7.throwError)(() => callError.error)))));
20316
+ await (0, import_cjs$7.firstValueFrom)((0, import_cjs$7.race)(callSession.selfId$.pipe((0, import_cjs$7.filter)((id) => Boolean(id)), (0, import_cjs$7.take)(1), (0, import_cjs$7.timeout)(this.callCreateTimeout)), callSession.errors$.pipe((0, import_cjs$7.filter)(shouldAbortDial), (0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)((callError) => (0, import_cjs$7.throwError)(() => callError.error)))));
19993
20317
  this._calls$.next({
19994
20318
  [`${callSession.id}`]: callSession,
19995
20319
  ...this._calls$.value
19996
20320
  });
19997
20321
  return callSession;
19998
20322
  } catch (error) {
19999
- logger$8.error("[Session] Error creating outbound call:", error);
20323
+ logger$9.error("[Session] Error creating outbound call:", error);
20000
20324
  callSession?.destroy();
20001
20325
  const callError = new CallCreateError(error instanceof import_cjs$7.TimeoutError ? "Call create timeout" : "Call creation failed", error, "outbound");
20002
20326
  this._errors$.next(callError);
@@ -20014,7 +20338,7 @@ var ClientSessionManager = class extends Destroyable {
20014
20338
  address = this._directory.get(addressId);
20015
20339
  if (!address) throw new DependencyError(`Address ID: ${addressId} not found`);
20016
20340
  } catch {
20017
- logger$8.warn(`[Session] Directory lookup failed for ${addressURI}, proceeding with raw URI`);
20341
+ logger$9.warn(`[Session] Directory lookup failed for ${addressURI}, proceeding with raw URI`);
20018
20342
  }
20019
20343
  const callSession = this.callFactory.createCall(address, { ...options });
20020
20344
  this.subscribeTo(callSession.status$.pipe((0, import_cjs$7.filter)((status) => status === "destroyed"), (0, import_cjs$7.take)(1)), () => {
@@ -20023,7 +20347,7 @@ var ClientSessionManager = class extends Destroyable {
20023
20347
  });
20024
20348
  return callSession;
20025
20349
  } catch (error) {
20026
- logger$8.error("[Session] Error creating call session:", error);
20350
+ logger$9.error("[Session] Error creating call session:", error);
20027
20351
  throw new CallCreateError("Call create error", error, options.initOffer ? "inbound" : "outbound");
20028
20352
  }
20029
20353
  }
@@ -20042,6 +20366,14 @@ var ClientSessionWrapper = class {
20042
20366
  get authenticated() {
20043
20367
  return this.clientSessionManager.authenticated;
20044
20368
  }
20369
+ /**
20370
+ * Whether the session is using a Client Bound SAT (DPoP). Sticky — set
20371
+ * when the binding is established or restored from a resumed session's
20372
+ * server authorization.
20373
+ */
20374
+ get clientBound() {
20375
+ return this.clientSessionManager.clientBound;
20376
+ }
20045
20377
  get signalingEvent$() {
20046
20378
  return this.clientSessionManager.signalingEvent$;
20047
20379
  }
@@ -20072,7 +20404,7 @@ const isString = (obj) => typeof obj === "string";
20072
20404
  //#endregion
20073
20405
  //#region src/managers/ConversationsManager.ts
20074
20406
  var import_cjs$6 = require_cjs();
20075
- const logger$7 = getLogger();
20407
+ const logger$8 = getLogger();
20076
20408
  var ConversationMessagesFetcher = class extends Fetcher {
20077
20409
  constructor(groupId, http) {
20078
20410
  super(`/api/fabric/conversations/${groupId}/messages`, "page_size=100", http);
@@ -20112,13 +20444,13 @@ var ConversationsManager = class {
20112
20444
  }
20113
20445
  throw new ConversationError("Join Failed - Unexpected response");
20114
20446
  } catch (error) {
20115
- logger$7.error("[ConversationsManager] Failed to join conversation:", error);
20447
+ logger$8.error("[ConversationsManager] Failed to join conversation:", error);
20116
20448
  throw error;
20117
20449
  }
20118
20450
  }
20119
20451
  async getConversationMessageCollection(addressId) {
20120
20452
  const groupId = this.groupIds.get(addressId) ?? await this.join(addressId);
20121
- return Promise.resolve(new ConversationMessageCollection(groupId, this.clientSession.signalingEvent$.pipe(filterAs(isConversationMessageMetadata, "params"), (0, import_cjs$6.tap)((event) => logger$7.debug("[ConversationsManager ] Conversation Event:", event)), (0, import_cjs$6.map)((params) => ({ ...params }))), this.http, this.onError));
20453
+ return Promise.resolve(new ConversationMessageCollection(groupId, this.clientSession.signalingEvent$.pipe(filterAs(isConversationMessageMetadata, "params"), (0, import_cjs$6.tap)((event) => logger$8.debug("[ConversationsManager ] Conversation Event:", event)), (0, import_cjs$6.map)((params) => ({ ...params }))), this.http, this.onError));
20122
20454
  }
20123
20455
  async sendText(text, destinationAddressId) {
20124
20456
  const groupId = this.groupIds.get(destinationAddressId) ?? await this.join(destinationAddressId);
@@ -20135,7 +20467,7 @@ var ConversationsManager = class {
20135
20467
  })).ok) return;
20136
20468
  throw new ConversationError("Send Text Failed - Unexpected response");
20137
20469
  } catch (error) {
20138
- logger$7.error("[ConversationsManager] Failed to send text message:", error);
20470
+ logger$8.error("[ConversationsManager] Failed to send text message:", error);
20139
20471
  throw error;
20140
20472
  }
20141
20473
  }
@@ -20144,7 +20476,7 @@ var ConversationsManager = class {
20144
20476
  //#endregion
20145
20477
  //#region src/managers/DeviceTokenManager.ts
20146
20478
  var import_cjs$5 = require_cjs();
20147
- const logger$6 = getLogger();
20479
+ const logger$7 = getLogger();
20148
20480
  /**
20149
20481
  * Resolves the token expiry timestamp (epoch seconds) using a 3-tier priority chain:
20150
20482
  * 1. `data.expires_at` — server-provided absolute timestamp
@@ -20154,7 +20486,7 @@ const logger$6 = getLogger();
20154
20486
  function resolveExpiresAt(data) {
20155
20487
  if (data.expires_at) return data.expires_at;
20156
20488
  if (data.expires_in) return Math.floor(Date.now() / 1e3) + data.expires_in;
20157
- logger$6.warn("[DeviceToken] Could not determine token expiry, using default");
20489
+ logger$7.warn("[DeviceToken] Could not determine token expiry, using default");
20158
20490
  return Math.floor(Date.now() / 1e3) + DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
20159
20491
  }
20160
20492
  /**
@@ -20187,11 +20519,12 @@ var DeviceTokenManager = class extends Destroyable {
20187
20519
  this.getCredential = getCredential;
20188
20520
  this._currentToken$ = this.createBehaviorSubject(null);
20189
20521
  this._refreshInProgress = false;
20522
+ this._paused = false;
20190
20523
  this._effectiveExpireIn = DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
20191
20524
  this.subscribeTo(this._currentToken$.pipe((0, import_cjs$5.filter)(Boolean), (0, import_cjs$5.switchMap)((tokenData) => {
20192
20525
  const expiresAt = resolveExpiresAt(tokenData);
20193
20526
  const refreshIn = Math.max(expiresAt * 1e3 - Date.now() - DEVICE_TOKEN_REFRESH_BUFFER_MS, 1e3);
20194
- logger$6.debug(`[DeviceToken] Scheduling Client Bound SAT refresh in ${refreshIn}ms`);
20527
+ logger$7.debug(`[DeviceToken] Scheduling Client Bound SAT refresh in ${refreshIn}ms`);
20195
20528
  return (0, import_cjs$5.timer)(refreshIn);
20196
20529
  })), () => {
20197
20530
  this.executeRefresh();
@@ -20205,7 +20538,12 @@ var DeviceTokenManager = class extends Destroyable {
20205
20538
  * Activates the Client Bound SAT flow when the user's token has
20206
20539
  * `sat:refresh` scope.
20207
20540
  *
20208
- * Steps:
20541
+ * Returns an {@link ActivationResult} indicating whether the manager
20542
+ * took ownership of refresh duties. The caller must use the `activated`
20543
+ * boolean to decide whether to keep its own refresh path armed — when
20544
+ * `activated` is `false`, the caller is responsible for refresh.
20545
+ *
20546
+ * Steps on success:
20209
20547
  * 1. Check user's `sat_claims` for `sat:refresh` scope
20210
20548
  * 2. Call `/api/fabric/subscriber/devices/token` with a DPoP proof
20211
20549
  * 3. Reauthenticate the session with the Client Bound SAT + DPoP proof
@@ -20214,32 +20552,66 @@ var DeviceTokenManager = class extends Destroyable {
20214
20552
  async activate(user, session, updateCredential) {
20215
20553
  const { satClaims } = user;
20216
20554
  if (!satClaims?.scope?.includes(SAT_REFRESH_SCOPE)) {
20217
- logger$6.debug("[DeviceToken] No sat:refresh scope, skipping Client Bound SAT activation");
20218
- return;
20555
+ logger$7.debug("[DeviceToken] No sat:refresh scope, skipping Client Bound SAT activation");
20556
+ return {
20557
+ activated: false,
20558
+ reason: "no-scope"
20559
+ };
20219
20560
  }
20220
20561
  this._session = session;
20221
20562
  this._updateCredential = updateCredential;
20222
20563
  try {
20564
+ const cached = this._currentToken$.value;
20565
+ if (cached && this.isTokenFresh(cached)) {
20566
+ logger$7.debug("[DeviceToken] Reusing cached Client Bound SAT — skipping /devices/token");
20567
+ const rpcProof$1 = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
20568
+ await session.reauthenticate(cached.token, rpcProof$1, { clientBound: true });
20569
+ updateCredential({ token: cached.token });
20570
+ return { activated: true };
20571
+ }
20223
20572
  const tokenData = await this.obtainToken();
20224
20573
  if (!tokenData.expires_at && !tokenData.expires_in && satClaims.expires_at) tokenData.expires_at = satClaims.expires_at;
20225
20574
  this._effectiveExpireIn = resolveExpireIn(tokenData);
20226
20575
  const rpcProof = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
20227
20576
  await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
20228
20577
  updateCredential({ token: tokenData.token });
20229
- logger$6.info("[DeviceToken] Client Bound SAT activated successfully");
20230
- this._currentToken$.next(tokenData);
20578
+ logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
20579
+ this.emitCurrentToken(tokenData);
20580
+ return { activated: true };
20231
20581
  } catch (error) {
20232
- logger$6.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
20582
+ logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
20233
20583
  this.errorHandler(new DPoPInitError(error, "Failed to activate Client Bound SAT"));
20234
- const credential = this.getCredential();
20235
- const expiresAt = satClaims.expires_at ?? (credential.expiry_at ? credential.expiry_at / 1e3 : Date.now() / 1e3 + DEVICE_TOKEN_DEFAULT_EXPIRE_IN / 1e3);
20236
- this._currentToken$.next({
20237
- token: credential.token ?? "",
20238
- expires_at: expiresAt
20239
- });
20584
+ return {
20585
+ activated: false,
20586
+ reason: "endpoint-failed"
20587
+ };
20240
20588
  }
20241
20589
  }
20242
20590
  /**
20591
+ * Emit a freshly received token to the reactive pipeline, stamping an
20592
+ * absolute `expires_at` when the response carried only `expires_in`.
20593
+ * Resolving the expiry at RECEIVE time (not at read time) is what lets
20594
+ * {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
20595
+ * re-resolved later would always compute a full TTL from "now" and never
20596
+ * cross the refresh buffer.
20597
+ */
20598
+ emitCurrentToken(token) {
20599
+ const stamped = token.expires_at ? token : {
20600
+ ...token,
20601
+ expires_at: resolveExpiresAt(token)
20602
+ };
20603
+ this._currentToken$.next(stamped);
20604
+ }
20605
+ /**
20606
+ * Returns true when the cached token has enough headroom before expiry to
20607
+ * be safely reused on reactivation. The headroom matches the refresh
20608
+ * buffer, so a token within the refresh window is treated as stale (the
20609
+ * reactive pipeline is about to refresh it anyway).
20610
+ */
20611
+ isTokenFresh(token) {
20612
+ return resolveExpiresAt(token) * 1e3 - Date.now() > DEVICE_TOKEN_REFRESH_BUFFER_MS;
20613
+ }
20614
+ /**
20243
20615
  * Obtains a Client Bound SAT from `/api/fabric/subscriber/devices/token`.
20244
20616
  * Returns the full {@link DeviceTokenResponse} including expiry metadata.
20245
20617
  */
@@ -20269,7 +20641,7 @@ var DeviceTokenManager = class extends Destroyable {
20269
20641
  * handled by the reactive pipeline).
20270
20642
  */
20271
20643
  async refreshToken(session, currentToken, updateCredential) {
20272
- logger$6.debug("[DeviceToken] Refreshing Client Bound SAT");
20644
+ logger$7.debug("[DeviceToken] Refreshing Client Bound SAT");
20273
20645
  const dpopProof = await this.dpopManager.createHttpProof({
20274
20646
  method: "POST",
20275
20647
  uri: DEVICE_REFRESH_ENDPOINT,
@@ -20291,7 +20663,7 @@ var DeviceTokenManager = class extends Destroyable {
20291
20663
  const rpcProof = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
20292
20664
  await session.reauthenticate(data.token, rpcProof);
20293
20665
  updateCredential({ token: data.token });
20294
- logger$6.info("[DeviceToken] Client Bound SAT refreshed successfully");
20666
+ logger$7.info("[DeviceToken] Client Bound SAT refreshed successfully");
20295
20667
  return data;
20296
20668
  }
20297
20669
  /**
@@ -20300,18 +20672,22 @@ var DeviceTokenManager = class extends Destroyable {
20300
20672
  * On all retries exhausted, emits to `errorHandler`.
20301
20673
  */
20302
20674
  async executeRefresh() {
20675
+ if (this._paused) {
20676
+ logger$7.debug("[DeviceToken] Manager paused, skipping refresh");
20677
+ return;
20678
+ }
20303
20679
  if (this._refreshInProgress) {
20304
- logger$6.debug("[DeviceToken] Refresh already in progress, skipping");
20680
+ logger$7.debug("[DeviceToken] Refresh already in progress, skipping");
20305
20681
  return;
20306
20682
  }
20307
20683
  const session = this._session;
20308
20684
  const updateCredential = this._updateCredential;
20309
20685
  if (!session || !updateCredential) {
20310
- logger$6.warn("[DeviceToken] Cannot refresh: session or updateCredential not set");
20686
+ logger$7.warn("[DeviceToken] Cannot refresh: session or updateCredential not set");
20311
20687
  return;
20312
20688
  }
20313
20689
  if (!session.authenticated) {
20314
- logger$6.debug("[DeviceToken] Session not authenticated, deferring refresh");
20690
+ logger$7.debug("[DeviceToken] Session not authenticated, deferring refresh");
20315
20691
  return;
20316
20692
  }
20317
20693
  this._refreshInProgress = true;
@@ -20319,9 +20695,9 @@ var DeviceTokenManager = class extends Destroyable {
20319
20695
  const currentToken = this.getCredential().token;
20320
20696
  if (!currentToken) throw new TokenRefreshError("No current token available for refresh");
20321
20697
  const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
20322
- this._currentToken$.next(newTokenData);
20698
+ this.emitCurrentToken(newTokenData);
20323
20699
  } catch (error) {
20324
- logger$6.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
20700
+ logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
20325
20701
  this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
20326
20702
  } finally {
20327
20703
  this._refreshInProgress = false;
@@ -20339,18 +20715,324 @@ var DeviceTokenManager = class extends Destroyable {
20339
20715
  lastError = error;
20340
20716
  if (attempt < DEVICE_TOKEN_REFRESH_MAX_RETRIES - 1) {
20341
20717
  const delay$1 = DEVICE_TOKEN_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt);
20342
- logger$6.warn(`[DeviceToken] Refresh attempt ${attempt + 1} failed, retrying in ${delay$1}ms`);
20718
+ logger$7.warn(`[DeviceToken] Refresh attempt ${attempt + 1} failed, retrying in ${delay$1}ms`);
20343
20719
  await new Promise((resolve) => setTimeout(resolve, delay$1));
20344
20720
  }
20345
20721
  }
20346
20722
  throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
20347
20723
  }
20724
+ /**
20725
+ * Force an immediate refresh when the cached Client Bound SAT is already
20726
+ * past its refresh window. Called on resume from suspension where
20727
+ * background-tab throttling can delay the reactive timer past the buffer.
20728
+ * A no-op when no token is cached or it still has headroom; the normal
20729
+ * {@link executeRefresh} guards (paused / in-progress / unauthenticated)
20730
+ * still apply.
20731
+ */
20732
+ refreshNowIfDue() {
20733
+ const token = this._currentToken$.value;
20734
+ if (!token) return;
20735
+ if (resolveExpiresAt(token) * 1e3 - Date.now() <= DEVICE_TOKEN_REFRESH_BUFFER_MS) {
20736
+ logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
20737
+ this.executeRefresh();
20738
+ }
20739
+ }
20740
+ /**
20741
+ * Stops the reactive refresh pipeline from firing. Use when the underlying
20742
+ * session is being torn down (e.g., during {@link SignalWire.disconnect})
20743
+ * so a scheduled refresh cannot fire against a destroyed session.
20744
+ *
20745
+ * The manager's state (cached token, effective TTL, subscriptions) is
20746
+ * preserved — call {@link resume} to re-enable firing after reconnect.
20747
+ */
20748
+ pause() {
20749
+ this._paused = true;
20750
+ }
20751
+ /** Re-enables the reactive refresh pipeline after a previous {@link pause}. */
20752
+ resume() {
20753
+ this._paused = false;
20754
+ }
20348
20755
  /** Cleans up the manager, cancelling the reactive pipeline and all subscriptions. */
20349
20756
  destroy() {
20350
20757
  super.destroy();
20351
20758
  }
20352
20759
  };
20353
20760
 
20761
+ //#endregion
20762
+ //#region src/managers/CredentialRefreshCoordinator.ts
20763
+ const logger$6 = getLogger();
20764
+ const defaultDeviceTokenManagerFactory = (dpopManager, http, errorHandler, getCredential) => new DeviceTokenManager(dpopManager, http, errorHandler, getCredential);
20765
+ /**
20766
+ * Centralizes credential-refresh ownership across the two competing
20767
+ * mechanisms — developer-provided `CredentialProvider.refresh()` and the
20768
+ * Client Bound SAT path via {@link DeviceTokenManager}.
20769
+ *
20770
+ * Maintains the invariant: **at most one refresh mechanism is armed at a
20771
+ * time, and at least one is armed whenever the current credential has an
20772
+ * `expiry_at`**.
20773
+ *
20774
+ * Replaces the previous design where refresh state was distributed across
20775
+ * `SignalWire` (timer field, scheduler method, activation helper) and
20776
+ * `DeviceTokenManager` (reactive pipeline). Centralizing the invariant in
20777
+ * one component eliminates the bug class that produced issue #19074.
20778
+ *
20779
+ * Race-safety:
20780
+ * - `_activating` flag prevents overlapping `activate()` calls from racing.
20781
+ * - `_activationGeneration` lets late resolutions detect they've been
20782
+ * preempted by a newer activation (e.g., reconnect during in-flight
20783
+ * `obtainToken`).
20784
+ */
20785
+ var CredentialRefreshCoordinator = class extends Destroyable {
20786
+ constructor(dpopManager, deps) {
20787
+ super();
20788
+ this.deps = deps;
20789
+ this._activating = false;
20790
+ this._activationGeneration = 0;
20791
+ this._developerRefreshInProgress = false;
20792
+ if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
20793
+ }
20794
+ /** True when the Client Bound SAT path is available (DPoP initialized). */
20795
+ get clientBoundSATAvailable() {
20796
+ return this._deviceTokenManager !== void 0;
20797
+ }
20798
+ /** True when the developer-provided refresh timer is currently armed. */
20799
+ get developerRefreshArmed() {
20800
+ return this._developerTimerId !== void 0;
20801
+ }
20802
+ /**
20803
+ * Arms the developer-provided refresh timer to fire shortly before
20804
+ * `expiresAt`. Replaces any previously scheduled developer refresh.
20805
+ *
20806
+ * Idempotent — multiple calls just reschedule. On retry exhaustion,
20807
+ * invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
20808
+ */
20809
+ scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
20810
+ this._activeProvider = provider;
20811
+ if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
20812
+ const refreshInterval = attempt === 0 ? Math.max(expiresAt - Date.now() - CREDENTIAL_REFRESH_BUFFER_MS, 1e3) : Math.min(CREDENTIAL_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt) * (.5 + Math.random() * .5), CREDENTIAL_REFRESH_MAX_DELAY_MS);
20813
+ this._developerTimerId = setTimeout(() => {
20814
+ this._developerTimerId = void 0;
20815
+ this.executeDeveloperRefresh(provider, expiresAt, attempt);
20816
+ }, refreshInterval);
20817
+ }
20818
+ /**
20819
+ * Runs the developer-provided refresh once: mints a new credential, stores
20820
+ * and persists it, reauthenticates the live session (via the notifier), and
20821
+ * reschedules against the new expiry. On failure retries with backoff up to
20822
+ * {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
20823
+ *
20824
+ * Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
20825
+ * `_developerRefreshInProgress` guard prevents the two from overlapping.
20826
+ */
20827
+ async executeDeveloperRefresh(provider, expiresAt, attempt) {
20828
+ if (this._developerRefreshInProgress) {
20829
+ logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
20830
+ return;
20831
+ }
20832
+ this._developerRefreshInProgress = true;
20833
+ try {
20834
+ const newCredentials = await this.refreshCredential(provider);
20835
+ this.deps.store.write(newCredentials);
20836
+ this.deps.store.persist(newCredentials);
20837
+ try {
20838
+ await this.deps.notifier.onCredentialRefreshed(newCredentials);
20839
+ } catch (reauthError) {
20840
+ logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
20841
+ }
20842
+ logger$6.info("[Coordinator] Credentials refreshed successfully.");
20843
+ if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
20844
+ } catch (error) {
20845
+ const nextAttempt = attempt + 1;
20846
+ logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
20847
+ this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
20848
+ if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
20849
+ else {
20850
+ logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
20851
+ this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
20852
+ this.deps.notifier.onRefreshExhausted();
20853
+ }
20854
+ } finally {
20855
+ this._developerRefreshInProgress = false;
20856
+ }
20857
+ }
20858
+ /**
20859
+ * Force an immediate refresh when the current credential is already past its
20860
+ * scheduled refresh window. Called on resume from suspension, where
20861
+ * background-tab timer throttling can delay the armed refresh well past
20862
+ * expiry, leaving the live session stale.
20863
+ *
20864
+ * Routes to whichever mechanism is armed: the developer timer if armed,
20865
+ * otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
20866
+ */
20867
+ forceRefreshIfDue() {
20868
+ if (this._developerTimerId !== void 0 && this._activeProvider) {
20869
+ const expiry = this.deps.store.read().expiry_at;
20870
+ if (expiry !== void 0 && Date.now() >= expiry - CREDENTIAL_REFRESH_BUFFER_MS) {
20871
+ logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
20872
+ clearTimeout(this._developerTimerId);
20873
+ this._developerTimerId = void 0;
20874
+ this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
20875
+ }
20876
+ return;
20877
+ }
20878
+ this._deviceTokenManager?.refreshNowIfDue();
20879
+ }
20880
+ /**
20881
+ * Sync the credential's expiry from the server-provided authorization (the
20882
+ * `signalwire.connect` result). SATs are opaque JWE, so
20883
+ * `fabric_subscriber.expires_at` is the authoritative expiry of the token
20884
+ * the session actually connected with — the provider-reported `expiry_at`
20885
+ * is only a hint (and may be wrong or absent). Corrects the stored
20886
+ * credential and re-arms the developer refresh timer against the real
20887
+ * deadline when the provider supports `refresh()`.
20888
+ */
20889
+ syncExpiryFromAuthorization(authorization, provider) {
20890
+ const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
20891
+ if (!expiresAtSec) return;
20892
+ const expiryAt = expiresAtSec * 1e3;
20893
+ const credential = this.deps.store.read();
20894
+ if (credential.expiry_at === expiryAt) return;
20895
+ logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
20896
+ const updated = {
20897
+ ...credential,
20898
+ expiry_at: expiryAt
20899
+ };
20900
+ this.deps.store.write(updated);
20901
+ this.deps.store.persist(updated);
20902
+ if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
20903
+ }
20904
+ /**
20905
+ * Invoke `provider.refresh()` deduped against any concurrent developer
20906
+ * refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
20907
+ * and the orchestrator's -32003 recovery / reconnect re-mint — share one
20908
+ * in-flight promise, so a provider backed by one-time-use rotating refresh
20909
+ * tokens is never invoked twice in parallel.
20910
+ *
20911
+ * The caller owns applying the returned credential (store write, session
20912
+ * reauth, rescheduling); this method only serializes the network call.
20913
+ */
20914
+ async refreshCredential(provider) {
20915
+ if (this._refreshInFlight) return this._refreshInFlight;
20916
+ if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
20917
+ const run = provider.refresh();
20918
+ this._refreshInFlight = run;
20919
+ const clear = () => {
20920
+ if (this._refreshInFlight === run) this._refreshInFlight = void 0;
20921
+ };
20922
+ run.then(clear, clear);
20923
+ return run;
20924
+ }
20925
+ /**
20926
+ * Cancels any scheduled developer-provided refresh. Idempotent.
20927
+ *
20928
+ * @internal Used by the coordinator's own activation flow. External
20929
+ * callers should use {@link suspend} for disconnect-time quiescence —
20930
+ * `suspend()` also pauses the internal Client Bound SAT pipeline.
20931
+ */
20932
+ cancelDeveloperRefresh() {
20933
+ if (this._developerTimerId !== void 0) {
20934
+ clearTimeout(this._developerTimerId);
20935
+ this._developerTimerId = void 0;
20936
+ }
20937
+ }
20938
+ /**
20939
+ * Suspends both refresh paths — cancels the developer timer and pauses
20940
+ * the internal reactive pipeline. Use when the underlying session is
20941
+ * being torn down (e.g., {@link SignalWire.disconnect}). The next
20942
+ * {@link activate} call re-enables the internal pipeline.
20943
+ *
20944
+ * The internal manager's cached token survives — see
20945
+ * {@link DeviceTokenManager.pause} — so a subsequent reconnect can
20946
+ * skip the `/devices/token` exchange entirely.
20947
+ */
20948
+ suspend() {
20949
+ this.cancelDeveloperRefresh();
20950
+ this._deviceTokenManager?.pause();
20951
+ }
20952
+ /**
20953
+ * Asks the Client Bound SAT path to take over refresh. If it accepts,
20954
+ * the developer-provided timer (if any) is cancelled. If it declines, the
20955
+ * developer timer remains armed and a `credential_refresh_fallback`
20956
+ * warning is emitted.
20957
+ *
20958
+ * **Idempotent** — re-entrant calls during an in-flight activation are
20959
+ * dropped. Use `_activationGeneration` to detect stale resolutions
20960
+ * (e.g., a reconnect-triggered activate() that races with an earlier one).
20961
+ */
20962
+ async activate(user, session) {
20963
+ if (this._activating) {
20964
+ logger$6.debug("[Coordinator] activate() in flight; ignoring re-entrant call");
20965
+ return;
20966
+ }
20967
+ if (!this._deviceTokenManager) return;
20968
+ this._deviceTokenManager.resume();
20969
+ const generation = ++this._activationGeneration;
20970
+ this._activating = true;
20971
+ try {
20972
+ const result = await this.withActivationTimeout(this._deviceTokenManager.activate(user, session, (cred) => this.deps.store.merge(cred)));
20973
+ if (generation !== this._activationGeneration) {
20974
+ logger$6.debug("[Coordinator] activate() result discarded (preempted by newer activation)");
20975
+ return;
20976
+ }
20977
+ if (result.activated) {
20978
+ this.cancelDeveloperRefresh();
20979
+ logger$6.debug("[Coordinator] Developer refresh disabled — Client Bound SAT owns refresh");
20980
+ return;
20981
+ }
20982
+ logger$6.warn(`[SignalWire] [SW-REFRESH-FALLBACK] Client Bound SAT declined (reason=${result.reason}); using developer-provided refresh handler.`);
20983
+ this.deps.notifier.onWarning({
20984
+ code: "credential_refresh_fallback",
20985
+ source: "CredentialProvider",
20986
+ reason: result.reason,
20987
+ message: `Client Bound SAT activation declined (${result.reason}); using developer-provided refresh.`
20988
+ });
20989
+ } finally {
20990
+ this._activating = false;
20991
+ }
20992
+ }
20993
+ destroy() {
20994
+ this.cancelDeveloperRefresh();
20995
+ this._deviceTokenManager?.destroy();
20996
+ this._deviceTokenManager = void 0;
20997
+ super.destroy();
20998
+ }
20999
+ /**
21000
+ * Races the manager's `activate()` against a hard timeout. A wedged HTTP
21001
+ * layer (e.g., proxy issues) could otherwise hang the activation
21002
+ * indefinitely, leaving the session with no refresh mechanism while the
21003
+ * `_activating` guard blocks subsequent retries.
21004
+ *
21005
+ * On timeout, the manager is paused immediately. The inner `activate()`
21006
+ * may still complete in the background and emit to `_currentToken$`,
21007
+ * which would normally arm the reactive refresh pipeline; pausing
21008
+ * prevents that pipeline from firing while the developer refresh path
21009
+ * is the active mechanism — preserving the "at most one mechanism
21010
+ * armed" invariant. The next `activate()` call resumes the manager.
21011
+ */
21012
+ async withActivationTimeout(inner) {
21013
+ return new Promise((resolve) => {
21014
+ const timer$4 = setTimeout(() => {
21015
+ this._deviceTokenManager?.pause();
21016
+ resolve({
21017
+ activated: false,
21018
+ reason: "activation-timeout"
21019
+ });
21020
+ }, CREDENTIAL_ACTIVATE_TIMEOUT_MS);
21021
+ inner.then((result) => {
21022
+ clearTimeout(timer$4);
21023
+ resolve(result);
21024
+ }, (error) => {
21025
+ clearTimeout(timer$4);
21026
+ this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error)));
21027
+ resolve({
21028
+ activated: false,
21029
+ reason: "endpoint-failed"
21030
+ });
21031
+ });
21032
+ });
21033
+ }
21034
+ };
21035
+
20354
21036
  //#endregion
20355
21037
  //#region src/managers/DiagnosticsCollector.ts
20356
21038
  const logger$5 = getLogger();
@@ -20871,6 +21553,10 @@ var TransportManager = class extends Destroyable {
20871
21553
  if (!isSignalwireRequest(message)) return true;
20872
21554
  const eventChannel = message.params.event_channel;
20873
21555
  if (!eventChannel) return true;
21556
+ if (message.params.event_type.startsWith("conversation.")) {
21557
+ logger$2.debug(`[Transport] Received conversation event: ${message.params.event_type} (event_channel: ${eventChannel})`);
21558
+ return true;
21559
+ }
20874
21560
  const currentProtocol = this._currentProtocol;
20875
21561
  if (!currentProtocol) return true;
20876
21562
  if (!eventChannel.includes(currentProtocol)) {
@@ -21014,6 +21700,33 @@ var TransportManager = class extends Destroyable {
21014
21700
  }
21015
21701
  };
21016
21702
 
21703
+ //#endregion
21704
+ //#region src/utils/authRecovery.ts
21705
+ /**
21706
+ * Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
21707
+ * Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
21708
+ * underlying signaling error is nested. Bounded by a visited set to guard
21709
+ * against cyclic causes.
21710
+ */
21711
+ function findJSONRPCError(error) {
21712
+ const seen = /* @__PURE__ */ new Set();
21713
+ let current = error;
21714
+ while (current instanceof Error && !seen.has(current)) {
21715
+ seen.add(current);
21716
+ if (current instanceof JSONRPCError) return current;
21717
+ current = current.error ?? current.cause;
21718
+ }
21719
+ }
21720
+ /**
21721
+ * Whether an error is a session-recoverable authentication failure
21722
+ * (`-32002` authentication failed or `-32003` requester validation failed)
21723
+ * that a credential re-mint + retry can heal.
21724
+ */
21725
+ function isRecoverableAuthError(error) {
21726
+ const rpcError = findJSONRPCError(error);
21727
+ return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
21728
+ }
21729
+
21017
21730
  //#endregion
21018
21731
  //#region src/clients/SignalWire.ts
21019
21732
  var import_cjs$1 = require_cjs();
@@ -21064,6 +21777,7 @@ var SignalWire = class extends Destroyable {
21064
21777
  this._isConnected$ = this.createBehaviorSubject(false);
21065
21778
  this._isRegistered$ = this.createBehaviorSubject(false);
21066
21779
  this._errors$ = this.createReplaySubject(1);
21780
+ this._warnings$ = this.createReplaySubject(10);
21067
21781
  this._options = {};
21068
21782
  this._deps = new DependencyContainer();
21069
21783
  this._credentialProvider = credentialProvider;
@@ -21123,6 +21837,29 @@ var SignalWire = class extends Destroyable {
21123
21837
  */
21124
21838
  async resolveCredentials() {
21125
21839
  const fingerprint = await this.initDPoP();
21840
+ this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
21841
+ http: this._deps.http,
21842
+ notifier: {
21843
+ onError: (error) => this._errors$.next(error),
21844
+ onWarning: (warning) => this._warnings$.next(warning),
21845
+ onRefreshExhausted: () => void this.disconnect(),
21846
+ onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
21847
+ },
21848
+ store: {
21849
+ read: () => this._deps.credential,
21850
+ write: (credential) => {
21851
+ this._deps.credential = credential;
21852
+ },
21853
+ merge: (partial) => {
21854
+ this._deps.credential = {
21855
+ ...this._deps.credential,
21856
+ ...partial
21857
+ };
21858
+ this.persistCredential(this._deps.credential);
21859
+ },
21860
+ persist: (credential) => this.persistCredential(credential)
21861
+ }
21862
+ });
21126
21863
  if (this._credentialProvider) return this.validateCredentials(this._credentialProvider, void 0, fingerprint);
21127
21864
  for (const scope of this._deps.persistSession ? ["local", "session"] : ["session"]) try {
21128
21865
  const cached = await this._deps.storage.getItem("sw:cached_credential", scope);
@@ -21152,11 +21889,32 @@ var SignalWire = class extends Destroyable {
21152
21889
  logger$1.error("[SignalWire] Provided credentials have expired.");
21153
21890
  throw new InvalidCredentialsError("Provided credentials have expired.");
21154
21891
  }
21155
- if (_credentials.expiry_at && credentialProvider?.refresh) this.scheduleCredentialRefresh(credentialProvider, _credentials.expiry_at);
21892
+ if (_credentials.expiry_at && credentialProvider?.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(credentialProvider, _credentials.expiry_at);
21893
+ else if (_credentials.expiry_at && !credentialProvider?.refresh) {
21894
+ logger$1.warn(`[SignalWire] [SW-NO-REFRESH-HANDLER] Credential has expiry_at=${_credentials.expiry_at} but no refresh handler. Session will terminate at expiry unless the SAT has 'sat:refresh' scope.`);
21895
+ this._warnings$.next({
21896
+ code: "credential_no_refresh_handler",
21897
+ source: "CredentialProvider",
21898
+ message: "Credential has expiry_at but no refresh handler. Session will terminate at expiry unless the SAT carries 'sat:refresh' scope.",
21899
+ expiresAt: _credentials.expiry_at
21900
+ });
21901
+ }
21156
21902
  this._deps.credential = _credentials;
21157
21903
  this.persistCredential(_credentials);
21158
- if (this.isConnected && this._clientSession.authenticated && _credentials.token) try {
21159
- await this._clientSession.reauthenticate(_credentials.token);
21904
+ await this.reauthenticateLiveSession(_credentials);
21905
+ }
21906
+ /**
21907
+ * Reauthenticate the currently-open session with a freshly obtained
21908
+ * credential so the new token takes effect on the live socket immediately —
21909
+ * not just on the next reconnect. No-op when the session is not
21910
+ * connected/authenticated or the credential carries no token (e.g. an
21911
+ * authorization-state-only refresh). Non-fatal: reauth failures surface on
21912
+ * `errors$` without aborting the refresh that triggered this.
21913
+ */
21914
+ async reauthenticateLiveSession(credential) {
21915
+ if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
21916
+ try {
21917
+ await this._clientSession.reauthenticate(credential.token);
21160
21918
  logger$1.info("[SignalWire] Session refreshed with new credentials.");
21161
21919
  } catch (error) {
21162
21920
  logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
@@ -21164,33 +21922,101 @@ var SignalWire = class extends Destroyable {
21164
21922
  }
21165
21923
  }
21166
21924
  /**
21167
- * Schedules credential refresh with exponential backoff retry on failure.
21168
- * On success, resets attempt counter and schedules the next refresh.
21169
- * After exhausting retries, emits TokenRefreshError and disconnects.
21925
+ * One-shot recovery for a live session that failed with a recoverable auth
21926
+ * error (`-32002`/`-32003`) because its token went stale e.g. a refresh
21927
+ * timer that was throttled while the tab was backgrounded.
21928
+ *
21929
+ * Two steps, first that succeeds wins:
21930
+ * 1. Reauthenticate with the in-memory token (a fresh DPoP proof is
21931
+ * generated automatically). Heals a session whose token was already
21932
+ * refreshed by the coordinator but never applied to the open socket,
21933
+ * and a client-bound session whose server-side auth merely drifted.
21934
+ * 2. Re-mint via the developer provider's `refresh()` and reauthenticate.
21935
+ * Skipped for client-bound sessions (the DeviceTokenManager owns their
21936
+ * refresh; re-minting a base SAT here would drop the DPoP binding).
21937
+ *
21938
+ * @param allowRemint - Whether step 2 (provider re-mint) may run. Callers
21939
+ * pass `false` for non-auth failures so a transient network error never
21940
+ * escalates to a token re-mint; step 1 (in-memory reauth) always runs.
21941
+ * @returns `true` if the session was reauthenticated, `false` otherwise.
21170
21942
  */
21171
- scheduleCredentialRefresh(credentialProvider, expiresAt, attempt = 0) {
21172
- if (this._refreshTimerId !== void 0) clearTimeout(this._refreshTimerId);
21173
- const refreshInterval = attempt === 0 ? Math.max(expiresAt - Date.now() - CREDENTIAL_REFRESH_BUFFER_MS, 1e3) : Math.min(CREDENTIAL_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt) * (.5 + Math.random() * .5), CREDENTIAL_REFRESH_MAX_DELAY_MS);
21174
- this._refreshTimerId = setTimeout(async () => {
21175
- try {
21176
- if (!credentialProvider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
21177
- const newCredentials = await credentialProvider.refresh();
21178
- this._deps.credential = newCredentials;
21179
- this.persistCredential(newCredentials);
21180
- logger$1.info("[SignalWire] Credentials refreshed successfully.");
21181
- if (newCredentials.expiry_at) this.scheduleCredentialRefresh(credentialProvider, newCredentials.expiry_at, 0);
21182
- } catch (error) {
21183
- const nextAttempt = attempt + 1;
21184
- logger$1.error(`[SignalWire] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
21185
- this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21186
- if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleCredentialRefresh(credentialProvider, expiresAt, nextAttempt);
21187
- else {
21188
- logger$1.error("[SignalWire] Credential refresh exhausted all retries. Disconnecting.");
21189
- this._errors$.next(new TokenRefreshError("Credential refresh failed after max retries"));
21190
- this.disconnect();
21191
- }
21943
+ async recoverStaleCredential(allowRemint = true) {
21944
+ const currentToken = this._deps.credential.token;
21945
+ if (currentToken) try {
21946
+ await this._clientSession.reauthenticate(currentToken);
21947
+ return true;
21948
+ } catch (error) {
21949
+ logger$1.debug("[SignalWire] In-memory reauth failed during recovery:", error);
21950
+ }
21951
+ if (!allowRemint || this._clientSession.clientBound || !this._credentialProvider?.refresh) return false;
21952
+ try {
21953
+ const newCredentials = await this.remintCredential(this._credentialProvider);
21954
+ if (!newCredentials.token) return false;
21955
+ this._deps.credential = newCredentials;
21956
+ this.persistCredential(newCredentials);
21957
+ await this._clientSession.reauthenticate(newCredentials.token);
21958
+ if (newCredentials.expiry_at) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
21959
+ return true;
21960
+ } catch (error) {
21961
+ logger$1.error("[SignalWire] Credential recovery failed:", error);
21962
+ return false;
21963
+ }
21964
+ }
21965
+ /**
21966
+ * Re-mint a credential via `provider.refresh()`, routed through the
21967
+ * coordinator's shared in-flight guard so concurrent re-mint paths (a
21968
+ * scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
21969
+ * second `provider.refresh()` in parallel — which rotating one-time-use
21970
+ * refresh tokens reject. Falls back to a direct call only if the coordinator
21971
+ * has not been constructed yet.
21972
+ */
21973
+ async remintCredential(provider) {
21974
+ if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
21975
+ if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
21976
+ return provider.refresh();
21977
+ }
21978
+ /**
21979
+ * Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
21980
+ * The session invokes this only when it is client-bound OR the in-memory
21981
+ * token is expired. The re-mint mechanism depends on the binding:
21982
+ * - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
21983
+ * fresh base SAT the upcoming reconnect can re-bind (the
21984
+ * DeviceTokenManager re-activates afterwards).
21985
+ * - Unbound: the developer's non-interactive `refresh()` handler.
21986
+ * `authenticate()` is deliberately NOT used here — it may be interactive
21987
+ * (a login prompt) and must not fire on a background reconnect.
21988
+ *
21989
+ * Rejects on failure so the session aborts the reconnect rather than
21990
+ * replaying a stale token.
21991
+ */
21992
+ async refreshCredentialForReconnect() {
21993
+ if (!this._credentialProvider) return;
21994
+ try {
21995
+ let newCredentials;
21996
+ if (this._clientSession.clientBound) {
21997
+ const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
21998
+ logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
21999
+ newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
22000
+ } else if (this._credentialProvider.refresh) {
22001
+ logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
22002
+ newCredentials = await this.remintCredential(this._credentialProvider);
22003
+ } else {
22004
+ logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
22005
+ return;
21192
22006
  }
21193
- }, refreshInterval);
22007
+ if (!newCredentials.token) {
22008
+ logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
22009
+ return;
22010
+ }
22011
+ this._deps.credential = newCredentials;
22012
+ this.persistCredential(newCredentials);
22013
+ if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
22014
+ logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
22015
+ } catch (error) {
22016
+ logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
22017
+ this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
22018
+ throw error;
22019
+ }
21194
22020
  }
21195
22021
  /** Persist credential to localStorage when persistSession is enabled. */
21196
22022
  persistCredential(credential) {
@@ -21278,51 +22104,20 @@ var SignalWire = class extends Destroyable {
21278
22104
  this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
21279
22105
  this._clientSession = new ClientSessionManager(() => this._deps.credential, this._transport, this._deps.storage, this._deps.authorizationStateKey, this._deps.deviceController, this._attachManager, this._deps.webRTCApiProvider, this._dpopManager, this._networkMonitor?.networkChange$);
21280
22106
  this._publicSession = new ClientSessionWrapper(this._clientSession);
21281
- this._clientSession.onBeforeReconnect = async () => {
21282
- if (!this._credentialProvider) return;
21283
- try {
21284
- const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
21285
- logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
21286
- const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
21287
- this._deps.credential = newCredentials;
21288
- logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
21289
- } catch (error) {
21290
- logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
21291
- this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21292
- throw error;
21293
- }
21294
- };
22107
+ this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
21295
22108
  this.subscribeTo(this._clientSession.errors$, (error) => {
21296
22109
  this._errors$.next(error);
21297
22110
  });
22111
+ this.subscribeTo(this._clientSession.authorization$, (authorization) => {
22112
+ this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
22113
+ });
21298
22114
  await this._clientSession.connect();
21299
- if (this._dpopManager?.initialized) {
21300
- if (this._refreshTimerId) {
21301
- clearTimeout(this._refreshTimerId);
21302
- this._refreshTimerId = void 0;
21303
- logger$1.debug("[SignalWire] Developer refresh disabled — Client Bound SAT activation starting");
21304
- }
21305
- this._deviceTokenManager = new DeviceTokenManager(this._dpopManager, this._deps.http, (error) => this._errors$.next(error), () => this._deps.credential);
21306
- await this._deviceTokenManager.activate(this._deps.user, this._clientSession, (cred) => {
21307
- this._deps.credential = {
21308
- ...this._deps.credential,
21309
- ...cred
21310
- };
21311
- });
21312
- }
22115
+ await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
21313
22116
  this.subscribeTo(this._clientSession.authenticated$.pipe((0, import_cjs$1.skip)(1), (0, import_cjs$1.filter)(Boolean)), async () => {
21314
22117
  try {
21315
- if (this._deviceTokenManager) {
21316
- await this._deviceTokenManager.activate(this._deps.user, this._clientSession, (cred) => {
21317
- this._deps.credential = {
21318
- ...this._deps.credential,
21319
- ...cred
21320
- };
21321
- });
21322
- logger$1.debug("[SignalWire] Client Bound SAT re-activated after reconnect");
21323
- }
22118
+ await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
21324
22119
  } catch (error) {
21325
- logger$1.error("[SignalWire] Client Bound SAT re-activation failed (non-fatal):", error);
22120
+ logger$1.error("[SignalWire] Refresh re-arm after reconnect failed (non-fatal):", error);
21326
22121
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21327
22122
  }
21328
22123
  try {
@@ -21408,6 +22203,19 @@ var SignalWire = class extends Destroyable {
21408
22203
  get errors$() {
21409
22204
  return this.deferEmission(this._errors$.asObservable());
21410
22205
  }
22206
+ /**
22207
+ * Observable stream of non-fatal SDK warnings.
22208
+ *
22209
+ * Subscribe to detect SDK behaviors that affect session liveness or developer-facing
22210
+ * contracts but do not warrant disconnection — e.g., a fallback from Client Bound SAT
22211
+ * refresh to the developer-provided `refresh()` because the SAT lacks `sat:refresh`
22212
+ * scope. Discriminated by `code`.
22213
+ *
22214
+ * Independent from {@link errors$}: existing error consumers are not notified.
22215
+ */
22216
+ get warnings$() {
22217
+ return this.deferEmission(this._warnings$.asObservable());
22218
+ }
21411
22219
  /** Platform WebRTC capabilities detected at construction time. */
21412
22220
  get platformCapabilities() {
21413
22221
  this._platformCapabilities ??= detectPlatformCapabilities(this._options.webRTCApiProvider);
@@ -21465,6 +22273,13 @@ var SignalWire = class extends Destroyable {
21465
22273
  }
21466
22274
  try {
21467
22275
  this._visibilityController = new VisibilityController();
22276
+ this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible")), () => {
22277
+ try {
22278
+ this._refreshCoordinator?.forceRefreshIfDue();
22279
+ } catch (error) {
22280
+ logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
22281
+ }
22282
+ });
21468
22283
  this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
21469
22284
  logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
21470
22285
  try {
@@ -21476,7 +22291,7 @@ var SignalWire = class extends Destroyable {
21476
22291
  logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
21477
22292
  }
21478
22293
  try {
21479
- this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "3.30.0" });
22294
+ this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.2" });
21480
22295
  } catch (error) {
21481
22296
  logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
21482
22297
  }
@@ -21484,14 +22299,19 @@ var SignalWire = class extends Destroyable {
21484
22299
  /**
21485
22300
  * Disconnects the WebSocket and tears down the current session.
21486
22301
  *
22302
+ * Ends the session identified by the protocol and clears its persisted
22303
+ * resume state (`authorization_state` + protocol) and attach records
22304
+ * together — a later {@link connect} with the same credentials starts a
22305
+ * fresh session and cannot reattach to the ended session's calls.
22306
+ * Credentials and device preferences are preserved. To temporarily stop
22307
+ * receiving inbound calls while keeping the session alive, use
22308
+ * `unregister()` instead.
22309
+ *
21487
22310
  * The client can be reconnected by calling {@link connect} again,
21488
22311
  * which creates a fresh transport and session.
21489
22312
  */
21490
22313
  async disconnect() {
21491
- if (this._refreshTimerId) {
21492
- clearTimeout(this._refreshTimerId);
21493
- this._refreshTimerId = void 0;
21494
- }
22314
+ this._refreshCoordinator?.suspend();
21495
22315
  this._diagnosticsCollector?.record("connection", "disconnected");
21496
22316
  await this.teardownTransportAndSession();
21497
22317
  this._isConnected$.next(false);
@@ -21542,21 +22362,23 @@ var SignalWire = class extends Destroyable {
21542
22362
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21543
22363
  throw error;
21544
22364
  }
21545
- logger$1.debug("[SignalWire] Failed to register user, trying reauthentication...");
21546
- try {
21547
- await this._clientSession.reauthenticate(this._deps.credential.token);
21548
- logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
22365
+ logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
22366
+ let failureCause = error;
22367
+ if (await this.recoverStaleCredential(isRecoverableAuthError(error))) try {
22368
+ logger$1.debug("[SignalWire] Recovery succeeded, retrying register()");
21549
22369
  await this._transport.execute(RPCExecute({
21550
22370
  method: "subscriber.online",
21551
22371
  params: {}
21552
22372
  }));
21553
22373
  this._isRegistered$.next(true);
21554
- } catch (reauthError) {
21555
- logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
21556
- const registerError = new InvalidCredentialsError("Failed to register user, and reauthentication attempt also failed. Please check your credentials.", { cause: reauthError instanceof Error ? reauthError : new Error(String(reauthError), { cause: reauthError }) });
21557
- this._errors$.next(registerError);
21558
- throw registerError;
22374
+ return;
22375
+ } catch (retryError) {
22376
+ logger$1.error("[SignalWire] Register retry after recovery failed:", retryError);
22377
+ failureCause = retryError;
21559
22378
  }
22379
+ const registerError = new InvalidCredentialsError("Failed to register user, and credential recovery also failed. Please check your credentials.", { cause: failureCause instanceof Error ? failureCause : new Error(String(failureCause), { cause: failureCause }) });
22380
+ this._errors$.next(registerError);
22381
+ throw registerError;
21560
22382
  }
21561
22383
  }
21562
22384
  /**
@@ -21609,7 +22431,15 @@ var SignalWire = class extends Destroyable {
21609
22431
  };
21610
22432
  await this.waitAuthentication();
21611
22433
  logger$1.debug("[SignalWire] Dialing with options:", computed_options);
21612
- return this._clientSession.createOutboundCall(destination, computed_options);
22434
+ try {
22435
+ return await this._clientSession.createOutboundCall(destination, computed_options);
22436
+ } catch (error) {
22437
+ if (!isRecoverableAuthError(error)) throw error;
22438
+ logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering credential and retrying once");
22439
+ if (!await this.recoverStaleCredential()) throw error;
22440
+ await this.waitAuthentication();
22441
+ return this._clientSession.createOutboundCall(destination, computed_options);
22442
+ }
21613
22443
  }
21614
22444
  /**
21615
22445
  * Runs a multi-phase connectivity test against the given destination.
@@ -21878,17 +22708,23 @@ var SignalWire = class extends Destroyable {
21878
22708
  prefs.preferredVideoInput = null;
21879
22709
  await this._deviceController.clearDeviceState();
21880
22710
  }
21881
- /** Destroys the client, clearing timers and releasing all resources. */
22711
+ /**
22712
+ * Destroys the client, clearing timers and releasing all resources.
22713
+ *
22714
+ * Intentionally destroying the client ends its session: the resume state
22715
+ * (`authorization_state` + protocol) and the attach records are both
22716
+ * cleared. Credentials and device preferences are preserved — use
22717
+ * {@link resetToDefaults} for a full wipe. To temporarily stop receiving
22718
+ * inbound calls while keeping the session alive, use `unregister()`.
22719
+ */
21882
22720
  destroy() {
21883
- if (this._refreshTimerId) {
21884
- clearTimeout(this._refreshTimerId);
21885
- this._refreshTimerId = void 0;
21886
- }
21887
- this._deviceTokenManager?.destroy();
22721
+ this._refreshCoordinator?.destroy();
22722
+ this._refreshCoordinator = void 0;
21888
22723
  this._dpopManager?.destroy();
21889
- if (this._attachManager) this._attachManager.detachAll();
21890
- this._transport.destroy();
21891
- this._clientSession.destroy();
22724
+ const session = this._clientSession;
22725
+ session?.teardownSessionState();
22726
+ this._transport?.destroy();
22727
+ session?.destroy();
21892
22728
  try {
21893
22729
  this._networkMonitor?.destroy();
21894
22730
  } catch {}
@@ -22001,7 +22837,7 @@ var StaticCredentialProvider = class {
22001
22837
  /**
22002
22838
  * Library version from package.json, injected at build time.
22003
22839
  */
22004
- const version = "3.30.0";
22840
+ const version = "4.0.0-rc.2";
22005
22841
  /**
22006
22842
  * Flag indicating the library has been loaded and is ready to use.
22007
22843
  * For UMD builds: `window.SignalWire.ready`
@@ -22023,7 +22859,7 @@ const ready = true;
22023
22859
  */
22024
22860
  const emitReadyEvent = () => {
22025
22861
  if (typeof window !== "undefined") {
22026
- const event = new CustomEvent("signalwire:js:ready", { detail: { version: "3.30.0" } });
22862
+ const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.2" } });
22027
22863
  window.dispatchEvent(event);
22028
22864
  }
22029
22865
  };
@@ -22049,16 +22885,19 @@ if (typeof process === "undefined") globalThis.process = { env: { NODE_ENV: "pro
22049
22885
  //#endregion
22050
22886
  exports.Address = Address;
22051
22887
  exports.CallCreateError = CallCreateError;
22888
+ exports.CallNotReadyError = CallNotReadyError;
22052
22889
  exports.ClientPreferences = ClientPreferences;
22053
22890
  exports.CollectionFetchError = CollectionFetchError;
22054
22891
  exports.DPoPInitError = DPoPInitError;
22055
22892
  exports.DeviceTokenError = DeviceTokenError;
22056
22893
  exports.EmbedTokenCredentialProvider = EmbedTokenCredentialProvider;
22057
22894
  exports.InvalidCredentialsError = InvalidCredentialsError;
22895
+ exports.MediaAccessError = MediaAccessError;
22058
22896
  exports.MediaTrackError = MediaTrackError;
22059
22897
  exports.MessageParseError = MessageParseError;
22060
22898
  exports.OverconstrainedFallbackError = OverconstrainedFallbackError;
22061
22899
  exports.Participant = Participant;
22900
+ exports.ParticipantNotReadyError = ParticipantNotReadyError;
22062
22901
  exports.PreflightError = PreflightError;
22063
22902
  exports.RecoveryError = RecoveryError;
22064
22903
  exports.SelfCapabilities = SelfCapabilities;