UNPKG

purecloud-platform-client-v2

Version:

A JavaScript library to interface with the PureCloud Platform API

1,481 lines (1,320 loc) 2.72 MB
'use strict'; var axios = require('axios'); var qs = require('qs'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var axios__default = /*#__PURE__*/_interopDefaultLegacy(axios); var qs__default = /*#__PURE__*/_interopDefaultLegacy(qs); var PureCloudRegionHosts = { us_east_1: 'mypurecloud.com', eu_west_1: 'mypurecloud.ie', ap_southeast_2: 'mypurecloud.com.au', ap_northeast_1: 'mypurecloud.jp', eu_central_1: 'mypurecloud.de', us_west_2: 'usw2.pure.cloud', ca_central_1: 'cac1.pure.cloud', ap_northeast_2: 'apne2.pure.cloud', eu_west_2: 'euw2.pure.cloud', ap_south_1: 'aps1.pure.cloud', us_east_2: 'use2.us-gov-pure.cloud', sa_east_1: 'sae1.pure.cloud', me_central_1: 'mec1.pure.cloud', ap_northeast_3: 'apne3.pure.cloud', eu_central_2: 'euc2.pure.cloud', }; class AbstractHttpClient { constructor() { this.timeout = 16000; } setTimeout(timeout) { if (timeout === null || timeout === undefined || typeof timeout !== 'number') { throw new Error("The 'timeout' property must be a number"); } this.timeout = timeout; } setHttpsAgent(httpsAgent) { if (httpsAgent && typeof httpsAgent !== 'object') { throw new Error("The 'httpsAgent' property must be an object"); } this.httpsAgent = httpsAgent; } request(httpRequestOptions) { throw new Error("method must be implemented"); } enableHooks() { throw new Error("method must be implemented"); } /** * Set a PreHook function that modifies the request config before execution. * @param {(config: object) => object | Promise<object> | void} hookFunction */ setPreHook(hookFunction) { if (typeof hookFunction !== "function" || hookFunction.length !== 1) { throw new Error("preHook must be a function that accepts (config)"); } this.preHook = hookFunction; this.enableHooks(); } /** * Set a PostHook function that processes the response or error after execution. * @param {(response: object | null, error: Error | null) => object | Promise<object> | void} hookFunction */ setPostHook(hookFunction) { if (typeof hookFunction !== "function" || hookFunction.length !== 1) { throw new Error("postHook must be a function that accepts (response)"); } this.postHook = hookFunction; this.enableHooks(); } } class HttpRequestOptions { constructor(url, method, headers, params, data, timeout) { this.setUrl(url); this.setMethod(method); if (headers) { this.setHeaders(headers); } if (params) { this.setParams(params); } if (data) { this.setData(data); } if (timeout !== null && timeout !== undefined) this.setTimeout(timeout); else this.timeout = 16000; } // Mandatory fields with validation setUrl(url) { if (!url) throw new Error("The 'url' property is required"); this.url = url; } setMethod(method) { const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD']; if (!method || !validMethods.includes(method.toUpperCase())) { throw new Error("The 'method' property is invalid or missing"); } this.method = method.toUpperCase(); } setData(data) { if (data === undefined || data === null) { throw new Error("The 'data' property is required"); } this.data = data; } // Optional fields setParams(params) { if (params && typeof params !== 'object') { throw new Error("The 'params' property must be an object"); } this.params = params; } // Optional fields setHeaders(headers) { if (headers && typeof headers !== 'object') { throw new Error("The 'headers' property must be an object"); } this.headers = headers; } setTimeout(timeout) { if (timeout === undefined || timeout === null || typeof timeout !== 'number') { throw new Error("The 'timeout' property must be a number"); } this.timeout = timeout; } } // Default client is Axios class DefaultHttpClient extends AbstractHttpClient{ constructor(timeout, httpsAgent) { super(); if (timeout !== null && timeout !== undefined) this.setTimeout(timeout); else this.timeout = 16000; if (httpsAgent !== null && httpsAgent !== undefined) this.setHttpsAgent(httpsAgent); else this.httpsAgent; this._axiosInstance = axios__default["default"].create({}); } enableHooks() { if (this.preHook && typeof this.preHook === 'function') { if (this.requestInterceptorId !== undefined) { axios__default["default"].interceptors.request.eject(this.requestInterceptorId); } this.requestInterceptorId = this._axiosInstance.interceptors.request.use( async (config) => { config = await this.preHook(config); // Call the custom pre-hook return config }, (error) => { // Handle errors before the request is sent console.error('Request Pre-Hook Error:', error.message); return Promise.reject(error); } ); } if (this.postHook && typeof this.postHook === 'function') { // Response interceptor (for post-hooks) if (this.responseInterceptorId !== undefined) { axios__default["default"].interceptors.response.eject(this.responseInterceptorId); } this.responseInterceptorId = this._axiosInstance.interceptors.response.use( async (response) => { response = await this.postHook(response); // Call the custom post-hook return response }, async (error) => { console.error('Post-Hook: Response Error', error.message); // Optionally call post-hook in case of errors return Promise.reject(error); } ); } } request(httpRequestOptions) { if(!(httpRequestOptions instanceof HttpRequestOptions)) { throw new Error(`httpRequestOptions must be instance of HttpRequestOptions `); } const config = this.toAxiosConfig(httpRequestOptions); return this._axiosInstance.request(config); } // Method to generate Axios-compatible config toAxiosConfig(httpRequestOptions) { if (!httpRequestOptions.url || !httpRequestOptions.method) { throw new Error( "Mandatory fields 'url' and 'method' must be set before making a request" ); } var config = { url: httpRequestOptions.url, method: httpRequestOptions.method }; if (httpRequestOptions.params) config.params = httpRequestOptions.params; if (httpRequestOptions.headers) config.headers = httpRequestOptions.headers; if(httpRequestOptions.data) config.data = httpRequestOptions.data; if (this.timeout != null && this.timeout != undefined) config.timeout = this.timeout; if (this.httpsAgent) config.httpsAgent = this.httpsAgent; return config; } } const logLevels = { levels: { none: 0, error: 1, debug: 2, trace: 3, }, }; const logLevelEnum = { level: { LNone: 'none', LError: 'error', LDebug: 'debug', LTrace: 'trace', }, }; const logFormatEnum = { formats: { JSON: 'json', TEXT: 'text', }, }; class Logger { get logLevelEnum() { return logLevelEnum; } get logFormatEnum() { return logFormatEnum; } constructor() { this.log_level = logLevelEnum.level.LNone; this.log_format = logFormatEnum.formats.TEXT; this.log_to_console = true; this.log_file_path; this.log_response_body = false; this.log_request_body = false; this.setLogger(); } setLogger() { if(typeof window === 'undefined') { const winston = require('winston'); this.logger = winston.createLogger({ levels: logLevels.levels, level: this.log_level, }); if (this.log_file_path && this.log_file_path !== '') { if (this.log_format === logFormatEnum.formats.JSON) { this.logger.add(new winston.transports.File({ format: winston.format.json(), filename: this.log_file_path })); } else { this.logger.add( new winston.transports.File({ format: winston.format.combine( winston.format((info) => { info.level = info.level.toUpperCase(); return info; })(), winston.format.simple() ), filename: this.log_file_path, }) ); } } if (this.log_to_console) { if (this.log_format === logFormatEnum.formats.JSON) { this.logger.add(new winston.transports.Console({ format: winston.format.json() })); } else { this.logger.add( new winston.transports.Console({ format: winston.format.combine( winston.format((info) => { info.level = info.level.toUpperCase(); return info; })(), winston.format.simple() ), }) ); } } } } log(level, statusCode, method, url, requestHeaders, responseHeaders, requestBody, responseBody) { var content = this.formatLog(level, statusCode, method, url, requestHeaders, responseHeaders, requestBody, responseBody); if (typeof window !== 'undefined') { var shouldLog = this.calculateLogLevel(level); if (shouldLog > 0 && this.log_to_console === true) { if(this.log_format === this.logFormatEnum.formats.JSON) { console.log(content); } else { console.log(`${level.toUpperCase()}: ${content}`); } } } else { if (this.logger.transports.length > 0) this.logger.log(level, content); } } calculateLogLevel(level) { switch (this.log_level) { case this.logLevelEnum.level.LError: if (level !== this.logLevelEnum.level.LError) { return -1; } return 1; case this.logLevelEnum.level.LDebug: if (level === this.logLevelEnum.level.LTrace) { return -1; } return 1; case this.logLevelEnum.level.LTrace: return 1; default: return -1; } } formatLog(level, statusCode, method, url, requestHeaders, responseHeaders, requestBody, responseBody) { var result; var localRequestHeaders = requestHeaders ? JSON.parse(JSON.stringify(requestHeaders)) : null; var localResponseHeaders = responseHeaders ? JSON.parse(JSON.stringify(responseHeaders)) : null; var localRequestBody = requestBody ? JSON.parse(JSON.stringify(requestBody)) : null; var localResponseBody = responseBody ? JSON.parse(JSON.stringify(responseBody)) : null; if (requestHeaders) localRequestHeaders['Authorization'] = '[REDACTED]'; if (!this.log_request_body) localRequestBody = undefined; if (!this.log_response_body) localResponseBody = undefined; if (this.log_format && this.log_format === logFormatEnum.formats.JSON) { result = { level: level, date: new Date().toISOString(), method: method, url: decodeURIComponent(url), correlationId: localResponseHeaders ? (localResponseHeaders['inin-correlation-id'] ? localResponseHeaders['inin-correlation-id'] : '') : '', statusCode: statusCode, }; if (localRequestHeaders) result.requestHeaders = localRequestHeaders; if (localResponseHeaders) result.responseHeaders = localResponseHeaders; if (localRequestBody) result.requestBody = localRequestBody; if (localResponseBody) result.responseBody = localResponseBody; } else { result = `${new Date().toISOString()} === REQUEST === ${this.formatValue('URL', decodeURIComponent(url))}${this.formatValue('Method', method)}${this.formatValue( 'Headers', this.formatHeaderString(localRequestHeaders) )}${this.formatValue('Body', localRequestBody ? JSON.stringify(localRequestBody, null, 2) : '')} === RESPONSE === ${this.formatValue('Status', statusCode)}${this.formatValue('Headers', this.formatHeaderString(localResponseHeaders))}${this.formatValue( 'CorrelationId', localResponseHeaders ? (localResponseHeaders['inin-correlation-id'] ? localResponseHeaders['inin-correlation-id'] : '') : '' )}${this.formatValue('Body', localResponseBody ? JSON.stringify(localResponseBody, null, 2) : '')}`; } return result; } formatHeaderString(headers) { var headerString = ''; if (!headers) return headerString; for (const [key, value] of Object.entries(headers)) { headerString += `\n\t${key}: ${value}`; } return headerString; } formatValue(key, value) { if (!value || value === '' || value === '{}') return ''; return `${key}: ${value}\n`; } getLogLevel(level) { switch (level) { case 'error': return logLevelEnum.level.LError; case 'debug': return logLevelEnum.level.LDebug; case 'trace': return logLevelEnum.level.LTrace; default: return logLevelEnum.level.LNone; } } getLogFormat(format) { switch (format) { case 'json': return logFormatEnum.formats.JSON; default: return logFormatEnum.formats.TEXT; } } } class Configuration { /** * Singleton getter */ get instance() { return Configuration.instance; } /** * Singleton setter */ set instance(value) { Configuration.instance = value; } constructor() { if (!Configuration.instance) { Configuration.instance = this; } if (typeof window !== 'undefined') { this.configPath = ''; } else { const os = require('os'); const path = require('path'); this.configPath = path.join(os.homedir(), '.genesyscloudjavascript', 'config'); } this.refresh_access_token = true; this.refresh_token_wait_max = 10; this.live_reload_config = true; this.host; this.environment; this.basePath; this.authUrl; this.config; this.gateway = undefined; this.logger = new Logger(); this.setEnvironment(); this.liveLoadConfig(); } liveLoadConfig() { if (typeof window === 'undefined') { // Please don't remove the typeof window === 'undefined' check here! // This safeguards the browser environment from using `fs`, which is only // available in node environment. this.updateConfigFromFile(); if (this.live_reload_config && this.live_reload_config === true) { try { const fs = require('fs'); fs.watchFile(this.configPath, { persistent: false }, (eventType, filename) => { this.updateConfigFromFile(); if (!this.live_reload_config) { fs.unwatchFile(this.configPath); } }); } catch (err) { // do nothing } } return; } // If in browser, don't read config file, use default values this.configPath = ''; } setConfigPath(path) { if (path && path !== this.configPath) { this.configPath = path; this.liveLoadConfig(); } } updateConfigFromFile() { if (typeof window === 'undefined') { // Please don't remove the typeof window === 'undefined' check here! // This safeguards the browser environment from using `fs`, which is only // available in node environment. const ConfigParser = require('configparser'); try { var configparser = new ConfigParser(); configparser.read(this.configPath); // If no error catched, indicates it's INI format this.config = configparser; } catch (error) { if (error.name && error.name === 'MissingSectionHeaderError') { // Not INI format, see if it's JSON format const fs = require('fs'); var configData = fs.readFileSync(this.configPath, 'utf8'); this.config = { _sections: JSON.parse(configData), // To match INI data format }; } } if (this.config) this.updateConfigValues(); } } updateConfigValues() { this.logger.log_level = this.logger.getLogLevel(this.getConfigString('logging', 'log_level')); this.logger.log_format = this.logger.getLogFormat(this.getConfigString('logging', 'log_format')); this.logger.log_to_console = this.getConfigBoolean('logging', 'log_to_console') !== undefined ? this.getConfigBoolean('logging', 'log_to_console') : this.logger.log_to_console; this.logger.log_file_path = this.getConfigString('logging', 'log_file_path') !== undefined ? this.getConfigString('logging', 'log_file_path') : this.logger.log_file_path; this.logger.log_response_body = this.getConfigBoolean('logging', 'log_response_body') !== undefined ? this.getConfigBoolean('logging', 'log_response_body') : this.logger.log_response_body; this.logger.log_request_body = this.getConfigBoolean('logging', 'log_request_body') !== undefined ? this.getConfigBoolean('logging', 'log_request_body') : this.logger.log_request_body; this.refresh_access_token = this.getConfigBoolean('reauthentication', 'refresh_access_token') !== undefined ? this.getConfigBoolean('reauthentication', 'refresh_access_token') : this.refresh_access_token; this.refresh_token_wait_max = this.getConfigInt('reauthentication', 'refresh_token_wait_max') !== undefined ? this.getConfigInt('reauthentication', 'refresh_token_wait_max') : this.refresh_token_wait_max; this.live_reload_config = this.getConfigBoolean('general', 'live_reload_config') !== undefined ? this.getConfigBoolean('general', 'live_reload_config') : this.live_reload_config; this.host = this.getConfigString('general', 'host') !== undefined ? this.getConfigString('general', 'host') : this.host; if (this.getConfigString('gateway', 'host') !== undefined) { let gateway = { host: this.getConfigString('gateway', 'host') }; if (this.getConfigString('gateway', 'protocol') !== undefined) gateway.protocol = this.getConfigString('gateway', 'protocol'); if (this.getConfigInt('gateway', 'port') !== undefined) gateway.port = this.getConfigInt('gateway', 'port'); if (this.getConfigString('gateway', 'path_params_login') !== undefined) gateway.path_params_login = this.getConfigString('gateway', 'path_params_login'); if (this.getConfigString('gateway', 'path_params_api') !== undefined) gateway.path_params_api = this.getConfigString('gateway', 'path_params_api'); if (this.getConfigString('gateway', 'username') !== undefined) gateway.username = this.getConfigString('gateway', 'username'); if (this.getConfigString('gateway', 'password') !== undefined) gateway.password = this.getConfigString('gateway', 'password'); this.setGateway(gateway); } else { this.setGateway(); } this.setEnvironment(); // Update logging configs this.logger.setLogger(); } /** * @description Sets the gateway used by the session * @param {object} gateway - Gateway Configuration interface * @param {string} gateway.host - The address of the gateway. * @param {string} gateway.protocol - (optional) The protocol to use. It will default to "https" if the parameter is not defined or empty. * @param {number} gateway.port - (optional) The port to target. This parameter can be defined if a non default port is used and needs to be specified in the url (value must be greater or equal to 0). * @param {string} gateway.path_params_login - (optional) An arbitrary string to be appended to the gateway url path for Login requests. * @param {string} gateway.path_params_api - (optional) An arbitrary string to be appended to the gateway url path for API requests. * @param {string} gateway.username - (optional) Not used at this stage (for a possible future use). * @param {string} gateway.password - (optional) Not used at this stage (for a possible future use). */ setGateway(gateway) { if (gateway) { this.gateway = { host: '' }; if (gateway.protocol) this.gateway.protocol = gateway.protocol; else this.gateway.protocol = 'https'; if (gateway.host) this.gateway.host = gateway.host; else this.gateway.host = ''; if (gateway.port && gateway.port > -1) this.gateway.port = gateway.port; else this.gateway.port = -1; if (gateway.path_params_login) { this.gateway.path_params_login = gateway.path_params_login; // Strip trailing slash this.gateway.path_params_login = this.gateway.path_params_login.replace(/\/+$/, ''); } else this.gateway.path_params_login = ''; if (gateway.path_params_api) { this.gateway.path_params_api = gateway.path_params_api; // Strip trailing slash this.gateway.path_params_api = this.gateway.path_params_api.replace(/\/+$/, ''); } else this.gateway.path_params_api = ''; if (gateway.username) this.gateway.username = gateway.username; if (gateway.password) this.gateway.password = gateway.password; } else { this.gateway = undefined; } } setEnvironment(env) { // Default value if (env) this.environment = env; else this.environment = this.host ? this.host : 'mypurecloud.com'; // Strip trailing slash this.environment = this.environment.replace(/\/+$/, ''); // Strip protocol and subdomain if (this.environment.startsWith('https://')) this.environment = this.environment.substring(8); if (this.environment.startsWith('http://')) this.environment = this.environment.substring(7); if (this.environment.startsWith('api.')) this.environment = this.environment.substring(4); this.basePath = `https://api.${this.environment}`; this.authUrl = `https://login.${this.environment}`; } getConfUrl(pathType, regionUrl) { if (!this.gateway) return regionUrl; if (!this.gateway.host) return regionUrl; var url = this.gateway.protocol + '://' + this.gateway.host; if (this.gateway.port > -1) url = url + ':' + this.gateway.port.toString(); if (pathType === 'login') { if (this.gateway.path_params_login) { if (this.gateway.path_params_login.startsWith('/')) url = url + this.gateway.path_params_login; else url = url + '/' + this.gateway.path_params_login; } } else { if (this.gateway.path_params_api) { if (this.gateway.path_params_api.startsWith('/')) url = url + this.gateway.path_params_api; else url = url + '/' + this.gateway.path_params_api; } } return url; } getConfigString(section, key) { if (this.config._sections[section]) return this.config._sections[section][key]; } getConfigBoolean(section, key) { if (this.config._sections[section] && this.config._sections[section][key] !== undefined) { if (typeof this.config._sections[section][key] === 'string') { return this.config._sections[section][key] === 'true'; } else return this.config._sections[section][key]; } } getConfigInt(section, key) { if (this.config._sections[section] && this.config._sections[section][key]) { if (typeof this.config._sections[section][key] === 'string') { return parseInt(this.config._sections[section][key]); } else return this.config._sections[section][key]; } } } /** * @module purecloud-platform-client-v2/ApiClient * @version 223.0.0 */ class ApiClient { /** * Singleton getter */ get instance() { return ApiClient.instance; } /** * Singleton setter */ set instance(value) { ApiClient.instance = value; } /** * Manages low level client-server communications, parameter marshalling, etc. There should not be any need for an * application to use this class directly - the *Api and model classes provide the public API for the service. The * contents of this file should be regarded as internal but are documented for completeness. * @alias module:purecloud-platform-client-v2/ApiClient * @class */ constructor() { /** * @description The default API client implementation. * @type {module:purecloud-platform-client-v2/ApiClient} */ if(!ApiClient.instance){ ApiClient.instance = this; } /** * Enumeration of collection format separator strategies. * @enum {String} * @readonly */ this.CollectionFormatEnum = { /** * Comma-separated values. Value: <code>csv</code> * @const */ CSV: ',', /** * Space-separated values. Value: <code>ssv</code> * @const */ SSV: ' ', /** * Tab-separated values. Value: <code>tsv</code> * @const */ TSV: '\t', /** * Pipe(|)-separated values. Value: <code>pipes</code> * @const */ PIPES: '|', /** * Native array. Value: <code>multi</code> * @const */ MULTI: 'multi' }; /** * @description Value is `true` if local storage exists. Otherwise, false. */ try { localStorage.setItem('purecloud_local_storage_test', 'purecloud_local_storage_test'); localStorage.removeItem('purecloud_local_storage_test'); this.hasLocalStorage = true; } catch(e) { this.hasLocalStorage = false; } /** * The authentication methods to be included for all API calls. * @type {Array.<String>} */ this.authentications = { 'Guest Chat JWT': {type: 'apiKey', 'in': 'header', name: 'Authorization'}, 'PureCloud OAuth': {type: 'oauth2'} }; /** * The default HTTP headers to be included for all API calls. * @type {Array.<String>} * @default {} */ this.defaultHeaders = {}; /** * The default HTTP timeout for all API calls. * @type {Number} * @default 16000 */ this.timeout = 16000; this.authData = {}; this.settingsPrefix = 'purecloud'; // Transparently request a new access token when it expires (Code Authorization only) this.refreshInProgress = false; this.httpClient; this.proxyAgent; this.config = new Configuration(); if (typeof(window) !== 'undefined') window.ApiClient = this; } /** * @description If set to `true`, the response object will contain additional information about the HTTP response. When `false` (default) only the body object will be returned. * @param {boolean} returnExtended - `true` to return extended responses */ setReturnExtendedResponses(returnExtended) { this.returnExtended = returnExtended; } /** * @description When `true`, persists the auth token to local storage to avoid a redirect to the login page on each page load. Defaults to `false`. * @param {boolean} doPersist - `true` to persist the auth token to local storage * @param {string} prefix - (Optional, default 'purecloud') The name prefix used for the local storage key */ setPersistSettings(doPersist, prefix) { this.persistSettings = doPersist; this.settingsPrefix = prefix ? prefix.replace(/\W+/g, '_') : 'purecloud'; } /** * @description Saves the auth token to local storage, if enabled. */ _saveSettings(opts) { try { this.authData.accessToken = opts.accessToken; this.authentications['PureCloud OAuth'].accessToken = opts.accessToken; if (opts.state) { this.authData.state = opts.state; } this.authData.error = opts.error; this.authData.error_description = opts.error_description; if (opts.tokenExpiryTime) { this.authData.tokenExpiryTime = opts.tokenExpiryTime; this.authData.tokenExpiryTimeString = opts.tokenExpiryTimeString; } // Don't save settings if we aren't supposed to be persisting them if (this.persistSettings !== true) return; // Ensure we can access local storage if (!this.hasLocalStorage) { return; } // Remove state from data so it's not persisted let tempData = JSON.parse(JSON.stringify(this.authData)); delete tempData.state; // Save updated auth data localStorage.setItem(`${this.settingsPrefix}_auth_data`, JSON.stringify(tempData)); } catch (e) { console.error(e); } } /** * @description Loads settings from local storage, if enabled. */ _loadSettings() { // Don't load settings if we aren't supposed to be persisting them if (this.persistSettings !== true) return; // Ensure we can access local storage if (!this.hasLocalStorage) { return; } // Load current auth data const tempState = this.authData.state; this.authData = localStorage.getItem(`${this.settingsPrefix}_auth_data`); if (!this.authData) this.authData = {}; else this.authData = JSON.parse(this.authData); if (this.authData.accessToken) this.setAccessToken(this.authData.accessToken); this.authData.state = tempState; } /** * @description Sets the environment used by the session * @param {string} environment - (Optional, default "mypurecloud.com") Environment the session use, e.g. mypurecloud.ie, mypurecloud.com.au, etc. */ setEnvironment(environment) { this.config.setEnvironment(environment); } /** * @description Sets the dynamic HttpClient used by the client * @param {object} httpClient - HttpClient to be injected */ setHttpClient(httpClient) { if (!(httpClient instanceof AbstractHttpClient)) { throw new Error("httpclient must be an instance of AbstractHttpClient. See DefaultltHttpClient for a prototype"); } this.httpClient = httpClient; } /** * @description Gets the HttpClient used by the client */ getHttpClient() { if (this.httpClient) { return this.httpClient; } else { this.httpClient = new DefaultHttpClient(this.timeout, this.proxyAgent); return this.httpClient; } } /** * @description Sets the certificate paths if MTLS authentication is needed * @param {string} certPath - path for certs * @param {string} keyPath - path for key * @param {string} caPath - path for public certs */ setMTLSCertificates(certPath, keyPath, caPath) { if (typeof window === 'undefined') { const agentOptions = {}; if (certPath) { agentOptions.cert = require('fs').readFileSync(certPath); } if (keyPath) { agentOptions.key = require('fs').readFileSync(keyPath); } if (caPath) { agentOptions.ca = require('fs').readFileSync(caPath); } agentOptions.rejectUnauthorized = true; this.proxyAgent = new require('https').Agent(agentOptions); const httpClient = this.getHttpClient(); httpClient.setHttpsAgent(this.proxyAgent); } else { throw new Error("MTLS authentication is managed by the Browser itself. MTLS certificates cannot be set via code on Browser."); } } /** * @description Sets preHook functions for the httpClient * @param {string} preHook - method definition for prehook */ setPreHook(preHook) { const httpClient = this.getHttpClient(); httpClient.setPreHook(preHook); } /** * @description Sets postHook functions for the httpClient * @param {string} postHook - method definition for posthook */ setPostHook(postHook) { const httpClient = this.getHttpClient(); httpClient.setPostHook(postHook); } /** * @description Sets the certificate content if MTLS authentication is needed * @param {string} certContent - content for certs * @param {string} keyContent - content for key * @param {string} caContent - content for public certs */ setMTLSContents(certContent, keyContent, caContent) { if (typeof window === 'undefined') { const agentOptions = {}; if (certContent) { agentOptions.cert = certContent; } if (keyContent) { agentOptions.key = keyContent; } if (caContent) { agentOptions.ca = caContent; } agentOptions.rejectUnauthorized = true; this.proxyAgent = new require('https').Agent(agentOptions); const httpClient = this.getHttpClient(); httpClient.setHttpsAgent(this.proxyAgent); } else { throw new Error("MTLS authentication is managed by the Browser itself. MTLS certificates cannot be set via code on Browser."); } } /** * @description Sets the gateway used by the session * @param {object} gateway - Gateway Configuration interface * @param {string} gateway.host - The address of the gateway. * @param {string} gateway.protocol - (optional) The protocol to use. It will default to "https" if the parameter is not defined or empty. * @param {string} gateway.port - (optional) The port to target. This parameter can be defined if a non default port is used and needs to be specified in the url (value must be greater than 0). * @param {string} gateway.path_params_login - (optional) An arbitrary string to be appended to the gateway url path for Login requests. * @param {string} gateway.path_params_api - (optional) An arbitrary string to be appended to the gateway url path for API requests. * @param {string} gateway.username - (optional) Not used at this stage (for a possible future use). * @param {string} gateway.password - (optional) Not used at this stage (for a possible future use). */ setGateway(gateway) { this.config.setGateway(gateway); } /** * @description Initiates the implicit grant login flow. Will attempt to load the token from local storage, if enabled. * @param {string} clientId - The client ID of an OAuth Implicit Grant client * @param {string} redirectUri - The redirect URI of the OAuth Implicit Grant client * @param {object} opts - (optional) Additional options * @param {string} opts.state - (optional) An arbitrary string to be passed back with the login response. Used for client apps to associate login responses with a request. * @param {string} opts.org - (optional) The organization name that would normally used when specifying an organization name when logging in. This is only used when a provider is also specified. * @param {string} opts.provider - (optional) Authentication provider to log in with e.g. okta, adfs, salesforce, onelogin. This is only used when an org is also specified. */ loginImplicitGrant(clientId, redirectUri, opts) { // Check for auth token in hash const hash = this._setValuesFromUrlHash(); this.clientId = clientId; this.redirectUri = redirectUri; if (!opts) opts = {}; return new Promise((resolve, reject) => { // Abort if org and provider are not set together if (opts.org && !opts.provider) { reject(new Error('opts.provider must be set if opts.org is set')); } else if (opts.provider && !opts.org) { reject(new Error('opts.org must be set if opts.provider is set')); } // Abort on auth error if (hash && hash.error) { hash.accessToken = undefined; this._saveSettings(hash); return reject(new Error(`[${hash.error}] ${hash.error_description}`)); } // Test token and proceed with login this._testTokenAccess() .then(() => { if (!this.authData.state && opts.state) this.authData.state = opts.state; resolve(this.authData); }) .catch((error) => { var query = { client_id: encodeURIComponent(this.clientId), redirect_uri: encodeURIComponent(this.redirectUri), response_type: 'token' }; if (opts.state) query.state = encodeURIComponent(opts.state); if (opts.org) query.org = encodeURIComponent(opts.org); if (opts.provider) query.provider = encodeURIComponent(opts.provider); var url = this._buildAuthUrl('oauth/authorize', query); window.location.replace(url); }); }); } /** * @description Initiates the client credentials login flow. Only available in node apps. * @param {string} clientId - The client ID of an OAuth Implicit Grant client * @param {string} clientSecret - The client secret of an OAuth Implicit Grant client */ loginClientCredentialsGrant(clientId, clientSecret) { this.clientId = clientId; var authHeader = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); var loginBasePath = this.config.getConfUrl('login', `https://login.${this.config.environment}`); return new Promise((resolve, reject) => { // Block browsers from using client credentials if (typeof window !== 'undefined') { reject(new Error('The client credentials grant is not supported in a browser.')); return; } const headers = { 'Authorization': `Basic ${authHeader}` }; var requestOptions = new HttpRequestOptions(`${loginBasePath}/oauth/token`, `POST`, headers, null, 'grant_type=client_credentials', this.timeout); const httpClient = this.getHttpClient(); httpClient.request(requestOptions) .then((response) => { // Logging this.config.logger.log( 'trace', response.status, 'POST', `${loginBasePath}/oauth/token`, headers, response.headers, { grant_type: 'client_credentials' }, undefined ); this.config.logger.log( 'debug', response.status, 'POST', `${loginBasePath}/oauth/token`, headers, undefined, { grant_type: 'client_credentials' }, undefined ); // Save access token this.setAccessToken(response.data['access_token']); // Set expiry time this.authData.tokenExpiryTime = (new Date()).getTime() + (response.data['expires_in'] * 1000); this.authData.tokenExpiryTimeString = (new Date(this.authData.tokenExpiryTime)).toUTCString(); // Return auth data resolve(this.authData); }) .catch((error) => { // Log error if (error.response) { this.config.logger.log( 'error', error.response.status, 'POST', `${loginBasePath}/oauth/token`, headers, error.response.headers, { grant_type: 'client_credentials' }, error.response.data ); } reject(error); }); }); } /** * @description Initiates the Saml2Bearerflow. Only available in node apps. * @param {string} clientId - The client ID of an OAuth Implicit Grant client * @param {string} clientSecret - The client secret of an OAuth Implicit Grant client * @param {string} orgName - The orgName of an OAuth Implicit Grant client * @param {string} assertion - The saml2bearer assertion */ loginSaml2BearerGrant(clientId, clientSecret, orgName, assertion) { this.clientId = clientId; var loginBasePath = this.config.getConfUrl('login', `https://login.${this.config.environment}`); return new Promise((resolve, reject) => { if (typeof window !== 'undefined') { reject(new Error('The saml2bearer grant is not supported in a browser.')); return; } var encodedData = Buffer.from(clientId + ':' + clientSecret).toString('base64'); var request = this._formAuthRequest(encodedData, { grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer', orgName: orgName, assertion: assertion }); request.proxy = this.proxy; var bodyParam = { grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer', orgName: orgName, assertion: assertion, }; // Handle response request .then((response) => { // Logging this.config.logger.log( 'trace', response.status, 'POST', `${loginBasePath}/oauth/token`, request.headers, response.headers, bodyParam, undefined ); this.config.logger.log( 'debug', response.status, 'POST', `${loginBasePath}/oauth/token`, request.headers, undefined, bodyParam, undefined ); // Get access token from response var access_token = response.data.access_token; this.setAccessToken(access_token); this.authData.tokenExpiryTime = new Date().getTime() + response.data['expires_in'] * 1000; this.authData.tokenExpiryTimeString = new Date(this.authData.tokenExpiryTime).toUTCString(); // Return auth data resolve(this.authData); }) .catch((error) => { // Log error if (error.response) { this.config.logger.log( 'error', error.response.status, 'POST', `${loginBasePath}/oauth/token`, request.headers, error.response.headers, bodyParam, error.response.data ); } reject(error); }); }); } /** * @description Completes the PKCE Code Authorization. * @param {string} clientId - The client ID of an OAuth Code Authorization Grant client * @param {string} codeVerifier - code verifier used to generate the code challenge * @param {string} authCode - Authorization code * @param {string} redirectUri - Authorized redirect URI for your Code Authorization client */ authorizePKCEGrant(clientId, codeVerifier, authCode, redirectUri) { this.clientId = clientId; var loginBasePath = this.config.getConfUrl('login', `https://login.${this.config.environment}`); return new Promise((resolve, reject) => { var headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; var data = qs__default["default"].stringify({ grant_type: 'authorization_code', code: authCode, code_verifier: codeVerifier, client_id: clientId, redirect_uri: redirectUri }); var requestOptions = new HttpRequestOptions(`${loginBasePath}/oauth/token`, `POST`, headers, null, data, this.timeout); const httpClient = this.getHttpClient(); var bodyParam = { grant_type: 'authorization_code', code: authCode, code_verifier: codeVerifier, client_id: clientId, redirect_uri: redirectUri, }; // Handle response httpClient.request(requestOptions) .then((response) => { // Logging this.config.logger.log( 'trace', response.status, 'POST', `${loginBasePath}/oauth/token`, requestOptions.headers, response.headers, bodyParam, undefined ); this.config.logger.log( 'debug', response.status, 'POST', `${loginBasePath}/oauth/token`, requestOptions.headers, undefined, bodyParam, undefined ); // Get access token from response var access_token = response.data.access_token; this.setAccessToken(access_token); this.authData.tokenExpiryTime = new Date().getTime() + response.data['expires_in'] * 1000; this.authData.tokenExpiryTimeString = new Date(this.authData.tokenExpiryTime).toUTCString(); // Return auth data resolve(this.authData); }) .catch((error) => { // Log error if (error.response) { this.config.logger.log( 'error', error.response.status, 'POST', `${loginBasePath}/oauth/token`, requestOptions.headers, error.response.headers, bodyParam, error.response.data ); } reject(error); }); }); } /** * @description Generate a random string used as PKCE Code Verifier - length = 43 to 128. * @param {number} nChar - code length */ generatePKCECodeVerifier(nChar) { if (nChar < 43 || nChar > 128) { throw new Error(`PKCE Code Verifier (length) must be between 43 and 128 characters`); } // Check for window if (typeof window === 'undefined') { try { const getRandomValues = require('crypto').getRandomValues; const unreservedCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~"; let randomString = Array.from(getRandomValues(new Uint32Array(nChar))) .map((x) => unreservedCharacters[x % unreservedCharacters.length]) .join(''); return randomString; } catch (err) { throw new Error(`Crypto module is missing/not supported.`); } } else { const unreservedCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~"; let randomString = Array.from(crypto.getRandomValues(new Uint32Array(nChar))) .map((x) => unreservedCharacters[x % unreservedCharacters.length]) .join(''); return randomString; } } /** * @description Compute Base64Url PKCE Code Challenge from Code Verifier. * @param {string} code - code verifier used to generate the code challenge */ computePKCECodeChallenge(code) { if (code.length < 43 || code.length > 128) { throw new Error(`PKCE Code Verifier (length) must be between 43 and 128 characters`); } // Check for window if (typeof window === 'undefined') { // nodejs try { const createHash = require('crypto').createHash; const utf8 = new TextEncoder().encode(code); return new Promise((resolve, reject) => { const hashHex = createHash('sha256').update(utf8).digest(); const hashBase64Url = Buffer.from(hashHex).toString('base64url'); resolve(hashBase64Url); }); } catch (err) { throw new Error(`Crypto module is missing/not supported.`); } } else { // browser const utf8 = new TextEncoder().encode(code); return new Promise((resolve, reject) => { window.crypto.subtle.digest("SHA-256", utf8).then((hashBuffer) => { const hashBase64 = Buffer.from(hashBuffer).toString('base64'); let hashBase64Url = hashBase64.replaceAll("+", "-").replaceAll("/", "_"); hashBase64Url = hashBase64Url.split("=")[0]; resolve(hashBase64Url); }) .catch((error) => { // Handle failure return reject(new Error(`Code Challenge Error ${error}`)); }); }); } } /** * @description Initiates the pkce auth code grant login flow. Will attempt to load the token from local storage, if enabled. * @param {string} clientId - The client ID of an OAuth Implicit Grant client * @param {string} redirectUri - The redirect URI of the OAuth Implicit Grant client * @param {object} opts - (optional) Additional options * @param {string} opts.state - (optional) An arbitrary string to be passed back with the login response. Used for client apps to associate login responses with a request. * @param {string} opts.org - (optional) The organization name that would normally used when specifying an organization name when logging in. This is only used when a provider is also specified. * @param {string} opts.provider - (optional) Authentication provider to log in with e.g. okta, adfs, salesforce, onelogin. This is only used when an org is also specified. * @param {string} codeVerifier - (optional) code verifier used to generate the code challenge */ loginPKCEGrant(clientId, redirectUri, opts, codeVerifier) { // Need Local Storage or non null codeVerifier as parameter if (!this.hasLocalStorage && !codeVerifier) { throw new Error(`loginPKCEGrant requires Local Storage or codeVerifier as input parameter`); } // Check for auth code in query const query = this._setValuesFromUrlQuery(); this.clientId = clientId; this.redirectUri = redirectUri; this.codeVerifier = codeVerifier; if (!opts) opts = {}; return new Promise((resolve, reject) => { // Abort if org and provider are not set together if (opts.org && !opts.provider) { reject(new Error('opts.provider must be set if opts.org is set')); } else if (opts.provider && !opts.org) { reject(new Error('opts.org must be set if opts.provider is set')); } // Abort on auth error if (query && query.error) { // remove codeVerifier from session storage if (this.hasLocalStorage) { sessionStorage.removeItem(`genesys_cloud_sdk_pkce_code_verifier`); } // reset access token if any was stored this._saveSettings({ accessToken: undefined }); return reject(new Error(`[${query.error}] ${query.error_description}`)); } // Get token on auth code if (query && query.code) { if (!this.codeVerifier) { // load codeVerifier from session storage if (this.hasLocalStorage) { this.codeVerifier = sessionStorage.getItem(`genesys_cloud_sdk_pkce_code_verifier`); } } this.authorizePKCEGrant(this.clientId, this.codeVerifier, query.code, this.redirectUri) .then(() => { // Do authenticated things this._testTokenAccess() .then(() => { if (!this.authData.state && query.state) this.authData.state = query.state; // remove codeVerifier from session storage if (this.hasLocalStorage) { sessionStorage.removeItem(`genesys_cloud_sdk_pkce_code_verifier`); } resolve(this.authData); }) .catch((error) => { // Handle failure response this._saveSettings({ accessToken: undefined}); // remove codeVerifier from session storage if (this.hasLocalStorage) { sessionStorage.removeItem(`genesys_cloud_sdk_pkce_code_verifier`); } return reject(new Error(`[${error.name}] ${error.msg}`)); }); }) .catch((error) => { // Handle failure response this._saveSettings({ accessToken: undefined}); // remove codeVerifier from session storage if (this.hasLocalStorage) { sessionStorage.removeItem(`genesys_cloud_sdk_pkce_code_verifier`); } return reject(new Error(`[${error.name}] ${error.msg}`)); }); } else { // Test token (if previously stored) and proceed with login this._testTokenAccess() .then(() => { if (!this.authData.state && opts.state) this.authData.state = opts.state; resolve(this.authData); }) .catch((error) => { if (!this.codeVerifier) { this.codeVerifier = this.generatePKCECodeVerifier(128); // save codeVerifier in session storage if (this.hasLocalStorage) { sessionStorage.setItem(`genesys_cloud_sdk_pkce_code_verifier`, this.codeVerifier); } } this.computePKCECodeChalle