@signalwire/js 4.0.0-dev-20260826210511 → 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 +255 -121
- package/dist/browser.mjs.map +1 -1
- package/dist/browser.umd.js +255 -121
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +255 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -20
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +100 -20
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +255 -121
- 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
|
/**
|
|
@@ -14508,33 +14562,6 @@ function throwOnRPCError() {
|
|
|
14508
14562
|
});
|
|
14509
14563
|
}
|
|
14510
14564
|
|
|
14511
|
-
//#endregion
|
|
14512
|
-
//#region src/utils/unwrapVertoReply.ts
|
|
14513
|
-
/**
|
|
14514
|
-
* Unwrap a method's own reply from a `webrtc.verto` envelope.
|
|
14515
|
-
*
|
|
14516
|
-
* A control verb sent in-dialog comes back nested two levels deep:
|
|
14517
|
-
*
|
|
14518
|
-
* ```
|
|
14519
|
-
* { result: { node_id, code, result: { jsonrpc, id, result: <method payload> } } }
|
|
14520
|
-
* ```
|
|
14521
|
-
*
|
|
14522
|
-
* whereas the routed transport resolves the method payload directly under
|
|
14523
|
-
* `.result`. Readers want the latter shape, and the difference is silent when it
|
|
14524
|
-
* is wrong — `response.result.layouts` simply evaluates to `undefined` against the
|
|
14525
|
-
* envelope, so the data arrives, nothing throws, and the caller sees an empty
|
|
14526
|
-
* value. That exact failure produced an empty layout dropdown with a successful
|
|
14527
|
-
* request behind it.
|
|
14528
|
-
*
|
|
14529
|
-
* Accepting both shapes here keeps every reader indifferent to which transport
|
|
14530
|
-
* produced the response. Anything that is not a nested envelope (a plain ack, or
|
|
14531
|
-
* an already-unwrapped reply) passes through untouched.
|
|
14532
|
-
*/
|
|
14533
|
-
function unwrapVertoReply(response) {
|
|
14534
|
-
const inner = getValueFrom(response, "result.result");
|
|
14535
|
-
return inner && typeof inner === "object" && "result" in inner ? inner : response;
|
|
14536
|
-
}
|
|
14537
|
-
|
|
14538
14565
|
//#endregion
|
|
14539
14566
|
//#region src/managers/CallEventsManager.ts
|
|
14540
14567
|
var import_cjs$20 = require_cjs();
|
|
@@ -14727,10 +14754,14 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14727
14754
|
updateLayouts() {
|
|
14728
14755
|
if (!this.selfId) return;
|
|
14729
14756
|
this.webRtcCallSession.executeMethod(this.selfId, "call.layout.list", {}).then((response) => {
|
|
14730
|
-
const
|
|
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
|
+
}
|
|
14731
14762
|
this._sessionState$.next({
|
|
14732
14763
|
...this._sessionState$.value,
|
|
14733
|
-
layouts
|
|
14764
|
+
layouts
|
|
14734
14765
|
});
|
|
14735
14766
|
}).catch((error) => {
|
|
14736
14767
|
logger$22.error("[CallEventsManager] Error fetching layouts:", error);
|
|
@@ -17125,7 +17156,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17125
17156
|
}
|
|
17126
17157
|
}));
|
|
17127
17158
|
const failure = findNestedVertoFailure(response);
|
|
17128
|
-
if (failure) throw new JSONRPCError(Number.parseInt(failure.code, 10) || 0,
|
|
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);
|
|
17129
17160
|
return response;
|
|
17130
17161
|
}
|
|
17131
17162
|
async executeVerto(message, optionals = {}) {
|
|
@@ -18433,6 +18464,33 @@ function mosToQualityLevel(mos) {
|
|
|
18433
18464
|
return "critical";
|
|
18434
18465
|
}
|
|
18435
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
|
+
|
|
18436
18494
|
//#endregion
|
|
18437
18495
|
//#region src/core/entities/Call.ts
|
|
18438
18496
|
var import_cjs$11 = require_cjs();
|
|
@@ -20283,17 +20341,14 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20283
20341
|
const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
|
|
20284
20342
|
if (isRecoverableAuthError$1 && hasStoredState) {
|
|
20285
20343
|
logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
|
|
20286
|
-
|
|
20287
|
-
await this.cleanupStoredConnectionParams();
|
|
20288
|
-
} catch (cleanupError) {
|
|
20289
|
-
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
20290
|
-
} finally {
|
|
20291
|
-
this.transport.reconnect();
|
|
20292
|
-
}
|
|
20344
|
+
await this.discardResumeStateAndReconnect();
|
|
20293
20345
|
} else this._errors$.next(error);
|
|
20294
20346
|
}
|
|
20295
20347
|
/**
|
|
20296
|
-
* 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.
|
|
20297
20352
|
*
|
|
20298
20353
|
* This is the stale-auth-state recovery helper used by handleAuthError:
|
|
20299
20354
|
* the server rejected a reconnect, so the resume state is discarded and a
|
|
@@ -20301,9 +20356,24 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20301
20356
|
* session lives on through the reconnect and reattachCalls() needs the
|
|
20302
20357
|
* stored call references afterwards. Do NOT add detachAll() here.
|
|
20303
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
|
+
*
|
|
20304
20365
|
* For public teardown (disconnect/destroy), use {@link teardownSessionState}
|
|
20305
20366
|
* instead, which clears the attach records as well.
|
|
20306
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
|
+
}
|
|
20307
20377
|
async cleanupStoredConnectionParams() {
|
|
20308
20378
|
await this.transport.setProtocol(void 0);
|
|
20309
20379
|
await this.updateAuthorizationStateInStorage(void 0);
|
|
@@ -21875,37 +21945,17 @@ var TransportManager = class extends Destroyable {
|
|
|
21875
21945
|
}
|
|
21876
21946
|
};
|
|
21877
21947
|
|
|
21878
|
-
//#endregion
|
|
21879
|
-
//#region src/utils/authRecovery.ts
|
|
21880
|
-
/**
|
|
21881
|
-
* Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
|
|
21882
|
-
* Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
|
|
21883
|
-
* underlying signaling error is nested. Bounded by a visited set to guard
|
|
21884
|
-
* against cyclic causes.
|
|
21885
|
-
*/
|
|
21886
|
-
function findJSONRPCError(error) {
|
|
21887
|
-
const seen = /* @__PURE__ */ new Set();
|
|
21888
|
-
let current = error;
|
|
21889
|
-
while (current instanceof Error && !seen.has(current)) {
|
|
21890
|
-
seen.add(current);
|
|
21891
|
-
if (current instanceof JSONRPCError) return current;
|
|
21892
|
-
current = current.error ?? current.cause;
|
|
21893
|
-
}
|
|
21894
|
-
}
|
|
21895
|
-
/**
|
|
21896
|
-
* Whether an error is a session-recoverable authentication failure
|
|
21897
|
-
* (`-32002` authentication failed or `-32003` requester validation failed)
|
|
21898
|
-
* that a credential re-mint + retry can heal.
|
|
21899
|
-
*/
|
|
21900
|
-
function isRecoverableAuthError(error) {
|
|
21901
|
-
const rpcError = findJSONRPCError(error);
|
|
21902
|
-
return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
21903
|
-
}
|
|
21904
|
-
|
|
21905
21948
|
//#endregion
|
|
21906
21949
|
//#region src/clients/SignalWire.ts
|
|
21907
21950
|
var import_cjs$1 = require_cjs();
|
|
21908
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";
|
|
21909
21959
|
const buildOptionsFromDestination = (destination) => {
|
|
21910
21960
|
if (typeof destination === "string") {
|
|
21911
21961
|
const queryStartIndex = destination.indexOf("?");
|
|
@@ -21949,6 +21999,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21949
21999
|
this.preferences = new ClientPreferences();
|
|
21950
22000
|
this._user$ = this.createBehaviorSubject(void 0);
|
|
21951
22001
|
this._directory$ = this.createBehaviorSubject(void 0);
|
|
22002
|
+
this._credentialRecovered = false;
|
|
21952
22003
|
this._isConnected$ = this.createBehaviorSubject(false);
|
|
21953
22004
|
this._isRegistered$ = this.createBehaviorSubject(false);
|
|
21954
22005
|
this._errors$ = this.createReplaySubject(1);
|
|
@@ -22114,43 +22165,76 @@ var SignalWire = class extends Destroyable {
|
|
|
22114
22165
|
}
|
|
22115
22166
|
}
|
|
22116
22167
|
/**
|
|
22117
|
-
*
|
|
22118
|
-
*
|
|
22119
|
-
* 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.
|
|
22120
22170
|
*
|
|
22121
|
-
*
|
|
22122
|
-
*
|
|
22123
|
-
*
|
|
22124
|
-
*
|
|
22125
|
-
*
|
|
22126
|
-
*
|
|
22127
|
-
*
|
|
22128
|
-
* 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.
|
|
22129
22178
|
*
|
|
22130
|
-
*
|
|
22131
|
-
*
|
|
22132
|
-
*
|
|
22133
|
-
*
|
|
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.
|
|
22134
22188
|
*/
|
|
22135
|
-
async
|
|
22136
|
-
|
|
22137
|
-
|
|
22138
|
-
await
|
|
22139
|
-
|
|
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
|
+
};
|
|
22140
22198
|
} catch (error) {
|
|
22141
|
-
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;
|
|
22142
22224
|
}
|
|
22143
|
-
if (!allowRemint || this._clientSession.clientBound || !this._credentialProvider?.refresh) return false;
|
|
22144
22225
|
try {
|
|
22145
|
-
const newCredentials = await
|
|
22146
|
-
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);
|
|
22147
22232
|
this._deps.credential = newCredentials;
|
|
22148
22233
|
this.persistCredential(newCredentials);
|
|
22149
|
-
|
|
22150
|
-
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);
|
|
22151
22235
|
return true;
|
|
22152
22236
|
} catch (error) {
|
|
22153
|
-
logger$1.
|
|
22237
|
+
logger$1.warn("[SignalWire] Re-mint recovery failed:", error);
|
|
22154
22238
|
return false;
|
|
22155
22239
|
}
|
|
22156
22240
|
}
|
|
@@ -22185,10 +22269,9 @@ var SignalWire = class extends Destroyable {
|
|
|
22185
22269
|
if (!this._credentialProvider) return;
|
|
22186
22270
|
try {
|
|
22187
22271
|
let newCredentials;
|
|
22188
|
-
if (this._clientSession.
|
|
22189
|
-
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
22272
|
+
if (this._clientSession?.clientBound ?? await this.wasClientBound()) {
|
|
22190
22273
|
logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
|
|
22191
|
-
newCredentials = await this._credentialProvider.authenticate(
|
|
22274
|
+
newCredentials = await this._credentialProvider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0);
|
|
22192
22275
|
} else if (this._credentialProvider.refresh) {
|
|
22193
22276
|
logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
|
|
22194
22277
|
newCredentials = await this.remintCredential(this._credentialProvider);
|
|
@@ -22216,6 +22299,26 @@ var SignalWire = class extends Destroyable {
|
|
|
22216
22299
|
this._deps.storage.setItem("sw:cached_credential", credential);
|
|
22217
22300
|
if (this._deps.persistSession) this._deps.storage.setItem("sw:cached_credential", credential, "local");
|
|
22218
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
|
+
}
|
|
22219
22322
|
async init() {
|
|
22220
22323
|
this._user$.next(new User(this._deps.http));
|
|
22221
22324
|
if (!this._options.skipConnection) await this.connect();
|
|
@@ -22239,6 +22342,42 @@ var SignalWire = class extends Destroyable {
|
|
|
22239
22342
|
} catch (error) {
|
|
22240
22343
|
logger$1.error("[SignalWire] Failed to reattach calls:", error);
|
|
22241
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
|
+
}
|
|
22242
22381
|
}
|
|
22243
22382
|
}
|
|
22244
22383
|
/**
|
|
@@ -22280,20 +22419,12 @@ var SignalWire = class extends Destroyable {
|
|
|
22280
22419
|
*/
|
|
22281
22420
|
async connect() {
|
|
22282
22421
|
await this.teardownTransportAndSession();
|
|
22283
|
-
|
|
22284
|
-
const user = this._user$.value;
|
|
22285
|
-
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
22286
|
-
if (!await (0, import_cjs$1.firstValueFrom)(user.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
22287
|
-
this._deps.user = user;
|
|
22288
|
-
} catch (error) {
|
|
22289
|
-
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.`);
|
|
22290
|
-
throw new UnexpectedError("Error fetching user information", { cause: error });
|
|
22291
|
-
}
|
|
22422
|
+
await this.fetchUserOrRecover();
|
|
22292
22423
|
const errorHandler = (error) => {
|
|
22293
22424
|
this._errors$.next(error);
|
|
22294
22425
|
};
|
|
22295
22426
|
this._transport = new TransportManager(this._deps.storage, this._deps.protocolKey, this._deps.WebSocket, PreferencesContainer.instance.relayHost ?? this._deps.relayHost, errorHandler);
|
|
22296
|
-
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);
|
|
22297
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$);
|
|
22298
22429
|
this._publicSession = new ClientSessionWrapper(this._clientSession);
|
|
22299
22430
|
this._clientSession.callControl = this._options.callControl ?? "routed";
|
|
@@ -22303,6 +22434,7 @@ var SignalWire = class extends Destroyable {
|
|
|
22303
22434
|
});
|
|
22304
22435
|
this.subscribeTo(this._clientSession.authorization$, (authorization) => {
|
|
22305
22436
|
this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
|
|
22437
|
+
this.persistClientBoundMarker(Boolean(authorization?.cnf?.jkt));
|
|
22306
22438
|
});
|
|
22307
22439
|
await this._clientSession.connect();
|
|
22308
22440
|
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
@@ -22556,19 +22688,18 @@ var SignalWire = class extends Destroyable {
|
|
|
22556
22688
|
throw error;
|
|
22557
22689
|
}
|
|
22558
22690
|
logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
|
|
22559
|
-
|
|
22560
|
-
if (await this.recoverStaleCredential(isRecoverableAuthError(error))) try {
|
|
22561
|
-
logger$1.debug("[SignalWire] Recovery succeeded, retrying register()");
|
|
22691
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
22562
22692
|
await this._transport.execute(RPCExecute({
|
|
22563
22693
|
method: "subscriber.online",
|
|
22564
22694
|
params: {}
|
|
22565
22695
|
}));
|
|
22696
|
+
});
|
|
22697
|
+
if (outcome.ok) {
|
|
22698
|
+
logger$1.debug("[SignalWire] Recovery restored registration");
|
|
22566
22699
|
this._isRegistered$.next(true);
|
|
22567
22700
|
return;
|
|
22568
|
-
} catch (retryError) {
|
|
22569
|
-
logger$1.error("[SignalWire] Register retry after recovery failed:", retryError);
|
|
22570
|
-
failureCause = retryError;
|
|
22571
22701
|
}
|
|
22702
|
+
const failureCause = outcome.error ?? error;
|
|
22572
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 }) });
|
|
22573
22704
|
this._errors$.next(registerError);
|
|
22574
22705
|
throw registerError;
|
|
@@ -22628,10 +22759,13 @@ var SignalWire = class extends Destroyable {
|
|
|
22628
22759
|
return await this._clientSession.createOutboundCall(destination, computed_options);
|
|
22629
22760
|
} catch (error) {
|
|
22630
22761
|
if (!isRecoverableAuthError(error)) throw error;
|
|
22631
|
-
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering
|
|
22632
|
-
|
|
22633
|
-
|
|
22634
|
-
|
|
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;
|
|
22635
22769
|
}
|
|
22636
22770
|
}
|
|
22637
22771
|
/**
|