@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.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,
|
|
13669
|
+
constructor(id, callExecuteMethod, deviceController) {
|
|
13648
13670
|
super();
|
|
13649
|
-
this.
|
|
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
|
-
|
|
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(
|
|
13933
|
+
await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
|
|
13879
13934
|
}
|
|
13880
13935
|
/** Mutes the participant's audio. */
|
|
13881
13936
|
async mute() {
|
|
13882
|
-
await this.executeMethod(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
13979
|
-
*
|
|
13980
|
-
*
|
|
13981
|
-
*
|
|
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
|
-
|
|
13989
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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,
|
|
14044
|
-
super(id,
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
18529
|
-
|
|
18530
|
-
|
|
18531
|
-
|
|
18532
|
-
|
|
18533
|
-
|
|
18534
|
-
|
|
18535
|
-
|
|
18536
|
-
|
|
18537
|
-
|
|
18538
|
-
|
|
18539
|
-
|
|
18540
|
-
|
|
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
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
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
|
|
19130
|
-
|
|
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
|
-
|
|
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
|
|
20158
|
-
|
|
20159
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
20719
|
-
|
|
20720
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21688
|
-
|
|
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.
|
|
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,
|
|
22044
|
-
|
|
22045
|
-
|
|
22046
|
-
logger$1.debug("[SignalWire]
|
|
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
|
-
|
|
22053
|
-
|
|
22054
|
-
|
|
22055
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|