@signalwire/js 4.0.0-dev-20260717142935 → 4.0.0-dev-20260721191222
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 +347 -57
- package/dist/browser.mjs.map +1 -1
- package/dist/browser.umd.js +347 -57
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +340 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +59 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +340 -58
- package/dist/index.mjs.map +1 -1
- package/dist/operators/index.cjs +1 -1
- package/dist/operators/index.mjs +1 -1
- package/dist/{operators-D3_VhSb1.cjs → operators-Bn4Ij3VB.cjs} +15 -1
- package/dist/operators-Bn4Ij3VB.cjs.map +1 -0
- package/dist/{operators-Cgz8swPu.mjs → operators-Zxmwpb0j.mjs} +10 -2
- package/dist/operators-Zxmwpb0j.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/operators-Cgz8swPu.mjs.map +0 -1
- package/dist/operators-D3_VhSb1.cjs.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
|
|
@@ -20093,9 +20101,9 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20093
20101
|
}
|
|
20094
20102
|
async handleAuthenticationError(error) {
|
|
20095
20103
|
logger$9.error("Authentication error:", error);
|
|
20096
|
-
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);
|
|
20097
20105
|
const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
|
|
20098
|
-
if (isRecoverableAuthError && hasStoredState) {
|
|
20106
|
+
if (isRecoverableAuthError$1 && hasStoredState) {
|
|
20099
20107
|
logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
|
|
20100
20108
|
try {
|
|
20101
20109
|
await this.cleanupStoredConnectionParams();
|
|
@@ -20191,11 +20199,15 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20191
20199
|
const isReconnect = hasReconnectState && storedToken;
|
|
20192
20200
|
let dpopToken;
|
|
20193
20201
|
if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
|
|
20194
|
-
else
|
|
20195
|
-
|
|
20196
|
-
|
|
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
|
+
}
|
|
20197
20209
|
}
|
|
20198
|
-
if (
|
|
20210
|
+
if (this.dpopManager?.initialized) try {
|
|
20199
20211
|
dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
|
|
20200
20212
|
} catch (error) {
|
|
20201
20213
|
if (this.clientBound) throw error;
|
|
@@ -20228,6 +20240,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20228
20240
|
});
|
|
20229
20241
|
if (response.protocol) await this.transport.setProtocol(response.protocol);
|
|
20230
20242
|
this._authorization$.next(response.authorization);
|
|
20243
|
+
if (response.authorization.cnf?.jkt) this._wasClientBound = true;
|
|
20231
20244
|
this._iceServers$.next(response.ice_servers ?? []);
|
|
20232
20245
|
this._authState$.next({ kind: "authenticated" });
|
|
20233
20246
|
logger$9.debug("[Session] Authentication completed successfully");
|
|
@@ -20347,6 +20360,14 @@ var ClientSessionWrapper = class {
|
|
|
20347
20360
|
get authenticated() {
|
|
20348
20361
|
return this.clientSessionManager.authenticated;
|
|
20349
20362
|
}
|
|
20363
|
+
/**
|
|
20364
|
+
* Whether the session is using a Client Bound SAT (DPoP). Sticky — set
|
|
20365
|
+
* when the binding is established or restored from a resumed session's
|
|
20366
|
+
* server authorization.
|
|
20367
|
+
*/
|
|
20368
|
+
get clientBound() {
|
|
20369
|
+
return this.clientSessionManager.clientBound;
|
|
20370
|
+
}
|
|
20350
20371
|
get signalingEvent$() {
|
|
20351
20372
|
return this.clientSessionManager.signalingEvent$;
|
|
20352
20373
|
}
|
|
@@ -20549,7 +20570,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20549
20570
|
await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
|
|
20550
20571
|
updateCredential({ token: tokenData.token });
|
|
20551
20572
|
logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
|
|
20552
|
-
this.
|
|
20573
|
+
this.emitCurrentToken(tokenData);
|
|
20553
20574
|
return { activated: true };
|
|
20554
20575
|
} catch (error) {
|
|
20555
20576
|
logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
|
|
@@ -20561,6 +20582,21 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20561
20582
|
}
|
|
20562
20583
|
}
|
|
20563
20584
|
/**
|
|
20585
|
+
* Emit a freshly received token to the reactive pipeline, stamping an
|
|
20586
|
+
* absolute `expires_at` when the response carried only `expires_in`.
|
|
20587
|
+
* Resolving the expiry at RECEIVE time (not at read time) is what lets
|
|
20588
|
+
* {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
|
|
20589
|
+
* re-resolved later would always compute a full TTL from "now" and never
|
|
20590
|
+
* cross the refresh buffer.
|
|
20591
|
+
*/
|
|
20592
|
+
emitCurrentToken(token) {
|
|
20593
|
+
const stamped = token.expires_at ? token : {
|
|
20594
|
+
...token,
|
|
20595
|
+
expires_at: resolveExpiresAt(token)
|
|
20596
|
+
};
|
|
20597
|
+
this._currentToken$.next(stamped);
|
|
20598
|
+
}
|
|
20599
|
+
/**
|
|
20564
20600
|
* Returns true when the cached token has enough headroom before expiry to
|
|
20565
20601
|
* be safely reused on reactivation. The headroom matches the refresh
|
|
20566
20602
|
* buffer, so a token within the refresh window is treated as stale (the
|
|
@@ -20653,7 +20689,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20653
20689
|
const currentToken = this.getCredential().token;
|
|
20654
20690
|
if (!currentToken) throw new TokenRefreshError("No current token available for refresh");
|
|
20655
20691
|
const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
|
|
20656
|
-
this.
|
|
20692
|
+
this.emitCurrentToken(newTokenData);
|
|
20657
20693
|
} catch (error) {
|
|
20658
20694
|
logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
|
|
20659
20695
|
this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
|
|
@@ -20680,6 +20716,22 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20680
20716
|
throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
|
|
20681
20717
|
}
|
|
20682
20718
|
/**
|
|
20719
|
+
* Force an immediate refresh when the cached Client Bound SAT is already
|
|
20720
|
+
* past its refresh window. Called on resume from suspension where
|
|
20721
|
+
* background-tab throttling can delay the reactive timer past the buffer.
|
|
20722
|
+
* A no-op when no token is cached or it still has headroom; the normal
|
|
20723
|
+
* {@link executeRefresh} guards (paused / in-progress / unauthenticated)
|
|
20724
|
+
* still apply.
|
|
20725
|
+
*/
|
|
20726
|
+
refreshNowIfDue() {
|
|
20727
|
+
const token = this._currentToken$.value;
|
|
20728
|
+
if (!token) return;
|
|
20729
|
+
if (resolveExpiresAt(token) * 1e3 - Date.now() <= DEVICE_TOKEN_REFRESH_BUFFER_MS) {
|
|
20730
|
+
logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
|
|
20731
|
+
this.executeRefresh();
|
|
20732
|
+
}
|
|
20733
|
+
}
|
|
20734
|
+
/**
|
|
20683
20735
|
* Stops the reactive refresh pipeline from firing. Use when the underlying
|
|
20684
20736
|
* session is being torn down (e.g., during {@link SignalWire.disconnect})
|
|
20685
20737
|
* so a scheduled refresh cannot fire against a destroyed session.
|
|
@@ -20730,6 +20782,7 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20730
20782
|
this.deps = deps;
|
|
20731
20783
|
this._activating = false;
|
|
20732
20784
|
this._activationGeneration = 0;
|
|
20785
|
+
this._developerRefreshInProgress = false;
|
|
20733
20786
|
if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
|
|
20734
20787
|
}
|
|
20735
20788
|
/** True when the Client Bound SAT path is available (DPoP initialized). */
|
|
@@ -20748,28 +20801,120 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20748
20801
|
* invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
|
|
20749
20802
|
*/
|
|
20750
20803
|
scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
|
|
20804
|
+
this._activeProvider = provider;
|
|
20751
20805
|
if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
|
|
20752
20806
|
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);
|
|
20753
|
-
this._developerTimerId = setTimeout(
|
|
20807
|
+
this._developerTimerId = setTimeout(() => {
|
|
20808
|
+
this._developerTimerId = void 0;
|
|
20809
|
+
this.executeDeveloperRefresh(provider, expiresAt, attempt);
|
|
20810
|
+
}, refreshInterval);
|
|
20811
|
+
}
|
|
20812
|
+
/**
|
|
20813
|
+
* Runs the developer-provided refresh once: mints a new credential, stores
|
|
20814
|
+
* and persists it, reauthenticates the live session (via the notifier), and
|
|
20815
|
+
* reschedules against the new expiry. On failure retries with backoff up to
|
|
20816
|
+
* {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
|
|
20817
|
+
*
|
|
20818
|
+
* Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
|
|
20819
|
+
* `_developerRefreshInProgress` guard prevents the two from overlapping.
|
|
20820
|
+
*/
|
|
20821
|
+
async executeDeveloperRefresh(provider, expiresAt, attempt) {
|
|
20822
|
+
if (this._developerRefreshInProgress) {
|
|
20823
|
+
logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
|
|
20824
|
+
return;
|
|
20825
|
+
}
|
|
20826
|
+
this._developerRefreshInProgress = true;
|
|
20827
|
+
try {
|
|
20828
|
+
const newCredentials = await this.refreshCredential(provider);
|
|
20829
|
+
this.deps.store.write(newCredentials);
|
|
20830
|
+
this.deps.store.persist(newCredentials);
|
|
20754
20831
|
try {
|
|
20755
|
-
|
|
20756
|
-
|
|
20757
|
-
|
|
20758
|
-
this.deps.store.persist(newCredentials);
|
|
20759
|
-
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
20760
|
-
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
20761
|
-
} catch (error) {
|
|
20762
|
-
const nextAttempt = attempt + 1;
|
|
20763
|
-
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
20764
|
-
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
20765
|
-
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
20766
|
-
else {
|
|
20767
|
-
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
20768
|
-
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
20769
|
-
this.deps.notifier.onRefreshExhausted();
|
|
20770
|
-
}
|
|
20832
|
+
await this.deps.notifier.onCredentialRefreshed(newCredentials);
|
|
20833
|
+
} catch (reauthError) {
|
|
20834
|
+
logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
|
|
20771
20835
|
}
|
|
20772
|
-
|
|
20836
|
+
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
20837
|
+
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
20838
|
+
} catch (error) {
|
|
20839
|
+
const nextAttempt = attempt + 1;
|
|
20840
|
+
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
20841
|
+
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
20842
|
+
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
20843
|
+
else {
|
|
20844
|
+
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
20845
|
+
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
20846
|
+
this.deps.notifier.onRefreshExhausted();
|
|
20847
|
+
}
|
|
20848
|
+
} finally {
|
|
20849
|
+
this._developerRefreshInProgress = false;
|
|
20850
|
+
}
|
|
20851
|
+
}
|
|
20852
|
+
/**
|
|
20853
|
+
* Force an immediate refresh when the current credential is already past its
|
|
20854
|
+
* scheduled refresh window. Called on resume from suspension, where
|
|
20855
|
+
* background-tab timer throttling can delay the armed refresh well past
|
|
20856
|
+
* expiry, leaving the live session stale.
|
|
20857
|
+
*
|
|
20858
|
+
* Routes to whichever mechanism is armed: the developer timer if armed,
|
|
20859
|
+
* otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
|
|
20860
|
+
*/
|
|
20861
|
+
forceRefreshIfDue() {
|
|
20862
|
+
if (this._developerTimerId !== void 0 && this._activeProvider) {
|
|
20863
|
+
const expiry = this.deps.store.read().expiry_at;
|
|
20864
|
+
if (expiry !== void 0 && Date.now() >= expiry - CREDENTIAL_REFRESH_BUFFER_MS) {
|
|
20865
|
+
logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
|
|
20866
|
+
clearTimeout(this._developerTimerId);
|
|
20867
|
+
this._developerTimerId = void 0;
|
|
20868
|
+
this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
|
|
20869
|
+
}
|
|
20870
|
+
return;
|
|
20871
|
+
}
|
|
20872
|
+
this._deviceTokenManager?.refreshNowIfDue();
|
|
20873
|
+
}
|
|
20874
|
+
/**
|
|
20875
|
+
* Sync the credential's expiry from the server-provided authorization (the
|
|
20876
|
+
* `signalwire.connect` result). SATs are opaque JWE, so
|
|
20877
|
+
* `fabric_subscriber.expires_at` is the authoritative expiry of the token
|
|
20878
|
+
* the session actually connected with — the provider-reported `expiry_at`
|
|
20879
|
+
* is only a hint (and may be wrong or absent). Corrects the stored
|
|
20880
|
+
* credential and re-arms the developer refresh timer against the real
|
|
20881
|
+
* deadline when the provider supports `refresh()`.
|
|
20882
|
+
*/
|
|
20883
|
+
syncExpiryFromAuthorization(authorization, provider) {
|
|
20884
|
+
const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
|
|
20885
|
+
if (!expiresAtSec) return;
|
|
20886
|
+
const expiryAt = expiresAtSec * 1e3;
|
|
20887
|
+
const credential = this.deps.store.read();
|
|
20888
|
+
if (credential.expiry_at === expiryAt) return;
|
|
20889
|
+
logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
|
|
20890
|
+
const updated = {
|
|
20891
|
+
...credential,
|
|
20892
|
+
expiry_at: expiryAt
|
|
20893
|
+
};
|
|
20894
|
+
this.deps.store.write(updated);
|
|
20895
|
+
this.deps.store.persist(updated);
|
|
20896
|
+
if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
|
|
20897
|
+
}
|
|
20898
|
+
/**
|
|
20899
|
+
* Invoke `provider.refresh()` deduped against any concurrent developer
|
|
20900
|
+
* refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
|
|
20901
|
+
* and the orchestrator's -32003 recovery / reconnect re-mint — share one
|
|
20902
|
+
* in-flight promise, so a provider backed by one-time-use rotating refresh
|
|
20903
|
+
* tokens is never invoked twice in parallel.
|
|
20904
|
+
*
|
|
20905
|
+
* The caller owns applying the returned credential (store write, session
|
|
20906
|
+
* reauth, rescheduling); this method only serializes the network call.
|
|
20907
|
+
*/
|
|
20908
|
+
async refreshCredential(provider) {
|
|
20909
|
+
if (this._refreshInFlight) return this._refreshInFlight;
|
|
20910
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
20911
|
+
const run = provider.refresh();
|
|
20912
|
+
this._refreshInFlight = run;
|
|
20913
|
+
const clear = () => {
|
|
20914
|
+
if (this._refreshInFlight === run) this._refreshInFlight = void 0;
|
|
20915
|
+
};
|
|
20916
|
+
run.then(clear, clear);
|
|
20917
|
+
return run;
|
|
20773
20918
|
}
|
|
20774
20919
|
/**
|
|
20775
20920
|
* Cancels any scheduled developer-provided refresh. Idempotent.
|
|
@@ -21549,6 +21694,33 @@ var TransportManager = class extends Destroyable {
|
|
|
21549
21694
|
}
|
|
21550
21695
|
};
|
|
21551
21696
|
|
|
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
|
+
|
|
21552
21724
|
//#endregion
|
|
21553
21725
|
//#region src/clients/SignalWire.ts
|
|
21554
21726
|
var import_cjs$1 = require_cjs();
|
|
@@ -21664,7 +21836,8 @@ var SignalWire = class extends Destroyable {
|
|
|
21664
21836
|
notifier: {
|
|
21665
21837
|
onError: (error) => this._errors$.next(error),
|
|
21666
21838
|
onWarning: (warning) => this._warnings$.next(warning),
|
|
21667
|
-
onRefreshExhausted: () => void this.disconnect()
|
|
21839
|
+
onRefreshExhausted: () => void this.disconnect(),
|
|
21840
|
+
onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
|
|
21668
21841
|
},
|
|
21669
21842
|
store: {
|
|
21670
21843
|
read: () => this._deps.credential,
|
|
@@ -21676,6 +21849,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21676
21849
|
...this._deps.credential,
|
|
21677
21850
|
...partial
|
|
21678
21851
|
};
|
|
21852
|
+
this.persistCredential(this._deps.credential);
|
|
21679
21853
|
},
|
|
21680
21854
|
persist: (credential) => this.persistCredential(credential)
|
|
21681
21855
|
}
|
|
@@ -21721,14 +21895,123 @@ var SignalWire = class extends Destroyable {
|
|
|
21721
21895
|
}
|
|
21722
21896
|
this._deps.credential = _credentials;
|
|
21723
21897
|
this.persistCredential(_credentials);
|
|
21724
|
-
|
|
21725
|
-
|
|
21898
|
+
await this.reauthenticateLiveSession(_credentials);
|
|
21899
|
+
}
|
|
21900
|
+
/**
|
|
21901
|
+
* Reauthenticate the currently-open session with a freshly obtained
|
|
21902
|
+
* credential so the new token takes effect on the live socket immediately —
|
|
21903
|
+
* not just on the next reconnect. No-op when the session is not
|
|
21904
|
+
* connected/authenticated or the credential carries no token (e.g. an
|
|
21905
|
+
* authorization-state-only refresh). Non-fatal: reauth failures surface on
|
|
21906
|
+
* `errors$` without aborting the refresh that triggered this.
|
|
21907
|
+
*/
|
|
21908
|
+
async reauthenticateLiveSession(credential) {
|
|
21909
|
+
if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
|
|
21910
|
+
try {
|
|
21911
|
+
await this._clientSession.reauthenticate(credential.token);
|
|
21726
21912
|
logger$1.info("[SignalWire] Session refreshed with new credentials.");
|
|
21727
21913
|
} catch (error) {
|
|
21728
21914
|
logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
|
|
21729
21915
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21730
21916
|
}
|
|
21731
21917
|
}
|
|
21918
|
+
/**
|
|
21919
|
+
* One-shot recovery for a live session that failed with a recoverable auth
|
|
21920
|
+
* error (`-32002`/`-32003`) because its token went stale — e.g. a refresh
|
|
21921
|
+
* timer that was throttled while the tab was backgrounded.
|
|
21922
|
+
*
|
|
21923
|
+
* Two steps, first that succeeds wins:
|
|
21924
|
+
* 1. Reauthenticate with the in-memory token (a fresh DPoP proof is
|
|
21925
|
+
* generated automatically). Heals a session whose token was already
|
|
21926
|
+
* refreshed by the coordinator but never applied to the open socket,
|
|
21927
|
+
* and a client-bound session whose server-side auth merely drifted.
|
|
21928
|
+
* 2. Re-mint via the developer provider's `refresh()` and reauthenticate.
|
|
21929
|
+
* Skipped for client-bound sessions (the DeviceTokenManager owns their
|
|
21930
|
+
* refresh; re-minting a base SAT here would drop the DPoP binding).
|
|
21931
|
+
*
|
|
21932
|
+
* @param allowRemint - Whether step 2 (provider re-mint) may run. Callers
|
|
21933
|
+
* pass `false` for non-auth failures so a transient network error never
|
|
21934
|
+
* escalates to a token re-mint; step 1 (in-memory reauth) always runs.
|
|
21935
|
+
* @returns `true` if the session was reauthenticated, `false` otherwise.
|
|
21936
|
+
*/
|
|
21937
|
+
async recoverStaleCredential(allowRemint = true) {
|
|
21938
|
+
const currentToken = this._deps.credential.token;
|
|
21939
|
+
if (currentToken) try {
|
|
21940
|
+
await this._clientSession.reauthenticate(currentToken);
|
|
21941
|
+
return true;
|
|
21942
|
+
} catch (error) {
|
|
21943
|
+
logger$1.debug("[SignalWire] In-memory reauth failed during recovery:", error);
|
|
21944
|
+
}
|
|
21945
|
+
if (!allowRemint || this._clientSession.clientBound || !this._credentialProvider?.refresh) return false;
|
|
21946
|
+
try {
|
|
21947
|
+
const newCredentials = await this.remintCredential(this._credentialProvider);
|
|
21948
|
+
if (!newCredentials.token) return false;
|
|
21949
|
+
this._deps.credential = newCredentials;
|
|
21950
|
+
this.persistCredential(newCredentials);
|
|
21951
|
+
await this._clientSession.reauthenticate(newCredentials.token);
|
|
21952
|
+
if (newCredentials.expiry_at) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
21953
|
+
return true;
|
|
21954
|
+
} catch (error) {
|
|
21955
|
+
logger$1.error("[SignalWire] Credential recovery failed:", error);
|
|
21956
|
+
return false;
|
|
21957
|
+
}
|
|
21958
|
+
}
|
|
21959
|
+
/**
|
|
21960
|
+
* Re-mint a credential via `provider.refresh()`, routed through the
|
|
21961
|
+
* coordinator's shared in-flight guard so concurrent re-mint paths (a
|
|
21962
|
+
* scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
|
|
21963
|
+
* second `provider.refresh()` in parallel — which rotating one-time-use
|
|
21964
|
+
* refresh tokens reject. Falls back to a direct call only if the coordinator
|
|
21965
|
+
* has not been constructed yet.
|
|
21966
|
+
*/
|
|
21967
|
+
async remintCredential(provider) {
|
|
21968
|
+
if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
|
|
21969
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
21970
|
+
return provider.refresh();
|
|
21971
|
+
}
|
|
21972
|
+
/**
|
|
21973
|
+
* Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
|
|
21974
|
+
* The session invokes this only when it is client-bound OR the in-memory
|
|
21975
|
+
* token is expired. The re-mint mechanism depends on the binding:
|
|
21976
|
+
* - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
|
|
21977
|
+
* fresh base SAT the upcoming reconnect can re-bind (the
|
|
21978
|
+
* DeviceTokenManager re-activates afterwards).
|
|
21979
|
+
* - Unbound: the developer's non-interactive `refresh()` handler.
|
|
21980
|
+
* `authenticate()` is deliberately NOT used here — it may be interactive
|
|
21981
|
+
* (a login prompt) and must not fire on a background reconnect.
|
|
21982
|
+
*
|
|
21983
|
+
* Rejects on failure so the session aborts the reconnect rather than
|
|
21984
|
+
* replaying a stale token.
|
|
21985
|
+
*/
|
|
21986
|
+
async refreshCredentialForReconnect() {
|
|
21987
|
+
if (!this._credentialProvider) return;
|
|
21988
|
+
try {
|
|
21989
|
+
let newCredentials;
|
|
21990
|
+
if (this._clientSession.clientBound) {
|
|
21991
|
+
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
21992
|
+
logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
|
|
21993
|
+
newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
|
|
21994
|
+
} else if (this._credentialProvider.refresh) {
|
|
21995
|
+
logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
|
|
21996
|
+
newCredentials = await this.remintCredential(this._credentialProvider);
|
|
21997
|
+
} else {
|
|
21998
|
+
logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
|
|
21999
|
+
return;
|
|
22000
|
+
}
|
|
22001
|
+
if (!newCredentials.token) {
|
|
22002
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
|
|
22003
|
+
return;
|
|
22004
|
+
}
|
|
22005
|
+
this._deps.credential = newCredentials;
|
|
22006
|
+
this.persistCredential(newCredentials);
|
|
22007
|
+
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
22008
|
+
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
22009
|
+
} catch (error) {
|
|
22010
|
+
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
22011
|
+
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22012
|
+
throw error;
|
|
22013
|
+
}
|
|
22014
|
+
}
|
|
21732
22015
|
/** Persist credential to localStorage when persistSession is enabled. */
|
|
21733
22016
|
persistCredential(credential) {
|
|
21734
22017
|
if (!credential.token) return;
|
|
@@ -21815,24 +22098,13 @@ var SignalWire = class extends Destroyable {
|
|
|
21815
22098
|
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
|
|
21816
22099
|
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$);
|
|
21817
22100
|
this._publicSession = new ClientSessionWrapper(this._clientSession);
|
|
21818
|
-
this._clientSession.onBeforeReconnect = async () =>
|
|
21819
|
-
if (!this._credentialProvider) return;
|
|
21820
|
-
try {
|
|
21821
|
-
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
21822
|
-
logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
|
|
21823
|
-
const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
|
|
21824
|
-
this._deps.credential = newCredentials;
|
|
21825
|
-
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
21826
|
-
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
21827
|
-
} catch (error) {
|
|
21828
|
-
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
21829
|
-
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21830
|
-
throw error;
|
|
21831
|
-
}
|
|
21832
|
-
};
|
|
22101
|
+
this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
|
|
21833
22102
|
this.subscribeTo(this._clientSession.errors$, (error) => {
|
|
21834
22103
|
this._errors$.next(error);
|
|
21835
22104
|
});
|
|
22105
|
+
this.subscribeTo(this._clientSession.authorization$, (authorization) => {
|
|
22106
|
+
this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
|
|
22107
|
+
});
|
|
21836
22108
|
await this._clientSession.connect();
|
|
21837
22109
|
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
21838
22110
|
this.subscribeTo(this._clientSession.authenticated$.pipe((0, import_cjs$1.skip)(1), (0, import_cjs$1.filter)(Boolean)), async () => {
|
|
@@ -21995,6 +22267,13 @@ var SignalWire = class extends Destroyable {
|
|
|
21995
22267
|
}
|
|
21996
22268
|
try {
|
|
21997
22269
|
this._visibilityController = new VisibilityController();
|
|
22270
|
+
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible")), () => {
|
|
22271
|
+
try {
|
|
22272
|
+
this._refreshCoordinator?.forceRefreshIfDue();
|
|
22273
|
+
} catch (error) {
|
|
22274
|
+
logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
|
|
22275
|
+
}
|
|
22276
|
+
});
|
|
21998
22277
|
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
|
|
21999
22278
|
logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
|
|
22000
22279
|
try {
|
|
@@ -22077,21 +22356,23 @@ var SignalWire = class extends Destroyable {
|
|
|
22077
22356
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22078
22357
|
throw error;
|
|
22079
22358
|
}
|
|
22080
|
-
logger$1.debug("[SignalWire] Failed to register user,
|
|
22081
|
-
|
|
22082
|
-
|
|
22083
|
-
logger$1.debug("[SignalWire]
|
|
22359
|
+
logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
|
|
22360
|
+
let failureCause = error;
|
|
22361
|
+
if (await this.recoverStaleCredential(isRecoverableAuthError(error))) try {
|
|
22362
|
+
logger$1.debug("[SignalWire] Recovery succeeded, retrying register()");
|
|
22084
22363
|
await this._transport.execute(RPCExecute({
|
|
22085
22364
|
method: "subscriber.online",
|
|
22086
22365
|
params: {}
|
|
22087
22366
|
}));
|
|
22088
22367
|
this._isRegistered$.next(true);
|
|
22089
|
-
|
|
22090
|
-
|
|
22091
|
-
|
|
22092
|
-
|
|
22093
|
-
throw registerError;
|
|
22368
|
+
return;
|
|
22369
|
+
} catch (retryError) {
|
|
22370
|
+
logger$1.error("[SignalWire] Register retry after recovery failed:", retryError);
|
|
22371
|
+
failureCause = retryError;
|
|
22094
22372
|
}
|
|
22373
|
+
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
|
+
this._errors$.next(registerError);
|
|
22375
|
+
throw registerError;
|
|
22095
22376
|
}
|
|
22096
22377
|
}
|
|
22097
22378
|
/**
|
|
@@ -22144,7 +22425,15 @@ var SignalWire = class extends Destroyable {
|
|
|
22144
22425
|
};
|
|
22145
22426
|
await this.waitAuthentication();
|
|
22146
22427
|
logger$1.debug("[SignalWire] Dialing with options:", computed_options);
|
|
22147
|
-
|
|
22428
|
+
try {
|
|
22429
|
+
return await this._clientSession.createOutboundCall(destination, computed_options);
|
|
22430
|
+
} catch (error) {
|
|
22431
|
+
if (!isRecoverableAuthError(error)) throw error;
|
|
22432
|
+
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering credential and retrying once");
|
|
22433
|
+
if (!await this.recoverStaleCredential()) throw error;
|
|
22434
|
+
await this.waitAuthentication();
|
|
22435
|
+
return this._clientSession.createOutboundCall(destination, computed_options);
|
|
22436
|
+
}
|
|
22148
22437
|
}
|
|
22149
22438
|
/**
|
|
22150
22439
|
* Runs a multi-phase connectivity test against the given destination.
|
|
@@ -22426,9 +22715,10 @@ var SignalWire = class extends Destroyable {
|
|
|
22426
22715
|
this._refreshCoordinator?.destroy();
|
|
22427
22716
|
this._refreshCoordinator = void 0;
|
|
22428
22717
|
this._dpopManager?.destroy();
|
|
22429
|
-
this._clientSession
|
|
22430
|
-
|
|
22431
|
-
this.
|
|
22718
|
+
const session = this._clientSession;
|
|
22719
|
+
session?.teardownSessionState();
|
|
22720
|
+
this._transport?.destroy();
|
|
22721
|
+
session?.destroy();
|
|
22432
22722
|
try {
|
|
22433
22723
|
this._networkMonitor?.destroy();
|
|
22434
22724
|
} catch {}
|