@signalwire/js 4.0.0-dev-20260714201720 → 4.0.0-dev-20260721161304

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.mjs CHANGED
@@ -9049,6 +9049,14 @@ const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
9049
9049
  /** Buffer in milliseconds before token expiry to trigger refresh. */
9050
9050
  const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
9051
9051
  /**
9052
+ * Clock-skew allowance (ms) for treating an in-memory credential as expired
9053
+ * when deciding whether a fresh (re)connect must re-mint the token before
9054
+ * authenticating. A token within this window of its `expiry_at` is treated as
9055
+ * stale so the reconnect re-mints via the credential provider instead of
9056
+ * replaying a dead token (which the server rejects with -32003).
9057
+ */
9058
+ const CREDENTIAL_EXPIRY_SKEW_MS = 3e4;
9059
+ /**
9052
9060
  * Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
9053
9061
  * to resolve before treating the activation as failed and falling back to
9054
9062
  * the developer-provided refresh path. Prevents a wedged HTTP layer from
@@ -9281,6 +9289,20 @@ var CallCreateError = class extends Error {
9281
9289
  this.name = "CallCreateError";
9282
9290
  }
9283
9291
  };
9292
+ var CallNotReadyError = class extends Error {
9293
+ constructor(callId, options) {
9294
+ super(`Call "${callId}" has no self member context yet: selfId/nodeId have not been received from the server`, options);
9295
+ this.callId = callId;
9296
+ this.name = "CallNotReadyError";
9297
+ }
9298
+ };
9299
+ var ParticipantNotReadyError = class extends Error {
9300
+ constructor(memberId, options) {
9301
+ super(`Participant "${memberId}" has no call context yet: its member state (call_id/node_id) has not been received from the server`, options);
9302
+ this.memberId = memberId;
9303
+ this.name = "ParticipantNotReadyError";
9304
+ }
9305
+ };
9284
9306
  var JSONRPCError = class extends Error {
9285
9307
  constructor(code, message, data, options, requestId) {
9286
9308
  super(message, options);
@@ -13644,9 +13666,9 @@ const initialState = {};
13644
13666
  * the local participant with additional device control.
13645
13667
  */
13646
13668
  var Participant = class extends Destroyable {
13647
- constructor(id, executeMethod, deviceController) {
13669
+ constructor(id, callExecuteMethod, deviceController) {
13648
13670
  super();
13649
- this.executeMethod = executeMethod;
13671
+ this.callExecuteMethod = callExecuteMethod;
13650
13672
  this.deviceController = deviceController;
13651
13673
  this._state$ = this.createBehaviorSubject(initialState);
13652
13674
  this.id = id;
@@ -13868,22 +13890,55 @@ var Participant = class extends Destroyable {
13868
13890
  get value() {
13869
13891
  return this._state$.value;
13870
13892
  }
13893
+ /**
13894
+ * Target triple for member RPCs, built from the participant's own state.
13895
+ * The backend locates the member's session by the target `call_id`/`node_id`,
13896
+ * so this must always be the participant's own call context — never the
13897
+ * local call's id (issue #19400).
13898
+ *
13899
+ * Reading it doubles as a readiness probe: it throws until the first full
13900
+ * member event (`member.joined`/`member.updated` or the `call.joined`
13901
+ * roster) arrives, and never regresses afterwards.
13902
+ *
13903
+ * @throws {ParticipantNotReadyError} If the member state has not been
13904
+ * received yet (e.g. a participant first seen via `member.talking`) — an
13905
+ * empty call context can never address the member, so fail fast instead of
13906
+ * sending a doomed RPC.
13907
+ */
13908
+ get target() {
13909
+ const { call_id, node_id } = this._state$.value;
13910
+ if (!call_id || !node_id) throw new ParticipantNotReadyError(this.id);
13911
+ return {
13912
+ member_id: this.id,
13913
+ call_id,
13914
+ node_id
13915
+ };
13916
+ }
13917
+ /**
13918
+ * Executes a member RPC against this participant, injecting its own
13919
+ * {@link target} as the target.
13920
+ *
13921
+ * @throws {ParticipantNotReadyError} Via {@link target}, when the
13922
+ * member state has not been received yet.
13923
+ */
13924
+ async executeMethod(method, args) {
13925
+ return this.callExecuteMethod(this.target, method, args);
13926
+ }
13871
13927
  /** Toggles the deafened state (mutes/unmutes incoming audio). */
13872
13928
  async toggleDeaf() {
13873
- const method = toggleDeafMethod(this.deaf);
13874
- await this.executeMethod(this.id, method, {});
13929
+ await this.executeMethod(toggleDeafMethod(this.deaf), {});
13875
13930
  }
13876
13931
  /** Toggles the hand-raised state. */
13877
13932
  async toggleHandraise() {
13878
- await this.executeMethod(this.id, toggleHandraiseMethod(this.handraised), {});
13933
+ await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
13879
13934
  }
13880
13935
  /** Mutes the participant's audio. */
13881
13936
  async mute() {
13882
- await this.executeMethod(this.id, "call.mute", { channels: ["audio"] });
13937
+ await this.executeMethod("call.mute", { channels: ["audio"] });
13883
13938
  }
13884
13939
  /** Unmutes the participant's audio. */
13885
13940
  async unmute() {
13886
- await this.executeMethod(this.id, "call.unmute", { channels: ["audio"] });
13941
+ await this.executeMethod("call.unmute", { channels: ["audio"] });
13887
13942
  }
13888
13943
  /** Toggles the participant's audio mute state. */
13889
13944
  async toggleMute() {
@@ -13891,11 +13946,11 @@ var Participant = class extends Destroyable {
13891
13946
  }
13892
13947
  /** Mutes the participant's video. */
13893
13948
  async muteVideo() {
13894
- await this.executeMethod(this.id, "call.mute", { channels: ["video"] });
13949
+ await this.executeMethod("call.mute", { channels: ["video"] });
13895
13950
  }
13896
13951
  /** Unmutes the participant's video. */
13897
13952
  async unmuteVideo() {
13898
- await this.executeMethod(this.id, "call.unmute", { channels: ["video"] });
13953
+ await this.executeMethod("call.unmute", { channels: ["video"] });
13899
13954
  }
13900
13955
  /** Toggles the participant's video mute state. */
13901
13956
  async toggleMuteVideo() {
@@ -13903,7 +13958,7 @@ var Participant = class extends Destroyable {
13903
13958
  }
13904
13959
  /** Toggles echo cancellation on the audio input. */
13905
13960
  async toggleEchoCancellation() {
13906
- await this.executeMethod(this.id, "call.audioflags.set", {
13961
+ await this.executeMethod("call.audioflags.set", {
13907
13962
  echo_cancellation: !this.echoCancellation,
13908
13963
  auto_gain: this.autoGain,
13909
13964
  noise_suppression: this.noiseSuppression
@@ -13911,7 +13966,7 @@ var Participant = class extends Destroyable {
13911
13966
  }
13912
13967
  /** Toggles automatic gain control on the audio input. */
13913
13968
  async toggleAudioInputAutoGain() {
13914
- await this.executeMethod(this.id, "call.audioflags.set", {
13969
+ await this.executeMethod("call.audioflags.set", {
13915
13970
  echo_cancellation: this.echoCancellation,
13916
13971
  auto_gain: !this.autoGain,
13917
13972
  noise_suppression: this.noiseSuppression
@@ -13919,7 +13974,7 @@ var Participant = class extends Destroyable {
13919
13974
  }
13920
13975
  /** Toggles noise suppression on the audio input. */
13921
13976
  async toggleNoiseSuppression() {
13922
- await this.executeMethod(this.id, "call.audioflags.set", {
13977
+ await this.executeMethod("call.audioflags.set", {
13923
13978
  echo_cancellation: this.echoCancellation,
13924
13979
  auto_gain: this.autoGain,
13925
13980
  noise_suppression: !this.noiseSuppression
@@ -13927,7 +13982,7 @@ var Participant = class extends Destroyable {
13927
13982
  }
13928
13983
  /** Toggles low-bitrate mode for this participant's media. */
13929
13984
  async toggleLowbitrate() {
13930
- await this.executeMethod(this.id, "call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
13985
+ await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
13931
13986
  }
13932
13987
  /**
13933
13988
  * Adjusts the **conference-only** microphone energy gate / sensitivity level
@@ -13943,7 +13998,7 @@ var Participant = class extends Destroyable {
13943
13998
  * (integer, larger values are more sensitive).
13944
13999
  */
13945
14000
  async setAudioInputSensitivity(value) {
13946
- await this.executeMethod(this.id, "call.microphone.sensitivity.set", { sensitivity: value });
14001
+ await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
13947
14002
  }
13948
14003
  /**
13949
14004
  * Sets the **server-side** microphone volume on this participant's bridged
@@ -13956,7 +14011,7 @@ var Participant = class extends Destroyable {
13956
14011
  * @param value - Volume level (0-100).
13957
14012
  */
13958
14013
  async setAudioInputVolume(value) {
13959
- await this.executeMethod(this.id, "call.microphone.volume.set", { volume: value });
14014
+ await this.executeMethod("call.microphone.volume.set", { volume: value });
13960
14015
  }
13961
14016
  /**
13962
14017
  * Sets the **server-side** speaker volume on this participant's bridged call
@@ -13970,45 +14025,31 @@ var Participant = class extends Destroyable {
13970
14025
  * @param value - Volume level (0-100).
13971
14026
  */
13972
14027
  async setAudioOutputVolume(value) {
13973
- await this.executeMethod(this.id, "call.speaker.volume.set", { volume: value });
14028
+ await this.executeMethod("call.speaker.volume.set", { volume: value });
13974
14029
  }
13975
14030
  /**
13976
14031
  * Sets the participant's position in the video layout.
13977
14032
  *
13978
- * Requires the `member.position` capability. The gateway keys positions by the
13979
- * **target member's own** `call_id`/`node_id` (see issue #19400 and the legacy
13980
- * `setPositions` implementation), so this sends the participant's own call
13981
- * context — matching {@link Participant.remove}. A resolved promise does not
13982
- * guarantee a visible change: the backend silently returns `200` (no-op) for
13983
- * non-conference targets.
14033
+ * Requires the `member.position` capability. The gateway requires a
14034
+ * `targets` array of `{ target, position }` entries (issue #19400). A
14035
+ * resolved promise does not guarantee a visible change: the backend silently
14036
+ * returns `200` (no-op) for non-conference targets.
13984
14037
  *
13985
14038
  * @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
13986
14039
  */
13987
14040
  async setPosition(value) {
13988
- const state = this._state$.value;
13989
- const target = {
13990
- member_id: this.id,
13991
- call_id: state.call_id ?? "",
13992
- node_id: state.node_id ?? ""
13993
- };
13994
- await this.executeMethod(target, "call.member.position.set", { targets: [{
13995
- target,
14041
+ await this.executeMethod("call.member.position.set", { targets: [{
14042
+ target: this.target,
13996
14043
  position: value
13997
14044
  }] });
13998
14045
  }
13999
14046
  /** Removes this participant from the call. */
14000
14047
  async remove() {
14001
- const state = this._state$.value;
14002
- const target = {
14003
- member_id: this.id,
14004
- call_id: state.call_id ?? "",
14005
- node_id: state.node_id ?? ""
14006
- };
14007
- await this.executeMethod(target, "call.member.remove", {});
14048
+ await this.executeMethod("call.member.remove", { targets: [this.target] });
14008
14049
  }
14009
14050
  /** Ends the call for this participant. */
14010
14051
  async end() {
14011
- await this.executeMethod(this.id, "call.end", {});
14052
+ await this.executeMethod("call.end", {});
14012
14053
  }
14013
14054
  /**
14014
14055
  * Replaces custom metadata for this participant.
@@ -14028,7 +14069,7 @@ var Participant = class extends Destroyable {
14028
14069
  }
14029
14070
  /** Destroys the participant, releasing all subscriptions and references. */
14030
14071
  destroy() {
14031
- this.executeMethod = void 0;
14072
+ this.callExecuteMethod = void 0;
14032
14073
  super.destroy();
14033
14074
  }
14034
14075
  };
@@ -14040,8 +14081,8 @@ var Participant = class extends Destroyable {
14040
14081
  */
14041
14082
  var SelfParticipant = class extends Participant {
14042
14083
  /** @internal */
14043
- constructor(id, executeMethod, vertoManager, deviceController) {
14044
- super(id, executeMethod, deviceController);
14084
+ constructor(id, callExecuteMethod, vertoManager, deviceController) {
14085
+ super(id, callExecuteMethod, deviceController);
14045
14086
  this.vertoManager = vertoManager;
14046
14087
  this._studioAudio$ = this.createBehaviorSubject(false);
14047
14088
  this.capabilities = new SelfCapabilities();
@@ -14065,7 +14106,7 @@ var SelfParticipant = class extends Participant {
14065
14106
  async enableStudioAudio() {
14066
14107
  if (this._studioAudio$.value) return;
14067
14108
  this._studioAudio$.next(true);
14068
- await this.executeMethod(this.id, "call.audioflags.set", {
14109
+ await this.executeMethod("call.audioflags.set", {
14069
14110
  echo_cancellation: false,
14070
14111
  auto_gain: false,
14071
14112
  noise_suppression: false
@@ -14078,7 +14119,7 @@ var SelfParticipant = class extends Participant {
14078
14119
  async disableStudioAudio() {
14079
14120
  if (!this._studioAudio$.value) return;
14080
14121
  this._studioAudio$.next(false);
14081
- await this.executeMethod(this.id, "call.audioflags.set", {
14122
+ await this.executeMethod("call.audioflags.set", {
14082
14123
  echo_cancellation: true,
14083
14124
  auto_gain: true,
14084
14125
  noise_suppression: true
@@ -18299,12 +18340,6 @@ function mosToQualityLevel(mos) {
18299
18340
  var import_cjs$11 = require_cjs();
18300
18341
  const logger$12 = getLogger();
18301
18342
  /**
18302
- * Verto method for setting member layout positions. Its gateway DTO requires a
18303
- * `targets` array whose entries are `{ target, position }` (NOT bare targets),
18304
- * so {@link WebRTCCall.buildMethodParams} special-cases it. See issue #19400.
18305
- */
18306
- const POSITION_SET_METHOD = "call.member.position.set";
18307
- /**
18308
18343
  * Ratio between the critical and warning RTT spike multipliers.
18309
18344
  * Warning threshold = baseline * warningMultiplier (default 3x)
18310
18345
  * Critical threshold = baseline * warningMultiplier * RTT_CRITICAL_TO_WARNING_RATIO
@@ -18455,7 +18490,7 @@ var WebRTCCall = class extends Destroyable {
18455
18490
  /** Toggles the call lock state, preventing or allowing new participants from joining. */
18456
18491
  async toggleLock() {
18457
18492
  const method = this.locked ? "call.unlock" : "call.lock";
18458
- await this.executeMethod(this.selfId ?? "", method, {});
18493
+ await this.executeMethod(this.callSelf, method, {});
18459
18494
  }
18460
18495
  /**
18461
18496
  * Toggles the hold state of the call (pauses/resumes local media transmission).
@@ -18504,14 +18539,24 @@ var WebRTCCall = class extends Destroyable {
18504
18539
  *
18505
18540
  * Constructs call context (node_id, call_id, member_id) and sends the RPC request.
18506
18541
  *
18507
- * @param target - Target member ID string, or a {@link MemberTarget} object.
18542
+ * @param target - Target {@link MemberTarget} triple, or the local member's
18543
+ * ID string for self-operations (any other string is rejected — a bare
18544
+ * member id cannot carry the remote member's own call context).
18508
18545
  * @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
18509
18546
  * @param args - Parameters for the RPC method.
18510
18547
  * @returns The RPC response.
18548
+ * @throws {CallNotReadyError} If the call has no self member context yet.
18549
+ * @throws {InvalidParams} If a string target is not the local member's ID.
18511
18550
  * @throws {JSONRPCError} If the RPC call returns an error.
18512
18551
  */
18513
18552
  async executeMethod(target, method, args) {
18514
- const params = this.buildMethodParams(target, args, method);
18553
+ const self = this.callSelf;
18554
+ 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}`);
18555
+ const params = {
18556
+ ...args,
18557
+ self,
18558
+ target: typeof target === "string" ? self : target
18559
+ };
18515
18560
  const request = buildRPCRequest({
18516
18561
  method,
18517
18562
  params
@@ -18525,29 +18570,23 @@ var WebRTCCall = class extends Destroyable {
18525
18570
  throw error;
18526
18571
  }
18527
18572
  }
18528
- buildMethodParams(target, args, method) {
18529
- const self = {
18530
- node_id: this.nodeId ?? "",
18531
- call_id: this.id,
18532
- member_id: this.vertoManager.selfId ?? ""
18533
- };
18534
- if (method === POSITION_SET_METHOD) return {
18535
- ...args,
18536
- self
18537
- };
18538
- if (typeof target === "object") return {
18539
- ...args,
18540
- self,
18541
- targets: [target]
18542
- };
18573
+ /**
18574
+ * The local leg's member triple — sent as `self` in every member RPC
18575
+ * envelope, and as the `target` of call-scoped self-operations (e.g. lock,
18576
+ * layout).
18577
+ *
18578
+ * @throws {CallNotReadyError} Before `call.joined` delivers the self member
18579
+ * context (`selfId`/`nodeId`) — an RPC without it cannot be routed, so fail
18580
+ * fast instead of sending a doomed request.
18581
+ */
18582
+ get callSelf() {
18583
+ const node_id = this.nodeId;
18584
+ const member_id = this.vertoManager.selfId;
18585
+ if (!node_id || !member_id) throw new CallNotReadyError(this.id);
18543
18586
  return {
18544
- ...args,
18545
- self,
18546
- target: {
18547
- node_id: this.nodeId ?? "",
18548
- call_id: this.id,
18549
- member_id: target
18550
- }
18587
+ node_id,
18588
+ call_id: this.id,
18589
+ member_id
18551
18590
  };
18552
18591
  }
18553
18592
  /** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
@@ -19110,12 +19149,17 @@ var WebRTCCall = class extends Destroyable {
19110
19149
  *
19111
19150
  * **These operations are NOT atomic.** The layout is applied first, then each
19112
19151
  * member position sequentially, so members may briefly flash into their
19113
- * default slots before being moved to the requested positions.
19152
+ * default slots before being moved to the requested positions. Targeted
19153
+ * members are validated upfront, though: when any of them has no
19154
+ * {@link Participant.target | member call context} yet, the whole call
19155
+ * rejects before any request is sent and the layout is left unchanged.
19114
19156
  *
19115
19157
  * @param layout - Layout name (must be one of {@link layouts}).
19116
19158
  * @param positions - Optional map of member IDs to {@link VideoPosition} values.
19117
19159
  * When omitted or empty, only the layout is changed.
19118
19160
  * @throws {InvalidParams} If the layout is not in the available {@link layouts}.
19161
+ * @throws {ParticipantNotReadyError} If a targeted member's call context has
19162
+ * not been received yet — thrown before any request is sent.
19119
19163
  *
19120
19164
  * @example
19121
19165
  * ```ts
@@ -19126,18 +19170,19 @@ var WebRTCCall = class extends Destroyable {
19126
19170
  */
19127
19171
  async setLayout(layout, positions) {
19128
19172
  if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
19129
- const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
19130
- await this.executeMethod(selfId, "call.layout.set", { layout });
19131
- const positionEntries = Object.entries(positions ?? {});
19132
- if (positionEntries.length === 0) return;
19133
- for (const [memberId, position] of positionEntries) {
19173
+ const targets = [];
19174
+ for (const [memberId, position] of Object.entries(positions ?? {})) {
19134
19175
  const participant = this.participants.find((p) => p.id === memberId);
19135
19176
  if (!participant) {
19136
19177
  logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
19137
19178
  continue;
19138
19179
  }
19139
- await participant.setPosition(position);
19180
+ participant.target;
19181
+ targets.push([participant, position]);
19140
19182
  }
19183
+ const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
19184
+ await this.executeMethod(selfId, "call.layout.set", { layout });
19185
+ for (const [participant, position] of targets) await participant.setPosition(position);
19141
19186
  }
19142
19187
  /**
19143
19188
  * Transfers the call to another destination.
@@ -20056,9 +20101,9 @@ var ClientSessionManager = class extends Destroyable {
20056
20101
  }
20057
20102
  async handleAuthenticationError(error) {
20058
20103
  logger$9.error("Authentication error:", error);
20059
- const isRecoverableAuthError = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
20104
+ 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);
20060
20105
  const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
20061
- if (isRecoverableAuthError && hasStoredState) {
20106
+ if (isRecoverableAuthError$1 && hasStoredState) {
20062
20107
  logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
20063
20108
  try {
20064
20109
  await this.cleanupStoredConnectionParams();
@@ -20154,9 +20199,13 @@ var ClientSessionManager = class extends Destroyable {
20154
20199
  const isReconnect = hasReconnectState && storedToken;
20155
20200
  let dpopToken;
20156
20201
  if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
20157
- else if (this.onBeforeReconnect && this.clientBound) {
20158
- logger$9.debug("[Session] Refreshing credentials before fresh connect");
20159
- await this.onBeforeReconnect();
20202
+ else {
20203
+ const credential = this.getCredential();
20204
+ const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + CREDENTIAL_EXPIRY_SKEW_MS;
20205
+ if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
20206
+ logger$9.debug("[Session] Refreshing credentials before fresh connect");
20207
+ await this.onBeforeReconnect();
20208
+ }
20160
20209
  }
20161
20210
  if ((!isReconnect || this.clientBound) && this.dpopManager?.initialized) try {
20162
20211
  dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
@@ -20512,7 +20561,7 @@ var DeviceTokenManager = class extends Destroyable {
20512
20561
  await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
20513
20562
  updateCredential({ token: tokenData.token });
20514
20563
  logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
20515
- this._currentToken$.next(tokenData);
20564
+ this.emitCurrentToken(tokenData);
20516
20565
  return { activated: true };
20517
20566
  } catch (error) {
20518
20567
  logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
@@ -20524,6 +20573,21 @@ var DeviceTokenManager = class extends Destroyable {
20524
20573
  }
20525
20574
  }
20526
20575
  /**
20576
+ * Emit a freshly received token to the reactive pipeline, stamping an
20577
+ * absolute `expires_at` when the response carried only `expires_in`.
20578
+ * Resolving the expiry at RECEIVE time (not at read time) is what lets
20579
+ * {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
20580
+ * re-resolved later would always compute a full TTL from "now" and never
20581
+ * cross the refresh buffer.
20582
+ */
20583
+ emitCurrentToken(token) {
20584
+ const stamped = token.expires_at ? token : {
20585
+ ...token,
20586
+ expires_at: resolveExpiresAt(token)
20587
+ };
20588
+ this._currentToken$.next(stamped);
20589
+ }
20590
+ /**
20527
20591
  * Returns true when the cached token has enough headroom before expiry to
20528
20592
  * be safely reused on reactivation. The headroom matches the refresh
20529
20593
  * buffer, so a token within the refresh window is treated as stale (the
@@ -20616,7 +20680,7 @@ var DeviceTokenManager = class extends Destroyable {
20616
20680
  const currentToken = this.getCredential().token;
20617
20681
  if (!currentToken) throw new TokenRefreshError("No current token available for refresh");
20618
20682
  const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
20619
- this._currentToken$.next(newTokenData);
20683
+ this.emitCurrentToken(newTokenData);
20620
20684
  } catch (error) {
20621
20685
  logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
20622
20686
  this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
@@ -20643,6 +20707,22 @@ var DeviceTokenManager = class extends Destroyable {
20643
20707
  throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
20644
20708
  }
20645
20709
  /**
20710
+ * Force an immediate refresh when the cached Client Bound SAT is already
20711
+ * past its refresh window. Called on resume from suspension where
20712
+ * background-tab throttling can delay the reactive timer past the buffer.
20713
+ * A no-op when no token is cached or it still has headroom; the normal
20714
+ * {@link executeRefresh} guards (paused / in-progress / unauthenticated)
20715
+ * still apply.
20716
+ */
20717
+ refreshNowIfDue() {
20718
+ const token = this._currentToken$.value;
20719
+ if (!token) return;
20720
+ if (resolveExpiresAt(token) * 1e3 - Date.now() <= DEVICE_TOKEN_REFRESH_BUFFER_MS) {
20721
+ logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
20722
+ this.executeRefresh();
20723
+ }
20724
+ }
20725
+ /**
20646
20726
  * Stops the reactive refresh pipeline from firing. Use when the underlying
20647
20727
  * session is being torn down (e.g., during {@link SignalWire.disconnect})
20648
20728
  * so a scheduled refresh cannot fire against a destroyed session.
@@ -20693,6 +20773,7 @@ var CredentialRefreshCoordinator = class extends Destroyable {
20693
20773
  this.deps = deps;
20694
20774
  this._activating = false;
20695
20775
  this._activationGeneration = 0;
20776
+ this._developerRefreshInProgress = false;
20696
20777
  if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
20697
20778
  }
20698
20779
  /** True when the Client Bound SAT path is available (DPoP initialized). */
@@ -20711,28 +20792,120 @@ var CredentialRefreshCoordinator = class extends Destroyable {
20711
20792
  * invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
20712
20793
  */
20713
20794
  scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
20795
+ this._activeProvider = provider;
20714
20796
  if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
20715
20797
  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);
20716
- this._developerTimerId = setTimeout(async () => {
20798
+ this._developerTimerId = setTimeout(() => {
20799
+ this._developerTimerId = void 0;
20800
+ this.executeDeveloperRefresh(provider, expiresAt, attempt);
20801
+ }, refreshInterval);
20802
+ }
20803
+ /**
20804
+ * Runs the developer-provided refresh once: mints a new credential, stores
20805
+ * and persists it, reauthenticates the live session (via the notifier), and
20806
+ * reschedules against the new expiry. On failure retries with backoff up to
20807
+ * {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
20808
+ *
20809
+ * Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
20810
+ * `_developerRefreshInProgress` guard prevents the two from overlapping.
20811
+ */
20812
+ async executeDeveloperRefresh(provider, expiresAt, attempt) {
20813
+ if (this._developerRefreshInProgress) {
20814
+ logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
20815
+ return;
20816
+ }
20817
+ this._developerRefreshInProgress = true;
20818
+ try {
20819
+ const newCredentials = await this.refreshCredential(provider);
20820
+ this.deps.store.write(newCredentials);
20821
+ this.deps.store.persist(newCredentials);
20717
20822
  try {
20718
- if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
20719
- const newCredentials = await provider.refresh();
20720
- this.deps.store.write(newCredentials);
20721
- this.deps.store.persist(newCredentials);
20722
- logger$6.info("[Coordinator] Credentials refreshed successfully.");
20723
- if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
20724
- } catch (error) {
20725
- const nextAttempt = attempt + 1;
20726
- logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
20727
- this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
20728
- if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
20729
- else {
20730
- logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
20731
- this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
20732
- this.deps.notifier.onRefreshExhausted();
20733
- }
20823
+ await this.deps.notifier.onCredentialRefreshed(newCredentials);
20824
+ } catch (reauthError) {
20825
+ logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
20734
20826
  }
20735
- }, refreshInterval);
20827
+ logger$6.info("[Coordinator] Credentials refreshed successfully.");
20828
+ if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
20829
+ } catch (error) {
20830
+ const nextAttempt = attempt + 1;
20831
+ logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
20832
+ this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
20833
+ if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
20834
+ else {
20835
+ logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
20836
+ this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
20837
+ this.deps.notifier.onRefreshExhausted();
20838
+ }
20839
+ } finally {
20840
+ this._developerRefreshInProgress = false;
20841
+ }
20842
+ }
20843
+ /**
20844
+ * Force an immediate refresh when the current credential is already past its
20845
+ * scheduled refresh window. Called on resume from suspension, where
20846
+ * background-tab timer throttling can delay the armed refresh well past
20847
+ * expiry, leaving the live session stale.
20848
+ *
20849
+ * Routes to whichever mechanism is armed: the developer timer if armed,
20850
+ * otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
20851
+ */
20852
+ forceRefreshIfDue() {
20853
+ if (this._developerTimerId !== void 0 && this._activeProvider) {
20854
+ const expiry = this.deps.store.read().expiry_at;
20855
+ if (expiry !== void 0 && Date.now() >= expiry - CREDENTIAL_REFRESH_BUFFER_MS) {
20856
+ logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
20857
+ clearTimeout(this._developerTimerId);
20858
+ this._developerTimerId = void 0;
20859
+ this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
20860
+ }
20861
+ return;
20862
+ }
20863
+ this._deviceTokenManager?.refreshNowIfDue();
20864
+ }
20865
+ /**
20866
+ * Sync the credential's expiry from the server-provided authorization (the
20867
+ * `signalwire.connect` result). SATs are opaque JWE, so
20868
+ * `fabric_subscriber.expires_at` is the authoritative expiry of the token
20869
+ * the session actually connected with — the provider-reported `expiry_at`
20870
+ * is only a hint (and may be wrong or absent). Corrects the stored
20871
+ * credential and re-arms the developer refresh timer against the real
20872
+ * deadline when the provider supports `refresh()`.
20873
+ */
20874
+ syncExpiryFromAuthorization(authorization, provider) {
20875
+ const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
20876
+ if (!expiresAtSec) return;
20877
+ const expiryAt = expiresAtSec * 1e3;
20878
+ const credential = this.deps.store.read();
20879
+ if (credential.expiry_at === expiryAt) return;
20880
+ logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
20881
+ const updated = {
20882
+ ...credential,
20883
+ expiry_at: expiryAt
20884
+ };
20885
+ this.deps.store.write(updated);
20886
+ this.deps.store.persist(updated);
20887
+ if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
20888
+ }
20889
+ /**
20890
+ * Invoke `provider.refresh()` deduped against any concurrent developer
20891
+ * refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
20892
+ * and the orchestrator's -32003 recovery / reconnect re-mint — share one
20893
+ * in-flight promise, so a provider backed by one-time-use rotating refresh
20894
+ * tokens is never invoked twice in parallel.
20895
+ *
20896
+ * The caller owns applying the returned credential (store write, session
20897
+ * reauth, rescheduling); this method only serializes the network call.
20898
+ */
20899
+ async refreshCredential(provider) {
20900
+ if (this._refreshInFlight) return this._refreshInFlight;
20901
+ if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
20902
+ const run = provider.refresh();
20903
+ this._refreshInFlight = run;
20904
+ const clear = () => {
20905
+ if (this._refreshInFlight === run) this._refreshInFlight = void 0;
20906
+ };
20907
+ run.then(clear, clear);
20908
+ return run;
20736
20909
  }
20737
20910
  /**
20738
20911
  * Cancels any scheduled developer-provided refresh. Idempotent.
@@ -21512,6 +21685,33 @@ var TransportManager = class extends Destroyable {
21512
21685
  }
21513
21686
  };
21514
21687
 
21688
+ //#endregion
21689
+ //#region src/utils/authRecovery.ts
21690
+ /**
21691
+ * Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
21692
+ * Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
21693
+ * underlying signaling error is nested. Bounded by a visited set to guard
21694
+ * against cyclic causes.
21695
+ */
21696
+ function findJSONRPCError(error) {
21697
+ const seen = /* @__PURE__ */ new Set();
21698
+ let current = error;
21699
+ while (current instanceof Error && !seen.has(current)) {
21700
+ seen.add(current);
21701
+ if (current instanceof JSONRPCError) return current;
21702
+ current = current.error ?? current.cause;
21703
+ }
21704
+ }
21705
+ /**
21706
+ * Whether an error is a session-recoverable authentication failure
21707
+ * (`-32002` authentication failed or `-32003` requester validation failed)
21708
+ * that a credential re-mint + retry can heal.
21709
+ */
21710
+ function isRecoverableAuthError(error) {
21711
+ const rpcError = findJSONRPCError(error);
21712
+ return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
21713
+ }
21714
+
21515
21715
  //#endregion
21516
21716
  //#region src/clients/SignalWire.ts
21517
21717
  var import_cjs$1 = require_cjs();
@@ -21627,7 +21827,8 @@ var SignalWire = class extends Destroyable {
21627
21827
  notifier: {
21628
21828
  onError: (error) => this._errors$.next(error),
21629
21829
  onWarning: (warning) => this._warnings$.next(warning),
21630
- onRefreshExhausted: () => void this.disconnect()
21830
+ onRefreshExhausted: () => void this.disconnect(),
21831
+ onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
21631
21832
  },
21632
21833
  store: {
21633
21834
  read: () => this._deps.credential,
@@ -21684,14 +21885,123 @@ var SignalWire = class extends Destroyable {
21684
21885
  }
21685
21886
  this._deps.credential = _credentials;
21686
21887
  this.persistCredential(_credentials);
21687
- if (this.isConnected && this._clientSession.authenticated && _credentials.token) try {
21688
- await this._clientSession.reauthenticate(_credentials.token);
21888
+ await this.reauthenticateLiveSession(_credentials);
21889
+ }
21890
+ /**
21891
+ * Reauthenticate the currently-open session with a freshly obtained
21892
+ * credential so the new token takes effect on the live socket immediately —
21893
+ * not just on the next reconnect. No-op when the session is not
21894
+ * connected/authenticated or the credential carries no token (e.g. an
21895
+ * authorization-state-only refresh). Non-fatal: reauth failures surface on
21896
+ * `errors$` without aborting the refresh that triggered this.
21897
+ */
21898
+ async reauthenticateLiveSession(credential) {
21899
+ if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
21900
+ try {
21901
+ await this._clientSession.reauthenticate(credential.token);
21689
21902
  logger$1.info("[SignalWire] Session refreshed with new credentials.");
21690
21903
  } catch (error) {
21691
21904
  logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
21692
21905
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21693
21906
  }
21694
21907
  }
21908
+ /**
21909
+ * One-shot recovery for a live session that failed with a recoverable auth
21910
+ * error (`-32002`/`-32003`) because its token went stale — e.g. a refresh
21911
+ * timer that was throttled while the tab was backgrounded.
21912
+ *
21913
+ * Two steps, first that succeeds wins:
21914
+ * 1. Reauthenticate with the in-memory token (a fresh DPoP proof is
21915
+ * generated automatically). Heals a session whose token was already
21916
+ * refreshed by the coordinator but never applied to the open socket,
21917
+ * and a client-bound session whose server-side auth merely drifted.
21918
+ * 2. Re-mint via the developer provider's `refresh()` and reauthenticate.
21919
+ * Skipped for client-bound sessions (the DeviceTokenManager owns their
21920
+ * refresh; re-minting a base SAT here would drop the DPoP binding).
21921
+ *
21922
+ * @param allowRemint - Whether step 2 (provider re-mint) may run. Callers
21923
+ * pass `false` for non-auth failures so a transient network error never
21924
+ * escalates to a token re-mint; step 1 (in-memory reauth) always runs.
21925
+ * @returns `true` if the session was reauthenticated, `false` otherwise.
21926
+ */
21927
+ async recoverStaleCredential(allowRemint = true) {
21928
+ const currentToken = this._deps.credential.token;
21929
+ if (currentToken) try {
21930
+ await this._clientSession.reauthenticate(currentToken);
21931
+ return true;
21932
+ } catch (error) {
21933
+ logger$1.debug("[SignalWire] In-memory reauth failed during recovery:", error);
21934
+ }
21935
+ if (!allowRemint || this._clientSession.clientBound || !this._credentialProvider?.refresh) return false;
21936
+ try {
21937
+ const newCredentials = await this.remintCredential(this._credentialProvider);
21938
+ if (!newCredentials.token) return false;
21939
+ this._deps.credential = newCredentials;
21940
+ this.persistCredential(newCredentials);
21941
+ await this._clientSession.reauthenticate(newCredentials.token);
21942
+ if (newCredentials.expiry_at) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
21943
+ return true;
21944
+ } catch (error) {
21945
+ logger$1.error("[SignalWire] Credential recovery failed:", error);
21946
+ return false;
21947
+ }
21948
+ }
21949
+ /**
21950
+ * Re-mint a credential via `provider.refresh()`, routed through the
21951
+ * coordinator's shared in-flight guard so concurrent re-mint paths (a
21952
+ * scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
21953
+ * second `provider.refresh()` in parallel — which rotating one-time-use
21954
+ * refresh tokens reject. Falls back to a direct call only if the coordinator
21955
+ * has not been constructed yet.
21956
+ */
21957
+ async remintCredential(provider) {
21958
+ if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
21959
+ if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
21960
+ return provider.refresh();
21961
+ }
21962
+ /**
21963
+ * Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
21964
+ * The session invokes this only when it is client-bound OR the in-memory
21965
+ * token is expired. The re-mint mechanism depends on the binding:
21966
+ * - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
21967
+ * fresh base SAT the upcoming reconnect can re-bind (the
21968
+ * DeviceTokenManager re-activates afterwards).
21969
+ * - Unbound: the developer's non-interactive `refresh()` handler.
21970
+ * `authenticate()` is deliberately NOT used here — it may be interactive
21971
+ * (a login prompt) and must not fire on a background reconnect.
21972
+ *
21973
+ * Rejects on failure so the session aborts the reconnect rather than
21974
+ * replaying a stale token.
21975
+ */
21976
+ async refreshCredentialForReconnect() {
21977
+ if (!this._credentialProvider) return;
21978
+ try {
21979
+ let newCredentials;
21980
+ if (this._clientSession.clientBound) {
21981
+ const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
21982
+ logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
21983
+ newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
21984
+ } else if (this._credentialProvider.refresh) {
21985
+ logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
21986
+ newCredentials = await this.remintCredential(this._credentialProvider);
21987
+ } else {
21988
+ logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
21989
+ return;
21990
+ }
21991
+ if (!newCredentials.token) {
21992
+ logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
21993
+ return;
21994
+ }
21995
+ this._deps.credential = newCredentials;
21996
+ this.persistCredential(newCredentials);
21997
+ if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
21998
+ logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
21999
+ } catch (error) {
22000
+ logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
22001
+ this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
22002
+ throw error;
22003
+ }
22004
+ }
21695
22005
  /** Persist credential to localStorage when persistSession is enabled. */
21696
22006
  persistCredential(credential) {
21697
22007
  if (!credential.token) return;
@@ -21778,24 +22088,13 @@ var SignalWire = class extends Destroyable {
21778
22088
  this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
21779
22089
  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$);
21780
22090
  this._publicSession = new ClientSessionWrapper(this._clientSession);
21781
- this._clientSession.onBeforeReconnect = async () => {
21782
- if (!this._credentialProvider) return;
21783
- try {
21784
- const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
21785
- logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
21786
- const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
21787
- this._deps.credential = newCredentials;
21788
- if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
21789
- logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
21790
- } catch (error) {
21791
- logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
21792
- this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21793
- throw error;
21794
- }
21795
- };
22091
+ this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
21796
22092
  this.subscribeTo(this._clientSession.errors$, (error) => {
21797
22093
  this._errors$.next(error);
21798
22094
  });
22095
+ this.subscribeTo(this._clientSession.authorization$, (authorization) => {
22096
+ this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
22097
+ });
21799
22098
  await this._clientSession.connect();
21800
22099
  await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
21801
22100
  this.subscribeTo(this._clientSession.authenticated$.pipe((0, import_cjs$1.skip)(1), (0, import_cjs$1.filter)(Boolean)), async () => {
@@ -21958,6 +22257,13 @@ var SignalWire = class extends Destroyable {
21958
22257
  }
21959
22258
  try {
21960
22259
  this._visibilityController = new VisibilityController();
22260
+ this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible")), () => {
22261
+ try {
22262
+ this._refreshCoordinator?.forceRefreshIfDue();
22263
+ } catch (error) {
22264
+ logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
22265
+ }
22266
+ });
21961
22267
  this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
21962
22268
  logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
21963
22269
  try {
@@ -21969,7 +22275,7 @@ var SignalWire = class extends Destroyable {
21969
22275
  logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
21970
22276
  }
21971
22277
  try {
21972
- this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.1" });
22278
+ this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.2" });
21973
22279
  } catch (error) {
21974
22280
  logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
21975
22281
  }
@@ -22040,21 +22346,23 @@ var SignalWire = class extends Destroyable {
22040
22346
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
22041
22347
  throw error;
22042
22348
  }
22043
- logger$1.debug("[SignalWire] Failed to register user, trying reauthentication...");
22044
- try {
22045
- await this._clientSession.reauthenticate(this._deps.credential.token);
22046
- logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
22349
+ logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
22350
+ let failureCause = error;
22351
+ if (await this.recoverStaleCredential(isRecoverableAuthError(error))) try {
22352
+ logger$1.debug("[SignalWire] Recovery succeeded, retrying register()");
22047
22353
  await this._transport.execute(RPCExecute({
22048
22354
  method: "subscriber.online",
22049
22355
  params: {}
22050
22356
  }));
22051
22357
  this._isRegistered$.next(true);
22052
- } catch (reauthError) {
22053
- logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
22054
- 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 }) });
22055
- this._errors$.next(registerError);
22056
- throw registerError;
22358
+ return;
22359
+ } catch (retryError) {
22360
+ logger$1.error("[SignalWire] Register retry after recovery failed:", retryError);
22361
+ failureCause = retryError;
22057
22362
  }
22363
+ 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 }) });
22364
+ this._errors$.next(registerError);
22365
+ throw registerError;
22058
22366
  }
22059
22367
  }
22060
22368
  /**
@@ -22107,7 +22415,15 @@ var SignalWire = class extends Destroyable {
22107
22415
  };
22108
22416
  await this.waitAuthentication();
22109
22417
  logger$1.debug("[SignalWire] Dialing with options:", computed_options);
22110
- return this._clientSession.createOutboundCall(destination, computed_options);
22418
+ try {
22419
+ return await this._clientSession.createOutboundCall(destination, computed_options);
22420
+ } catch (error) {
22421
+ if (!isRecoverableAuthError(error)) throw error;
22422
+ logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering credential and retrying once");
22423
+ if (!await this.recoverStaleCredential()) throw error;
22424
+ await this.waitAuthentication();
22425
+ return this._clientSession.createOutboundCall(destination, computed_options);
22426
+ }
22111
22427
  }
22112
22428
  /**
22113
22429
  * Runs a multi-phase connectivity test against the given destination.
@@ -22504,7 +22820,7 @@ var StaticCredentialProvider = class {
22504
22820
  /**
22505
22821
  * Library version from package.json, injected at build time.
22506
22822
  */
22507
- const version = "4.0.0-rc.1";
22823
+ const version = "4.0.0-rc.2";
22508
22824
  /**
22509
22825
  * Flag indicating the library has been loaded and is ready to use.
22510
22826
  * For UMD builds: `window.SignalWire.ready`
@@ -22526,7 +22842,7 @@ const ready = true;
22526
22842
  */
22527
22843
  const emitReadyEvent = () => {
22528
22844
  if (typeof window !== "undefined") {
22529
- const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.1" } });
22845
+ const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.2" } });
22530
22846
  window.dispatchEvent(event);
22531
22847
  }
22532
22848
  };
@@ -22550,5 +22866,5 @@ emitReadyEvent();
22550
22866
  if (typeof process === "undefined") globalThis.process = { env: { NODE_ENV: "production" } };
22551
22867
 
22552
22868
  //#endregion
22553
- export { Address, CallCreateError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaAccessError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, PreflightError, RecoveryError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
22869
+ export { Address, CallCreateError, CallNotReadyError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaAccessError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, ParticipantNotReadyError, PreflightError, RecoveryError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
22554
22870
  //# sourceMappingURL=browser.mjs.map