UNPKG

@nebula.js/stardust

Version:

Product and framework agnostic integration API for Qlik's Associative Engine

1,286 lines (1,270 loc) 107 kB
/* * @nebula.js/stardust v7.0.0 * Copyright (c) 2026 QlikTech International AB * Released under the MIT license. */ System.register(['./index-CyZHHxmY.js'], (function (exports) { 'use strict'; var sortKeys, isBrowser, cleanFalsyValues, isNode; return { setters: [function (module) { sortKeys = module.s; isBrowser = module.i; cleanFalsyValues = module.c; isNode = module.a; }], execute: (function () { exports({ determineAuthType: determineAuthType, getAccessToken: getAccessToken, getDefaultHostConfig: getDefaultHostConfig, getRestCallAuthParams: getRestCallAuthParams, getWebResourceAuthParams: getWebResourceAuthParams, getWebSocketAuthParams: getWebSocketAuthParams, handleAuthenticationError: handleAuthenticationError, isHostCrossOrigin: isHostCrossOrigin, isWindows: isWindows, normalizeHostConfig: normalizeHostConfig, onFatalAuthError: onFatalAuthError, onPageRedirectRequested: onPageRedirectRequested, onPageRedirectStarted: onPageRedirectStarted, registerAuthModule: registerAuthModule, registerHostConfig: registerHostConfig, serializeHostConfig: serializeHostConfig, setDefaultHostConfig: setDefaultHostConfig, toValidLocationUrl: toValidLocationUrl, toValidWebsocketLocationUrl: toValidWebsocketLocationUrl, unregisterHostConfig: unregisterHostConfig }); //#region src/auth/auth-types.ts /** * These properties are always allowed in the host config, even if they are not defined in the HostConfig interface * for the specific auth module. */ const hostConfigCommonProperties = [ "authType", "autoRedirect", "authRedirectUserConfirmation", "embedRuntimeUrl", "host", "onAuthFailed" ]; const authTypesThatCanBeOmitted = [ "apikey", "oauth2", "cookie", "windowscookie", "reference", "anonymous", "pfx" ]; //#region src/platform/platform-functions.ts const getPlatform = async (options = {}) => { const hc = resolveHostConfig(options.hostConfig); if (hc.authType === "mock-backend-rest-recorder") return hc.recordGetPlatform(); if (hc?.authType === "mock-backend") return hc.mockGetPlatform(); if (hc?.authType === "noauth") return result({ isUnknown: true }); let productInfo = isBrowser() ? window.QlikMain?.PRODUCT_INFO : void 0; if (!productInfo) { const { data, status } = await getProductInfo(options); productInfo = data; if (status === 404) return result({ isUnknown: true }); if (!productInfo || status <= 399 && status >= 300) return result({ isQSE: true, isWindows: true }); } const deploymentType = (productInfo.composition?.deploymentType || "").toLowerCase(); if (deploymentType === "qliksenseserver") return result({ isQSE: true, isWindows: true, meta: extractMeta(productInfo) }); if (deploymentType === "qliksensedesktop") return result({ isQSD: true, isWindows: true, meta: extractMeta(productInfo) }); if (deploymentType === "qliksensemobile") return result({ isQSE: true, isWindows: true, meta: extractMeta(productInfo) }); if (deploymentType === "cloud-console") return result({ isCloud: true, isCloudConsole: true, meta: extractMeta(productInfo) }); if (productInfo.composition?.provider === "fedramp") return result({ isCloud: true, isQCG: true, meta: extractMeta(productInfo) }); return result({ isCloud: true, isQCS: true, meta: extractMeta(productInfo) }); }; const productInfoPromises = {}; /** * Retrieves the full complete url to the product-info file. The full URL * is the cache key. Without templating the URL we'd cache `""` as an entry. * @private */ function templateUrl(baseUrl) { return `${baseUrl}/resources/autogenerated/product-info.json`; } /** * Retrieves the product information as a JSON object. * It makes an HTTP GET request to fetch the autogenerated product-info.json file. * * `data` is `undefined` if the file can not be retrieved. */ const getProductInfo = async ({ hostConfig, noCache } = {}) => { const completeUrl = templateUrl(toValidLocationUrl(hostConfig)); try { if (!(completeUrl in productInfoPromises)) { const fetchOptions = {}; if (globalThis.QlikMain?.resourceNeedsCredentials(completeUrl)) fetchOptions.credentials = "include"; productInfoPromises[completeUrl] = fetch(completeUrl, fetchOptions).then(async (res) => { if (res.ok) return { data: await res.json(), status: res.status }; return { data: void 0, status: res.status }; }); } const response = await productInfoPromises[completeUrl]; if (response.status >= 400 || !response.data) delete productInfoPromises[completeUrl]; return response; } catch { delete productInfoPromises[completeUrl]; return { data: void 0, status: 500 }; } finally { if (noCache) delete productInfoPromises[completeUrl]; } }; /** @internal */ const extractMeta = (data) => { const urls = data.externalUrls; if (!urls) return; const productName = data.composition?.productName ?? "Qlik"; const releaseLabel = data.composition?.releaseLabel || "-"; const productLabel = releaseLabel === "-" ? productName : `${productName} (${releaseLabel})`; return { productId: data.composition?.senseId ?? "qlik", productLabel, version: data.composition?.version, urls: { personalHelpBaseUrl: urls.personalHelpBaseUrl, personalUpgradeBase: urls.personalUpgradeBase, personalUpgradeUrl: urls.personalUpgradeUrl, serverHelpBaseUrl: urls.serverHelpBaseUrl, qlikWebPageUrl: urls.qlikWebPageUrl } }; }; const result = (data) => ({ isCloud: false, isQCS: false, isQCG: false, isCloudConsole: false, isWindows: false, isQSE: false, isQSD: false, isUnknown: false, ...data }); //#endregion //#region src/utils/random.ts const NANOID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"; const HEX_ALPHABET = "0123456789abcdef"; /** * Generates a random string from the given alphabet using crypto.getRandomValues * Works in both browser and Node.js environments */ function generateRandomFromAlphabet(alphabet, length) { const bytes = new Uint8Array(length); globalThis.crypto.getRandomValues(bytes); let result = ""; for (let i = 0; i < length; i++) result += alphabet[bytes[i] % alphabet.length]; return result; } /** * Method helper for generating a random string [a-zA-Z0-9\-_]{length} * @param length - the length of the string */ function generateRandomString(targetLength) { return generateRandomFromAlphabet(NANOID_ALPHABET, targetLength); } /** * Method helper for generating a random hexadecimal-string [0-9a-f]{length} * @param length - the length of the string */ function generateRandomHexString(targetLength) { return generateRandomFromAlphabet(HEX_ALPHABET, targetLength); } //#endregion //#region src/auth/internal/host-config-functions.ts const emptyHostConfig = {}; const normalizedHostConfigs = /* @__PURE__ */ new Map(); /** * Returns a new host config with all default and falsy values removed. * @param hostConfig - The host config to fill with defaults * @returns */ function removeDefaults(hostConfig) { const cleanedHostConfig = cleanFalsyValues(hostConfig) || {}; if (cleanedHostConfig.host) cleanedHostConfig.host = toValidLocationUrl(cleanedHostConfig); if (isBrowser()) { if (toValidLocationUrl(cleanedHostConfig) === window.location.origin) delete cleanedHostConfig.host; } if (cleanedHostConfig.authType && authTypesThatCanBeOmitted.includes(cleanedHostConfig.authType)) delete cleanedHostConfig.authType; return cleanedHostConfig; } function globalReplacer(key, value) { if (typeof value === "function") return; return value; } /** * Serializes the provided hostConfig, if present, otherwise the default one. */ function serializeHostConfig$1(hostConfig) { const sorted = sortKeys(removeDefaults(resolveHostConfig(hostConfig))); return JSON.stringify(sorted, globalReplacer); } const registeredHostConfigs = /* @__PURE__ */ new Map(); /** * Registers a host config with the given name. * @param name The name of the host config to be used to reference the host config later. * @param hostConfig The host config to register. */ function registerHostConfig$1(name, hostConfig) { const reference = hostConfig?.reference || null; if (reference && !registeredHostConfigs.has(reference)) throw new InvalidHostConfigError(`Host config with reference "${reference}" is not registered. Please register it before using it.`); if (registeredHostConfigs.has(name)) console.warn(`registerHostConfig: Host config with name "${name}" is already registered. Overwriting.`); registeredHostConfigs.set(name, hostConfig); } /** * Unregisters a host config with the given name. * @param name The name of the host config to unregister. */ function unregisterHostConfig$1(name) { if (registeredHostConfigs.has(name)) registeredHostConfigs.delete(name); else console.warn(`unregisterHostConfig: Host config with name "${name}" not found.`); } /** * Gets the host config with the given name. * @private * @param name The name of the host config to get. * @returns The host config, or undefined if not found. */ function getRegisteredHostConfig(name) { return registeredHostConfigs.get(name); } /** * Sets the default host config that will be used for all api calls that do not include a HostConfig * @private * @param hostConfig the default HostConfig to use */ function setDefaultHostConfig$1(hostConfig) { registerHostConfig$1("default", hostConfig || {}); } /** * Gets the default host config that will be used for all qmfe api calls that do not include a HostConfig. * @private * @returns The default host config that will be used for all qmfe api calls that do not include a HostConfig */ function getDefaultHostConfig$1() { return getRegisteredHostConfig("default") || {}; } function normalizeHostConfig$1(hostConfig) { const suppliedHostConfigOrEmpty = hostConfig || emptyHostConfig; const serializedHostConfigKey = serializeHostConfig$1(suppliedHostConfigOrEmpty); let normalizedHostConfig = normalizedHostConfigs.get(serializedHostConfigKey); if (!normalizedHostConfig) { normalizedHostConfig = removeDefaults(resolveHostConfig(suppliedHostConfigOrEmpty)); normalizedHostConfigs.set(serializedHostConfigKey, normalizedHostConfig); } return normalizedHostConfig; } //#endregion //#region src/auth/internal/page-redirect-request-listeners.ts /** * A store containing all listener registries for different host configurations. */ const listenerRegistries$1 = /* @__PURE__ */ new Map(); /** * Retrieves the listener registry for a given host configuration, creating one if it doesn't exist. */ function getRegistryForHostConfig(hostConfig) { const key = serializeHostConfig$1(hostConfig); let registry = listenerRegistries$1.get(key); if (!registry) { registry = { redirectRequestedListeners: /* @__PURE__ */ new Set(), redirectStartedListeners: /* @__PURE__ */ new Set() }; listenerRegistries$1.set(key, registry); } return registry; } /** * Registers a listener for page redirects requested by auth modules. * @param hostConfig * @param listener * @returns */ function onPageRedirectRequested$1(hostConfig, listener) { const registry = getRegistryForHostConfig(hostConfig); registry.redirectRequestedListeners.add(listener); return () => { registry.redirectRequestedListeners.delete(listener); }; } function onPageRedirectStarted$1(hostConfig, listener) { const registry = getRegistryForHostConfig(hostConfig); registry.redirectStartedListeners.add(listener); return () => { registry.redirectStartedListeners.delete(listener); }; } function requestRedirect(hostConfig) { const registry = getRegistryForHostConfig(hostConfig); const hasRedirectListener = registry.redirectRequestedListeners.size > 0 || hostConfig.authRedirectUserConfirmation; if (hostConfig.autoRedirect || !hasRedirectListener) return Promise.resolve(); if (typeof hostConfig.authRedirectUserConfirmation === "function") return hostConfig.authRedirectUserConfirmation().catch(() => {}); return new Promise((resolve) => { const proceed = () => { for (const startedListener of registry.redirectStartedListeners) try { startedListener(); } catch (error) { console.warn(error); } resolve(); }; for (const requestListener of registry.redirectRequestedListeners) try { requestListener({ proceed }); } catch (error) { console.warn(error); } }); } //#endregion //#region src/utils/expose-internal-test-apis.ts function exposeInternalApiOnWindow(name, fn) { if (globalThis.location?.origin.startsWith("https://localhost:") || globalThis.location?.origin?.endsWith("qlik-stage.com")) { if (globalThis.QlikMain) { if (globalThis.QlikMain.INTERNAL__DO_NOT_USE.qix === void 0) globalThis.QlikMain.INTERNAL__DO_NOT_USE.qix = {}; globalThis.QlikMain.INTERNAL__DO_NOT_USE.qix[name] = fn; } } } //#endregion //#region src/auth/internal/default-auth-modules/oauth/storage-helpers.ts const storagePrefix = "qlik-qmfe-api"; function getTopicFromOauthHostConfig(hostConfig) { let topic = hostConfig.clientId + (hostConfig.scope ? `_${hostConfig.scope}` : "_user_default"); if (hostConfig.subject) topic += `_${hostConfig.subject}`; if (hostConfig.userId) topic += `_${hostConfig.userId}`; return topic; } function getTopicFromAnonHostConfig(hostConfig) { return `${hostConfig.accessCode}_${hostConfig.clientId}`; } /** * A map from oauth client id to promise of an access token */ const cachedTokens = {}; /** * Used for testing. */ function clearAllCachedTokens() { for (const key in cachedTokens) delete cachedTokens[key]; } exposeInternalApiOnWindow("clearAllAccessTokens", () => { console.log("Clearing tokens", cachedTokens); Object.keys(cachedTokens).forEach((key) => { console.log("Clearing access tokens for", key); deleteFromLocalStorage(key, ["access-token", "refresh-token"]); deleteFromSessionStorage(key, ["access-token", "refresh-token"]); }); clearAllCachedTokens(); }); function saveInLocalStorage(topic, name, value) { localStorage.setItem(`${storagePrefix}-${topic}-${name}`, value); } function saveInSessionStorage(topic, name, value) { sessionStorage.setItem(`${storagePrefix}-${topic}-${name}`, value); } function saveInCustomSecretStorage(storage, topic, name, value) { return storage.store(`${storagePrefix}-${topic}-${name}`, value); } function loadFromLocalStorage(topic, name) { return localStorage.getItem(`${"qlik-qmfe-api"}-${topic}-${name}`) || void 0; } function loadFromSessionStorage(topic, name) { return sessionStorage.getItem(`${"qlik-qmfe-api"}-${topic}-${name}`) || void 0; } function loadFromCustomSecretStorage(storage, topic, name) { return storage.get(`${storagePrefix}-${topic}-${name}`); } function deleteFromLocalStorage(topic, names) { names.forEach((name) => localStorage.removeItem(`${storagePrefix}-${topic}-${name}`)); } function deleteFromSessionStorage(topic, names) { names.forEach((name) => sessionStorage.removeItem(`${storagePrefix}-${topic}-${name}`)); } function deleteFromCustomSecretStorage(storage, topic, names) { return Promise.all(names.map((name) => storage.delete(`${storagePrefix}-${topic}-${name}`))); } function loadAndDeleteFromSessionStorage(topic, name) { const id = `${storagePrefix}-${topic}-${name}`; const result = sessionStorage.getItem(id) || void 0; sessionStorage.removeItem(id); return result; } /** * Returns true if the provided storage object implements the SecretStorage interface, false otherwise. */ function isCustomSecretStorage(storage) { return typeof storage === "object" && storage !== null && typeof storage.store === "function" && typeof storage.get === "function" && typeof storage.delete === "function"; } async function loadOauthTokensFromStorage(topic, accessTokenStorage) { let accessToken; let refreshToken; if (accessTokenStorage === "local") { accessToken = loadFromLocalStorage(topic, "access-token"); refreshToken = loadFromLocalStorage(topic, "refresh-token"); } else if (accessTokenStorage === "session") { accessToken = loadFromSessionStorage(topic, "access-token"); refreshToken = loadFromSessionStorage(topic, "refresh-token"); } else if (isCustomSecretStorage(accessTokenStorage)) { accessToken = await loadFromCustomSecretStorage(accessTokenStorage, topic, "access-token"); refreshToken = await loadFromCustomSecretStorage(accessTokenStorage, topic, "refresh-token"); } if (accessToken) return { accessToken, refreshToken }; } async function loadCachedOauthTokens(hostConfig) { return cachedTokens[getTopicFromOauthHostConfig(hostConfig)]; } async function loadOrAcquireAccessTokenOauth(hostConfig, acquireTokens) { if (!hostConfig.clientId) throw new InvalidHostConfigError("A host config with authType set to \"oauth2\" has to also provide a clientId"); return loadOrAcquireAccessToken(getTopicFromOauthHostConfig(hostConfig), acquireTokens, hostConfig.noCache, hostConfig.accessTokenStorage); } async function loadOrAcquireAccessTokenAnon(hostConfig, acquireTokens) { if (!hostConfig.accessCode) throw new InvalidHostConfigError("A host config with authType set to \"anonymous\" has to also provide an accessCode"); return loadOrAcquireAccessToken(getTopicFromAnonHostConfig(hostConfig), acquireTokens, false, void 0); } async function loadOrAcquireAccessToken(topic, acquireTokens, noCache, accessTokenStorage) { if (noCache) return acquireTokens(); cachedTokens[topic] = cachedTokens[topic] || (async () => { let tokens = await loadOauthTokensFromStorage(topic, accessTokenStorage); if (tokens) return tokens; tokens = await acquireTokens(); if (accessTokenStorage === "local" && tokens) { if (tokens.accessToken) saveInLocalStorage(topic, "access-token", tokens.accessToken); if (tokens.refreshToken) saveInLocalStorage(topic, "refresh-token", tokens.refreshToken); } else if (accessTokenStorage === "session" && tokens) { if (tokens.accessToken) saveInSessionStorage(topic, "access-token", tokens.accessToken); if (tokens.refreshToken) saveInSessionStorage(topic, "refresh-token", tokens.refreshToken); } else if (isCustomSecretStorage(accessTokenStorage) && tokens) { if (tokens.accessToken) await saveInCustomSecretStorage(accessTokenStorage, topic, "access-token", tokens.accessToken); if (tokens.refreshToken) await saveInCustomSecretStorage(accessTokenStorage, topic, "refresh-token", tokens.refreshToken); } return tokens; })(); return cachedTokens[topic]; } function clearStoredOauthTokens(hostConfig) { const topic = getTopicFromOauthHostConfig(hostConfig); delete cachedTokens[topic]; if (isBrowser()) { deleteFromLocalStorage(topic, ["access-token", "refresh-token"]); deleteFromSessionStorage(topic, ["access-token", "refresh-token"]); } if (isCustomSecretStorage(hostConfig.accessTokenStorage)) deleteFromCustomSecretStorage(hostConfig.accessTokenStorage, topic, ["access-token", "refresh-token"]); } function clearStoredAnonymousTokens(hostConfig) { const topic = getTopicFromAnonHostConfig(hostConfig); delete cachedTokens[topic]; if (isBrowser()) { deleteFromLocalStorage(topic, ["access-token", "refresh-token"]); deleteFromSessionStorage(topic, ["access-token", "refresh-token"]); } } //#endregion //#region src/auth/internal/default-auth-modules/oauth/oauth-utils.ts function toPerformInteractiveLoginFunction(performInteractiveLogin) { if (typeof performInteractiveLogin === "string") { const fn = lookupInteractiveLoginFn(performInteractiveLogin); if (!fn) throw new Error(`No such function: ${performInteractiveLogin}`); return fn; } return performInteractiveLogin; } function lookupGetAccessFn(getAccessToken) { return globalThis[getAccessToken]; } function lookupInteractiveLoginFn(name) { return globalThis[name]; } function handlePossibleErrors(data) { if (data.errors) throw new AuthorizationError(data.errors); } function toQueryString(queryParams) { const queryParamsKeys = Object.keys(queryParams); queryParamsKeys.sort(); return queryParamsKeys.map((k) => `${k}=${queryParams[k]}`).join("&"); } function byteArrayToBase64(hashArray) { let result; if (isBrowser()) { const byteArrayToString = String.fromCharCode.apply(null, hashArray); result = btoa(byteArrayToString); } else if (isNode()) result = Buffer.from(hashArray).toString("base64"); else throw new Error("Environment not supported for oauth2 authentication"); return result; } /** * @param message string to hash * @returns sha-256 hashed string */ async function sha256(message) { const msgBuffer = new TextEncoder().encode(message); const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", msgBuffer); return byteArrayToBase64(Array.from(new Uint8Array(hashBuffer))).replaceAll(/\+/g, "-").replaceAll(/\//g, "_").replace(/=+$/, ""); } async function createInteractiveLoginUrl(hostConfig, redirectUri, state, verifier) { const clientId = hostConfig.clientId || ""; const locationUrl = toValidLocationUrl(hostConfig); const codeChallenge = await sha256(verifier); return `${locationUrl}/oauth/authorize?${toQueryString({ response_type: "code", client_id: clientId, redirect_uri: redirectUri, scope: hostConfig.scope || "user_default", state, code_challenge: codeChallenge, code_challenge_method: "S256" })}`; } async function startFullPageLoginFlow(hostConfig) { const clientId = hostConfig.clientId || ""; const locationUrl = toValidLocationUrl(hostConfig); const verifier = generateRandomString(128); const state = generateRandomString(43); const codeChallenge = await sha256(verifier); const redirectUri = hostConfig.redirectUri || globalThis.location.href; const topic = getTopicFromOauthHostConfig(hostConfig); clearStoredOauthTokens(hostConfig); saveInSessionStorage(topic, "state", state); saveInSessionStorage(topic, "verifier", verifier); saveInSessionStorage(topic, "href", globalThis.location.href); saveInSessionStorage("", "client-in-progress", topic); const url = `${locationUrl}/oauth/authorize?${toQueryString({ response_type: "code", client_id: clientId, redirect_uri: redirectUri, scope: hostConfig.scope || "user_default", state, code_challenge: codeChallenge, code_challenge_method: "S256" })}`; globalThis.location.replace(url); } async function exchangeCodeAndVerifierForAccessTokenData(hostConfig, code, verifier, redirectUri) { try { const data = await (await fetch(`${toValidLocationUrl(hostConfig)}/oauth/token`, { method: "POST", credentials: "include", mode: "cors", headers: { "content-type": "application/json" }, redirect: "follow", body: JSON.stringify({ grant_type: "authorization_code", scope: hostConfig.scope || "user_default", ...code ? { code } : {}, redirect_uri: redirectUri || globalThis.location.href, ...verifier ? { code_verifier: verifier } : {}, client_id: hostConfig.clientId }) })).json(); handlePossibleErrors(data); return { accessToken: data.access_token, refreshToken: data.refresh_token, errors: data.errors }; } catch (err) { console.error(err); return new Promise(() => {}); } } function createBodyWithCredentialsEtc(clientId, clientSecret, scope, subject, userId) { const commonProps = { client_id: clientId, client_secret: clientSecret, scope }; if (subject) return { ...commonProps, grant_type: "urn:qlik:oauth:user-impersonation", user_lookup: { field: "subject", value: subject } }; if (userId) return { ...commonProps, grant_type: "urn:qlik:oauth:user-impersonation", user_lookup: { field: "userId", value: userId } }; return { ...commonProps, grant_type: "client_credentials" }; } async function getOauthTokensWithCredentials(baseUrl, clientId, clientSecret, scope = "user_default", subject, userId) { const data = await (await fetch(`${baseUrl}/oauth/token`, { method: "POST", mode: "cors", headers: { "content-type": "application/json" }, body: JSON.stringify(createBodyWithCredentialsEtc(clientId, clientSecret, scope, subject, userId)) })).json(); return { accessToken: data.access_token, refreshToken: data.refresh_token, errors: data.errors }; } async function getOauthTokensWithRefreshToken(baseUrl, refreshToken, clientSecret) { const data = await (await fetch(`${baseUrl}/oauth/token`, { method: "POST", mode: "cors", headers: { "content-type": "application/json" }, body: JSON.stringify({ grant_type: "refresh_token", refresh_token: refreshToken, client_secret: clientSecret }) })).json(); return { accessToken: data.access_token, refreshToken: data.refresh_token, errors: data.errors }; } async function getAnonymousOauthAccessToken(baseUrl, accessCode, clientId, trackingCode) { const data = await (await fetch(`${baseUrl}/oauth/token/anonymous-embed`, { method: "POST", mode: "cors", headers: { "content-type": "application/json" }, body: JSON.stringify({ eac: accessCode, client_id: clientId, grant_type: "urn:qlik:oauth:anonymous-embed", tracking_code: trackingCode }) })).json(); return { accessToken: data.access_token, refreshToken: data.refresh_token, errors: data.errors }; } /** * Fetches the access token from storage or memory. * This code is intended to run in a node environment */ async function getOAuthTokensForNode(hostConfig) { const { clientId, clientSecret, performInteractiveLogin } = hostConfig; if (!clientId || !clientSecret && !performInteractiveLogin) throw new InvalidHostConfigError("A host config with authType set to \"oauth2\" has to provide a clientId and a clientSecret or a performInteractiveLogin function"); return await loadOrAcquireAccessTokenOauth(hostConfig, async () => { if (hostConfig.performInteractiveLogin) return getOauthTokensWithInteractiveLogin(hostConfig); if (!hostConfig.clientId || !hostConfig.clientSecret) throw new InvalidHostConfigError("A host config with authType set to \"oauth2\" has to provide a clientId and a clientSecret"); return getOauthTokensWithCredentials(toValidLocationUrl(hostConfig), hostConfig.clientId, hostConfig.clientSecret, hostConfig.scope, hostConfig.subject, hostConfig.userId); }); } /** * Fetches the access token from storage or memory. If no one is found a code and verifier is expected to be found in the sesion storage * This code is intended to run in a browser */ async function getOAuthTokensForBrowser(hostConfig) { const { clientId } = hostConfig; if (!clientId) throw new InvalidHostConfigError("A host config with authType set to \"oauth2\" has to also provide a clientId"); const oauthTokens = await loadOrAcquireAccessTokenOauth(hostConfig, async () => { if (hostConfig.getAccessToken) try { return { accessToken: typeof hostConfig.getAccessToken === "string" ? await lookupGetAccessFn(hostConfig.getAccessToken)() : await hostConfig.getAccessToken(), refreshToken: void 0, errors: void 0 }; } catch { return errorMessageToAuthData("Could not fetch access token using custom function"); } if (hostConfig.performInteractiveLogin) return getOauthTokensWithInteractiveLogin(hostConfig); const topic = getTopicFromOauthHostConfig(hostConfig); const code = loadAndDeleteFromSessionStorage(topic, "code"); const verifier = loadAndDeleteFromSessionStorage(topic, "verifier"); if (code && verifier) { const tokenResponse = await exchangeCodeAndVerifierForAccessTokenData(hostConfig, code, verifier, hostConfig.redirectUri); if (tokenResponse) return tokenResponse; } }); if (oauthTokens) return oauthTokens; if (hostConfig.performInteractiveLogin) return new Promise(() => {}); await requestRedirect(hostConfig); startFullPageLoginFlow(hostConfig); return new Promise(() => {}); } async function getOauthTokensWithInteractiveLogin(hostConfig) { if (!hostConfig.performInteractiveLogin) return errorMessageToAuthData("No performInteractiveLogin function in hostConfig"); let usedRedirectUri; try { const verifier = generateRandomString(128); const originalState = generateRandomString(43); const { code, state } = extractCodeAndState(await toPerformInteractiveLoginFunction(hostConfig.performInteractiveLogin)({ getLoginUrl: async ({ redirectUri }) => { usedRedirectUri = redirectUri; return createInteractiveLoginUrl(hostConfig, redirectUri, originalState, verifier); } })); if (!usedRedirectUri) return errorMessageToAuthData("No redirect uri provided"); if (originalState !== state) return errorMessageToAuthData("State returned by custom interactive login function does not match original"); if (!code) return errorMessageToAuthData("No code found in response from custom interactive login function"); return await exchangeCodeAndVerifierForAccessTokenData(hostConfig, code, verifier, usedRedirectUri); } catch (error) { return { accessToken: void 0, refreshToken: void 0, errors: [{ code: "", status: 401, title: "Could not perform custom interactive login", detail: String(error) }] }; } } let lastOauthTokensCall = Promise.resolve(""); async function getOAuthAccessToken(hostConfig) { if (isNode()) { const tokens = await getOAuthTokensForNode(hostConfig); if (tokens) { handlePossibleErrors(tokens); return tokens.accessToken || ""; } return ""; } if (isBrowser()) lastOauthTokensCall = lastOauthTokensCall.then(async () => { const tokens = await getOAuthTokensForBrowser(hostConfig); if (tokens) { handlePossibleErrors(tokens); return tokens.accessToken || ""; } return ""; }); return lastOauthTokensCall; } async function refreshAccessToken(hostConfig) { const tokens = await loadCachedOauthTokens(hostConfig); clearStoredOauthTokens(hostConfig); if (tokens && tokens.refreshToken && hostConfig.clientSecret) { const refreshedTokens = await loadOrAcquireAccessTokenOauth(hostConfig, async () => { if (!tokens || !tokens.refreshToken || !hostConfig.clientSecret) throw new Error("Trying to refresh tokens without refreshToken or clientSecret"); return getOauthTokensWithRefreshToken(toValidLocationUrl(hostConfig), tokens.refreshToken, hostConfig.clientSecret); }); if (refreshedTokens) handlePossibleErrors(refreshedTokens); } } function extractCodeAndState(input) { if (typeof input === "string") { let parsedUrl; try { parsedUrl = new URL(input); } catch { parsedUrl = new URL(input, "http://localhost"); } const queryParams = new URLSearchParams(parsedUrl.search); return { code: queryParams.get("code") || "", state: queryParams.get("state") || "" }; } return input; } function errorMessageToAuthData(message) { return { accessToken: void 0, refreshToken: void 0, errors: [{ code: "", status: 401, title: message, detail: "" }] }; } //#endregion //#region src/auth/internal/default-auth-modules/oauth/temporary-token.ts async function exchangeAccessTokenForTemporaryToken(hostConfig, accessToken, purpose) { const response = await fetch(`${toValidLocationUrl(hostConfig)}/oauth/token`, { method: "POST", credentials: "include", mode: "cors", headers: { "content-type": "application/json" }, redirect: "follow", body: JSON.stringify({ subject_token: accessToken, subject_token_type: "urn:ietf:params:oauth:token-type:access_token", grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", purpose, redirect_uri: globalThis.location?.href, client_id: hostConfig.clientId }) }); if (response.status !== 200) throw await toError(response); return (await response.json()).access_token; } async function toError(response) { const body = await response.text(); try { return new AuthorizationError(JSON.parse(body).errors); } catch { return new AuthorizationError([{ code: "unknown", status: response.status, detail: body, title: "Unknown authentication error" }]); } } //#endregion //#region src/auth/internal/default-auth-modules/anonymous.ts /** * Retries the passed in function after calling handleAuthenticationError */ async function handlePotentialAuthenticationErrorAndRetry$1(hostConfig, fn) { try { return await fn(); } catch (err) { const { retry } = await handleAuthenticationError$10({ hostConfig}); if (retry) return fn(); throw err; } } async function getOrCreateTrackingCode(hostConfig) { let trackingCode; if (isBrowser()) { const topic = getTopicFromAnonHostConfig(hostConfig); trackingCode = loadFromLocalStorage(topic, "tracking-code"); if (!trackingCode) trackingCode = createTrackingCode(); saveInLocalStorage(topic, "tracking-code", trackingCode); } else trackingCode = createTrackingCode(); return trackingCode; } function createTrackingCode() { const timeStamp = Math.floor(Date.now() / 1e3).toString(16); return `${timeStamp}${generateRandomHexString(40 - timeStamp.length)}`; } async function getAnonymousAccessToken(hostConfig) { const { accessCode, clientId } = hostConfig; if (!accessCode || !clientId) throw new InvalidHostConfigError("A host config with authType set to \"anonymous\" has to provide both an accessCode and clientId"); const tokens = await loadOrAcquireAccessTokenAnon(hostConfig, async () => { return getAnonymousOauthAccessToken(toValidLocationUrl(hostConfig), accessCode, clientId, await getOrCreateTrackingCode(hostConfig)); }); if (!tokens) return ""; if (tokens.errors) throw new AuthorizationError(tokens.errors); if (tokens.accessToken) return tokens.accessToken; return ""; } async function getRestCallAuthParams$10({ hostConfig }) { return { headers: { Authorization: `Bearer ${await getAnonymousAccessToken(hostConfig)}` }, queryParams: {}, credentials: "omit" }; } async function getWebSocketAuthParams$10({ hostConfig }) { if (isNode()) return { headers: { Authorization: `Bearer ${await getAnonymousAccessToken(hostConfig)}` } }; return { queryParams: { accessToken: await handlePotentialAuthenticationErrorAndRetry$1(hostConfig, async () => { return exchangeAccessTokenForTemporaryToken(hostConfig, await getAnonymousAccessToken(hostConfig), "websocket"); }) } }; } async function getWebResourceAuthParams$2({ hostConfig }) { return { queryParams: { accessToken: await handlePotentialAuthenticationErrorAndRetry$1(hostConfig, async () => { return exchangeAccessTokenForTemporaryToken(hostConfig, await getAnonymousAccessToken(hostConfig), "websocket"); }) } }; } async function handleAuthenticationError$10({ hostConfig }) { clearStoredAnonymousTokens(hostConfig); return { preventDefault: false, retry: true }; } var anonymous_default = { requiredProps: ["clientId", "accessCode"], optionalProps: [], getRestCallAuthParams: getRestCallAuthParams$10, getWebSocketAuthParams: getWebSocketAuthParams$10, getWebResourceAuthParams: getWebResourceAuthParams$2, handleAuthenticationError: handleAuthenticationError$10 }; //#endregion //#region src/auth/internal/default-auth-modules/apikey.ts function getRestCallAuthParams$9({ hostConfig }) { return Promise.resolve({ headers: { Authorization: `Bearer ${hostConfig?.apiKey}` }, queryParams: {}, credentials: "omit" }); } async function getWebSocketAuthParams$9({ hostConfig }) { if (isBrowser()) throw new Error("Not supported in browser environment"); return Promise.resolve({ headers: { Authorization: `Bearer ${hostConfig?.apiKey}` } }); } function handleAuthenticationError$9() { return Promise.resolve({}); } var apikey_default = { requiredProps: ["apiKey"], optionalProps: [], getRestCallAuthParams: getRestCallAuthParams$9, getWebSocketAuthParams: getWebSocketAuthParams$9, handleAuthenticationError: handleAuthenticationError$9 }; //#endregion //#region src/invoke-fetch/invoke-fetch-errors.ts /** * Error thrown by the invokeFetch function */ var InvokeFetchError = class extends Error { status; headers; data; constructor(errorMessage, status, headers, data) { super(errorMessage); this.status = status; this.headers = headers; this.data = data; this.stack = cleanStack(this.stack); } }; /** * Error thrown if a request-body cannot be encoded. */ var EncodingError = class extends Error { contentType; data; constructor(errorMessage, contentType, data) { super(errorMessage); this.contentType = contentType; this.data = data; this.stack = cleanStack(this.stack); } }; const regex = /^.+\/qmfe-api(?:\.js)?:(\d+)(?::\d+)?$/gim; const isFromQmfeApi = (line) => { const matches = line.match(regex); return Boolean(matches && matches.length > 0); }; /** * Function that removes stack entries that are from the qmfe-api module */ function cleanStack(stack) { if (!stack) return stack; const newStack = []; const lines = stack.split("\n"); lines.reverse(); for (const line of lines) { if (isFromQmfeApi(line)) break; newStack.unshift(line); } return newStack.join("\n"); } /** * Encode the query map into a query string * @private */ function encodeQueryParams(query) { if (!query) return ""; return Object.entries(query).map((kv) => { const [, value] = kv; if (value === void 0) return; return kv.map((val) => encodeURIComponent(decodeURIComponent(val))).join("="); }).filter(Boolean).join("&"); } /** * Replaces the variable placeholders in the path template with the variable values * @param pathTemplate * @param pathVariables * @private */ function applyPathVariables(pathTemplate, pathVariables) { let result = pathTemplate; if (pathVariables) Object.keys(pathVariables).forEach((key) => { result = result.replace(`{${key}}`, pathVariables[key]); }); return result; } /** * Join url and query to a complete url * @private */ function toCompleteUrl(url, query) { if (query !== "") return `${url}?${query}`; return url; } /** * Returns true if the status code is 301 or 302 or for ..../qps/csrftoken with 403 which really should be 401 on windows but isn't for historical reasons */ function means401OnWindows(completeUrl, statusCode) { const url = new URL(completeUrl, "http://dummy"); const isQpsCsrfTokenUrl = url.pathname.endsWith("/qps/csrftoken") && url.searchParams.has("xrfkey"); return statusCode === 301 || statusCode === 302 || isQpsCsrfTokenUrl && statusCode === 403; } //#endregion //#region src/invoke-fetch/internal/invoke-xhr.ts async function invokeXHR(completeUrl, { method, headers, credentials, keepalive, body, signal, progress }) { const xhr = new XMLHttpRequest(); let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); xhr.open(method || "GET", completeUrl); if (typeof headers === "object") for (const [key, value] of Object.entries(headers)) xhr.setRequestHeader(key, value); else throw Error("malformed headers", headers); if (keepalive) xhr.setRequestHeader("Connection", "keep-alive"); if (signal) signal.addEventListener("abort", () => { xhr.abort(); reject(); }); if (credentials === "include") xhr.withCredentials = true; else xhr.withCredentials = false; if (progress?.onUpload) xhr.upload.onprogress = (event) => { const { loaded, total, lengthComputable } = event; progress.onUpload?.({ loaded, total: lengthComputable ? total : void 0 }); }; if (progress?.onDownload) xhr.onprogress = (event) => { const { loaded, total, lengthComputable } = event; progress.onDownload?.({ loaded, total: lengthComputable ? total : void 0 }); }; xhr.onloadend = () => { const { status } = xhr; const responseHeaders = {}; for (const line of xhr.getAllResponseHeaders().split("\r\n")) { const [key, value] = line.split(":", 2); if (key && value) responseHeaders[key] = value; } if (xhr.response) resolve(new Response(xhr.response, { status, headers: responseHeaders })); else resolve(new Response(void 0, { status, headers: responseHeaders })); }; try { const bod = body; xhr.send(bod); } catch (e) { return Promise.reject(new InvokeFetchError(getErrorMessage(e), 0, new Headers(), {})); } return promise; } //#endregion //#region src/invoke-fetch/internal/response-cache.ts const responseCaches = {}; globalThis.__API_CACHE__DO_NOT_USE_OR_YOU_WILL_BE_FIRED = responseCaches; if (globalThis.QlikMain?.INTERNAL__DO_NOT_USE) globalThis.QlikMain.INTERNAL__DO_NOT_USE.apiCache = responseCaches; let defaultCacheTime = 1e3 * 60 * 10; /** * The global namespace is used as a fallback if an entry is not found in a specific cache. * This cache can be used internally, specifically by BFF interceptors, * where we cannot reliably map a response to an API-module. */ const globalCacheNamespace = ".global"; /** * Gets a cached response-promise from the specified API-namespace (or global cache). * A promise is returned if options.noCache is not true, the promise exists and is not too * old (based on defaults and cache-options). * * @param api The cache-namespace of the API. * @param props CachingContext * @returns The cached response (cloned and ready to use) or undefined. */ function getFromCache(api, props) { if (props?.options?.noCache) return; const { method, completeUrl, cacheKey } = props; const caches = getPossibleCaches(api); if (isModifyingOperation(method, completeUrl)) return; let entry; for (const cache of caches) if (cacheKey in cache) { entry = cache[cacheKey]; if (entry && typeof entry.value !== "undefined" && shouldUseCachedResult(props?.options, entry, defaultCacheTime)) break; entry = void 0; } if (!entry) return; entry.lastHit = /* @__PURE__ */ new Date(); entry.hitCount += 1; entry.accessedPaths.add(globalThis.location ? globalThis.location.pathname : "node"); const value = entry.value; return cloneResultPromise(value); } /** * Updates the cache of 'api' with the response-promise based on the caching-context. * If the promise comes from a modifying operation, related cache entries will be removed. * The returned promise is either the original promise (if it isn't cached) or a * cloned promise that is safe to use. * * Note: The original response-promise should not be reused directly, use the returned * equivalent instead. * * @private * @param api The cache-namespace of the API. * @param props CachingContext * @param responsePromise Promise<InvokeFetchResponse>, the thing to cache * @returns */ function updateCache(api, props, responsePromise) { if (!responseCaches[api]) responseCaches[api] = {}; const { cacheKey } = props; if (isModifyingOperation(props.method, props.completeUrl)) return responsePromise.then((res) => { const caches = getPossibleCaches(api); for (const cache of caches) clearRelatedCacheEntries(cache, cacheKey); return res; }); const cacheNamespace = responseCaches[api]; const responsePromiseWithCacheClearing = responsePromise.catch((error) => { delete cacheNamespace[cacheKey]; return Promise.reject(error); }); cacheNamespace[cacheKey] = { lastPulled: Date.now(), value: responsePromiseWithCacheClearing, lastHit: null, hitCount: 0, accessedPaths: /* @__PURE__ */ new Set() }; return cloneResultPromise(responsePromiseWithCacheClearing); } /** * Returns true if it is ok the to use the cached promise from a previous invocation * @private */ function shouldUseCachedResult(options, cacheEntry, defaultMaxCacheTime) { if (options?.noCache) return false; if (!cacheEntry || typeof cacheEntry.value === "undefined") return false; if (options?.useCacheIfAfter) return options.useCacheIfAfter.getTime() <= cacheEntry.lastPulled; const age = Date.now() - cacheEntry.lastPulled; if (typeof options?.maxCacheAge === "number") return age <= options?.maxCacheAge; return age < defaultMaxCacheTime; } /** * Builds a key used for caching based on a CachingContext * @private * @param cachingContext * @returns a key used for caching */ function toCacheKey({ url, query, headers, serializedHostConfig }) { let cacheKey = url; if (query && Object.keys(query).length > 0) { const queryString = encodeQueryParams(query); if (url.includes("?")) cacheKey = cacheKey.concat(`&${queryString}`); else cacheKey = cacheKey.concat(`?${queryString}`); } if (headers && Object.keys(headers).length > 0) cacheKey = cacheKey.concat(`+headers=${JSON.stringify(headers)}`); if (serializedHostConfig && serializedHostConfig !== "{}") cacheKey = cacheKey.concat(`+host-config=${serializedHostConfig}`); return cacheKey; } /** * Convenience function for getting a list of caches that are OK to use * in the context of an API-namespace. * @param api * @returns */ function getPossibleCaches(api) { const caches = []; if (responseCaches[api]) caches.push(responseCaches[api]); if (responseCaches[".global"]) caches.push(responseCaches[globalCacheNamespace]); return caches; } /** * @private * Clears all cache entries where the modifying URL starts with the cached URL * @param cache * @param cacheKey */ function clearRelatedCacheEntries(cache, cacheKey) { const modifyingUrl = cacheKeyToUrl(cacheKey); for (const existingCacheKey in cache) { const cleanUrl = cacheKeyToUrl(existingCacheKey); if (modifyingUrl.startsWith(cleanUrl) || cleanUrl.startsWith(modifyingUrl)) delete cache[existingCacheKey]; } } /** * Removes query, headers and host-config from the cache key to get a clean URL. */ function cacheKeyToUrl(cachedUrl) { const queryIdx = cachedUrl.indexOf("?"); if (queryIdx >= 0) return cachedUrl.substring(0, queryIdx); const headersIdx = cachedUrl.indexOf("+headers="); if (headersIdx >= 0) return cachedUrl.substring(0, headersIdx); const hostConfigIdx = cachedUrl.indexOf("+host-config="); if (hostConfigIdx >= 0) return cachedUrl.substring(0, hostConfigIdx); return cachedUrl; } /** * Clone mutable-objects using JSON.stringify. * Strings, Blobs and ReadableStreams are not cloned. * @private */ function clone(value) { if (typeof value === "undefined" || value === null) return value; if (value && (value instanceof Blob || value instanceof Object && value.toString() === "[object Blob]")) return value; if (value && value instanceof ReadableStream) return value; if (typeof value === "string") return value; return JSON.parse(JSON.stringify(value)); } /** * Clones the result so that no one can tamper with the cache. Note that the headers object is NOT cloned * @private */ function cloneResultPromise(value) { return value.then((resp) => { const result = { data: clone(resp.data), headers: resp.headers, status: resp.status }; if (resp.next) result.next = resp.next; if (resp.prev) result.prev = resp.prev; return result; }); } /** * Clears cached responses * @param api the api to clear cac