UNPKG

cumulocity-cypress

Version:
1,340 lines (1,328 loc) 51.2 kB
'use strict'; var _$1 = require('lodash'); var setCookieParser = require('set-cookie-parser'); var libCookie = require('cookie'); var client = require('@c8y/client'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var ___namespace = /*#__PURE__*/_interopNamespaceDefault(_$1); var setCookieParser__namespace = /*#__PURE__*/_interopNamespaceDefault(setCookieParser); var libCookie__namespace = /*#__PURE__*/_interopNamespaceDefault(libCookie); /** * Gets the case-sensitive path for a given case-insensitive path. The path is * assumed to be a dot-separated string. If the path is an array, it is assumed * to be a list of keys. * * @param obj The object to query * @param path The case-insensitive path to find * @returns The actual case-sensitive path if found, undefined otherwise */ function toSensitiveObjectKeyPath(obj, path) { if (!obj) return undefined; const inputStr = _$1.isArray(path) ? null : path; const keys = _$1.isArray(path) ? path.filter((k) => !_$1.isEmpty(k)) : path.split(/[.[\]]/g).filter((k) => !_$1.isEmpty(k)); let current = obj; const resolved = []; for (const key of keys) { if (current === null || current === undefined) return undefined; if (_$1.isArray(current)) { const index = parseInt(key); if (!isNaN(index)) { if (index >= 0 && index < current.length) { resolved.push(key); current = current[index]; } else { return undefined; // index out of bounds } } else if (current.length > 0 && _$1.isString(current[0])) { const matchedIndex = current.findIndex((item) => _$1.isString(item) && item.toLowerCase() === key.toLowerCase()); if (matchedIndex !== -1) { resolved.push(String(matchedIndex)); current = current[matchedIndex]; } else { return undefined; } } else if (current.length > 0 && _$1.isObjectLike(current[0])) { // For arrays of objects, resolve case through the first element so // the caller gets the correctly-cased key without needing an index. const matchingKey = Object.keys(current[0]).find((k) => k.toLowerCase() === key.toLowerCase()); if (matchingKey !== undefined) { resolved.push(matchingKey); current = current[0][matchingKey]; } else { return undefined; } } else { return undefined; } continue; } if (_$1.isObjectLike(current)) { const matchingKey = Object.keys(current).find((k) => k.toLowerCase() === key.toLowerCase()); if (matchingKey !== undefined) { resolved.push(matchingKey); current = current[matchingKey]; } else { return undefined; } } else { return undefined; } } // Fast path: array input or no brackets in input — plain dot-joined output if (!inputStr || !inputStr.includes("[")) return resolved.join("."); // Mirror bracket vs. dot notation from the input when building the output. // Walk the original string in parallel with the resolved keys: wherever the // input had `[key]` we emit `[resolvedKey]`, otherwise `.resolvedKey`. let result = ""; let pos = 0; for (let i = 0; i < resolved.length; i++) { // skip separators (dot after a `]`, or the `]` itself) while (pos < inputStr.length && (inputStr[pos] === "." || inputStr[pos] === "]")) pos++; const useBracket = inputStr[pos] === "["; if (useBracket) pos++; // skip `[` // skip past the key characters in the input while (pos < inputStr.length && inputStr[pos] !== "." && inputStr[pos] !== "[" && inputStr[pos] !== "]") pos++; if (i === 0) result = resolved[i]; else result += useBracket ? `[${resolved[i]}]` : `.${resolved[i]}`; } return result; } /** * Gets the value of a case-insensitive key path from an object. The path is * assumed to be a dot-separated string. If the path is an array, it is assumed * to be a list of keys. * * This function supports deep access to cookie and set-cookie headers, e.g. * `requestHeaders.cookie.authorization`. Cookie headers are parsed and the value * of the specified cookie is returned. If the cookie is not found, undefined is returned. * * @example * get_i(obj, "obj.key.token") * get_i(obj, ["obj", "key", "token"]) * get_i(obj, "obj.key[0].token") * get_i(obj, "obj.key.0.token") * get_i(obj, "requestHeaders.cookie.authorization") * get_i(obj, "requestHeaders.set-cookie.authorization") * * @param obj The object to query * @param keyPath The case-insensitive key path to find * @returns The value of the key path if found, undefined otherwise */ function get_i(obj, keyPath) { if (obj == null || keyPath == null) return undefined; // Handle case where obj itself is an array of strings with a single key lookup const keys = _$1.isArray(keyPath) ? keyPath.filter((k) => !_$1.isEmpty(k)) : keyPath.split(/[.[\]]/g).filter((k) => !_$1.isEmpty(k)); if (keys.length === 1 && _$1.isArray(obj) && obj.length > 0 && _$1.isString(obj[0])) { const matchedString = obj.find((item) => _$1.isString(item) && item.toLowerCase() === keys[0].toLowerCase()); if (matchedString !== undefined) { return matchedString; } } const sensitivePath = toSensitiveObjectKeyPath(obj, keyPath); let direct = undefined; // Try direct access first if we have a valid path if (sensitivePath != null) { direct = _$1.get(obj, sensitivePath); if (direct !== undefined) return direct; } // Handle cookie and set-cookie deep access, e.g. requestHeaders.cookie.authorization if (!keys || keys.length === 0) return undefined; const indexOfKey = (arr, val) => arr.findIndex((k) => k.toLowerCase() === val.toLowerCase()); const cookieIdx = indexOfKey(keys, "cookie"); const setCookieIdx = indexOfKey(keys, "set-cookie"); // Helper to resolve the real path up to a certain index (inclusive) const resolvePathUpTo = (idx) => { const part = keys.slice(0, idx + 1); return toSensitiveObjectKeyPath(obj, part) ?? part.join("."); }; // requestHeaders.cookie.<name> if (cookieIdx >= 0) { const parentPath = resolvePathUpTo(cookieIdx); const cookieHeader = parentPath ? _$1.get(obj, parentPath) : undefined; const cookieName = keys[cookieIdx + 1]; if (cookieHeader == null) return undefined; if (!cookieName) return cookieHeader; // return full header if no name // Parse Cookie header string into key/value if (_$1.isString(cookieHeader)) { const parsed = libCookie__namespace.parse(cookieHeader); const matchKey = Object.keys(parsed).find((k) => k.toLowerCase() === cookieName.toLowerCase()); return matchKey ? parsed[matchKey] : undefined; } return undefined; } // headers.set-cookie.<name> if (setCookieIdx >= 0) { const parentPath = resolvePathUpTo(setCookieIdx); const setCookieHeader = parentPath ? _$1.get(obj, parentPath) : undefined; const cookieName = keys[setCookieIdx + 1]; if (setCookieHeader == null) return undefined; if (!cookieName) return setCookieHeader; // return full header if no name // Parse Set-Cookie header (array or string) const headerInput = _$1.isString(setCookieHeader) ? setCookieParser__namespace.splitCookiesString(setCookieHeader) : setCookieHeader; const cookies = setCookieParser__namespace.parse(headerInput, { decodeValues: false, }); const found = (cookies || []).find((c) => c?.name?.toLowerCase() === cookieName.toLowerCase()); return found?.value; } // Handle arrays of strings with case-insensitive matching // For paths like "headers.authorization" where headers is ["Content-Type", "Authorization"] for (let i = 0; i < keys.length; i++) { const parentPath = resolvePathUpTo(i); const parentValue = parentPath ? _$1.get(obj, parentPath) : undefined; if (_$1.isArray(parentValue) && parentValue.length > 0 && _$1.isString(parentValue[0])) { const searchKey = keys[i + 1]; if (searchKey) { const index = parseInt(searchKey); if (isNaN(index)) { // Non-numeric key, try to find case-insensitive match in string array const matchedString = parentValue.find((item) => _$1.isString(item) && item.toLowerCase() === searchKey.toLowerCase()); // Only return a match when this segment is the final path segment if (matchedString !== undefined && i + 1 === keys.length - 1) { return matchedString; } } } } } return direct; } /** * Converts a value to an array. If the value is an array, it is returned as is. * @param value The value to convert to an array * @returns The value as an array if it is not already an array */ function to_array(value) { if (value == null) return undefined; if (_$1.isArray(value)) return value; return [value]; } function isURL(obj) { return obj instanceof URL; } function relativeURL(url) { try { const u = isURL(url) ? url : new URL(url); return u.pathname + u.search; } catch { return undefined; } } function urlForBaseUrl(baseUrl, relativeOrAbsoluteUrl) { if (relativeOrAbsoluteUrl) { try { const url = new URL(relativeOrAbsoluteUrl, baseUrl); return url.toString(); } catch { // no-op } } else { return baseUrl; } return relativeOrAbsoluteUrl; } function removeBaseUrlFromString(url, baseUrl) { if (!url || !baseUrl) { return url; } let normalizedBaseUrl = _$1.clone(baseUrl); while (normalizedBaseUrl.endsWith("/")) { normalizedBaseUrl = normalizedBaseUrl.slice(0, -1); } let result = url.replace(normalizedBaseUrl, ""); if (_$1.isEmpty(result)) { result = "/"; } return result; } function removeBaseUrlFromRequestUrl(record, baseUrl) { if (!record?.request?.url || !baseUrl || !_$1.isString(baseUrl)) { return; } record.request.url = removeBaseUrlFromString(record.request.url, baseUrl); } function normalizeUrl(url) { return url.replace(/\/+$/, ""); } function tenantUrl(baseUrl, tenant) { if (!baseUrl || !tenant) return undefined; try { const url = new URL(baseUrl); const hostComponents = url.host.split("."); if (hostComponents.length <= 2) { url.host = `${tenant}.${hostComponents.join(".")}`; } else { const instance = url.host.split(".")?.slice(1)?.join("."); url.host = `${tenant}.${instance}`; } return normalizeUrl(url.toString()); } catch { // no-op } return undefined; } function updateURLs(value, from, to) { if (!value || !from || !to) return value; let result = value; const fromTenantUrl = tenantUrl(from.baseUrl, from.tenant); const toTenantUrl = tenantUrl(to.baseUrl, to.tenant); if (fromTenantUrl && toTenantUrl) { result = result.replace(new RegExp(fromTenantUrl, "g"), toTenantUrl); } if (from.baseUrl && to.baseUrl) { const fromBaseUrl = normalizeUrl(from.baseUrl); const toBaseUrl = normalizeUrl(to.baseUrl); if (fromBaseUrl && toBaseUrl) { result = result.replace(new RegExp(fromBaseUrl, "g"), toBaseUrl); } result = result.replace(new RegExp(from.baseUrl.replace(/https?:\/\//i, ""), "g"), to.baseUrl.replace(/https?:\/\//i, "")); if (fromTenantUrl) { result = result.replace(new RegExp(fromTenantUrl, "g"), toTenantUrl || toBaseUrl); } } return result; } /** * Checks if the given URL is an absolute URL. * @param url The URL to check. * @returns True if the URL is an absolute URL, false otherwise. */ function isAbsoluteURL(url) { if (!url || !_$1.isString(url) || _$1.isEmpty(url)) return false; return /^https?:\/\//i.test(url); } /** * Validates the base URL and throws an error if the base URL is not an absolute URL. This * is required as commands expect an absolute URL as baseUrl. Will not fail for undefined values. * `Cypress.config().baseUrl` is validated by Cypress itself and throw an error. * * @param baseUrl The url to validate. */ function validateBaseUrl(baseUrl) { if (baseUrl != null && !isAbsoluteURL(baseUrl)) { const error = new Error(`Invalid value for base url. '${baseUrl}' must be an absolute URL or undefined.`); error.name = "C8yPactError"; throw error; } } /** * Normalizes a URL to ensure it has a protocol and proper trailing slash. * If no protocol is present, HTTPS is added by default. * If the URL has no path component, a trailing slash is appended. * * @param url - The URL string to normalize * @returns The normalized URL with HTTPS protocol and trailing slash if appropriate, or undefined for invalid input */ function normalizeBaseUrl(url) { if (!url || !_$1.isString(url)) { return undefined; } const trimmedUrl = url.trim(); if (!trimmedUrl) { return undefined; } let normalizedUrl; // Check if URL already has a protocol if (/^https?:\/\//i.test(trimmedUrl)) { normalizedUrl = trimmedUrl; } else { // Add https:// if no protocol is present normalizedUrl = `https://${trimmedUrl}`; } try { const urlObj = new URL(normalizedUrl); // remove all components other than protocol, host normalizedUrl = `${urlObj.protocol}//${urlObj.host}`; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to normalize base url ${url}. ${errorMessage}`); } return normalizedUrl; } /** * Converts the given URL to a string. * @param url The URL or RequestInfo to convert. * @returns The URL as a string. */ function toUrlString(url) { if (_$1.isString(url)) { return url; } else if (url instanceof URL) { return url.toString(); } else if (url instanceof Request) { return url.url; } else { throw new Error(`Type for URL not supported. Expected URL, string or Request, but found $'{typeof url}}'.`); } } /// <reference types="cypress" /> const C8yPactAuthObjectKeys = [ "userAlias", "user", "type", ]; /** * Checks if the given object is a C8yAuthOptions. * * @param obj The object to check. * @param options Options to check for additional properties. * @returns True if the object is a C8yAuthOptions, false otherwise. */ function isAuthOptions(obj) { return (_$1.isObjectLike(obj) && (("user" in obj && "password" in obj) || "token" in obj)); } // new function to convert C8yAuthOptions to IAuthentication function toC8yAuthentication(obj) { if (!obj || !_$1.isObjectLike(obj)) { return undefined; } if (_$1.get(obj, "getFetchOptions")) { return obj; } if (!isAuthOptions(obj)) { return undefined; } if (obj.token) { return new client.BearerAuth(obj.token); } else if (obj.user && obj.password) { return new client.BasicAuth({ user: obj.user, password: obj.password, tenant: obj.tenant, }); } return undefined; } // map from case insensitive auth type to C8yAuthOptionType function getAuthType(auth) { const type = _$1.isString(auth) ? auth.toLowerCase() : auth?.type?.toLowerCase(); if (type === "bearerauth") { return "BearerAuth"; } if (type === "basicauth") { return "BasicAuth"; } if (type === "cookieauth") { return "CookieAuth"; } return undefined; } function hasAuthentication(client) { if (!client) return false; const fetchClient = _$1.get(client, "_client.core") ?? _$1.get(client, "core") ?? client; const getFetchOptionsFn = _$1.get(fetchClient, "getFetchOptions"); if (_$1.isFunction(getFetchOptionsFn)) { const options = getFetchOptionsFn.apply(fetchClient); if (!options) return false; if (get_i(options, "headers.X-XSRF-TOKEN")) return true; if (get_i(options, "headers.authorization")) return true; } if (_$1.get(fetchClient, "_auth")) return true; return false; } function toPactAuthObject(obj) { return _$1.pick(obj, C8yPactAuthObjectKeys); } function isPactAuthObject(obj) { return (_$1.isObjectLike(obj) && ("user" in obj || "token" in obj) && ("userAlias" in obj || "type" in obj || "token" in obj) && Object.keys(obj).every((key) => ["token", ...C8yPactAuthObjectKeys].includes(key))); } function normalizeAuthHeaders(headers) { // required to fix inconsistencies between c8yclient and interceptions // using lowercase and uppercase. fix here. const xsrfTokenHeader = Object.keys(headers || {}).find((key) => key.toLowerCase() === "x-xsrf-token"); const authorizationHeader = Object.keys(headers || {}).find((key) => key.toLowerCase() === "authorization"); if (xsrfTokenHeader && xsrfTokenHeader !== "X-XSRF-TOKEN") { headers["X-XSRF-TOKEN"] = headers[xsrfTokenHeader]; delete headers[xsrfTokenHeader]; } if (authorizationHeader && authorizationHeader !== "Authorization") { headers["Authorization"] = headers[authorizationHeader]; delete headers[authorizationHeader]; } return headers; } function getAuthOptionsFromEnv(env) { if (env == null || !_$1.isObjectLike(env)) { return undefined; } // check first environment variables const jwtToken = env["C8Y_TOKEN"]; let tokenAuth = undefined; try { const authFromToken = getAuthOptionsFromJWT(jwtToken); if (authFromToken) { tokenAuth = authWithTenant(env, authFromToken); } } catch { // ignore errors from extractTokensFromJWT // this is expected if the token is not a valid JWT } const user = env[`C8Y_USERNAME`] ?? env[`C8Y_USER`]; const password = env[`C8Y_PASSWORD`]; let basicAuth = undefined; if (!_$1.isEmpty(user) && !_$1.isEmpty(password)) { basicAuth = authWithTenant(env, { user, password, }); } if (!tokenAuth && !basicAuth) { return undefined; } return { ...(basicAuth ?? {}), ...(tokenAuth ?? {}) }; } function authWithTenant(env, options) { if (env == null || !_$1.isObjectLike(env)) { return options; } const tenant = env[`C8Y_TENANT`]; if (tenant && !options?.tenant) { _$1.extend(options, { tenant }); } return options; } function getAuthOptionsFromBasicAuthHeader(authHeader) { if (!authHeader || !_$1.isString(authHeader) || !authHeader.startsWith("Basic ")) { return undefined; } const base64Credentials = authHeader.slice("Basic ".length); const credentials = decodeBase64(base64Credentials); const components = credentials.split(":"); if (!components || components.length < 2) { return undefined; } return { user: components[0], password: components.slice(1).join(":") }; } /** * Extracts the authentication options from a JWT token. * @param jwtToken The JWT token to extract the authentication options from. * @returns The extracted authentication options. */ function getAuthOptionsFromJWT(jwtToken) { try { const payload = JSON.parse(atob(jwtToken.split(".")[1])); // Remove all characters not valid in JWT tokens (base64url: A-Z, a-z, 0-9, -, _, .) const cleanedToken = jwtToken?.replace(/[^A-Za-z0-9\-_.]/g, ""); return { token: cleanedToken, xsrfToken: payload.xsrfToken, tenant: payload.ten, user: payload.sub, baseUrl: normalizeBaseUrl(payload.aud ?? payload.iss), }; } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`Failed to decode JWT token: ${message}`); } } /** * Extracts the tenant from the basic auth object. * @param auth The basic auth object containing the user property. * @returns The tenant or undefined if not found. */ function tenantFromBasicAuth(auth) { if (_$1.isString(auth)) { auth = { user: auth }; } if (!auth || !_$1.isObjectLike(auth) || !auth.user) return undefined; const components = auth.user.split("/"); if (!components || components.length < 2 || _$1.isEmpty(components[1]) || _$1.isEmpty(components[0])) return undefined; return components[0]; } function encodeBase64(str) { if (!str) return ""; let encoded; if (typeof Buffer !== "undefined") { encoded = Buffer.from(str).toString("base64"); } else { encoded = btoa(str); } return encoded; } function decodeBase64(base64) { if (!base64) return ""; let decoded; if (typeof Buffer !== "undefined") { decoded = Buffer.from(base64, "base64").toString("utf-8"); } else { decoded = atob(base64); } return decoded; } /// <reference types="cypress" /> // workaround for lodash import in Cypress nodejs typescript runtime and browser const _ = _$1 || ___namespace; /** * Checks if the given object is a C8yPactRecord. * * @param obj The object to check. * @returns True if the object is a C8yPactRecord, false otherwise. */ function isPactRecord(obj) { return (_.isObjectLike(obj) && "request" in obj && _.isObjectLike(_.get(obj, "request")) && "response" in obj && _.isObjectLike(_.get(obj, "response")) && _.isFunction(_.get(obj, "toCypressResponse"))); } async function wrapFetchResponse(response, options = {}) { // only wrap valid responses or new Response() will fail later if (response.status == null || response.status < 200 || response.status > 599) { return response; } const responseObj = await (async () => { return toCypressResponse(response, options.duration, options.fetchOptions, options.url); })(); if (!responseObj) return response; let rawBody = undefined; if (response.data) { responseObj.body = response.data; rawBody = _$1.isObject(responseObj.body) ? JSON.stringify(responseObj.body) : responseObj.body; } else if (response.body) { try { rawBody = await response.text(); const json = JSON.parse(rawBody); responseObj.body = _$1.isObjectLike(json) ? json : rawBody; } catch { responseObj.body = rawBody; } } // empty body ("") is not allowed, make sure to use undefined instead if (_$1.isEmpty(rawBody)) { rawBody = undefined; } const fetchOptions = options?.fetchOptions ?? {}; const logOptions = options?.logOptions; try { responseObj.requestBody = fetchOptions && _$1.isString(fetchOptions?.body) ? JSON.parse(fetchOptions.body) : fetchOptions?.body; } catch { responseObj.requestBody = fetchOptions?.body; } // res.ok = response.ok, responseObj.method = fetchOptions?.method || response.method || "GET"; updateConsoleProps(responseObj, fetchOptions, logOptions, options.url, options.duration); // create a new window.Response for Client. this is required as the body // stream can not be read more than once. as we just read it, recreate the response // and resolve json() and text() promises using the values we read from the stream. const result = new Response(rawBody, { status: response.status, statusText: response.statusText, headers: response.headers, }); // pass the responseObj as part of the window.Response object. this way we can access // in the Clients response and do not need to reprocess result.responseObj = responseObj; result.requestBody = responseObj.requestBody; result.method = responseObj.method; result.data = responseObj.body; // result.json = () => Promise.resolve(responseObj.body); // result.text = () => Promise.resolve(rawBody || ""); return result; } function updateConsoleProps(responseObj, fetchOptions, logOptions, url, duration) { const authInfo = {}; const authorizationHeader = get_i(responseObj, "requestHeaders.authorization"); if (authorizationHeader) { const auth = getAuthOptionsFromBasicAuthHeader(authorizationHeader); if (auth?.user && auth?.password) { authInfo["Basicauth"] = `${authorizationHeader} (${auth.user})`; } else { if (authorizationHeader.startsWith("Bearer ")) { try { const jwt = authorizationHeader.replace("Bearer ", ""); const authOptions = getAuthOptionsFromJWT(jwt); authInfo["BearerAuth"] = authOptions; } catch { // ignore errors parsing JWT } } } } else { let token = get_i(responseObj, "requestHeaders.cookie.authorization"); if (!token) { token = get_i(responseObj, "requestHeaders.X-XSRF-TOKEN"); } // props["Options"] = options; if (token) { const loggedInUser = logOptions?.loggedInUser || ""; authInfo["CookieAuth"] = `${token} (${loggedInUser})`; } } // Call onRequestEnd callback if available if (logOptions?.onRequestEnd && logOptions?.contextId) { logOptions.onRequestEnd({ contextId: logOptions.contextId, url: toUrlString(url || responseObj.url || ""), method: fetchOptions?.method || responseObj.method || "GET", status: responseObj.status || 0, headers: responseObj.headers || {}, body: responseObj.body, duration: duration || responseObj.duration || 0, success: responseObj.isOkStatusCode || false, fetchOptions, options: logOptions.options, yielded: responseObj, additionalInfo: authInfo, }); } } /** * Converts the given object to a Cypress.Response. * @param obj The object to convert. * @param duration The duration of the request. * @param fetchOptions The fetch options used for the request. * @param url The URL of the request. * @param schema The schema of the response. */ function toCypressResponse(obj, duration = 0, fetchOptions = {}, url, schema) { if (!obj) return undefined; if (typeof isPactRecord === "function" && isPactRecord(obj)) { return obj.toCypressResponse(); } let fetchResponse; if (isIResult(obj)) { fetchResponse = obj.res; } else if (isWindowFetchResponse(obj)) { fetchResponse = obj; } else { fetchResponse = obj; } if ("responseObj" in fetchResponse) { return _$1.get(fetchResponse, "responseObj"); } return { status: fetchResponse.status, isOkStatusCode: fetchResponse.ok || (fetchResponse.status > 199 && fetchResponse.status < 300), statusText: fetchResponse.statusText, headers: Object.fromEntries(fetchResponse.headers || []), requestHeaders: fetchOptions.headers, duration: duration, ...(url && { url: toUrlString(url) }), allRequestResponses: [], body: fetchResponse.data, requestBody: fetchResponse.requestBody, method: fetchResponse.method || "GET", ...(schema), }; } /** * Checks if the given object is a window.Response. * @param obj The object to check. */ function isWindowFetchResponse(obj) { return (obj != null && _$1.isObjectLike(obj) && "status" in obj && "statusText" in obj && "headers" in obj && "body" in obj && "url" in obj && _$1.isFunction(_$1.get(obj, "json")) && _$1.isFunction(_$1.get(obj, "arrayBuffer"))); } /** * Checks if the given object is an IResult. * @param obj The object to check. */ function isIResult(obj) { return (obj != null && _$1.isObjectLike(obj) && "data" in obj && "res" in obj && isWindowFetchResponse(obj.res)); } const maxPageSize = 2000; /** * Validates that a delete operation was successful. * * Accepts status codes: * - 204 (No Content) - Successfully deleted * - 404 (Not Found) - Resource already deleted or never existed * * @param status - HTTP status code to validate * @throws Error if status is not 204 or 404 * * @internal */ function expectSuccessfulDelete(status) { if (status !== 204 && status !== 404) { throw new Error(`Expected status 204 or 404, but got ${status}`); } } /** * Creates a user with the specified global roles and optionally assigns applications. * * This function: * 1. Creates the user in Cumulocity * 2. Assigns the user to the specified global role groups * 3. Optionally assigns applications to the user (by name or IApplication object) * * @param client - The Cumulocity client instance * @param user - The user object to create (must include userName, email, etc.) * @param globalRoles - Array of global role names to assign to the user * @param applications - Optional array of application names (strings) or IApplication objects to assign * @returns Promise resolving to the created user result * * @throws Error if user creation fails or if roles/applications cannot be assigned * * @example * const userResult = await createUser( * client, * { userName: 'john.doe', email: 'john@example.com', password: 'SecurePass123!' }, * ['business'], * ['cockpit', 'devicemanagement'] * ); */ async function createUser(client, user, globalRoles, applications) { const userResponse = await client.user.create(user); for (const role of globalRoles) { const groupResponse = await wrapFetchResponse(await client.core.fetch("/user/" + client.core.tenant + "/groupByName/" + role)); const childId = userResponse?.data?.self; const groupId = groupResponse?.data?.id; if (!childId || !groupId) { throw `Failed to add user ${childId} to group ${childId}.`; } await client.userGroup.addUserToGroup(groupId, childId); } const userId = userResponse.data.id; // Handle applications if provided if (applications && applications.length > 0) { const allApps = []; for (const app of applications) { if (typeof app === "string") { // Fetch application by name const applicationResponse = await wrapFetchResponse(await client.core.fetch(`/application/applicationsByName/${app}`, { headers: { accept: "application/vnd.com.nsn.cumulocity.applicationcollection+json", }, })); const applicationsData = applicationResponse.data?.applications || applicationResponse.data; if (!applicationsData || !Array.isArray(applicationsData)) { throw new Error(`Application ${app} not found. No or empty response.`); } const apps = applicationsData .map((a) => { if (typeof a === "string") { return { type: "HOSTED", id: a }; } else if (typeof a === "object" && a.id) { return { id: a.id, type: a.type || "HOSTED" }; } return undefined; }) .filter((a) => a !== undefined); allApps.push(...apps); } else if (typeof app === "object" && app.id) { allApps.push({ id: app.id, type: app.type || "HOSTED", }); } else { throw new Error("Invalid application format. Expected string (name) or IApplication object with id."); } } // Get user details and merge applications if (userId && allApps.length > 0) { const userDetailResponse = await client.user.detail(userId); const existingApps = userDetailResponse.data?.applications || []; // Merge with existing applications, avoiding duplicates by id const mergedApps = [...existingApps]; for (const app of allApps) { if (!mergedApps.find((existing) => existing.id === app.id)) { mergedApps.push(app); } } await client.user.update({ id: userId, applications: mergedApps }); } } return userResponse; } function isIdentifiedObject(user) { return (typeof user === "object" && (user.id != null || user.userName != null || user.displayName != null || user.self != null || user.email != null)); } function needsAllUsersFetch(users) { const userArray = to_array(users) ?? []; return (userArray.filter((u) => typeof u === "string" || typeof u === "function" || typeof u === "string" || (typeof u === "object" && u.userName == null && u.id == null)).length > 0); } /** * Deletes one or more users from Cumulocity. * * Supports multiple input formats: * - Single username string * - Single IUser object (matched by id, userName, displayName, self, or email) * - Array of usernames or IUser objects * - Filter function to select users to delete * * When an IUser object is provided, the function matches it against existing users using * any available identifying properties (id, userName, displayName, self, email). This allows * for flexible matching even with partial user objects. * * @param client - The Cumulocity client instance * @param user - Username(s), IUser object(s), or filter function to identify users to delete * @param options - Optional configuration * @param options.ignoreNotFound - If true (default), ignores 404 errors when user is not found * @returns Promise that resolves when all users are deleted * * @throws Error if user is missing required properties or if deletion fails (unless ignoreNotFound is true) * * @example * // Delete single user by username * await deleteUser(client, 'john.doe'); * * @example * // Delete multiple users * await deleteUser(client, ['user1', 'user2', 'user3']); * * @example * // Delete users matching a filter * await deleteUser(client, (user) => user.email?.includes('@example.com')); * * @example * // Delete user by partial IUser object * await deleteUser(client, { displayName: 'John Doe', email: 'john@example.com' }); */ async function deleteUser(client, user, options) { if (!user) { throw new Error("Missing user argument. deleteUser() requires IUser object or username string."); } const userArray = to_array(user) ?? []; const ignoreNotFound = options?.ignoreNotFound ?? true; let allUsersResponse = undefined; if (needsAllUsersFetch(user)) { try { allUsersResponse = await client.user.list({ pageSize: maxPageSize, }); } catch (error) { throw new Error(`Failed to fetch list of users for list of usernames or filter function: ${error}`); } } let allUsers = undefined; if (typeof user === "function") { const fn = user; allUsers = allUsersResponse?.data.filter((userItem) => fn(userItem)); } else { allUsers = userArray.reduce((acc, u) => { if (typeof u === "string") { const lowerU = u.toLowerCase(); const foundUser = allUsersResponse?.data.find((userItem) => userItem.userName?.toLowerCase() === lowerU || userItem.id?.toLowerCase() === lowerU) ?? false; if (!foundUser) { if (ignoreNotFound) { return acc; } throw new Error(`User with username '${u}' not found.`); } acc.push(foundUser); } else if (typeof u === "object") { if (!isIdentifiedObject(u)) { throw new Error("IUser object must have at least one identifying property (id, userName, displayName, self, or email)."); } if (u.id != null || u.userName != null) { acc.push(u); return acc; } // If u is IUser object, match using fields available in u // Properties used for matching: id, userName, displayName, self, email const foundUser = allUsersResponse?.data.find((userItem) => { if (u.displayName && userItem.displayName !== u.displayName) return false; if (u.self && userItem.self !== u.self) return false; if (u.email && userItem.email?.toLowerCase() !== u.email.toLowerCase()) return false; return true; }) ?? false; if (!foundUser) { if (ignoreNotFound) { return acc; } const identifier = u.userName || u.email || u.displayName || u.id || "unknown"; throw new Error(`User with identifier '${identifier}' not found.`); } acc.push(foundUser); } return acc; }, []); } for (const user of allUsers ?? []) { try { const response = await client.user.delete(user.id ?? user.userName); expectSuccessfulDelete(response.res?.status || 204); } catch (error) { if (error?.res?.status && error?.res?.status !== 404) throw error; } } } /** * Assigns one or more global roles to a user. * * This function adds the user to the specified global role groups, granting them * the permissions associated with those roles. * * @param client - The Cumulocity client instance * @param username - Username string or IUser object (must have userName property) * @param roles - Array of global role names to assign to the user * @returns Promise that resolves when all roles are assigned * * @throws Error if username is missing, roles array is empty, or if role assignment fails * * @example * await assignUserRoles(client, 'john.doe', ['business', 'admins']); * * @example * const user = await client.user.detail('john.doe'); * await assignUserRoles(client, user.data, ['devicemanagement']); */ async function assignUserRoles(client, username, roles) { const userIdentifier = typeof username === "object" && username.userName ? username.userName : username; if (!userIdentifier || (typeof username === "object" && !username.userName)) { throw new Error("Missing argument. Requiring IUser object with userName or username argument."); } if (!roles || roles.length === 0) { throw new Error("Missing argument. Requiring a string array with roles."); } const userResponse = await client.user.detail(userIdentifier); const childId = userResponse.data?.self; if (!childId) { throw new Error(`Failed to assign roles to user ${userIdentifier}. User data null or does not contain self linking.`); } for (const role of roles) { const groupResponse = await client.core.fetch(`/user/${client.core.tenant}/groupByName/${role}`); const groupId = groupResponse.data?.id; if (!childId || !groupId) { throw new Error(`Failed to add user ${childId} to group ${groupId}.`); } await client.userGroup.addUserToGroup(groupId, childId); } } /** * Removes all global roles currently assigned to a user. * * This function removes the user from all global role groups, effectively * revoking all role-based permissions. * * @param client - The Cumulocity client instance * @param username - Username string or IUser object (must have userName property) * @returns Promise that resolves when all roles are removed * * @throws Error if username is missing or if role removal fails * * @example * await clearUserRoles(client, 'john.doe'); * * @example * const user = await client.user.detail('john.doe'); * await clearUserRoles(client, user.data); */ async function clearUserRoles(client, username) { const userIdentifier = typeof username === "object" && username.userName ? username.userName : username; if (!userIdentifier || (typeof username === "object" && !username.userName)) { throw new Error("Missing argument. Requiring IUser object with userName or username argument."); } const response = await client.user.detail(userIdentifier); const assignedRoles = response.data.groups?.references; if (!assignedRoles || assignedRoles.length === 0) { return; } for (const assignedRole of assignedRoles) { await client.userGroup.removeUserFromGroup(assignedRole.group.id, userIdentifier); } } /** * Generates a secure random password with mixed case letters, numbers, and special characters. * * The password includes: * - Uppercase and lowercase letters (50% chance for each letter) * - Numbers (from timestamp) * - Special characters (!@#$%^&*()) * * @param length - The desired length of the password (default: 28, minimum: 8) * @returns A randomly generated password string * * @example * const password = generatePassword(); * // Returns something like: "2Kl9j8Gh!4m2@x7n#5p3q8r9" * * @example * const shortPassword = generatePassword(12); * // Returns a 12-character password */ function generatePassword(length = 28) { const minLength = 8; const targetLength = Math.max(length, minLength); const timestamp = Date.now().toString(36); const random1 = Math.random().toString(36).substring(2); const random2 = Math.random().toString(36).substring(2); // Build password ensuring minimum length before randomization const base = `${timestamp}-${random1}-${random2}`.substring(0, targetLength); const specialChars = ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"]; return randomizePassword(base, specialChars); } /** * Converts letters to uppercase by 50% chance and replaces hyphens with special characters. * Ensures at least one uppercase character is included. * * @param text - The base text to randomize * @param replaceOptions - Array of special characters to use for replacing hyphens * @returns The randomized password string * * @internal */ function randomizePassword(text, replaceOptions) { let randomizedString = ""; // make sure that at least one char is uppercase by using isFirst flag let isFirst = true; for (let i = 0; i < text.length; i++) { const character = text.charAt(i); if (/^[a-zA-Z]$/.test(character)) { const transformedCharacter = isFirst || Math.random() < 0.5 ? character.toUpperCase() : character.toLowerCase(); randomizedString += transformedCharacter; isFirst = false; continue; } if (character === "-") { const index = Math.floor(Math.random() * replaceOptions.length); randomizedString += replaceOptions[index]; continue; } randomizedString += character; } return randomizedString; } /** * Creates a global role (user group) with the specified permissions. * * Global roles are user groups that define a set of permissions. This function: * 1. Creates a new user group with the specified name * 2. Assigns the specified role permissions to the group * * @param client - The Cumulocity client instance * @param roleOptions - Role name as string, or object with name and optional description * @param permissions - Array of permission (role) IDs or names to assign to this global role (e.g., ['ROLE_USER_MANAGEMENT', 'ROLE_INVENTORY_READ']) * @returns Promise resolving to the created user group result * * @throws Error if role creation fails or if any of the specified roles cannot be found * * @example * const roleResult = await createGlobalRole( * client, * { name: 'Custom Admin', description: 'Custom admin role with specific permissions' }, * ['ROLE_USER_MANAGEMENT', 'ROLE_INVENTORY_ADMIN'] * ); */ async function createGlobalRole(client, roleOptions, permissions) { const roleConfig = typeof roleOptions === "string" ? { name: roleOptions } : roleOptions; if (!roleConfig.name || roleConfig.name.trim() === "") { throw new Error("Missing argument. Requiring a name for the global role."); } // Create the user group const createResponse = await client.userGroup.create(roleConfig); const userGroup = createResponse.data; const userGroupId = userGroup.id; if (!userGroupId) { throw new Error("Failed to create global role. UserGroup id is missing."); } // Get all available roles const listResponse = await client.userRole.list({ pageSize: maxPageSize, withTotalPages: false, }); const listRoles = listResponse.data || []; if (!listRoles || listRoles.length === 0) { throw new Error("Failed to load roles. No roles found."); } // Find matching roles const matches = listRoles.filter((r) => permissions?.find((item) => item === r.id || item === r.name) != null); if (matches.length < permissions.length) { throw new Error(`Failed to assign one of provided userRoles to ${roleConfig.name}. User role not found.`); } // Assign roles to the group for (const match of matches) { if (!match.self) continue; await client.userGroup.addRoleToGroup(userGroupId, match.self); } return createResponse; } /** * Deletes one or more global roles (user groups) by name. * * @param client - The Cumulocity client instance * @param roleNames - Single role name or array of role names to delete * @param options - Optional configuration * @param options.ignoreNotFound - If true (default), ignores 404 errors when role is not found * @returns Promise that resolves when all roles are deleted * * @throws Error if role names are missing or if deletion fails (unless ignoreNotFound is true) * * @example * // Delete single role * await deleteGlobalRoles(client, 'CustomRole'); * * @example * // Delete multiple roles * await deleteGlobalRoles(client, ['Role1', 'Role2', 'Role3']); */ async function deleteGlobalRoles(client, roleNames, options) { const roleNamesArray = to_array(roleNames) ?? []; if (!roleNamesArray || roleNamesArray.length === 0) { throw new Error("Missing argument. Requiring an array of role names."); } const ignoreNotFound = options?.ignoreNotFound ?? true; const listResponse = await client.userGroup.list({ pageSize: maxPageSize }); const groups = listResponse.data || []; if (!ignoreNotFound && (!groups || groups.length === 0)) { throw new Error("Failed to load userGroups. No groups found."); } for (const group of groups) { if (group.name && roleNamesArray.some((name) => name.toLowerCase() === group.name?.toLowerCase()) && group.id) { try { const response = await client.userGroup.delete(group.id); expectSuccessfulDelete(response.res?.status || 204); } catch (error) { if (error?.res?.status !== 404) { throw error; } } } } } exports.C8yPactAuthObjectKeys = C8yPactAuthObjectKeys; exports.assignUserRoles = assignUserRoles; exports.authWithTenant = authWithTenant; exports.clearUserRoles = clearUserRoles; exports.createGlobalRole = createGlobalRole; exports.createUser = createUser; exports.decodeBase64 = decodeBase64; exports.deleteGlobal