@signalwire/js 4.0.0-rc.2 → 4.0.0-rc.4

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.
@@ -4192,7 +4192,7 @@ var require_merge$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
4192
4192
  var empty_1$7 = require_empty();
4193
4193
  var args_1$8 = require_args();
4194
4194
  var from_1$4 = require_from();
4195
- function merge$6() {
4195
+ function merge$7() {
4196
4196
  var args = [];
4197
4197
  for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
4198
4198
  var scheduler = args_1$8.popScheduler(args);
@@ -4200,7 +4200,7 @@ var require_merge$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
4200
4200
  var sources = args;
4201
4201
  return !sources.length ? empty_1$7.EMPTY : sources.length === 1 ? innerFrom_1$31.innerFrom(sources[0]) : mergeAll_1$3.mergeAll(concurrent)(from_1$4.from(sources, scheduler));
4202
4202
  }
4203
- exports.merge = merge$6;
4203
+ exports.merge = merge$7;
4204
4204
  }));
4205
4205
 
4206
4206
  //#endregion
@@ -6082,7 +6082,7 @@ var require_merge = /* @__PURE__ */ __commonJSMin(((exports) => {
6082
6082
  var mergeAll_1$2 = require_mergeAll();
6083
6083
  var args_1$3 = require_args();
6084
6084
  var from_1$1 = require_from();
6085
- function merge$5() {
6085
+ function merge$6() {
6086
6086
  var args = [];
6087
6087
  for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
6088
6088
  var scheduler = args_1$3.popScheduler(args);
@@ -6091,7 +6091,7 @@ var require_merge = /* @__PURE__ */ __commonJSMin(((exports) => {
6091
6091
  mergeAll_1$2.mergeAll(concurrent)(from_1$1.from(__spreadArray$8([source], __read$8(args)), scheduler)).subscribe(subscriber);
6092
6092
  });
6093
6093
  }
6094
- exports.merge = merge$5;
6094
+ exports.merge = merge$6;
6095
6095
  }));
6096
6096
 
6097
6097
  //#endregion
@@ -6829,12 +6829,12 @@ var require_skip = /* @__PURE__ */ __commonJSMin(((exports) => {
6829
6829
  Object.defineProperty(exports, "__esModule", { value: true });
6830
6830
  exports.skip = void 0;
6831
6831
  var filter_1$3 = require_filter();
6832
- function skip$3(count$1) {
6832
+ function skip$5(count$1) {
6833
6833
  return filter_1$3.filter(function(_, index) {
6834
6834
  return count$1 <= index;
6835
6835
  });
6836
6836
  }
6837
- exports.skip = skip$3;
6837
+ exports.skip = skip$5;
6838
6838
  }));
6839
6839
 
6840
6840
  //#endregion
@@ -6938,7 +6938,7 @@ var require_switchMap = /* @__PURE__ */ __commonJSMin(((exports) => {
6938
6938
  var innerFrom_1$6 = require_innerFrom();
6939
6939
  var lift_1$13 = require_lift();
6940
6940
  var OperatorSubscriber_1$11 = require_OperatorSubscriber();
6941
- function switchMap$7(project, resultSelector) {
6941
+ function switchMap$8(project, resultSelector) {
6942
6942
  return lift_1$13.operate(function(source, subscriber) {
6943
6943
  var innerSubscriber = null;
6944
6944
  var index = 0;
@@ -6962,7 +6962,7 @@ var require_switchMap = /* @__PURE__ */ __commonJSMin(((exports) => {
6962
6962
  }));
6963
6963
  });
6964
6964
  }
6965
- exports.switchMap = switchMap$7;
6965
+ exports.switchMap = switchMap$8;
6966
6966
  }));
6967
6967
 
6968
6968
  //#endregion
@@ -9028,6 +9028,37 @@ const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
9028
9028
  const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
9029
9029
  const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
9030
9030
  const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
9031
+ /**
9032
+ * The server sends `signalwire.ping` every 10s. A socket that stays silent
9033
+ * longer than this is half-open: the server has dropped the session but the
9034
+ * browser never fired `close`.
9035
+ */
9036
+ const SERVER_PING_TIMEOUT_MS = 15e3;
9037
+ /**
9038
+ * How long a `signalwire.ping` the client sends after the server's pings
9039
+ * stopped may take to be answered. The server defers its own pings while its
9040
+ * threadpool is congested, so silence alone does not mean the socket is dead.
9041
+ */
9042
+ const SERVER_PING_PROBE_TIMEOUT_MS = 1e4;
9043
+ /**
9044
+ * How long call setup may wait for the network, measured from local media
9045
+ * settling until the member id arrives.
9046
+ *
9047
+ * Local media acquisition is deliberately NOT inside this: a permission prompt
9048
+ * or a device picker is human time, and a human may take as long as they like
9049
+ * without spending the server's budget. The clock starts when acquisition ends.
9050
+ *
9051
+ * Must exceed `iceGatheringTimeout + the RPC timeout`, which both run inside it.
9052
+ */
9053
+ const DEFAULT_CALL_SIGNALING_TIMEOUT_MS = 12e3;
9054
+ /**
9055
+ * How long an auxiliary leg (screen share, additional device) may take to
9056
+ * connect once its media is in hand.
9057
+ *
9058
+ * One bound for both: the picker is human time and sits outside it. What remains
9059
+ * — offer, ICE, invite, answer, DTLS — is the same work for either leg kind.
9060
+ */
9061
+ const DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS = 15e3;
9031
9062
  const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
9032
9063
  const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
9033
9064
  const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
@@ -9055,6 +9086,14 @@ const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
9055
9086
  /** Buffer in milliseconds before token expiry to trigger refresh. */
9056
9087
  const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
9057
9088
  /**
9089
+ * Clock-skew allowance (ms) for treating an in-memory credential as expired
9090
+ * when deciding whether a fresh (re)connect must re-mint the token before
9091
+ * authenticating. A token within this window of its `expiry_at` is treated as
9092
+ * stale so the reconnect re-mints via the credential provider instead of
9093
+ * replaying a dead token (which the server rejects with -32003).
9094
+ */
9095
+ const CREDENTIAL_EXPIRY_SKEW_MS = 3e4;
9096
+ /**
9058
9097
  * Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
9059
9098
  * to resolve before treating the activation as failed and falling back to
9060
9099
  * the developer-provided refresh path. Prevents a wedged HTTP layer from
@@ -9073,6 +9112,8 @@ const MEDIA_ACCESS_DENIAL_NAMES = [
9073
9112
  "SecurityError",
9074
9113
  "PermissionDeniedError"
9075
9114
  ];
9115
+ /** Error names browsers use when the capture hardware is already held exclusively. */
9116
+ const MEDIA_DEVICE_IN_USE_NAMES = ["NotReadableError", "TrackStartError"];
9076
9117
  /** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
9077
9118
  const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
9078
9119
  /** Number of initial samples used to build a baseline for spike detection. */
@@ -9287,6 +9328,20 @@ var CallCreateError = class extends Error {
9287
9328
  this.name = "CallCreateError";
9288
9329
  }
9289
9330
  };
9331
+ var CallNotReadyError = class extends Error {
9332
+ constructor(callId, options) {
9333
+ super(`Call "${callId}" has no self member context yet: selfId/nodeId have not been received from the server`, options);
9334
+ this.callId = callId;
9335
+ this.name = "CallNotReadyError";
9336
+ }
9337
+ };
9338
+ var ParticipantNotReadyError = class extends Error {
9339
+ constructor(memberId, options) {
9340
+ super(`Participant "${memberId}" has no call context yet: its member state (call_id/node_id) has not been received from the server`, options);
9341
+ this.memberId = memberId;
9342
+ this.name = "ParticipantNotReadyError";
9343
+ }
9344
+ };
9290
9345
  var JSONRPCError = class extends Error {
9291
9346
  constructor(code, message, data, options, requestId) {
9292
9347
  super(message, options);
@@ -9358,6 +9413,32 @@ var CollectionFetchError = class extends Error {
9358
9413
  this.name = "CollectionFetchError";
9359
9414
  }
9360
9415
  };
9416
+ /**
9417
+ * An auxiliary leg did not connect within its budget. Typed rather than a bare
9418
+ * RxJS `TimeoutError` so the leg and cause survive.
9419
+ */
9420
+ var AuxiliaryLegTimeoutError = class extends Error {
9421
+ constructor(leg, originalError) {
9422
+ super(`Timed out waiting for the ${leg} connection to be established`, { cause: originalError });
9423
+ this.leg = leg;
9424
+ this.originalError = originalError;
9425
+ this.name = "AuxiliaryLegTimeoutError";
9426
+ }
9427
+ };
9428
+ /**
9429
+ * An auxiliary leg was removed before it finished connecting.
9430
+ *
9431
+ * Typed rather than a bare resolve so a caller awaiting the start can tell a
9432
+ * cancel apart from a share that actually came up — the public methods return
9433
+ * `void`, so the promise is the only signal they have.
9434
+ */
9435
+ var AuxiliaryLegCancelledError = class extends Error {
9436
+ constructor(leg) {
9437
+ super(`The ${leg} leg was removed before it finished connecting`);
9438
+ this.leg = leg;
9439
+ this.name = "AuxiliaryLegCancelledError";
9440
+ }
9441
+ };
9361
9442
  var MediaTrackError = class extends Error {
9362
9443
  constructor(operation, kind, originalError) {
9363
9444
  super(`Media track ${operation} failed for ${kind}`, { cause: originalError instanceof Error ? originalError : void 0 });
@@ -9371,6 +9452,10 @@ var MediaTrackError = class extends Error {
9371
9452
  function isMediaAccessDenial(originalError) {
9372
9453
  return originalError instanceof Error && MEDIA_ACCESS_DENIAL_NAMES.includes(originalError.name);
9373
9454
  }
9455
+ /** True when a `getUserMedia` rejection means the hardware is already held exclusively. */
9456
+ function isMediaDeviceInUse(originalError) {
9457
+ return originalError instanceof Error && MEDIA_DEVICE_IN_USE_NAMES.includes(originalError.name);
9458
+ }
9374
9459
  /**
9375
9460
  * Failure to acquire local media (camera, microphone, or screen capture)
9376
9461
  * via `getUserMedia`/`getDisplayMedia`.
@@ -9394,6 +9479,22 @@ var MediaAccessError = class extends Error {
9394
9479
  return isMediaAccessDenial(this.originalError);
9395
9480
  }
9396
9481
  };
9482
+ /**
9483
+ * Thrown by `startScreenShare()` when the call is already sharing a screen.
9484
+ *
9485
+ * A call carries at most one screen share. Accepting a second one would
9486
+ * overwrite the only reference the SDK holds to the first, leaving it
9487
+ * capturing and sending with no way to stop it — so the second request is
9488
+ * rejected and the live share is left untouched. Call `stopScreenShare()`
9489
+ * first to replace it.
9490
+ */
9491
+ var ScreenShareAlreadyActiveError = class extends Error {
9492
+ constructor(screenShareId, options) {
9493
+ super(`A screen share is already active on this call (${screenShareId}). Call stopScreenShare() before starting another one.`, options);
9494
+ this.screenShareId = screenShareId;
9495
+ this.name = "ScreenShareAlreadyActiveError";
9496
+ }
9497
+ };
9397
9498
  var DPoPInitError = class extends Error {
9398
9499
  constructor(originalError, message = "Failed to initialize DPoP key pair") {
9399
9500
  super(message, { cause: originalError instanceof Error ? originalError : void 0 });
@@ -9634,9 +9735,9 @@ var require_loglevel = /* @__PURE__ */ __commonJSMin(((exports, module) => {
9634
9735
  defaultLogger$1 = new Logger();
9635
9736
  defaultLogger$1.getLogger = function getLogger$1(name) {
9636
9737
  if (typeof name !== "symbol" && typeof name !== "string" || name === "") throw new TypeError("You must supply a name when creating a logger.");
9637
- var logger$33 = _loggersByName[name];
9638
- if (!logger$33) logger$33 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
9639
- return logger$33;
9738
+ var logger$34 = _loggersByName[name];
9739
+ if (!logger$34) logger$34 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
9740
+ return logger$34;
9640
9741
  };
9641
9742
  var _log = typeof window !== undefinedType ? window.log : void 0;
9642
9743
  defaultLogger$1.noConflict = function() {
@@ -9672,8 +9773,8 @@ const defaultLoggerLevel = defaultLogger.levels.WARN;
9672
9773
  defaultLogger.setLevel(defaultLoggerLevel);
9673
9774
  let userLogger = null;
9674
9775
  /** Replace the built-in logger with a custom implementation. Pass `null` to restore defaults. */
9675
- const setLogger = (logger$33) => {
9676
- userLogger = logger$33;
9776
+ const setLogger = (logger$34) => {
9777
+ userLogger = logger$34;
9677
9778
  };
9678
9779
  let debugOptions = {};
9679
9780
  /** Configure debug options (e.g., `{ logWsTraffic: true }`). */
@@ -9717,8 +9818,8 @@ const wsTraffic = (options) => {
9717
9818
  loggerInstance.debug(`${options.type.toUpperCase()}: \n`, msg, "\n");
9718
9819
  };
9719
9820
  const getLogger = () => {
9720
- const logger$33 = getLoggerInstance();
9721
- return new Proxy(logger$33, { get(_target, prop, _receiver) {
9821
+ const logger$34 = getLoggerInstance();
9822
+ return new Proxy(logger$34, { get(_target, prop, _receiver) {
9722
9823
  if (prop === "wsTraffic") return wsTraffic;
9723
9824
  const instance = getLoggerInstance();
9724
9825
  const value = Reflect.get(instance, prop);
@@ -9770,7 +9871,7 @@ const asyncRetry = async ({ asyncCallable, maxRetries: retries = DEFAULT_MAX_RET
9770
9871
 
9771
9872
  //#endregion
9772
9873
  //#region src/controllers/HTTPRequestController.ts
9773
- const logger$32 = getLogger();
9874
+ const logger$33 = getLogger();
9774
9875
  const GET_PARAMS = {
9775
9876
  method: "GET",
9776
9877
  headers: { Accept: "application/json" }
@@ -9834,7 +9935,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9834
9935
  this._responses$.next(response);
9835
9936
  return response;
9836
9937
  } catch (error) {
9837
- logger$32.error("[HTTPRequestController] Request error:", error);
9938
+ logger$33.error("[HTTPRequestController] Request error:", error);
9838
9939
  this._status$.next("error");
9839
9940
  const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
9840
9941
  this._errors$.next(err);
@@ -9861,7 +9962,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9861
9962
  const url = this.buildURL(request.url);
9862
9963
  const headers = this.buildHeaders(request.headers);
9863
9964
  const timeout$5 = request.timeout ?? this.requestTimeout;
9864
- logger$32.debug("[HTTPRequestController] Executing request:", {
9965
+ logger$33.debug("[HTTPRequestController] Executing request:", {
9865
9966
  method: request.method,
9866
9967
  url,
9867
9968
  headers: Object.keys(headers).reduce((acc, key) => {
@@ -9881,7 +9982,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9881
9982
  });
9882
9983
  clearTimeout(timeoutId);
9883
9984
  const httpResponse = await this.convertResponse(response);
9884
- logger$32.debug("[HTTPRequestController] Response received:", {
9985
+ logger$33.debug("[HTTPRequestController] Response received:", {
9885
9986
  status: response.status,
9886
9987
  statusText: response.statusText,
9887
9988
  headers: [...response.headers.entries()],
@@ -9891,7 +9992,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9891
9992
  } catch (error) {
9892
9993
  clearTimeout(timeoutId);
9893
9994
  if (error instanceof Error && error.name === "AbortError") throw new RequestTimeoutError(`Request timeout after ${timeout$5}ms`, { cause: error });
9894
- logger$32.error("[HTTPRequestController] Request failed:", error);
9995
+ logger$33.error("[HTTPRequestController] Request failed:", error);
9895
9996
  throw error;
9896
9997
  }
9897
9998
  }
@@ -9905,8 +10006,8 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9905
10006
  const credential = this.getCredential();
9906
10007
  if (credential.token) {
9907
10008
  headers.Authorization = `Bearer ${credential.token}`;
9908
- logger$32.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
9909
- } else logger$32.warn("[HTTPRequestController] No credentials available for authentication");
10009
+ logger$33.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
10010
+ } else logger$33.warn("[HTTPRequestController] No credentials available for authentication");
9910
10011
  return headers;
9911
10012
  }
9912
10013
  /**
@@ -10054,7 +10155,7 @@ function fromMsToSec(milliseconds) {
10054
10155
 
10055
10156
  //#endregion
10056
10157
  //#region src/containers/PreferencesContainer.ts
10057
- const logger$31 = getLogger();
10158
+ const logger$32 = getLogger();
10058
10159
  var PreferencesContainer = class PreferencesContainer {
10059
10160
  static get instance() {
10060
10161
  this._instance ??= new PreferencesContainer();
@@ -10716,7 +10817,7 @@ var ClientPreferences = class {
10716
10817
  if (!this._storage) return;
10717
10818
  const data = collectStoredPreferences();
10718
10819
  this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
10719
- logger$31.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
10820
+ logger$32.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
10720
10821
  });
10721
10822
  }
10722
10823
  /** Loads preferences from storage and applies them to the container. */
@@ -10725,7 +10826,7 @@ var ClientPreferences = class {
10725
10826
  this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
10726
10827
  if (stored) applyStoredPreferences(stored);
10727
10828
  }).catch((error) => {
10728
- logger$31.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
10829
+ logger$32.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
10729
10830
  });
10730
10831
  }
10731
10832
  };
@@ -10747,7 +10848,7 @@ function toError(value) {
10747
10848
  //#endregion
10748
10849
  //#region src/controllers/NavigatorDeviceController.ts
10749
10850
  var import_cjs$29 = require_cjs();
10750
- const logger$30 = getLogger();
10851
+ const logger$31 = getLogger();
10751
10852
  /** Maps a device kind to its storage key. */
10752
10853
  const DEVICE_STORAGE_KEYS = {
10753
10854
  audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
@@ -10769,7 +10870,7 @@ var NavigatorDeviceController = class extends Destroyable {
10769
10870
  super();
10770
10871
  this.webRTCApiProvider = webRTCApiProvider;
10771
10872
  this.deviceChangeHandler = () => {
10772
- logger$30.debug("[DeviceController] Device change detected");
10873
+ logger$31.debug("[DeviceController] Device change detected");
10773
10874
  this.enumerateDevices();
10774
10875
  };
10775
10876
  this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
@@ -10834,13 +10935,13 @@ var NavigatorDeviceController = class extends Destroyable {
10834
10935
  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$)));
10835
10936
  }
10836
10937
  get selectedAudioInputDevice$() {
10837
- 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))));
10938
+ 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$31.debug("[DeviceController] Selected audio input device changed:", info))));
10838
10939
  }
10839
10940
  get selectedAudioOutputDevice$() {
10840
- 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))));
10941
+ 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$31.debug("[DeviceController] Selected audio output device changed:", info))));
10841
10942
  }
10842
10943
  get selectedVideoInputDevice$() {
10843
- 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))));
10944
+ 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$31.debug("[DeviceController] Selected video input device changed:", info))));
10844
10945
  }
10845
10946
  get selectedAudioInputDevice() {
10846
10947
  if (this._audioInputDisabled$.value) return null;
@@ -10915,7 +11016,7 @@ var NavigatorDeviceController = class extends Destroyable {
10915
11016
  if (device) this.persistDeviceSelection("audioinput", device);
10916
11017
  }
10917
11018
  selectVideoInputDevice(device) {
10918
- logger$30.debug("[DeviceController] Setting selected video input device:", device);
11019
+ logger$31.debug("[DeviceController] Setting selected video input device:", device);
10919
11020
  if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
10920
11021
  const previous = this._selectedDevicesState$.value.videoinput;
10921
11022
  if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
@@ -10972,7 +11073,7 @@ var NavigatorDeviceController = class extends Destroyable {
10972
11073
  }
10973
11074
  const fromHistory = this._deviceHistory.findInHistory(kind, devices);
10974
11075
  if (fromHistory) {
10975
- logger$30.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
11076
+ logger$31.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
10976
11077
  this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
10977
11078
  return fromHistory;
10978
11079
  }
@@ -11025,7 +11126,7 @@ var NavigatorDeviceController = class extends Destroyable {
11025
11126
  try {
11026
11127
  await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
11027
11128
  } catch (error) {
11028
- logger$30.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
11129
+ logger$31.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
11029
11130
  }
11030
11131
  }
11031
11132
  async loadPersistedDevices() {
@@ -11041,7 +11142,7 @@ var NavigatorDeviceController = class extends Destroyable {
11041
11142
  [kind]: stored
11042
11143
  };
11043
11144
  } catch (error) {
11044
- logger$30.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
11145
+ logger$31.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
11045
11146
  }
11046
11147
  }
11047
11148
  /** Clears device history, persisted selections, and re-enumerates devices. */
@@ -11059,7 +11160,7 @@ var NavigatorDeviceController = class extends Destroyable {
11059
11160
  this.disableDeviceMonitoring();
11060
11161
  this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
11061
11162
  if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = (0, import_cjs$29.interval)(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
11062
- logger$30.debug("[DeviceController] Polling devices due to interval");
11163
+ logger$31.debug("[DeviceController] Polling devices due to interval");
11063
11164
  this.enumerateDevices();
11064
11165
  });
11065
11166
  this.enumerateDevices();
@@ -11085,13 +11186,13 @@ var NavigatorDeviceController = class extends Destroyable {
11085
11186
  videoinput: []
11086
11187
  });
11087
11188
  this._devicesState$.next(devicesByKind);
11088
- logger$30.debug("[DeviceController] Devices enumerated:", {
11189
+ logger$31.debug("[DeviceController] Devices enumerated:", {
11089
11190
  audioInputs: devicesByKind.audioinput.length,
11090
11191
  audioOutputs: devicesByKind.audiooutput.length,
11091
11192
  videoInputs: devicesByKind.videoinput.length
11092
11193
  });
11093
11194
  } catch (error) {
11094
- logger$30.error("[DeviceController] Failed to enumerate devices:", error);
11195
+ logger$31.error("[DeviceController] Failed to enumerate devices:", error);
11095
11196
  this._errors$.next(toError(error));
11096
11197
  }
11097
11198
  }
@@ -11107,7 +11208,7 @@ var NavigatorDeviceController = class extends Destroyable {
11107
11208
  stream.getTracks().forEach((t) => t.stop());
11108
11209
  return capabilities;
11109
11210
  } catch (error) {
11110
- logger$30.error("[DeviceController] Failed to get device capabilities:", error);
11211
+ logger$31.error("[DeviceController] Failed to get device capabilities:", error);
11111
11212
  this._errors$.next(toError(error));
11112
11213
  throw error;
11113
11214
  }
@@ -11358,7 +11459,7 @@ var DependencyContainer = class {
11358
11459
 
11359
11460
  //#endregion
11360
11461
  //#region src/controllers/CryptoController.ts
11361
- const logger$29 = getLogger();
11462
+ const logger$30 = getLogger();
11362
11463
  const DPOP_DB_NAME = "sw-dpop";
11363
11464
  const DPOP_DB_VERSION = 1;
11364
11465
  const DPOP_STORE_NAME = "keys";
@@ -11417,7 +11518,7 @@ async function loadKeyPairFromDB() {
11417
11518
  tx.oncomplete = () => db.close();
11418
11519
  });
11419
11520
  } catch (error) {
11420
- logger$29.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
11521
+ logger$30.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
11421
11522
  return null;
11422
11523
  }
11423
11524
  }
@@ -11437,7 +11538,7 @@ async function saveKeyPairToDB(keyPair) {
11437
11538
  };
11438
11539
  });
11439
11540
  } catch (error) {
11440
- logger$29.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
11541
+ logger$30.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
11441
11542
  }
11442
11543
  }
11443
11544
  async function deleteKeyPairFromDB() {
@@ -11456,7 +11557,7 @@ async function deleteKeyPairFromDB() {
11456
11557
  };
11457
11558
  });
11458
11559
  } catch (error) {
11459
- logger$29.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
11560
+ logger$30.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
11460
11561
  }
11461
11562
  }
11462
11563
  /**
@@ -11516,13 +11617,13 @@ var CryptoController = class {
11516
11617
  this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
11517
11618
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
11518
11619
  this._initialized = true;
11519
- logger$29.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
11620
+ logger$30.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
11520
11621
  return this._fingerprint;
11521
11622
  } catch (error) {
11522
- logger$29.warn("[DPoP] Stored key pair unusable, generating new one:", error);
11623
+ logger$30.warn("[DPoP] Stored key pair unusable, generating new one:", error);
11523
11624
  await deleteKeyPairFromDB();
11524
11625
  }
11525
- logger$29.debug("[DPoP] Generating RSA key pair");
11626
+ logger$30.debug("[DPoP] Generating RSA key pair");
11526
11627
  this._keyPair = await crypto.subtle.generateKey({
11527
11628
  name: "RSASSA-PKCS1-v1_5",
11528
11629
  modulusLength: 2048,
@@ -11537,7 +11638,7 @@ var CryptoController = class {
11537
11638
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
11538
11639
  this._initialized = true;
11539
11640
  await saveKeyPairToDB(this._keyPair);
11540
- logger$29.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
11641
+ logger$30.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
11541
11642
  return this._fingerprint;
11542
11643
  }
11543
11644
  /**
@@ -11603,7 +11704,7 @@ var CryptoController = class {
11603
11704
  this._fingerprint = null;
11604
11705
  this._initialized = false;
11605
11706
  deleteKeyPairFromDB();
11606
- logger$29.debug("[DPoP] Controller destroyed");
11707
+ logger$30.debug("[DPoP] Controller destroyed");
11607
11708
  }
11608
11709
  get publicJwk() {
11609
11710
  if (!this._publicJwk) throw new DPoPInitError("CryptoController not initialized. Call init() first.");
@@ -11627,7 +11728,7 @@ var CryptoController = class {
11627
11728
  //#endregion
11628
11729
  //#region src/controllers/NetworkMonitor.ts
11629
11730
  var import_cjs$28 = require_cjs();
11630
- const logger$28 = getLogger();
11731
+ const logger$29 = getLogger();
11631
11732
  /**
11632
11733
  * Safely check whether we are running in a browser environment
11633
11734
  * with `window` and the relevant event targets.
@@ -11684,7 +11785,7 @@ var NetworkMonitor = class extends Destroyable {
11684
11785
  }
11685
11786
  attachListeners() {
11686
11787
  if (!hasBrowserNetworkEvents()) {
11687
- logger$28.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
11788
+ logger$29.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
11688
11789
  return;
11689
11790
  }
11690
11791
  window.addEventListener("online", this._onOnline);
@@ -11692,7 +11793,7 @@ var NetworkMonitor = class extends Destroyable {
11692
11793
  const connection = getNetworkConnection();
11693
11794
  if (connection) connection.addEventListener("change", this._onConnectionChange);
11694
11795
  this._listenersAttached = true;
11695
- logger$28.debug("NetworkMonitor: event listeners attached");
11796
+ logger$29.debug("NetworkMonitor: event listeners attached");
11696
11797
  }
11697
11798
  removeListeners() {
11698
11799
  if (!this._listenersAttached) return;
@@ -11703,10 +11804,10 @@ var NetworkMonitor = class extends Destroyable {
11703
11804
  if (connection) connection.removeEventListener("change", this._onConnectionChange);
11704
11805
  }
11705
11806
  this._listenersAttached = false;
11706
- logger$28.debug("NetworkMonitor: event listeners removed");
11807
+ logger$29.debug("NetworkMonitor: event listeners removed");
11707
11808
  }
11708
11809
  handleOnline() {
11709
- logger$28.info("NetworkMonitor: browser went online");
11810
+ logger$29.info("NetworkMonitor: browser went online");
11710
11811
  this._isOnline$.next(true);
11711
11812
  this._networkChange$.next({
11712
11813
  type: "online",
@@ -11715,7 +11816,7 @@ var NetworkMonitor = class extends Destroyable {
11715
11816
  });
11716
11817
  }
11717
11818
  handleOffline() {
11718
- logger$28.info("NetworkMonitor: browser went offline");
11819
+ logger$29.info("NetworkMonitor: browser went offline");
11719
11820
  this._isOnline$.next(false);
11720
11821
  this._networkChange$.next({
11721
11822
  type: "offline",
@@ -11724,7 +11825,7 @@ var NetworkMonitor = class extends Destroyable {
11724
11825
  }
11725
11826
  handleConnectionChange() {
11726
11827
  const networkType = getNetworkType();
11727
- logger$28.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
11828
+ logger$29.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
11728
11829
  this._networkChange$.next({
11729
11830
  type: "connection_change",
11730
11831
  timestamp: Date.now(),
@@ -11840,7 +11941,7 @@ function getNavigatorMediaDevices() {
11840
11941
  //#endregion
11841
11942
  //#region src/controllers/PreflightRunner.ts
11842
11943
  var import_cjs$27 = require_cjs();
11843
- const logger$27 = getLogger();
11944
+ const logger$28 = getLogger();
11844
11945
  const DEFAULT_MEDIA_TEST_DURATION_S = 10;
11845
11946
  const ICE_GATHERING_TIMEOUT_MS = 1e4;
11846
11947
  const SIGNALING_RTT_TIMEOUT_MS = 5e3;
@@ -11889,7 +11990,7 @@ var PreflightRunner = class extends Destroyable {
11889
11990
  if (!this._options.skipMediaTest) try {
11890
11991
  bandwidth = await this.testMediaBandwidth(destination);
11891
11992
  } catch (error) {
11892
- logger$27.warn("[PreflightRunner] Media bandwidth test failed:", error);
11993
+ logger$28.warn("[PreflightRunner] Media bandwidth test failed:", error);
11893
11994
  warnings.push("Media bandwidth test failed");
11894
11995
  }
11895
11996
  return {
@@ -11901,7 +12002,7 @@ var PreflightRunner = class extends Destroyable {
11901
12002
  warnings
11902
12003
  };
11903
12004
  } catch (error) {
11904
- logger$27.error("[PreflightRunner] Preflight test failed:", error);
12005
+ logger$28.error("[PreflightRunner] Preflight test failed:", error);
11905
12006
  throw new PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
11906
12007
  } finally {
11907
12008
  this.destroy();
@@ -11932,7 +12033,7 @@ var PreflightRunner = class extends Destroyable {
11932
12033
  if (track.kind === "video" && track.readyState === "live") videoWorking = true;
11933
12034
  }
11934
12035
  } catch (error) {
11935
- logger$27.warn("[PreflightRunner] Device test failed:", error);
12036
+ logger$28.warn("[PreflightRunner] Device test failed:", error);
11936
12037
  } finally {
11937
12038
  if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
11938
12039
  }
@@ -11990,7 +12091,7 @@ var PreflightRunner = class extends Destroyable {
11990
12091
  rttMs
11991
12092
  };
11992
12093
  } catch (error) {
11993
- logger$27.warn("[PreflightRunner] ICE connectivity test failed:", error);
12094
+ logger$28.warn("[PreflightRunner] ICE connectivity test failed:", error);
11994
12095
  return {
11995
12096
  type: "failed",
11996
12097
  turnReachable: false,
@@ -12038,7 +12139,7 @@ var PreflightRunner = class extends Destroyable {
12038
12139
  //#endregion
12039
12140
  //#region src/controllers/VisibilityController.ts
12040
12141
  var import_cjs$26 = require_cjs();
12041
- const logger$26 = getLogger();
12142
+ const logger$27 = getLogger();
12042
12143
  /**
12043
12144
  * Checks whether the document visibility API is available.
12044
12145
  */
@@ -12075,8 +12176,8 @@ var VisibilityController = class extends Destroyable {
12075
12176
  this._boundHandler = this._handleVisibilityChange.bind(this);
12076
12177
  if (this._hasVisibilityApi) {
12077
12178
  document.addEventListener("visibilitychange", this._boundHandler);
12078
- logger$26.debug("VisibilityController: listening for visibilitychange events");
12079
- } else logger$26.debug("VisibilityController: document visibility API not available, defaulting to visible");
12179
+ logger$27.debug("VisibilityController: listening for visibilitychange events");
12180
+ } else logger$27.debug("VisibilityController: document visibility API not available, defaulting to visible");
12080
12181
  }
12081
12182
  /**
12082
12183
  * Observable of the current visibility state.
@@ -12101,7 +12202,7 @@ var VisibilityController = class extends Destroyable {
12101
12202
  destroy() {
12102
12203
  if (this._hasVisibilityApi) {
12103
12204
  document.removeEventListener("visibilitychange", this._boundHandler);
12104
- logger$26.debug("VisibilityController: removed visibilitychange listener");
12205
+ logger$27.debug("VisibilityController: removed visibilitychange listener");
12105
12206
  }
12106
12207
  super.destroy();
12107
12208
  }
@@ -12119,7 +12220,7 @@ var VisibilityController = class extends Destroyable {
12119
12220
  timestamp: Date.now()
12120
12221
  };
12121
12222
  this._visibilityChange$.next(changeEvent);
12122
- logger$26.debug("VisibilityController: visibility changed", {
12223
+ logger$27.debug("VisibilityController: visibility changed", {
12123
12224
  from: previousState,
12124
12225
  to: newState
12125
12226
  });
@@ -12270,6 +12371,12 @@ const RPCReauthenticate = (params) => {
12270
12371
 
12271
12372
  //#endregion
12272
12373
  //#region src/core/RPCMessages/RPCPing.ts
12374
+ const RPCPing = () => {
12375
+ return buildRPCRequest({
12376
+ method: "signalwire.ping",
12377
+ params: { timestamp: Date.now() / 1e3 }
12378
+ });
12379
+ };
12273
12380
  const RPCPingResponse = (id, timestamp$1) => {
12274
12381
  return makeRPCResponse({
12275
12382
  id,
@@ -12358,15 +12465,57 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
12358
12465
  result: {}
12359
12466
  });
12360
12467
 
12468
+ //#endregion
12469
+ //#region src/utils/authRecovery.ts
12470
+ /**
12471
+ * Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
12472
+ * Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
12473
+ * underlying signaling error is nested. Bounded by a visited set to guard
12474
+ * against cyclic causes.
12475
+ */
12476
+ function findJSONRPCError(error) {
12477
+ const seen = /* @__PURE__ */ new Set();
12478
+ let current = error;
12479
+ while (current instanceof Error && !seen.has(current)) {
12480
+ seen.add(current);
12481
+ if (current instanceof JSONRPCError) return current;
12482
+ current = current.error ?? current.cause;
12483
+ }
12484
+ }
12485
+ /**
12486
+ * Whether an error is a session-recoverable authentication failure
12487
+ * (`-32002` authentication failed or `-32003` requester validation failed)
12488
+ * that a credential re-mint + retry can heal.
12489
+ */
12490
+ function isRecoverableAuthError(error) {
12491
+ const rpcError = findJSONRPCError(error);
12492
+ return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
12493
+ }
12494
+ /**
12495
+ * Whether an error is specifically a requester-validation rejection
12496
+ * (`-32003`) — the server refusing the session's credential.
12497
+ *
12498
+ * Narrower than {@link isRecoverableAuthError} on purpose. `-32002` is
12499
+ * overloaded server-side: a rejected reattach arrives as `-32002` with
12500
+ * `cause: INVALID_MSG_UNSPECIFIED` and message `CALL ERROR`, which is a
12501
+ * call-level rejection and says nothing about the credential. Use this where
12502
+ * the decision must not be fooled by that, such as deciding whether retrying
12503
+ * an operation could possibly succeed.
12504
+ */
12505
+ function isRequesterValidationError(error) {
12506
+ return findJSONRPCError(error)?.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED;
12507
+ }
12508
+
12361
12509
  //#endregion
12362
12510
  //#region src/managers/AttachManager.ts
12363
- const logger$25 = getLogger();
12511
+ const logger$26 = getLogger();
12364
12512
  var AttachManager = class {
12365
- constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
12513
+ constructor(storage, deviceController, reconnectCallsTimeout, attachKey, credentialRecovered) {
12366
12514
  this.storage = storage;
12367
12515
  this.deviceController = deviceController;
12368
12516
  this.reconnectCallsTimeout = reconnectCallsTimeout;
12369
12517
  this.attachKey = attachKey;
12518
+ this.credentialRecovered = credentialRecovered;
12370
12519
  this.writeQueue = Promise.resolve();
12371
12520
  }
12372
12521
  async detachAll() {
@@ -12381,7 +12530,7 @@ var AttachManager = class {
12381
12530
  try {
12382
12531
  return await this.storage.getItem(this.attachKey) ?? {};
12383
12532
  } catch (error) {
12384
- logger$25.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
12533
+ logger$26.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
12385
12534
  return {};
12386
12535
  }
12387
12536
  }
@@ -12389,7 +12538,7 @@ var AttachManager = class {
12389
12538
  try {
12390
12539
  await this.storage.setItem(this.attachKey, attached);
12391
12540
  } catch (error) {
12392
- logger$25.warn("[AttachManager] Failed to write attached calls to storage", error);
12541
+ logger$26.warn("[AttachManager] Failed to write attached calls to storage", error);
12393
12542
  }
12394
12543
  }
12395
12544
  /**
@@ -12408,11 +12557,39 @@ var AttachManager = class {
12408
12557
  }
12409
12558
  async attach(call) {
12410
12559
  if (!call.to) {
12411
- logger$25.warn("[AttachManager] Skip attach for calls with no destination");
12560
+ logger$26.warn("[AttachManager] Skip attach for calls with no destination");
12412
12561
  return;
12413
12562
  }
12563
+ const attachment = this.buildAttachment(call, call.to);
12564
+ await this.mutate((attached) => ({
12565
+ ...attached,
12566
+ [call.id]: attachment
12567
+ }));
12568
+ }
12569
+ /**
12570
+ * Keep an already-stored call's reference alive and current — the periodic
12571
+ * refresh the `verto.ping` keepalive drives.
12572
+ *
12573
+ * Only ever updates: a call with no record is one nothing wants reattached,
12574
+ * and re-creating it here would undo a `detach`. That matters because a ping
12575
+ * can land in the window between `bye()` detaching and the call being torn
12576
+ * down, and a record revived there survives the hangup — so the next page
12577
+ * load dials a call nobody is on. The existence check and the write share
12578
+ * one {@link mutate} turn, so a concurrent detach cannot slip between them.
12579
+ */
12580
+ async refresh(call) {
12581
+ if (!call.to) return;
12414
12582
  const destination = call.to;
12415
- const attachment = {
12583
+ await this.mutate((attached) => {
12584
+ if (!Object.hasOwn(attached, call.id)) return attached;
12585
+ return {
12586
+ ...attached,
12587
+ [call.id]: this.buildAttachment(call, destination)
12588
+ };
12589
+ });
12590
+ }
12591
+ buildAttachment(call, destination) {
12592
+ return {
12416
12593
  nodeId: call.nodeId,
12417
12594
  destination,
12418
12595
  mediaDirections: call.mediaDirections,
@@ -12420,10 +12597,6 @@ var AttachManager = class {
12420
12597
  videoInputDevice: call.mediaDirections.video !== "inactive" ? this.deviceController.selectedVideoInputDevice : null,
12421
12598
  attachedAt: Date.now()
12422
12599
  };
12423
- await this.mutate((attached) => ({
12424
- ...attached,
12425
- [call.id]: attachment
12426
- }));
12427
12600
  }
12428
12601
  async detach(call) {
12429
12602
  await this.mutate((attached) => {
@@ -12446,8 +12619,14 @@ var AttachManager = class {
12446
12619
  * rejecting. Once that fix is deployed, this will work for both
12447
12620
  * page reloads and WebSocket reconnects.
12448
12621
  *
12449
- * Failed reattach attempts are handled gracefully — the stale call
12450
- * reference is cleaned up from storage.
12622
+ * A failed reattach does NOT generally cost the stored reference. It is
12623
+ * discarded only when the server denied the reattach on a session whose
12624
+ * credential it had already accepted — a verified reauthentication followed
12625
+ * by a refusal is the server saying the call is gone, and that is the one
12626
+ * refusal worth acting on. Until then the credential may be what is being
12627
+ * refused, and the record is the only way a later reload can try again;
12628
+ * keeping it costs nothing, since `detachExpired` reaps it once it is older
12629
+ * than `reconnectCallsTimeout`.
12451
12630
  */
12452
12631
  async reattachCalls() {
12453
12632
  const attached = await this.readAttached();
@@ -12456,25 +12635,31 @@ var AttachManager = class {
12456
12635
  const { destination } = attachment;
12457
12636
  const options = this.buildCallOptions(attachment);
12458
12637
  let succeeded = false;
12638
+ let refusedOnCredentials = false;
12459
12639
  for (let attempt = 1; attempt <= 3; attempt++) try {
12460
12640
  await this.session.createOutboundCall(destination, {
12461
12641
  callId,
12462
12642
  ...options
12463
12643
  });
12464
- logger$25.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
12644
+ logger$26.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
12465
12645
  succeeded = true;
12466
12646
  break;
12467
12647
  } catch (error) {
12468
- logger$25.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
12648
+ logger$26.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
12649
+ if (isRequesterValidationError(error)) {
12650
+ refusedOnCredentials = true;
12651
+ logger$26.warn(`[AttachManager] Reattach of ${callId} was refused on credentials; not retrying.`);
12652
+ break;
12653
+ }
12469
12654
  if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
12470
12655
  }
12471
- if (!succeeded) {
12472
- logger$25.warn(`[AttachManager] Reattach failed after 3 attempts for call ${callId}, removing reference`);
12656
+ if (!succeeded) if (this.credentialRecovered() && !refusedOnCredentials) {
12657
+ logger$26.warn(`[AttachManager] Reattach of ${callId} was denied after a verified reauthentication, removing reference`);
12473
12658
  await this.detach({
12474
12659
  id: callId,
12475
12660
  mediaDirections: attachment.mediaDirections
12476
12661
  });
12477
- }
12662
+ } else logger$26.warn(`[AttachManager] Reattach failed for call ${callId}; keeping the reference (credential refused or never proven good)`);
12478
12663
  }
12479
12664
  }
12480
12665
  /**
@@ -13640,7 +13825,7 @@ function toggleHandraiseMethod(is) {
13640
13825
 
13641
13826
  //#endregion
13642
13827
  //#region src/core/entities/Participant.ts
13643
- const logger$24 = getLogger();
13828
+ const logger$25 = getLogger();
13644
13829
  const initialState = {};
13645
13830
  /**
13646
13831
  * Represents a participant in a call.
@@ -13650,9 +13835,9 @@ const initialState = {};
13650
13835
  * the local participant with additional device control.
13651
13836
  */
13652
13837
  var Participant = class extends Destroyable {
13653
- constructor(id, executeMethod, deviceController) {
13838
+ constructor(id, callExecuteMethod, deviceController) {
13654
13839
  super();
13655
- this.executeMethod = executeMethod;
13840
+ this.callExecuteMethod = callExecuteMethod;
13656
13841
  this.deviceController = deviceController;
13657
13842
  this._state$ = this.createBehaviorSubject(initialState);
13658
13843
  this.id = id;
@@ -13874,22 +14059,55 @@ var Participant = class extends Destroyable {
13874
14059
  get value() {
13875
14060
  return this._state$.value;
13876
14061
  }
14062
+ /**
14063
+ * Target triple for member RPCs, built from the participant's own state.
14064
+ * The backend locates the member's session by the target `call_id`/`node_id`,
14065
+ * so this must always be the participant's own call context — never the
14066
+ * local call's id (issue #19400).
14067
+ *
14068
+ * Reading it doubles as a readiness probe: it throws until the first full
14069
+ * member event (`member.joined`/`member.updated` or the `call.joined`
14070
+ * roster) arrives, and never regresses afterwards.
14071
+ *
14072
+ * @throws {ParticipantNotReadyError} If the member state has not been
14073
+ * received yet (e.g. a participant first seen via `member.talking`) — an
14074
+ * empty call context can never address the member, so fail fast instead of
14075
+ * sending a doomed RPC.
14076
+ */
14077
+ get target() {
14078
+ const { call_id, node_id } = this._state$.value;
14079
+ if (!call_id || !node_id) throw new ParticipantNotReadyError(this.id);
14080
+ return {
14081
+ member_id: this.id,
14082
+ call_id,
14083
+ node_id
14084
+ };
14085
+ }
14086
+ /**
14087
+ * Executes a member RPC against this participant, injecting its own
14088
+ * {@link target} as the target.
14089
+ *
14090
+ * @throws {ParticipantNotReadyError} Via {@link target}, when the
14091
+ * member state has not been received yet.
14092
+ */
14093
+ async executeMethod(method, args) {
14094
+ return this.callExecuteMethod(this.target, method, args);
14095
+ }
13877
14096
  /** Toggles the deafened state (mutes/unmutes incoming audio). */
13878
14097
  async toggleDeaf() {
13879
- const method = toggleDeafMethod(this.deaf);
13880
- await this.executeMethod(this.id, method, {});
14098
+ await this.executeMethod(toggleDeafMethod(this.deaf), {});
13881
14099
  }
13882
14100
  /** Toggles the hand-raised state. */
13883
14101
  async toggleHandraise() {
13884
- await this.executeMethod(this.id, toggleHandraiseMethod(this.handraised), {});
14102
+ await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
13885
14103
  }
13886
14104
  /** Mutes the participant's audio. */
13887
14105
  async mute() {
13888
- await this.executeMethod(this.id, "call.mute", { channels: ["audio"] });
14106
+ await this.executeMethod("call.mute", { channels: ["audio"] });
13889
14107
  }
13890
14108
  /** Unmutes the participant's audio. */
13891
14109
  async unmute() {
13892
- await this.executeMethod(this.id, "call.unmute", { channels: ["audio"] });
14110
+ await this.executeMethod("call.unmute", { channels: ["audio"] });
13893
14111
  }
13894
14112
  /** Toggles the participant's audio mute state. */
13895
14113
  async toggleMute() {
@@ -13897,11 +14115,11 @@ var Participant = class extends Destroyable {
13897
14115
  }
13898
14116
  /** Mutes the participant's video. */
13899
14117
  async muteVideo() {
13900
- await this.executeMethod(this.id, "call.mute", { channels: ["video"] });
14118
+ await this.executeMethod("call.mute", { channels: ["video"] });
13901
14119
  }
13902
14120
  /** Unmutes the participant's video. */
13903
14121
  async unmuteVideo() {
13904
- await this.executeMethod(this.id, "call.unmute", { channels: ["video"] });
14122
+ await this.executeMethod("call.unmute", { channels: ["video"] });
13905
14123
  }
13906
14124
  /** Toggles the participant's video mute state. */
13907
14125
  async toggleMuteVideo() {
@@ -13909,7 +14127,7 @@ var Participant = class extends Destroyable {
13909
14127
  }
13910
14128
  /** Toggles echo cancellation on the audio input. */
13911
14129
  async toggleEchoCancellation() {
13912
- await this.executeMethod(this.id, "call.audioflags.set", {
14130
+ await this.executeMethod("call.audioflags.set", {
13913
14131
  echo_cancellation: !this.echoCancellation,
13914
14132
  auto_gain: this.autoGain,
13915
14133
  noise_suppression: this.noiseSuppression
@@ -13917,7 +14135,7 @@ var Participant = class extends Destroyable {
13917
14135
  }
13918
14136
  /** Toggles automatic gain control on the audio input. */
13919
14137
  async toggleAudioInputAutoGain() {
13920
- await this.executeMethod(this.id, "call.audioflags.set", {
14138
+ await this.executeMethod("call.audioflags.set", {
13921
14139
  echo_cancellation: this.echoCancellation,
13922
14140
  auto_gain: !this.autoGain,
13923
14141
  noise_suppression: this.noiseSuppression
@@ -13925,7 +14143,7 @@ var Participant = class extends Destroyable {
13925
14143
  }
13926
14144
  /** Toggles noise suppression on the audio input. */
13927
14145
  async toggleNoiseSuppression() {
13928
- await this.executeMethod(this.id, "call.audioflags.set", {
14146
+ await this.executeMethod("call.audioflags.set", {
13929
14147
  echo_cancellation: this.echoCancellation,
13930
14148
  auto_gain: this.autoGain,
13931
14149
  noise_suppression: !this.noiseSuppression
@@ -13933,7 +14151,7 @@ var Participant = class extends Destroyable {
13933
14151
  }
13934
14152
  /** Toggles low-bitrate mode for this participant's media. */
13935
14153
  async toggleLowbitrate() {
13936
- await this.executeMethod(this.id, "call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
14154
+ await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
13937
14155
  }
13938
14156
  /**
13939
14157
  * Adjusts the **conference-only** microphone energy gate / sensitivity level
@@ -13949,7 +14167,7 @@ var Participant = class extends Destroyable {
13949
14167
  * (integer, larger values are more sensitive).
13950
14168
  */
13951
14169
  async setAudioInputSensitivity(value) {
13952
- await this.executeMethod(this.id, "call.microphone.sensitivity.set", { sensitivity: value });
14170
+ await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
13953
14171
  }
13954
14172
  /**
13955
14173
  * Sets the **server-side** microphone volume on this participant's bridged
@@ -13962,7 +14180,7 @@ var Participant = class extends Destroyable {
13962
14180
  * @param value - Volume level (0-100).
13963
14181
  */
13964
14182
  async setAudioInputVolume(value) {
13965
- await this.executeMethod(this.id, "call.microphone.volume.set", { volume: value });
14183
+ await this.executeMethod("call.microphone.volume.set", { volume: value });
13966
14184
  }
13967
14185
  /**
13968
14186
  * Sets the **server-side** speaker volume on this participant's bridged call
@@ -13976,45 +14194,31 @@ var Participant = class extends Destroyable {
13976
14194
  * @param value - Volume level (0-100).
13977
14195
  */
13978
14196
  async setAudioOutputVolume(value) {
13979
- await this.executeMethod(this.id, "call.speaker.volume.set", { volume: value });
14197
+ await this.executeMethod("call.speaker.volume.set", { volume: value });
13980
14198
  }
13981
14199
  /**
13982
14200
  * Sets the participant's position in the video layout.
13983
14201
  *
13984
- * Requires the `member.position` capability. The gateway keys positions by the
13985
- * **target member's own** `call_id`/`node_id` (see issue #19400 and the legacy
13986
- * `setPositions` implementation), so this sends the participant's own call
13987
- * context — matching {@link Participant.remove}. A resolved promise does not
13988
- * guarantee a visible change: the backend silently returns `200` (no-op) for
13989
- * non-conference targets.
14202
+ * Requires the `member.position` capability. The gateway requires a
14203
+ * `targets` array of `{ target, position }` entries (issue #19400). A
14204
+ * resolved promise does not guarantee a visible change: the backend silently
14205
+ * returns `200` (no-op) for non-conference targets.
13990
14206
  *
13991
14207
  * @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
13992
14208
  */
13993
14209
  async setPosition(value) {
13994
- const state = this._state$.value;
13995
- const target = {
13996
- member_id: this.id,
13997
- call_id: state.call_id ?? "",
13998
- node_id: state.node_id ?? ""
13999
- };
14000
- await this.executeMethod(target, "call.member.position.set", { targets: [{
14001
- target,
14210
+ await this.executeMethod("call.member.position.set", { targets: [{
14211
+ target: this.target,
14002
14212
  position: value
14003
14213
  }] });
14004
14214
  }
14005
14215
  /** Removes this participant from the call. */
14006
14216
  async remove() {
14007
- const state = this._state$.value;
14008
- const target = {
14009
- member_id: this.id,
14010
- call_id: state.call_id ?? "",
14011
- node_id: state.node_id ?? ""
14012
- };
14013
- await this.executeMethod(target, "call.member.remove", {});
14217
+ await this.executeMethod("call.member.remove", { targets: [this.target] });
14014
14218
  }
14015
14219
  /** Ends the call for this participant. */
14016
14220
  async end() {
14017
- await this.executeMethod(this.id, "call.end", {});
14221
+ await this.executeMethod("call.end", {});
14018
14222
  }
14019
14223
  /**
14020
14224
  * Replaces custom metadata for this participant.
@@ -14034,7 +14238,7 @@ var Participant = class extends Destroyable {
14034
14238
  }
14035
14239
  /** Destroys the participant, releasing all subscriptions and references. */
14036
14240
  destroy() {
14037
- this.executeMethod = void 0;
14241
+ this.callExecuteMethod = void 0;
14038
14242
  super.destroy();
14039
14243
  }
14040
14244
  };
@@ -14046,8 +14250,8 @@ var Participant = class extends Destroyable {
14046
14250
  */
14047
14251
  var SelfParticipant = class extends Participant {
14048
14252
  /** @internal */
14049
- constructor(id, executeMethod, vertoManager, deviceController) {
14050
- super(id, executeMethod, deviceController);
14253
+ constructor(id, callExecuteMethod, vertoManager, deviceController) {
14254
+ super(id, callExecuteMethod, deviceController);
14051
14255
  this.vertoManager = vertoManager;
14052
14256
  this._studioAudio$ = this.createBehaviorSubject(false);
14053
14257
  this.capabilities = new SelfCapabilities();
@@ -14071,7 +14275,7 @@ var SelfParticipant = class extends Participant {
14071
14275
  async enableStudioAudio() {
14072
14276
  if (this._studioAudio$.value) return;
14073
14277
  this._studioAudio$.next(true);
14074
- await this.executeMethod(this.id, "call.audioflags.set", {
14278
+ await this.executeMethod("call.audioflags.set", {
14075
14279
  echo_cancellation: false,
14076
14280
  auto_gain: false,
14077
14281
  noise_suppression: false
@@ -14084,7 +14288,7 @@ var SelfParticipant = class extends Participant {
14084
14288
  async disableStudioAudio() {
14085
14289
  if (!this._studioAudio$.value) return;
14086
14290
  this._studioAudio$.next(false);
14087
- await this.executeMethod(this.id, "call.audioflags.set", {
14291
+ await this.executeMethod("call.audioflags.set", {
14088
14292
  echo_cancellation: true,
14089
14293
  auto_gain: true,
14090
14294
  noise_suppression: true
@@ -14093,17 +14297,27 @@ var SelfParticipant = class extends Participant {
14093
14297
  /**
14094
14298
  * Starts sharing the local screen.
14095
14299
  *
14300
+ * A call carries at most one screen share. Read `screenShareStatus` before
14301
+ * calling and treat `'starting'`/`'stopping'` as busy.
14302
+ *
14096
14303
  * The call is unaffected when acquisition fails.
14097
14304
  *
14305
+ * @param options - Pass `{ audio: true }` to also request the shared
14306
+ * surface's audio. Defaults to video only.
14307
+ * @throws {ScreenShareAlreadyActiveError} When this call is already
14308
+ * sharing a screen. Call {@link stopScreenShare} before starting another.
14309
+ * @throws {AuxiliaryLegCancelledError} When {@link stopScreenShare} removes
14310
+ * the share before its leg finishes connecting.
14098
14311
  * @throws The raw `getDisplayMedia` error. A dismissed picker or a
14099
14312
  * permission denial rejects with a `NotAllowedError` `DOMException` —
14100
14313
  * inspect `error.name` to tell benign cancels apart from real failures.
14101
14314
  */
14102
- async startScreenShare() {
14315
+ async startScreenShare(options) {
14103
14316
  try {
14104
- await this.vertoManager.addScreenMedia();
14317
+ await this.vertoManager.addScreenMedia(options);
14105
14318
  } catch (error) {
14106
- logger$24.error("[Participant.startScreenShare] Screen share error:", error);
14319
+ if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.startScreenShare] Screen share cancelled before connecting.");
14320
+ else logger$25.error("[Participant.startScreenShare] Screen share error:", error);
14107
14321
  throw error;
14108
14322
  }
14109
14323
  }
@@ -14124,14 +14338,18 @@ var SelfParticipant = class extends Participant {
14124
14338
  *
14125
14339
  * The call is unaffected when acquisition fails.
14126
14340
  *
14341
+ * @throws {AuxiliaryLegCancelledError} When {@link removeAdditionalDevice}
14342
+ * removes the device before its leg finishes connecting.
14127
14343
  * @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
14128
- * permission denial) — inspect `error.name` to decide how to react.
14344
+ * permission denial) — inspect `error.name` to decide how to react — or
14345
+ * `AuxiliaryLegTimeoutError` if the leg does not connect in time.
14129
14346
  */
14130
14347
  async addAdditionalDevice(options) {
14131
14348
  try {
14132
14349
  await this.vertoManager.addInputDevice(options);
14133
14350
  } catch (error) {
14134
- logger$24.error("[Participant.addAdditionalDevice] Additional device error:", error);
14351
+ if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.addAdditionalDevice] Device removed before connecting.");
14352
+ else logger$25.error("[Participant.addAdditionalDevice] Additional device error:", error);
14135
14353
  throw error;
14136
14354
  }
14137
14355
  }
@@ -14166,22 +14384,31 @@ var SelfParticipant = class extends Participant {
14166
14384
  this.deviceController.selectAudioInputDevice(device);
14167
14385
  if (options.savePreference) PreferencesContainer.instance.preferredAudioInput = device;
14168
14386
  }
14169
- /** Updates the audio input track constraints for the active call. */
14387
+ /**
14388
+ * Updates the audio input track constraints for the active call.
14389
+ * @returns whether the constraints reached the media the call is sending.
14390
+ */
14170
14391
  async setAudioInputDeviceConstraints(constraints) {
14171
- await this.vertoManager.updateMediaConstraints({ audio: constraints });
14392
+ return this.vertoManager.updateMediaConstraints({ audio: constraints });
14172
14393
  }
14173
- /** Updates both audio and video input track constraints for the active call. */
14394
+ /**
14395
+ * Updates both audio and video input track constraints for the active call.
14396
+ * @returns whether both kinds took the constraints.
14397
+ */
14174
14398
  async setInputDevicesConstraints(constraints) {
14175
- await this.vertoManager.updateMediaConstraints(constraints);
14399
+ return this.vertoManager.updateMediaConstraints(constraints);
14176
14400
  }
14177
14401
  /** Selects the video input device for future calls. Optionally saves as a preference. */
14178
14402
  selectVideoInputDevice(device, options = {}) {
14179
14403
  this.deviceController.selectVideoInputDevice(device);
14180
14404
  if (options.savePreference) PreferencesContainer.instance.preferredVideoInput = device;
14181
14405
  }
14182
- /** Updates the video input track constraints for the active call. */
14406
+ /**
14407
+ * Updates the video input track constraints for the active call.
14408
+ * @returns whether the constraints reached the media the call is sending.
14409
+ */
14183
14410
  async setVideoInputDeviceConstraints(constraints) {
14184
- await this.vertoManager.updateMediaConstraints({ video: constraints });
14411
+ return this.vertoManager.updateMediaConstraints({ video: constraints });
14185
14412
  }
14186
14413
  /** Selects the audio output device. Optionally saves as a preference. */
14187
14414
  selectAudioOutputDevice(device, options = {}) {
@@ -14194,7 +14421,7 @@ var SelfParticipant = class extends Participant {
14194
14421
  */
14195
14422
  exitStudioModeIfActive() {
14196
14423
  if (this._studioAudio$.value) {
14197
- logger$24.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
14424
+ logger$25.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
14198
14425
  this._studioAudio$.next(false);
14199
14426
  }
14200
14427
  }
@@ -14218,7 +14445,7 @@ var SelfParticipant = class extends Participant {
14218
14445
  try {
14219
14446
  await super.mute();
14220
14447
  } catch (error) {
14221
- logger$24.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
14448
+ logger$25.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
14222
14449
  } finally {
14223
14450
  this.vertoManager.muteMainAudioInputDevice();
14224
14451
  }
@@ -14228,7 +14455,7 @@ var SelfParticipant = class extends Participant {
14228
14455
  try {
14229
14456
  await super.unmute();
14230
14457
  } catch (error) {
14231
- logger$24.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
14458
+ logger$25.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
14232
14459
  } finally {
14233
14460
  await this.vertoManager.unmuteMainAudioInputDevice();
14234
14461
  }
@@ -14238,7 +14465,7 @@ var SelfParticipant = class extends Participant {
14238
14465
  try {
14239
14466
  await super.muteVideo();
14240
14467
  } catch (error) {
14241
- logger$24.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
14468
+ logger$25.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
14242
14469
  } finally {
14243
14470
  this.vertoManager.muteMainVideoInputDevice();
14244
14471
  }
@@ -14248,7 +14475,7 @@ var SelfParticipant = class extends Participant {
14248
14475
  try {
14249
14476
  await super.unmuteVideo();
14250
14477
  } catch (error) {
14251
- logger$24.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
14478
+ logger$25.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
14252
14479
  } finally {
14253
14480
  await this.vertoManager.unmuteMainVideoInputDevice();
14254
14481
  }
@@ -14453,7 +14680,7 @@ function filterAs(predicate, resultPath) {
14453
14680
  //#endregion
14454
14681
  //#region src/operators/throwOnRPCError.ts
14455
14682
  var import_cjs$21 = require_cjs();
14456
- const logger$23 = getLogger();
14683
+ const logger$24 = getLogger();
14457
14684
  /**
14458
14685
  * RxJS operator that throws a {@link JSONRPCError} when the RPC response contains an error.
14459
14686
  * Passes successful responses through unchanged.
@@ -14461,14 +14688,14 @@ const logger$23 = getLogger();
14461
14688
  function throwOnRPCError() {
14462
14689
  return (0, import_cjs$21.map)((response) => {
14463
14690
  if (response.error) {
14464
- logger$23.error("[throwOnRPCError] RPC error response:", {
14691
+ logger$24.error("[throwOnRPCError] RPC error response:", {
14465
14692
  code: response.error.code,
14466
14693
  message: response.error.message,
14467
14694
  data: response.error.data
14468
14695
  });
14469
14696
  throw new JSONRPCError(response.error.code, response.error.message, response.error.data);
14470
14697
  }
14471
- logger$23.debug("[throwOnRPCError] RPC successful response:", response);
14698
+ logger$24.debug("[throwOnRPCError] RPC successful response:", response);
14472
14699
  return response;
14473
14700
  });
14474
14701
  }
@@ -14476,7 +14703,7 @@ function throwOnRPCError() {
14476
14703
  //#endregion
14477
14704
  //#region src/managers/CallEventsManager.ts
14478
14705
  var import_cjs$20 = require_cjs();
14479
- const logger$22 = getLogger();
14706
+ const logger$23 = getLogger();
14480
14707
  const initialSessionState = {};
14481
14708
  /** @internal */
14482
14709
  var CallEventsManager = class extends Destroyable {
@@ -14580,7 +14807,7 @@ var CallEventsManager = class extends Destroyable {
14580
14807
  }
14581
14808
  initSubscriptions() {
14582
14809
  this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
14583
- logger$22.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
14810
+ logger$23.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
14584
14811
  callId: callJoinedEvent.call_id,
14585
14812
  roomSessionId: callJoinedEvent.room_session_id
14586
14813
  });
@@ -14607,19 +14834,19 @@ var CallEventsManager = class extends Destroyable {
14607
14834
  if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
14608
14835
  });
14609
14836
  this.subscribeTo(this.memberUpdates$, (member) => {
14610
- logger$22.debug("[CallEventsManager] Handling member update event for member ID:", member);
14837
+ logger$23.debug("[CallEventsManager] Handling member update event for member ID:", member);
14611
14838
  this.upsertParticipant(member);
14612
14839
  });
14613
14840
  this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
14614
- logger$22.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
14841
+ logger$23.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
14615
14842
  const participants = { ...this._participants$.value };
14616
14843
  if (memberLeftEvent.member.member_id in participants) {
14617
14844
  delete participants[memberLeftEvent.member.member_id];
14618
14845
  this._participants$.next(participants);
14619
- } else logger$22.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
14846
+ } else logger$23.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
14620
14847
  });
14621
14848
  this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
14622
- logger$22.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
14849
+ logger$23.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
14623
14850
  const roomSession = callUpdatedEvent.room_session;
14624
14851
  this._sessionState$.next({
14625
14852
  ...this._sessionState$.value,
@@ -14634,7 +14861,7 @@ var CallEventsManager = class extends Destroyable {
14634
14861
  });
14635
14862
  });
14636
14863
  this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
14637
- logger$22.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
14864
+ logger$23.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
14638
14865
  this._sessionState$.next({
14639
14866
  ...this._sessionState$.value,
14640
14867
  layout_name: layoutChangedEvent.id,
@@ -14644,10 +14871,10 @@ var CallEventsManager = class extends Destroyable {
14644
14871
  });
14645
14872
  }
14646
14873
  updateParticipantPositions(layoutChangedEvent) {
14647
- 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.");
14874
+ if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$23.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
14648
14875
  layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
14649
14876
  if (!(layer.member_id in this._participants$.value)) {
14650
- logger$22.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
14877
+ logger$23.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
14651
14878
  return false;
14652
14879
  }
14653
14880
  return true;
@@ -14665,12 +14892,17 @@ var CallEventsManager = class extends Destroyable {
14665
14892
  updateLayouts() {
14666
14893
  if (!this.selfId) return;
14667
14894
  this.webRtcCallSession.executeMethod(this.selfId, "call.layout.list", {}).then((response) => {
14895
+ const layouts = response.result?.layouts;
14896
+ if (!layouts) {
14897
+ logger$23.warn("[CallEventsManager] Layout list response carried no layouts; keeping current layouts");
14898
+ return;
14899
+ }
14668
14900
  this._sessionState$.next({
14669
14901
  ...this._sessionState$.value,
14670
- layouts: response.result.layouts
14902
+ layouts
14671
14903
  });
14672
14904
  }).catch((error) => {
14673
- logger$22.error("[CallEventsManager] Error fetching layouts:", error);
14905
+ logger$23.error("[CallEventsManager] Error fetching layouts:", error);
14674
14906
  });
14675
14907
  }
14676
14908
  updateParticipants(members) {
@@ -14686,7 +14918,7 @@ var CallEventsManager = class extends Destroyable {
14686
14918
  }
14687
14919
  const participant = this._participants$.value[member.member_id];
14688
14920
  const oldValue = participant.value;
14689
- logger$22.debug("[CallEventsManager] Updating participant:", member.member_id, {
14921
+ logger$23.debug("[CallEventsManager] Updating participant:", member.member_id, {
14690
14922
  oldValue,
14691
14923
  newValue: member
14692
14924
  });
@@ -14699,17 +14931,17 @@ var CallEventsManager = class extends Destroyable {
14699
14931
  }
14700
14932
  get callJoinedEvent$() {
14701
14933
  return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, import_cjs$20.filter)(isCallJoinedPayload), (0, import_cjs$20.tap)((event) => {
14702
- logger$22.debug("[CallEventsManager] Call joined event:", event);
14934
+ logger$23.debug("[CallEventsManager] Call joined event:", event);
14703
14935
  })));
14704
14936
  }
14705
14937
  get layoutChangedEvent$() {
14706
14938
  return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filterAs(isLayoutChangedPayload, "layout"), (0, import_cjs$20.tap)((event) => {
14707
- logger$22.debug("[CallEventsManager] Layout changed event:", event);
14939
+ logger$23.debug("[CallEventsManager] Layout changed event:", event);
14708
14940
  })));
14709
14941
  }
14710
14942
  get memberUpdates$() {
14711
14943
  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) => {
14712
- logger$22.debug("[CallEventsManager] Member update event:", event);
14944
+ logger$23.debug("[CallEventsManager] Member update event:", event);
14713
14945
  })));
14714
14946
  }
14715
14947
  destroy() {
@@ -14728,6 +14960,111 @@ var CallEventsManager = class extends Destroyable {
14728
14960
  }
14729
14961
  };
14730
14962
 
14963
+ //#endregion
14964
+ //#region src/controllers/ConstraintFallbackHelper.ts
14965
+ /**
14966
+ * ConstraintFallbackHelper - Provides getUserMedia with automatic constraint
14967
+ * fallback on OverconstrainedError.
14968
+ *
14969
+ * When a specific device ID is requested, the helper tries progressively
14970
+ * looser constraints:
14971
+ * 1. `{ deviceId: { exact: deviceId } }` -- exact match
14972
+ * 2. `{ deviceId: deviceId }` -- preferred (browser may pick another)
14973
+ * 3. `{}` -- no constraint, browser default
14974
+ *
14975
+ * This prevents stale device IDs from blocking call setup.
14976
+ *
14977
+ * @see Section 5.8 and Section 11 of the Implementation Guide
14978
+ */
14979
+ const logger$22 = getLogger();
14980
+ /**
14981
+ * Attempts getUserMedia with progressively looser constraints.
14982
+ *
14983
+ * The function tries three levels of constraint specificity for the given
14984
+ * device kind. Each level is only attempted if the previous one fails with
14985
+ * an OverconstrainedError. Non-OverconstrainedError failures (e.g.,
14986
+ * NotAllowedError) are thrown immediately without fallback.
14987
+ *
14988
+ * @param mediaDevices - Anything exposing `getUserMedia` (a full
14989
+ * `WebRTCMediaDevices`, or a shim wrapping one)
14990
+ * @param constraints - The full MediaStreamConstraints to use as a base
14991
+ * @param kind - Which track kind to apply fallback to ('audio' | 'video')
14992
+ * @param deviceId - The device ID to try (if undefined, calls getUserMedia as-is)
14993
+ * @returns The stream and the fallback level that succeeded
14994
+ * @throws When all fallback levels fail, or when a non-OverconstrainedError occurs
14995
+ */
14996
+ async function getUserMediaWithFallback(mediaDevices, constraints, kind, deviceId) {
14997
+ if (!deviceId) return {
14998
+ stream: await mediaDevices.getUserMedia(constraints),
14999
+ fallbackLevel: "default"
15000
+ };
15001
+ const baseConstraints = typeof constraints[kind] === "object" ? constraints[kind] : {};
15002
+ try {
15003
+ const exactConstraints = {
15004
+ ...constraints,
15005
+ [kind]: {
15006
+ ...baseConstraints,
15007
+ deviceId: { exact: deviceId }
15008
+ }
15009
+ };
15010
+ return {
15011
+ stream: await mediaDevices.getUserMedia(exactConstraints),
15012
+ fallbackLevel: "exact"
15013
+ };
15014
+ } catch (error) {
15015
+ if (!isOverconstrainedError(error)) throw error;
15016
+ logger$22.debug(`[ConstraintFallbackHelper] Exact constraint failed for ${kind}, trying preferred`, { deviceId });
15017
+ }
15018
+ try {
15019
+ const preferredConstraints = {
15020
+ ...constraints,
15021
+ [kind]: {
15022
+ ...baseConstraints,
15023
+ deviceId
15024
+ }
15025
+ };
15026
+ return {
15027
+ stream: await mediaDevices.getUserMedia(preferredConstraints),
15028
+ fallbackLevel: "preferred"
15029
+ };
15030
+ } catch (error) {
15031
+ if (!isOverconstrainedError(error)) throw error;
15032
+ logger$22.debug(`[ConstraintFallbackHelper] Preferred constraint failed for ${kind}, trying default`, { deviceId });
15033
+ }
15034
+ try {
15035
+ const defaultConstraints = {
15036
+ ...constraints,
15037
+ [kind]: { ...baseConstraints }
15038
+ };
15039
+ if (typeof defaultConstraints[kind] === "object") {
15040
+ const { deviceId: _removed, ...rest } = defaultConstraints[kind];
15041
+ defaultConstraints[kind] = rest;
15042
+ }
15043
+ const stream = await mediaDevices.getUserMedia(defaultConstraints);
15044
+ logger$22.warn(`[ConstraintFallbackHelper] Fell back to browser default for ${kind}`, { requestedDeviceId: deviceId });
15045
+ return {
15046
+ stream,
15047
+ fallbackLevel: "default"
15048
+ };
15049
+ } catch (error) {
15050
+ logger$22.error(`[ConstraintFallbackHelper] All fallback levels exhausted for ${kind}`, {
15051
+ deviceId,
15052
+ error
15053
+ });
15054
+ throw error;
15055
+ }
15056
+ }
15057
+ /**
15058
+ * Checks whether an error is an OverconstrainedError.
15059
+ *
15060
+ * Browsers may throw either a native OverconstrainedError or a DOMException
15061
+ * with a specific name.
15062
+ */
15063
+ function isOverconstrainedError(error) {
15064
+ if (error instanceof Error) return error.name === "OverconstrainedError" || error.name === "ConstraintNotSatisfiedError";
15065
+ return false;
15066
+ }
15067
+
14731
15068
  //#endregion
14732
15069
  //#region src/helpers/SDPHelper.ts
14733
15070
  /**
@@ -15286,6 +15623,7 @@ var LocalStreamController = class extends Destroyable {
15286
15623
  this._localAudioTracks$ = this.createBehaviorSubject([]);
15287
15624
  this._localVideoTracks$ = this.createBehaviorSubject([]);
15288
15625
  this._mediaTrackEnded$ = this.createSubject();
15626
+ this._trackOrigins = /* @__PURE__ */ new WeakMap();
15289
15627
  }
15290
15628
  get localStream$() {
15291
15629
  return this._localStream$.asObservable().pipe((0, import_cjs$17.takeUntil)(this.destroyed$));
@@ -15308,6 +15646,22 @@ var LocalStreamController = class extends Destroyable {
15308
15646
  get localVideoTracks() {
15309
15647
  return this._localVideoTracks$.value;
15310
15648
  }
15649
+ tagTracks(tracks, origin) {
15650
+ for (const track of tracks) this._trackOrigins.set(track, origin);
15651
+ }
15652
+ setTrackOrigin(track, origin) {
15653
+ this._trackOrigins.set(track, origin);
15654
+ }
15655
+ getTrackOrigin(track) {
15656
+ return this._trackOrigins.get(track);
15657
+ }
15658
+ /**
15659
+ * Fail-safe: an unrecorded track reads as not-a-device-capture, so a missed
15660
+ * tagging site leaves media alone rather than destroying it.
15661
+ */
15662
+ isDeviceCapture(track) {
15663
+ return this._trackOrigins.get(track) === "device";
15664
+ }
15311
15665
  /**
15312
15666
  * Build the local media stream based on the provided options.
15313
15667
  */
@@ -15317,13 +15671,16 @@ var LocalStreamController = class extends Destroyable {
15317
15671
  if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
15318
15672
  const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
15319
15673
  stream = new MediaStream(tracks);
15674
+ this.tagTracks(tracks, "application");
15320
15675
  } else if (this.options.propose === "screenshare") {
15321
- logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
15676
+ const audio = this.options.screenShareAudio ?? false;
15677
+ logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", audio);
15322
15678
  stream = await this.options.getDisplayMedia({
15323
15679
  video: true,
15324
- audio: Boolean(this.options.inputAudioDeviceConstraints)
15680
+ audio
15325
15681
  });
15326
15682
  logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
15683
+ this.tagTracks(stream.getTracks(), "display");
15327
15684
  } else {
15328
15685
  const constraints = {
15329
15686
  audio: this.options.inputAudioDeviceConstraints,
@@ -15332,6 +15689,7 @@ var LocalStreamController = class extends Destroyable {
15332
15689
  logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
15333
15690
  stream = await this.options.getUserMedia(constraints);
15334
15691
  logger$19.debug("[LocalStreamController] User media obtained:", stream);
15692
+ this.tagTracks(stream.getTracks(), "device");
15335
15693
  }
15336
15694
  this._localStream$.next(stream);
15337
15695
  this._localAudioTracks$.next(stream.getAudioTracks());
@@ -15341,10 +15699,13 @@ var LocalStreamController = class extends Destroyable {
15341
15699
  /**
15342
15700
  * Add a local media track to the local stream.
15343
15701
  * @param track - The MediaStreamTrack to add
15702
+ * @param origin - Defaults to `'device'`; every internal caller passes a
15703
+ * fresh `getUserMedia` capture.
15344
15704
  * @returns The MediaStream (either existing or newly created)
15345
15705
  */
15346
- addTrack(track) {
15706
+ addTrack(track, origin = "device") {
15347
15707
  const localStream = this._localStream$.value ?? new MediaStream();
15708
+ this._trackOrigins.set(track, origin);
15348
15709
  track.addEventListener("ended", this.mediaTrackEndedHandler);
15349
15710
  localStream.addTrack(track);
15350
15711
  this._localStream$.next(localStream);
@@ -15614,15 +15975,27 @@ var TransceiverController = class extends Destroyable {
15614
15975
  for (let i = 0; i < Number(msStreamsNumber); i++) this.peerConnection.addTransceiver("video", { direction: "recvonly" });
15615
15976
  }
15616
15977
  }
15978
+ /**
15979
+ * @returns whether every live sender of the kind took the constraints. A
15980
+ * skipped non-device sender, an exhausted fallback, and having no live sender
15981
+ * at all all report `false` — `mediaParamsUpdated.applied` is built from this,
15982
+ * and an application told `true` cannot tell a working push from a no-op.
15983
+ */
15617
15984
  async updateSendersConstraints(kind, constraints) {
15618
15985
  if (!constraints) {
15619
15986
  this.stopTrackSender(kind);
15620
- return Promise.resolve();
15987
+ return false;
15621
15988
  }
15622
15989
  const senders = this.peerConnection.getSenders().filter((sender) => sender.track?.kind === kind && sender.track.readyState === "live");
15990
+ let applied = senders.length > 0;
15623
15991
  for (const sender of senders) {
15624
15992
  const { track } = sender;
15625
15993
  if (track) {
15994
+ if (!this.options.localStreamController.isDeviceCapture(track)) {
15995
+ logger$18.debug(`[TransceiverController] Skipping ${kind} constraints for a non-device track (origin: ${this.options.localStreamController.getTrackOrigin(track) ?? "unrecorded"}), track ${track.id}`);
15996
+ applied = false;
15997
+ continue;
15998
+ }
15626
15999
  const constraintsToApply = {
15627
16000
  ...track.getConstraints(),
15628
16001
  ...constraints
@@ -15638,33 +16011,39 @@ var TransceiverController = class extends Destroyable {
15638
16011
  } catch (fallbackError) {
15639
16012
  logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
15640
16013
  this.options.onError?.(new MediaTrackError("updateSendersConstraints", kind, fallbackError));
16014
+ applied = false;
15641
16015
  }
15642
16016
  }
15643
16017
  }
15644
16018
  }
16019
+ return applied;
15645
16020
  }
15646
16021
  /**
15647
- * Fallback when applyConstraints fails: stop the current track, acquire a new
15648
- * one via getUserMedia with the merged constraints (preserving the current
15649
- * deviceId), replace the sender track, and update the localStream.
16022
+ * Fallback when applyConstraints fails, which on iOS Safari it silently does.
15650
16023
  *
15651
- * This is critical for iOS Safari where applyConstraints on audio tracks
15652
- * silently fails or throws.
16024
+ * Order matters: acquiring before stopping means a failed acquisition leaves
16025
+ * the existing media playing. The deviceId goes through the fallback ladder
16026
+ * rather than pinned `{ exact }`, so a stale id degrades instead of failing.
15653
16027
  */
15654
16028
  async replaceTrackFallback(sender, oldTrack, kind, mergedConstraints) {
15655
16029
  const { deviceId } = oldTrack.getSettings();
15656
- const constraintsWithDevice = {
15657
- ...mergedConstraints,
15658
- ...deviceId ? { deviceId: { exact: deviceId } } : {}
15659
- };
15660
- const trackId = oldTrack.id;
16030
+ const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: this.options.getUserMedia }, { [kind]: mergedConstraints }, kind, deviceId);
16031
+ const newTrack = stream.getTracks().find((t) => t.kind === kind);
16032
+ if (!newTrack) {
16033
+ stream.getTracks().forEach((t) => t.stop());
16034
+ throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
16035
+ }
16036
+ try {
16037
+ await sender.replaceTrack(newTrack);
16038
+ } catch (error) {
16039
+ stream.getTracks().forEach((t) => t.stop());
16040
+ throw error;
16041
+ }
16042
+ const oldTrackId = oldTrack.id;
16043
+ this.options.localStreamController.removeTrack(oldTrackId);
15661
16044
  oldTrack.stop();
15662
- this.options.localStreamController.removeTrack(trackId);
15663
- const newTrack = (await this.options.getUserMedia({ [kind]: constraintsWithDevice })).getTracks().find((t) => t.kind === kind);
15664
- if (!newTrack) throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
15665
- await sender.replaceTrack(newTrack);
15666
16045
  this.options.localStreamController.addTrack(newTrack);
15667
- logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
16046
+ logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind} (deviceId fallback level: ${fallbackLevel}). New track: ${newTrack.id}`);
15668
16047
  }
15669
16048
  getMediaDirections() {
15670
16049
  if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
@@ -15734,48 +16113,44 @@ var RTCPeerConnectionController = class extends Destroyable {
15734
16113
  this.negotiationNeeded$.next();
15735
16114
  };
15736
16115
  this.updateSelectedInputDevice = async (kind, deviceInfo) => {
16116
+ const { localStream } = this;
16117
+ if (!localStream) {
16118
+ logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
16119
+ return;
16120
+ }
16121
+ const currentTrack = localStream.getTracks().find((track) => track.kind === kind);
16122
+ if (!currentTrack) {
16123
+ logger$17.debug(`[RTCPeerConnectionController] No ${kind} track to switch.`);
16124
+ return;
16125
+ }
16126
+ if (!deviceInfo) {
16127
+ logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
16128
+ this.stopTrackSender(kind);
16129
+ return;
16130
+ }
16131
+ const constraints = {
16132
+ ...currentTrack.getConstraints(),
16133
+ ...this.deviceController.deviceInfoToConstraints(deviceInfo)
16134
+ };
15737
16135
  try {
15738
- const { localStream } = this;
15739
- if (!localStream) {
15740
- logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
15741
- return;
15742
- }
15743
- logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
15744
- const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
15745
- if (track) {
15746
- this.transceiverController?.stopTrackSender(kind);
15747
- this.localStreamController.removeTrack(track.id);
15748
- logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
15749
- if (!deviceInfo) {
15750
- logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
15751
- return;
15752
- }
15753
- const streamTrack = (await this.getUserMedia({ [kind]: {
15754
- ...track.getConstraints(),
15755
- ...this.deviceController.deviceInfoToConstraints(deviceInfo)
15756
- } })).getTracks().find((t) => t.kind === kind);
15757
- if (streamTrack) {
15758
- logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
15759
- this.localStreamController.addTrack(streamTrack);
15760
- await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
15761
- logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
15762
- }
15763
- }
15764
- logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
16136
+ const newTrack = await this.acquireInputTrack(kind, constraints, deviceInfo, currentTrack);
16137
+ await this.attachInputTrack(kind, newTrack, currentTrack);
16138
+ logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo.label, newTrack.id);
15765
16139
  } catch (error) {
15766
16140
  logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
15767
16141
  this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
15768
- throw error;
15769
16142
  }
15770
16143
  };
15771
16144
  this._isNegotiating$ = this.createBehaviorSubject(false);
15772
16145
  this._memberId = null;
16146
+ this._nodeId = null;
15773
16147
  this._iceConnectionState$ = this.createReplaySubject(1);
15774
16148
  this._connectionState$ = this.createReplaySubject(1);
15775
16149
  this._signalingState$ = this.createReplaySubject(1);
15776
16150
  this._iceGatheringState$ = this.createReplaySubject(1);
15777
16151
  this._errors$ = this.createReplaySubject(1);
15778
16152
  this._iceCandidates$ = this.createReplaySubject(1);
16153
+ this._localMediaSettled$ = this.createReplaySubject(1);
15779
16154
  this._initialized$ = this.createReplaySubject(1);
15780
16155
  this._remoteDescription$ = this.createReplaySubject(1);
15781
16156
  this._remoteStream$ = this.createBehaviorSubject(null);
@@ -15808,6 +16183,7 @@ var RTCPeerConnectionController = class extends Destroyable {
15808
16183
  inputVideoStream: this.options.inputVideoStream,
15809
16184
  inputAudioDeviceConstraints: this.inputAudioDeviceConstraints,
15810
16185
  inputVideoDeviceConstraints: this.inputVideoDeviceConstraints,
16186
+ screenShareAudio: this.options.screenShareAudio,
15811
16187
  getUserMedia: async (constraints) => this.getUserMedia(constraints),
15812
16188
  getDisplayMedia: async (options$1) => this.getDisplayMedia(options$1)
15813
16189
  });
@@ -15834,6 +16210,13 @@ var RTCPeerConnectionController = class extends Destroyable {
15834
16210
  get memberId() {
15835
16211
  return this._memberId;
15836
16212
  }
16213
+ /** The node this leg's invite landed on — auxiliary legs are placed independently. */
16214
+ setNodeId(nodeId) {
16215
+ this._nodeId = nodeId;
16216
+ }
16217
+ get nodeId() {
16218
+ return this._nodeId;
16219
+ }
15837
16220
  stopTrackSender(kind, options = { updateTransceiverDirection: false }) {
15838
16221
  const audioCovered = kind === "audio" || kind === "both";
15839
16222
  if (audioCovered && this._localAudioPipeline) this.stopRawAudioInputForPipeline();
@@ -15843,10 +16226,7 @@ var RTCPeerConnectionController = class extends Destroyable {
15843
16226
  }
15844
16227
  stopRawAudioInputForPipeline() {
15845
16228
  const rawTracks = this.localStreamController.localAudioTracks;
15846
- for (const track of rawTracks) if (track.readyState === "live") {
15847
- track.stop();
15848
- this.localStreamController.removeTrack(track.id);
15849
- }
16229
+ for (const track of rawTracks) if (track.readyState === "live") this.localStreamController.removeTrack(track.id);
15850
16230
  this._localAudioPipeline?.setInputTrack(null);
15851
16231
  }
15852
16232
  get isNegotiating$() {
@@ -15879,6 +16259,10 @@ var RTCPeerConnectionController = class extends Destroyable {
15879
16259
  get remoteDescription$() {
15880
16260
  return this.cachedObservable("remoteDescription$", () => this._remoteDescription$.asObservable().pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
15881
16261
  }
16262
+ /** Emits once local media is settled — acquired, or knowingly receive-only. */
16263
+ get localMediaSettled$() {
16264
+ return this.cachedObservable("localMediaSettled$", () => this._localMediaSettled$.asObservable().pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
16265
+ }
15882
16266
  get localStream$() {
15883
16267
  return this.cachedObservable("localStream$", () => this.localStreamController.localStream$.pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
15884
16268
  }
@@ -15906,6 +16290,9 @@ var RTCPeerConnectionController = class extends Destroyable {
15906
16290
  get propose() {
15907
16291
  return this.options.propose ?? "main";
15908
16292
  }
16293
+ get connectionState() {
16294
+ return this.peerConnection?.connectionState;
16295
+ }
15909
16296
  get isAdditionalDevice() {
15910
16297
  return this.propose === "additional-device";
15911
16298
  }
@@ -16034,7 +16421,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16034
16421
  this._isNegotiating$.next(true);
16035
16422
  await this._setRemoteDescription(this.sdpInit);
16036
16423
  } else {
16037
- await this.setupTrackHandling();
16424
+ if (!await this.setupTrackHandling()) return;
16038
16425
  this._initialized$.next(true);
16039
16426
  }
16040
16427
  } catch (error) {
@@ -16158,7 +16545,10 @@ var RTCPeerConnectionController = class extends Destroyable {
16158
16545
  inputVideoDeviceConstraints: this.inputVideoDeviceConstraints
16159
16546
  });
16160
16547
  }
16161
- await this.setupLocalTracks();
16548
+ if (!await this.setupLocalTracks()) {
16549
+ logger$17.debug("[RTCPeerConnectionController] Inbound answer abandoned; the connection went away.");
16550
+ return;
16551
+ }
16162
16552
  const { answerOptions } = this;
16163
16553
  logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
16164
16554
  await this.createAnswer(answerOptions);
@@ -16296,35 +16686,52 @@ var RTCPeerConnectionController = class extends Destroyable {
16296
16686
  }
16297
16687
  /**
16298
16688
  * Setup track handling for remote tracks.
16689
+ *
16690
+ * @returns `false` when the connection went away while local media was being
16691
+ * acquired — see {@link setupLocalTracks}.
16299
16692
  */
16300
16693
  async setupTrackHandling() {
16301
16694
  if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
16302
- await this.setupLocalTracks();
16695
+ if (!await this.setupLocalTracks()) return false;
16303
16696
  await this.setupRemoteTracks();
16697
+ return true;
16304
16698
  }
16699
+ /**
16700
+ * @returns `false` when the connection was torn down while getUserMedia was
16701
+ * in flight. The acquisition is not cancellable, so the caller must stop
16702
+ * rather than go on to touch a peer connection that is closed or gone.
16703
+ */
16305
16704
  async setupLocalTracks() {
16306
16705
  logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
16307
16706
  if (this.hasNoLocalMediaToSend()) {
16308
16707
  if (!this.receiveAudio && !this.receiveVideo) throw new InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
16309
16708
  logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
16310
16709
  this.setupReceiveOnlyTransceivers();
16311
- return;
16710
+ this._localMediaSettled$.next();
16711
+ return true;
16312
16712
  }
16313
16713
  let localStream;
16314
16714
  try {
16315
16715
  localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
16316
16716
  } catch (error) {
16317
16717
  this.handleLocalMediaFailure(error);
16318
- return;
16718
+ this._localMediaSettled$.next();
16719
+ return true;
16720
+ }
16721
+ if (!this.peerConnection || this.peerConnection.signalingState === "closed") {
16722
+ logger$17.debug("[RTCPeerConnectionController] Local media arrived after teardown; releasing it.");
16723
+ localStream.getTracks().forEach((track) => track.stop());
16724
+ return false;
16319
16725
  }
16726
+ this._localMediaSettled$.next();
16320
16727
  if (this.transceiverController?.useAddStream ?? false) {
16321
16728
  logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
16322
- this.peerConnection?.addStream(localStream);
16729
+ this.peerConnection.addStream(localStream);
16323
16730
  if (!this.isNegotiating) {
16324
16731
  logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
16325
16732
  this.negotiationNeeded$.next();
16326
16733
  }
16327
- return;
16734
+ return true;
16328
16735
  }
16329
16736
  for (const kind of ["audio", "video"]) {
16330
16737
  const tracks = (kind === "audio" ? localStream.getAudioTracks() : localStream.getVideoTracks()).map((track, index) => ({
@@ -16338,10 +16745,11 @@ var RTCPeerConnectionController = class extends Destroyable {
16338
16745
  await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
16339
16746
  } else {
16340
16747
  logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
16341
- this.peerConnection?.addTrack(track, localStream);
16748
+ this.peerConnection.addTrack(track, localStream);
16342
16749
  }
16343
16750
  }
16344
16751
  }
16752
+ return true;
16345
16753
  }
16346
16754
  /** True for a main connection with no local media to send. */
16347
16755
  hasNoLocalMediaToSend() {
@@ -16430,6 +16838,61 @@ var RTCPeerConnectionController = class extends Destroyable {
16430
16838
  this._localAudioPipeline.setInputTrack(newTrack);
16431
16839
  }
16432
16840
  /**
16841
+ * Capture the newly selected device, leaving the current capture running.
16842
+ *
16843
+ * A rejection must leave the current track sending, so nothing is released
16844
+ * until the replacement is in hand. The one exception is hardware that admits
16845
+ * a single opener — a phone's front and back cameras, typically — which
16846
+ * rejects the second capture until the first is closed.
16847
+ */
16848
+ async acquireInputTrack(kind, constraints, deviceInfo, currentTrack) {
16849
+ try {
16850
+ return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
16851
+ } catch (error) {
16852
+ if (!isMediaDeviceInUse(error)) throw error;
16853
+ logger$17.warn(`[RTCPeerConnectionController] ${kind} device is held exclusively; releasing the current capture to retry:`, error);
16854
+ const previousDeviceId = currentTrack.getSettings().deviceId;
16855
+ this.stopTrackSender(kind);
16856
+ try {
16857
+ return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
16858
+ } catch (retryError) {
16859
+ await this.restorePreviousInputTrack(kind, constraints, previousDeviceId, currentTrack);
16860
+ throw retryError;
16861
+ }
16862
+ }
16863
+ }
16864
+ async captureTrack(kind, constraints, deviceId) {
16865
+ const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { [kind]: constraints }, kind, deviceId);
16866
+ const track = stream.getTracks().find((t) => t.kind === kind);
16867
+ if (!track) {
16868
+ stream.getTracks().forEach((t) => t.stop());
16869
+ throw new MediaTrackError("updateSelectedInputDevice", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
16870
+ }
16871
+ if (fallbackLevel !== "exact") logger$17.warn(`[RTCPeerConnectionController] ${kind} device acquired at fallback level '${fallbackLevel}'; the capture may not be the requested device.`);
16872
+ return track;
16873
+ }
16874
+ /** Best-effort return to the device that was released for an exclusive retry. */
16875
+ async restorePreviousInputTrack(kind, constraints, previousDeviceId, releasedTrack) {
16876
+ try {
16877
+ const restored = await this.captureTrack(kind, constraints, previousDeviceId);
16878
+ await this.attachInputTrack(kind, restored, releasedTrack);
16879
+ } catch (error) {
16880
+ logger$17.error(`[RTCPeerConnectionController] Failed to restore the previous ${kind} device:`, error);
16881
+ }
16882
+ }
16883
+ async attachInputTrack(kind, newTrack, oldTrack) {
16884
+ const pipelineOwnsAudio = kind === "audio" && this._localAudioPipeline;
16885
+ if (!pipelineOwnsAudio) try {
16886
+ await this.transceiverController?.replaceSenderTrack(kind, newTrack);
16887
+ } catch (error) {
16888
+ newTrack.stop();
16889
+ throw error;
16890
+ }
16891
+ this.localStreamController.removeTrack(oldTrack.id);
16892
+ this.localStreamController.addTrack(newTrack);
16893
+ if (pipelineOwnsAudio) this._localAudioPipeline?.setInputTrack(newTrack);
16894
+ }
16895
+ /**
16433
16896
  * Return the lazily-created {@link LocalAudioPipeline}, constructing it on
16434
16897
  * first access. On creation the current audio sender's track is routed
16435
16898
  * through the pipeline (input → gain → analyser → destination) and the
@@ -16462,6 +16925,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16462
16925
  const sender = (this.transceiverController?.audioTransceivers.at(0))?.sender ?? this.peerConnection.getSenders().find((s) => s.track?.kind === "audio");
16463
16926
  if (!sender || !raw) return;
16464
16927
  try {
16928
+ this.localStreamController.setTrackOrigin(this._localAudioPipeline.outputTrack, "processed");
16465
16929
  await sender.replaceTrack(this._localAudioPipeline.outputTrack);
16466
16930
  } catch (error) {
16467
16931
  logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
@@ -16522,37 +16986,68 @@ var RTCPeerConnectionController = class extends Destroyable {
16522
16986
  for (const existingTrack of existingTracks) this.removeLocalTrack(existingTrack.id);
16523
16987
  this.addLocalTrack(track);
16524
16988
  }
16989
+ /**
16990
+ * @returns whether the constraints reached the media the leg is sending.
16991
+ *
16992
+ * With the pipeline engaged the audio sender carries the processed
16993
+ * destination track, so the sender scan would find nothing it may touch and
16994
+ * every audio constraint API would silently no-op. The constraints belong to
16995
+ * the pipeline's device source, which is the capture that sender ultimately
16996
+ * carries.
16997
+ */
16525
16998
  async updateSendersConstraints(kind, constraints) {
16526
- await this.transceiverController?.updateSendersConstraints(kind, constraints);
16999
+ if (kind === "audio" && this._localAudioPipeline) {
17000
+ if (!constraints) {
17001
+ this.stopTrackSender("audio");
17002
+ return false;
17003
+ }
17004
+ return this.applyPipelineSourceConstraints(constraints);
17005
+ }
17006
+ return await this.transceiverController?.updateSendersConstraints(kind, constraints) ?? false;
16527
17007
  }
16528
17008
  /**
16529
- * Replace the current audio track with a new one using the given constraints.
16530
- * Used for server-pushed audio constraint changes where applyConstraints
16531
- * fails on iOS Safari. Stops the current track, acquires a new one via
16532
- * getUserMedia, and replaces the sender track.
17009
+ * Mirror of the sender path for a piped audio leg: same merge, same fallback
17010
+ * ladder, same device-capture invariant — but the swap target is the pipeline
17011
+ * input, so the sender keeps emitting the pipeline's output track and its
17012
+ * identity survives the change.
16533
17013
  */
16534
- async replaceAudioTrackWithConstraints(constraints) {
16535
- const senders = this.peerConnection?.getSenders().filter((s) => s.track?.kind === "audio" && s.track.readyState === "live");
16536
- if (!senders || senders.length === 0) {
16537
- logger$17.warn("[RTCPeerConnectionController] No live audio sender to replace");
16538
- return;
17014
+ async applyPipelineSourceConstraints(constraints) {
17015
+ const pipeline = this._localAudioPipeline;
17016
+ const source = this.localStreamController.localAudioTracks.at(0);
17017
+ if (!pipeline || !source) {
17018
+ logger$17.debug("[RTCPeerConnectionController] No pipeline input to constrain.");
17019
+ return false;
16539
17020
  }
16540
- for (const sender of senders) {
16541
- const oldTrack = sender.track;
16542
- if (!oldTrack) continue;
16543
- const { deviceId } = oldTrack.getSettings();
16544
- const mergedConstraints = {
16545
- ...oldTrack.getConstraints(),
16546
- ...constraints,
16547
- ...deviceId ? { deviceId: { exact: deviceId } } : {}
16548
- };
16549
- const trackId = oldTrack.id;
16550
- oldTrack.stop();
16551
- this.localStreamController.removeTrack(trackId);
16552
- const newTrack = (await this.getUserMedia({ audio: mergedConstraints })).getAudioTracks()[0];
16553
- await sender.replaceTrack(newTrack);
17021
+ if (!this.localStreamController.isDeviceCapture(source)) {
17022
+ logger$17.debug(`[RTCPeerConnectionController] Skipping audio constraints for a non-device pipeline input (origin: ${this.localStreamController.getTrackOrigin(source) ?? "unrecorded"}).`);
17023
+ return false;
17024
+ }
17025
+ const merged = {
17026
+ ...source.getConstraints(),
17027
+ ...constraints
17028
+ };
17029
+ try {
17030
+ await source.applyConstraints(merged);
17031
+ logger$17.debug("[RTCPeerConnectionController] Pipeline input constraints updated:", merged);
17032
+ return true;
17033
+ } catch (error) {
17034
+ logger$17.warn("[RTCPeerConnectionController] applyConstraints failed on the pipeline input, re-acquiring:", error);
17035
+ }
17036
+ try {
17037
+ const { stream } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { audio: merged }, "audio", source.getSettings().deviceId);
17038
+ const newTrack = stream.getAudioTracks().at(0);
17039
+ if (!newTrack) {
17040
+ stream.getTracks().forEach((track) => track.stop());
17041
+ throw new Error("getUserMedia returned no audio track");
17042
+ }
17043
+ this.localStreamController.removeTrack(source.id);
16554
17044
  this.localStreamController.addTrack(newTrack);
16555
- logger$17.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
17045
+ pipeline.setInputTrack(newTrack);
17046
+ return true;
17047
+ } catch (error) {
17048
+ logger$17.warn("[RTCPeerConnectionController] Failed to re-acquire the pipeline input for constraints:", error);
17049
+ this._errors$.next(new MediaTrackError("updateSendersConstraints", "audio", error));
17050
+ return false;
16556
17051
  }
16557
17052
  }
16558
17053
  /**
@@ -16672,6 +17167,48 @@ const logger$16 = getLogger();
16672
17167
  function resolveInviteNodeId(args) {
16673
17168
  return args.isInvite && !args.reattach && !args.explicitNodeId ? "" : args.currentNodeId ?? "";
16674
17169
  }
17170
+ /**
17171
+ * Surface the real outcome of a `webrtc.verto` reply.
17172
+ *
17173
+ * A webrtc.verto response nests several envelopes, each keyed by a verto-style
17174
+ * string `code` ("200" ok, "400"/etc. fail) rather than a JSON-RPC `error`. An outer
17175
+ * layer reports only whether the frame was delivered; an inner layer carries the op's
17176
+ * own outcome:
17177
+ *
17178
+ * response.result = { code:"200", result:{…} } ← delivery acknowledgement
17179
+ * .result = { jsonrpc, id, result:{…} } ← the reply payload
17180
+ * .result = { code:"400", message:"Bad request" } ← the actual op outcome
17181
+ *
17182
+ * A failure can appear at any layer (delivery refused, or the op itself rejected
17183
+ * deeper down), so walk every nested `.result` object and return the FIRST non-2xx
17184
+ * `code` with its message. Returns null when every `code` seen is 2xx or absent —
17185
+ * i.e. the op succeeded. This is the only way to detect that e.g. a mute/kick was
17186
+ * rejected, since the outer delivery `code` is "200" (delivered) even then.
17187
+ *
17188
+ * Pure function — exported for unit testing.
17189
+ */
17190
+ function findNestedVertoFailure(response) {
17191
+ let node = response;
17192
+ while (node !== null && typeof node === "object") {
17193
+ const obj = node;
17194
+ const err = obj.error;
17195
+ if (err !== null && typeof err === "object") {
17196
+ const e = err;
17197
+ const errCode = typeof e.code === "string" || typeof e.code === "number" ? String(e.code) : void 0;
17198
+ if (errCode !== void 0 && !/^2\d\d$/.test(errCode)) return {
17199
+ code: errCode,
17200
+ message: typeof e.message === "string" ? e.message : void 0
17201
+ };
17202
+ }
17203
+ const code = typeof obj.code === "string" || typeof obj.code === "number" ? String(obj.code) : void 0;
17204
+ if (code !== void 0 && !/^2\d\d$/.test(code)) return {
17205
+ code,
17206
+ message: typeof obj.message === "string" ? obj.message : void 0
17207
+ };
17208
+ node = obj.result !== null && typeof obj.result === "object" ? obj.result : null;
17209
+ }
17210
+ return null;
17211
+ }
16675
17212
  var VertoManager = class extends Destroyable {
16676
17213
  constructor(callSession) {
16677
17214
  super();
@@ -16694,7 +17231,8 @@ var WebRTCVertoManager = class extends VertoManager {
16694
17231
  this._signalingStatus$ = this.createReplaySubject(1);
16695
17232
  this._screenShareStatus$ = this.createBehaviorSubject("none");
16696
17233
  this._rtcPeerConnectionsMap = /* @__PURE__ */ new Map();
16697
- this._screenShareTimeoutMs = 5e4;
17234
+ this._lastVertoPings = /* @__PURE__ */ new Map();
17235
+ this._legErrors$ = this.createSubject();
16698
17236
  this._nodeId$ = this.createBehaviorSubject(options.nodeId ?? null);
16699
17237
  this.onError = options.onError;
16700
17238
  this.onModifyFailed = options.onModifyFailed;
@@ -16742,6 +17280,10 @@ var WebRTCVertoManager = class extends VertoManager {
16742
17280
  get selfId$() {
16743
17281
  return this._selfId$.asObservable();
16744
17282
  }
17283
+ /** Separates the media phase of call creation from the signalling phase. */
17284
+ get localMediaSettled$() {
17285
+ return this.mainPeerConnection.localMediaSettled$;
17286
+ }
16745
17287
  get localStream() {
16746
17288
  return this._rtcPeerConnectionsMap.get(this.webRtcCallSession.id)?.localStream ?? null;
16747
17289
  }
@@ -16798,25 +17340,89 @@ var WebRTCVertoManager = class extends VertoManager {
16798
17340
  const { mediaParams, callID } = event;
16799
17341
  const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
16800
17342
  const { audio, video } = mediaParams;
16801
- (async () => {
16802
- try {
16803
- if (audio && rtcPeerConnController) await rtcPeerConnController.replaceAudioTrackWithConstraints(audio);
16804
- if (video) await rtcPeerConnController?.updateSendersConstraints("video", video);
16805
- this.webRtcCallSession.emitMediaParamsUpdated({
16806
- audio,
16807
- video,
16808
- timestamp: Date.now()
16809
- });
16810
- } catch (error) {
16811
- logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
16812
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16813
- }
16814
- })();
17343
+ if (!rtcPeerConnController) {
17344
+ logger$16.warn(`[WebRTCManager] Ignoring server-pushed media params for unknown leg ${callID}`);
17345
+ return;
17346
+ }
17347
+ this.applyServerMediaParams(rtcPeerConnController, audio, video);
16815
17348
  });
16816
17349
  this.subscribeTo(this.vertoPing$, (vertoPing) => {
16817
- this.attachManager.attach(this.buildAttachableCall());
17350
+ this._lastVertoPings.set(vertoPing.callID, vertoPing);
17351
+ this.attachManager.refresh(this.buildAttachableCall());
16818
17352
  this.sendVertoPong(vertoPing);
16819
17353
  });
17354
+ this.subscribeTo(this.webRtcCallSession.clientSession.authenticated$.pipe((0, import_cjs$15.skip)(1), (0, import_cjs$15.filter)(Boolean)), () => {
17355
+ this._lastVertoPings.forEach((vertoPing) => void this.sendVertoPong(vertoPing));
17356
+ });
17357
+ }
17358
+ /**
17359
+ * An auxiliary-leg failure must never destroy the call; main-leg and
17360
+ * call-level errors keep `CallFactory.isFatalError`'s classification.
17361
+ *
17362
+ * Every site holding a peer connection reports through here, so the invariant
17363
+ * is structural rather than per-call-site — which is how cloud-product#20523
17364
+ * happened, with only one of fourteen sites passing `{ fatal: false }`.
17365
+ *
17366
+ * `override` composes rather than replaces: a caller may force non-fatal for a
17367
+ * reason of its own (a `verto.info` frame is best-effort whichever leg carries
17368
+ * it), and an auxiliary leg stays non-fatal regardless.
17369
+ */
17370
+ reportLegError(error, rtcPeerConnController, override) {
17371
+ const leg = rtcPeerConnController?.propose;
17372
+ const legId = rtcPeerConnController?.id;
17373
+ const auxiliary = Boolean(rtcPeerConnController) && !rtcPeerConnController?.isMainDevice;
17374
+ this.onError?.(error, {
17375
+ ...override?.fatal === false || auxiliary ? { fatal: false } : {},
17376
+ ...leg ? { leg } : {},
17377
+ ...legId ? { legId } : {}
17378
+ });
17379
+ if (legId) this._legErrors$.next({
17380
+ legId,
17381
+ error
17382
+ });
17383
+ }
17384
+ /**
17385
+ * Errors reported for one leg, as a stream that fails with them.
17386
+ *
17387
+ * Signaling failures are reported, never thrown — so nothing that waits on a
17388
+ * leg's progress would otherwise learn of a rejected invite. Merging this in
17389
+ * lets the wait end with the reason the server gave.
17390
+ */
17391
+ legError$(legId) {
17392
+ return this._legErrors$.pipe((0, import_cjs$15.filter)((report) => report.legId === legId), (0, import_cjs$15.map)((report) => {
17393
+ throw report.error;
17394
+ }));
17395
+ }
17396
+ /**
17397
+ * Audio and video are applied independently so a failure in one cannot
17398
+ * suppress the other, and `mediaParamsUpdated` is emitted whatever happens —
17399
+ * an application should not be starved of the params by a constraint failure.
17400
+ */
17401
+ async applyServerMediaParams(rtcPeerConnController, audio, video) {
17402
+ const failures = [];
17403
+ let applied = true;
17404
+ if (audio) try {
17405
+ applied = await rtcPeerConnController.updateSendersConstraints("audio", audio) && applied;
17406
+ } catch (error) {
17407
+ applied = false;
17408
+ failures.push(toError(error));
17409
+ }
17410
+ if (video) try {
17411
+ applied = await rtcPeerConnController.updateSendersConstraints("video", video) && applied;
17412
+ } catch (error) {
17413
+ applied = false;
17414
+ failures.push(toError(error));
17415
+ }
17416
+ this.webRtcCallSession.emitMediaParamsUpdated({
17417
+ audio,
17418
+ video,
17419
+ timestamp: Date.now(),
17420
+ applied
17421
+ });
17422
+ for (const failure of failures) {
17423
+ logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", failure);
17424
+ this.reportLegError(failure, rtcPeerConnController, { fatal: false });
17425
+ }
16820
17426
  }
16821
17427
  /**
16822
17428
  * Set node_id/selfId only when the current value is null.
@@ -16825,8 +17431,8 @@ var WebRTCVertoManager = class extends VertoManager {
16825
17431
  * these identifiers before the `verto.invite` RPC response (`CALL CREATED`)
16826
17432
  * arrives. These methods let early events populate them eagerly so that
16827
17433
  * downstream RPC calls (e.g. `call.layout.list`) don't fail with empty
16828
- * identifiers. `processInviteResponse()` remains the authoritative source
16829
- * and always overwrites unconditionally.
17434
+ * identifiers. `processInviteResponse()` remains the authoritative source and
17435
+ * overwrites unconditionally — for selfId, on the main leg only.
16830
17436
  */
16831
17437
  setNodeIdIfNull(nodeId) {
16832
17438
  if (!this._nodeId$.value && nodeId) {
@@ -16849,24 +17455,33 @@ var WebRTCVertoManager = class extends VertoManager {
16849
17455
  this.onError?.(new VertoPongError(error));
16850
17456
  }
16851
17457
  }
17458
+ /**
17459
+ * @returns whether the constraints reached the media the call is sending.
17460
+ * `false` is an outcome, not a failure: the leg may have no live sender of
17461
+ * the kind, or carry media the SDK did not capture and may not replace.
17462
+ * A failure behind it still reaches the call's `errors$`, so a caller that
17463
+ * ignores this value learns of it there.
17464
+ */
16852
17465
  async updateMediaConstraints(options = {}) {
16853
17466
  const { audio, video } = options;
17467
+ let applied = true;
16854
17468
  try {
16855
- if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
16856
- if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
17469
+ if (audio) applied = await this.mainPeerConnection.updateSendersConstraints("audio", audio) && applied;
17470
+ if (video) applied = await this.mainPeerConnection.updateSendersConstraints("video", video) && applied;
16857
17471
  } catch (error) {
16858
17472
  logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
16859
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17473
+ this.reportLegError(toError(error), this.mainPeerConnection);
16860
17474
  throw error;
16861
17475
  }
17476
+ return applied;
16862
17477
  }
16863
17478
  get selfId() {
16864
17479
  return this._selfId$.value;
16865
17480
  }
16866
17481
  /** Build an AttachableCall from the current call state. */
16867
- buildAttachableCall(idOverride) {
17482
+ buildAttachableCall(idOverride, nodeIdOverride) {
16868
17483
  return {
16869
- nodeId: this.nodeId ?? void 0,
17484
+ nodeId: nodeIdOverride ?? this.nodeId ?? void 0,
16870
17485
  id: idOverride ?? this.webRtcCallSession.id,
16871
17486
  to: this.webRtcCallSession.to,
16872
17487
  mediaDirections: this.webRtcCallSession.mediaDirections
@@ -16996,24 +17611,49 @@ var WebRTCVertoManager = class extends VertoManager {
16996
17611
  get vertoPing$() {
16997
17612
  return this.cachedObservable("vertoPing$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoPingInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16998
17613
  }
17614
+ /**
17615
+ * Send a member-control op in-dialog via verto.info.
17616
+ *
17617
+ * The control payload rides in `params.command` — a sibling of `dialogParams`,
17618
+ * at the same level as `dtmf` in {@link sendDigits} — and the inner verto.info
17619
+ * is matched to this call's channel by `dialogParams.callID`, the in-dialog
17620
+ * convention for member-scoped frames. Because it is delivered on the dialog
17621
+ * itself, control lands on the call's own channel with no {node_id,call_id,member_id}
17622
+ * "self" tuple to get wrong. The outer webrtc.verto envelope (added by executeVerto)
17623
+ * still carries the own-leg callID + node_id for session routing.
17624
+ *
17625
+ * Keep `command` OUT of `dialogParams`: it is read at the params level, and
17626
+ * filterVertoParams rewrites/filters dialogParams keys but passes params-level
17627
+ * keys through verbatim.
17628
+ */
17629
+ async sendCallControl(method, params) {
17630
+ const response = await this.executeVerto(VertoInfo({
17631
+ dialogParams: { callID: this.webRtcCallSession.id },
17632
+ command: {
17633
+ method,
17634
+ params
17635
+ }
17636
+ }));
17637
+ const failure = findNestedVertoFailure(response);
17638
+ if (failure) throw new JSONRPCError(Number.parseInt(failure.code, 10) || 0, `Call control "${method}" failed (code ${failure.code})${failure.message ? `: ${failure.message}` : ""}`, void 0);
17639
+ return response;
17640
+ }
16999
17641
  async executeVerto(message, optionals = {}) {
17000
- const webrtcVertoMessage = WebrtcVerto({
17642
+ const params = {
17001
17643
  callID: optionals.callID ?? this.webRtcCallSession.id,
17002
17644
  node_id: optionals.node_id ?? this._nodeId$.value ?? "",
17003
17645
  message,
17004
17646
  subscribe: optionals.subscribe
17005
- });
17647
+ };
17648
+ const webrtcVertoMessage = WebrtcVerto(params);
17006
17649
  const response = await this.webRtcCallSession.execute(webrtcVertoMessage);
17007
- if (response.error) {
17008
- const error = new JSONRPCError(response.error.code, response.error.message, response.error.data);
17009
- this.onError?.(error);
17010
- return response;
17011
- }
17650
+ const nonFatal = message.method === "verto.info" ? { fatal: false } : void 0;
17012
17651
  const innerResult = getValueFrom(response, "result.result");
17013
- if (innerResult?.error) {
17014
- const error = new JSONRPCError(innerResult.error.code, innerResult.error.message, innerResult.error.data);
17015
- this.onError?.(error);
17016
- return response;
17652
+ const failure = response.error ?? innerResult?.error;
17653
+ if (failure) {
17654
+ const error = new JSONRPCError(failure.code, failure.message, failure.data);
17655
+ if (message.method === "verto.invite" || message.method === "verto.answer") throw error;
17656
+ this.reportLegError(error, this._rtcPeerConnectionsMap.get(params.callID), nonFatal);
17017
17657
  }
17018
17658
  return response;
17019
17659
  }
@@ -17032,8 +17672,9 @@ var WebRTCVertoManager = class extends VertoManager {
17032
17672
  default:
17033
17673
  }
17034
17674
  } catch (error) {
17675
+ if (vertoMethod === "verto.answer") throw error;
17035
17676
  logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
17036
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17677
+ this.reportLegError(toError(error), rtcPeerConnController);
17037
17678
  if (vertoMethod === "verto.modify") this.onModifyFailed?.();
17038
17679
  }
17039
17680
  }
@@ -17049,7 +17690,7 @@ var WebRTCVertoManager = class extends VertoManager {
17049
17690
  } catch (error) {
17050
17691
  logger$16.warn("[WebRTCManager] Error processing modify response:", error);
17051
17692
  const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
17052
- this.onError?.(modifyError);
17693
+ this.reportLegError(modifyError, rtcPeerConnController);
17053
17694
  }
17054
17695
  }
17055
17696
  }
@@ -17062,37 +17703,35 @@ var WebRTCVertoManager = class extends VertoManager {
17062
17703
  status,
17063
17704
  signalingError
17064
17705
  });
17065
- this.onError?.(signalingError);
17706
+ this.reportLegError(signalingError, null, { fatal: false });
17066
17707
  return;
17067
17708
  }
17068
17709
  if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
17069
17710
  }
17070
17711
  processInviteResponse(response, rtcPeerConnController) {
17071
- if (!response.error && getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
17712
+ if (getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
17072
17713
  this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
17073
- this._nodeId$.next(getValueFrom(response, "result.node_id") ?? null);
17714
+ const nodeId = getValueFrom(response, "result.node_id") ?? null;
17074
17715
  const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
17075
- const callId = getValueFrom(response, "result.result.result.callID") ?? null;
17716
+ const callId = getValueFrom(response, "result.result.result.callID");
17076
17717
  logger$16.debug("[WebRTCManager] Verto invite response:", {
17077
17718
  callId,
17078
17719
  memberId,
17079
17720
  response
17080
17721
  });
17081
- this._selfId$.next(memberId);
17082
17722
  rtcPeerConnController.setMemberId(memberId);
17083
- if (callId) {
17084
- this.webRtcCallSession.addCallId(callId);
17085
- this.attachManager.attach(this.buildAttachableCall(callId));
17086
- } else logger$16.warn("[WebRTCManager] Cannot attach call, missing callId:", {
17087
- nodeId: this.nodeId,
17088
- callId
17089
- });
17723
+ rtcPeerConnController.setNodeId(nodeId);
17724
+ if (rtcPeerConnController.isMainDevice) {
17725
+ this._selfId$.next(memberId);
17726
+ this._nodeId$.next(nodeId);
17727
+ this.attachManager.attach(this.buildAttachableCall(callId, nodeId ?? void 0));
17728
+ }
17729
+ if (callId) this.webRtcCallSession.addCallId(callId);
17090
17730
  logger$16.info("[WebRTCManager] Verto invite successful");
17091
17731
  logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
17092
17732
  } else {
17093
17733
  logger$16.error("[WebRTCManager] Verto invite failed:", response);
17094
- const inviteError = response.error ? new JSONRPCError(response.error.code, response.error.message, response.error.data) : /* @__PURE__ */ new Error("Verto invite failed: unexpected response");
17095
- this.onError?.(inviteError);
17734
+ this.reportLegError(/* @__PURE__ */ new Error("Verto invite failed: unexpected response"), rtcPeerConnController);
17096
17735
  }
17097
17736
  }
17098
17737
  get RTCPeerConnectionConfig() {
@@ -17131,7 +17770,7 @@ var WebRTCVertoManager = class extends VertoManager {
17131
17770
  this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
17132
17771
  this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
17133
17772
  this.subscribeTo(rtcPeerConnController.errors$, (error) => {
17134
- this.onError?.(error);
17773
+ this.reportLegError(error, rtcPeerConnController);
17135
17774
  });
17136
17775
  if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
17137
17776
  }
@@ -17160,7 +17799,7 @@ var WebRTCVertoManager = class extends VertoManager {
17160
17799
  await rtcPeerConnController.acceptInbound(answerOptions);
17161
17800
  } catch (error) {
17162
17801
  logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
17163
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17802
+ this.reportLegError(toError(error), rtcPeerConnController);
17164
17803
  }
17165
17804
  }
17166
17805
  }
@@ -17232,7 +17871,7 @@ var WebRTCVertoManager = class extends VertoManager {
17232
17871
  isInvite: isVertoInviteMessage(vertoMessage),
17233
17872
  reattach: this.webRtcCallSession.options.reattach === true,
17234
17873
  explicitNodeId: this.webRtcCallSession.options.nodeId,
17235
- currentNodeId: this._nodeId$.value
17874
+ currentNodeId: rtcPeerConnController.nodeId ?? this._nodeId$.value
17236
17875
  }),
17237
17876
  subscribe
17238
17877
  };
@@ -17264,7 +17903,7 @@ var WebRTCVertoManager = class extends VertoManager {
17264
17903
  await this.attachManager.attach(this.buildAttachableCall());
17265
17904
  } catch (error) {
17266
17905
  logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
17267
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17906
+ this.reportLegError(toError(error), rtcPeerConnectionController);
17268
17907
  await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
17269
17908
  }
17270
17909
  }
@@ -17336,15 +17975,19 @@ var WebRTCVertoManager = class extends VertoManager {
17336
17975
  await this.mainPeerConnection.restoreTrackSender(deviceKind);
17337
17976
  } else {
17338
17977
  const error = new InvalidParams("No valid device to be added");
17339
- this.onError?.(error);
17978
+ this.reportLegError(error, this.mainPeerConnection);
17340
17979
  throw error;
17341
17980
  }
17342
17981
  }
17343
- async addScreenMedia(options = { audio: false }) {
17344
- await this.initAdditionalPeerConnection("screenshare", options);
17982
+ async addScreenMedia(options = {}) {
17983
+ await this.initAdditionalPeerConnection("screenshare", {
17984
+ audio: false,
17985
+ screenShareAudio: options.audio ?? false
17986
+ });
17345
17987
  }
17346
17988
  async initAdditionalPeerConnection(propose, options) {
17347
17989
  const isScreenShare = propose === "screenshare";
17990
+ if (isScreenShare && this._screenShareId && this._rtcPeerConnectionsMap.has(this._screenShareId)) throw new ScreenShareAlreadyActiveError(this._screenShareId);
17348
17991
  let firstPeerConnectionError;
17349
17992
  let rtcPeerConnController = null;
17350
17993
  try {
@@ -17361,21 +18004,36 @@ var WebRTCVertoManager = class extends VertoManager {
17361
18004
  this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
17362
18005
  this.subscribeTo(rtcPeerConnController.errors$, (error) => {
17363
18006
  firstPeerConnectionError ??= error;
17364
- this.onError?.(error, { fatal: false });
18007
+ this.reportLegError(error, rtcPeerConnController);
17365
18008
  });
17366
- 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$)));
18009
+ const pc = rtcPeerConnController;
18010
+ await (0, import_cjs$15.firstValueFrom)((0, import_cjs$15.merge)(pc.localMediaSettled$.pipe((0, import_cjs$15.take)(1), (0, import_cjs$15.switchMap)(() => pc.connectionState$.pipe((0, import_cjs$15.filter)((state) => state === "connected"), (0, import_cjs$15.take)(1), (0, import_cjs$15.timeout)(DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS)))), this.legError$(pc.id)).pipe((0, import_cjs$15.takeUntil)((0, import_cjs$15.merge)(this.destroyed$, pc.destroyed$))));
17367
18011
  if (isScreenShare) this._screenShareStatus$.next("started");
17368
18012
  logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
17369
18013
  return rtcPeerConnController.id;
17370
18014
  } catch (error) {
17371
- logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
17372
- if (rtcPeerConnController) rtcPeerConnController.destroy();
17373
- if (isScreenShare) this._screenShareStatus$.next("none");
18015
+ const cancelled = error instanceof AuxiliaryLegCancelledError;
18016
+ const aborted = error instanceof import_cjs$15.EmptyError && !firstPeerConnectionError;
18017
+ if (!cancelled && !aborted) logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
18018
+ if (rtcPeerConnController && this._rtcPeerConnectionsMap.has(rtcPeerConnController.id)) {
18019
+ rtcPeerConnController.destroy();
18020
+ this._rtcPeerConnectionsMap.delete(rtcPeerConnController.id);
18021
+ this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
18022
+ }
18023
+ if (isScreenShare) {
18024
+ this._screenShareId = void 0;
18025
+ this._screenShareStatus$.next("none");
18026
+ }
18027
+ if (cancelled) {
18028
+ logger$16.debug("[WebRTCManager] Additional peer connection removed before connecting.");
18029
+ throw error;
18030
+ }
17374
18031
  if (firstPeerConnectionError) throw firstPeerConnectionError instanceof MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
17375
18032
  if (error instanceof import_cjs$15.EmptyError) {
17376
18033
  logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
17377
18034
  return;
17378
18035
  }
18036
+ if (error instanceof import_cjs$15.TimeoutError) throw new AuxiliaryLegTimeoutError(propose, error);
17379
18037
  throw error instanceof Error ? error : new Error(String(error), { cause: error });
17380
18038
  }
17381
18039
  }
@@ -17392,7 +18050,10 @@ var WebRTCVertoManager = class extends VertoManager {
17392
18050
  if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
17393
18051
  }
17394
18052
  async removeScreenMedia() {
17395
- if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$16.warn("[WebRTCManager] No active screen share to stop.");
18053
+ if (!["starting", "started"].includes(this._screenShareStatus$.value)) {
18054
+ logger$16.warn("[WebRTCManager] No active screen share to stop.");
18055
+ return;
18056
+ }
17396
18057
  if (!this._screenShareId) {
17397
18058
  logger$16.debug("[WebRTCManager] No screen share peer connection found.");
17398
18059
  return;
@@ -17407,6 +18068,10 @@ var WebRTCVertoManager = class extends VertoManager {
17407
18068
  try {
17408
18069
  if (rtcPeerConnController) await this.executeVertoBye(rtcPeerConnController);
17409
18070
  } finally {
18071
+ if (rtcPeerConnController && rtcPeerConnController.connectionState !== "connected") this._legErrors$.next({
18072
+ legId: id,
18073
+ error: new AuxiliaryLegCancelledError(rtcPeerConnController.propose)
18074
+ });
17410
18075
  rtcPeerConnController?.destroy();
17411
18076
  this._rtcPeerConnectionsMap.delete(id);
17412
18077
  this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
@@ -17421,7 +18086,10 @@ var WebRTCVertoManager = class extends VertoManager {
17421
18086
  await this.executeVerto(VertoBye({
17422
18087
  ...causeParams,
17423
18088
  dialogParams: this.dialogParams(rtcPeerConnController)
17424
- }));
18089
+ }), {
18090
+ callID: rtcPeerConnController.id,
18091
+ node_id: rtcPeerConnController.nodeId ?? void 0
18092
+ });
17425
18093
  } catch (error) {
17426
18094
  logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
17427
18095
  throw error;
@@ -18300,17 +18968,38 @@ function mosToQualityLevel(mos) {
18300
18968
  return "critical";
18301
18969
  }
18302
18970
 
18971
+ //#endregion
18972
+ //#region src/utils/unwrapVertoReply.ts
18973
+ /**
18974
+ * Unwrap a method's own reply from a `webrtc.verto` envelope.
18975
+ *
18976
+ * A control verb sent in-dialog comes back nested two levels deep:
18977
+ *
18978
+ * ```
18979
+ * { result: { node_id, code, result: { jsonrpc, id, result: <method payload> } } }
18980
+ * ```
18981
+ *
18982
+ * whereas the routed transport resolves the method payload directly under
18983
+ * `.result`. Readers want the latter shape, and the difference is silent when it
18984
+ * is wrong — `response.result.layouts` simply evaluates to `undefined` against the
18985
+ * envelope, so the data arrives, nothing throws, and the caller sees an empty
18986
+ * value. That exact failure produced an empty layout dropdown with a successful
18987
+ * request behind it.
18988
+ *
18989
+ * Accepting both shapes here keeps every reader indifferent to which transport
18990
+ * produced the response. Anything that is not a nested envelope (a plain ack, or
18991
+ * an already-unwrapped reply) passes through untouched.
18992
+ */
18993
+ function unwrapVertoReply(response) {
18994
+ const inner = getValueFrom(response, "result.result");
18995
+ return inner && typeof inner === "object" && "result" in inner ? inner : response;
18996
+ }
18997
+
18303
18998
  //#endregion
18304
18999
  //#region src/core/entities/Call.ts
18305
19000
  var import_cjs$11 = require_cjs();
18306
19001
  const logger$12 = getLogger();
18307
19002
  /**
18308
- * Verto method for setting member layout positions. Its gateway DTO requires a
18309
- * `targets` array whose entries are `{ target, position }` (NOT bare targets),
18310
- * so {@link WebRTCCall.buildMethodParams} special-cases it. See issue #19400.
18311
- */
18312
- const POSITION_SET_METHOD = "call.member.position.set";
18313
- /**
18314
19003
  * Ratio between the critical and warning RTT spike multipliers.
18315
19004
  * Warning threshold = baseline * warningMultiplier (default 3x)
18316
19005
  * Critical threshold = baseline * warningMultiplier * RTT_CRITICAL_TO_WARNING_RATIO
@@ -18405,8 +19094,11 @@ var WebRTCCall = class extends Destroyable {
18405
19094
  emitError(callError) {
18406
19095
  if (this._status$.value === "destroyed" || this._status$.value === "failed") return;
18407
19096
  this._errors$.next(callError);
18408
- if (callError.fatal) {
19097
+ if (callError.fatal && this._status$.value !== "disconnecting") {
18409
19098
  this._status$.next("failed");
19099
+ this.vertoManager.bye().catch((error) => {
19100
+ logger$12.debug("[Call] fatal-teardown bye failed (signaling likely already dead):", error);
19101
+ });
18410
19102
  this.destroy();
18411
19103
  }
18412
19104
  }
@@ -18461,7 +19153,7 @@ var WebRTCCall = class extends Destroyable {
18461
19153
  /** Toggles the call lock state, preventing or allowing new participants from joining. */
18462
19154
  async toggleLock() {
18463
19155
  const method = this.locked ? "call.unlock" : "call.lock";
18464
- await this.executeMethod(this.selfId ?? "", method, {});
19156
+ await this.executeMethod(this.callSelf, method, {});
18465
19157
  }
18466
19158
  /**
18467
19159
  * Toggles the hold state of the call (pauses/resumes local media transmission).
@@ -18510,14 +19202,25 @@ var WebRTCCall = class extends Destroyable {
18510
19202
  *
18511
19203
  * Constructs call context (node_id, call_id, member_id) and sends the RPC request.
18512
19204
  *
18513
- * @param target - Target member ID string, or a {@link MemberTarget} object.
19205
+ * @param target - Target {@link MemberTarget} triple, or the local member's
19206
+ * ID string for self-operations (any other string is rejected — a bare
19207
+ * member id cannot carry the remote member's own call context).
18514
19208
  * @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
18515
19209
  * @param args - Parameters for the RPC method.
18516
19210
  * @returns The RPC response.
19211
+ * @throws {CallNotReadyError} If the call has no self member context yet.
19212
+ * @throws {InvalidParams} If a string target is not the local member's ID.
18517
19213
  * @throws {JSONRPCError} If the RPC call returns an error.
18518
19214
  */
18519
19215
  async executeMethod(target, method, args) {
18520
- const params = this.buildMethodParams(target, args, method);
19216
+ const self = this.callSelf;
19217
+ 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}`);
19218
+ if (this.clientSession.callControl === "in-dialog") return this.executeMethodInDialog(target, method, args);
19219
+ const params = {
19220
+ ...args,
19221
+ self,
19222
+ target: typeof target === "string" ? self : target
19223
+ };
18521
19224
  const request = buildRPCRequest({
18522
19225
  method,
18523
19226
  params
@@ -18531,29 +19234,98 @@ var WebRTCCall = class extends Destroyable {
18531
19234
  throw error;
18532
19235
  }
18533
19236
  }
18534
- buildMethodParams(target, args, method) {
18535
- const self = {
18536
- node_id: this.nodeId ?? "",
18537
- call_id: this.id,
18538
- member_id: this.vertoManager.selfId ?? ""
18539
- };
18540
- if (method === POSITION_SET_METHOD) return {
18541
- ...args,
18542
- self
18543
- };
18544
- if (typeof target === "object") return {
18545
- ...args,
18546
- self,
18547
- targets: [target]
18548
- };
18549
- return {
18550
- ...args,
18551
- self,
18552
- target: {
18553
- node_id: this.nodeId ?? "",
19237
+ /**
19238
+ * `executeMethod` for a call opened with `callControl: 'in-dialog'`.
19239
+ *
19240
+ * Translates the routed transport's calling convention into the in-dialog one. No
19241
+ * `self` tuple is sent, but a `target` is — the same {call_id, member_id} the routed
19242
+ * transport puts in `target` (minus node_id), for self-ops and cross-member ops alike.
19243
+ *
19244
+ * Target shapes are per-verb and irregular, so they are centralised here rather
19245
+ * than left to callers: most verbs take a singular `target`, `call.member.remove`
19246
+ * takes a plural `targets` array, and `call.member.position.set` takes a flat
19247
+ * `targets` of `{call_id, position}` — the one verb keyed on call_id rather than
19248
+ * member_id, so the member triple `Participant.setPosition` built is unwrapped.
19249
+ */
19250
+ async executeMethodInDialog(target, method, args) {
19251
+ const control = { ...args };
19252
+ if (method === "call.member.position.set") control.targets = (args.targets ?? []).map((entry) => ({
19253
+ call_id: entry.target?.call_id ?? entry.call_id,
19254
+ position: entry.position
19255
+ }));
19256
+ else {
19257
+ const member = typeof target === "object" ? {
19258
+ call_id: target.call_id,
19259
+ member_id: target.member_id
19260
+ } : {
18554
19261
  call_id: this.id,
18555
19262
  member_id: target
18556
- }
19263
+ };
19264
+ if (method === "call.member.remove") control.targets = [member];
19265
+ else control.target = member;
19266
+ }
19267
+ return this.sendCommand(method, control);
19268
+ }
19269
+ /**
19270
+ * Sends a `call.*` control verb **in-dialog** via `verto.info`, as an alternative
19271
+ * to the routed {@link executeMethod} transport.
19272
+ *
19273
+ * Why both exist: `executeMethod` addresses the member with an explicit
19274
+ * `{node_id, call_id, member_id}` tuple, which does not resolve for every conference,
19275
+ * so the op can fail. An in-dialog frame carries the verb on the member's own
19276
+ * signaling channel instead, so control works without the client needing to know how
19277
+ * the conference is hosted.
19278
+ *
19279
+ * The trade-off is reach: the in-dialog transport is only accepted for calls that
19280
+ * join a conference over SWML (e.g. an SWML `join_conference`); use the routed
19281
+ * default otherwise.
19282
+ *
19283
+ * `params` are sent verbatim — nothing is built for you, which includes the target.
19284
+ * **A self-directed op still needs one**, or it is refused; name yourself explicitly:
19285
+ *
19286
+ * ```ts
19287
+ * const { call_id, member_id } = call.self.target;
19288
+ * await call.sendCommand('call.mute', { channels: ['audio'], target: { call_id, member_id } });
19289
+ * ```
19290
+ *
19291
+ * Never include `node_id` — only the two ids. The shapes are per-verb: most take a
19292
+ * singular `target`, `call.member.remove` takes a plural `targets` array, and
19293
+ * `call.member.position.set` takes a flat `targets: [{call_id, position}]` (the one
19294
+ * verb keyed on `call_id` rather than `member_id`). Verbs that act on the call as a
19295
+ * whole, or that the SDK does not wrap at all, take no target.
19296
+ *
19297
+ * For the typed alternative that handles all of this, create the client with
19298
+ * `callControl: 'in-dialog'` and use the ordinary `Call`/`Participant` methods.
19299
+ *
19300
+ * @internal Not part of the supported surface while the in-dialog transport is still
19301
+ * rolling out. `WebRTCCall` is exported from the package entry, so without this tag
19302
+ * TypeDoc publishes the method — and the example above — as public API.
19303
+ *
19304
+ * @param method - A `call.*` method name (e.g. `'call.mute'`).
19305
+ * @param params - Method parameters, sent verbatim.
19306
+ * @returns The method's own reply, unwrapped from the `verto.info` envelope.
19307
+ * @throws {JSONRPCError} If the control op fails.
19308
+ */
19309
+ async sendCommand(method, params = {}) {
19310
+ return unwrapVertoReply(await this.vertoManager.sendCallControl(method, params));
19311
+ }
19312
+ /**
19313
+ * The local leg's member triple — sent as `self` in every member RPC
19314
+ * envelope, and as the `target` of call-scoped self-operations (e.g. lock,
19315
+ * layout).
19316
+ *
19317
+ * @throws {CallNotReadyError} Before `call.joined` delivers the self member
19318
+ * context (`selfId`/`nodeId`) — an RPC without it cannot be routed, so fail
19319
+ * fast instead of sending a doomed request.
19320
+ */
19321
+ get callSelf() {
19322
+ const node_id = this.nodeId;
19323
+ const member_id = this.vertoManager.selfId;
19324
+ if (!node_id || !member_id) throw new CallNotReadyError(this.id);
19325
+ return {
19326
+ node_id,
19327
+ call_id: this.id,
19328
+ member_id
18557
19329
  };
18558
19330
  }
18559
19331
  /** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
@@ -18709,13 +19481,13 @@ var WebRTCCall = class extends Destroyable {
18709
19481
  get bandwidthConstrained$() {
18710
19482
  return this.deferEmission(this._bandwidthConstrained$.asObservable());
18711
19483
  }
18712
- /** Observable that emits when server-pushed media params are applied. */
19484
+ /** Observable that emits when the server pushes media params. */
18713
19485
  get mediaParamsUpdated$() {
18714
19486
  return this.deferEmission(this._mediaParamsUpdated$.asObservable());
18715
19487
  }
18716
19488
  /**
18717
19489
  * @internal Emit a media params update event.
18718
- * Called by the VertoManager when server-pushed media params are applied.
19490
+ * Called by the VertoManager when the server pushes media params.
18719
19491
  */
18720
19492
  emitMediaParamsUpdated(event) {
18721
19493
  this._mediaParamsUpdated$.next(event);
@@ -18923,6 +19695,10 @@ var WebRTCCall = class extends Destroyable {
18923
19695
  get selfId$() {
18924
19696
  return this.vertoManager.selfId$;
18925
19697
  }
19698
+ /** @internal Lets call creation bound the media and signalling phases apart. */
19699
+ get localMediaSettled$() {
19700
+ return this.vertoManager.localMediaSettled$;
19701
+ }
18926
19702
  /** Local participant's member ID, or `null` if not joined. */
18927
19703
  get selfId() {
18928
19704
  return this.vertoManager.selfId;
@@ -19116,12 +19892,17 @@ var WebRTCCall = class extends Destroyable {
19116
19892
  *
19117
19893
  * **These operations are NOT atomic.** The layout is applied first, then each
19118
19894
  * member position sequentially, so members may briefly flash into their
19119
- * default slots before being moved to the requested positions.
19895
+ * default slots before being moved to the requested positions. Targeted
19896
+ * members are validated upfront, though: when any of them has no
19897
+ * {@link Participant.target | member call context} yet, the whole call
19898
+ * rejects before any request is sent and the layout is left unchanged.
19120
19899
  *
19121
19900
  * @param layout - Layout name (must be one of {@link layouts}).
19122
19901
  * @param positions - Optional map of member IDs to {@link VideoPosition} values.
19123
19902
  * When omitted or empty, only the layout is changed.
19124
19903
  * @throws {InvalidParams} If the layout is not in the available {@link layouts}.
19904
+ * @throws {ParticipantNotReadyError} If a targeted member's call context has
19905
+ * not been received yet — thrown before any request is sent.
19125
19906
  *
19126
19907
  * @example
19127
19908
  * ```ts
@@ -19132,18 +19913,19 @@ var WebRTCCall = class extends Destroyable {
19132
19913
  */
19133
19914
  async setLayout(layout, positions) {
19134
19915
  if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
19135
- const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
19136
- await this.executeMethod(selfId, "call.layout.set", { layout });
19137
- const positionEntries = Object.entries(positions ?? {});
19138
- if (positionEntries.length === 0) return;
19139
- for (const [memberId, position] of positionEntries) {
19916
+ const targets = [];
19917
+ for (const [memberId, position] of Object.entries(positions ?? {})) {
19140
19918
  const participant = this.participants.find((p) => p.id === memberId);
19141
19919
  if (!participant) {
19142
19920
  logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
19143
19921
  continue;
19144
19922
  }
19145
- await participant.setPosition(position);
19923
+ participant.target;
19924
+ targets.push([participant, position]);
19146
19925
  }
19926
+ const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
19927
+ await this.executeMethod(selfId, "call.layout.set", { layout });
19928
+ for (const [participant, position] of targets) await participant.setPosition(position);
19147
19929
  }
19148
19930
  /**
19149
19931
  * Transfers the call to another destination.
@@ -19245,17 +20027,28 @@ var WebRTCCall = class extends Destroyable {
19245
20027
  * (notably iOS Safari) fall back to re-acquiring the track with the new
19246
20028
  * constraint set and plumbing the replacement through the local audio
19247
20029
  * pipeline if one is active.
20030
+ *
20031
+ * @returns whether the constraint reached the microphone. `false` is an
20032
+ * outcome rather than an error — a leg sending media the SDK did not capture
20033
+ * is left alone — so a UI that reflects the toggle must read it. Any failure
20034
+ * behind a `false` is also reported on {@link errors$}.
19248
20035
  */
19249
20036
  async setEchoCancellation(enabled) {
19250
- await this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
20037
+ return this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
19251
20038
  }
19252
- /** Toggle browser noise suppression on the local mic at runtime. */
20039
+ /**
20040
+ * Toggle browser noise suppression on the local mic at runtime.
20041
+ * @returns whether the constraint reached the microphone.
20042
+ */
19253
20043
  async setNoiseSuppression(enabled) {
19254
- await this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
20044
+ return this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
19255
20045
  }
19256
- /** Toggle browser automatic gain control on the local mic at runtime. */
20046
+ /**
20047
+ * Toggle browser automatic gain control on the local mic at runtime.
20048
+ * @returns whether the constraint reached the microphone.
20049
+ */
19257
20050
  async setAutoGainControl(enabled) {
19258
- await this.vertoManager.updateMediaConstraints({ audio: { autoGainControl: enabled } });
20051
+ return this.vertoManager.updateMediaConstraints({ audio: { autoGainControl: enabled } });
19259
20052
  }
19260
20053
  /**
19261
20054
  * Observable of the aggregate remote audio level, 0..1 RMS. The server
@@ -19312,6 +20105,9 @@ var WebRTCCall = class extends Destroyable {
19312
20105
  /**
19313
20106
  * Infers the semantic error category from a raw Error thrown by VertoManager
19314
20107
  * or an RTCPeerConnection layer.
20108
+ *
20109
+ * Pure function — exported for unit testing.
20110
+ * @internal
19315
20111
  */
19316
20112
  function inferCallErrorKind(error) {
19317
20113
  if (error instanceof RPCTimeoutError) return "timeout";
@@ -19330,7 +20126,14 @@ const RECOVERABLE_RPC_CODES = new Set([
19330
20126
  RPC_ERROR_AUTHENTICATION_FAILED,
19331
20127
  RPC_ERROR_INVALID_PARAMS
19332
20128
  ]);
19333
- /** Determines whether an error should be fatal (destroy the call). */
20129
+ /**
20130
+ * A *fallback*: callers knowing which leg failed pass an explicit `fatal` and
20131
+ * never reach here, so the default-fatal branch only sees call- and main-leg
20132
+ * errors. Auxiliary legs go through `WebRTCVertoManager.reportLegError`.
20133
+ *
20134
+ * Pure function — exported for unit testing.
20135
+ * @internal
20136
+ */
19334
20137
  function isFatalError(error) {
19335
20138
  if (error instanceof VertoPongError) return false;
19336
20139
  if (error instanceof MediaTrackError) return false;
@@ -19365,7 +20168,9 @@ var CallFactory = class {
19365
20168
  kind: inferCallErrorKind(error),
19366
20169
  fatal: options$1?.fatal ?? isFatalError(error),
19367
20170
  error,
19368
- callId: callInstance.id
20171
+ callId: callInstance.id,
20172
+ ...options$1?.leg ? { leg: options$1.leg } : {},
20173
+ ...options$1?.legId ? { legId: options$1.legId } : {}
19369
20174
  };
19370
20175
  callInstance.emitError(callError);
19371
20176
  },
@@ -19837,6 +20642,30 @@ const logger$9 = getLogger();
19837
20642
  function shouldAbortDial(callError) {
19838
20643
  return callError.fatal || !(callError.error instanceof MediaAccessError);
19839
20644
  }
20645
+ /**
20646
+ * Wait for a dialed call to be ready, or for the failure that stops it.
20647
+ *
20648
+ * Local media acquisition is deliberately unbounded: a permission prompt or a
20649
+ * device picker is human time, and `getUserMedia` cannot be cancelled anyway.
20650
+ * The clock starts only once acquisition settles, so a slow human never spends
20651
+ * the server's budget.
20652
+ *
20653
+ * `merge` rather than `race`, because the two legs settle asymmetrically. A
20654
+ * fatal acquisition failure reports the error and then destroys the call in the
20655
+ * same synchronous step; `errors$` defers delivery by a microtask while
20656
+ * `localMediaSettled$` completes immediately. Under `race` that bare completion
20657
+ * ended the wait first and `dial()` rejected with an RxJS `EmptyError`, burying
20658
+ * the `NotAllowedError` applications are told to inspect. Under `merge` the
20659
+ * completed leg is simply spent, and the queued error — enqueued before the
20660
+ * completion, so delivered before it — arrives to reject the wait. A dial
20661
+ * abandoned with no error at all still ends both legs, and the resulting
20662
+ * `EmptyError` remains the benign-cancel signal.
20663
+ *
20664
+ * Exported for unit testing.
20665
+ */
20666
+ async function awaitDialReady(session, signalingTimeoutMs) {
20667
+ return (0, import_cjs$7.firstValueFrom)((0, import_cjs$7.merge)(session.localMediaSettled$.pipe((0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)(() => session.selfId$.pipe((0, import_cjs$7.filter)((id) => Boolean(id)), (0, import_cjs$7.take)(1), (0, import_cjs$7.timeout)(signalingTimeoutMs)))), session.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)))));
20668
+ }
19840
20669
  const getAddressSearchURI = (options) => {
19841
20670
  const to = options.to?.split("?")[0];
19842
20671
  const from$9 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
@@ -19853,7 +20682,6 @@ var ClientSessionManager = class extends Destroyable {
19853
20682
  this.authorizationStateKey = authorizationStateKey;
19854
20683
  this.attachManager = attachManager;
19855
20684
  this.dpopManager = dpopManager;
19856
- this.callCreateTimeout = 6e3;
19857
20685
  this.agent = `signalwire-js/4.0.0`;
19858
20686
  this.eventAcks = true;
19859
20687
  this.authorizationState$ = this.createReplaySubject(1);
@@ -19862,6 +20690,7 @@ var ClientSessionManager = class extends Destroyable {
19862
20690
  minor: 0,
19863
20691
  revision: 0
19864
20692
  };
20693
+ this.callControl = "routed";
19865
20694
  this._authorization$ = this.createBehaviorSubject(void 0);
19866
20695
  this._errors$ = this.createReplaySubject(1);
19867
20696
  this._authState$ = this.createBehaviorSubject({ kind: "unauthenticated" });
@@ -20061,22 +20890,23 @@ var ClientSessionManager = class extends Destroyable {
20061
20890
  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 })));
20062
20891
  }
20063
20892
  async handleAuthenticationError(error) {
20893
+ if (error instanceof TransportConnectionError) {
20894
+ logger$9.debug("[Session] Socket closed during authentication, waiting for reconnect");
20895
+ return;
20896
+ }
20064
20897
  logger$9.error("Authentication error:", error);
20065
- const isRecoverableAuthError = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
20898
+ 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);
20066
20899
  const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
20067
- if (isRecoverableAuthError && hasStoredState) {
20900
+ if (isRecoverableAuthError$1 && hasStoredState) {
20068
20901
  logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
20069
- try {
20070
- await this.cleanupStoredConnectionParams();
20071
- } catch (cleanupError) {
20072
- logger$9.error("Failed to cleanup stored connection params:", cleanupError);
20073
- } finally {
20074
- this.transport.reconnect();
20075
- }
20902
+ await this.discardResumeStateAndReconnect();
20076
20903
  } else this._errors$.next(error);
20077
20904
  }
20078
20905
  /**
20079
- * Clear the resume state (authorization_state + protocol) only.
20906
+ * Clear the resume state (authorization_state + protocol) and ask the
20907
+ * transport to reconnect. The `connected` event re-triggers
20908
+ * `authenticate()`, which now has no stored state and so performs a fresh
20909
+ * connect.
20080
20910
  *
20081
20911
  * This is the stale-auth-state recovery helper used by handleAuthError:
20082
20912
  * the server rejected a reconnect, so the resume state is discarded and a
@@ -20084,9 +20914,24 @@ var ClientSessionManager = class extends Destroyable {
20084
20914
  * session lives on through the reconnect and reattachCalls() needs the
20085
20915
  * stored call references afterwards. Do NOT add detachAll() here.
20086
20916
  *
20917
+ * Connect-time recovery only. A *request* refused on an already
20918
+ * authenticated session is never healed here: dropping the resume state
20919
+ * destroys the association between the socket and the previous session,
20920
+ * which is what reattach depends on. That path mints a fresh credential and
20921
+ * reauthenticates instead (see `SignalWire.recoverAndRetry`).
20922
+ *
20087
20923
  * For public teardown (disconnect/destroy), use {@link teardownSessionState}
20088
20924
  * instead, which clears the attach records as well.
20089
20925
  */
20926
+ async discardResumeStateAndReconnect() {
20927
+ try {
20928
+ await this.cleanupStoredConnectionParams();
20929
+ } catch (cleanupError) {
20930
+ logger$9.error("Failed to cleanup stored connection params:", cleanupError);
20931
+ } finally {
20932
+ this.transport.reconnect();
20933
+ }
20934
+ }
20090
20935
  async cleanupStoredConnectionParams() {
20091
20936
  await this.transport.setProtocol(void 0);
20092
20937
  await this.updateAuthorizationStateInStorage(void 0);
@@ -20160,11 +21005,15 @@ var ClientSessionManager = class extends Destroyable {
20160
21005
  const isReconnect = hasReconnectState && storedToken;
20161
21006
  let dpopToken;
20162
21007
  if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
20163
- else if (this.onBeforeReconnect && this.clientBound) {
20164
- logger$9.debug("[Session] Refreshing credentials before fresh connect");
20165
- await this.onBeforeReconnect();
21008
+ else {
21009
+ const credential = this.getCredential();
21010
+ const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + CREDENTIAL_EXPIRY_SKEW_MS;
21011
+ if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
21012
+ logger$9.debug("[Session] Refreshing credentials before fresh connect");
21013
+ await this.onBeforeReconnect();
21014
+ }
20166
21015
  }
20167
- if ((!isReconnect || this.clientBound) && this.dpopManager?.initialized) try {
21016
+ if (this.dpopManager?.initialized) try {
20168
21017
  dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
20169
21018
  } catch (error) {
20170
21019
  if (this.clientBound) throw error;
@@ -20184,7 +21033,9 @@ var ClientSessionManager = class extends Destroyable {
20184
21033
  protocol: persistedParams.protocol
20185
21034
  } : {}
20186
21035
  });
20187
- 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)(() => {
21036
+ if (this.transport.connectionStatus !== "connected") throw new TransportConnectionError("WebSocket closed during authentication");
21037
+ const socketClosed$ = this.transport.connectionStatus$.pipe((0, import_cjs$7.skip)(1), (0, import_cjs$7.filter)((status) => status === "reconnecting" || status === "disconnected"), (0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)(() => (0, import_cjs$7.throwError)(() => new TransportConnectionError("WebSocket closed during authentication"))));
21038
+ const response = await (0, import_cjs$7.lastValueFrom)((0, import_cjs$7.race)((0, import_cjs$7.from)(this.transport.execute(rpcConnectRequest)), socketClosed$).pipe(throwOnRPCError(), (0, import_cjs$7.map)((res) => res.result), (0, import_cjs$7.filter)(isRPCConnectResult), (0, import_cjs$7.tap)(() => {
20188
21039
  logger$9.debug("[Session] Response passed filter, processing authentication result");
20189
21040
  }), (0, import_cjs$7.take)(1), (0, import_cjs$7.catchError)((err) => {
20190
21041
  logger$9.error("[Session] Authentication RPC failed:", err);
@@ -20197,7 +21048,9 @@ var ClientSessionManager = class extends Destroyable {
20197
21048
  });
20198
21049
  if (response.protocol) await this.transport.setProtocol(response.protocol);
20199
21050
  this._authorization$.next(response.authorization);
21051
+ if (response.authorization.cnf?.jkt) this._wasClientBound = true;
20200
21052
  this._iceServers$.next(response.ice_servers ?? []);
21053
+ this.transport.setAuthenticated();
20201
21054
  this._authState$.next({ kind: "authenticated" });
20202
21055
  logger$9.debug("[Session] Authentication completed successfully");
20203
21056
  }
@@ -20263,7 +21116,7 @@ var ClientSessionManager = class extends Destroyable {
20263
21116
  to: destinationURI,
20264
21117
  ...options
20265
21118
  });
20266
- 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)))));
21119
+ await awaitDialReady(callSession, DEFAULT_CALL_SIGNALING_TIMEOUT_MS);
20267
21120
  this._calls$.next({
20268
21121
  [`${callSession.id}`]: callSession,
20269
21122
  ...this._calls$.value
@@ -20316,12 +21169,23 @@ var ClientSessionWrapper = class {
20316
21169
  get authenticated() {
20317
21170
  return this.clientSessionManager.authenticated;
20318
21171
  }
21172
+ /**
21173
+ * Whether the session is using a Client Bound SAT (DPoP). Sticky — set
21174
+ * when the binding is established or restored from a resumed session's
21175
+ * server authorization.
21176
+ */
21177
+ get clientBound() {
21178
+ return this.clientSessionManager.clientBound;
21179
+ }
20319
21180
  get signalingEvent$() {
20320
21181
  return this.clientSessionManager.signalingEvent$;
20321
21182
  }
20322
21183
  get iceServers() {
20323
21184
  return this.clientSessionManager.iceServers;
20324
21185
  }
21186
+ get callControl() {
21187
+ return this.clientSessionManager.callControl;
21188
+ }
20325
21189
  async execute(request, options) {
20326
21190
  return this.clientSessionManager.execute(request, options);
20327
21191
  }
@@ -20518,7 +21382,7 @@ var DeviceTokenManager = class extends Destroyable {
20518
21382
  await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
20519
21383
  updateCredential({ token: tokenData.token });
20520
21384
  logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
20521
- this._currentToken$.next(tokenData);
21385
+ this.emitCurrentToken(tokenData);
20522
21386
  return { activated: true };
20523
21387
  } catch (error) {
20524
21388
  logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
@@ -20530,6 +21394,21 @@ var DeviceTokenManager = class extends Destroyable {
20530
21394
  }
20531
21395
  }
20532
21396
  /**
21397
+ * Emit a freshly received token to the reactive pipeline, stamping an
21398
+ * absolute `expires_at` when the response carried only `expires_in`.
21399
+ * Resolving the expiry at RECEIVE time (not at read time) is what lets
21400
+ * {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
21401
+ * re-resolved later would always compute a full TTL from "now" and never
21402
+ * cross the refresh buffer.
21403
+ */
21404
+ emitCurrentToken(token) {
21405
+ const stamped = token.expires_at ? token : {
21406
+ ...token,
21407
+ expires_at: resolveExpiresAt(token)
21408
+ };
21409
+ this._currentToken$.next(stamped);
21410
+ }
21411
+ /**
20533
21412
  * Returns true when the cached token has enough headroom before expiry to
20534
21413
  * be safely reused on reactivation. The headroom matches the refresh
20535
21414
  * buffer, so a token within the refresh window is treated as stale (the
@@ -20547,7 +21426,7 @@ var DeviceTokenManager = class extends Destroyable {
20547
21426
  method: "POST",
20548
21427
  uri: DEVICE_TOKEN_ENDPOINT
20549
21428
  });
20550
- const response = await this.http.request({
21429
+ const response = await this.http().request({
20551
21430
  url: DEVICE_TOKEN_ENDPOINT,
20552
21431
  ...POST_PARAMS,
20553
21432
  body: JSON.stringify({
@@ -20574,7 +21453,7 @@ var DeviceTokenManager = class extends Destroyable {
20574
21453
  uri: DEVICE_REFRESH_ENDPOINT,
20575
21454
  accessToken: currentToken
20576
21455
  });
20577
- const response = await this.http.request({
21456
+ const response = await this.http().request({
20578
21457
  url: DEVICE_REFRESH_ENDPOINT,
20579
21458
  ...POST_PARAMS,
20580
21459
  body: JSON.stringify({
@@ -20622,7 +21501,7 @@ var DeviceTokenManager = class extends Destroyable {
20622
21501
  const currentToken = this.getCredential().token;
20623
21502
  if (!currentToken) throw new TokenRefreshError("No current token available for refresh");
20624
21503
  const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
20625
- this._currentToken$.next(newTokenData);
21504
+ this.emitCurrentToken(newTokenData);
20626
21505
  } catch (error) {
20627
21506
  logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
20628
21507
  this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
@@ -20649,6 +21528,22 @@ var DeviceTokenManager = class extends Destroyable {
20649
21528
  throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
20650
21529
  }
20651
21530
  /**
21531
+ * Force an immediate refresh when the cached Client Bound SAT is already
21532
+ * past its refresh window. Called on resume from suspension where
21533
+ * background-tab throttling can delay the reactive timer past the buffer.
21534
+ * A no-op when no token is cached or it still has headroom; the normal
21535
+ * {@link executeRefresh} guards (paused / in-progress / unauthenticated)
21536
+ * still apply.
21537
+ */
21538
+ refreshNowIfDue() {
21539
+ const token = this._currentToken$.value;
21540
+ if (!token) return;
21541
+ if (resolveExpiresAt(token) * 1e3 - Date.now() <= DEVICE_TOKEN_REFRESH_BUFFER_MS) {
21542
+ logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
21543
+ this.executeRefresh();
21544
+ }
21545
+ }
21546
+ /**
20652
21547
  * Stops the reactive refresh pipeline from firing. Use when the underlying
20653
21548
  * session is being torn down (e.g., during {@link SignalWire.disconnect})
20654
21549
  * so a scheduled refresh cannot fire against a destroyed session.
@@ -20699,6 +21594,7 @@ var CredentialRefreshCoordinator = class extends Destroyable {
20699
21594
  this.deps = deps;
20700
21595
  this._activating = false;
20701
21596
  this._activationGeneration = 0;
21597
+ this._developerRefreshInProgress = false;
20702
21598
  if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
20703
21599
  }
20704
21600
  /** True when the Client Bound SAT path is available (DPoP initialized). */
@@ -20717,28 +21613,120 @@ var CredentialRefreshCoordinator = class extends Destroyable {
20717
21613
  * invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
20718
21614
  */
20719
21615
  scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
21616
+ this._activeProvider = provider;
20720
21617
  if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
20721
21618
  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);
20722
- this._developerTimerId = setTimeout(async () => {
21619
+ this._developerTimerId = setTimeout(() => {
21620
+ this._developerTimerId = void 0;
21621
+ this.executeDeveloperRefresh(provider, expiresAt, attempt);
21622
+ }, refreshInterval);
21623
+ }
21624
+ /**
21625
+ * Runs the developer-provided refresh once: mints a new credential, stores
21626
+ * and persists it, reauthenticates the live session (via the notifier), and
21627
+ * reschedules against the new expiry. On failure retries with backoff up to
21628
+ * {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
21629
+ *
21630
+ * Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
21631
+ * `_developerRefreshInProgress` guard prevents the two from overlapping.
21632
+ */
21633
+ async executeDeveloperRefresh(provider, expiresAt, attempt) {
21634
+ if (this._developerRefreshInProgress) {
21635
+ logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
21636
+ return;
21637
+ }
21638
+ this._developerRefreshInProgress = true;
21639
+ try {
21640
+ const newCredentials = await this.refreshCredential(provider);
21641
+ this.deps.store.write(newCredentials);
21642
+ this.deps.store.persist(newCredentials);
20723
21643
  try {
20724
- if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
20725
- const newCredentials = await provider.refresh();
20726
- this.deps.store.write(newCredentials);
20727
- this.deps.store.persist(newCredentials);
20728
- logger$6.info("[Coordinator] Credentials refreshed successfully.");
20729
- if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
20730
- } catch (error) {
20731
- const nextAttempt = attempt + 1;
20732
- logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
20733
- this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
20734
- if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
20735
- else {
20736
- logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
20737
- this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
20738
- this.deps.notifier.onRefreshExhausted();
20739
- }
21644
+ await this.deps.notifier.onCredentialRefreshed(newCredentials);
21645
+ } catch (reauthError) {
21646
+ logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
20740
21647
  }
20741
- }, refreshInterval);
21648
+ logger$6.info("[Coordinator] Credentials refreshed successfully.");
21649
+ if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
21650
+ } catch (error) {
21651
+ const nextAttempt = attempt + 1;
21652
+ logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
21653
+ this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
21654
+ if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
21655
+ else {
21656
+ logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
21657
+ this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
21658
+ this.deps.notifier.onRefreshExhausted();
21659
+ }
21660
+ } finally {
21661
+ this._developerRefreshInProgress = false;
21662
+ }
21663
+ }
21664
+ /**
21665
+ * Force an immediate refresh when the current credential is already past its
21666
+ * scheduled refresh window. Called on resume from suspension, where
21667
+ * background-tab timer throttling can delay the armed refresh well past
21668
+ * expiry, leaving the live session stale.
21669
+ *
21670
+ * Routes to whichever mechanism is armed: the developer timer if armed,
21671
+ * otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
21672
+ */
21673
+ forceRefreshIfDue() {
21674
+ if (this._developerTimerId !== void 0 && this._activeProvider) {
21675
+ const expiry = this.deps.store.read().expiry_at;
21676
+ if (expiry !== void 0 && Date.now() >= expiry - CREDENTIAL_REFRESH_BUFFER_MS) {
21677
+ logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
21678
+ clearTimeout(this._developerTimerId);
21679
+ this._developerTimerId = void 0;
21680
+ this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
21681
+ }
21682
+ return;
21683
+ }
21684
+ this._deviceTokenManager?.refreshNowIfDue();
21685
+ }
21686
+ /**
21687
+ * Sync the credential's expiry from the server-provided authorization (the
21688
+ * `signalwire.connect` result). SATs are opaque JWE, so
21689
+ * `fabric_subscriber.expires_at` is the authoritative expiry of the token
21690
+ * the session actually connected with — the provider-reported `expiry_at`
21691
+ * is only a hint (and may be wrong or absent). Corrects the stored
21692
+ * credential and re-arms the developer refresh timer against the real
21693
+ * deadline when the provider supports `refresh()`.
21694
+ */
21695
+ syncExpiryFromAuthorization(authorization, provider) {
21696
+ const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
21697
+ if (!expiresAtSec) return;
21698
+ const expiryAt = expiresAtSec * 1e3;
21699
+ const credential = this.deps.store.read();
21700
+ if (credential.expiry_at === expiryAt) return;
21701
+ logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
21702
+ const updated = {
21703
+ ...credential,
21704
+ expiry_at: expiryAt
21705
+ };
21706
+ this.deps.store.write(updated);
21707
+ this.deps.store.persist(updated);
21708
+ if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
21709
+ }
21710
+ /**
21711
+ * Invoke `provider.refresh()` deduped against any concurrent developer
21712
+ * refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
21713
+ * and the orchestrator's -32003 recovery / reconnect re-mint — share one
21714
+ * in-flight promise, so a provider backed by one-time-use rotating refresh
21715
+ * tokens is never invoked twice in parallel.
21716
+ *
21717
+ * The caller owns applying the returned credential (store write, session
21718
+ * reauth, rescheduling); this method only serializes the network call.
21719
+ */
21720
+ async refreshCredential(provider) {
21721
+ if (this._refreshInFlight) return this._refreshInFlight;
21722
+ if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
21723
+ const run = provider.refresh();
21724
+ this._refreshInFlight = run;
21725
+ const clear = () => {
21726
+ if (this._refreshInFlight === run) this._refreshInFlight = void 0;
21727
+ };
21728
+ run.then(clear, clear);
21729
+ return run;
20742
21730
  }
20743
21731
  /**
20744
21732
  * Cancels any scheduled developer-provided refresh. Idempotent.
@@ -21259,6 +22247,7 @@ var WebSocketController = class WebSocketController extends Destroyable {
21259
22247
  }
21260
22248
  handleClose(_event) {
21261
22249
  this.clearConnectionTimeout();
22250
+ this.messageQueue = [];
21262
22251
  if (this.shouldReconnect) {
21263
22252
  this._status$.next("reconnecting");
21264
22253
  this.scheduleReconnection();
@@ -21328,6 +22317,9 @@ var WebSocketController = class WebSocketController extends Destroyable {
21328
22317
  function isSignalwirePingRequest(value) {
21329
22318
  return isJSONRPCRequest(value) && value.method === "signalwire.ping";
21330
22319
  }
22320
+ function isSignalwireConnectRequest(value) {
22321
+ return isJSONRPCRequest(value) && value.method === "signalwire.connect";
22322
+ }
21331
22323
 
21332
22324
  //#endregion
21333
22325
  //#region src/managers/TransportManager.ts
@@ -21342,6 +22334,10 @@ var TransportManager = class extends Destroyable {
21342
22334
  this.protocol$ = this.createReplaySubject(1);
21343
22335
  this.isConnecting = false;
21344
22336
  this.isConnected = false;
22337
+ this.socketStatus = "disconnected";
22338
+ this.sessionAuthenticated = false;
22339
+ this.heldRequests = [];
22340
+ this.serverPingWatch = 0;
21345
22341
  this.ackEvent = () => {
21346
22342
  return (0, import_cjs$2.tap)((message) => {
21347
22343
  if (isSignalwireRequest(message)) try {
@@ -21357,6 +22353,7 @@ var TransportManager = class extends Destroyable {
21357
22353
  if (isSignalwirePingRequest(message)) {
21358
22354
  try {
21359
22355
  logger$2.debug("[Transport] Received ping, sending pong", { pingId: message.id });
22356
+ if (this.sessionAuthenticated) this.armServerPingWatchdog();
21360
22357
  this.send(RPCPingResponse(message.id));
21361
22358
  } catch (error) {
21362
22359
  logger$2.error("[Transport] Failed to send ping response:", error);
@@ -21394,6 +22391,14 @@ var TransportManager = class extends Destroyable {
21394
22391
  this.subscribeTo(this._webSocketConnections.errors$, (error) => {
21395
22392
  this.onError?.(error);
21396
22393
  });
22394
+ this.subscribeTo(this._webSocketConnections.status$, (status) => {
22395
+ this.socketStatus = status;
22396
+ if (status !== "connected") {
22397
+ this.sessionAuthenticated = false;
22398
+ this.clearServerPingWatchdog();
22399
+ }
22400
+ if (status === "disconnected") this.heldRequests = [];
22401
+ });
21397
22402
  this.initialized$ = (0, import_cjs$2.defer)(() => (0, import_cjs$2.from)(this._init())).pipe((0, import_cjs$2.shareReplay)(1), (0, import_cjs$2.takeUntil)(this.destroyed$));
21398
22403
  this._jsonRPCMessage$ = this._webSocketConnections.incomingMessages$.pipe((0, import_cjs$2.map)((event) => {
21399
22404
  try {
@@ -21422,6 +22427,9 @@ var TransportManager = class extends Destroyable {
21422
22427
  get connectionStatus$() {
21423
22428
  return this._webSocketConnections.status$;
21424
22429
  }
22430
+ get connectionStatus() {
22431
+ return this.socketStatus;
22432
+ }
21425
22433
  async connect() {
21426
22434
  if (this.isConnecting || this.isConnected) {
21427
22435
  logger$2.warn("[Transport] Already connecting or connected");
@@ -21466,16 +22474,45 @@ var TransportManager = class extends Destroyable {
21466
22474
  }
21467
22475
  async execute(request, options) {
21468
22476
  this.send(request);
21469
- return new PendingRPC(request, this._jsonRPCResponse$, options).promise;
22477
+ const pending = new PendingRPC(request, this._jsonRPCResponse$, options).promise;
22478
+ pending.catch(() => {
22479
+ this.heldRequests = this.heldRequests.filter((held) => held.id !== request.id);
22480
+ });
22481
+ return pending;
21470
22482
  }
21471
22483
  send(message) {
21472
22484
  const payload = JSON.stringify(message);
22485
+ if (isJSONRPCResponse(message)) {
22486
+ if (this.socketStatus !== "connected") {
22487
+ logger$2.debug("[Transport] Dropping response while disconnected", { id: message.id });
22488
+ return;
22489
+ }
22490
+ } else if (!this.sessionAuthenticated && !isSignalwireConnectRequest(message)) {
22491
+ this.heldRequests.push({
22492
+ id: message.id,
22493
+ payload
22494
+ });
22495
+ return;
22496
+ }
21473
22497
  this._outgoingMessages$.next(payload);
21474
22498
  }
22499
+ /**
22500
+ * Called by the session when signalwire.connect succeeds on the current
22501
+ * socket. Sends the requests held since the socket opened, in order.
22502
+ */
22503
+ setAuthenticated() {
22504
+ if (this.socketStatus !== "connected") return;
22505
+ this.sessionAuthenticated = true;
22506
+ this.armServerPingWatchdog();
22507
+ const held = this.heldRequests;
22508
+ this.heldRequests = [];
22509
+ held.forEach(({ payload }) => this._outgoingMessages$.next(payload));
22510
+ }
21475
22511
  disconnect() {
21476
22512
  logger$2.debug("[Transport] Disconnecting");
21477
22513
  this.isConnected = false;
21478
22514
  this.isConnecting = false;
22515
+ this.clearServerPingWatchdog();
21479
22516
  this._webSocketConnections.disconnect();
21480
22517
  }
21481
22518
  destroy() {
@@ -21484,6 +22521,28 @@ var TransportManager = class extends Destroyable {
21484
22521
  super.destroy();
21485
22522
  this._webSocketConnections.destroy();
21486
22523
  }
22524
+ armServerPingWatchdog() {
22525
+ this.clearServerPingWatchdog();
22526
+ const watch = this.serverPingWatch;
22527
+ this.serverPingTimer = setTimeout(() => void this.probeServer(watch), SERVER_PING_TIMEOUT_MS);
22528
+ }
22529
+ /** Bumps the watch so a probe still in flight no longer acts on its result. */
22530
+ clearServerPingWatchdog() {
22531
+ clearTimeout(this.serverPingTimer);
22532
+ this.serverPingTimer = void 0;
22533
+ this.serverPingWatch += 1;
22534
+ }
22535
+ async probeServer(watch) {
22536
+ logger$2.debug(`[Transport] No signalwire.ping for ${SERVER_PING_TIMEOUT_MS}ms, probing the server`);
22537
+ try {
22538
+ await this.execute(RPCPing(), { timeoutMs: SERVER_PING_PROBE_TIMEOUT_MS });
22539
+ if (watch === this.serverPingWatch) this.armServerPingWatchdog();
22540
+ } catch (error) {
22541
+ if (watch !== this.serverPingWatch) return;
22542
+ logger$2.warn("[Transport] Server did not answer the ping probe, replacing the socket", error);
22543
+ this.reconnect();
22544
+ }
22545
+ }
21487
22546
  async _loadProtocolFromStorage() {
21488
22547
  try {
21489
22548
  const storedProtocol = await this.storage.getItem(this.protocolKey);
@@ -21522,6 +22581,13 @@ var TransportManager = class extends Destroyable {
21522
22581
  //#region src/clients/SignalWire.ts
21523
22582
  var import_cjs$1 = require_cjs();
21524
22583
  const logger$1 = getLogger();
22584
+ /**
22585
+ * Storage key for the client-bound marker. The SAT and authorization_state are
22586
+ * both opaque to the SDK, so on a page reload the preflight recovery — which
22587
+ * runs before any session exists — has no other way to know the session was
22588
+ * client-bound. See {@link SignalWire.persistClientBoundMarker}.
22589
+ */
22590
+ const CLIENT_BOUND_STORAGE_KEY = "sw:client_bound";
21525
22591
  const buildOptionsFromDestination = (destination) => {
21526
22592
  if (typeof destination === "string") {
21527
22593
  const queryStartIndex = destination.indexOf("?");
@@ -21565,6 +22631,7 @@ var SignalWire = class extends Destroyable {
21565
22631
  this.preferences = new ClientPreferences();
21566
22632
  this._user$ = this.createBehaviorSubject(void 0);
21567
22633
  this._directory$ = this.createBehaviorSubject(void 0);
22634
+ this._credentialRecovered = false;
21568
22635
  this._isConnected$ = this.createBehaviorSubject(false);
21569
22636
  this._isRegistered$ = this.createBehaviorSubject(false);
21570
22637
  this._errors$ = this.createReplaySubject(1);
@@ -21603,6 +22670,16 @@ var SignalWire = class extends Destroyable {
21603
22670
  });
21604
22671
  }
21605
22672
  /**
22673
+ * Build the refresh path's own HTTP controller, against whatever host is current.
22674
+ *
22675
+ * Called on first use rather than up front, so `apiHost` already reflects the
22676
+ * token's `ch` claim. Same credential source as the container's controller — only
22677
+ * the instance, and therefore its observable streams, is separate.
22678
+ */
22679
+ createRefreshHttpController() {
22680
+ return new HTTPRequestController(this._deps.apiHost, () => this._deps.credential);
22681
+ }
22682
+ /**
21606
22683
  * Initializes DPoP if not already set up. Returns the fingerprint on success.
21607
22684
  */
21608
22685
  async initDPoP() {
@@ -21629,11 +22706,19 @@ var SignalWire = class extends Destroyable {
21629
22706
  async resolveCredentials() {
21630
22707
  const fingerprint = await this.initDPoP();
21631
22708
  this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
21632
- http: this._deps.http,
22709
+ http: () => {
22710
+ if (!this._refreshHttp || this._refreshHttpHost !== this._deps.apiHost) {
22711
+ this._refreshHttp?.destroy();
22712
+ this._refreshHttp = this.createRefreshHttpController();
22713
+ this._refreshHttpHost = this._deps.apiHost;
22714
+ }
22715
+ return this._refreshHttp;
22716
+ },
21633
22717
  notifier: {
21634
22718
  onError: (error) => this._errors$.next(error),
21635
22719
  onWarning: (warning) => this._warnings$.next(warning),
21636
- onRefreshExhausted: () => void this.disconnect()
22720
+ onRefreshExhausted: () => void this.disconnect(),
22721
+ onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
21637
22722
  },
21638
22723
  store: {
21639
22724
  read: () => this._deps.credential,
@@ -21645,6 +22730,7 @@ var SignalWire = class extends Destroyable {
21645
22730
  ...this._deps.credential,
21646
22731
  ...partial
21647
22732
  };
22733
+ this.persistCredential(this._deps.credential);
21648
22734
  },
21649
22735
  persist: (credential) => this.persistCredential(credential)
21650
22736
  }
@@ -21690,20 +22776,181 @@ var SignalWire = class extends Destroyable {
21690
22776
  }
21691
22777
  this._deps.credential = _credentials;
21692
22778
  this.persistCredential(_credentials);
21693
- if (this.isConnected && this._clientSession.authenticated && _credentials.token) try {
21694
- await this._clientSession.reauthenticate(_credentials.token);
22779
+ await this.reauthenticateLiveSession(_credentials);
22780
+ }
22781
+ /**
22782
+ * Reauthenticate the currently-open session with a freshly obtained
22783
+ * credential so the new token takes effect on the live socket immediately —
22784
+ * not just on the next reconnect. No-op when the session is not
22785
+ * connected/authenticated or the credential carries no token (e.g. an
22786
+ * authorization-state-only refresh). Non-fatal: reauth failures surface on
22787
+ * `errors$` without aborting the refresh that triggered this.
22788
+ */
22789
+ async reauthenticateLiveSession(credential) {
22790
+ if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
22791
+ try {
22792
+ await this._clientSession.reauthenticate(credential.token);
21695
22793
  logger$1.info("[SignalWire] Session refreshed with new credentials.");
21696
22794
  } catch (error) {
21697
22795
  logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
21698
22796
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21699
22797
  }
21700
22798
  }
22799
+ /**
22800
+ * Recover a session the server is refusing: mint a fresh credential,
22801
+ * reauthenticate the live session with it, and retry the operation.
22802
+ *
22803
+ * The connection is deliberately kept. A reload authenticates the new socket
22804
+ * against the persisted `authorization_state`, and that handshake is what
22805
+ * associates the socket with the previous session — the association reattach
22806
+ * depends on. `signalwire.reauthenticate` swaps the credential *on that same
22807
+ * session*, so recovery never touches the resume state. Discarding it would
22808
+ * heal the credential by destroying the very thing the caller is trying to
22809
+ * get back to.
22810
+ *
22811
+ * The operation is still the verdict, never the RPC. Reauthenticating with
22812
+ * the in-memory token is accepted by a resume even while requests stay
22813
+ * refused, because the persisted `authorization_state` short-circuits token
22814
+ * validation — and `signalwire.reauthenticate` with a *freshly minted* token
22815
+ * has also been observed accepted while `subscriber.online` keeps being
22816
+ * refused (staging run 33826974634). Both look like success and are not.
22817
+ *
22818
+ * @returns the operation's value, or the reason recovery could not deliver
22819
+ * one. `error` is undefined when there was no way to mint at all.
22820
+ */
22821
+ async recoverAndRetry(operation) {
22822
+ if (!await this.remintAndReauthenticate()) return { ok: false };
22823
+ try {
22824
+ const value = await operation();
22825
+ this._credentialRecovered = true;
22826
+ return {
22827
+ ok: true,
22828
+ value
22829
+ };
22830
+ } catch (error) {
22831
+ logger$1.warn("[SignalWire] Reauthentication was accepted but the operation is still refused:", error);
22832
+ return {
22833
+ ok: false,
22834
+ error
22835
+ };
22836
+ }
22837
+ }
22838
+ /**
22839
+ * Re-mint a credential and adopt it only if the live session accepts it.
22840
+ *
22841
+ * The mechanism follows the binding: a client-bound session re-mints a bound
22842
+ * base SAT through `authenticate()` with the DPoP fingerprint, because the
22843
+ * developer refresh handler would hand back an unbound token and silently
22844
+ * degrade the session. An unbound session uses the refresh handler. Rotation
22845
+ * cost is not a reason to skip this — the only reason is having no mechanism.
22846
+ *
22847
+ * @returns whether the session is now running on a freshly accepted credential.
22848
+ */
22849
+ async remintAndReauthenticate() {
22850
+ const provider = this._credentialProvider;
22851
+ if (!provider) return false;
22852
+ const { clientBound } = this._clientSession;
22853
+ if (!clientBound && !provider.refresh) {
22854
+ logger$1.debug("[SignalWire] [SW-NO-REFRESH-HANDLER] Unbound session with no refresh handler; cannot re-mint.");
22855
+ return false;
22856
+ }
22857
+ try {
22858
+ const newCredentials = clientBound ? await provider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0) : await this.remintCredential(provider);
22859
+ if (!newCredentials.token) {
22860
+ logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the current one.");
22861
+ return false;
22862
+ }
22863
+ await this._clientSession.reauthenticate(newCredentials.token);
22864
+ this._deps.credential = newCredentials;
22865
+ this.persistCredential(newCredentials);
22866
+ if (newCredentials.expiry_at && provider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(provider, newCredentials.expiry_at);
22867
+ return true;
22868
+ } catch (error) {
22869
+ logger$1.warn("[SignalWire] Re-mint recovery failed:", error);
22870
+ return false;
22871
+ }
22872
+ }
22873
+ /**
22874
+ * Re-mint a credential via `provider.refresh()`, routed through the
22875
+ * coordinator's shared in-flight guard so concurrent re-mint paths (a
22876
+ * scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
22877
+ * second `provider.refresh()` in parallel — which rotating one-time-use
22878
+ * refresh tokens reject. Falls back to a direct call only if the coordinator
22879
+ * has not been constructed yet.
22880
+ */
22881
+ async remintCredential(provider) {
22882
+ if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
22883
+ if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
22884
+ return provider.refresh();
22885
+ }
22886
+ /**
22887
+ * Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
22888
+ * The session invokes this only when it is client-bound OR the in-memory
22889
+ * token is expired. The re-mint mechanism depends on the binding:
22890
+ * - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
22891
+ * fresh base SAT the upcoming reconnect can re-bind (the
22892
+ * DeviceTokenManager re-activates afterwards).
22893
+ * - Unbound: the developer's non-interactive `refresh()` handler.
22894
+ * `authenticate()` is deliberately NOT used here — it may be interactive
22895
+ * (a login prompt) and must not fire on a background reconnect.
22896
+ *
22897
+ * Rejects on failure so the session aborts the reconnect rather than
22898
+ * replaying a stale token.
22899
+ */
22900
+ async refreshCredentialForReconnect() {
22901
+ if (!this._credentialProvider) return;
22902
+ try {
22903
+ let newCredentials;
22904
+ if (this._clientSession?.clientBound ?? await this.wasClientBound()) {
22905
+ logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
22906
+ newCredentials = await this._credentialProvider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0);
22907
+ } else if (this._credentialProvider.refresh) {
22908
+ logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
22909
+ newCredentials = await this.remintCredential(this._credentialProvider);
22910
+ } else {
22911
+ logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
22912
+ return;
22913
+ }
22914
+ if (!newCredentials.token) {
22915
+ logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
22916
+ return;
22917
+ }
22918
+ this._deps.credential = newCredentials;
22919
+ this.persistCredential(newCredentials);
22920
+ if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
22921
+ logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
22922
+ } catch (error) {
22923
+ logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
22924
+ this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
22925
+ throw error;
22926
+ }
22927
+ }
21701
22928
  /** Persist credential to localStorage when persistSession is enabled. */
21702
22929
  persistCredential(credential) {
21703
22930
  if (!credential.token) return;
21704
22931
  this._deps.storage.setItem("sw:cached_credential", credential);
21705
22932
  if (this._deps.persistSession) this._deps.storage.setItem("sw:cached_credential", credential, "local");
21706
22933
  }
22934
+ /**
22935
+ * Persist whether the session is client-bound, mirroring the credential's
22936
+ * storage scopes so it survives a reload. The preflight recovery reads it
22937
+ * before any session exists to decide whether to re-bind via `authenticate()`
22938
+ * or refresh an unbound token; the marker tracks the latest binding, so an
22939
+ * unbound reconnect clears a stale marker from an earlier client-bound login.
22940
+ */
22941
+ persistClientBoundMarker(bound) {
22942
+ const scopes = this._deps.persistSession ? ["session", "local"] : ["session"];
22943
+ for (const scope of scopes) if (bound) this._deps.storage.setItem(CLIENT_BOUND_STORAGE_KEY, true, scope);
22944
+ else this._deps.storage.removeItem(CLIENT_BOUND_STORAGE_KEY, scope);
22945
+ }
22946
+ /** Read the persisted client-bound marker (see {@link persistClientBoundMarker}). */
22947
+ async wasClientBound() {
22948
+ const scopes = this._deps.persistSession ? ["local", "session"] : ["session"];
22949
+ for (const scope of scopes) try {
22950
+ if (await this._deps.storage.getItem(CLIENT_BOUND_STORAGE_KEY, scope)) return true;
22951
+ } catch {}
22952
+ return false;
22953
+ }
21707
22954
  async init() {
21708
22955
  this._user$.next(new User(this._deps.http));
21709
22956
  if (!this._options.skipConnection) await this.connect();
@@ -21727,6 +22974,42 @@ var SignalWire = class extends Destroyable {
21727
22974
  } catch (error) {
21728
22975
  logger$1.error("[SignalWire] Failed to reattach calls:", error);
21729
22976
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
22977
+ } finally {
22978
+ this._credentialRecovered = false;
22979
+ }
22980
+ }
22981
+ /**
22982
+ * Fetch the authenticated user profile, recovering a stale credential.
22983
+ *
22984
+ * On a reload the persisted credential can be expired. Unlike the WS resume —
22985
+ * which the server accepts against the persisted `authorization_state` even
22986
+ * with an expired token — this REST preflight has no such short-circuit and is
22987
+ * refused (401). There is no session yet to reauthenticate, so recovery
22988
+ * re-mints the credential through the provider ({@link refreshCredentialForReconnect})
22989
+ * and retries with a FRESH {@link User}: Fetchable memoizes its result
22990
+ * (shareReplay), so reusing the instance would replay the 401 instead of
22991
+ * re-fetching with the new token. Without the user id the transport/session —
22992
+ * and the reattach a reload is trying to preserve — cannot even be addressed.
22993
+ */
22994
+ async fetchUserOrRecover() {
22995
+ const fetchUser = async (user$1) => {
22996
+ if (!await (0, import_cjs$1.firstValueFrom)(user$1.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
22997
+ this._deps.user = user$1;
22998
+ };
22999
+ const user = this._user$.value;
23000
+ if (!user) throw new UnexpectedError("User not initialized before connect");
23001
+ try {
23002
+ await fetchUser(user);
23003
+ } catch (firstError) {
23004
+ logger$1.error(`[SignalWire] Failed to fetch user information: ${firstError instanceof Error ? firstError.message : "Unknown error"}. This usually means the user token is invalid or expired. Re-minting the credential and retrying.`);
23005
+ try {
23006
+ await this.refreshCredentialForReconnect();
23007
+ const refetched = new User(this._deps.http);
23008
+ await fetchUser(refetched);
23009
+ this._user$.next(refetched);
23010
+ } catch (retryError) {
23011
+ throw new UnexpectedError("Error fetching user information", { cause: retryError });
23012
+ }
21730
23013
  }
21731
23014
  }
21732
23015
  /**
@@ -21768,40 +23051,23 @@ var SignalWire = class extends Destroyable {
21768
23051
  */
21769
23052
  async connect() {
21770
23053
  await this.teardownTransportAndSession();
21771
- try {
21772
- const user = this._user$.value;
21773
- if (!user) throw new UnexpectedError("User not initialized before connect");
21774
- if (!await (0, import_cjs$1.firstValueFrom)(user.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
21775
- this._deps.user = user;
21776
- } catch (error) {
21777
- logger$1.error(`[SignalWire] Failed to fetch user information: ${error instanceof Error ? error.message : "Unknown error"}. This usually means the user token is invalid or expired.`);
21778
- throw new UnexpectedError("Error fetching user information", { cause: error });
21779
- }
23054
+ await this.fetchUserOrRecover();
21780
23055
  const errorHandler = (error) => {
21781
23056
  this._errors$.next(error);
21782
23057
  };
21783
23058
  this._transport = new TransportManager(this._deps.storage, this._deps.protocolKey, this._deps.WebSocket, PreferencesContainer.instance.relayHost ?? this._deps.relayHost, errorHandler);
21784
- this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
23059
+ this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey, () => this._credentialRecovered);
21785
23060
  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$);
21786
23061
  this._publicSession = new ClientSessionWrapper(this._clientSession);
21787
- this._clientSession.onBeforeReconnect = async () => {
21788
- if (!this._credentialProvider) return;
21789
- try {
21790
- const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
21791
- logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
21792
- const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
21793
- this._deps.credential = newCredentials;
21794
- if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
21795
- logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
21796
- } catch (error) {
21797
- logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
21798
- this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21799
- throw error;
21800
- }
21801
- };
23062
+ this._clientSession.callControl = this._options.callControl ?? "routed";
23063
+ this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
21802
23064
  this.subscribeTo(this._clientSession.errors$, (error) => {
21803
23065
  this._errors$.next(error);
21804
23066
  });
23067
+ this.subscribeTo(this._clientSession.authorization$, (authorization) => {
23068
+ this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
23069
+ this.persistClientBoundMarker(Boolean(authorization?.cnf?.jkt));
23070
+ });
21805
23071
  await this._clientSession.connect();
21806
23072
  await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
21807
23073
  this.subscribeTo(this._clientSession.authenticated$.pipe((0, import_cjs$1.skip)(1), (0, import_cjs$1.filter)(Boolean)), async () => {
@@ -21964,6 +23230,13 @@ var SignalWire = class extends Destroyable {
21964
23230
  }
21965
23231
  try {
21966
23232
  this._visibilityController = new VisibilityController();
23233
+ this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible")), () => {
23234
+ try {
23235
+ this._refreshCoordinator?.forceRefreshIfDue();
23236
+ } catch (error) {
23237
+ logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
23238
+ }
23239
+ });
21967
23240
  this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
21968
23241
  logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
21969
23242
  try {
@@ -21975,7 +23248,7 @@ var SignalWire = class extends Destroyable {
21975
23248
  logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
21976
23249
  }
21977
23250
  try {
21978
- this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.2" });
23251
+ this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.4" });
21979
23252
  } catch (error) {
21980
23253
  logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
21981
23254
  }
@@ -22046,21 +23319,22 @@ var SignalWire = class extends Destroyable {
22046
23319
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
22047
23320
  throw error;
22048
23321
  }
22049
- logger$1.debug("[SignalWire] Failed to register user, trying reauthentication...");
22050
- try {
22051
- await this._clientSession.reauthenticate(this._deps.credential.token);
22052
- logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
23322
+ logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
23323
+ const outcome = await this.recoverAndRetry(async () => {
22053
23324
  await this._transport.execute(RPCExecute({
22054
23325
  method: "subscriber.online",
22055
23326
  params: {}
22056
23327
  }));
23328
+ });
23329
+ if (outcome.ok) {
23330
+ logger$1.debug("[SignalWire] Recovery restored registration");
22057
23331
  this._isRegistered$.next(true);
22058
- } catch (reauthError) {
22059
- logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
22060
- 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 }) });
22061
- this._errors$.next(registerError);
22062
- throw registerError;
23332
+ return;
22063
23333
  }
23334
+ const failureCause = outcome.error ?? error;
23335
+ 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 }) });
23336
+ this._errors$.next(registerError);
23337
+ throw registerError;
22064
23338
  }
22065
23339
  }
22066
23340
  /**
@@ -22091,6 +23365,11 @@ var SignalWire = class extends Destroyable {
22091
23365
  * Returns a {@link Call} in `'ringing'` state. Subscribe to {@link Call.status$}
22092
23366
  * to track progression through `'connected'` → `'disconnected'`.
22093
23367
  *
23368
+ * Local media acquisition is deliberately unbounded: an unanswered permission
23369
+ * prompt leaves this promise pending indefinitely, so apply your own bound if
23370
+ * your UI needs one. The 12 s signaling budget starts only once acquisition
23371
+ * settles.
23372
+ *
22094
23373
  * @param destination - Address URI string (e.g. `'/public/my-room'`) or {@link Address} instance.
22095
23374
  * @param options - Media and dial options (audio/video, device constraints). Overrides defaults.
22096
23375
  * @returns The created {@link Call} instance.
@@ -22113,7 +23392,18 @@ var SignalWire = class extends Destroyable {
22113
23392
  };
22114
23393
  await this.waitAuthentication();
22115
23394
  logger$1.debug("[SignalWire] Dialing with options:", computed_options);
22116
- return this._clientSession.createOutboundCall(destination, computed_options);
23395
+ try {
23396
+ return await this._clientSession.createOutboundCall(destination, computed_options);
23397
+ } catch (error) {
23398
+ if (!isRecoverableAuthError(error)) throw error;
23399
+ logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering the session and retrying");
23400
+ const outcome = await this.recoverAndRetry(async () => {
23401
+ await this.waitAuthentication();
23402
+ return this._clientSession.createOutboundCall(destination, computed_options);
23403
+ });
23404
+ if (outcome.ok) return outcome.value;
23405
+ throw outcome.error ?? error;
23406
+ }
22117
23407
  }
22118
23408
  /**
22119
23409
  * Runs a multi-phase connectivity test against the given destination.
@@ -22394,10 +23684,13 @@ var SignalWire = class extends Destroyable {
22394
23684
  destroy() {
22395
23685
  this._refreshCoordinator?.destroy();
22396
23686
  this._refreshCoordinator = void 0;
23687
+ this._refreshHttp?.destroy();
23688
+ this._refreshHttp = void 0;
22397
23689
  this._dpopManager?.destroy();
22398
- this._clientSession.teardownSessionState();
22399
- this._transport.destroy();
22400
- this._clientSession.destroy();
23690
+ const session = this._clientSession;
23691
+ session?.teardownSessionState();
23692
+ this._transport?.destroy();
23693
+ session?.destroy();
22401
23694
  try {
22402
23695
  this._networkMonitor?.destroy();
22403
23696
  } catch {}
@@ -22510,7 +23803,7 @@ var StaticCredentialProvider = class {
22510
23803
  /**
22511
23804
  * Library version from package.json, injected at build time.
22512
23805
  */
22513
- const version = "4.0.0-rc.2";
23806
+ const version = "4.0.0-rc.4";
22514
23807
  /**
22515
23808
  * Flag indicating the library has been loaded and is ready to use.
22516
23809
  * For UMD builds: `window.SignalWire.ready`
@@ -22532,7 +23825,7 @@ const ready = true;
22532
23825
  */
22533
23826
  const emitReadyEvent = () => {
22534
23827
  if (typeof window !== "undefined") {
22535
- const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.2" } });
23828
+ const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.4" } });
22536
23829
  window.dispatchEvent(event);
22537
23830
  }
22538
23831
  };
@@ -22557,7 +23850,10 @@ if (typeof process === "undefined") globalThis.process = { env: { NODE_ENV: "pro
22557
23850
 
22558
23851
  //#endregion
22559
23852
  exports.Address = Address;
23853
+ exports.AuxiliaryLegCancelledError = AuxiliaryLegCancelledError;
23854
+ exports.AuxiliaryLegTimeoutError = AuxiliaryLegTimeoutError;
22560
23855
  exports.CallCreateError = CallCreateError;
23856
+ exports.CallNotReadyError = CallNotReadyError;
22561
23857
  exports.ClientPreferences = ClientPreferences;
22562
23858
  exports.CollectionFetchError = CollectionFetchError;
22563
23859
  exports.DPoPInitError = DPoPInitError;
@@ -22569,8 +23865,10 @@ exports.MediaTrackError = MediaTrackError;
22569
23865
  exports.MessageParseError = MessageParseError;
22570
23866
  exports.OverconstrainedFallbackError = OverconstrainedFallbackError;
22571
23867
  exports.Participant = Participant;
23868
+ exports.ParticipantNotReadyError = ParticipantNotReadyError;
22572
23869
  exports.PreflightError = PreflightError;
22573
23870
  exports.RecoveryError = RecoveryError;
23871
+ exports.ScreenShareAlreadyActiveError = ScreenShareAlreadyActiveError;
22574
23872
  exports.SelfCapabilities = SelfCapabilities;
22575
23873
  exports.SelfParticipant = SelfParticipant;
22576
23874
  exports.SignalWire = SignalWire;