@nebula.js/stardust
Version:
Product and framework agnostic integration API for Qlik's Associative Engine
1,286 lines (1,270 loc) • 107 kB
JavaScript
/*
* @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