UNPKG

convex

Version:

Client for the Convex Cloud

313 lines (312 loc) 10.7 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; var authentication_manager_exports = {}; __export(authentication_manager_exports, { AuthenticationManager: () => AuthenticationManager }); module.exports = __toCommonJS(authentication_manager_exports); var import_jwt_decode = __toESM(require("jwt-decode"), 1); const MAXIMUM_REFRESH_DELAY = 20 * 24 * 60 * 60 * 1e3; class AuthenticationManager { constructor(syncState, { authenticate, stopSocket, restartSocket, pauseSocket, resumeSocket, clearAuth, verbose }) { __publicField(this, "authState", { state: "noAuth" }); // Used to detect races involving `setConfig` calls // while a token is being fetched. __publicField(this, "configVersion", 0); // Shared by the BaseClient so that the auth manager can easily inspect it __publicField(this, "syncState"); // Passed down by BaseClient, sends a message to the server __publicField(this, "authenticate"); __publicField(this, "stopSocket"); __publicField(this, "restartSocket"); __publicField(this, "pauseSocket"); __publicField(this, "resumeSocket"); // Passed down by BaseClient, sends a message to the server __publicField(this, "clearAuth"); __publicField(this, "verbose"); this.syncState = syncState; this.authenticate = authenticate; this.stopSocket = stopSocket; this.restartSocket = restartSocket; this.pauseSocket = pauseSocket; this.resumeSocket = resumeSocket; this.clearAuth = clearAuth; this.verbose = verbose; } async setConfig(fetchToken, onChange) { this.resetAuthState(); this._logVerbose("pausing WS for auth token fetch"); this.pauseSocket(); const token = await this.fetchTokenAndGuardAgainstRace(fetchToken, { forceRefreshToken: false }); if (token.isFromOutdatedConfig) { return; } if (token.value) { this.setAuthState({ state: "waitingForServerConfirmationOfCachedToken", config: { fetchToken, onAuthChange: onChange }, hasRetried: false }); this.authenticate(token.value); this._logVerbose("resuming WS after auth token fetch"); this.resumeSocket(); } else { this.setAuthState({ state: "initialRefetch", config: { fetchToken, onAuthChange: onChange } }); await this.refetchToken(); } } onTransition(serverMessage) { if (!this.syncState.isCurrentOrNewerAuthVersion( serverMessage.endVersion.identity )) { return; } if (serverMessage.endVersion.identity <= serverMessage.startVersion.identity) { return; } if (this.authState.state === "waitingForServerConfirmationOfCachedToken") { this._logVerbose("server confirmed auth token is valid"); void this.refetchToken(); this.authState.config.onAuthChange(true); return; } if (this.authState.state === "waitingForServerConfirmationOfFreshToken") { this._logVerbose("server confirmed new auth token is valid"); this.scheduleTokenRefetch(this.authState.token); if (!this.authState.hadAuth) { this.authState.config.onAuthChange(true); } } } onAuthError(serverMessage) { const { baseVersion } = serverMessage; if (baseVersion !== null && baseVersion !== void 0) { if (!this.syncState.isCurrentOrNewerAuthVersion(baseVersion + 1)) { this._logVerbose("ignoring auth error for previous auth attempt"); return; } void this.tryToReauthenticate(serverMessage); return; } void this.tryToReauthenticate(serverMessage); } // This is similar to `refetchToken` defined below, in fact we // don't represent them as different states, but it is different // in that we pause the WebSocket so that mutations // don't retry with bad auth. async tryToReauthenticate(serverMessage) { if ( // No way to fetch another token, kaboom this.authState.state === "noAuth" || // We failed on a fresh token, trying another one won't help this.authState.state === "waitingForServerConfirmationOfFreshToken" ) { console.error( `Failed to authenticate: "${serverMessage.error}", check your server auth config` ); if (this.syncState.hasAuth()) { this.syncState.clearAuth(); } if (this.authState.state !== "noAuth") { this.setAndReportAuthFailed(this.authState.config.onAuthChange); } return; } this._logVerbose("attempting to reauthenticate"); await this.stopSocket(); const token = await this.fetchTokenAndGuardAgainstRace( this.authState.config.fetchToken, { forceRefreshToken: true } ); if (token.isFromOutdatedConfig) { return; } if (token.value && this.syncState.isNewAuth(token.value)) { this.authenticate(token.value); this.setAuthState({ state: "waitingForServerConfirmationOfFreshToken", config: this.authState.config, token: token.value, hadAuth: this.authState.state === "notRefetching" || this.authState.state === "waitingForScheduledRefetch" }); } else { this._logVerbose("reauthentication failed, could not fetch a new token"); if (this.syncState.hasAuth()) { this.syncState.clearAuth(); } this.setAndReportAuthFailed(this.authState.config.onAuthChange); } this.restartSocket(); } // Force refetch the token and schedule another refetch // before the token expires - an active client should never // need to reauthenticate. async refetchToken() { if (this.authState.state === "noAuth") { return; } this._logVerbose("refetching auth token"); const token = await this.fetchTokenAndGuardAgainstRace( this.authState.config.fetchToken, { forceRefreshToken: true } ); if (token.isFromOutdatedConfig) { return; } if (token.value) { if (this.syncState.isNewAuth(token.value)) { this.setAuthState({ state: "waitingForServerConfirmationOfFreshToken", hadAuth: this.syncState.hasAuth(), token: token.value, config: this.authState.config }); this.authenticate(token.value); } else { this.setAuthState({ state: "notRefetching", config: this.authState.config }); } } else { this._logVerbose("refetching token failed"); if (this.syncState.hasAuth()) { this.clearAuth(); } this.setAndReportAuthFailed(this.authState.config.onAuthChange); } this._logVerbose( "resuming WS after auth token fetch (if currently paused)" ); this.resumeSocket(); } scheduleTokenRefetch(token) { if (this.authState.state === "noAuth") { return; } const decodedToken = this.decodeToken(token); if (!decodedToken) { console.error("Auth token is not a valid JWT, cannot refetch the token"); return; } const { iat, exp } = decodedToken; if (!iat || !exp) { console.error( "Auth token does not have required fields, cannot refetch the token" ); return; } const leewaySeconds = 2; const delay = Math.min( MAXIMUM_REFRESH_DELAY, (exp - iat - leewaySeconds) * 1e3 ); if (delay <= 0) { console.error( "Auth token does not live long enough, cannot refetch the token" ); return; } const refetchTokenTimeoutId = setTimeout(() => { void this.refetchToken(); }, delay); this.setAuthState({ state: "waitingForScheduledRefetch", refetchTokenTimeoutId, config: this.authState.config }); this._logVerbose( `scheduled preemptive auth token refetching in ${delay}ms` ); } // Protects against simultaneous calls to `setConfig` // while we're fetching a token async fetchTokenAndGuardAgainstRace(fetchToken, fetchArgs) { const originalConfigVersion = ++this.configVersion; const token = await fetchToken(fetchArgs); if (this.configVersion !== originalConfigVersion) { return { isFromOutdatedConfig: true }; } return { isFromOutdatedConfig: false, value: token }; } stop() { this.resetAuthState(); this.configVersion++; } setAndReportAuthFailed(onAuthChange) { onAuthChange(false); this.resetAuthState(); } resetAuthState() { this.setAuthState({ state: "noAuth" }); } setAuthState(newAuth) { if (this.authState.state === "waitingForScheduledRefetch") { clearTimeout(this.authState.refetchTokenTimeoutId); this.syncState.markAuthCompletion(); } this.authState = newAuth; } decodeToken(token) { try { return (0, import_jwt_decode.default)(token); } catch (e) { return null; } } _logVerbose(message) { if (this.verbose) { console.debug( `${(/* @__PURE__ */ new Date()).toISOString()} ${message} [v${this.configVersion}]` ); } } } //# sourceMappingURL=authentication_manager.js.map