UNPKG

@propelauth/javascript

Version:

A library for managing authentication in the browser, backed by PropelAuth

1,048 lines (1,009 loc) 34.8 kB
/*! js-cookie v3.0.5 | MIT */ /* eslint-disable no-var */ function assign (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { target[key] = source[key]; } } return target } /* eslint-enable no-var */ /* eslint-disable no-var */ var defaultConverter = { read: function (value) { if (value[0] === '"') { value = value.slice(1, -1); } return value.replace(/(%[\dA-F]{2})+/gi, decodeURIComponent) }, write: function (value) { return encodeURIComponent(value).replace( /%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g, decodeURIComponent ) } }; /* eslint-enable no-var */ /* eslint-disable no-var */ function init (converter, defaultAttributes) { function set (name, value, attributes) { if (typeof document === 'undefined') { return } attributes = assign({}, defaultAttributes, attributes); if (typeof attributes.expires === 'number') { attributes.expires = new Date(Date.now() + attributes.expires * 864e5); } if (attributes.expires) { attributes.expires = attributes.expires.toUTCString(); } name = encodeURIComponent(name) .replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent) .replace(/[()]/g, escape); var stringifiedAttributes = ''; for (var attributeName in attributes) { if (!attributes[attributeName]) { continue } stringifiedAttributes += '; ' + attributeName; if (attributes[attributeName] === true) { continue } // Considers RFC 6265 section 5.2: // ... // 3. If the remaining unparsed-attributes contains a %x3B (";") // character: // Consume the characters of the unparsed-attributes up to, // not including, the first %x3B (";") character. // ... stringifiedAttributes += '=' + attributes[attributeName].split(';')[0]; } return (document.cookie = name + '=' + converter.write(value, name) + stringifiedAttributes) } function get (name) { if (typeof document === 'undefined' || (arguments.length && !name)) { return } // To prevent the for loop in the first place assign an empty array // in case there are no cookies at all. var cookies = document.cookie ? document.cookie.split('; ') : []; var jar = {}; for (var i = 0; i < cookies.length; i++) { var parts = cookies[i].split('='); var value = parts.slice(1).join('='); try { var found = decodeURIComponent(parts[0]); jar[found] = converter.read(value, found); if (name === found) { break } } catch (e) {} } return name ? jar[name] : jar } return Object.create( { set, get, remove: function (name, attributes) { set( name, '', assign({}, attributes, { expires: -1 }) ); }, withAttributes: function (attributes) { return init(this.converter, assign({}, this.attributes, attributes)) }, withConverter: function (converter) { return init(assign({}, this.converter, converter), this.attributes) } }, { attributes: { value: Object.freeze(defaultAttributes) }, converter: { value: Object.freeze(converter) } } ) } var api = init(defaultConverter, { path: '/' }); const ACTIVE_ORG_ID_COOKIE_NAME = "active_org_id"; let OrgRoleStructure = /*#__PURE__*/function (OrgRoleStructure) { OrgRoleStructure["SingleRole"] = "single_role_in_hierarchy"; OrgRoleStructure["MultiRole"] = "multi_role"; return OrgRoleStructure; }({}); const setActiveOrgId = orgId => { api.set(ACTIVE_ORG_ID_COOKIE_NAME, orgId, { sameSite: "lax", secure: true }); }; const getActiveOrgId = () => { return api.get(ACTIVE_ORG_ID_COOKIE_NAME); }; function getAccessHelper(orgIdToOrgMemberInfo) { function isRole(orgId, role) { const orgMemberInfo = orgIdToOrgMemberInfo[orgId]; if (orgMemberInfo === undefined) { return false; } if (orgMemberInfo.orgRoleStructure === OrgRoleStructure.MultiRole) { return orgMemberInfo.userAssignedRole === role || orgMemberInfo.userAssignedAdditionalRoles.includes(role); } else { return orgMemberInfo.userAssignedRole === role; } } function isAtLeastRole(orgId, role) { const orgMemberInfo = orgIdToOrgMemberInfo[orgId]; if (orgMemberInfo === undefined) { return false; } if (orgMemberInfo.orgRoleStructure === OrgRoleStructure.MultiRole) { return orgMemberInfo.userAssignedRole === role || orgMemberInfo.userAssignedAdditionalRoles.includes(role); } else { return orgMemberInfo.userInheritedRolesPlusCurrentRole.includes(role); } } function hasPermission(orgId, permission) { const orgMemberInfo = orgIdToOrgMemberInfo[orgId]; if (orgMemberInfo === undefined) { return false; } return orgMemberInfo.userPermissions.includes(permission); } function hasAllPermissions(orgId, permissions) { const orgMemberInfo = orgIdToOrgMemberInfo[orgId]; if (orgMemberInfo === undefined) { return false; } return permissions.every(permission => orgMemberInfo.userPermissions.includes(permission)); } function getAccessHelperWithOrgId(orgId) { return { isRole(role) { return isRole(orgId, role); }, isAtLeastRole(role) { return isAtLeastRole(orgId, role); }, hasPermission(permission) { return hasPermission(orgId, permission); }, hasAllPermissions(permissions) { return hasAllPermissions(orgId, permissions); } }; } return { isRole, isAtLeastRole, hasPermission, hasAllPermissions, getAccessHelperWithOrgId }; } function getOrgHelper(orgIdToOrgMemberInfo) { return { getOrg(orgId) { if (orgIdToOrgMemberInfo.hasOwnProperty(orgId)) { return orgIdToOrgMemberInfo[orgId]; } else { return undefined; } }, getOrgIds() { return Object.keys(orgIdToOrgMemberInfo); }, getOrgs() { return Object.values(orgIdToOrgMemberInfo); }, getOrgByName(orgName) { for (const orgMemberInfo of Object.values(orgIdToOrgMemberInfo)) { if (orgMemberInfo.orgName === orgName || orgMemberInfo.urlSafeOrgName === orgName) { return orgMemberInfo; } } return undefined; } }; } class UserClass { // Metadata about the user // If you used our migration APIs to migrate this user from a different system, // this is their original ID from that system. constructor(userFields, orgIdToUserOrgInfo) { this.userId = userFields.userId; this.orgIdToUserOrgInfo = orgIdToUserOrgInfo; this.email = userFields.email; this.firstName = userFields.firstName; this.lastName = userFields.lastName; this.username = userFields.username; this.createdAt = userFields.createdAt; this.pictureUrl = userFields.pictureUrl; this.hasPassword = userFields.hasPassword; this.hasMfaEnabled = userFields.hasMfaEnabled; this.canCreateOrgs = userFields.canCreateOrgs; this.legacyUserId = userFields.legacyUserId; this.impersonatorUserId = userFields.impersonatorUserId; this.properties = userFields.properties; } getOrg(orgId) { if (!this.orgIdToUserOrgInfo) { return undefined; } return this.orgIdToUserOrgInfo[orgId]; } getOrgByName(orgName) { if (!this.orgIdToUserOrgInfo) { return undefined; } const urlSafeOrgName = orgName.toLowerCase().replace(/ /g, "-"); for (const orgId in this.orgIdToUserOrgInfo) { const orgMemberInfo = this.orgIdToUserOrgInfo[orgId]; if ((orgMemberInfo === null || orgMemberInfo === void 0 ? void 0 : orgMemberInfo.urlSafeOrgName) === urlSafeOrgName) { return orgMemberInfo; } } return undefined; } getUserProperty(key) { if (!this.properties) { return undefined; } return this.properties[key]; } getOrgs() { if (!this.orgIdToUserOrgInfo) { return []; } return Object.values(this.orgIdToUserOrgInfo); } isImpersonating() { return !!this.impersonatorUserId; } isRole(orgId, role) { const orgMemberInfo = this.getOrg(orgId); if (!orgMemberInfo) { return false; } return orgMemberInfo.isRole(role); } isAtLeastRole(orgId, role) { const orgMemberInfo = this.getOrg(orgId); if (!orgMemberInfo) { return false; } return orgMemberInfo.isAtLeastRole(role); } hasPermission(orgId, permission) { const orgMemberInfo = this.getOrg(orgId); if (!orgMemberInfo) { return false; } return orgMemberInfo.hasPermission(permission); } hasAllPermissions(orgId, permissions) { const orgMemberInfo = this.getOrg(orgId); if (!orgMemberInfo) { return false; } return orgMemberInfo.hasAllPermissions(permissions); } static fromJSON(json) { const obj = JSON.parse(json); const orgIdToUserOrgInfo = {}; for (const orgId in obj.orgIdToUserOrgInfo) { orgIdToUserOrgInfo[orgId] = OrgMemberInfoClass.fromJSON(JSON.stringify(obj.orgIdToUserOrgInfo[orgId])); } try { return new UserClass({ userId: obj.userId, email: obj.email, createdAt: obj.createdAt, firstName: obj.firstName, lastName: obj.lastName, username: obj.username, legacyUserId: obj.legacyUserId, impersonatorUserId: obj.impersonatorUserId, properties: obj.properties, pictureUrl: obj.pictureUrl, hasPassword: obj.hasPassword, hasMfaEnabled: obj.hasMfaEnabled, canCreateOrgs: obj.canCreateOrgs }, orgIdToUserOrgInfo); } catch (e) { console.error("Unable to parse User. Make sure the JSON string is a stringified `UserClass` type.", e); throw e; } } } class OrgMemberInfoClass { constructor(orgId, orgName, orgMetadata, urlSafeOrgName, userAssignedRole, userInheritedRolesPlusCurrentRole, userPermissions, orgRoleStructure, userAssignedAdditionalRoles, legacyOrgId) { this.orgId = orgId; this.orgName = orgName; this.legacyOrgId = legacyOrgId; this.orgMetadata = orgMetadata; this.urlSafeOrgName = urlSafeOrgName; this.orgRoleStructure = orgRoleStructure !== null && orgRoleStructure !== void 0 ? orgRoleStructure : OrgRoleStructure.SingleRole; this.userAssignedRole = userAssignedRole; this.userInheritedRolesPlusCurrentRole = userInheritedRolesPlusCurrentRole; this.userPermissions = userPermissions; this.userAssignedAdditionalRoles = userAssignedAdditionalRoles !== null && userAssignedAdditionalRoles !== void 0 ? userAssignedAdditionalRoles : []; } // validation methods isRole(role) { if (this.orgRoleStructure === OrgRoleStructure.MultiRole) { return this.userAssignedRole === role || this.userAssignedAdditionalRoles.includes(role); } else { return this.userAssignedRole === role; } } isAtLeastRole(role) { if (this.orgRoleStructure === OrgRoleStructure.MultiRole) { return this.userAssignedRole === role || this.userAssignedAdditionalRoles.includes(role); } else { return this.userInheritedRolesPlusCurrentRole.includes(role); } } hasPermission(permission) { return this.userPermissions.includes(permission); } hasAllPermissions(permissions) { return permissions.every(permission => this.hasPermission(permission)); } static fromJSON(json) { const obj = JSON.parse(json); try { return new OrgMemberInfoClass(obj.orgId, obj.orgName, obj.orgMetadata, obj.urlSafeOrgName, obj.userAssignedRole, obj.userInheritedRolesPlusCurrentRole, obj.userPermissions, obj.orgRoleStructure, obj.userAssignedAdditionalRoles, obj.legacyOrgId); } catch (e) { console.error("Unable to parse UserOrgInfo. Make sure the JSON string is a stringified `UserOrgInfo` type.", e); throw e; } } } function convertOrgIdToOrgMemberInfo(orgIdToOrgMemberInfo) { if (orgIdToOrgMemberInfo === undefined) { return undefined; } const orgIdToUserOrgInfo = {}; for (const orgMemberInfo of Object.values(orgIdToOrgMemberInfo)) { orgIdToUserOrgInfo[orgMemberInfo.orgId] = new OrgMemberInfoClass(orgMemberInfo.orgId, orgMemberInfo.orgName, orgMemberInfo.orgMetadata, orgMemberInfo.urlSafeOrgName, orgMemberInfo.userAssignedRole, orgMemberInfo.userInheritedRolesPlusCurrentRole, orgMemberInfo.userPermissions, orgMemberInfo.orgRoleStructure, orgMemberInfo.userAssignedAdditionalRoles, orgMemberInfo.legacyOrgId); } return orgIdToUserOrgInfo; } function fetchAuthenticationInfo(authUrl, activeOrgId) { const queryParams = new URLSearchParams(); if (activeOrgId) { queryParams.append("active_org_id", activeOrgId); } let path = `${authUrl}/api/v1/refresh_token`; if (queryParams.toString()) { path += `?${queryParams.toString()}`; } return fetch(path, { method: "GET", credentials: "include", headers: { "Content-Type": "application/json" } }).then(res => { if (res.status === 401) { return null; } else if (res.status === 0) { logCorsError(); return Promise.reject({ status: 503, message: "Unable to process authentication response" }); } else if (!res.ok) { return Promise.reject({ status: res.status, message: res.statusText }); } else { return parseResponse(res); } }); } function logout(authUrl) { return fetch(`${authUrl}/api/v1/logout`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" } }).then(res => { if (res.status === 0) { logCorsError(); return Promise.reject({ status: 503, message: "Unable to process authentication response" }); } else if (!res.ok) { console.error("Logout error", res.status, res.statusText); return Promise.reject({ status: res.status, message: res.statusText }); } else { return res.json(); } }); } function parseResponse(res) { return res.text().then(httpResponse => { try { const authInfoWithoutUserClass = parseJsonConvertingSnakeToCamel(httpResponse); return withExtraArgs(authInfoWithoutUserClass); } catch (e) { console.error("Unable to process authentication response", e); return Promise.reject({ status: 500, message: "Unable to process authentication response" }); } }, e => { console.error("Unable to process authentication response", e); return Promise.reject({ status: 500, message: "Unable to process authentication response" }); }); } // The API responds with snake_case, but TypeScript convention is camelCase. // When parsing JSON, we pass in reviver function to convert from snake_case to camelCase. function parseJsonConvertingSnakeToCamel(str) { return JSON.parse(str, function (key, value) { if (key === "org_id") { this.orgId = value; } else if (key === "org_name") { this.orgName = value; } else if (key === "org_metadata") { this.orgMetadata = value; } else if (key === "url_safe_org_name") { this.urlSafeOrgName = value; } else if (key === "user_role") { this.userAssignedRole = value; } else if (key === "inherited_user_roles_plus_current_role") { this.userInheritedRolesPlusCurrentRole = value; } else if (key === "user_permissions") { this.userPermissions = value; } else if (key === "access_token") { this.accessToken = value; } else if (key === "expires_at_seconds") { this.expiresAtSeconds = value; } else if (key === "org_id_to_org_member_info") { this.orgIdToOrgMemberInfo = value; } else if (key === "user_id") { this.userId = value; } else if (key === "email_confirmed") { this.emailConfirmed = value; } else if (key === "first_name") { this.firstName = value; } else if (key === "last_name") { this.lastName = value; } else if (key === "picture_url") { this.pictureUrl = value; } else if (key === "mfa_enabled") { this.mfaEnabled = value; } else if (key === "has_password") { this.hasPassword = value; } else if (key === "can_create_orgs") { this.canCreateOrgs = value; } else if (key === "created_at") { this.createdAt = value; } else if (key === "last_active_at") { this.lastActiveAt = value; } else if (key === "legacy_user_id") { this.legacyUserId = value; } else if (key === "legacy_org_id") { this.legacyOrgId = value; } else if (key === "impersonator_user") { this.impersonatorUserId = value; } else if (key === "org_role_structure") { this.orgRoleStructure = value; } else if (key === "additional_roles") { this.userAssignedAdditionalRoles = value; } else { return value; } }); } function withExtraArgs(authInfoWithoutExtraArgs) { if (authInfoWithoutExtraArgs.orgIdToOrgMemberInfo) { authInfoWithoutExtraArgs.orgHelper = getOrgHelper(authInfoWithoutExtraArgs.orgIdToOrgMemberInfo); authInfoWithoutExtraArgs.accessHelper = getAccessHelper(authInfoWithoutExtraArgs.orgIdToOrgMemberInfo); } authInfoWithoutExtraArgs.userClass = new UserClass({ userId: authInfoWithoutExtraArgs.user.userId, email: authInfoWithoutExtraArgs.user.email, createdAt: authInfoWithoutExtraArgs.user.createdAt, firstName: authInfoWithoutExtraArgs.user.firstName, lastName: authInfoWithoutExtraArgs.user.lastName, username: authInfoWithoutExtraArgs.user.username, properties: authInfoWithoutExtraArgs.user.properties, pictureUrl: authInfoWithoutExtraArgs.user.pictureUrl, hasPassword: authInfoWithoutExtraArgs.user.hasPassword, hasMfaEnabled: authInfoWithoutExtraArgs.user.mfaEnabled, canCreateOrgs: authInfoWithoutExtraArgs.user.canCreateOrgs, legacyUserId: authInfoWithoutExtraArgs.user.legacyUserId, impersonatorUserId: authInfoWithoutExtraArgs.impersonatorUserId }, convertOrgIdToOrgMemberInfo(authInfoWithoutExtraArgs.orgIdToOrgMemberInfo)); return Promise.resolve(authInfoWithoutExtraArgs); } function logCorsError() { console.error("Request to PropelAuth failed due to a CORS error. There are a few likely causes: \n" + " 1. In the Frontend Integration section of your dashboard, make sure your requests are coming either the specified Application URL or localhost with a matching port.\n" + " 2. Make sure your server is hosted on HTTPS in production."); } const DEFAULT_RETRIES = 3; const runWithRetriesOnAnyError = async fn => { return runWithRetriesInner(fn, DEFAULT_RETRIES); }; const runWithRetriesInner = async (fn, numRetriesLeft) => { try { return await fn(); } catch (e) { if (numRetriesLeft <= 0) { throw e; } await delay(numRetriesLeftToDelay(numRetriesLeft)); return runWithRetriesInner(fn, numRetriesLeft - 1); } }; const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); const numRetriesLeftToDelay = numRetriesLeft => { // We could be fancy, but we only retry 3 times so... if (numRetriesLeft >= 3) { return 100; } else if (numRetriesLeft === 2) { return 200; } else { return 300; } }; function currentTimeSeconds() { return Date.now() / 1000; } function hasLocalStorage() { return typeof localStorage !== "undefined"; } function hasWindow() { return typeof window !== "undefined"; } function getLocalStorageNumber(key) { if (!hasLocalStorage()) { return null; } const value = localStorage.getItem(key); if (!value) { return null; } const num = parseInt(value, 10); if (Number.isNaN(num)) { return null; } return num; } const LOGGED_IN_AT_KEY = "__PROPEL_AUTH_LOGGED_IN_AT"; const LOGGED_OUT_AT_KEY = "__PROPEL_AUTH_LOGGED_OUT_AT"; const AUTH_TOKEN_REFRESH_BEFORE_EXPIRATION_SECONDS = 10 * 60; const DEFAULT_MIN_SECONDS_BEFORE_REFRESH = 60 * 2; const ACTIVE_ORG_ACCESS_TOKEN_REFRESH_EXPIRATION_SECONDS = 60 * 5; const encodeBase64 = str => { const encode = window ? window.btoa : btoa; return encode(str); }; function validateAndCleanupOptions(authOptions) { try { // This helps make sure we have a consistent URL ignoring things like trailing slashes const authUrl = new URL(authOptions.authUrl); authOptions.authUrl = authUrl.origin; } catch (e) { console.error("Invalid authUrl", e); throw new Error("Unable to initialize auth client"); } if (authOptions.enableBackgroundTokenRefresh === undefined) { authOptions.enableBackgroundTokenRefresh = true; } } function createClient(authOptions) { const { minSecondsBeforeRefresh = DEFAULT_MIN_SECONDS_BEFORE_REFRESH } = authOptions; validateAndCleanupOptions(authOptions); // Internal state const clientState = { initialLoadFinished: false, authenticationInfo: null, observers: [], accessTokenObservers: [], lastLoggedInAtMessage: getLocalStorageNumber(LOGGED_IN_AT_KEY), lastLoggedOutAtMessage: getLocalStorageNumber(LOGGED_OUT_AT_KEY), authUrl: authOptions.authUrl, refreshInterval: null, lastRefresh: null, accessTokenActiveOrgMap: {} }; // Helper functions function notifyObservers(isLoggedIn) { for (let i = 0; i < clientState.observers.length; i++) { const observer = clientState.observers[i]; if (observer) { observer(isLoggedIn); } } } function notifyObserversOfAccessTokenChange(accessToken) { for (let i = 0; i < clientState.accessTokenObservers.length; i++) { const observer = clientState.accessTokenObservers[i]; if (observer) { observer(accessToken); } } } function userJustLoggedOut(accessToken, previousAccessToken) { // Edge case: the first time we go to the page, if we can't load the // auth token we should treat it as a logout event return !accessToken && (previousAccessToken || !clientState.initialLoadFinished); } function userJustLoggedIn(accessToken, previousAccessToken) { return !previousAccessToken && accessToken; } function updateLastLoggedOutAt() { const loggedOutAt = currentTimeSeconds(); clientState.lastLoggedOutAtMessage = loggedOutAt; if (hasLocalStorage()) { localStorage.setItem(LOGGED_OUT_AT_KEY, String(loggedOutAt)); } } function updateLastLoggedInAt() { const loggedInAt = currentTimeSeconds(); clientState.lastLoggedInAtMessage = loggedInAt; if (hasLocalStorage()) { localStorage.setItem(LOGGED_IN_AT_KEY, String(loggedInAt)); } } /** * Invalidates all org's access tokens. */ function resetAccessTokenActiveOrgMap() { clientState.accessTokenActiveOrgMap = {}; } function setAuthenticationInfoAndUpdateDownstream(authenticationInfo) { var _clientState$authenti; const previousAccessToken = (_clientState$authenti = clientState.authenticationInfo) === null || _clientState$authenti === void 0 ? void 0 : _clientState$authenti.accessToken; clientState.authenticationInfo = authenticationInfo; const accessToken = authenticationInfo === null || authenticationInfo === void 0 ? void 0 : authenticationInfo.accessToken; if (userJustLoggedOut(accessToken, previousAccessToken)) { notifyObservers(false); updateLastLoggedOutAt(); } else if (userJustLoggedIn(accessToken, previousAccessToken)) { notifyObservers(true); updateLastLoggedInAt(); } if (previousAccessToken !== accessToken) { notifyObserversOfAccessTokenChange(accessToken); } resetAccessTokenActiveOrgMap(); clientState.lastRefresh = currentTimeSeconds(); clientState.initialLoadFinished = true; } async function forceRefreshToken(returnCached) { try { // Happy case, we fetch auth info and save it const authenticationInfo = await runWithRetriesOnAnyError(() => fetchAuthenticationInfo(clientState.authUrl)); setAuthenticationInfoAndUpdateDownstream(authenticationInfo); return authenticationInfo; } catch (e) { // If there was an error, we sometimes still want to return the value we have cached // (e.g. if we were prefetching), so in those cases we swallow the exception if (returnCached) { return clientState.authenticationInfo; } else { setAuthenticationInfoAndUpdateDownstream(null); throw e; } } } const getSignupPageUrl = options => { let qs = new URLSearchParams(); let url = `${clientState.authUrl}/signup`; if (options) { const { postSignupRedirectUrl, userSignupQueryParameters } = options; if (postSignupRedirectUrl) { qs.set("rt", encodeBase64(postSignupRedirectUrl)); } if (userSignupQueryParameters) { Object.entries(userSignupQueryParameters).forEach(([key, value]) => { qs.set(key, value); }); } } if (qs.toString()) { url += `?${qs.toString()}`; } return url; }; const getLoginPageUrl = options => { let qs = new URLSearchParams(); let url = `${clientState.authUrl}/login`; if (options) { const { postLoginRedirectUrl, userSignupQueryParameters } = options; if (postLoginRedirectUrl) { qs.set("rt", encodeBase64(postLoginRedirectUrl)); } if (userSignupQueryParameters) { Object.entries(userSignupQueryParameters).forEach(([key, value]) => { qs.set(key, value); }); } } if (qs.toString()) { url += `?${qs.toString()}`; } return url; }; const getAccountPageUrl = options => { let qs = new URLSearchParams(); let url = `${clientState.authUrl}/account`; if (options) { const { redirectBackToUrl } = options; if (redirectBackToUrl) { qs.set("rt", encodeBase64(redirectBackToUrl)); } } if (qs.toString()) { url += `?${qs.toString()}`; } return url; }; const getOrgPageUrl = (orgId, options) => { let qs = new URLSearchParams(); let url = `${clientState.authUrl}/org`; if (orgId) { qs.set("id", orgId); } if (options) { if (options.redirectBackToUrl) { qs.set("rt", encodeBase64(options.redirectBackToUrl)); } } if (qs.toString()) { url += `?${qs.toString()}`; } return url; }; const getCreateOrgPageUrl = options => { let qs = new URLSearchParams(); let url = `${clientState.authUrl}/create_org`; if (options) { const { redirectBackToUrl } = options; if (redirectBackToUrl) { qs.set("rt", encodeBase64(redirectBackToUrl)); } } if (qs.toString()) { url += `?${qs.toString()}`; } return url; }; const getSetupSAMLPageUrl = (orgId, options) => { let qs = new URLSearchParams(); if (options) { if (options.redirectBackToUrl) { qs.set("rt", encodeBase64(options.redirectBackToUrl)); } } qs.set("id", orgId); return `${clientState.authUrl}/saml?${qs.toString()}`; }; const client = { addLoggedInChangeObserver(loggedInChangeObserver) { const hasObserver = clientState.observers.includes(loggedInChangeObserver); if (hasObserver) { console.error("Observer has been attached already."); } else if (!loggedInChangeObserver) { console.error("Cannot add a null observer"); } else { clientState.observers.push(loggedInChangeObserver); } }, removeLoggedInChangeObserver(loggedInChangeObserver) { const observerIndex = clientState.observers.indexOf(loggedInChangeObserver); if (observerIndex === -1) { console.error("Cannot find observer to remove"); } else { clientState.observers.splice(observerIndex, 1); } }, addAccessTokenChangeObserver(observer) { const hasObserver = clientState.accessTokenObservers.includes(observer); if (hasObserver) { console.error("Observer has been attached already."); } else if (!observer) { console.error("Cannot add a null observer"); } else { clientState.accessTokenObservers.push(observer); } }, removeAccessTokenChangeObserver(observer) { const observerIndex = clientState.accessTokenObservers.indexOf(observer); if (observerIndex === -1) { console.error("Cannot find observer to remove"); } else { clientState.accessTokenObservers.splice(observerIndex, 1); } }, async getAuthenticationInfoOrNull(forceRefresh) { const currentTimeSecs = currentTimeSeconds(); if (forceRefresh) { return await forceRefreshToken(false); } else if (!clientState.authenticationInfo) { return await forceRefreshToken(false); } else if (currentTimeSecs + AUTH_TOKEN_REFRESH_BEFORE_EXPIRATION_SECONDS > clientState.authenticationInfo.expiresAtSeconds) { // Small edge case: If we were being proactive // and the auth information hasn't expired yet, swallow any exceptions const returnCached = currentTimeSecs < clientState.authenticationInfo.expiresAtSeconds; return await forceRefreshToken(returnCached); } else { return clientState.authenticationInfo; } }, async getAccessTokenForOrg(orgId) { // First, check if there is a valid access token for the org ID in the // valid time frame. const currentTimeSecs = currentTimeSeconds(); const activeOrgAccessToken = clientState.accessTokenActiveOrgMap[orgId]; if (!!activeOrgAccessToken) { if (currentTimeSecs < activeOrgAccessToken.fetchedAt + ACTIVE_ORG_ACCESS_TOKEN_REFRESH_EXPIRATION_SECONDS) { return { accessToken: activeOrgAccessToken.accessToken, error: undefined }; } } // Fetch the access token for the org ID and update. try { const authenticationInfo = await runWithRetriesOnAnyError(() => fetchAuthenticationInfo(clientState.authUrl, orgId)); if (!authenticationInfo) { // Only null if 401 unauthorized. return { error: "user_not_in_org", accessToken: null }; } const { accessToken } = authenticationInfo; clientState.accessTokenActiveOrgMap[orgId] = { accessToken, fetchedAt: currentTimeSecs }; return { accessToken, error: undefined }; } catch (e) { return { error: "unexpected_error", accessToken: null }; } }, getSignupPageUrl(options) { return getSignupPageUrl(options); }, getLoginPageUrl(options) { return getLoginPageUrl(options); }, getAccountPageUrl(options) { return getAccountPageUrl(options); }, getOrgPageUrl(orgId, options) { return getOrgPageUrl(orgId, options); }, getCreateOrgPageUrl(options) { return getCreateOrgPageUrl(options); }, getSetupSAMLPageUrl(orgId, options) { return getSetupSAMLPageUrl(orgId, options); }, redirectToSignupPage(options) { window.location.href = getSignupPageUrl(options); }, redirectToLoginPage(options) { window.location.href = getLoginPageUrl(options); }, redirectToAccountPage(options) { window.location.href = getAccountPageUrl(options); }, redirectToOrgPage(orgId, options) { window.location.href = getOrgPageUrl(orgId, options); }, redirectToCreateOrgPage(options) { window.location.href = getCreateOrgPageUrl(options); }, redirectToSetupSAMLPage(orgId, options) { window.location.href = getSetupSAMLPageUrl(orgId, options); }, async logout(redirectAfterLogout) { const logoutResponse = await logout(clientState.authUrl); setAuthenticationInfoAndUpdateDownstream(null); if (redirectAfterLogout) { window.location.href = logoutResponse.redirect_to; } }, destroy() { clientState.observers = []; clientState.accessTokenObservers = []; window.removeEventListener("storage", onStorageChange); window.removeEventListener("online", onOnlineOrFocus); if (!authOptions.disableRefreshOnFocus) { window.removeEventListener("focus", onOnlineOrFocus); } if (clientState.refreshInterval) { clearInterval(clientState.refreshInterval); } } }; const onStorageChange = async function () { // If localStorage isn't available, nothing to do here. // This usually happens in frameworks that have some SSR components if (!hasLocalStorage()) { return; } const loggedOutAt = getLocalStorageNumber(LOGGED_OUT_AT_KEY); const loggedInAt = getLocalStorageNumber(LOGGED_IN_AT_KEY); // If we've detected a logout event after the last one our client is aware of, trigger a refresh if (loggedOutAt && (!clientState.lastLoggedOutAtMessage || loggedOutAt > clientState.lastLoggedOutAtMessage)) { clientState.lastLoggedOutAtMessage = loggedOutAt; if (clientState.authenticationInfo) { await forceRefreshToken(true); } } // If we've detected a login event after the last one our client is aware of, trigger a refresh if (loggedInAt && (!clientState.lastLoggedInAtMessage || loggedInAt > clientState.lastLoggedInAtMessage)) { clientState.lastLoggedInAtMessage = loggedInAt; if (!clientState.authenticationInfo) { await forceRefreshToken(true); } } }; // If we were offline or on a different tab, when we return, refetch auth info // Some browsers trigger focus more often than we'd like, so we'll debounce a little here as well const onOnlineOrFocus = async function () { if (clientState.lastRefresh && currentTimeSeconds() > clientState.lastRefresh + minSecondsBeforeRefresh) { await forceRefreshToken(true); } else { await client.getAuthenticationInfoOrNull(); } }; if (hasWindow()) { window.addEventListener("storage", onStorageChange); window.addEventListener("online", onOnlineOrFocus); if (!authOptions.disableRefreshOnFocus) { window.addEventListener("focus", onOnlineOrFocus); } if (authOptions.enableBackgroundTokenRefresh) { client.getAuthenticationInfoOrNull(); clientState.refreshInterval = window.setInterval(client.getAuthenticationInfoOrNull, 60000); } } return client; } export { ACTIVE_ORG_ID_COOKIE_NAME, OrgMemberInfoClass, UserClass, createClient, getActiveOrgId, setActiveOrgId }; //# sourceMappingURL=index.js.map