@nebula.js/stardust
Version:
Product and framework agnostic integration API for Qlik's Associative Engine
2,520 lines • 96.1 kB
JavaScript
/*
* @nebula.js/stardust v6.3.0
* Copyright (c) 2025 QlikTech International AB
* Released under the MIT license.
*/
System.register(['./index-DC5bJ1TW.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$1,
getAccessToken: getAccessToken,
getDefaultHostConfig: getDefaultHostConfig$1,
getRestCallAuthParams: getRestCallAuthParams,
getWebResourceAuthParams: getWebResourceAuthParams,
getWebSocketAuthParams: getWebSocketAuthParams,
handleAuthenticationError: handleAuthenticationError,
isHostCrossOrigin: isHostCrossOrigin,
isWindows: isWindows,
registerAuthModule: registerAuthModule$1,
registerHostConfig: registerHostConfig$1,
serializeHostConfig: serializeHostConfig$1,
setDefaultHostConfig: setDefaultHostConfig$1,
toValidLocationUrl: toValidLocationUrl,
toValidWebsocketLocationUrl: toValidWebsocketLocationUrl,
unregisterHostConfig: unregisterHostConfig$1
});
//#region src/interceptors/interceptors.ts
let GLOBAL_INTERCEPTORS;
function createInterceptors() {
const interceptors$1 = [...GLOBAL_INTERCEPTORS?.getInterceptors() || []];
return {
addInterceptor: (interceptor) => {
interceptors$1.push(interceptor);
return interceptor;
},
removeInterceptor: (interceptor) => {
const index = interceptors$1.indexOf(interceptor);
let removed;
if (index !== -1) removed = interceptors$1.splice(index, 1)[0];
return removed || null;
},
getInterceptors: () => interceptors$1
};
}
/**
* The global interceptor stack
*/
GLOBAL_INTERCEPTORS = createInterceptors();
/**
* Gets all registered interceptors
*/
function getInterceptors() {
return GLOBAL_INTERCEPTORS.getInterceptors();
}
//#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"
];
const urlAlphabet =
'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict';
/* @ts-self-types="./index.d.ts" */
let random = bytes => crypto.getRandomValues(new Uint8Array(bytes));
let customRandom = (alphabet, defaultSize, getRandom) => {
let mask = (2 << Math.log2(alphabet.length - 1)) - 1;
let step = -~((1.6 * mask * defaultSize) / alphabet.length);
return (size = defaultSize) => {
let id = '';
while (true) {
let bytes = getRandom(step);
let j = step | 0;
while (j--) {
id += alphabet[bytes[j] & mask] || '';
if (id.length >= size) return id
}
}
}
};
let customAlphabet = (alphabet, size = 21) =>
customRandom(alphabet, size | 0, random);
let nanoid = (size = 21) => {
let id = '';
let bytes = crypto.getRandomValues(new Uint8Array((size |= 0)));
while (size--) {
id += urlAlphabet[bytes[size] & 63];
}
return id
};
//#region src/platform/platform-functions.ts
const getPlatform = async (options = {}) => {
const hc = withResolvedHostConfig(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?.productInfo?.() : 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
/**
* Method helper for generating a random string [a-zA-Z0-9\-_]{length}
* @param length - the length of the string
*/
function generateRandomString(targetLength) {
return nanoid(targetLength);
}
/**
* Method helper for generating a random hexadecimal-string [0-9a-f]{length}
* @param length - the length of the string
*/
function generateRandomHexString(targetLength) {
return customAlphabet("1234567890abcdef", targetLength)();
}
//#endregion
//#region src/utils/expose-internal-test-apis.ts
const internalApisName = "__QLIK_INTERNAL__DO_NOT_USE_OR_YOU_WILL_BE_FIRED";
function exposeInternalApiOnWindow(name, fn) {
if (globalThis.location?.origin.startsWith("https://localhost:") || globalThis.location?.origin?.endsWith("qlik-stage.com")) {
if (globalThis[internalApisName] === void 0) globalThis[internalApisName] = {};
globalThis[internalApisName][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 loadFromLocalStorage(topic, name) {
return localStorage.getItem(`${storagePrefix}-${topic}-${name}`) || void 0;
}
function loadFromSessionStorage(topic, name) {
return sessionStorage.getItem(`${storagePrefix}-${topic}-${name}`) || void 0;
}
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 loadAndDeleteFromSessionStorage(topic, name) {
const id = `${storagePrefix}-${topic}-${name}`;
const result$1 = sessionStorage.getItem(id) || void 0;
sessionStorage.removeItem(id);
return result$1;
}
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");
}
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();
const mayUseStorage = isBrowser();
const storedOauthTokens = cachedTokens[topic] || (mayUseStorage ? loadOauthTokensFromStorage(topic, accessTokenStorage) : void 0);
if (storedOauthTokens) {
cachedTokens[topic] = storedOauthTokens;
return Promise.resolve(storedOauthTokens);
}
const tokensPromise = acquireTokens();
cachedTokens[topic] = tokensPromise;
if (mayUseStorage) {
const tokens = await tokensPromise;
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);
}
}
return tokensPromise;
}
function clearStoredOauthTokens(hostConfig) {
const topic = getTopicFromOauthHostConfig(hostConfig);
delete cachedTokens[topic];
if (isBrowser()) {
deleteFromLocalStorage(topic, ["access-token", "refresh-token"]);
deleteFromSessionStorage(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$1) {
return globalThis[getAccessToken$1];
}
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$1 = "";
if (isBrowser()) {
const byteArrayToString = String.fromCharCode.apply(null, hashArray);
result$1 = btoa(byteArrayToString);
} else if (isNode()) result$1 = Buffer.from(hashArray).toString("base64");
else throw new Error("Environment not supported for oauth2 authentication");
return result$1;
}
/**
* @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 } = hostConfig;
if (!clientId || !clientSecret) throw new InvalidHostConfigError("A host config with authType set to \"oauth2\" has to provide a clientId and a clientSecret");
return await loadOrAcquireAccessTokenOauth(hostConfig, async () => {
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) {
let usedRedirectUri;
try {
const verifier$1 = generateRandomString(128);
const originalState = generateRandomString(43);
const { code: code$1, state } = extractCodeAndState(await toPerformInteractiveLoginFunction(hostConfig.performInteractiveLogin)({ getLoginUrl: async ({ redirectUri }) => {
usedRedirectUri = redirectUri;
return createInteractiveLoginUrl(hostConfig, redirectUri, originalState, verifier$1);
} }));
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$1) return errorMessageToAuthData("No code found in response from custom interactive login function");
return await exchangeCodeAndVerifierForAccessTokenData(hostConfig, code$1, verifier$1, usedRedirectUri);
} catch (error) {
return {
accessToken: void 0,
refreshToken: void 0,
errors: [{
code: "",
status: 401,
title: "Could not perform custom interactive login",
detail: `${error}`
}]
};
}
}
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(() => {});
if (hostConfig.authRedirectUserConfirmation) await hostConfig.authRedirectUserConfirmation();
startFullPageLoginFlow(hostConfig);
return new Promise(() => {});
}
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") {
const queryParams = new URLSearchParams(new URL(input).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$9({
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$9({ hostConfig }) {
return {
headers: { Authorization: `Bearer ${await getAnonymousAccessToken(hostConfig)}` },
queryParams: {},
credentials: "omit"
};
}
async function getWebSocketAuthParams$9({ 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$9({ hostConfig }) {
clearStoredAnonymousTokens(hostConfig);
return {
preventDefault: false,
retry: true
};
}
var anonymous_default = {
requiredProps: ["clientId", "accessCode"],
optionalProps: [],
getRestCallAuthParams: getRestCallAuthParams$9,
getWebSocketAuthParams: getWebSocketAuthParams$9,
getWebResourceAuthParams: getWebResourceAuthParams$2,
handleAuthenticationError: handleAuthenticationError$9
};
//#endregion
//#region src/auth/internal/default-auth-modules/apikey.ts
function getRestCallAuthParams$8({ hostConfig }) {
return Promise.resolve({
headers: { Authorization: `Bearer ${hostConfig?.apiKey}` },
queryParams: {},
credentials: "omit"
});
}
async function getWebSocketAuthParams$8({ hostConfig }) {
if (isBrowser()) throw new Error("Not supported in browser environment");
return Promise.resolve({ headers: { Authorization: `Bearer ${hostConfig?.apiKey}` } });
}
function handleAuthenticationError$8() {
return Promise.resolve({});
}
var apikey_default = {
requiredProps: ["apiKey"],
optionalProps: [],
getRestCallAuthParams: getRestCallAuthParams$8,
getWebSocketAuthParams: getWebSocketAuthParams$8,
handleAuthenticationError: handleAuthenticationError$8
};
//#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$1 = pathTemplate;
if (pathVariables) Object.keys(pathVariables).forEach((key) => {
result$1 = result$1.replace(`{${key}}`, pathVariables[key]);
});
return result$1;
}
/**
* Join url and query to a complete url
* @private
*/
function toCompleteUrl(url, query) {
if (query !== "") return `${url}?${query}`;
return url;
}
//#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;
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) {
const queryString = encodeQueryParams(query);
if (url.includes("?")) cacheKey = cacheKey.concat(`&${queryString}`);
else cacheKey = cacheKey.concat(`?${queryString}`);
}
if (headers) 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[globalCacheNamespace]) 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$1 = {
data: clone(resp.data),
headers: resp.headers,
status: resp.status
};
if (resp.next) result$1.next = resp.next;
if (resp.prev) result$1.prev = resp.prev;
return result$1;
});
}
/**
* Returns true if the method might modify things requiring caches to be cleared
*/
function isModifyingOperation(method, url) {
if (method === "get" || method === "GET") return false;
if (url.endsWith("/api/v1/licenses/allotments") && (method === "post" || method === "POST")) return false;
return true;
}
//#endregion
//#region src/invoke-fetch/internal/invoke-fetch-methods.ts
function getErrorMessage(error) {
if (error instanceof Error) {
if (error.cause) {
if (error.message) return `${error.message} - ${getErrorMessage(error.cause)}`;
return getErrorMessage(error.cause);
}
return error.message;
}
return String(error);
}
async function fetchAndTransformExceptions(input, init) {
try {
return await fetch(input, init);
} catch (e) {
return Promise.reject(new InvokeFetchError(getErrorMessage(e), 0, new Headers(), {}));
}
}
async function performActualHttpFetch(method, completeUrl, unencodedBody, contentType, options, authHeaders, credentials, userAgent) {
const { body, contentTypeHeader, requestOptions } = encodeBody(unencodedBody, contentType ?? "");
const headers = {
...contentTypeHeader,
...authHeaders,
...options?.headers,
...getServiceOverrideHeaderFromLocalStorage()
};
if (!headers["User-Agent"] && userAgent) headers["User-Agent"] = userAgent;
const request = {
method,
credentials,
mode: "cors",
headers,
redirect: await isWindows(options?.hostConfig) ? "manual" : "follow",
keepalive: options?.keepalive,
progress: options?.progress,
body,
...requestOptions
};
let fetchTimeoutId;
if (options?.signal) request.signal = options.signal;
else if (options?.timeoutMs && options.timeoutMs > 0) {
const controller = new AbortController();
fetchTimeoutId = setTimeout(() => {
controller.abort();
}, options.timeoutMs);
request.signal = controller.signal;
}
let response;
if (options?.progress) response = await invokeXHR(completeUrl, request);
else response = await fetchAndTransformExceptions(completeUrl, request);
if (fetchTimeoutId) clearTimeout(fetchTimeoutId);
return await parseFetchResponse(response, completeUrl);
}
function encodeBody(unencodedBody, contentType) {
if (!unencodedBody) return {
body: null,
contentTypeHeader: {},
requestOptions: {}
};
const contentTypeHeader = {};
const requestOptions = {};
let body = null;
switch (contentType) {
case "":
case "application/json":
contentTypeHeader["Content-Type"] = "application/json";
body = JSON.stringify(unencodedBody);
break;
case "multipart/form-data":
body = encodeMultipartBody(unencodedBody);
break;
case "application/octet-stream":
contentTypeHeader["Content-Type"] = contentType;
requestOptions["duplex"] = "half";
body = unencodedBody;
break;
case "text/plain":
if (typeof unencodedBody === "string") {
contentTypeHeader["Content-Type"] = contentType;
body = unencodedBody;
} else throw new EncodingError(`Cannot send ${typeof unencodedBody} as ${contentType}, body should be a string.`, contentType, unencodedBody);
break;
default: throw new EncodingError(`Unsupported content-type "${contentType}", supported are: application/json, multipart/form-data, application/octet-stream and text/plain`, contentType, unencodedBody);
}
return {
body,
contentTypeHeader,
requestOptions
};
}
function encodeMultipartBody(unencodedBody) {
const contentType = "multipart/form-data";
if (typeof unencodedBody !== "object") throw new EncodingError(`Cannot encode ${typeof unencodedBody} as ${contentType}, body should be an object.`, contentType, unencodedBody);
if (Array.isArray(unencodedBody)) throw new EncodingError(`Cannot encode ${typeof unencodedBody} as ${contentType}, body should be an object.`, contentType, unencodedBody);
if (unencodedBody instanceof FormData) return unencodedBody;
const form = new FormData();
Object.entries(unencodedBody).forEach((entry) => {
const [key, value] = entry;
switch (typeof value) {
case "boolean":
case "number":
case "string":
form.set(key, `${value}`);
break;
case "object":
if (value instanceof Blob) form.set(key, value);
else if (value instanceof Uint8Array) {
const data = new Blob([value], { type: "application/octet-stream" });
form.set(key, data);
} else {
const json = JSON.stringify(value);
const data = new Blob([json], { type: "application/json" });
form.set(key, data, "");
}
break;
default: throw new EncodingError(`Cannot encode multipart-field "${key}" with value of type ${typeof value}, values must be objects, strings, numbers or boolean.`, contentType, unencodedBody);
}
});
return form;
}
async function getInvokeFetchUrlParams({ method, pathTemplate, pathVariables, query, options }) {
const locationUrl = toValidLocationUrl(options?.hostConfig);
const { headers: authHeaders, queryParams: authQueryParams, credentials } = await getRestCallAuthParams({
hostConfig: options?.hostConfig,
method
});
const url = locationUrl + applyPathVariables(pathTemplate, pathVariables);
const completeUrl = toCompleteUrl(url, encodeQueryParams({
...query,
...authQueryParams
}));
const serializedHostConfig = serializeHostConfig$1(options?.hostConfig);
return {
completeUrl,
authHeaders,
credentials,
cacheKey: toCacheKey({
url,
query,
headers: options?.headers,
serializedHostConfig
})
};
}
function invokeFetchWithUrl(api, props) {
return invokeFetchWithUrlAndRetry(api, props, async () => {
const { authHeaders, credentials } = await getInvokeFetchUrlParams(props);
return invokeFetchWithUrlAndRetry(api, {
...props,
authHeaders,
credentials,
options: {
...props.options,
noCache: true
}
}, void 0);
});
}
function invokeFetchWithUrlAndRetry(api, props, performRetry) {
const { method, completeUrl, body, options, authHeaders, credentials, cacheKey, contentType, userAgent } = props;
const cachingContext = {
method,
completeUrl,
cacheKey,
options
};
const cachedResponse = getFromCache(api, cachingContext);
if (cachedResponse) return cachedResponse;
const resultPromiseFromBackend = performActualHttpFetch(method, completeUrl, body, contentType, options, authHeaders, credentials, userAgent);
const resultAfterAuthenticationCheck = interceptAuthenticationErrors(options?.hostConfig, resultPromiseFromBackend, performRetry);
return updateCache(api, cachingContext, addPagingFunctions(api, {
...props,
value: resultAfterAuthenticationCheck
}));
}
/**
* Adds paging functions to the response object if there are paging links present in the response body
*/
function addPagingFunctions(api, { method, body, options, authHeaders, credentials, value }) {
return value.then((resp) => {
const dataWithPotentialLinks = resp.data;
if (!dataWithPotentialLinks) return resp;
const serializedHostConfig = serializeHostConfig$1(options?.hostConfig);
const prevUrl = dataWithPotentialLinks.links?.prev?.href;
const nextUrl = dataWithPotentialLinks.links?.next?.href;
if (prevUrl) resp.prev = (prevOptions) => invokeFetchWithUrl(api, {
method,
completeUrl: prevUrl,
body,
options: prevOptions || options,
authHeaders,
credentials,
cacheKey: toCacheKey({
url: prevUrl,
headers: options?.headers,
serializedHostConfig
})
});
if (nextUrl) resp.next = (nextOptions) => invokeFetchWithUrl(api, {
method,
completeUrl: nextUrl,
body,
options: nextOptions || options,
authHeaders,
credentials,
cacheKey: toCacheKey({
url: nextUrl,
headers: options?.headers,
serializedHostConfig
})
});
return resp;
});
}
function neverResolvingPromise() {
return new Promise(() => {});
}
async function interceptAuthenticationErrors(hostConfig, resultPromise, performRetry) {
try {
return await resultPromise;
} catch (error) {
const err = error;
const errorBody = err.data;
if (err.status === 401 || err.status === 403 && errorBody?.code === "CSRF-TOKEN-2" || (err.status === 301 || err.status === 302) && await isWindows(hostConfig)) {
if (globalThis.loggingOut) return neverResolvingPromise();
const { retry, preventDefault } = await handleAuthenticationError({
hostConfig,
status: err.status,
headers: err.headers,
errorBody,
canRetry: !!performRetry
});
if (retry && performRetry) return performRetry();
if (preventDefault) return neverResolvingPromise();
}
throw error;
}
}
/** Set the X-Qlik-Overrides header according to what's written to localStorage
* by qmfe-devtool */
function getServiceOverrideHeaderFromLocalStorage() {
if (!isBrowser()) return {};
const header = localStorage.getItem("qmfe-api-service-overrides-header");
if (!header) return {};
return { "X-Qlik-Overrides": header };
}
/** Convert a Blob to a DownloadableBlob */
function toDownloadableBlob(blob, name) {
const result$1 = blob;
if (name) result$1.download = (filename = name) => download(blob, filename);
else result$1.download = (filename) => download(blob, filename);
return result$1;
}
/** Convenience-function for downloading a blob. */
async function download(blob, filename) {
if (isBrowser()) {
const a = document.createElement("a");
const blobUrl = window.URL.createObjectURL(blob);
a.href = blobUrl;
a.download = filename;
a.click();
window.URL.revokeObjectURL(blobUrl);
} else {
const { writeFileSync } = await Promise.resolve().then(function () { return _nodeResolve_empty; });
const arrayBuffer = await blob.arrayBuffer();
writeFileSync(filename, new Uint8Array(arrayBuffer));
}
}
//#endregion
//#region src/invoke-fetch/invoke-fetch-functions.ts
const defaultUserAgent = "qmfe-api/latest";
/**
* Makes an HTTP call with the browser's fetch API to the endpoint configured in the call's arguments
* @param api Name of api to call. Will be used for caching responses.
* @param invokeFetchProps InvokeFetchProperties
*/
async function invokeFetch(api, props, interceptors) {
const effectiveInterceptors = getInterceptors();
const invokeFetchFinal = (reqeust) => invokeFetchIntercepted(api, reqeust);
return (effectiveInterceptors || []).reduce((proceed, interceptor) => (request) => interceptor(request, proceed), invokeFetchFinal)(props);
}
/** @private Runs the intercepted version of invoke fetch */
async function invokeFetchIntercepted(api, props) {
if (props.options?.hostConfig?.authType === "mock-backend-rest-recorder") return Promise.resolve(props.options?.hostConfig.recordInvokeFetch(props));
if (props.options?.hostConfig?.authType === "mock-backend") return Promise.resolve(props.options?.hostConfig.mockInvokeFetch(props));
checkForCrossDomainRequest(props.options?.hostConfig);
if (props.options?.hostConfig?.authType === "noauth") {
console.info("API call blocked: noauth auth module is configured, no API calls should be made");
return {
status: 204,
headers: new Headers(),
data: null
};
}
let userAgent;
if (props?.userAgent) userAgent = props.userAgent;
else if (isBrowser()) userAgent = `${window.navigator.userAgent} ${defaultUserAgent}`;
else userAgent = defaultUserAgent;
const { completeUrl, authHeaders, credentials, cacheKey } = await getInvokeFetchUrlParams(props);
return invokeFetchWithUrl(api, {
...props,
method: props.method.toUpperCase(),
completeUrl,
authHeaders,
credentials,
cacheKey,
userAgent
});
}
/**
* Function that extracts the payload from the fetch response.
* @param fetchResponse fetch response
* @param url to where the request as made
* @returns parsed fetch response
*/
async function parseFetchResponse(fetchResponse, url) {
let resultData;
const contentType = fetchResponse.headers.get("content-type")?.split(";")[0];
const contentDisposition = fetchResponse.headers.get("content-disposition")?.split(";");
if (contentDisposition && contentDisposition[0] === "attachment") {
let filename = "";
for (let i = 1; i < contentDisposition.length; i++) {
const attr = contentDisposition[i].trim();
if (attr.indexOf("filename") === 0) {
const start = attr.indexOf("\"");
const end = attr.lastIndexOf("\"");
filename = attr.slice(start + 1, end);
}
}
resultData = toDownloadableBlob(await fetchResponse.blob(), filename);
} else switch (contentType) {
case "image/png":
case "image/jpeg":
case "image/x-icon":
case "application/offset+octet-stream":
case "application/octet-stream":
case "application/zip":
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
case "application/pdf":
resultData = toDownloadableBlob(await fetchResponse.blob());
break;
case "text/event-stream":
resultData = fetchResponse.body;
break;
default:
try {
resultData = await fetchResponse.text();
resultData = JSON.parse(resultData);
} catch {}
break;
}
const { status, statusText, headers } = fetchResponse;
const errorMsg = `request to '${url}' failed with status ${status} ${statusText}.`;
if (status >= 300) throw new InvokeFetchError(errorMsg, status, headers, resultData);
if (status === 0) throw new InvokeFetchError(errorMsg, 302, headers, resultData);
return {
status,
headers,
data: resultData
};
}
//#endregion
//#region src/http/http-constants.ts
/**
* Name of the CSRF Token header.
*/
const QLIK_CSRF_TOKEN = "qlik-csrf-token";
//#endregion
//#region src/http/http-functions.ts
/**
* Clears the CSRF token
* @param location an optional location that is used to clear the csrf tokens when embedded
*/
function clearCsrfToken(hostConfig) {
const locationUrl = toValidLocationUrl(hostConfig);
delete csrfTokens[locationUrl];
}
/**
* Fetches the CSRF token from the server. The token is cached and will be reused for subsequent calls.
* @param hostConfig HostConfig object containing the location url and authentication options
* @param noCache Set to true to force a new token to be fetched
* @returns csrf token
*/
async function getCsrfToken(hostConfig, noCache) {
const locationUrl = toValidLocationUrl(hostConfig);
let pathTemplate;
if (await isWindows(hostConfig)) pathTemplate = "/qps/csrftoken";
else pathTemplate = "/api/v1/csrf-token";
const fetchCsrfToken = async () => {
try {
const csrfToken = (await invokeFetch("csrf-token", {
method: "get",
pathTemplate,
options: {
hostConfig,
noCache: true
}
})).headers.get(QLIK_CSRF_TOKEN);
if (!csrfToken) return "";
return csrfToken;
} catch (e) {
if (e.status === 404) return "";
throw e;
}
};
if (noCache) {
csrfTokens[locationUrl] = fetchCsrfToken();
return csrfTokens[locationUrl];
}
csrfTokens[locationUrl] = csrfTokens[locationUrl] || fetchCsrfToken();
return csrfTokens[locationUrl];
}
const csrfTokens = {};
//#endregion
//#region src/auth/internal/internal-auth-functions.ts
/**
* Returns the fetch credentials string based on the host config. Note that this
* assumes cookies are used so it should only be used by the cookie auth modes
* @param hostConfig the HostConfig containing authentication details
* @private
*/
function internalGetCredentialsForCookieAuth(hostConfig) {
if (hostConfig.crossSiteCookies === false) return "same-origin";
if (isHostCrossOrigin(hostConfig)) return "include";
return "same-origin";
}
async function resolveTokenIfPresent(getAccessToken$1) {
if (typeof getAccessToken$1 === "function") return getAccessToken$1();
if (typeof getAccessToken$1 === "string") {
const accessTokenFn = lookupGlobalGetAccessTokenFn(getAccessToken$1);
if (typeof accessTokenFn !== "function") throw new Error(`getAccessToken function "${getAccessToken$1}" not found on globalThis. Please make sure it is defined.`);
return accessTokenFn();
}
}
function lookupGlobalGetAccessTokenFn(getAccessToken$1) {
return globalThis[getAccessToken$1];
}
//#endregion
//#region src/auth/internal/default-auth-modules/cookie.ts
/**
* Returns true if the verb might modify things requiring caches to be cleared
*/
function isModifyingVerb(verb) {
return !(verb === "get" || verb === "GET");
}
async function getRestCallAuthParams$7({ hostConfig, method }) {
const headers = {};
if (isModifyingVerb(method)) headers["qlik-csrf-token"] = await getCsrfToken(hostConfig);
if (hostConfig.webIntegrationId) headers["qlik-web-integration-id"] = hostConfig.webIntegrationId;
return {
headers,
queryParams: {},
credentials: internalGetCredentialsForCookieAuth(hostConfig)
};
}
async function getWebSocketAuthParams$7({ hostConfig }) {
if (isNode()) {
const headers = {};
headers["qlik-csrf-token"] = await getCsrfToken(hostConfig);
if (hostConfig.webIntegrationId) headers["qlik-web-integration-id"] = hostConfig.webIntegrationId;
return { headers };
}
const params = { "qlik-csrf-token": await getCsrfToken(hostConfig, true) };
if (hostConfig.webIntegrationId) params["qlik-web-integration-id"] = hostConfig.webIntegrationId;
return { queryParams: params };
}
async function handleAuthenticationError$7({ hostConfig, status }) {
clearCsrfToken(hostConfig);
if (status === 403) return {
preventDefault: false,
retry: true
};
const webIntegrationParam = hostConfig.webIntegrationId ? `qlik-web-integration-id=${hostConfig?.webIntegrationId}&` : "";
const locationUrl = toValidLocationUrl(hostConfig);
if (hostConfig.authRedirectUserConfirmation) await hostConfig.authRedirectUserConfirmation();
globalThis.location.replace(`${locationUrl}/login?${webIntegrationParam}returnto=${encodeURIComponent(globalThis.location.href)}`);
return { preventDefault: true };
}
/** @private */
var cookie_default = {
requiredProps: [],
optionalProps: [
"webIntegrationId",
"crossSiteCookies",
"anonymousMode"
],
getRestCallAuthParams: getRestCallAuthParams$7,
getWebSocketAuthParams: getWebSocketAuthParams$7,
handleAuthenticationError: handleAuthenticationError$7
};
//#endregion
//#region src/auth/internal/default-auth-modules/noauth.ts
function getRestCallAuthParams$6(_props) {
return Promise.resolve({
headers: {},
queryParams: {},
credentials: "same-origin"
});
}
function getWebSocketAuthParams$6(_props) {
return Promise.resolve({ queryParams: {} });
}
function handleAuthenticationError$6(_props) {
return Promise.resolve({});
}
/** @private */
var noauth_default = {
requiredProps: [],
optionalProps: [],
getRestCallAuthParams: getRestCallAuthParams$6,
getWebSocketAuthParams: getWebSocketAuthParams$6,
handleAuthenticationError: handleAuthenticationError$6
};
//#endregion
//#region src/auth/internal/default-auth-modules/none.ts
function getRestCallAuthParams$5() {
return Promise.resolve({
headers: {},
queryParams: {},
credentials: "same-origin"
});
}
function getWebSocketAuthParams$5() {
return Promise.resolve({ queryParams: {} });
}
function handleAuthenticationError$5() {
return Promise.resolve({});
}
/** @private */
var none_default = {
requiredProps: [],
optionalProps: [],
getRestCallAuthParams: getRestCallAuthParams$5,
getWebSocketAuthParams: getWebSocketAuthParams$5,
handleAuthenticationError: handleAuthenticationError$5
};
//#endregion
//#region src/auth/internal/default-auth-modules/oauth/callback.ts
/**
* Receives the oauth code and state callback query parameters,
* verifies the state, saves the code in session storage, and then
* redirects the page to the final href if it is set and differes from current uri
*/
function handleOAuthCallback() {
const urlParams = new URLSearchParams(globalThis.location.search);
const callbackCode = urlParams.get("code") || void 0;
const callbackState = urlParams.get("state") || void 0;
if (urlParams.get("error")) {
const element = document.createElement("pre");
element.innerText = `<code>${JSON.stringify({
error: urlParams.get("error"),
error_code: urlParams.get("error_code"),
error_description: urlParams.get("error_description"),
error_detail: urlParams.get("error_detail"),
error_uri: urlParams.get("error_uri")
})}</code>`;
document.body.prepend(element);
}
const topic = loadAndDeleteFromSessionStorage("", "client-in-progress");
if (topic && callbackCode && callbackState) {
const stateFromLocalStorage = loadAndDeleteFromSessionStorage(topic, "state");
const finalRedirectUri = loadAndDeleteFromSessionStorage(topic, "href");
if (stateFromLocalStorage && stateFromLocalStorage === callbackState && finalRedirectUri) {
saveInSessionStorage(topic, "code", callbackCode);
if (finalRedirectUri !== globalThis.location.href) globalThis.location.replace(finalRedirectUri);
}
}
}
//#endregion
//#region src/auth/internal/default-auth-modules/oauth.ts
if (isBrowser()) handleOAuthCallback();
/**
* Retries the passed in function after calling handleAuthenticationError
*/
async function handlePotentialAuthenticationErrorAndRetry(hostConfig, fn) {
try {
return await fn();
} catch (err) {
const { retry } = await handleAuthenticationError$4({
hostConfig});
if (retry) return fn();
throw err;
}
}
async function getRestCallAuthParams$4({ hostConfig }) {
return {
headers: { Authorization: `Bearer ${await getOAuthAccessToken(hostConfig)}` },
queryParams: {},
credentials: "omit"
};
}
async function getWebSocketAuthParams$4({ hostConfig }) {
if (isNode()) return { headers: { Authorization: `Bearer ${await getOAuthAccessToken(hostConfig)}` } };
return { queryParams: { accessToken: await handlePotentialAuthenticationErrorAndRetry(hostConfig, async () => {
return exchangeAccessTokenForTemporaryToken(hostConfig, await getOAuthAccessToken(hostConfig), "websocket");
}) } };
}
async function getWebResourceAuthParams$1({ hostConfig }) {
return { queryParams: { accessToken: await handlePotentialAuthenticationErrorAndRetry(hostConfig, async () => {
return exchangeAccessTokenForTemporaryToken(hostConfig, await getOAuthAccessToken(hostConfig), "webresource");
}) } };
}
async function handleAuthenticationError$4({ hostConfig }) {
if (hostConfig.getAccessToken) {
clearStoredOauthTokens(hostConfig);
return {
preventDefault: false,
retry: true
};
}
if (isBrowser()) {
if (hostConfig.performInteractiveLogin) {
clearStoredOauthTokens(hostConfig);
return { retry: true };
}
if (hostConfig.authRedirectUserConfirmation) await hostConfig.authRedirectUserConfirmation();
startFullPageLoginFlow(hostConfig);
return { preventDefault: true };
}
await refreshAccessToken(hostConfig);
return {
preventDefault: false,
retry: true
};
}
var oauth_default = {
requiredProps: ["clientId"],
optionalProps: [
"clientSecret",
"redirectUri",
"accessTokenStorage",
"scope",
"subject",
"userId",
"noCache",
"getAccessToken",
"performInteractiveLogin"
],
getRestCallAuthParams: getRestCallAuthParams$4,
getWebSocketAuthParams: getWebSocketAuthParams$4,
getWebResourceAuthParams: getWebResourceAuthParams$1,
handleAuthenticationError: handleAuthenticationError$4
};
//#endregion
//#region src/auth/internal/default-auth-modules/reference.ts
function getRestCallAuthParams$3() {
throw new Error("getRestCallAuthParams should never be called for reference auth module");
}
function getWebSocketAuthParams$3() {
throw new Error("getWebSocketAuthParams should never be called for reference auth module");
}
function handleAuthenticationError$3() {
throw new Error("handleAuthenticationError should never be called for reference auth module");
}
/**
* This auth module is only here to provide a reference for the auth type "reference". It should never be used in practice.
* @private */
var reference_default = {
requiredProps: ["reference"],
optionalProps: [],
getRestCallAuthParams: getRestCallAuthParams$3,
getWebSocketAuthParams: getWebSocketAuthParams$3,
handleAuthenticationError: handleAuthenticationError$3
};
//#endregion
//#region src/auth/internal/default-auth-modules/windows-cookie/xrf-keys.ts
const xrfKeys = {};
/**
* @returns {string} 16 character long xrf-key
*/
function createXrfKey() {
let result$1 = "";
for (let i = 0; i < 16; i += 1) {
const j = Math.floor(Math.random() * 62);
if (j < 10) result$1 += j;
else if (j > 9 && j < 36) result$1 += String.fromCharCode(j + 55);
else result$1 += String.fromCharCode(j + 61);
}
return result$1;
}
function getXrfKey(hostConfig) {
const locationUrl = toValidLocationUrl(hostConfig);
xrfKeys[locationUrl] = xrfKeys[locationUrl] || createXrfKey();
return xrfKeys[locationUrl];
}
//#endregion
//#region src/auth/internal/default-auth-modules/windows-cookie.ts
async function getRestCallAuthParams$2({ hostConfig }) {
return {
headers: { "X-Qlik-XrfKey": getXrfKey(hostConfig) },
queryParams: { xrfkey: getXrfKey(hostConfig) },
credentials: internalGetCredentialsForCookieAuth(hostConfig)
};
}
async function getWebSocketAuthParams$2({ hostConfig }) {
return { queryParams: {
xrfkey: getXrfKey(hostConfig),
"qlik-csrf-token": await getCsrfToken(hostConfig, true)
} };
}
async function handleAuthenticationError$2({ hostConfig }) {
if (hostConfig.loginUri) {
if (hostConfig.authRedirectUserConfirmation) await hostConfig.authRedirectUserConfirmation();
globalThis.location.replace(hostConfig.loginUri.replace("{location}", encodeURIComponent(globalThis.location.href)));
return { preventDefault: true };
}
if (hostConfig.getAccessToken) {
const token = await resolveTokenIfPresent(hostConfig.getAccessToken);
if (token) {
await fetch(`${toValidLocationUrl(hostConfig)}/qlik-embed/main.js`, {
method: "GET",
mode: "cors",
credentials: "include",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
}
});
return { retry: true };
}
}
return {};
}
/** @private */
var windows_cookie_default = {
requiredProps: [],
optionalProps: [
"loginUri",
"crossSiteCookies",
"getAccessToken"
],
getRestCallAuthParams: getRestCallAuthParams$2,
getWebSocketAuthParams: getWebSocketAuthParams$2,
handleAuthenticationError: handleAuthenticationError$2
};
//#endregion
//#region src/auth/internal/default-auth-modules/windows-cookie-node.ts
const nodeCookieStore = /* @__PURE__ */ new Map();
function clearCookieInStore(hostConfig) {
const key = serializeHostConfig$1(hostConfig);
nodeCookieStore.delete(key);
}
function setCookieInStore(hostConfig, cookie) {
const key = serializeHostConfig$1(hostConfig);
nodeCookieStore.set(key, cookie);
}
function getCookieInStore(hostConfig) {
const key = serializeHostConfig$1(hostConfig);
return nodeCookieStore.get(key);
}
async function getCookie(hostConfig) {
let cookie = getCookieInStore(hostConfig);
if (!cookie) {
const token = await resolveTokenIfPresent(hostConfig.getAccessToken);
if (!token) throw new Error("Failed to acquire access token");
cookie = (await fetch(`${toValidLocationUrl(hostConfig)}/qlik-embed/main.js`, {
method: "GET",
mode: "cors",
credentials: "include",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
}
})).headers.get("Set-Cookie") || void 0;
if (cookie) setCookieInStore(hostConfig, cookie);
}
if (!cookie) throw new Error("Failed to exchange access token for cookie");
return cookie;
}
async function getRestCallAuthParams$1({ hostConfig }) {
return {
queryParams: { xrfkey: getXrfKey(hostConfig) },
headers: {
Cookie: await getCookie(hostConfig),
"X-Qlik-XrfKey": getXrfKey(hostConfig)
},
credentials: internalGetCredentialsForCookieAuth(hostConfig)
};
}
async function getWebSocketAuthParams$1({ hostConfig }) {
return {
queryParams: { "qlik-csrf-token": await getCsrfToken(hostConfig, true) },
headers: { Cookie: await getCookie(hostConfig) }
};
}
async function handleAuthenticationError$1({ hostConfig }) {
clearCookieInStore(hostConfig);
return { retry: true };
}
/** @private */
var windows_cookie_node_default = {
requiredProps: [],
optionalProps: [
"loginUri",
"crossSiteCookies",
"getAccessToken"
],
validateHostConfig: (hostConfig) => {
if (typeof hostConfig.getAccessToken === "function") return true;
if (typeof hostConfig.getAccessToken === "string") {
if (!!!globalThis[hostConfig.getAccessToken]) throw new InvalidHostConfigError("The \"getAccessToken\" function name provided does not exist on globalThis.");
return true;
}
throw new InvalidHostConfigError("The \"getAccessToken\" property must be a function or the name of a globally defined function.");
},
getRestCallAuthParams: getRestCallAuthParams$1,
getWebSocketAuthParams: getWebSocketAuthParams$1,
handleAuthenticationError: handleAuthenticationError$1
};
//#endregion
//#region src/auth/internal/auth-module-registry.ts
/**
* Auth module registry
*/
const authModules = {};
/**
* A promise that gets recreated when an auth module is currently being loaded
*/
let ongoingAuthModuleLoading = Promise.resolve();
let authModulesRegistered = false;
(function registerDefaultAuthModules() {
if (!authModulesRegistered) {
registerAuthModule("apikey", apikey_default);
registerAuthModule("cookie", cookie_default);
registerAuthModule("none", none_default);
registerAuthModule("noauth", noauth_default);
registerAuthModule("oauth2", oauth_default);
registerAuthModule("anonymous", anonymous_default);
if (isBrowser()) registerAuthModule("windowscookie", windows_cookie_default);
else registerAuthModule("windowscookie", windows_cookie_node_default);
registerAuthModule("reference", reference_default);
authModulesRegistered = true;
}
})();
/**
* Registers an auth module that can handle authentication. An auth module is used by specifying its name as authType in the HostConfig passed in to api calls.
* @param name the name of the module
* @param authModule the implementation of the AuthModule interface
*/
function registerAuthModule(name, authModule) {
authModules[name.toLowerCase()] = authModule;
}
/**
* Get the names of all registered AuthModules.
* @returns Array of all registered AuthModules
*/
function getRegisteredAuthModules() {
return Object.keys(authModules);
}
/**
* Gets the auth module with the given name. The name is case insensitive.
* @param authType The name of the auth module to get. The name is case insensitive.
* @returns
*/
function getRegisteredAuthModule(authType) {
return authModules[authType.toLowerCase()];
}
/**
* Gets the auth module for a host config. If only clientId is set, then authType is assumed and set to "oauth2".
* If host config is undefined or empty, then authType is assumed and set to "cookie" or "windowscookie" depending
* on which platform the host is running on.
*
* @private
* @param hostConfig
*/
async function getAuthModule(hostConfig) {
const hostConfigToUse = withResolvedHostConfig(hostConfig);
const authType = await determineAuthType(hostConfigToUse);
if (ongoingAuthModuleLoading) await ongoingAuthModuleLoading;
let authModule = getRegisteredAuthModule(authType);
if (!authModule) {
/**
* This is basically poor mans synchronous block so that one globally defined asynchronous auth module factory function
* is not called more than once if several calls to getAuthModule come in in sequence.
*/
ongoingAuthModuleLoading = (async () => {
authModule = await resolveGloballyDefinedAuthModule(authType);
if (authModule) registerAuthModule(authType, authModule);
})();
await ongoingAuthModuleLoading;
}
if (!authModule) throw new InvalidAuthTypeError(authType);
if (authModule.validateHostConfig) authModule.validateHostConfig({
authType,
...hostConfigToUse
});
else internalValidateHostConfig({
authType,
...hostConfigToUse
}, {
requiredProps: authModule.requiredProps || [],
optionalProps: authModule.optionalProps || []
});
return authModule;
}
async function resolveGloballyDefinedAuthModule(authType) {
const globalVariable = globalThis[authType];
if (globalVariable) {
let potentialAuthModule;
if (typeof globalVariable === "function") potentialAuthModule = await globalVariable();
else potentialAuthModule = globalVariable;
if (potentialAuthModule && potentialAuthModule.getRestCallAuthParams && potentialAuthModule.getWebSocketAuthParams && potentialAuthModule.handleAuthenticationError) return potentialAuthModule;
console.error("Not a valid auth module", potentialAuthModule);
throw new InvalidAuthTypeError(authType);
}
return Promise.resolve(void 0);
}
/**
* @private
* Helper function for validating a host config in runtime
* @param hostConfig - The host config to validate
* @param options - Options for validation
* @param options.requiredProps - The required properties in the host config
* @param options.optionalProps - The optional properties in the host config
* @throws {InvalidHostConfigError} If the host config is invalid
* @returns {boolean} Returns true if the host config is valid
* */
function internalValidateHostConfig(hostConfig, { requiredProps, optionalProps }) {
const missingRequiredProps = [];
for (const requiredProp of requiredProps) if (!hostConfig[requiredProp]) missingRequiredProps.push(requiredProp);
if (missingRequiredProps.length > 0) throw new InvalidHostConfigError(`missing required properties in host config; '${missingRequiredProps.join("', '")}'`);
const validProps = [
...hostConfigCommonProperties,
...requiredProps,
...optionalProps
];
const invalidKeys = [];
Object.keys(hostConfig).forEach((key) => {
if (!validProps.includes(key)) invalidKeys.push(key);
});
if (invalidKeys.length > 0) console.warn(`WARNING: unknown properties in host config; '${invalidKeys.join("', '")}'`);
return true;
}
/**
* Determines the authType associated with a HostConfig even if it's
* not explicitly set.
*/
async function determineAuthType(hostConfig) {
if (hostConfig.authType) return hostConfig.authType;
if (hostConfig.apiKey) return "apikey";
if (hostConfig.accessCode) return "anonymous";
if (hostConfig.clientId) return "oauth2";
if (hostConfig.webIntegrationId) return "cookie";
if (hostConfig.reference) return "reference";
if (await isWindows(hostConfig)) return "windowscookie";
return "cookie";
}
//#endregion
//#region src/auth/auth-errors.ts
/** Error is thrown when a host config has incorrect or missing settings */
var InvalidHostConfigError = exports("InvalidHostConfigError", class extends Error {
constructor(message) {
super(`Invalid host config: ${message}`);
this.name = "InvalidHostConfigError";
}
});
/** Error is thrown when a host config has an auth type with incompatible settings */
var UnexpectedAuthTypeError = exports("UnexpectedAuthTypeError", class extends Error {
constructor(...expectedAuthTypes) {
const ors = expectedAuthTypes.map((item, index) => index === 0 ? `"${item}"` : `or "${item}"`).join(" ");
super(`HostConfig is not properly configured. authType is expected to be ${ors}`);
this.name = "UnexpectedAuthTypeError";
}
});
/** Error is thrown when a host config uses an auth type that has not been registered */
var InvalidAuthTypeError = exports("InvalidAuthTypeError", class extends Error {
constructor(authType) {
const validAuthModules = getRegisteredAuthModules();
super(`Not a valid auth type: ${authType}, valid auth types are; '${validAuthModules.filter((name) => name.toLowerCase() !== "qmfeembedframerauthmodule").join("', '")}'`);
this.name = "InvalidAuthTypeError";
}
});
function errorToString({ title, detail, code, status }) {
if (detail) return `${title} - ${detail} (Status: ${status}, Code: ${code})`;
return `${title} (Status: ${status}, Code: ${code})`;
}
var AuthorizationError = exports("AuthorizationError", class extends Error {
errors;
constructor(errors) {
if (typeof errors !== "object") {
super("Unknown error");
return;
}
const errorArray = Array.isArray(errors) ? errors : [errors];
super(errorArray.map(errorToString).join(", "));
this.errors = errorArray;
}
});
//#endregion
//#region src/auth/internal/host-config-functions.ts
/**
* 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(hostConfig) {
const sorted = sortKeys(removeDefaults(withResolvedHostConfig(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(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(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(hostConfig) {
registerHostConfig("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() {
return getRegisteredHostConfig("default") || {};
}
//#endregion
//#region src/auth/auth-functions.ts
/**
* Set initial loggingOut value to false to make sure it has a value before any auth module is loaded
*/
globalThis.loggingOut = false;
let lastErrorMessage = "";
/** Default error logger that simply prints the error unless it is identical to the last one (to prevent bloating of the console) */
function logToConsole({ message }) {
if (message !== lastErrorMessage) {
lastErrorMessage = message;
console.error(message);
}
}
/**
* Determines the authType associated with a HostConfig even if it's
* not explicitly set.
* @param hostConfig the HostConfig containing authentication details
* @returns promise that resolvs with the determined authentication type
*/
function determineAuthType$1(hostConfig) {
return determineAuthType(hostConfig);
}
/**
* Determines if a host is cross origin or not
* @param hostConfig the HostConfig containing authentication details
* @private
*/
function isHostCrossOrigin(hostConfig) {
if (!globalThis.location?.origin) return true;
const hostConfigToUse = withResolvedHostConfig(hostConfig);
if (Object.keys(hostConfigToUse).length === 0) return false;
try {
return new URL(toValidLocationUrl(hostConfigToUse)).origin !== globalThis.location.origin;
} catch {}
return false;
}
/**
* Determines if the host we're connecting to is running on windows or not
* @param hostConfig the HostConfig containing authentication details
*/
async function isWindows(hostConfig) {
const hostConfigToUse = withResolvedHostConfig(hostConfig);
if (typeof hostConfigToUse.forceIsWindows === "boolean") return hostConfigToUse.forceIsWindows;
if (hostConfigToUse.host?.endsWith(".qlik-stage.com") || hostConfigToUse.host?.endsWith(".qlikcloud.com") || hostConfigToUse.host?.endsWith(".qlikcloudgov.com")) return false;
if (hostConfigToUse.authType === "cookie") return false;
if (hostConfigToUse.authType === "windowscookie") return true;
return (await getPlatform({ hostConfig })).isWindows;
}
/**
* Returns a valid location url based on the supplied location string. If location is undefined the default is returned
* @param hostConfig the HostConfig containing authentication details
*/
function toValidLocationUrl(hostConfig) {
const url = withResolvedHostConfig(hostConfig)?.host?.trim();
let locationUrl;
if (!url) locationUrl = "";
else if (url.toLowerCase().startsWith("https://") || url.toLowerCase().startsWith("http://")) locationUrl = url;
else locationUrl = `https://${url}`;
while (locationUrl[locationUrl.length - 1] === "/") locationUrl = locationUrl.substring(0, locationUrl.length - 1);
return locationUrl;
}
/**
* Returns a valid websocket location url based on the supplied location string. If location is undefined the default is returned
* @param hostConfig the HostConfig containing authentication details
*/
function toValidWebsocketLocationUrl(hostConfig) {
const url = withResolvedHostConfig(hostConfig)?.host;
let locationUrl;
if (!url) locationUrl = globalThis.location.origin;
else if (url.toLowerCase().startsWith("https://") || url.toLowerCase().startsWith("http://")) locationUrl = url;
else locationUrl = `https://${url}`;
while (locationUrl[locationUrl.length - 1] === "/") locationUrl = locationUrl.substring(0, locationUrl.length - 1);
return locationUrl.replace(leadingHttp, "ws");
}
/**
* Returns a record of query parameters that needs to be added to websockets
* @param hostConfig the HostConfig containing authentication details
*/
async function getWebSocketAuthParams(props) {
const hostConfigToUse = withResolvedHostConfig(props.hostConfig);
try {
return await (await getAuthModule(hostConfigToUse)).getWebSocketAuthParams({
...props,
hostConfig: hostConfigToUse
});
} catch (err) {
(hostConfigToUse.onAuthFailed || logToConsole)(normalizeAuthModuleError(err));
throw err;
}
}
/**
* Returns a record of query parameters that needs to be added to resources requests, e.g.
* image tags, etc.
* @param hostConfig the HostConfig containing authentication details
*/
async function getWebResourceAuthParams(props) {
const hostConfigToUse = withResolvedHostConfig(props.hostConfig);
try {
return await (await getAuthModule(hostConfigToUse)).getWebResourceAuthParams?.({
...props,
hostConfig: hostConfigToUse
}) || { queryParams: {} };
} catch (err) {
(hostConfigToUse.onAuthFailed || logToConsole)(normalizeAuthModuleError(err));
throw err;
}
}
/**
* Calls and return handleAuthenticationError on the authModule a host config is associated with.
*/
async function handleAuthenticationError(props) {
const hostConfigToUse = withResolvedHostConfig(props.hostConfig);
const result$1 = await (await getAuthModule(hostConfigToUse)).handleAuthenticationError({
...props,
hostConfig: hostConfigToUse
});
const willRetry = props.canRetry && result$1.retry;
const willHangUntilANewPageIsLoaded = result$1.preventDefault;
if (!willRetry && !willHangUntilANewPageIsLoaded) {
const { status, errorBody } = props;
(hostConfigToUse.onAuthFailed || logToConsole)(normalizeInbandAuthError({
status,
errorBody
}));
}
return result$1;
}
/**
* Returns a record of headers and a record of query params that needs to be added to outgoing rest calls
* @param props.hostConfig the HostConfig containing authentication details
* @param props.method the http method, which may affect what authentication are needed.
*/
async function getRestCallAuthParams(props) {
const hostConfigToUse = withResolvedHostConfig(props.hostConfig);
try {
return await (await getAuthModule(hostConfigToUse)).getRestCallAuthParams({
...props,
hostConfig: hostConfigToUse
});
} catch (err) {
(hostConfigToUse.onAuthFailed || logToConsole)(normalizeAuthModuleError(err));
throw err;
}
}
/**
* Returns a record of headers and a record of query params that needs to be added to outgoing rest calls
* @param props.hostConfig the HostConfig containing authentication details
* @param props.method the http method, which may affect what authentication are needed.
*/
async function getAccessToken(props) {
const authorizationHeader = (await getRestCallAuthParams({
method: "GET",
...props
})).headers?.Authorization;
if (authorizationHeader.indexOf("Bearer ") === 0) return authorizationHeader.substring(7);
throw new Error("Unknown format of authorization header returned by remote auth module");
}
/**
* Registers an auth module that can handle authentication. An auth module is used by specifying its name as authType in the HostConfig passed in to api calls.
* @param name the name of the module
* @param authModule the implementation of the AuthModule interface
*/
function registerAuthModule$1(name, authModule) {
registerAuthModule(name, authModule);
}
/**
* Sets the default host config that will be used for all api calls that do not include a HostConfig
* @param hostConfig the default HostConfig to use
*/
function setDefaultHostConfig$1(hostConfig) {
setDefaultHostConfig(hostConfig);
}
/**
* 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) {
registerHostConfig(name, hostConfig);
}
/**
* Unregisters a host config with the given name.
* @param name The name of the host config to unregister.
*/
function unregisterHostConfig$1(name) {
unregisterHostConfig(name);
}
/**
* Serializes the provided hostConfig, if present, otherwise the default one.
*/
function serializeHostConfig$1(hostConfig) {
return serializeHostConfig(hostConfig);
}
/**
* Throws errors if hostConfig is missing or missing properties on cross domain requests
*/
function checkForCrossDomainRequest(hostConfig) {
const hostConfigToUse = withResolvedHostConfig(hostConfig);
if (isHostCrossOrigin(hostConfigToUse)) {
if (Object.keys(hostConfigToUse).length === 0) throw new InvalidHostConfigError("a host config must be provided when making a cross domain request");
if (!hostConfigToUse.host) throw new InvalidHostConfigError("A 'host' property must be set in host config when making a cross domain request");
}
}
/**
* Logs out the user and sets `global.loggingOut` to true.
* **NOTE**: Does not abort pending requests.
*/
const logout = exports("logout", () => {
globalThis.loggingOut = true;
globalThis.location.href = "/logout";
});
const leadingHttp = /^http/;
/**
* Normalizes the error object returned from the auth module to a FatalAuthError
* @param errorBody The error body to normalize
* @param status The status code of the error
* @returns Either the error message or a default message
* @private
*/
function normalizeInbandAuthError({ errorBody, status }) {
const authError = errorBody;
if (typeof authError?.errors === "object") return { message: new AuthorizationError(authError?.errors).message };
return { message: `HTTP ${status}` };
}
/**
* Normalizes the error object returned from the auth module to a FatalAuthError
* @param err The error to normalize
* @returns Either the error message or a default message
* @private
*/
function normalizeAuthModuleError(err) {
return { message: err.message || "Unknown error" };
}
/**
* Returns a resolved host config. If the host config is a reference to a registered host config, it will be resolved
* to the actual host config.
* If the host config is undefined or empty, the default host config will be used.
* If the host config is not a reference, it will be returned as is.
* @private
* @param hostConfig - The host config to resolve
* @returns
*/
function withResolvedHostConfig(hostConfig) {
if (hostConfig?.reference) {
const refConfig = getRegisteredHostConfig(hostConfig.reference);
if (!refConfig) throw new InvalidHostConfigError(`Host config with name "${hostConfig.reference}" not found.`);
return refConfig;
}
if (hostConfig && Object.keys(hostConfig).length > 0) return hostConfig;
return getDefaultHostConfig();
}
/**
* Returns the default host config that is used for all qmfe api calls that do not include a host config
*/
function getDefaultHostConfig$1() {
return getDefaultHostConfig();
}
//#region src/auth/auth.ts
/**
* Provides functionalities for authentication with Qlik backends.
*/
const auth = {
determineAuthType: determineAuthType$1,
getDefaultHostConfig: getDefaultHostConfig$1,
getRestCallAuthParams,
getWebResourceAuthParams,
getWebSocketAuthParams,
handleAuthenticationError,
isHostCrossOrigin,
isWindows,
logout,
registerAuthModule: registerAuthModule$1,
registerHostConfig: registerHostConfig$1,
serializeHostConfig: serializeHostConfig$1,
setDefaultHostConfig: setDefaultHostConfig$1,
toValidLocationUrl,
toValidWebsocketLocationUrl,
unregisterHostConfig: unregisterHostConfig$1
};
var auth_default = exports("default", auth);
var _nodeResolve_empty = /*#__PURE__*/Object.freeze({
__proto__: null
});
})
};
}));
//# sourceMappingURL=auth-sNi1A67--Belh4pQc.js.map