@signalwire/js 4.0.0-dev-20260804221355 → 4.0.0-dev-20260908172838
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 +433 -98
- package/dist/browser.mjs.map +1 -1
- package/dist/browser.umd.js +433 -98
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +433 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +210 -20
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +210 -20
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +433 -98
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/browser.mjs
CHANGED
|
@@ -12374,15 +12374,57 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
|
|
|
12374
12374
|
result: {}
|
|
12375
12375
|
});
|
|
12376
12376
|
|
|
12377
|
+
//#endregion
|
|
12378
|
+
//#region src/utils/authRecovery.ts
|
|
12379
|
+
/**
|
|
12380
|
+
* Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
|
|
12381
|
+
* Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
|
|
12382
|
+
* underlying signaling error is nested. Bounded by a visited set to guard
|
|
12383
|
+
* against cyclic causes.
|
|
12384
|
+
*/
|
|
12385
|
+
function findJSONRPCError(error) {
|
|
12386
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12387
|
+
let current = error;
|
|
12388
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
12389
|
+
seen.add(current);
|
|
12390
|
+
if (current instanceof JSONRPCError) return current;
|
|
12391
|
+
current = current.error ?? current.cause;
|
|
12392
|
+
}
|
|
12393
|
+
}
|
|
12394
|
+
/**
|
|
12395
|
+
* Whether an error is a session-recoverable authentication failure
|
|
12396
|
+
* (`-32002` authentication failed or `-32003` requester validation failed)
|
|
12397
|
+
* that a credential re-mint + retry can heal.
|
|
12398
|
+
*/
|
|
12399
|
+
function isRecoverableAuthError(error) {
|
|
12400
|
+
const rpcError = findJSONRPCError(error);
|
|
12401
|
+
return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
12402
|
+
}
|
|
12403
|
+
/**
|
|
12404
|
+
* Whether an error is specifically a requester-validation rejection
|
|
12405
|
+
* (`-32003`) — the server refusing the session's credential.
|
|
12406
|
+
*
|
|
12407
|
+
* Narrower than {@link isRecoverableAuthError} on purpose. `-32002` is
|
|
12408
|
+
* overloaded server-side: a rejected reattach arrives as `-32002` with
|
|
12409
|
+
* `cause: INVALID_MSG_UNSPECIFIED` and message `CALL ERROR`, which is a
|
|
12410
|
+
* call-level rejection and says nothing about the credential. Use this where
|
|
12411
|
+
* the decision must not be fooled by that, such as deciding whether retrying
|
|
12412
|
+
* an operation could possibly succeed.
|
|
12413
|
+
*/
|
|
12414
|
+
function isRequesterValidationError(error) {
|
|
12415
|
+
return findJSONRPCError(error)?.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED;
|
|
12416
|
+
}
|
|
12417
|
+
|
|
12377
12418
|
//#endregion
|
|
12378
12419
|
//#region src/managers/AttachManager.ts
|
|
12379
12420
|
const logger$25 = getLogger();
|
|
12380
12421
|
var AttachManager = class {
|
|
12381
|
-
constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
|
|
12422
|
+
constructor(storage, deviceController, reconnectCallsTimeout, attachKey, credentialRecovered) {
|
|
12382
12423
|
this.storage = storage;
|
|
12383
12424
|
this.deviceController = deviceController;
|
|
12384
12425
|
this.reconnectCallsTimeout = reconnectCallsTimeout;
|
|
12385
12426
|
this.attachKey = attachKey;
|
|
12427
|
+
this.credentialRecovered = credentialRecovered;
|
|
12386
12428
|
this.writeQueue = Promise.resolve();
|
|
12387
12429
|
}
|
|
12388
12430
|
async detachAll() {
|
|
@@ -12462,8 +12504,14 @@ var AttachManager = class {
|
|
|
12462
12504
|
* rejecting. Once that fix is deployed, this will work for both
|
|
12463
12505
|
* page reloads and WebSocket reconnects.
|
|
12464
12506
|
*
|
|
12465
|
-
*
|
|
12466
|
-
*
|
|
12507
|
+
* A failed reattach does NOT generally cost the stored reference. It is
|
|
12508
|
+
* discarded only when the server denied the reattach on a session whose
|
|
12509
|
+
* credential it had already accepted — a verified reauthentication followed
|
|
12510
|
+
* by a refusal is the server saying the call is gone, and that is the one
|
|
12511
|
+
* refusal worth acting on. Until then the credential may be what is being
|
|
12512
|
+
* refused, and the record is the only way a later reload can try again;
|
|
12513
|
+
* keeping it costs nothing, since `detachExpired` reaps it once it is older
|
|
12514
|
+
* than `reconnectCallsTimeout`.
|
|
12467
12515
|
*/
|
|
12468
12516
|
async reattachCalls() {
|
|
12469
12517
|
const attached = await this.readAttached();
|
|
@@ -12472,6 +12520,7 @@ var AttachManager = class {
|
|
|
12472
12520
|
const { destination } = attachment;
|
|
12473
12521
|
const options = this.buildCallOptions(attachment);
|
|
12474
12522
|
let succeeded = false;
|
|
12523
|
+
let refusedOnCredentials = false;
|
|
12475
12524
|
for (let attempt = 1; attempt <= 3; attempt++) try {
|
|
12476
12525
|
await this.session.createOutboundCall(destination, {
|
|
12477
12526
|
callId,
|
|
@@ -12482,15 +12531,20 @@ var AttachManager = class {
|
|
|
12482
12531
|
break;
|
|
12483
12532
|
} catch (error) {
|
|
12484
12533
|
logger$25.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
|
|
12534
|
+
if (isRequesterValidationError(error)) {
|
|
12535
|
+
refusedOnCredentials = true;
|
|
12536
|
+
logger$25.warn(`[AttachManager] Reattach of ${callId} was refused on credentials; not retrying.`);
|
|
12537
|
+
break;
|
|
12538
|
+
}
|
|
12485
12539
|
if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
|
|
12486
12540
|
}
|
|
12487
|
-
if (!succeeded) {
|
|
12488
|
-
logger$25.warn(`[AttachManager] Reattach
|
|
12541
|
+
if (!succeeded) if (this.credentialRecovered() && !refusedOnCredentials) {
|
|
12542
|
+
logger$25.warn(`[AttachManager] Reattach of ${callId} was denied after a verified reauthentication, removing reference`);
|
|
12489
12543
|
await this.detach({
|
|
12490
12544
|
id: callId,
|
|
12491
12545
|
mediaDirections: attachment.mediaDirections
|
|
12492
12546
|
});
|
|
12493
|
-
}
|
|
12547
|
+
} else logger$25.warn(`[AttachManager] Reattach failed for call ${callId}; keeping the reference (credential refused or never proven good)`);
|
|
12494
12548
|
}
|
|
12495
12549
|
}
|
|
12496
12550
|
/**
|
|
@@ -14700,9 +14754,14 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14700
14754
|
updateLayouts() {
|
|
14701
14755
|
if (!this.selfId) return;
|
|
14702
14756
|
this.webRtcCallSession.executeMethod(this.selfId, "call.layout.list", {}).then((response) => {
|
|
14757
|
+
const layouts = response.result?.layouts;
|
|
14758
|
+
if (!layouts) {
|
|
14759
|
+
logger$22.warn("[CallEventsManager] Layout list response carried no layouts; keeping current layouts");
|
|
14760
|
+
return;
|
|
14761
|
+
}
|
|
14703
14762
|
this._sessionState$.next({
|
|
14704
14763
|
...this._sessionState$.value,
|
|
14705
|
-
layouts
|
|
14764
|
+
layouts
|
|
14706
14765
|
});
|
|
14707
14766
|
}).catch((error) => {
|
|
14708
14767
|
logger$22.error("[CallEventsManager] Error fetching layouts:", error);
|
|
@@ -16707,6 +16766,48 @@ const logger$16 = getLogger();
|
|
|
16707
16766
|
function resolveInviteNodeId(args) {
|
|
16708
16767
|
return args.isInvite && !args.reattach && !args.explicitNodeId ? "" : args.currentNodeId ?? "";
|
|
16709
16768
|
}
|
|
16769
|
+
/**
|
|
16770
|
+
* Surface the real outcome of a `webrtc.verto` reply.
|
|
16771
|
+
*
|
|
16772
|
+
* A webrtc.verto response nests several envelopes, each keyed by a verto-style
|
|
16773
|
+
* string `code` ("200" ok, "400"/etc. fail) rather than a JSON-RPC `error`. An outer
|
|
16774
|
+
* layer reports only whether the frame was delivered; an inner layer carries the op's
|
|
16775
|
+
* own outcome:
|
|
16776
|
+
*
|
|
16777
|
+
* response.result = { code:"200", result:{…} } ← delivery acknowledgement
|
|
16778
|
+
* .result = { jsonrpc, id, result:{…} } ← the reply payload
|
|
16779
|
+
* .result = { code:"400", message:"Bad request" } ← the actual op outcome
|
|
16780
|
+
*
|
|
16781
|
+
* A failure can appear at any layer (delivery refused, or the op itself rejected
|
|
16782
|
+
* deeper down), so walk every nested `.result` object and return the FIRST non-2xx
|
|
16783
|
+
* `code` with its message. Returns null when every `code` seen is 2xx or absent —
|
|
16784
|
+
* i.e. the op succeeded. This is the only way to detect that e.g. a mute/kick was
|
|
16785
|
+
* rejected, since the outer delivery `code` is "200" (delivered) even then.
|
|
16786
|
+
*
|
|
16787
|
+
* Pure function — exported for unit testing.
|
|
16788
|
+
*/
|
|
16789
|
+
function findNestedVertoFailure(response) {
|
|
16790
|
+
let node = response;
|
|
16791
|
+
while (node !== null && typeof node === "object") {
|
|
16792
|
+
const obj = node;
|
|
16793
|
+
const err = obj.error;
|
|
16794
|
+
if (err !== null && typeof err === "object") {
|
|
16795
|
+
const e = err;
|
|
16796
|
+
const errCode = typeof e.code === "string" || typeof e.code === "number" ? String(e.code) : void 0;
|
|
16797
|
+
if (errCode !== void 0 && !/^2\d\d$/.test(errCode)) return {
|
|
16798
|
+
code: errCode,
|
|
16799
|
+
message: typeof e.message === "string" ? e.message : void 0
|
|
16800
|
+
};
|
|
16801
|
+
}
|
|
16802
|
+
const code = typeof obj.code === "string" || typeof obj.code === "number" ? String(obj.code) : void 0;
|
|
16803
|
+
if (code !== void 0 && !/^2\d\d$/.test(code)) return {
|
|
16804
|
+
code,
|
|
16805
|
+
message: typeof obj.message === "string" ? obj.message : void 0
|
|
16806
|
+
};
|
|
16807
|
+
node = obj.result !== null && typeof obj.result === "object" ? obj.result : null;
|
|
16808
|
+
}
|
|
16809
|
+
return null;
|
|
16810
|
+
}
|
|
16710
16811
|
var VertoManager = class extends Destroyable {
|
|
16711
16812
|
constructor(callSession) {
|
|
16712
16813
|
super();
|
|
@@ -17031,6 +17132,33 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17031
17132
|
get vertoPing$() {
|
|
17032
17133
|
return this.cachedObservable("vertoPing$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoPingInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
|
|
17033
17134
|
}
|
|
17135
|
+
/**
|
|
17136
|
+
* Send a member-control op in-dialog via verto.info.
|
|
17137
|
+
*
|
|
17138
|
+
* The control payload rides in `params.command` — a sibling of `dialogParams`,
|
|
17139
|
+
* at the same level as `dtmf` in {@link sendDigits} — and the inner verto.info
|
|
17140
|
+
* is matched to this call's channel by `dialogParams.callID`, the in-dialog
|
|
17141
|
+
* convention for member-scoped frames. Because it is delivered on the dialog
|
|
17142
|
+
* itself, control lands on the call's own channel with no {node_id,call_id,member_id}
|
|
17143
|
+
* "self" tuple to get wrong. The outer webrtc.verto envelope (added by executeVerto)
|
|
17144
|
+
* still carries the own-leg callID + node_id for session routing.
|
|
17145
|
+
*
|
|
17146
|
+
* Keep `command` OUT of `dialogParams`: it is read at the params level, and
|
|
17147
|
+
* filterVertoParams rewrites/filters dialogParams keys but passes params-level
|
|
17148
|
+
* keys through verbatim.
|
|
17149
|
+
*/
|
|
17150
|
+
async sendCallControl(method, params) {
|
|
17151
|
+
const response = await this.executeVerto(VertoInfo({
|
|
17152
|
+
dialogParams: { callID: this.webRtcCallSession.id },
|
|
17153
|
+
command: {
|
|
17154
|
+
method,
|
|
17155
|
+
params
|
|
17156
|
+
}
|
|
17157
|
+
}));
|
|
17158
|
+
const failure = findNestedVertoFailure(response);
|
|
17159
|
+
if (failure) throw new JSONRPCError(Number.parseInt(failure.code, 10) || 0, `Call control "${method}" failed (code ${failure.code})${failure.message ? `: ${failure.message}` : ""}`, void 0);
|
|
17160
|
+
return response;
|
|
17161
|
+
}
|
|
17034
17162
|
async executeVerto(message, optionals = {}) {
|
|
17035
17163
|
const webrtcVertoMessage = WebrtcVerto({
|
|
17036
17164
|
callID: optionals.callID ?? this.webRtcCallSession.id,
|
|
@@ -17039,15 +17167,16 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17039
17167
|
subscribe: optionals.subscribe
|
|
17040
17168
|
});
|
|
17041
17169
|
const response = await this.webRtcCallSession.execute(webrtcVertoMessage);
|
|
17170
|
+
const nonFatal = message.method === "verto.info" ? { fatal: false } : void 0;
|
|
17042
17171
|
if (response.error) {
|
|
17043
17172
|
const error = new JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
17044
|
-
this.onError?.(error);
|
|
17173
|
+
this.onError?.(error, nonFatal);
|
|
17045
17174
|
return response;
|
|
17046
17175
|
}
|
|
17047
17176
|
const innerResult = getValueFrom(response, "result.result");
|
|
17048
17177
|
if (innerResult?.error) {
|
|
17049
17178
|
const error = new JSONRPCError(innerResult.error.code, innerResult.error.message, innerResult.error.data);
|
|
17050
|
-
this.onError?.(error);
|
|
17179
|
+
this.onError?.(error, nonFatal);
|
|
17051
17180
|
return response;
|
|
17052
17181
|
}
|
|
17053
17182
|
return response;
|
|
@@ -18335,6 +18464,33 @@ function mosToQualityLevel(mos) {
|
|
|
18335
18464
|
return "critical";
|
|
18336
18465
|
}
|
|
18337
18466
|
|
|
18467
|
+
//#endregion
|
|
18468
|
+
//#region src/utils/unwrapVertoReply.ts
|
|
18469
|
+
/**
|
|
18470
|
+
* Unwrap a method's own reply from a `webrtc.verto` envelope.
|
|
18471
|
+
*
|
|
18472
|
+
* A control verb sent in-dialog comes back nested two levels deep:
|
|
18473
|
+
*
|
|
18474
|
+
* ```
|
|
18475
|
+
* { result: { node_id, code, result: { jsonrpc, id, result: <method payload> } } }
|
|
18476
|
+
* ```
|
|
18477
|
+
*
|
|
18478
|
+
* whereas the routed transport resolves the method payload directly under
|
|
18479
|
+
* `.result`. Readers want the latter shape, and the difference is silent when it
|
|
18480
|
+
* is wrong — `response.result.layouts` simply evaluates to `undefined` against the
|
|
18481
|
+
* envelope, so the data arrives, nothing throws, and the caller sees an empty
|
|
18482
|
+
* value. That exact failure produced an empty layout dropdown with a successful
|
|
18483
|
+
* request behind it.
|
|
18484
|
+
*
|
|
18485
|
+
* Accepting both shapes here keeps every reader indifferent to which transport
|
|
18486
|
+
* produced the response. Anything that is not a nested envelope (a plain ack, or
|
|
18487
|
+
* an already-unwrapped reply) passes through untouched.
|
|
18488
|
+
*/
|
|
18489
|
+
function unwrapVertoReply(response) {
|
|
18490
|
+
const inner = getValueFrom(response, "result.result");
|
|
18491
|
+
return inner && typeof inner === "object" && "result" in inner ? inner : response;
|
|
18492
|
+
}
|
|
18493
|
+
|
|
18338
18494
|
//#endregion
|
|
18339
18495
|
//#region src/core/entities/Call.ts
|
|
18340
18496
|
var import_cjs$11 = require_cjs();
|
|
@@ -18434,8 +18590,11 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18434
18590
|
emitError(callError) {
|
|
18435
18591
|
if (this._status$.value === "destroyed" || this._status$.value === "failed") return;
|
|
18436
18592
|
this._errors$.next(callError);
|
|
18437
|
-
if (callError.fatal) {
|
|
18593
|
+
if (callError.fatal && this._status$.value !== "disconnecting") {
|
|
18438
18594
|
this._status$.next("failed");
|
|
18595
|
+
this.vertoManager.bye().catch((error) => {
|
|
18596
|
+
logger$12.debug("[Call] fatal-teardown bye failed (signaling likely already dead):", error);
|
|
18597
|
+
});
|
|
18439
18598
|
this.destroy();
|
|
18440
18599
|
}
|
|
18441
18600
|
}
|
|
@@ -18552,6 +18711,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18552
18711
|
async executeMethod(target, method, args) {
|
|
18553
18712
|
const self = this.callSelf;
|
|
18554
18713
|
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}`);
|
|
18714
|
+
if (this.clientSession.callControl === "in-dialog") return this.executeMethodInDialog(target, method, args);
|
|
18555
18715
|
const params = {
|
|
18556
18716
|
...args,
|
|
18557
18717
|
self,
|
|
@@ -18571,6 +18731,81 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18571
18731
|
}
|
|
18572
18732
|
}
|
|
18573
18733
|
/**
|
|
18734
|
+
* `executeMethod` for a call opened with `callControl: 'in-dialog'`.
|
|
18735
|
+
*
|
|
18736
|
+
* Translates the routed transport's calling convention into the in-dialog one. No
|
|
18737
|
+
* `self` tuple is sent, but a `target` is — the same {call_id, member_id} the routed
|
|
18738
|
+
* transport puts in `target` (minus node_id), for self-ops and cross-member ops alike.
|
|
18739
|
+
*
|
|
18740
|
+
* Target shapes are per-verb and irregular, so they are centralised here rather
|
|
18741
|
+
* than left to callers: most verbs take a singular `target`, `call.member.remove`
|
|
18742
|
+
* takes a plural `targets` array, and `call.member.position.set` takes a flat
|
|
18743
|
+
* `targets` of `{call_id, position}` — the one verb keyed on call_id rather than
|
|
18744
|
+
* member_id, so the member triple `Participant.setPosition` built is unwrapped.
|
|
18745
|
+
*/
|
|
18746
|
+
async executeMethodInDialog(target, method, args) {
|
|
18747
|
+
const control = { ...args };
|
|
18748
|
+
if (method === "call.member.position.set") control.targets = (args.targets ?? []).map((entry) => ({
|
|
18749
|
+
call_id: entry.target?.call_id ?? entry.call_id,
|
|
18750
|
+
position: entry.position
|
|
18751
|
+
}));
|
|
18752
|
+
else {
|
|
18753
|
+
const member = typeof target === "object" ? {
|
|
18754
|
+
call_id: target.call_id,
|
|
18755
|
+
member_id: target.member_id
|
|
18756
|
+
} : {
|
|
18757
|
+
call_id: this.id,
|
|
18758
|
+
member_id: target
|
|
18759
|
+
};
|
|
18760
|
+
if (method === "call.member.remove") control.targets = [member];
|
|
18761
|
+
else control.target = member;
|
|
18762
|
+
}
|
|
18763
|
+
return this.sendCommand(method, control);
|
|
18764
|
+
}
|
|
18765
|
+
/**
|
|
18766
|
+
* Sends a `call.*` control verb **in-dialog** via `verto.info`, as an alternative
|
|
18767
|
+
* to the routed {@link executeMethod} transport.
|
|
18768
|
+
*
|
|
18769
|
+
* Why both exist: `executeMethod` addresses the member with an explicit
|
|
18770
|
+
* `{node_id, call_id, member_id}` tuple, which does not resolve for every conference,
|
|
18771
|
+
* so the op can fail. An in-dialog frame carries the verb on the member's own
|
|
18772
|
+
* signaling channel instead, so control works without the client needing to know how
|
|
18773
|
+
* the conference is hosted.
|
|
18774
|
+
*
|
|
18775
|
+
* The trade-off is reach: the in-dialog transport is only accepted for calls that
|
|
18776
|
+
* join a conference over SWML (e.g. an SWML `join_conference`); use the routed
|
|
18777
|
+
* default otherwise.
|
|
18778
|
+
*
|
|
18779
|
+
* `params` are sent verbatim — nothing is built for you, which includes the target.
|
|
18780
|
+
* **A self-directed op still needs one**, or it is refused; name yourself explicitly:
|
|
18781
|
+
*
|
|
18782
|
+
* ```ts
|
|
18783
|
+
* const { call_id, member_id } = call.self.target;
|
|
18784
|
+
* await call.sendCommand('call.mute', { channels: ['audio'], target: { call_id, member_id } });
|
|
18785
|
+
* ```
|
|
18786
|
+
*
|
|
18787
|
+
* Never include `node_id` — only the two ids. The shapes are per-verb: most take a
|
|
18788
|
+
* singular `target`, `call.member.remove` takes a plural `targets` array, and
|
|
18789
|
+
* `call.member.position.set` takes a flat `targets: [{call_id, position}]` (the one
|
|
18790
|
+
* verb keyed on `call_id` rather than `member_id`). Verbs that act on the call as a
|
|
18791
|
+
* whole, or that the SDK does not wrap at all, take no target.
|
|
18792
|
+
*
|
|
18793
|
+
* For the typed alternative that handles all of this, create the client with
|
|
18794
|
+
* `callControl: 'in-dialog'` and use the ordinary `Call`/`Participant` methods.
|
|
18795
|
+
*
|
|
18796
|
+
* @internal Not part of the supported surface while the in-dialog transport is still
|
|
18797
|
+
* rolling out. `WebRTCCall` is exported from the package entry, so without this tag
|
|
18798
|
+
* TypeDoc publishes the method — and the example above — as public API.
|
|
18799
|
+
*
|
|
18800
|
+
* @param method - A `call.*` method name (e.g. `'call.mute'`).
|
|
18801
|
+
* @param params - Method parameters, sent verbatim.
|
|
18802
|
+
* @returns The method's own reply, unwrapped from the `verto.info` envelope.
|
|
18803
|
+
* @throws {JSONRPCError} If the control op fails.
|
|
18804
|
+
*/
|
|
18805
|
+
async sendCommand(method, params = {}) {
|
|
18806
|
+
return unwrapVertoReply(await this.vertoManager.sendCallControl(method, params));
|
|
18807
|
+
}
|
|
18808
|
+
/**
|
|
18574
18809
|
* The local leg's member triple — sent as `self` in every member RPC
|
|
18575
18810
|
* envelope, and as the `target` of call-scoped self-operations (e.g. lock,
|
|
18576
18811
|
* layout).
|
|
@@ -19901,6 +20136,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19901
20136
|
minor: 0,
|
|
19902
20137
|
revision: 0
|
|
19903
20138
|
};
|
|
20139
|
+
this.callControl = "routed";
|
|
19904
20140
|
this._authorization$ = this.createBehaviorSubject(void 0);
|
|
19905
20141
|
this._errors$ = this.createReplaySubject(1);
|
|
19906
20142
|
this._authState$ = this.createBehaviorSubject({ kind: "unauthenticated" });
|
|
@@ -20105,17 +20341,14 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20105
20341
|
const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
|
|
20106
20342
|
if (isRecoverableAuthError$1 && hasStoredState) {
|
|
20107
20343
|
logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
|
|
20108
|
-
|
|
20109
|
-
await this.cleanupStoredConnectionParams();
|
|
20110
|
-
} catch (cleanupError) {
|
|
20111
|
-
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
20112
|
-
} finally {
|
|
20113
|
-
this.transport.reconnect();
|
|
20114
|
-
}
|
|
20344
|
+
await this.discardResumeStateAndReconnect();
|
|
20115
20345
|
} else this._errors$.next(error);
|
|
20116
20346
|
}
|
|
20117
20347
|
/**
|
|
20118
|
-
* Clear the resume state (authorization_state + protocol)
|
|
20348
|
+
* Clear the resume state (authorization_state + protocol) and ask the
|
|
20349
|
+
* transport to reconnect. The `connected` event re-triggers
|
|
20350
|
+
* `authenticate()`, which now has no stored state and so performs a fresh
|
|
20351
|
+
* connect.
|
|
20119
20352
|
*
|
|
20120
20353
|
* This is the stale-auth-state recovery helper used by handleAuthError:
|
|
20121
20354
|
* the server rejected a reconnect, so the resume state is discarded and a
|
|
@@ -20123,9 +20356,24 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20123
20356
|
* session lives on through the reconnect and reattachCalls() needs the
|
|
20124
20357
|
* stored call references afterwards. Do NOT add detachAll() here.
|
|
20125
20358
|
*
|
|
20359
|
+
* Connect-time recovery only. A *request* refused on an already
|
|
20360
|
+
* authenticated session is never healed here: dropping the resume state
|
|
20361
|
+
* destroys the association between the socket and the previous session,
|
|
20362
|
+
* which is what reattach depends on. That path mints a fresh credential and
|
|
20363
|
+
* reauthenticates instead (see `SignalWire.recoverAndRetry`).
|
|
20364
|
+
*
|
|
20126
20365
|
* For public teardown (disconnect/destroy), use {@link teardownSessionState}
|
|
20127
20366
|
* instead, which clears the attach records as well.
|
|
20128
20367
|
*/
|
|
20368
|
+
async discardResumeStateAndReconnect() {
|
|
20369
|
+
try {
|
|
20370
|
+
await this.cleanupStoredConnectionParams();
|
|
20371
|
+
} catch (cleanupError) {
|
|
20372
|
+
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
20373
|
+
} finally {
|
|
20374
|
+
this.transport.reconnect();
|
|
20375
|
+
}
|
|
20376
|
+
}
|
|
20129
20377
|
async cleanupStoredConnectionParams() {
|
|
20130
20378
|
await this.transport.setProtocol(void 0);
|
|
20131
20379
|
await this.updateAuthorizationStateInStorage(void 0);
|
|
@@ -20374,6 +20622,9 @@ var ClientSessionWrapper = class {
|
|
|
20374
20622
|
get iceServers() {
|
|
20375
20623
|
return this.clientSessionManager.iceServers;
|
|
20376
20624
|
}
|
|
20625
|
+
get callControl() {
|
|
20626
|
+
return this.clientSessionManager.callControl;
|
|
20627
|
+
}
|
|
20377
20628
|
async execute(request, options) {
|
|
20378
20629
|
return this.clientSessionManager.execute(request, options);
|
|
20379
20630
|
}
|
|
@@ -20614,7 +20865,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20614
20865
|
method: "POST",
|
|
20615
20866
|
uri: DEVICE_TOKEN_ENDPOINT
|
|
20616
20867
|
});
|
|
20617
|
-
const response = await this.http.request({
|
|
20868
|
+
const response = await this.http().request({
|
|
20618
20869
|
url: DEVICE_TOKEN_ENDPOINT,
|
|
20619
20870
|
...POST_PARAMS,
|
|
20620
20871
|
body: JSON.stringify({
|
|
@@ -20641,7 +20892,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20641
20892
|
uri: DEVICE_REFRESH_ENDPOINT,
|
|
20642
20893
|
accessToken: currentToken
|
|
20643
20894
|
});
|
|
20644
|
-
const response = await this.http.request({
|
|
20895
|
+
const response = await this.http().request({
|
|
20645
20896
|
url: DEVICE_REFRESH_ENDPOINT,
|
|
20646
20897
|
...POST_PARAMS,
|
|
20647
20898
|
body: JSON.stringify({
|
|
@@ -21694,37 +21945,17 @@ var TransportManager = class extends Destroyable {
|
|
|
21694
21945
|
}
|
|
21695
21946
|
};
|
|
21696
21947
|
|
|
21697
|
-
//#endregion
|
|
21698
|
-
//#region src/utils/authRecovery.ts
|
|
21699
|
-
/**
|
|
21700
|
-
* Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
|
|
21701
|
-
* Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
|
|
21702
|
-
* underlying signaling error is nested. Bounded by a visited set to guard
|
|
21703
|
-
* against cyclic causes.
|
|
21704
|
-
*/
|
|
21705
|
-
function findJSONRPCError(error) {
|
|
21706
|
-
const seen = /* @__PURE__ */ new Set();
|
|
21707
|
-
let current = error;
|
|
21708
|
-
while (current instanceof Error && !seen.has(current)) {
|
|
21709
|
-
seen.add(current);
|
|
21710
|
-
if (current instanceof JSONRPCError) return current;
|
|
21711
|
-
current = current.error ?? current.cause;
|
|
21712
|
-
}
|
|
21713
|
-
}
|
|
21714
|
-
/**
|
|
21715
|
-
* Whether an error is a session-recoverable authentication failure
|
|
21716
|
-
* (`-32002` authentication failed or `-32003` requester validation failed)
|
|
21717
|
-
* that a credential re-mint + retry can heal.
|
|
21718
|
-
*/
|
|
21719
|
-
function isRecoverableAuthError(error) {
|
|
21720
|
-
const rpcError = findJSONRPCError(error);
|
|
21721
|
-
return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
21722
|
-
}
|
|
21723
|
-
|
|
21724
21948
|
//#endregion
|
|
21725
21949
|
//#region src/clients/SignalWire.ts
|
|
21726
21950
|
var import_cjs$1 = require_cjs();
|
|
21727
21951
|
const logger$1 = getLogger();
|
|
21952
|
+
/**
|
|
21953
|
+
* Storage key for the client-bound marker. The SAT and authorization_state are
|
|
21954
|
+
* both opaque to the SDK, so on a page reload the preflight recovery — which
|
|
21955
|
+
* runs before any session exists — has no other way to know the session was
|
|
21956
|
+
* client-bound. See {@link SignalWire.persistClientBoundMarker}.
|
|
21957
|
+
*/
|
|
21958
|
+
const CLIENT_BOUND_STORAGE_KEY = "sw:client_bound";
|
|
21728
21959
|
const buildOptionsFromDestination = (destination) => {
|
|
21729
21960
|
if (typeof destination === "string") {
|
|
21730
21961
|
const queryStartIndex = destination.indexOf("?");
|
|
@@ -21768,6 +21999,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21768
21999
|
this.preferences = new ClientPreferences();
|
|
21769
22000
|
this._user$ = this.createBehaviorSubject(void 0);
|
|
21770
22001
|
this._directory$ = this.createBehaviorSubject(void 0);
|
|
22002
|
+
this._credentialRecovered = false;
|
|
21771
22003
|
this._isConnected$ = this.createBehaviorSubject(false);
|
|
21772
22004
|
this._isRegistered$ = this.createBehaviorSubject(false);
|
|
21773
22005
|
this._errors$ = this.createReplaySubject(1);
|
|
@@ -21806,6 +22038,16 @@ var SignalWire = class extends Destroyable {
|
|
|
21806
22038
|
});
|
|
21807
22039
|
}
|
|
21808
22040
|
/**
|
|
22041
|
+
* Build the refresh path's own HTTP controller, against whatever host is current.
|
|
22042
|
+
*
|
|
22043
|
+
* Called on first use rather than up front, so `apiHost` already reflects the
|
|
22044
|
+
* token's `ch` claim. Same credential source as the container's controller — only
|
|
22045
|
+
* the instance, and therefore its observable streams, is separate.
|
|
22046
|
+
*/
|
|
22047
|
+
createRefreshHttpController() {
|
|
22048
|
+
return new HTTPRequestController(this._deps.apiHost, () => this._deps.credential);
|
|
22049
|
+
}
|
|
22050
|
+
/**
|
|
21809
22051
|
* Initializes DPoP if not already set up. Returns the fingerprint on success.
|
|
21810
22052
|
*/
|
|
21811
22053
|
async initDPoP() {
|
|
@@ -21832,7 +22074,14 @@ var SignalWire = class extends Destroyable {
|
|
|
21832
22074
|
async resolveCredentials() {
|
|
21833
22075
|
const fingerprint = await this.initDPoP();
|
|
21834
22076
|
this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
|
|
21835
|
-
http:
|
|
22077
|
+
http: () => {
|
|
22078
|
+
if (!this._refreshHttp || this._refreshHttpHost !== this._deps.apiHost) {
|
|
22079
|
+
this._refreshHttp?.destroy();
|
|
22080
|
+
this._refreshHttp = this.createRefreshHttpController();
|
|
22081
|
+
this._refreshHttpHost = this._deps.apiHost;
|
|
22082
|
+
}
|
|
22083
|
+
return this._refreshHttp;
|
|
22084
|
+
},
|
|
21836
22085
|
notifier: {
|
|
21837
22086
|
onError: (error) => this._errors$.next(error),
|
|
21838
22087
|
onWarning: (warning) => this._warnings$.next(warning),
|
|
@@ -21916,43 +22165,76 @@ var SignalWire = class extends Destroyable {
|
|
|
21916
22165
|
}
|
|
21917
22166
|
}
|
|
21918
22167
|
/**
|
|
21919
|
-
*
|
|
21920
|
-
*
|
|
21921
|
-
* timer that was throttled while the tab was backgrounded.
|
|
22168
|
+
* Recover a session the server is refusing: mint a fresh credential,
|
|
22169
|
+
* reauthenticate the live session with it, and retry the operation.
|
|
21922
22170
|
*
|
|
21923
|
-
*
|
|
21924
|
-
*
|
|
21925
|
-
*
|
|
21926
|
-
*
|
|
21927
|
-
*
|
|
21928
|
-
*
|
|
21929
|
-
*
|
|
21930
|
-
* refresh; re-minting a base SAT here would drop the DPoP binding).
|
|
22171
|
+
* The connection is deliberately kept. A reload authenticates the new socket
|
|
22172
|
+
* against the persisted `authorization_state`, and that handshake is what
|
|
22173
|
+
* associates the socket with the previous session — the association reattach
|
|
22174
|
+
* depends on. `signalwire.reauthenticate` swaps the credential *on that same
|
|
22175
|
+
* session*, so recovery never touches the resume state. Discarding it would
|
|
22176
|
+
* heal the credential by destroying the very thing the caller is trying to
|
|
22177
|
+
* get back to.
|
|
21931
22178
|
*
|
|
21932
|
-
*
|
|
21933
|
-
*
|
|
21934
|
-
*
|
|
21935
|
-
*
|
|
22179
|
+
* The operation is still the verdict, never the RPC. Reauthenticating with
|
|
22180
|
+
* the in-memory token is accepted by a resume even while requests stay
|
|
22181
|
+
* refused, because the persisted `authorization_state` short-circuits token
|
|
22182
|
+
* validation — and `signalwire.reauthenticate` with a *freshly minted* token
|
|
22183
|
+
* has also been observed accepted while `subscriber.online` keeps being
|
|
22184
|
+
* refused (staging run 33826974634). Both look like success and are not.
|
|
22185
|
+
*
|
|
22186
|
+
* @returns the operation's value, or the reason recovery could not deliver
|
|
22187
|
+
* one. `error` is undefined when there was no way to mint at all.
|
|
21936
22188
|
*/
|
|
21937
|
-
async
|
|
21938
|
-
|
|
21939
|
-
|
|
21940
|
-
await
|
|
21941
|
-
|
|
22189
|
+
async recoverAndRetry(operation) {
|
|
22190
|
+
if (!await this.remintAndReauthenticate()) return { ok: false };
|
|
22191
|
+
try {
|
|
22192
|
+
const value = await operation();
|
|
22193
|
+
this._credentialRecovered = true;
|
|
22194
|
+
return {
|
|
22195
|
+
ok: true,
|
|
22196
|
+
value
|
|
22197
|
+
};
|
|
21942
22198
|
} catch (error) {
|
|
21943
|
-
logger$1.
|
|
22199
|
+
logger$1.warn("[SignalWire] Reauthentication was accepted but the operation is still refused:", error);
|
|
22200
|
+
return {
|
|
22201
|
+
ok: false,
|
|
22202
|
+
error
|
|
22203
|
+
};
|
|
22204
|
+
}
|
|
22205
|
+
}
|
|
22206
|
+
/**
|
|
22207
|
+
* Re-mint a credential and adopt it only if the live session accepts it.
|
|
22208
|
+
*
|
|
22209
|
+
* The mechanism follows the binding: a client-bound session re-mints a bound
|
|
22210
|
+
* base SAT through `authenticate()` with the DPoP fingerprint, because the
|
|
22211
|
+
* developer refresh handler would hand back an unbound token and silently
|
|
22212
|
+
* degrade the session. An unbound session uses the refresh handler. Rotation
|
|
22213
|
+
* cost is not a reason to skip this — the only reason is having no mechanism.
|
|
22214
|
+
*
|
|
22215
|
+
* @returns whether the session is now running on a freshly accepted credential.
|
|
22216
|
+
*/
|
|
22217
|
+
async remintAndReauthenticate() {
|
|
22218
|
+
const provider = this._credentialProvider;
|
|
22219
|
+
if (!provider) return false;
|
|
22220
|
+
const { clientBound } = this._clientSession;
|
|
22221
|
+
if (!clientBound && !provider.refresh) {
|
|
22222
|
+
logger$1.debug("[SignalWire] [SW-NO-REFRESH-HANDLER] Unbound session with no refresh handler; cannot re-mint.");
|
|
22223
|
+
return false;
|
|
21944
22224
|
}
|
|
21945
|
-
if (!allowRemint || this._clientSession.clientBound || !this._credentialProvider?.refresh) return false;
|
|
21946
22225
|
try {
|
|
21947
|
-
const newCredentials = await
|
|
21948
|
-
if (!newCredentials.token)
|
|
22226
|
+
const newCredentials = clientBound ? await provider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0) : await this.remintCredential(provider);
|
|
22227
|
+
if (!newCredentials.token) {
|
|
22228
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the current one.");
|
|
22229
|
+
return false;
|
|
22230
|
+
}
|
|
22231
|
+
await this._clientSession.reauthenticate(newCredentials.token);
|
|
21949
22232
|
this._deps.credential = newCredentials;
|
|
21950
22233
|
this.persistCredential(newCredentials);
|
|
21951
|
-
|
|
21952
|
-
if (newCredentials.expiry_at) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
22234
|
+
if (newCredentials.expiry_at && provider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(provider, newCredentials.expiry_at);
|
|
21953
22235
|
return true;
|
|
21954
22236
|
} catch (error) {
|
|
21955
|
-
logger$1.
|
|
22237
|
+
logger$1.warn("[SignalWire] Re-mint recovery failed:", error);
|
|
21956
22238
|
return false;
|
|
21957
22239
|
}
|
|
21958
22240
|
}
|
|
@@ -21987,10 +22269,9 @@ var SignalWire = class extends Destroyable {
|
|
|
21987
22269
|
if (!this._credentialProvider) return;
|
|
21988
22270
|
try {
|
|
21989
22271
|
let newCredentials;
|
|
21990
|
-
if (this._clientSession.
|
|
21991
|
-
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
22272
|
+
if (this._clientSession?.clientBound ?? await this.wasClientBound()) {
|
|
21992
22273
|
logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
|
|
21993
|
-
newCredentials = await this._credentialProvider.authenticate(
|
|
22274
|
+
newCredentials = await this._credentialProvider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0);
|
|
21994
22275
|
} else if (this._credentialProvider.refresh) {
|
|
21995
22276
|
logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
|
|
21996
22277
|
newCredentials = await this.remintCredential(this._credentialProvider);
|
|
@@ -22018,6 +22299,26 @@ var SignalWire = class extends Destroyable {
|
|
|
22018
22299
|
this._deps.storage.setItem("sw:cached_credential", credential);
|
|
22019
22300
|
if (this._deps.persistSession) this._deps.storage.setItem("sw:cached_credential", credential, "local");
|
|
22020
22301
|
}
|
|
22302
|
+
/**
|
|
22303
|
+
* Persist whether the session is client-bound, mirroring the credential's
|
|
22304
|
+
* storage scopes so it survives a reload. The preflight recovery reads it
|
|
22305
|
+
* before any session exists to decide whether to re-bind via `authenticate()`
|
|
22306
|
+
* or refresh an unbound token; the marker tracks the latest binding, so an
|
|
22307
|
+
* unbound reconnect clears a stale marker from an earlier client-bound login.
|
|
22308
|
+
*/
|
|
22309
|
+
persistClientBoundMarker(bound) {
|
|
22310
|
+
const scopes = this._deps.persistSession ? ["session", "local"] : ["session"];
|
|
22311
|
+
for (const scope of scopes) if (bound) this._deps.storage.setItem(CLIENT_BOUND_STORAGE_KEY, true, scope);
|
|
22312
|
+
else this._deps.storage.removeItem(CLIENT_BOUND_STORAGE_KEY, scope);
|
|
22313
|
+
}
|
|
22314
|
+
/** Read the persisted client-bound marker (see {@link persistClientBoundMarker}). */
|
|
22315
|
+
async wasClientBound() {
|
|
22316
|
+
const scopes = this._deps.persistSession ? ["local", "session"] : ["session"];
|
|
22317
|
+
for (const scope of scopes) try {
|
|
22318
|
+
if (await this._deps.storage.getItem(CLIENT_BOUND_STORAGE_KEY, scope)) return true;
|
|
22319
|
+
} catch {}
|
|
22320
|
+
return false;
|
|
22321
|
+
}
|
|
22021
22322
|
async init() {
|
|
22022
22323
|
this._user$.next(new User(this._deps.http));
|
|
22023
22324
|
if (!this._options.skipConnection) await this.connect();
|
|
@@ -22041,6 +22342,42 @@ var SignalWire = class extends Destroyable {
|
|
|
22041
22342
|
} catch (error) {
|
|
22042
22343
|
logger$1.error("[SignalWire] Failed to reattach calls:", error);
|
|
22043
22344
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22345
|
+
} finally {
|
|
22346
|
+
this._credentialRecovered = false;
|
|
22347
|
+
}
|
|
22348
|
+
}
|
|
22349
|
+
/**
|
|
22350
|
+
* Fetch the authenticated user profile, recovering a stale credential.
|
|
22351
|
+
*
|
|
22352
|
+
* On a reload the persisted credential can be expired. Unlike the WS resume —
|
|
22353
|
+
* which the server accepts against the persisted `authorization_state` even
|
|
22354
|
+
* with an expired token — this REST preflight has no such short-circuit and is
|
|
22355
|
+
* refused (401). There is no session yet to reauthenticate, so recovery
|
|
22356
|
+
* re-mints the credential through the provider ({@link refreshCredentialForReconnect})
|
|
22357
|
+
* and retries with a FRESH {@link User}: Fetchable memoizes its result
|
|
22358
|
+
* (shareReplay), so reusing the instance would replay the 401 instead of
|
|
22359
|
+
* re-fetching with the new token. Without the user id the transport/session —
|
|
22360
|
+
* and the reattach a reload is trying to preserve — cannot even be addressed.
|
|
22361
|
+
*/
|
|
22362
|
+
async fetchUserOrRecover() {
|
|
22363
|
+
const fetchUser = async (user$1) => {
|
|
22364
|
+
if (!await (0, import_cjs$1.firstValueFrom)(user$1.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
22365
|
+
this._deps.user = user$1;
|
|
22366
|
+
};
|
|
22367
|
+
const user = this._user$.value;
|
|
22368
|
+
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
22369
|
+
try {
|
|
22370
|
+
await fetchUser(user);
|
|
22371
|
+
} catch (firstError) {
|
|
22372
|
+
logger$1.error(`[SignalWire] Failed to fetch user information: ${firstError instanceof Error ? firstError.message : "Unknown error"}. This usually means the user token is invalid or expired. Re-minting the credential and retrying.`);
|
|
22373
|
+
try {
|
|
22374
|
+
await this.refreshCredentialForReconnect();
|
|
22375
|
+
const refetched = new User(this._deps.http);
|
|
22376
|
+
await fetchUser(refetched);
|
|
22377
|
+
this._user$.next(refetched);
|
|
22378
|
+
} catch (retryError) {
|
|
22379
|
+
throw new UnexpectedError("Error fetching user information", { cause: retryError });
|
|
22380
|
+
}
|
|
22044
22381
|
}
|
|
22045
22382
|
}
|
|
22046
22383
|
/**
|
|
@@ -22082,28 +22419,22 @@ var SignalWire = class extends Destroyable {
|
|
|
22082
22419
|
*/
|
|
22083
22420
|
async connect() {
|
|
22084
22421
|
await this.teardownTransportAndSession();
|
|
22085
|
-
|
|
22086
|
-
const user = this._user$.value;
|
|
22087
|
-
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
22088
|
-
if (!await (0, import_cjs$1.firstValueFrom)(user.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
22089
|
-
this._deps.user = user;
|
|
22090
|
-
} catch (error) {
|
|
22091
|
-
logger$1.error(`[SignalWire] Failed to fetch user information: ${error instanceof Error ? error.message : "Unknown error"}. This usually means the user token is invalid or expired.`);
|
|
22092
|
-
throw new UnexpectedError("Error fetching user information", { cause: error });
|
|
22093
|
-
}
|
|
22422
|
+
await this.fetchUserOrRecover();
|
|
22094
22423
|
const errorHandler = (error) => {
|
|
22095
22424
|
this._errors$.next(error);
|
|
22096
22425
|
};
|
|
22097
22426
|
this._transport = new TransportManager(this._deps.storage, this._deps.protocolKey, this._deps.WebSocket, PreferencesContainer.instance.relayHost ?? this._deps.relayHost, errorHandler);
|
|
22098
|
-
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
|
|
22427
|
+
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey, () => this._credentialRecovered);
|
|
22099
22428
|
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$);
|
|
22100
22429
|
this._publicSession = new ClientSessionWrapper(this._clientSession);
|
|
22430
|
+
this._clientSession.callControl = this._options.callControl ?? "routed";
|
|
22101
22431
|
this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
|
|
22102
22432
|
this.subscribeTo(this._clientSession.errors$, (error) => {
|
|
22103
22433
|
this._errors$.next(error);
|
|
22104
22434
|
});
|
|
22105
22435
|
this.subscribeTo(this._clientSession.authorization$, (authorization) => {
|
|
22106
22436
|
this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
|
|
22437
|
+
this.persistClientBoundMarker(Boolean(authorization?.cnf?.jkt));
|
|
22107
22438
|
});
|
|
22108
22439
|
await this._clientSession.connect();
|
|
22109
22440
|
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
@@ -22357,19 +22688,18 @@ var SignalWire = class extends Destroyable {
|
|
|
22357
22688
|
throw error;
|
|
22358
22689
|
}
|
|
22359
22690
|
logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
|
|
22360
|
-
|
|
22361
|
-
if (await this.recoverStaleCredential(isRecoverableAuthError(error))) try {
|
|
22362
|
-
logger$1.debug("[SignalWire] Recovery succeeded, retrying register()");
|
|
22691
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
22363
22692
|
await this._transport.execute(RPCExecute({
|
|
22364
22693
|
method: "subscriber.online",
|
|
22365
22694
|
params: {}
|
|
22366
22695
|
}));
|
|
22696
|
+
});
|
|
22697
|
+
if (outcome.ok) {
|
|
22698
|
+
logger$1.debug("[SignalWire] Recovery restored registration");
|
|
22367
22699
|
this._isRegistered$.next(true);
|
|
22368
22700
|
return;
|
|
22369
|
-
} catch (retryError) {
|
|
22370
|
-
logger$1.error("[SignalWire] Register retry after recovery failed:", retryError);
|
|
22371
|
-
failureCause = retryError;
|
|
22372
22701
|
}
|
|
22702
|
+
const failureCause = outcome.error ?? error;
|
|
22373
22703
|
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 }) });
|
|
22374
22704
|
this._errors$.next(registerError);
|
|
22375
22705
|
throw registerError;
|
|
@@ -22429,10 +22759,13 @@ var SignalWire = class extends Destroyable {
|
|
|
22429
22759
|
return await this._clientSession.createOutboundCall(destination, computed_options);
|
|
22430
22760
|
} catch (error) {
|
|
22431
22761
|
if (!isRecoverableAuthError(error)) throw error;
|
|
22432
|
-
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering
|
|
22433
|
-
|
|
22434
|
-
|
|
22435
|
-
|
|
22762
|
+
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering the session and retrying");
|
|
22763
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
22764
|
+
await this.waitAuthentication();
|
|
22765
|
+
return this._clientSession.createOutboundCall(destination, computed_options);
|
|
22766
|
+
});
|
|
22767
|
+
if (outcome.ok) return outcome.value;
|
|
22768
|
+
throw outcome.error ?? error;
|
|
22436
22769
|
}
|
|
22437
22770
|
}
|
|
22438
22771
|
/**
|
|
@@ -22714,6 +23047,8 @@ var SignalWire = class extends Destroyable {
|
|
|
22714
23047
|
destroy() {
|
|
22715
23048
|
this._refreshCoordinator?.destroy();
|
|
22716
23049
|
this._refreshCoordinator = void 0;
|
|
23050
|
+
this._refreshHttp?.destroy();
|
|
23051
|
+
this._refreshHttp = void 0;
|
|
22717
23052
|
this._dpopManager?.destroy();
|
|
22718
23053
|
const session = this._clientSession;
|
|
22719
23054
|
session?.teardownSessionState();
|