@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 +454 -138
- package/dist/browser.mjs.map +1 -1
- package/dist/browser.umd.js +455 -137
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +434 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -13
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +117 -13
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +433 -139
- package/dist/index.mjs.map +1 -1
- package/dist/operators/index.cjs +1 -1
- package/dist/operators/index.mjs +1 -1
- package/dist/{operators-B8ipd4Xl.cjs → operators-Bn4Ij3VB.cjs} +41 -1
- package/dist/operators-Bn4Ij3VB.cjs.map +1 -0
- package/dist/{operators-BlUtq-t0.mjs → operators-Zxmwpb0j.mjs} +24 -2
- package/dist/operators-Zxmwpb0j.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/operators-B8ipd4Xl.cjs.map +0 -1
- package/dist/operators-BlUtq-t0.mjs.map +0 -1
package/dist/browser.umd.js
CHANGED
|
@@ -9055,6 +9055,14 @@ const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
|
|
|
9055
9055
|
/** Buffer in milliseconds before token expiry to trigger refresh. */
|
|
9056
9056
|
const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
|
|
9057
9057
|
/**
|
|
9058
|
+
* Clock-skew allowance (ms) for treating an in-memory credential as expired
|
|
9059
|
+
* when deciding whether a fresh (re)connect must re-mint the token before
|
|
9060
|
+
* authenticating. A token within this window of its `expiry_at` is treated as
|
|
9061
|
+
* stale so the reconnect re-mints via the credential provider instead of
|
|
9062
|
+
* replaying a dead token (which the server rejects with -32003).
|
|
9063
|
+
*/
|
|
9064
|
+
const CREDENTIAL_EXPIRY_SKEW_MS = 3e4;
|
|
9065
|
+
/**
|
|
9058
9066
|
* Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
|
|
9059
9067
|
* to resolve before treating the activation as failed and falling back to
|
|
9060
9068
|
* the developer-provided refresh path. Prevents a wedged HTTP layer from
|
|
@@ -9287,6 +9295,20 @@ var CallCreateError = class extends Error {
|
|
|
9287
9295
|
this.name = "CallCreateError";
|
|
9288
9296
|
}
|
|
9289
9297
|
};
|
|
9298
|
+
var CallNotReadyError = class extends Error {
|
|
9299
|
+
constructor(callId, options) {
|
|
9300
|
+
super(`Call "${callId}" has no self member context yet: selfId/nodeId have not been received from the server`, options);
|
|
9301
|
+
this.callId = callId;
|
|
9302
|
+
this.name = "CallNotReadyError";
|
|
9303
|
+
}
|
|
9304
|
+
};
|
|
9305
|
+
var ParticipantNotReadyError = class extends Error {
|
|
9306
|
+
constructor(memberId, options) {
|
|
9307
|
+
super(`Participant "${memberId}" has no call context yet: its member state (call_id/node_id) has not been received from the server`, options);
|
|
9308
|
+
this.memberId = memberId;
|
|
9309
|
+
this.name = "ParticipantNotReadyError";
|
|
9310
|
+
}
|
|
9311
|
+
};
|
|
9290
9312
|
var JSONRPCError = class extends Error {
|
|
9291
9313
|
constructor(code, message, data, options, requestId) {
|
|
9292
9314
|
super(message, options);
|
|
@@ -13650,9 +13672,9 @@ const initialState = {};
|
|
|
13650
13672
|
* the local participant with additional device control.
|
|
13651
13673
|
*/
|
|
13652
13674
|
var Participant = class extends Destroyable {
|
|
13653
|
-
constructor(id,
|
|
13675
|
+
constructor(id, callExecuteMethod, deviceController) {
|
|
13654
13676
|
super();
|
|
13655
|
-
this.
|
|
13677
|
+
this.callExecuteMethod = callExecuteMethod;
|
|
13656
13678
|
this.deviceController = deviceController;
|
|
13657
13679
|
this._state$ = this.createBehaviorSubject(initialState);
|
|
13658
13680
|
this.id = id;
|
|
@@ -13874,22 +13896,55 @@ var Participant = class extends Destroyable {
|
|
|
13874
13896
|
get value() {
|
|
13875
13897
|
return this._state$.value;
|
|
13876
13898
|
}
|
|
13899
|
+
/**
|
|
13900
|
+
* Target triple for member RPCs, built from the participant's own state.
|
|
13901
|
+
* The backend locates the member's session by the target `call_id`/`node_id`,
|
|
13902
|
+
* so this must always be the participant's own call context — never the
|
|
13903
|
+
* local call's id (issue #19400).
|
|
13904
|
+
*
|
|
13905
|
+
* Reading it doubles as a readiness probe: it throws until the first full
|
|
13906
|
+
* member event (`member.joined`/`member.updated` or the `call.joined`
|
|
13907
|
+
* roster) arrives, and never regresses afterwards.
|
|
13908
|
+
*
|
|
13909
|
+
* @throws {ParticipantNotReadyError} If the member state has not been
|
|
13910
|
+
* received yet (e.g. a participant first seen via `member.talking`) — an
|
|
13911
|
+
* empty call context can never address the member, so fail fast instead of
|
|
13912
|
+
* sending a doomed RPC.
|
|
13913
|
+
*/
|
|
13914
|
+
get target() {
|
|
13915
|
+
const { call_id, node_id } = this._state$.value;
|
|
13916
|
+
if (!call_id || !node_id) throw new ParticipantNotReadyError(this.id);
|
|
13917
|
+
return {
|
|
13918
|
+
member_id: this.id,
|
|
13919
|
+
call_id,
|
|
13920
|
+
node_id
|
|
13921
|
+
};
|
|
13922
|
+
}
|
|
13923
|
+
/**
|
|
13924
|
+
* Executes a member RPC against this participant, injecting its own
|
|
13925
|
+
* {@link target} as the target.
|
|
13926
|
+
*
|
|
13927
|
+
* @throws {ParticipantNotReadyError} Via {@link target}, when the
|
|
13928
|
+
* member state has not been received yet.
|
|
13929
|
+
*/
|
|
13930
|
+
async executeMethod(method, args) {
|
|
13931
|
+
return this.callExecuteMethod(this.target, method, args);
|
|
13932
|
+
}
|
|
13877
13933
|
/** Toggles the deafened state (mutes/unmutes incoming audio). */
|
|
13878
13934
|
async toggleDeaf() {
|
|
13879
|
-
|
|
13880
|
-
await this.executeMethod(this.id, method, {});
|
|
13935
|
+
await this.executeMethod(toggleDeafMethod(this.deaf), {});
|
|
13881
13936
|
}
|
|
13882
13937
|
/** Toggles the hand-raised state. */
|
|
13883
13938
|
async toggleHandraise() {
|
|
13884
|
-
await this.executeMethod(
|
|
13939
|
+
await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
|
|
13885
13940
|
}
|
|
13886
13941
|
/** Mutes the participant's audio. */
|
|
13887
13942
|
async mute() {
|
|
13888
|
-
await this.executeMethod(
|
|
13943
|
+
await this.executeMethod("call.mute", { channels: ["audio"] });
|
|
13889
13944
|
}
|
|
13890
13945
|
/** Unmutes the participant's audio. */
|
|
13891
13946
|
async unmute() {
|
|
13892
|
-
await this.executeMethod(
|
|
13947
|
+
await this.executeMethod("call.unmute", { channels: ["audio"] });
|
|
13893
13948
|
}
|
|
13894
13949
|
/** Toggles the participant's audio mute state. */
|
|
13895
13950
|
async toggleMute() {
|
|
@@ -13897,11 +13952,11 @@ var Participant = class extends Destroyable {
|
|
|
13897
13952
|
}
|
|
13898
13953
|
/** Mutes the participant's video. */
|
|
13899
13954
|
async muteVideo() {
|
|
13900
|
-
await this.executeMethod(
|
|
13955
|
+
await this.executeMethod("call.mute", { channels: ["video"] });
|
|
13901
13956
|
}
|
|
13902
13957
|
/** Unmutes the participant's video. */
|
|
13903
13958
|
async unmuteVideo() {
|
|
13904
|
-
await this.executeMethod(
|
|
13959
|
+
await this.executeMethod("call.unmute", { channels: ["video"] });
|
|
13905
13960
|
}
|
|
13906
13961
|
/** Toggles the participant's video mute state. */
|
|
13907
13962
|
async toggleMuteVideo() {
|
|
@@ -13909,7 +13964,7 @@ var Participant = class extends Destroyable {
|
|
|
13909
13964
|
}
|
|
13910
13965
|
/** Toggles echo cancellation on the audio input. */
|
|
13911
13966
|
async toggleEchoCancellation() {
|
|
13912
|
-
await this.executeMethod(
|
|
13967
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13913
13968
|
echo_cancellation: !this.echoCancellation,
|
|
13914
13969
|
auto_gain: this.autoGain,
|
|
13915
13970
|
noise_suppression: this.noiseSuppression
|
|
@@ -13917,7 +13972,7 @@ var Participant = class extends Destroyable {
|
|
|
13917
13972
|
}
|
|
13918
13973
|
/** Toggles automatic gain control on the audio input. */
|
|
13919
13974
|
async toggleAudioInputAutoGain() {
|
|
13920
|
-
await this.executeMethod(
|
|
13975
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13921
13976
|
echo_cancellation: this.echoCancellation,
|
|
13922
13977
|
auto_gain: !this.autoGain,
|
|
13923
13978
|
noise_suppression: this.noiseSuppression
|
|
@@ -13925,7 +13980,7 @@ var Participant = class extends Destroyable {
|
|
|
13925
13980
|
}
|
|
13926
13981
|
/** Toggles noise suppression on the audio input. */
|
|
13927
13982
|
async toggleNoiseSuppression() {
|
|
13928
|
-
await this.executeMethod(
|
|
13983
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13929
13984
|
echo_cancellation: this.echoCancellation,
|
|
13930
13985
|
auto_gain: this.autoGain,
|
|
13931
13986
|
noise_suppression: !this.noiseSuppression
|
|
@@ -13933,7 +13988,7 @@ var Participant = class extends Destroyable {
|
|
|
13933
13988
|
}
|
|
13934
13989
|
/** Toggles low-bitrate mode for this participant's media. */
|
|
13935
13990
|
async toggleLowbitrate() {
|
|
13936
|
-
await this.executeMethod(
|
|
13991
|
+
await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
|
|
13937
13992
|
}
|
|
13938
13993
|
/**
|
|
13939
13994
|
* Adjusts the **conference-only** microphone energy gate / sensitivity level
|
|
@@ -13949,7 +14004,7 @@ var Participant = class extends Destroyable {
|
|
|
13949
14004
|
* (integer, larger values are more sensitive).
|
|
13950
14005
|
*/
|
|
13951
14006
|
async setAudioInputSensitivity(value) {
|
|
13952
|
-
await this.executeMethod(
|
|
14007
|
+
await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
|
|
13953
14008
|
}
|
|
13954
14009
|
/**
|
|
13955
14010
|
* Sets the **server-side** microphone volume on this participant's bridged
|
|
@@ -13962,7 +14017,7 @@ var Participant = class extends Destroyable {
|
|
|
13962
14017
|
* @param value - Volume level (0-100).
|
|
13963
14018
|
*/
|
|
13964
14019
|
async setAudioInputVolume(value) {
|
|
13965
|
-
await this.executeMethod(
|
|
14020
|
+
await this.executeMethod("call.microphone.volume.set", { volume: value });
|
|
13966
14021
|
}
|
|
13967
14022
|
/**
|
|
13968
14023
|
* Sets the **server-side** speaker volume on this participant's bridged call
|
|
@@ -13976,45 +14031,31 @@ var Participant = class extends Destroyable {
|
|
|
13976
14031
|
* @param value - Volume level (0-100).
|
|
13977
14032
|
*/
|
|
13978
14033
|
async setAudioOutputVolume(value) {
|
|
13979
|
-
await this.executeMethod(
|
|
14034
|
+
await this.executeMethod("call.speaker.volume.set", { volume: value });
|
|
13980
14035
|
}
|
|
13981
14036
|
/**
|
|
13982
14037
|
* Sets the participant's position in the video layout.
|
|
13983
14038
|
*
|
|
13984
|
-
* Requires the `member.position` capability. The gateway
|
|
13985
|
-
*
|
|
13986
|
-
*
|
|
13987
|
-
*
|
|
13988
|
-
* guarantee a visible change: the backend silently returns `200` (no-op) for
|
|
13989
|
-
* non-conference targets.
|
|
14039
|
+
* Requires the `member.position` capability. The gateway requires a
|
|
14040
|
+
* `targets` array of `{ target, position }` entries (issue #19400). A
|
|
14041
|
+
* resolved promise does not guarantee a visible change: the backend silently
|
|
14042
|
+
* returns `200` (no-op) for non-conference targets.
|
|
13990
14043
|
*
|
|
13991
14044
|
* @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
|
|
13992
14045
|
*/
|
|
13993
14046
|
async setPosition(value) {
|
|
13994
|
-
|
|
13995
|
-
|
|
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,
|
|
14047
|
+
await this.executeMethod("call.member.position.set", { targets: [{
|
|
14048
|
+
target: this.target,
|
|
14002
14049
|
position: value
|
|
14003
14050
|
}] });
|
|
14004
14051
|
}
|
|
14005
14052
|
/** Removes this participant from the call. */
|
|
14006
14053
|
async remove() {
|
|
14007
|
-
|
|
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", {});
|
|
14054
|
+
await this.executeMethod("call.member.remove", { targets: [this.target] });
|
|
14014
14055
|
}
|
|
14015
14056
|
/** Ends the call for this participant. */
|
|
14016
14057
|
async end() {
|
|
14017
|
-
await this.executeMethod(
|
|
14058
|
+
await this.executeMethod("call.end", {});
|
|
14018
14059
|
}
|
|
14019
14060
|
/**
|
|
14020
14061
|
* Replaces custom metadata for this participant.
|
|
@@ -14034,7 +14075,7 @@ var Participant = class extends Destroyable {
|
|
|
14034
14075
|
}
|
|
14035
14076
|
/** Destroys the participant, releasing all subscriptions and references. */
|
|
14036
14077
|
destroy() {
|
|
14037
|
-
this.
|
|
14078
|
+
this.callExecuteMethod = void 0;
|
|
14038
14079
|
super.destroy();
|
|
14039
14080
|
}
|
|
14040
14081
|
};
|
|
@@ -14046,8 +14087,8 @@ var Participant = class extends Destroyable {
|
|
|
14046
14087
|
*/
|
|
14047
14088
|
var SelfParticipant = class extends Participant {
|
|
14048
14089
|
/** @internal */
|
|
14049
|
-
constructor(id,
|
|
14050
|
-
super(id,
|
|
14090
|
+
constructor(id, callExecuteMethod, vertoManager, deviceController) {
|
|
14091
|
+
super(id, callExecuteMethod, deviceController);
|
|
14051
14092
|
this.vertoManager = vertoManager;
|
|
14052
14093
|
this._studioAudio$ = this.createBehaviorSubject(false);
|
|
14053
14094
|
this.capabilities = new SelfCapabilities();
|
|
@@ -14071,7 +14112,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14071
14112
|
async enableStudioAudio() {
|
|
14072
14113
|
if (this._studioAudio$.value) return;
|
|
14073
14114
|
this._studioAudio$.next(true);
|
|
14074
|
-
await this.executeMethod(
|
|
14115
|
+
await this.executeMethod("call.audioflags.set", {
|
|
14075
14116
|
echo_cancellation: false,
|
|
14076
14117
|
auto_gain: false,
|
|
14077
14118
|
noise_suppression: false
|
|
@@ -14084,7 +14125,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14084
14125
|
async disableStudioAudio() {
|
|
14085
14126
|
if (!this._studioAudio$.value) return;
|
|
14086
14127
|
this._studioAudio$.next(false);
|
|
14087
|
-
await this.executeMethod(
|
|
14128
|
+
await this.executeMethod("call.audioflags.set", {
|
|
14088
14129
|
echo_cancellation: true,
|
|
14089
14130
|
auto_gain: true,
|
|
14090
14131
|
noise_suppression: true
|
|
@@ -18305,12 +18346,6 @@ function mosToQualityLevel(mos) {
|
|
|
18305
18346
|
var import_cjs$11 = require_cjs();
|
|
18306
18347
|
const logger$12 = getLogger();
|
|
18307
18348
|
/**
|
|
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
18349
|
* Ratio between the critical and warning RTT spike multipliers.
|
|
18315
18350
|
* Warning threshold = baseline * warningMultiplier (default 3x)
|
|
18316
18351
|
* Critical threshold = baseline * warningMultiplier * RTT_CRITICAL_TO_WARNING_RATIO
|
|
@@ -18461,7 +18496,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18461
18496
|
/** Toggles the call lock state, preventing or allowing new participants from joining. */
|
|
18462
18497
|
async toggleLock() {
|
|
18463
18498
|
const method = this.locked ? "call.unlock" : "call.lock";
|
|
18464
|
-
await this.executeMethod(this.
|
|
18499
|
+
await this.executeMethod(this.callSelf, method, {});
|
|
18465
18500
|
}
|
|
18466
18501
|
/**
|
|
18467
18502
|
* Toggles the hold state of the call (pauses/resumes local media transmission).
|
|
@@ -18510,14 +18545,24 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18510
18545
|
*
|
|
18511
18546
|
* Constructs call context (node_id, call_id, member_id) and sends the RPC request.
|
|
18512
18547
|
*
|
|
18513
|
-
* @param target - Target
|
|
18548
|
+
* @param target - Target {@link MemberTarget} triple, or the local member's
|
|
18549
|
+
* ID string for self-operations (any other string is rejected — a bare
|
|
18550
|
+
* member id cannot carry the remote member's own call context).
|
|
18514
18551
|
* @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
|
|
18515
18552
|
* @param args - Parameters for the RPC method.
|
|
18516
18553
|
* @returns The RPC response.
|
|
18554
|
+
* @throws {CallNotReadyError} If the call has no self member context yet.
|
|
18555
|
+
* @throws {InvalidParams} If a string target is not the local member's ID.
|
|
18517
18556
|
* @throws {JSONRPCError} If the RPC call returns an error.
|
|
18518
18557
|
*/
|
|
18519
18558
|
async executeMethod(target, method, args) {
|
|
18520
|
-
const
|
|
18559
|
+
const self = this.callSelf;
|
|
18560
|
+
if (typeof target === "string" && target !== self.member_id) throw new InvalidParams(`Target member ID ${target} does not match call's self member ID ${self.member_id}`);
|
|
18561
|
+
const params = {
|
|
18562
|
+
...args,
|
|
18563
|
+
self,
|
|
18564
|
+
target: typeof target === "string" ? self : target
|
|
18565
|
+
};
|
|
18521
18566
|
const request = buildRPCRequest({
|
|
18522
18567
|
method,
|
|
18523
18568
|
params
|
|
@@ -18531,29 +18576,23 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18531
18576
|
throw error;
|
|
18532
18577
|
}
|
|
18533
18578
|
}
|
|
18534
|
-
|
|
18535
|
-
|
|
18536
|
-
|
|
18537
|
-
|
|
18538
|
-
|
|
18539
|
-
|
|
18540
|
-
|
|
18541
|
-
|
|
18542
|
-
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
18547
|
-
targets: [target]
|
|
18548
|
-
};
|
|
18579
|
+
/**
|
|
18580
|
+
* The local leg's member triple — sent as `self` in every member RPC
|
|
18581
|
+
* envelope, and as the `target` of call-scoped self-operations (e.g. lock,
|
|
18582
|
+
* layout).
|
|
18583
|
+
*
|
|
18584
|
+
* @throws {CallNotReadyError} Before `call.joined` delivers the self member
|
|
18585
|
+
* context (`selfId`/`nodeId`) — an RPC without it cannot be routed, so fail
|
|
18586
|
+
* fast instead of sending a doomed request.
|
|
18587
|
+
*/
|
|
18588
|
+
get callSelf() {
|
|
18589
|
+
const node_id = this.nodeId;
|
|
18590
|
+
const member_id = this.vertoManager.selfId;
|
|
18591
|
+
if (!node_id || !member_id) throw new CallNotReadyError(this.id);
|
|
18549
18592
|
return {
|
|
18550
|
-
|
|
18551
|
-
|
|
18552
|
-
|
|
18553
|
-
node_id: this.nodeId ?? "",
|
|
18554
|
-
call_id: this.id,
|
|
18555
|
-
member_id: target
|
|
18556
|
-
}
|
|
18593
|
+
node_id,
|
|
18594
|
+
call_id: this.id,
|
|
18595
|
+
member_id
|
|
18557
18596
|
};
|
|
18558
18597
|
}
|
|
18559
18598
|
/** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
|
|
@@ -19116,12 +19155,17 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19116
19155
|
*
|
|
19117
19156
|
* **These operations are NOT atomic.** The layout is applied first, then each
|
|
19118
19157
|
* member position sequentially, so members may briefly flash into their
|
|
19119
|
-
* default slots before being moved to the requested positions.
|
|
19158
|
+
* default slots before being moved to the requested positions. Targeted
|
|
19159
|
+
* members are validated upfront, though: when any of them has no
|
|
19160
|
+
* {@link Participant.target | member call context} yet, the whole call
|
|
19161
|
+
* rejects before any request is sent and the layout is left unchanged.
|
|
19120
19162
|
*
|
|
19121
19163
|
* @param layout - Layout name (must be one of {@link layouts}).
|
|
19122
19164
|
* @param positions - Optional map of member IDs to {@link VideoPosition} values.
|
|
19123
19165
|
* When omitted or empty, only the layout is changed.
|
|
19124
19166
|
* @throws {InvalidParams} If the layout is not in the available {@link layouts}.
|
|
19167
|
+
* @throws {ParticipantNotReadyError} If a targeted member's call context has
|
|
19168
|
+
* not been received yet — thrown before any request is sent.
|
|
19125
19169
|
*
|
|
19126
19170
|
* @example
|
|
19127
19171
|
* ```ts
|
|
@@ -19132,18 +19176,19 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19132
19176
|
*/
|
|
19133
19177
|
async setLayout(layout, positions) {
|
|
19134
19178
|
if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
|
|
19135
|
-
const
|
|
19136
|
-
|
|
19137
|
-
const positionEntries = Object.entries(positions ?? {});
|
|
19138
|
-
if (positionEntries.length === 0) return;
|
|
19139
|
-
for (const [memberId, position] of positionEntries) {
|
|
19179
|
+
const targets = [];
|
|
19180
|
+
for (const [memberId, position] of Object.entries(positions ?? {})) {
|
|
19140
19181
|
const participant = this.participants.find((p) => p.id === memberId);
|
|
19141
19182
|
if (!participant) {
|
|
19142
19183
|
logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
|
|
19143
19184
|
continue;
|
|
19144
19185
|
}
|
|
19145
|
-
|
|
19186
|
+
participant.target;
|
|
19187
|
+
targets.push([participant, position]);
|
|
19146
19188
|
}
|
|
19189
|
+
const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
|
|
19190
|
+
await this.executeMethod(selfId, "call.layout.set", { layout });
|
|
19191
|
+
for (const [participant, position] of targets) await participant.setPosition(position);
|
|
19147
19192
|
}
|
|
19148
19193
|
/**
|
|
19149
19194
|
* Transfers the call to another destination.
|
|
@@ -20062,9 +20107,9 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20062
20107
|
}
|
|
20063
20108
|
async handleAuthenticationError(error) {
|
|
20064
20109
|
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);
|
|
20110
|
+
const isRecoverableAuthError$1 = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
20066
20111
|
const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
|
|
20067
|
-
if (isRecoverableAuthError && hasStoredState) {
|
|
20112
|
+
if (isRecoverableAuthError$1 && hasStoredState) {
|
|
20068
20113
|
logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
|
|
20069
20114
|
try {
|
|
20070
20115
|
await this.cleanupStoredConnectionParams();
|
|
@@ -20160,9 +20205,13 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20160
20205
|
const isReconnect = hasReconnectState && storedToken;
|
|
20161
20206
|
let dpopToken;
|
|
20162
20207
|
if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
|
|
20163
|
-
else
|
|
20164
|
-
|
|
20165
|
-
|
|
20208
|
+
else {
|
|
20209
|
+
const credential = this.getCredential();
|
|
20210
|
+
const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + CREDENTIAL_EXPIRY_SKEW_MS;
|
|
20211
|
+
if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
|
|
20212
|
+
logger$9.debug("[Session] Refreshing credentials before fresh connect");
|
|
20213
|
+
await this.onBeforeReconnect();
|
|
20214
|
+
}
|
|
20166
20215
|
}
|
|
20167
20216
|
if ((!isReconnect || this.clientBound) && this.dpopManager?.initialized) try {
|
|
20168
20217
|
dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
|
|
@@ -20518,7 +20567,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20518
20567
|
await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
|
|
20519
20568
|
updateCredential({ token: tokenData.token });
|
|
20520
20569
|
logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
|
|
20521
|
-
this.
|
|
20570
|
+
this.emitCurrentToken(tokenData);
|
|
20522
20571
|
return { activated: true };
|
|
20523
20572
|
} catch (error) {
|
|
20524
20573
|
logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
|
|
@@ -20530,6 +20579,21 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20530
20579
|
}
|
|
20531
20580
|
}
|
|
20532
20581
|
/**
|
|
20582
|
+
* Emit a freshly received token to the reactive pipeline, stamping an
|
|
20583
|
+
* absolute `expires_at` when the response carried only `expires_in`.
|
|
20584
|
+
* Resolving the expiry at RECEIVE time (not at read time) is what lets
|
|
20585
|
+
* {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
|
|
20586
|
+
* re-resolved later would always compute a full TTL from "now" and never
|
|
20587
|
+
* cross the refresh buffer.
|
|
20588
|
+
*/
|
|
20589
|
+
emitCurrentToken(token) {
|
|
20590
|
+
const stamped = token.expires_at ? token : {
|
|
20591
|
+
...token,
|
|
20592
|
+
expires_at: resolveExpiresAt(token)
|
|
20593
|
+
};
|
|
20594
|
+
this._currentToken$.next(stamped);
|
|
20595
|
+
}
|
|
20596
|
+
/**
|
|
20533
20597
|
* Returns true when the cached token has enough headroom before expiry to
|
|
20534
20598
|
* be safely reused on reactivation. The headroom matches the refresh
|
|
20535
20599
|
* buffer, so a token within the refresh window is treated as stale (the
|
|
@@ -20622,7 +20686,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20622
20686
|
const currentToken = this.getCredential().token;
|
|
20623
20687
|
if (!currentToken) throw new TokenRefreshError("No current token available for refresh");
|
|
20624
20688
|
const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
|
|
20625
|
-
this.
|
|
20689
|
+
this.emitCurrentToken(newTokenData);
|
|
20626
20690
|
} catch (error) {
|
|
20627
20691
|
logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
|
|
20628
20692
|
this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
|
|
@@ -20649,6 +20713,22 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20649
20713
|
throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
|
|
20650
20714
|
}
|
|
20651
20715
|
/**
|
|
20716
|
+
* Force an immediate refresh when the cached Client Bound SAT is already
|
|
20717
|
+
* past its refresh window. Called on resume from suspension where
|
|
20718
|
+
* background-tab throttling can delay the reactive timer past the buffer.
|
|
20719
|
+
* A no-op when no token is cached or it still has headroom; the normal
|
|
20720
|
+
* {@link executeRefresh} guards (paused / in-progress / unauthenticated)
|
|
20721
|
+
* still apply.
|
|
20722
|
+
*/
|
|
20723
|
+
refreshNowIfDue() {
|
|
20724
|
+
const token = this._currentToken$.value;
|
|
20725
|
+
if (!token) return;
|
|
20726
|
+
if (resolveExpiresAt(token) * 1e3 - Date.now() <= DEVICE_TOKEN_REFRESH_BUFFER_MS) {
|
|
20727
|
+
logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
|
|
20728
|
+
this.executeRefresh();
|
|
20729
|
+
}
|
|
20730
|
+
}
|
|
20731
|
+
/**
|
|
20652
20732
|
* Stops the reactive refresh pipeline from firing. Use when the underlying
|
|
20653
20733
|
* session is being torn down (e.g., during {@link SignalWire.disconnect})
|
|
20654
20734
|
* so a scheduled refresh cannot fire against a destroyed session.
|
|
@@ -20699,6 +20779,7 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20699
20779
|
this.deps = deps;
|
|
20700
20780
|
this._activating = false;
|
|
20701
20781
|
this._activationGeneration = 0;
|
|
20782
|
+
this._developerRefreshInProgress = false;
|
|
20702
20783
|
if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
|
|
20703
20784
|
}
|
|
20704
20785
|
/** True when the Client Bound SAT path is available (DPoP initialized). */
|
|
@@ -20717,28 +20798,120 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20717
20798
|
* invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
|
|
20718
20799
|
*/
|
|
20719
20800
|
scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
|
|
20801
|
+
this._activeProvider = provider;
|
|
20720
20802
|
if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
|
|
20721
20803
|
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(
|
|
20804
|
+
this._developerTimerId = setTimeout(() => {
|
|
20805
|
+
this._developerTimerId = void 0;
|
|
20806
|
+
this.executeDeveloperRefresh(provider, expiresAt, attempt);
|
|
20807
|
+
}, refreshInterval);
|
|
20808
|
+
}
|
|
20809
|
+
/**
|
|
20810
|
+
* Runs the developer-provided refresh once: mints a new credential, stores
|
|
20811
|
+
* and persists it, reauthenticates the live session (via the notifier), and
|
|
20812
|
+
* reschedules against the new expiry. On failure retries with backoff up to
|
|
20813
|
+
* {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
|
|
20814
|
+
*
|
|
20815
|
+
* Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
|
|
20816
|
+
* `_developerRefreshInProgress` guard prevents the two from overlapping.
|
|
20817
|
+
*/
|
|
20818
|
+
async executeDeveloperRefresh(provider, expiresAt, attempt) {
|
|
20819
|
+
if (this._developerRefreshInProgress) {
|
|
20820
|
+
logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
|
|
20821
|
+
return;
|
|
20822
|
+
}
|
|
20823
|
+
this._developerRefreshInProgress = true;
|
|
20824
|
+
try {
|
|
20825
|
+
const newCredentials = await this.refreshCredential(provider);
|
|
20826
|
+
this.deps.store.write(newCredentials);
|
|
20827
|
+
this.deps.store.persist(newCredentials);
|
|
20723
20828
|
try {
|
|
20724
|
-
|
|
20725
|
-
|
|
20726
|
-
|
|
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
|
-
}
|
|
20829
|
+
await this.deps.notifier.onCredentialRefreshed(newCredentials);
|
|
20830
|
+
} catch (reauthError) {
|
|
20831
|
+
logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
|
|
20740
20832
|
}
|
|
20741
|
-
|
|
20833
|
+
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
20834
|
+
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
20835
|
+
} catch (error) {
|
|
20836
|
+
const nextAttempt = attempt + 1;
|
|
20837
|
+
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
20838
|
+
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
20839
|
+
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
20840
|
+
else {
|
|
20841
|
+
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
20842
|
+
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
20843
|
+
this.deps.notifier.onRefreshExhausted();
|
|
20844
|
+
}
|
|
20845
|
+
} finally {
|
|
20846
|
+
this._developerRefreshInProgress = false;
|
|
20847
|
+
}
|
|
20848
|
+
}
|
|
20849
|
+
/**
|
|
20850
|
+
* Force an immediate refresh when the current credential is already past its
|
|
20851
|
+
* scheduled refresh window. Called on resume from suspension, where
|
|
20852
|
+
* background-tab timer throttling can delay the armed refresh well past
|
|
20853
|
+
* expiry, leaving the live session stale.
|
|
20854
|
+
*
|
|
20855
|
+
* Routes to whichever mechanism is armed: the developer timer if armed,
|
|
20856
|
+
* otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
|
|
20857
|
+
*/
|
|
20858
|
+
forceRefreshIfDue() {
|
|
20859
|
+
if (this._developerTimerId !== void 0 && this._activeProvider) {
|
|
20860
|
+
const expiry = this.deps.store.read().expiry_at;
|
|
20861
|
+
if (expiry !== void 0 && Date.now() >= expiry - CREDENTIAL_REFRESH_BUFFER_MS) {
|
|
20862
|
+
logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
|
|
20863
|
+
clearTimeout(this._developerTimerId);
|
|
20864
|
+
this._developerTimerId = void 0;
|
|
20865
|
+
this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
|
|
20866
|
+
}
|
|
20867
|
+
return;
|
|
20868
|
+
}
|
|
20869
|
+
this._deviceTokenManager?.refreshNowIfDue();
|
|
20870
|
+
}
|
|
20871
|
+
/**
|
|
20872
|
+
* Sync the credential's expiry from the server-provided authorization (the
|
|
20873
|
+
* `signalwire.connect` result). SATs are opaque JWE, so
|
|
20874
|
+
* `fabric_subscriber.expires_at` is the authoritative expiry of the token
|
|
20875
|
+
* the session actually connected with — the provider-reported `expiry_at`
|
|
20876
|
+
* is only a hint (and may be wrong or absent). Corrects the stored
|
|
20877
|
+
* credential and re-arms the developer refresh timer against the real
|
|
20878
|
+
* deadline when the provider supports `refresh()`.
|
|
20879
|
+
*/
|
|
20880
|
+
syncExpiryFromAuthorization(authorization, provider) {
|
|
20881
|
+
const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
|
|
20882
|
+
if (!expiresAtSec) return;
|
|
20883
|
+
const expiryAt = expiresAtSec * 1e3;
|
|
20884
|
+
const credential = this.deps.store.read();
|
|
20885
|
+
if (credential.expiry_at === expiryAt) return;
|
|
20886
|
+
logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
|
|
20887
|
+
const updated = {
|
|
20888
|
+
...credential,
|
|
20889
|
+
expiry_at: expiryAt
|
|
20890
|
+
};
|
|
20891
|
+
this.deps.store.write(updated);
|
|
20892
|
+
this.deps.store.persist(updated);
|
|
20893
|
+
if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
|
|
20894
|
+
}
|
|
20895
|
+
/**
|
|
20896
|
+
* Invoke `provider.refresh()` deduped against any concurrent developer
|
|
20897
|
+
* refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
|
|
20898
|
+
* and the orchestrator's -32003 recovery / reconnect re-mint — share one
|
|
20899
|
+
* in-flight promise, so a provider backed by one-time-use rotating refresh
|
|
20900
|
+
* tokens is never invoked twice in parallel.
|
|
20901
|
+
*
|
|
20902
|
+
* The caller owns applying the returned credential (store write, session
|
|
20903
|
+
* reauth, rescheduling); this method only serializes the network call.
|
|
20904
|
+
*/
|
|
20905
|
+
async refreshCredential(provider) {
|
|
20906
|
+
if (this._refreshInFlight) return this._refreshInFlight;
|
|
20907
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
20908
|
+
const run = provider.refresh();
|
|
20909
|
+
this._refreshInFlight = run;
|
|
20910
|
+
const clear = () => {
|
|
20911
|
+
if (this._refreshInFlight === run) this._refreshInFlight = void 0;
|
|
20912
|
+
};
|
|
20913
|
+
run.then(clear, clear);
|
|
20914
|
+
return run;
|
|
20742
20915
|
}
|
|
20743
20916
|
/**
|
|
20744
20917
|
* Cancels any scheduled developer-provided refresh. Idempotent.
|
|
@@ -21518,6 +21691,33 @@ var TransportManager = class extends Destroyable {
|
|
|
21518
21691
|
}
|
|
21519
21692
|
};
|
|
21520
21693
|
|
|
21694
|
+
//#endregion
|
|
21695
|
+
//#region src/utils/authRecovery.ts
|
|
21696
|
+
/**
|
|
21697
|
+
* Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
|
|
21698
|
+
* Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
|
|
21699
|
+
* underlying signaling error is nested. Bounded by a visited set to guard
|
|
21700
|
+
* against cyclic causes.
|
|
21701
|
+
*/
|
|
21702
|
+
function findJSONRPCError(error) {
|
|
21703
|
+
const seen = /* @__PURE__ */ new Set();
|
|
21704
|
+
let current = error;
|
|
21705
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
21706
|
+
seen.add(current);
|
|
21707
|
+
if (current instanceof JSONRPCError) return current;
|
|
21708
|
+
current = current.error ?? current.cause;
|
|
21709
|
+
}
|
|
21710
|
+
}
|
|
21711
|
+
/**
|
|
21712
|
+
* Whether an error is a session-recoverable authentication failure
|
|
21713
|
+
* (`-32002` authentication failed or `-32003` requester validation failed)
|
|
21714
|
+
* that a credential re-mint + retry can heal.
|
|
21715
|
+
*/
|
|
21716
|
+
function isRecoverableAuthError(error) {
|
|
21717
|
+
const rpcError = findJSONRPCError(error);
|
|
21718
|
+
return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
21719
|
+
}
|
|
21720
|
+
|
|
21521
21721
|
//#endregion
|
|
21522
21722
|
//#region src/clients/SignalWire.ts
|
|
21523
21723
|
var import_cjs$1 = require_cjs();
|
|
@@ -21633,7 +21833,8 @@ var SignalWire = class extends Destroyable {
|
|
|
21633
21833
|
notifier: {
|
|
21634
21834
|
onError: (error) => this._errors$.next(error),
|
|
21635
21835
|
onWarning: (warning) => this._warnings$.next(warning),
|
|
21636
|
-
onRefreshExhausted: () => void this.disconnect()
|
|
21836
|
+
onRefreshExhausted: () => void this.disconnect(),
|
|
21837
|
+
onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
|
|
21637
21838
|
},
|
|
21638
21839
|
store: {
|
|
21639
21840
|
read: () => this._deps.credential,
|
|
@@ -21690,14 +21891,123 @@ var SignalWire = class extends Destroyable {
|
|
|
21690
21891
|
}
|
|
21691
21892
|
this._deps.credential = _credentials;
|
|
21692
21893
|
this.persistCredential(_credentials);
|
|
21693
|
-
|
|
21694
|
-
|
|
21894
|
+
await this.reauthenticateLiveSession(_credentials);
|
|
21895
|
+
}
|
|
21896
|
+
/**
|
|
21897
|
+
* Reauthenticate the currently-open session with a freshly obtained
|
|
21898
|
+
* credential so the new token takes effect on the live socket immediately —
|
|
21899
|
+
* not just on the next reconnect. No-op when the session is not
|
|
21900
|
+
* connected/authenticated or the credential carries no token (e.g. an
|
|
21901
|
+
* authorization-state-only refresh). Non-fatal: reauth failures surface on
|
|
21902
|
+
* `errors$` without aborting the refresh that triggered this.
|
|
21903
|
+
*/
|
|
21904
|
+
async reauthenticateLiveSession(credential) {
|
|
21905
|
+
if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
|
|
21906
|
+
try {
|
|
21907
|
+
await this._clientSession.reauthenticate(credential.token);
|
|
21695
21908
|
logger$1.info("[SignalWire] Session refreshed with new credentials.");
|
|
21696
21909
|
} catch (error) {
|
|
21697
21910
|
logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
|
|
21698
21911
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21699
21912
|
}
|
|
21700
21913
|
}
|
|
21914
|
+
/**
|
|
21915
|
+
* One-shot recovery for a live session that failed with a recoverable auth
|
|
21916
|
+
* error (`-32002`/`-32003`) because its token went stale — e.g. a refresh
|
|
21917
|
+
* timer that was throttled while the tab was backgrounded.
|
|
21918
|
+
*
|
|
21919
|
+
* Two steps, first that succeeds wins:
|
|
21920
|
+
* 1. Reauthenticate with the in-memory token (a fresh DPoP proof is
|
|
21921
|
+
* generated automatically). Heals a session whose token was already
|
|
21922
|
+
* refreshed by the coordinator but never applied to the open socket,
|
|
21923
|
+
* and a client-bound session whose server-side auth merely drifted.
|
|
21924
|
+
* 2. Re-mint via the developer provider's `refresh()` and reauthenticate.
|
|
21925
|
+
* Skipped for client-bound sessions (the DeviceTokenManager owns their
|
|
21926
|
+
* refresh; re-minting a base SAT here would drop the DPoP binding).
|
|
21927
|
+
*
|
|
21928
|
+
* @param allowRemint - Whether step 2 (provider re-mint) may run. Callers
|
|
21929
|
+
* pass `false` for non-auth failures so a transient network error never
|
|
21930
|
+
* escalates to a token re-mint; step 1 (in-memory reauth) always runs.
|
|
21931
|
+
* @returns `true` if the session was reauthenticated, `false` otherwise.
|
|
21932
|
+
*/
|
|
21933
|
+
async recoverStaleCredential(allowRemint = true) {
|
|
21934
|
+
const currentToken = this._deps.credential.token;
|
|
21935
|
+
if (currentToken) try {
|
|
21936
|
+
await this._clientSession.reauthenticate(currentToken);
|
|
21937
|
+
return true;
|
|
21938
|
+
} catch (error) {
|
|
21939
|
+
logger$1.debug("[SignalWire] In-memory reauth failed during recovery:", error);
|
|
21940
|
+
}
|
|
21941
|
+
if (!allowRemint || this._clientSession.clientBound || !this._credentialProvider?.refresh) return false;
|
|
21942
|
+
try {
|
|
21943
|
+
const newCredentials = await this.remintCredential(this._credentialProvider);
|
|
21944
|
+
if (!newCredentials.token) return false;
|
|
21945
|
+
this._deps.credential = newCredentials;
|
|
21946
|
+
this.persistCredential(newCredentials);
|
|
21947
|
+
await this._clientSession.reauthenticate(newCredentials.token);
|
|
21948
|
+
if (newCredentials.expiry_at) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
21949
|
+
return true;
|
|
21950
|
+
} catch (error) {
|
|
21951
|
+
logger$1.error("[SignalWire] Credential recovery failed:", error);
|
|
21952
|
+
return false;
|
|
21953
|
+
}
|
|
21954
|
+
}
|
|
21955
|
+
/**
|
|
21956
|
+
* Re-mint a credential via `provider.refresh()`, routed through the
|
|
21957
|
+
* coordinator's shared in-flight guard so concurrent re-mint paths (a
|
|
21958
|
+
* scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
|
|
21959
|
+
* second `provider.refresh()` in parallel — which rotating one-time-use
|
|
21960
|
+
* refresh tokens reject. Falls back to a direct call only if the coordinator
|
|
21961
|
+
* has not been constructed yet.
|
|
21962
|
+
*/
|
|
21963
|
+
async remintCredential(provider) {
|
|
21964
|
+
if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
|
|
21965
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
21966
|
+
return provider.refresh();
|
|
21967
|
+
}
|
|
21968
|
+
/**
|
|
21969
|
+
* Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
|
|
21970
|
+
* The session invokes this only when it is client-bound OR the in-memory
|
|
21971
|
+
* token is expired. The re-mint mechanism depends on the binding:
|
|
21972
|
+
* - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
|
|
21973
|
+
* fresh base SAT the upcoming reconnect can re-bind (the
|
|
21974
|
+
* DeviceTokenManager re-activates afterwards).
|
|
21975
|
+
* - Unbound: the developer's non-interactive `refresh()` handler.
|
|
21976
|
+
* `authenticate()` is deliberately NOT used here — it may be interactive
|
|
21977
|
+
* (a login prompt) and must not fire on a background reconnect.
|
|
21978
|
+
*
|
|
21979
|
+
* Rejects on failure so the session aborts the reconnect rather than
|
|
21980
|
+
* replaying a stale token.
|
|
21981
|
+
*/
|
|
21982
|
+
async refreshCredentialForReconnect() {
|
|
21983
|
+
if (!this._credentialProvider) return;
|
|
21984
|
+
try {
|
|
21985
|
+
let newCredentials;
|
|
21986
|
+
if (this._clientSession.clientBound) {
|
|
21987
|
+
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
21988
|
+
logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
|
|
21989
|
+
newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
|
|
21990
|
+
} else if (this._credentialProvider.refresh) {
|
|
21991
|
+
logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
|
|
21992
|
+
newCredentials = await this.remintCredential(this._credentialProvider);
|
|
21993
|
+
} else {
|
|
21994
|
+
logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
|
|
21995
|
+
return;
|
|
21996
|
+
}
|
|
21997
|
+
if (!newCredentials.token) {
|
|
21998
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
|
|
21999
|
+
return;
|
|
22000
|
+
}
|
|
22001
|
+
this._deps.credential = newCredentials;
|
|
22002
|
+
this.persistCredential(newCredentials);
|
|
22003
|
+
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
22004
|
+
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
22005
|
+
} catch (error) {
|
|
22006
|
+
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
22007
|
+
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22008
|
+
throw error;
|
|
22009
|
+
}
|
|
22010
|
+
}
|
|
21701
22011
|
/** Persist credential to localStorage when persistSession is enabled. */
|
|
21702
22012
|
persistCredential(credential) {
|
|
21703
22013
|
if (!credential.token) return;
|
|
@@ -21784,24 +22094,13 @@ var SignalWire = class extends Destroyable {
|
|
|
21784
22094
|
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
|
|
21785
22095
|
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
22096
|
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
|
-
};
|
|
22097
|
+
this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
|
|
21802
22098
|
this.subscribeTo(this._clientSession.errors$, (error) => {
|
|
21803
22099
|
this._errors$.next(error);
|
|
21804
22100
|
});
|
|
22101
|
+
this.subscribeTo(this._clientSession.authorization$, (authorization) => {
|
|
22102
|
+
this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
|
|
22103
|
+
});
|
|
21805
22104
|
await this._clientSession.connect();
|
|
21806
22105
|
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
21807
22106
|
this.subscribeTo(this._clientSession.authenticated$.pipe((0, import_cjs$1.skip)(1), (0, import_cjs$1.filter)(Boolean)), async () => {
|
|
@@ -21964,6 +22263,13 @@ var SignalWire = class extends Destroyable {
|
|
|
21964
22263
|
}
|
|
21965
22264
|
try {
|
|
21966
22265
|
this._visibilityController = new VisibilityController();
|
|
22266
|
+
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible")), () => {
|
|
22267
|
+
try {
|
|
22268
|
+
this._refreshCoordinator?.forceRefreshIfDue();
|
|
22269
|
+
} catch (error) {
|
|
22270
|
+
logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
|
|
22271
|
+
}
|
|
22272
|
+
});
|
|
21967
22273
|
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
|
|
21968
22274
|
logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
|
|
21969
22275
|
try {
|
|
@@ -21975,7 +22281,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21975
22281
|
logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
|
|
21976
22282
|
}
|
|
21977
22283
|
try {
|
|
21978
|
-
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.
|
|
22284
|
+
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.2" });
|
|
21979
22285
|
} catch (error) {
|
|
21980
22286
|
logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
|
|
21981
22287
|
}
|
|
@@ -22046,21 +22352,23 @@ var SignalWire = class extends Destroyable {
|
|
|
22046
22352
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22047
22353
|
throw error;
|
|
22048
22354
|
}
|
|
22049
|
-
logger$1.debug("[SignalWire] Failed to register user,
|
|
22050
|
-
|
|
22051
|
-
|
|
22052
|
-
logger$1.debug("[SignalWire]
|
|
22355
|
+
logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
|
|
22356
|
+
let failureCause = error;
|
|
22357
|
+
if (await this.recoverStaleCredential(isRecoverableAuthError(error))) try {
|
|
22358
|
+
logger$1.debug("[SignalWire] Recovery succeeded, retrying register()");
|
|
22053
22359
|
await this._transport.execute(RPCExecute({
|
|
22054
22360
|
method: "subscriber.online",
|
|
22055
22361
|
params: {}
|
|
22056
22362
|
}));
|
|
22057
22363
|
this._isRegistered$.next(true);
|
|
22058
|
-
|
|
22059
|
-
|
|
22060
|
-
|
|
22061
|
-
|
|
22062
|
-
throw registerError;
|
|
22364
|
+
return;
|
|
22365
|
+
} catch (retryError) {
|
|
22366
|
+
logger$1.error("[SignalWire] Register retry after recovery failed:", retryError);
|
|
22367
|
+
failureCause = retryError;
|
|
22063
22368
|
}
|
|
22369
|
+
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 }) });
|
|
22370
|
+
this._errors$.next(registerError);
|
|
22371
|
+
throw registerError;
|
|
22064
22372
|
}
|
|
22065
22373
|
}
|
|
22066
22374
|
/**
|
|
@@ -22113,7 +22421,15 @@ var SignalWire = class extends Destroyable {
|
|
|
22113
22421
|
};
|
|
22114
22422
|
await this.waitAuthentication();
|
|
22115
22423
|
logger$1.debug("[SignalWire] Dialing with options:", computed_options);
|
|
22116
|
-
|
|
22424
|
+
try {
|
|
22425
|
+
return await this._clientSession.createOutboundCall(destination, computed_options);
|
|
22426
|
+
} catch (error) {
|
|
22427
|
+
if (!isRecoverableAuthError(error)) throw error;
|
|
22428
|
+
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering credential and retrying once");
|
|
22429
|
+
if (!await this.recoverStaleCredential()) throw error;
|
|
22430
|
+
await this.waitAuthentication();
|
|
22431
|
+
return this._clientSession.createOutboundCall(destination, computed_options);
|
|
22432
|
+
}
|
|
22117
22433
|
}
|
|
22118
22434
|
/**
|
|
22119
22435
|
* Runs a multi-phase connectivity test against the given destination.
|
|
@@ -22510,7 +22826,7 @@ var StaticCredentialProvider = class {
|
|
|
22510
22826
|
/**
|
|
22511
22827
|
* Library version from package.json, injected at build time.
|
|
22512
22828
|
*/
|
|
22513
|
-
const version = "4.0.0-rc.
|
|
22829
|
+
const version = "4.0.0-rc.2";
|
|
22514
22830
|
/**
|
|
22515
22831
|
* Flag indicating the library has been loaded and is ready to use.
|
|
22516
22832
|
* For UMD builds: `window.SignalWire.ready`
|
|
@@ -22532,7 +22848,7 @@ const ready = true;
|
|
|
22532
22848
|
*/
|
|
22533
22849
|
const emitReadyEvent = () => {
|
|
22534
22850
|
if (typeof window !== "undefined") {
|
|
22535
|
-
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.
|
|
22851
|
+
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.2" } });
|
|
22536
22852
|
window.dispatchEvent(event);
|
|
22537
22853
|
}
|
|
22538
22854
|
};
|
|
@@ -22558,6 +22874,7 @@ if (typeof process === "undefined") globalThis.process = { env: { NODE_ENV: "pro
|
|
|
22558
22874
|
//#endregion
|
|
22559
22875
|
exports.Address = Address;
|
|
22560
22876
|
exports.CallCreateError = CallCreateError;
|
|
22877
|
+
exports.CallNotReadyError = CallNotReadyError;
|
|
22561
22878
|
exports.ClientPreferences = ClientPreferences;
|
|
22562
22879
|
exports.CollectionFetchError = CollectionFetchError;
|
|
22563
22880
|
exports.DPoPInitError = DPoPInitError;
|
|
@@ -22569,6 +22886,7 @@ exports.MediaTrackError = MediaTrackError;
|
|
|
22569
22886
|
exports.MessageParseError = MessageParseError;
|
|
22570
22887
|
exports.OverconstrainedFallbackError = OverconstrainedFallbackError;
|
|
22571
22888
|
exports.Participant = Participant;
|
|
22889
|
+
exports.ParticipantNotReadyError = ParticipantNotReadyError;
|
|
22572
22890
|
exports.PreflightError = PreflightError;
|
|
22573
22891
|
exports.RecoveryError = RecoveryError;
|
|
22574
22892
|
exports.SelfCapabilities = SelfCapabilities;
|