convex
Version:
Client for the Convex Cloud
313 lines (312 loc) • 10.7 kB
JavaScript
"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