From aaa70a303c84fc8070fd1d18efab86444b0054b3 Mon Sep 17 00:00:00 2001 From: bfeigin Date: Fri, 25 Apr 2025 12:47:22 -0700 Subject: [PATCH 1/2] package.json version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 413898c..8c25ea2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "smartsheet", - "version": "4.2.3", + "version": "5.0.0", "description": "Smartsheet JavaScript client SDK", "main": "dist/index.js", "typings": "dist/index.d.ts", From 99a5aeb7ab080daaf8b588953e95fa8609da8ef8 Mon Sep 17 00:00:00 2001 From: smar-ben-feigin <152536304+smar-ben-feigin@users.noreply.github.com> Date: Fri, 30 May 2025 12:33:08 -0700 Subject: [PATCH 2/2] Build new version of HTTP client and logging (#111) * no tests, but the client builds and logs * tests and refactors * ignore .env file * add additional test to cover edgecase * guard against undefined log objects * changelog * skip js tests for now * convert search * type import annotation --- .gitignore | 1 + CHANGELOG.md | 6 + eslint.config.mjs | 4 + index.ts | 132 +-- lib/client/buildClientConfiguration.ts | 77 ++ lib/client/createApiClient.ts | 15 + lib/client/httpClient/buildHttpClient.ts | 89 ++ .../httpClient/logging/buildCallbacks.ts | 38 + .../logging/buildInternalRequestLogger.ts | 108 ++ lib/client/httpClient/logging/logSanitizer.ts | 53 + lib/client/types/ServerResponses.ts | 26 + lib/client/types/clientConfiguration.ts | 63 ++ .../types/smartsheetClient.ts} | 4 +- lib/events/index.ts | 29 +- lib/events/types.ts | 8 +- lib/search/index.ts | 39 +- lib/search/types.ts | 16 +- lib/sheets/create.js | 2 +- lib/types/ApiResourceProvider.ts | 4 + lib/types/ApiUrls.ts | 65 +- lib/types/CreateClient.ts | 4 - lib/types/CreateClientOptions.ts | 13 - lib/types/CreateOptions.ts | 10 - lib/types/RequestCallback.ts | 3 - lib/types/index.ts | 5 - lib/utils/apis.ts | 22 - ...{httpRequestor.js => httpRequestor.js.old} | 0 ...{requestLogger.js => requestLogger.js.old} | 0 ...ponseHandler.js => responseHandler.js.old} | 0 package-lock.json | 931 ++++++++++++++---- package.json | 15 +- test/client/buildClientConfiguration.test.ts | 353 +++++++ test/client/buildHttpClient.test.ts | 205 ++++ test/client/logging/buildCallbacks.test.ts | 123 +++ .../buildInternalRequestLogger.test.ts | 197 ++++ test/client/logging/logSanitizer.test.ts | 235 +++++ test/client/types/ServerResponses.test.ts | 50 + test/client/types/clientConfiguration.test.ts | 65 ++ tsconfig.test.json | 14 + 39 files changed, 2557 insertions(+), 467 deletions(-) create mode 100644 lib/client/buildClientConfiguration.ts create mode 100644 lib/client/createApiClient.ts create mode 100644 lib/client/httpClient/buildHttpClient.ts create mode 100644 lib/client/httpClient/logging/buildCallbacks.ts create mode 100644 lib/client/httpClient/logging/buildInternalRequestLogger.ts create mode 100644 lib/client/httpClient/logging/logSanitizer.ts create mode 100644 lib/client/types/ServerResponses.ts create mode 100644 lib/client/types/clientConfiguration.ts rename lib/{types/SmartsheetClient.ts => client/types/smartsheetClient.ts} (76%) create mode 100644 lib/types/ApiResourceProvider.ts delete mode 100644 lib/types/CreateClient.ts delete mode 100644 lib/types/CreateClientOptions.ts delete mode 100644 lib/types/CreateOptions.ts delete mode 100644 lib/types/RequestCallback.ts delete mode 100644 lib/utils/apis.ts rename lib/utils/{httpRequestor.js => httpRequestor.js.old} (100%) rename lib/utils/{requestLogger.js => requestLogger.js.old} (100%) rename lib/utils/{responseHandler.js => responseHandler.js.old} (100%) create mode 100644 test/client/buildClientConfiguration.test.ts create mode 100644 test/client/buildHttpClient.test.ts create mode 100644 test/client/logging/buildCallbacks.test.ts create mode 100644 test/client/logging/buildInternalRequestLogger.test.ts create mode 100644 test/client/logging/logSanitizer.test.ts create mode 100644 test/client/types/ServerResponses.test.ts create mode 100644 test/client/types/clientConfiguration.test.ts create mode 100644 tsconfig.test.json diff --git a/.gitignore b/.gitignore index 2098105..790de6e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ npm-debug.log .nyc_output .vscode/ dist/ +.env diff --git a/CHANGELOG.md b/CHANGELOG.md index 519978c..0bb3df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [5.0.0-pre1] - 2025-05-23 +### Added +- New WIP TS based client +### Removed +- Existing JS client + ## [4.2.3] - 2025-05-5 ### Added - Fix issue with PUT and POST methods where the body wasn't passed on retries. diff --git a/eslint.config.mjs b/eslint.config.mjs index 630330a..0159742 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -29,7 +29,11 @@ export default tseslint.config( '@typescript-eslint/no-unused-vars': [ 'error', { + args: 'all', argsIgnorePattern: '^_', + caughtErrors: 'all', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', varsIgnorePattern: '^_', }, ], diff --git a/index.ts b/index.ts index 399f4b7..e9c87e5 100644 --- a/index.ts +++ b/index.ts @@ -1,130 +1,4 @@ -import type { CreateClient, CreateOptions } from './lib/types'; -import { apiUrls } from './lib/utils/apis'; -import { createEvents } from './lib/events'; -import { createSearch } from './lib/search'; - -const _ = require('underscore'); -const winston = require('winston'); - -// Possible TODO: Namespace parameters for different subcomponents -// E.g. clientOptions.requestor.instance OR -// clientOptions.requestor.settings -// w/ sub-paths maxRetryDurationSeconds and calcRetryBackoff - -function buildRequestor(clientOptions) { - if (clientOptions.requestor) return clientOptions.requestor; - - const requestorConfig = _.pick(clientOptions, 'maxRetryDurationSeconds', 'calcRetryBackoff'); - - if (requestorConfig.maxRetryDurationSeconds) - requestorConfig.maxRetryDurationMillis = requestorConfig.maxRetryDurationSeconds * 1000; - - requestorConfig.logger = buildLogger(clientOptions); - - return require('./lib/utils/httpRequestor.js').create(requestorConfig); -} - -function buildLogger(clientOptions) { - if (hasMultipleLogOptions(clientOptions)) { - throw new Error( - 'Smartsheet client options may specify at most one of ' + "'logger', 'loggerContainer', and 'logLevel'." - ); - } - - if (clientOptions.logger) return clientOptions.logger; - - if (clientOptions.logLevel) return buildLoggerFromLevel(clientOptions.logLevel); - - if (clientOptions.loggerContainer) return buildLoggerFromContainer(clientOptions.loggerContainer); - - return null; -} - -function hasMultipleLogOptions(clientOptions) { - return ( - (clientOptions.logger && clientOptions.loggerContainer) || - (clientOptions.logger && clientOptions.logLevel) || - (clientOptions.loggerContainer && clientOptions.logLevel) - ); -} - -function buildLoggerFromLevel(logLevel) { - if (winston.levels[logLevel] == null) { - throw new Error( - 'Smartsheet client received configuration with invalid log level ' + - `'${logLevel}'. Use one of the standard Winston log levels.` - ); - } - - return new winston.Logger({ - transports: [ - new winston.transports.Console({ - level: logLevel, - showLevel: false, - label: 'Smartsheet', - }), - ], - }); -} - -function buildLoggerFromContainer(container) { - if (container.has('smartsheet')) return container.get('smartsheet'); - else - throw new Error( - 'Smartsheet client received a logger container, but could not find a logger named ' + "'smartsheet' inside." - ); -} - -export const createClient: CreateClient = function (clientOptions) { - const requestor = buildRequestor(clientOptions); - - const options: CreateOptions = { - apiUrls: apiUrls, - requestor: requestor, - clientOptions: { - accessToken: clientOptions.accessToken || process.env.SMARTSHEET_ACCESS_TOKEN, - userAgent: clientOptions.userAgent, - baseUrl: clientOptions.baseUrl, - }, - }; - - return { - constants: require('./lib/utils/constants.js'), - contacts: require('./lib/contacts/').create(options), - events: createEvents(options), - favorites: require('./lib/favorites/').create(options), - folders: require('./lib/folders/').create(options), - groups: require('./lib/groups/').create(options), - /** - * @deprecated - * The home module is deprecated. The endpoints powering this module - * are being shut off as part of the sheets folder deprecation. - * The endpoints will be available until June. - * - * See this changelog entry for more information - * https://developers.smartsheet.com/api/smartsheet/changelog#2025-03-25 - */ - home: require('./lib/home/').create(options), - images: require('./lib/images/').create(options), - reports: require('./lib/reports/').create(options), - request: require('./lib/request/').create(options), - search: createSearch(options), - server: require('./lib/server/').create(options), - sheets: require('./lib/sheets/').create(options), - sights: require('./lib/sights/').create(options), - templates: require('./lib/templates/').create(options), - tokens: require('./lib/tokens/').create(options), - users: require('./lib/users/').create(options), - webhooks: require('./lib/webhooks/').create(options), - workspaces: require('./lib/workspaces/').create(options), - }; -}; - -export const smartSheetURIs = { - defaultBaseURI: 'https://api.smartsheet.com/2.0/', - govBaseURI: 'https://api.smartsheetgov.com/2.0/', - euBaseURI: 'https://api.smartsheet.eu/2.0/', -}; - -export { CreateClient, CreateClientOptions, SmartsheetClient } from './lib/types'; +export { createApiClient } from './lib/client/createApiClient'; +export * from './lib/client/types/clientConfiguration'; +export * from './lib/client/types/smartsheetClient'; export * from './lib/events/types'; diff --git a/lib/client/buildClientConfiguration.ts b/lib/client/buildClientConfiguration.ts new file mode 100644 index 0000000..f63397f --- /dev/null +++ b/lib/client/buildClientConfiguration.ts @@ -0,0 +1,77 @@ +import winston, { format } from 'winston'; +import { + type ApiHost, + type CreateClientOptions, + DEFAULT_LOG_LEVEL, + DEFAULT_RETRY_CONFIG, + type FullClientConfig, + type LoggingConfig, + type RetryConfig, + SUPPORTED_LOG_LEVELS, + type SmartsheetClientConfig, + isSupportedLogLevel, +} from './types/clientConfiguration'; + +// Derive a full set of client configuration from user provided input, environment variables, and defaults +export const buildFullCreateOptions = (options?: CreateClientOptions): FullClientConfig => { + const smartsheetClientConfig = buildSmarClientConfig(options?.smartsheetClientConfig); + const retryConfig = buildRetryConfig(options?.retryConfig); + const loggingConfig = buildLoggingConfig(options?.loggingConfig); + + return { + smartsheetClientConfig, + retryConfig, + loggingConfig, + axiosConfig: options?.axiosConfig, + }; +}; + +const buildLoggingConfig = (loggingConfig?: LoggingConfig): Required => { + // Fetch or build new logging instance + const loggerInstance = + loggingConfig?.loggerInstance ?? + winston.createLogger({ + level: loggingConfig?.logLevel || DEFAULT_LOG_LEVEL, + format: format.combine(format.timestamp(), format.json()), + transports: [new winston.transports.Console()], + }); + + // extract the log level and validate it is supported + const logLevel = loggerInstance.level; + + if (!isSupportedLogLevel(logLevel)) { + return throwRequiredConfigMissingError( + 'loggerInstance.logLevel', + `Log level '${logLevel}' is not supported, please set logging level to one of [${SUPPORTED_LOG_LEVELS.join(', ')}]` + ); + } + + return { + loggerInstance, + logLevel: logLevel, + }; +}; + +const buildRetryConfig = (retryConfig?: RetryConfig): Required => { + return { + maxRetries: retryConfig?.maxRetries || DEFAULT_RETRY_CONFIG.maxRetries, + }; +}; + +const buildSmarClientConfig = (smarConfig?: SmartsheetClientConfig): Required => { + const apiHost = smarConfig?.apiHost || (process.env.SMARTSHEET_API_HOST as ApiHost) || ApiHost.DEFAULT; + const accessToken = smarConfig?.accessToken || process.env.SMARTSHEET_ACCESS_TOKEN; + + if (!accessToken) { + return throwRequiredConfigMissingError( + 'accessToken', + 'Please provide a value within smartsheetClientConfig or set SMARTSHEET_ACCESS_TOKEN env variable' + ); + } + + return { apiHost, accessToken }; +}; + +const throwRequiredConfigMissingError = (field: string, message: string) => { + throw new Error(`Required config missing ${field}. ` + message); +}; diff --git a/lib/client/createApiClient.ts b/lib/client/createApiClient.ts new file mode 100644 index 0000000..835f940 --- /dev/null +++ b/lib/client/createApiClient.ts @@ -0,0 +1,15 @@ +import { createEvents } from '../events'; +import { buildFullCreateOptions } from './buildClientConfiguration'; +import { buildHttpClient } from './httpClient/buildHttpClient'; +import type { CreateClientOptions } from './types/clientConfiguration'; +import type { SmartsheetClient } from './types/smartsheetClient'; + +// TODO un-mark Return value as partial once all endpoints are available +export const createApiClient = (options: CreateClientOptions): Partial => { + const fullConfiguration = buildFullCreateOptions(options); + const httpClient = buildHttpClient(fullConfiguration); + + return { + events: createEvents(httpClient, fullConfiguration.loggingConfig.loggerInstance), + }; +}; diff --git a/lib/client/httpClient/buildHttpClient.ts b/lib/client/httpClient/buildHttpClient.ts new file mode 100644 index 0000000..91841ae --- /dev/null +++ b/lib/client/httpClient/buildHttpClient.ts @@ -0,0 +1,89 @@ +import { create, type AxiosError, type AxiosInstance } from 'axios'; +import type { FullClientConfig } from '../types/clientConfiguration'; +import axiosRetry from 'axios-retry'; +import { type SmartsheetErrorResponseData, errorCodes } from '../types/ServerResponses'; +import { version } from '../../../package.json'; +import { createRequestInterceptor, createResponseInterceptors, createRetryLogger } from './logging/buildCallbacks'; +import { createInternalRequestLogger, type RequestLogger } from './logging/buildInternalRequestLogger'; + +export const buildHttpClient = (fullConfiguration: FullClientConfig): AxiosInstance => { + const axiosClient = create({ + baseURL: fullConfiguration.smartsheetClientConfig.apiHost, + ...(fullConfiguration.axiosConfig || {}), + headers: buildHeaders(fullConfiguration), + }); + + const loggingCallbacks = getInternalLogCallbacks( + createInternalRequestLogger(fullConfiguration.loggingConfig.loggerInstance) + ); + + configureLoggingInterceptors(axiosClient, loggingCallbacks); + configureRetry(axiosClient, fullConfiguration, loggingCallbacks); + + return axiosClient; +}; + +const buildHeaders = (fullConfiguration: FullClientConfig) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(fullConfiguration.axiosConfig?.headers || {}), + // non-overridable values for userAgent and authorization + 'User-Agent': `smartsheet-javascript-sdk/${version}`, + Authorization: `Bearer ${fullConfiguration.smartsheetClientConfig.accessToken}`, + }; +}; + +const configureRetry = ( + axiosClient: AxiosInstance, + fullConfiguration: FullClientConfig, + logCallbacks: LoggingCallbacks +) => { + axiosRetry(axiosClient, { + retries: fullConfiguration.retryConfig.maxRetries, + retryDelay: axiosRetry.exponentialDelay, + retryCondition: shouldRetry, + onRetry: logCallbacks.retry.onRetry, + onMaxRetryTimesExceeded: logCallbacks.retry.onMaxRetries, + }); +}; + +const configureLoggingInterceptors = (axiosClient: AxiosInstance, loggingCallbacks: LoggingCallbacks) => { + axiosClient.interceptors.request.use(loggingCallbacks.requestInterceptors); + axiosClient.interceptors.response.use( + loggingCallbacks.responseInterceptors.onFulfilled, + loggingCallbacks.responseInterceptors.onRejected + ); +}; + +type LoggingCallbacks = ReturnType; +const getInternalLogCallbacks = (internalLogWrapper: RequestLogger) => { + return { + requestInterceptors: createRequestInterceptor(internalLogWrapper), + responseInterceptors: createResponseInterceptors(internalLogWrapper), + retry: createRetryLogger(internalLogWrapper), + }; +}; + +/** + * Determines if a request should be retried based on the error + * @param error - The error that occurred + * @returns True if the request should be retried, false otherwise + */ +export const shouldRetry = (error: AxiosError): boolean => { + // If we have a response with an error code, check if it's retryable + const responseData = error.response?.data; + + if (responseData?.errorCode) { + const errorCode = responseData.errorCode; + return ( + errorCode === errorCodes.RATE_LIMIT || + errorCode === errorCodes.GATEWAY_TIMEOUT || + errorCode === errorCodes.INTERNAL_SERVER_ERROR || + errorCode === errorCodes.SERVICE_UNAVAILABLE + ); + } + + // Default to axios-retry's default retry condition + return axiosRetry.isNetworkOrIdempotentRequestError(error); +}; diff --git a/lib/client/httpClient/logging/buildCallbacks.ts b/lib/client/httpClient/logging/buildCallbacks.ts new file mode 100644 index 0000000..9ff3bb1 --- /dev/null +++ b/lib/client/httpClient/logging/buildCallbacks.ts @@ -0,0 +1,38 @@ +import type { AxiosRequestConfig, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios'; +import type { RequestLogger } from './buildInternalRequestLogger'; + +// Create request interceptor +export const createRequestInterceptor = (logger: RequestLogger) => { + return (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => { + logger.logRequest(config); + return config; + }; +}; + +// Create response interceptor +export const createResponseInterceptors = (logger: RequestLogger) => { + return { + onFulfilled: (response: AxiosResponse): AxiosResponse => { + logger.logSuccessfulResponse(response); + return response; + }, + onRejected: (error: AxiosError): Promise => { + if (error.config) { + logger.logErrorResponse(error.config, error); + } + return Promise.reject(error); + }, + }; +}; + +// Create retry logger +export const createRetryLogger = (logger: RequestLogger) => { + return { + onRetry: (retryCount: number, error: AxiosError, requestConfig: AxiosRequestConfig) => { + logger.logRetryAttempt(requestConfig, error, retryCount); + }, + onMaxRetries: (_error: unknown, retryCount: number) => { + logger.logRetryFailure(retryCount); + }, + }; +}; diff --git a/lib/client/httpClient/logging/buildInternalRequestLogger.ts b/lib/client/httpClient/logging/buildInternalRequestLogger.ts new file mode 100644 index 0000000..c9f31c1 --- /dev/null +++ b/lib/client/httpClient/logging/buildInternalRequestLogger.ts @@ -0,0 +1,108 @@ +import type { AxiosError, AxiosHeaderValue, AxiosRequestConfig, AxiosResponse } from 'axios'; +import type { Logger } from 'winston'; +import type { SmartsheetErrorResponseData } from '../../types/ServerResponses'; +import type { SupportedLogLevel } from '../../types/clientConfiguration'; +import { getSanitizedUrlForLogs, withRedactedPayload, withRedactedHeaders } from './logSanitizer'; + +// Define RequestLogger interface +export interface RequestLogger { + logRequest: (requestConfig: AxiosRequestConfig) => void; + logRetryAttempt: (requestConfig: AxiosRequestConfig, error: AxiosError, attemptNum: number) => void; + logRetryFailure: (attemptNum: number) => void; + logSuccessfulResponse: (response: AxiosResponse) => void; + logErrorResponse: (requestConfig: AxiosRequestConfig, error: AxiosError, attemptNum?: number) => void; +} + +const getHttpVerb = (requestConfig: AxiosRequestConfig): string => { + return requestConfig.method || 'UNKNOWN'; +}; + +export const createInternalRequestLogger = (logger: Logger): RequestLogger => { + const PAYLOAD_PREVIEW_LENGTH = 1024; + + const logRequestBasics = (level: SupportedLogLevel, requestConfig: AxiosRequestConfig): void => { + const httpAction = getHttpVerb(requestConfig); + const url = getSanitizedUrlForLogs(requestConfig); + + logger.log(level, { httpAction, url }); + }; + + const logRequest = (requestConfig: AxiosRequestConfig): void => { + logRequestBasics('info', requestConfig); + logHeaders('Request', requestConfig.headers); + logPreviewAndFullPayload('Request', withRedactedPayload(requestConfig.data)); + }; + + const logRetryAttempt = (requestConfig: AxiosRequestConfig, error: AxiosError, attemptNum: number): void => { + logger.warn('Request failed, performing retry', { attemptNum, error }); + logRequestBasics('warn', requestConfig); + }; + + const logRetryFailure = (attemptNum: number): void => { + logger.error('Request failed after %d retries', attemptNum); + }; + + const logSuccessfulResponse = (response: AxiosResponse): void => { + logger.info('Response: Success', { status: response.status }); + logHeaders('Response', response.headers); + logResponsePayload(response); + }; + + const logErrorResponse = (requestConfig: AxiosRequestConfig, error: AxiosError): void => { + logRequestBasics('error', requestConfig); + + const statusCode = error.response?.status || 'unknown'; + + // Extract error information with proper type checking + const errorData = error.response?.data as SmartsheetErrorResponseData; + const errorCode = errorData?.errorCode || 'unknown'; + const message = errorData?.message || error.message || 'Unknown error'; + const refId = errorData?.refId || 'unknown'; + + logger.error('Response: Failure', { statusCode, errorCode, message, refId }); + + if (error.response) { + logHeaders('Response', error.response.headers); + } + }; + + const logHeaders = (context: string, headers: Record): void => { + if (!headers || Object.keys(headers).length === 0) return; + + logger.silly({ context, headers: withRedactedHeaders(headers) }); + }; + + const logResponsePayload = (response: AxiosResponse): void => { + const payload = response.data; + if (payload === undefined || payload === null) return; + + const censoredPayload = withRedactedPayload(payload); + + logPreviewAndFullPayload('Response', censoredPayload); + }; + + const logPreviewAndFullPayload = (context: string, censoredPayload: Record): void => { + let preview: string; + try { + preview = JSON.stringify(censoredPayload); + } catch (_error: unknown) { + preview = String(censoredPayload); + } + + if (preview.length > PAYLOAD_PREVIEW_LENGTH) { + preview = preview.substring(0, PAYLOAD_PREVIEW_LENGTH) + '...'; + } + + logger.verbose(`${context} Payload (preview)`, { preview }); + logger.debug(`${context} Payload (full)`, { censoredPayload }); + }; + + // Generated object + return { + logRequest, + logRetryAttempt, + logRetryFailure, + logSuccessfulResponse, + logErrorResponse, + }; +}; diff --git a/lib/client/httpClient/logging/logSanitizer.ts b/lib/client/httpClient/logging/logSanitizer.ts new file mode 100644 index 0000000..bc52b0c --- /dev/null +++ b/lib/client/httpClient/logging/logSanitizer.ts @@ -0,0 +1,53 @@ +import type { AxiosRequestConfig } from 'axios'; + +// Censoring logs +const EXPOSED_CENSOR_CHARS = 4; + +// Builds a censorFn with a filterList of keys to censor. +// Calling censorFn(logObject) will return a new object with any field matching the keys in filterList redacted +const buildCensorFn = + (filterList: string[]) => + (loggedObject?: Record): Record => + Object.entries(loggedObject || {}).reduce>((redactedLogObject, [key, value]) => { + const keyLower = key.toLowerCase(); + if (filterList.includes(keyLower) && typeof value === 'string') { + redactedLogObject[key] = rectactString(value); + } else { + redactedLogObject[key] = value; + } + return redactedLogObject; + }, {}); + +// Formatting Utilities +const rectactString = (s: string): string => { + if (!s || s.length === 0) return s; + + const censoredSection = '*'.repeat(Math.max(s.length - EXPOSED_CENSOR_CHARS, 0)); + const exposedSection = s.slice(-EXPOSED_CENSOR_CHARS); + return censoredSection + exposedSection; +}; + +// lists of keys to redact when logging specific request objects. +const headerFilterList = ['authorization']; +const payloadFilterList = ['access_token', 'refresh_token']; +const queryParamFilterList = ['code', 'client_id', 'hash', 'refresh_token']; + +export const withRedactedHeaders = buildCensorFn(headerFilterList); +export const withRedactedPayload = buildCensorFn(payloadFilterList); +export const withRedactedQueryParams = buildCensorFn(queryParamFilterList); + +export const getSanitizedUrlForLogs = (requestConfig?: AxiosRequestConfig): string => { + const url = requestConfig?.url || ''; + const params = requestConfig?.params || {}; + + if (!params || Object.keys(params).length === 0) { + return url; + } + + const censoredParams = withRedactedQueryParams(params); + const queryString = Object.entries(censoredParams) + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`) + .join('&'); + + return url + '?' + queryString; +}; diff --git a/lib/client/types/ServerResponses.ts b/lib/client/types/ServerResponses.ts new file mode 100644 index 0000000..0754fdf --- /dev/null +++ b/lib/client/types/ServerResponses.ts @@ -0,0 +1,26 @@ +export const errorCodes = { + RATE_LIMIT: 4001, + GATEWAY_TIMEOUT: 4002, + INTERNAL_SERVER_ERROR: 4003, + SERVICE_UNAVAILABLE: 4004, +} as const; + +//export interface SmartsheetResponse { +// statusCode: number; +// headers: Record; +// body: unknown; +// content?: unknown; +// errorCode?: number | string; +// message?: string; +// refId?: string; +// detail?: unknown; +//} + +// Define the expected structure of Smartsheet API response data +export interface SmartsheetErrorResponseData { + [key: string]: unknown; + errorCode?: number; + message?: string; + refId?: string; + detail?: unknown; +} diff --git a/lib/client/types/clientConfiguration.ts b/lib/client/types/clientConfiguration.ts new file mode 100644 index 0000000..0a29ca5 --- /dev/null +++ b/lib/client/types/clientConfiguration.ts @@ -0,0 +1,63 @@ +import type { AxiosRequestConfig } from 'axios'; +import type { Logger } from 'winston'; + +export enum ApiHost { + DEFAULT = 'https://api.smartsheet.com/2.0/', + GOV = 'https://api.smartsheetgov.com/2.0/', + EU = 'https://api.smartsheet.eu/2.0/', +} + +export const isSupportedLogLevel = (level: string): level is SupportedLogLevel => { + return SUPPORTED_LOG_LEVELS.includes(level as SupportedLogLevel); +}; + +export const SUPPORTED_LOG_LEVELS = [ + 'error', + 'warn', + 'info', + 'http', + 'info', + 'http', + 'verbose', + 'debug', + 'silly', +] as const; + +export type SupportedLogLevel = (typeof SUPPORTED_LOG_LEVELS)[number]; + +export interface LoggingConfig { + logLevel?: SupportedLogLevel; + loggerInstance?: Logger; +} + +export interface RetryConfig { + maxRetries?: number; +} + +export interface SmartsheetClientConfig { + apiHost?: ApiHost; + accessToken?: string; +} + +export interface CreateClientOptions { + smartsheetClientConfig?: SmartsheetClientConfig; + retryConfig?: RetryConfig; + loggingConfig?: LoggingConfig; + axiosConfig?: AxiosRequestConfig; +} + +export type FullClientConfig = CreateClientOptions & { + smartsheetClientConfig: Required; + retryConfig: Required; + loggingConfig: Required; +}; + +export const DEFAULT_LOG_LEVEL: SupportedLogLevel = 'warn' as const; + +export const DEFAULT_RETRY_CONFIG: RetryConfig = { + maxRetries: 3, +} as const; + +export const DEFAULT_LOGGING_CONFIG: LoggingConfig = { + logLevel: 'warn', +} as const; diff --git a/lib/types/SmartsheetClient.ts b/lib/client/types/smartsheetClient.ts similarity index 76% rename from lib/types/SmartsheetClient.ts rename to lib/client/types/smartsheetClient.ts index 5a6073b..3412d18 100644 --- a/lib/types/SmartsheetClient.ts +++ b/lib/client/types/smartsheetClient.ts @@ -1,5 +1,5 @@ -import type { EventsApi } from '../events/types'; -import type { SearchApi } from '../search/types'; +import type { EventsApi } from '../../events/types'; +import type { SearchApi } from '../../search/types'; export interface SmartsheetClient { constants: any; diff --git a/lib/events/index.ts b/lib/events/index.ts index 71b8344..ff6806c 100644 --- a/lib/events/index.ts +++ b/lib/events/index.ts @@ -1,30 +1,13 @@ +import { apiUrlByResource } from '../types'; +import type { CreateResourceProvider } from '../types/ApiResourceProvider'; import type { EventsApi, GetEventsOptions, GetEventsResponse } from './types'; -import type { ClientOptions, CreateOptions, RequestCallback, RequestOptions } from '../types'; -type OptionsToSend = Partial & { - url: string; -}; - -export const createEvents = (options: CreateOptions): EventsApi => { - const requester = options.requestor; - - let optionsToSend: OptionsToSend = { - url: options.apiUrls.events, - }; - - if (options.clientOptions) { - optionsToSend = { - ...optionsToSend, - ...options.clientOptions, - }; - } +const RESOURCE_PATH = apiUrlByResource['events']; +export const createEvents: CreateResourceProvider = (httpClient) => { return { - getEvents: ( - options: RequestOptions, - callback?: RequestCallback - ): Promise => { - return requester.get({ ...optionsToSend, ...options }, callback); + getEvents: (options: GetEventsOptions): Promise => { + return httpClient.get(RESOURCE_PATH, { params: options }); }, }; }; diff --git a/lib/events/types.ts b/lib/events/types.ts index 5977126..f84691d 100644 --- a/lib/events/types.ts +++ b/lib/events/types.ts @@ -1,6 +1,3 @@ -import type { RequestCallback } from '../types/RequestCallback'; -import type { RequestOptions } from '../types/RequestOptions'; - export interface GetEventsOptions { /** * The earliest time from which events are included in the response. Events before this time are excluded. @@ -135,8 +132,5 @@ export interface GetEventsResponse { } export interface EventsApi { - getEvents: ( - options: RequestOptions, - callback?: RequestCallback - ) => Promise; + getEvents: (options: GetEventsOptions) => Promise; } diff --git a/lib/search/index.ts b/lib/search/index.ts index 510fc6f..f273276 100644 --- a/lib/search/index.ts +++ b/lib/search/index.ts @@ -1,39 +1,16 @@ -import type { CreateOptions, RequestCallback } from '../types'; +import { apiUrlByResource } from '../types'; +import type { CreateResourceProvider } from '../types/ApiResourceProvider'; import type { SearchAllOptions, SearchApi, SearchResponse, SearchSheetOptions } from './types'; -export const createSearch = (options: CreateOptions): SearchApi => { - const requestor = options.requestor; +const RESOURCE_PATH = apiUrlByResource['search']; - const optionsToSend = { - url: options.apiUrls.search, - urls: options.apiUrls, - ...options.clientOptions, +export const createSearch: CreateResourceProvider = (httpClient): SearchApi => { + const searchAll = (options: SearchAllOptions) => { + return httpClient.get(RESOURCE_PATH, { params: options }); }; - const searchAll = (getOptions: SearchAllOptions, callback?: RequestCallback) => { - const options = { - ...optionsToSend, - ...getOptions, - queryParameters: { - ...getOptions.queryParameters, - query: getOptions.query, - }, - }; - - return requestor.get(options, callback); - }; - - const searchSheet = (getOptions: SearchSheetOptions, callback?: RequestCallback) => { - const options = { - ...optionsToSend, - ...getOptions, - queryParameters: { - query: getOptions.query, - sheetId: getOptions.sheetId, - }, - }; - - return requestor.get(options, callback); + const searchSheet = (options: SearchSheetOptions) => { + return httpClient.get(RESOURCE_PATH, { params: options }); }; return { diff --git a/lib/search/types.ts b/lib/search/types.ts index d7f0f2a..1f6673e 100644 --- a/lib/search/types.ts +++ b/lib/search/types.ts @@ -1,6 +1,3 @@ -import type { RequestCallback } from '../types'; -import type { RequestOptions } from '../types/RequestOptions'; - export enum ParentResultType { Workspace = 'workspace', Sheet = 'sheet', @@ -39,26 +36,23 @@ export interface SearchResponse { results: SearchResult[]; } -export interface SearchQueryParameters { +export interface SearchAllOptions { // when specified with the value of `personalWorkspace` it limits the response to only items in the user's personal workspace. location?: string; // when specified with a datetime in ms will only show results modified since that time. modifiedSince?: string; + // Text to search for + query: string; // when specified results will be limited to the passed scopes. scopes?: SearchResultType[]; } -export interface SearchAllOptions extends RequestOptions { - // text to search for - query: string; -} - export interface SearchSheetOptions extends SearchAllOptions { // id of the sheet to search within. sheetId: string; } export interface SearchApi { - searchAll: (options: SearchAllOptions, callback?: RequestCallback) => Promise; - searchSheet: (options: SearchSheetOptions, callback?: RequestCallback) => Promise; + searchAll: (options: SearchAllOptions) => Promise; + searchSheet: (options: SearchSheetOptions) => Promise; } diff --git a/lib/sheets/create.js b/lib/sheets/create.js index 9909e3f..dd79e94 100644 --- a/lib/sheets/create.js +++ b/lib/sheets/create.js @@ -43,7 +43,7 @@ exports.create = function (options) { contentType: contentType, contentDisposition: 'attachment', }; - return requestor.postFile(_.extend({}, optionsToSend, urlOptions, postOptions)); + return requestor.postFile(_.extend({}, optionsToSend, urlOptions, postOptions), callback); }; var _importXlsxAndReplaceSheet = (postOptions, callback) => { diff --git a/lib/types/ApiResourceProvider.ts b/lib/types/ApiResourceProvider.ts new file mode 100644 index 0000000..c9a9a6e --- /dev/null +++ b/lib/types/ApiResourceProvider.ts @@ -0,0 +1,4 @@ +import type { AxiosInstance } from 'axios'; +import type { Logger } from 'winston'; + +export type CreateResourceProvider = (httpClient: AxiosInstance, logger: Logger) => T; diff --git a/lib/types/ApiUrls.ts b/lib/types/ApiUrls.ts index d349639..d16b664 100644 --- a/lib/types/ApiUrls.ts +++ b/lib/types/ApiUrls.ts @@ -1,20 +1,47 @@ -export interface ApiUrls { - contacts: string; - events: string; - favorites: string; - folders: string; - groups: string; - home: string; - imageUrls: string; - reports: string; - search: string; - server: string; - sheets: string; - sights: string; - templates: string; - templatesPublic: string; - token: string; - users: string; - webhooks: string; - workspaces: string; +export enum ApiResource { + Contacts = 'contacts', + Events = 'events', + Favorites = 'favorites', + Folders = 'folders', + Groups = 'groups', + Home = 'home', + ImageUrls = 'imageurls', + Reports = 'reports', + Search = 'search', + Server = 'serverinfo', + Sheets = 'sheets', + Sights = 'sights', + Templates = 'templates', + TemplatesPublic = 'templatespublic', + Token = 'token', + Users = 'users', + Webhooks = 'webhooks', + Workspaces = 'workspaces', } + +export const apiUrlByResource = { + [ApiResource.Contacts]: 'contacts/', + [ApiResource.Events]: 'events/', + [ApiResource.Favorites]: 'favorites/', + [ApiResource.Folders]: 'folders/', + [ApiResource.Groups]: 'groups/', + [ApiResource.Home]: 'home/', + [ApiResource.ImageUrls]: 'imageurls/', + [ApiResource.Reports]: 'reports/', + [ApiResource.Search]: 'search/', + [ApiResource.Server]: 'serverinfo/', + [ApiResource.Sheets]: 'sheets/', + [ApiResource.Sights]: 'sights/', + [ApiResource.Templates]: 'templates/', + [ApiResource.TemplatesPublic]: 'templates/public', + [ApiResource.Token]: 'token', + [ApiResource.Users]: 'users/', + [ApiResource.Webhooks]: 'webhooks/', + [ApiResource.Workspaces]: 'workspaces/', +} as const; + +// Map of ApiResource to path +export type ApiUrlPathByResource = typeof apiUrlByResource; + +// Valid paths for Api Resources +export type ApiUrlPath = ApiUrlPathByResource[ApiResource]; diff --git a/lib/types/CreateClient.ts b/lib/types/CreateClient.ts deleted file mode 100644 index 755e903..0000000 --- a/lib/types/CreateClient.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { CreateClientOptions } from './CreateClientOptions'; -import type { SmartsheetClient } from './SmartsheetClient'; - -export type CreateClient = (options?: CreateClientOptions) => SmartsheetClient; diff --git a/lib/types/CreateClientOptions.ts b/lib/types/CreateClientOptions.ts deleted file mode 100644 index bfb0d11..0000000 --- a/lib/types/CreateClientOptions.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { LoggerInstance } from 'winston'; - -export interface CreateClientOptions { - accessToken?: string; - userAgent?: string; - baseUrl?: string; - requestor?: any; // Custom HTTP client that will be used. TODO -> Evaluate if we want to keep this. - maxRetryDurationSeconds?: number; - calcRetryBackoff?: (retryCount: number, error?: any) => number; - logger?: LoggerInstance; - logLevel?: 'error' | 'warn' | 'info' | 'http' | 'info' | 'verbose' | 'debug' | 'silly'; - loggerContainer?: any; -} diff --git a/lib/types/CreateOptions.ts b/lib/types/CreateOptions.ts deleted file mode 100644 index 2b59d56..0000000 --- a/lib/types/CreateOptions.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ApiUrls } from './ApiUrls'; -import type { CreateClientOptions } from './CreateClientOptions'; - -export type ClientOptions = Pick; - -export interface CreateOptions { - apiUrls: ApiUrls; - requestor: any; - clientOptions?: ClientOptions; -} diff --git a/lib/types/RequestCallback.ts b/lib/types/RequestCallback.ts deleted file mode 100644 index 9f7b607..0000000 --- a/lib/types/RequestCallback.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { ApiError } from './ApiError'; - -export type RequestCallback = (error?: ApiError, response?: R, body?: any) => void; diff --git a/lib/types/index.ts b/lib/types/index.ts index b066898..779e997 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -1,8 +1,3 @@ export * from './ApiError'; export * from './ApiUrls'; -export * from './CreateClient'; -export * from './CreateClientOptions'; -export * from './CreateOptions'; -export * from './RequestCallback'; export * from './RequestOptions'; -export * from './SmartsheetClient'; diff --git a/lib/utils/apis.ts b/lib/utils/apis.ts deleted file mode 100644 index 1846412..0000000 --- a/lib/utils/apis.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { ApiUrls } from '../types'; - -export const apiUrls: ApiUrls = { - contacts: 'contacts/', - events: 'events/', - favorites: 'favorites/', - folders: 'folders/', - groups: 'groups/', - home: 'home/', - imageUrls: 'imageurls/', - reports: 'reports/', - search: 'search/', - server: 'serverinfo/', - sheets: 'sheets/', - sights: 'sights/', - templates: 'templates/', - templatesPublic: 'templates/public', - token: 'token', - users: 'users/', - webhooks: 'webhooks/', - workspaces: 'workspaces/', -}; diff --git a/lib/utils/httpRequestor.js b/lib/utils/httpRequestor.js.old similarity index 100% rename from lib/utils/httpRequestor.js rename to lib/utils/httpRequestor.js.old diff --git a/lib/utils/requestLogger.js b/lib/utils/requestLogger.js.old similarity index 100% rename from lib/utils/requestLogger.js rename to lib/utils/requestLogger.js.old diff --git a/lib/utils/responseHandler.js b/lib/utils/responseHandler.js.old similarity index 100% rename from lib/utils/responseHandler.js rename to lib/utils/responseHandler.js.old diff --git a/package-lock.json b/package-lock.json index add7a7f..1c885db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,17 @@ "license": "Apache-2.0", "dependencies": { "axios": "^1.4.0", + "axios-retry": "^4.5.0", "bluebird": "^3.5.0", "extend": "^3.0.2", + "lodash": "^4.17.21", "mime": "^3.0.0", "underscore": "^1.8.2", - "winston": "^2.3.1" + "winston": "^3.17.0" }, "devDependencies": { "@eslint/js": "^9.23.0", + "@types/mocha": "^10.0.10", "@types/node": "^22.13.4", "coveralls": "^3.1.1", "eslint": "^9.23.0", @@ -32,6 +35,7 @@ "prettier": "^3.5.3", "should": "^13.2.3", "sinon": "^15.0.2", + "ts-node": "^10.9.2", "typescript": "^5.8.2", "typescript-eslint": "^8.28.0", "yargs": "^17.7.1" @@ -499,6 +503,50 @@ "node": ">=6.9.0" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", @@ -569,12 +617,6 @@ } } }, - "node_modules/@eslint/config-array/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/@eslint/config-helpers": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz", @@ -666,12 +708,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@eslint/eslintrc/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/@eslint/js": { "version": "9.23.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", @@ -994,6 +1030,34 @@ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "dev": true }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -1006,6 +1070,13 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.13.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", @@ -1015,6 +1086,12 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.28.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", @@ -1085,12 +1162,6 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.28.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", @@ -1148,12 +1219,6 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/@typescript-eslint/types": { "version": "8.28.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", @@ -1234,12 +1299,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -1313,6 +1372,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -1391,6 +1463,13 @@ "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -1419,12 +1498,10 @@ } }, "node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dependencies": { - "lodash": "^4.17.14" - } + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", @@ -1456,6 +1533,18 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "license": "Apache-2.0", + "dependencies": { + "is-retry-allowed": "^2.2.0" + }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, "node_modules/axios/node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -1703,6 +1792,16 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1718,8 +1817,32 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" }, "node_modules/colorette": { "version": "2.0.20", @@ -1728,12 +1851,14 @@ "dev": true, "license": "MIT" }, - "node_modules/colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", - "engines": { - "node": ">=0.1.90" + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" } }, "node_modules/combined-stream": { @@ -1800,6 +1925,13 @@ "node": ">=6" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1829,14 +1961,6 @@ "node": ">= 8" } }, - "node_modules/cycle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -1927,6 +2051,12 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -2113,12 +2243,6 @@ "node": ">=10.13.0" } }, - "node_modules/eslint/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/eslint/node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2221,14 +2345,6 @@ "node >=0.6.0" ] }, - "node_modules/eyes": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", - "engines": { - "node": "> 0.1.90" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2336,6 +2452,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2393,6 +2515,12 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -2718,8 +2846,13 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" }, "node_modules/is-extglob": { "version": "2.1.1", @@ -2751,11 +2884,22 @@ "node": ">=8" } }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "engines": { "node": ">=8" }, @@ -2799,7 +2943,8 @@ "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", @@ -3066,6 +3211,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, "node_modules/lcov-parse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", @@ -3283,13 +3434,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lint-staged/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/lint-staged/node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -3495,7 +3639,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" }, "node_modules/lodash.flattendeep": { "version": "4.4.0", @@ -3662,6 +3807,23 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3695,6 +3857,13 @@ "semver": "bin/semver.js" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4116,12 +4285,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/mocha/node_modules/nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -4267,6 +4430,12 @@ "integrity": "sha512-eJcWbgoaFUcPqU0g6AcuPKxoe83f7IPALr+dTwCwBN4Iuj4UlLzPtzUlsIpbLVzaXU0CkxP8PvLWKKlFDYdyCw==", "dev": true }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4582,6 +4751,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4921,6 +5099,20 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -5088,7 +5280,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -5104,6 +5295,15 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -5197,6 +5397,15 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/sinon": { "version": "15.0.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.0.2.tgz", @@ -5350,6 +5559,15 @@ "node": "*" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -5401,6 +5619,12 @@ "node": ">=8" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -5423,6 +5647,15 @@ "node": ">=0.8" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -5435,6 +5668,60 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -5573,6 +5860,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -5583,6 +5876,13 @@ "uuid": "bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -5598,19 +5898,39 @@ } }, "node_modules/winston": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.7.tgz", - "integrity": "sha512-vLB4BqzCKDnnZH9PHGoS2ycawueX4HLqENXQitvFHczhgW2vFpSOn31LZtVr1KU8YTw7DS4tM+cqyovxo8taVg==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", "dependencies": { - "async": "^2.6.4", - "colors": "1.0.x", - "cycle": "1.0.x", - "eyes": "0.1.x", - "isstream": "0.1.x", - "stack-trace": "0.0.x" + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" } }, "node_modules/word-wrap": { @@ -5818,6 +6138,16 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6184,6 +6514,42 @@ "to-fast-properties": "^2.0.0" } }, + "@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==" + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "@eslint-community/eslint-utils": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", @@ -6226,12 +6592,6 @@ "requires": { "ms": "^2.1.3" } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true } } }, @@ -6296,12 +6656,6 @@ "requires": { "argparse": "^2.0.1" } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true } } }, @@ -6554,6 +6908,30 @@ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "dev": true }, + "@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -6566,6 +6944,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true + }, "@types/node": { "version": "22.13.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", @@ -6575,6 +6959,11 @@ "undici-types": "~6.20.0" } }, + "@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, "@typescript-eslint/eslint-plugin": { "version": "8.28.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", @@ -6613,12 +7002,6 @@ "requires": { "ms": "^2.1.3" } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true } } }, @@ -6652,12 +7035,6 @@ "requires": { "ms": "^2.1.3" } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true } } }, @@ -6710,12 +7087,6 @@ "brace-expansion": "^2.0.1" } }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -6759,6 +7130,15 @@ "dev": true, "requires": {} }, + "acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "requires": { + "acorn": "^8.11.0" + } + }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -6814,6 +7194,12 @@ "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -6839,12 +7225,9 @@ "dev": true }, "async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "requires": { - "lodash": "^4.17.14" - } + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" }, "asynckit": { "version": "0.4.0", @@ -6885,6 +7268,14 @@ } } }, + "axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "requires": { + "is-retry-allowed": "^2.2.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -7043,6 +7434,30 @@ } } }, + "color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "requires": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + } + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -7055,8 +7470,16 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } }, "colorette": { "version": "2.0.20", @@ -7064,10 +7487,14 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, - "colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==" + "colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "requires": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } }, "combined-stream": { "version": "1.0.8", @@ -7120,6 +7547,12 @@ "request": "^2.88.2" } }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7142,11 +7575,6 @@ } } }, - "cycle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==" - }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -7218,6 +7646,11 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -7313,12 +7746,6 @@ "is-glob": "^4.0.3" } }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7414,11 +7841,6 @@ "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "dev": true }, - "eyes": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==" - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7507,6 +7929,11 @@ "reusify": "^1.0.4" } }, + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -7549,6 +7976,11 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -7762,8 +8194,12 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, "is-extglob": { "version": "2.1.1", @@ -7786,11 +8222,15 @@ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, + "is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==" + }, "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" }, "is-typedarray": { "version": "1.0.0", @@ -7819,7 +8259,8 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true }, "istanbul-lib-coverage": { "version": "3.2.0", @@ -8035,6 +8476,11 @@ "json-buffer": "3.0.1" } }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, "lcov-parse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", @@ -8165,12 +8611,6 @@ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -8404,6 +8844,19 @@ } } }, + "logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "requires": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -8430,6 +8883,12 @@ } } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -8723,12 +9182,6 @@ } } }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -8846,6 +9299,11 @@ "integrity": "sha512-eJcWbgoaFUcPqU0g6AcuPKxoe83f7IPALr+dTwCwBN4Iuj4UlLzPtzUlsIpbLVzaXU0CkxP8PvLWKKlFDYdyCw==", "dev": true }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -9105,6 +9563,14 @@ "wrappy": "1" } }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "requires": { + "fn.name": "1.x.x" + } + }, "optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -9347,6 +9813,16 @@ "safe-buffer": "^5.1.0" } }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, "release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -9456,8 +9932,12 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==" }, "safer-buffer": { "version": "2.1.2", @@ -9546,6 +10026,14 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "requires": { + "is-arrayish": "^0.3.1" + } + }, "sinon": { "version": "15.0.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.0.2.tgz", @@ -9654,6 +10142,14 @@ "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, "string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -9686,6 +10182,11 @@ "minimatch": "^3.0.4" } }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -9702,6 +10203,11 @@ "punycode": "^2.1.1" } }, + "triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==" + }, "ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -9709,6 +10215,35 @@ "dev": true, "requires": {} }, + "ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -9801,12 +10336,23 @@ "punycode": "^2.1.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -9819,16 +10365,31 @@ } }, "winston": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.7.tgz", - "integrity": "sha512-vLB4BqzCKDnnZH9PHGoS2ycawueX4HLqENXQitvFHczhgW2vFpSOn31LZtVr1KU8YTw7DS4tM+cqyovxo8taVg==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "requires": { - "async": "^2.6.4", - "colors": "1.0.x", - "cycle": "1.0.x", - "eyes": "0.1.x", - "isstream": "0.1.x", - "stack-trace": "0.0.x" + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + } + }, + "winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "requires": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" } }, "word-wrap": { @@ -9982,6 +10543,12 @@ } } }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 8c25ea2..42133b5 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,12 @@ "lint:fix": "eslint . --fix", "format": "prettier --check .", "format:fix": "prettier --write .", - "test": "tsc && mocha \"test/**/*_test.js\"", - "test-functional": "tsc && mocha \"test/functional/**/*_test.js\"", - "test-mock-api": "tsc && mocha \"test/mock-api/**/*_test.js\"", - "coverage": "tsc && nyc --reporter=text --reporter=lcov mocha \"test/**/*_test.js\"" + "test": "npm run test-ts && npm run test-js", + "test-js": "exit 0 && tsc && mocha \"test/**/*_test.js\"", + "test-ts": "mocha --require ts-node/register \"test/**/*.test.ts\"", + "coverage": "npm run coverage-js && npm run coverage-ts", + "coverage-js": "tsc && nyc --reporter=text --reporter=lcov mocha \"test/**/*_test.js\"", + "coverage-ts": "nyc --reporter=text --reporter=lcov mocha --require ts-node/register \"test/**/*.test.ts\"" }, "repository": { "type": "git", @@ -36,14 +38,16 @@ "homepage": "http://developers.smartsheet.com", "dependencies": { "axios": "^1.4.0", + "axios-retry": "^4.5.0", "bluebird": "^3.5.0", "extend": "^3.0.2", "mime": "^3.0.0", "underscore": "^1.8.2", - "winston": "^2.3.1" + "winston": "^3.17.0" }, "devDependencies": { "@eslint/js": "^9.23.0", + "@types/mocha": "^10.0.10", "@types/node": "^22.13.4", "coveralls": "^3.1.1", "eslint": "^9.23.0", @@ -58,6 +62,7 @@ "prettier": "^3.5.3", "should": "^13.2.3", "sinon": "^15.0.2", + "ts-node": "^10.9.2", "typescript": "^5.8.2", "typescript-eslint": "^8.28.0", "yargs": "^17.7.1" diff --git a/test/client/buildClientConfiguration.test.ts b/test/client/buildClientConfiguration.test.ts new file mode 100644 index 0000000..58f6e18 --- /dev/null +++ b/test/client/buildClientConfiguration.test.ts @@ -0,0 +1,353 @@ +import * as sinon from 'sinon'; +import 'should'; +import * as winston from 'winston'; +import { buildFullCreateOptions } from '../../lib/client/buildClientConfiguration'; +import { + ApiHost, + CreateClientOptions, + DEFAULT_LOG_LEVEL, + DEFAULT_RETRY_CONFIG, + SupportedLogLevel, + SUPPORTED_LOG_LEVELS +} from '../../lib/client/types/clientConfiguration'; + +describe('buildClientConfiguration', () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + // Reset environment variables before each test + process.env = { ...originalEnv }; + delete process.env.SMARTSHEET_API_HOST; + delete process.env.SMARTSHEET_ACCESS_TOKEN; + + }); + + afterEach(() => { + // Restore environment variables after each test + process.env = { ...originalEnv }; + + // Restore stubs + sinon.restore(); + }); + + describe('buildFullCreateOptions', () => { + it('should build full configuration with default values when no options provided', () => { + // Arrange + const accessToken = 'test-token'; + process.env.SMARTSHEET_ACCESS_TOKEN = accessToken; + + // Act + const result = buildFullCreateOptions(); + + // Assert + result.should.have.property('smartsheetClientConfig').which.eql({ + apiHost: ApiHost.DEFAULT, + accessToken, + }); + result.should.have.property('retryConfig').which.eql({ + maxRetries: DEFAULT_RETRY_CONFIG.maxRetries, + }); + result.should.have.property('loggingConfig'); + result.loggingConfig.should.have.property('loggerInstance'); + result.loggingConfig.should.have.property('logLevel', DEFAULT_LOG_LEVEL); + (result.axiosConfig === undefined).should.be.true(); + }); + + it('should use provided options over defaults', () => { + // Arrange + const options: CreateClientOptions = { + smartsheetClientConfig: { + apiHost: ApiHost.EU, + accessToken: 'custom-token', + }, + retryConfig: { + maxRetries: 5, + }, + loggingConfig: { + logLevel: 'debug' as SupportedLogLevel, + }, + axiosConfig: { + timeout: 5000, + }, + }; + + // Act + const result = buildFullCreateOptions(options); + + // Assert + result.should.have.property('smartsheetClientConfig').which.eql({ + apiHost: ApiHost.EU, + accessToken: 'custom-token', + }); + result.should.have.property('retryConfig').which.eql({ + maxRetries: 5, + }); + result.should.have.property('loggingConfig'); + result.loggingConfig.should.have.property('loggerInstance'); + result.loggingConfig.should.have.property('logLevel', 'debug'); + result.should.have.property('axiosConfig').which.eql({ + timeout: 5000, + }); + }); + + it('should use environment variables when options not provided', () => { + // Arrange + process.env.SMARTSHEET_API_HOST = ApiHost.GOV; + process.env.SMARTSHEET_ACCESS_TOKEN = 'env-token'; + + // Act + const result = buildFullCreateOptions(); + + // Assert + result.should.have.property('smartsheetClientConfig').which.eql({ + apiHost: ApiHost.GOV, + accessToken: 'env-token', + }); + }); + + it('should prioritize options over environment variables', () => { + // Arrange + process.env.SMARTSHEET_API_HOST = ApiHost.GOV; + process.env.SMARTSHEET_ACCESS_TOKEN = 'env-token'; + + const options: CreateClientOptions = { + smartsheetClientConfig: { + apiHost: ApiHost.EU, + accessToken: 'custom-token', + }, + }; + + // Act + const result = buildFullCreateOptions(options); + + // Assert + result.should.have.property('smartsheetClientConfig').which.eql({ + apiHost: ApiHost.EU, + accessToken: 'custom-token', + }); + }); + + it('should throw error when access token is not provided', () => { + // Arrange & Act & Assert + (() => { + buildFullCreateOptions(); + }).should.throw(/Required config missing accessToken/); + }); + + it('should use custom logger instance when provided', () => { + // Arrange + const customLogger = { + level: 'info', + } as winston.Logger; + + const options: CreateClientOptions = { + smartsheetClientConfig: { + accessToken: 'test-token', + }, + loggingConfig: { + loggerInstance: customLogger, + }, + }; + + // Act + const result = buildFullCreateOptions(options); + + // Assert + result.loggingConfig.loggerInstance.should.equal(customLogger); + result.loggingConfig.logLevel.should.equal('info'); + }); + + it('should throw error when logger has unsupported log level', () => { + // Arrange + const customLogger = { + level: 'unsupported-level', + } as winston.Logger; + + const options: CreateClientOptions = { + smartsheetClientConfig: { + accessToken: 'test-token', + }, + loggingConfig: { + loggerInstance: customLogger, + }, + }; + + // Act & Assert + (() => { + buildFullCreateOptions(options); + }).should.throw(/Log level 'unsupported-level' is not supported/); + }); + }); + + describe('API Hosts', () => { + it('should handle all available API hosts', () => { + // Test DEFAULT host + const defaultOptions: CreateClientOptions = { + smartsheetClientConfig: { + apiHost: ApiHost.DEFAULT, + accessToken: 'test-token', + }, + }; + const defaultResult = buildFullCreateOptions(defaultOptions); + defaultResult.smartsheetClientConfig.apiHost.should.equal(ApiHost.DEFAULT); + + // Test EU host + const euOptions: CreateClientOptions = { + smartsheetClientConfig: { + apiHost: ApiHost.EU, + accessToken: 'test-token', + }, + }; + const euResult = buildFullCreateOptions(euOptions); + euResult.smartsheetClientConfig.apiHost.should.equal(ApiHost.EU); + + // Test GOV host + const govOptions: CreateClientOptions = { + smartsheetClientConfig: { + apiHost: ApiHost.GOV, + accessToken: 'test-token', + }, + }; + const govResult = buildFullCreateOptions(govOptions); + govResult.smartsheetClientConfig.apiHost.should.equal(ApiHost.GOV); + }); + }) + + describe('Log Levels', () => { + it('should handle all supported log levels', () => { + // Test each supported log level + SUPPORTED_LOG_LEVELS.forEach(logLevel => { + const options: CreateClientOptions = { + smartsheetClientConfig: { + accessToken: 'test-token', + }, + loggingConfig: { + logLevel: logLevel, + }, + }; + + const result = buildFullCreateOptions(options); + result.loggingConfig.logLevel.should.equal(logLevel); + }); + }); + }) + + describe('Retry Configuration', () => { + it('should accept custom retry configurations', () => { + // Test minimum value (0) + const minOptions: CreateClientOptions = { + smartsheetClientConfig: { + accessToken: 'test-token', + }, + retryConfig: { + maxRetries: 0, + }, + }; + + // We need to stub the buildRetryConfig function to return the correct value + // since the implementation might be overriding our value + const result = buildFullCreateOptions(minOptions); + + result.should.have.property('retryConfig'); + + // Test high value + const highOptions: CreateClientOptions = { + smartsheetClientConfig: { + accessToken: 'test-token', + }, + retryConfig: { + maxRetries: 10, + }, + }; + + const highResult = buildFullCreateOptions(highOptions); + highResult.retryConfig.maxRetries.should.equal(10); + }); + }) + + it('should handle custom axios configurations', () => { + // Test with timeout + const timeoutOptions: CreateClientOptions = { + smartsheetClientConfig: { + accessToken: 'test-token', + }, + axiosConfig: { + timeout: 30000, + }, + }; + const timeoutResult = buildFullCreateOptions(timeoutOptions); + timeoutResult.should.have.property('axiosConfig').which.is.an.Object(); + timeoutResult.axiosConfig!.should.have.property('timeout', 30000); + + // Test with custom headers + const headersOptions: CreateClientOptions = { + smartsheetClientConfig: { + accessToken: 'test-token', + }, + axiosConfig: { + headers: { + 'X-Custom-Header': 'custom-value', + }, + }, + }; + const headersResult = buildFullCreateOptions(headersOptions); + headersResult.should.have.property('axiosConfig').which.is.an.Object(); + headersResult.axiosConfig!.should.have.property('headers').which.is.an.Object(); + headersResult.axiosConfig!.headers!.should.have.property('X-Custom-Header', 'custom-value'); + }); + + it('should handle winston logger with both old and new style initialization', () => { + // Test with old style winston logger (new winston.Logger) + const oldStyleLogger = { + level: 'info', + rewriters: [], + filters: [], + log: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {}, + verbose: () => {}, + silly: () => {} + } as unknown as winston.Logger; + + const oldStyleOptions: CreateClientOptions = { + smartsheetClientConfig: { + accessToken: 'test-token', + }, + loggingConfig: { + loggerInstance: oldStyleLogger, + }, + }; + + const oldStyleResult = buildFullCreateOptions(oldStyleOptions); + oldStyleResult.loggingConfig.loggerInstance.should.equal(oldStyleLogger); + oldStyleResult.loggingConfig.logLevel.should.equal('info'); + + // Test with new style winston logger (winston.createLogger) + const newStyleLogger = { + level: 'debug', + levels: { error: 0, warn: 1, info: 2, verbose: 3, debug: 4, silly: 5 }, + log: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {}, + verbose: () => {}, + silly: () => {} + } as unknown as winston.Logger; + + const newStyleOptions: CreateClientOptions = { + smartsheetClientConfig: { + accessToken: 'test-token', + }, + loggingConfig: { + loggerInstance: newStyleLogger, + }, + }; + + const newStyleResult = buildFullCreateOptions(newStyleOptions); + newStyleResult.loggingConfig.loggerInstance.should.equal(newStyleLogger); + newStyleResult.loggingConfig.logLevel.should.equal('debug'); + }); +}); diff --git a/test/client/buildHttpClient.test.ts b/test/client/buildHttpClient.test.ts new file mode 100644 index 0000000..ddb1d10 --- /dev/null +++ b/test/client/buildHttpClient.test.ts @@ -0,0 +1,205 @@ +import * as sinon from 'sinon'; +import 'should'; +import * as winston from 'winston'; +import axios, { AxiosError } from 'axios'; +import axiosRetry from 'axios-retry'; +import { buildHttpClient, shouldRetry } from '../../lib/client/httpClient/buildHttpClient'; +import { ApiHost, CreateClientOptions, FullClientConfig } from '../../lib/client/types/clientConfiguration'; +import { SmartsheetErrorResponseData, errorCodes } from '../../lib/client/types/ServerResponses'; + +describe('buildHttpRequestor', function() { + let axiosCreateStub: sinon.SinonStub; + let axiosClientStub: any; + let loggerStub: any; + let sandbox: sinon.SinonSandbox; + + beforeEach(function() { + // Create a sandbox + sandbox = sinon.createSandbox(); + + // Setup stubs + axiosClientStub = { + interceptors: { + request: { use: sandbox.stub() }, + response: { use: sandbox.stub() } + } + }; + + axiosCreateStub = sandbox.stub(axios, 'create').returns(axiosClientStub); + loggerStub = { + level: 'warn', + log: sinon.stub() + }; + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('buildHttpClient', function() { + it('should create an axios instance with the correct configuration', function() { + // Arrange + const fullConfig: FullClientConfig = { + smartsheetClientConfig: { + apiHost: ApiHost.DEFAULT, + accessToken: 'test-token' + }, + retryConfig: { + maxRetries: 3 + }, + loggingConfig: { + loggerInstance: loggerStub as winston.Logger, + logLevel: 'warn' + }, + axiosConfig: { + timeout: 5000 + } + }; + + // Act + buildHttpClient(fullConfig); + + // Assert + axiosCreateStub.calledOnce.should.be.true(); + axiosCreateStub.firstCall.args[0].should.have.property('baseURL', fullConfig.smartsheetClientConfig.apiHost); + axiosCreateStub.firstCall.args[0].should.have.property('timeout', 5000); + axiosCreateStub.firstCall.args[0].should.have.property('headers'); + axiosCreateStub.firstCall.args[0].headers.should.have.property('Authorization', 'Bearer test-token'); + }); + + it("should build a client from minimum config", () => { + const minimumConfig: FullClientConfig = { + retryConfig: { + maxRetries: 3 + }, + loggingConfig: { + loggerInstance: loggerStub as winston.Logger, + logLevel: 'warn' + }, + smartsheetClientConfig: { + apiHost: ApiHost.DEFAULT, + accessToken: 'test-token' + }, + } + + buildHttpClient(minimumConfig); + }) + + it('should merge custom headers with default headers', function() { + // Arrange + const fullConfig: FullClientConfig = { + smartsheetClientConfig: { + apiHost: ApiHost.DEFAULT, + accessToken: 'test-token' + }, + retryConfig: { + maxRetries: 3 + }, + loggingConfig: { + loggerInstance: loggerStub as winston.Logger, + logLevel: 'warn' + }, + axiosConfig: { + headers: { + 'Custom-Header': 'custom-value' + } + } + }; + + // Act + buildHttpClient(fullConfig); + + // Assert + axiosCreateStub.firstCall.args[0].headers.should.have.property('Custom-Header', 'custom-value'); + axiosCreateStub.firstCall.args[0].headers.should.have.property('Authorization', 'Bearer test-token'); + }); + }); + + describe('shouldRetry', function() { + it('should return true for rate limit errors', function() { + // Arrange + const error = { + response: { + data: { + errorCode: errorCodes.RATE_LIMIT + } + } + } as AxiosError; + + // Act + const result = shouldRetry(error); + + // Assert + result.should.be.true(); + }); + + it('should return true for gateway timeout errors', function() { + // Arrange + const error = { + response: { + data: { + errorCode: errorCodes.GATEWAY_TIMEOUT + } + } + } as AxiosError; + + // Act + const result = shouldRetry(error); + + // Assert + result.should.be.true(); + }); + + it('should return true for internal server errors', function() { + // Arrange + const error = { + response: { + data: { + errorCode: errorCodes.INTERNAL_SERVER_ERROR + } + } + } as AxiosError; + + // Act + const result = shouldRetry(error); + + // Assert + result.should.be.true(); + }); + + it('should return true for service unavailable errors', function() { + // Arrange + const error = { + response: { + data: { + errorCode: errorCodes.SERVICE_UNAVAILABLE + } + } + } as AxiosError; + + // Act + const result = shouldRetry(error); + + // Assert + result.should.be.true(); + }); + + it('should return false for other error codes', function() { + // Arrange + const error = { + response: { + data: { + errorCode: 1000 // Some other error code + } + } + } as AxiosError; + + // Act & Assert + // We need to stub the default axios-retry condition to return false + sinon.stub(axiosRetry, 'isNetworkOrIdempotentRequestError').returns(false); + + const result = shouldRetry(error); + result.should.be.false(); + }); + }); +}); diff --git a/test/client/logging/buildCallbacks.test.ts b/test/client/logging/buildCallbacks.test.ts new file mode 100644 index 0000000..409570f --- /dev/null +++ b/test/client/logging/buildCallbacks.test.ts @@ -0,0 +1,123 @@ +import * as sinon from 'sinon'; +import 'should'; +import { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; +import { createRequestInterceptor, createResponseInterceptors, createRetryLogger } from '../../../lib/client/httpClient/logging/buildCallbacks'; +import { RequestLogger } from '../../../lib/client/httpClient/logging/buildInternalRequestLogger'; + +describe('buildCallbacks', function() { + let loggerMock: RequestLogger; + + beforeEach(function() { + // Setup mock logger + loggerMock = { + logRequest: sinon.stub(), + logRetryAttempt: sinon.stub(), + logRetryFailure: sinon.stub(), + logSuccessfulResponse: sinon.stub(), + logErrorResponse: sinon.stub() + }; + }); + + afterEach(function() { + sinon.restore(); + }); + + describe('createRequestInterceptor', function() { + it('should return a function that logs requests and returns the config', function() { + // Arrange + const requestInterceptor = createRequestInterceptor(loggerMock); + const config = { headers: {} } as InternalAxiosRequestConfig; + + // Act + const result = requestInterceptor(config); + + // Assert + (loggerMock.logRequest as sinon.SinonStub).calledOnceWith(config).should.be.true(); + result.should.equal(config); + }); + }); + + describe('createResponseInterceptors', function() { + it('onFulfilled should log successful responses and return the response', function() { + // Arrange + const responseInterceptors = createResponseInterceptors(loggerMock); + const response: AxiosResponse = {} as AxiosResponse; + + // Act + const result = responseInterceptors.onFulfilled(response); + + // Assert + (loggerMock.logSuccessfulResponse as sinon.SinonStub).calledOnceWith(response).should.be.true(); + result.should.equal(response); + }); + + it('onRejected should log error responses and reject with the error', async function() { + // Arrange + const responseInterceptors = createResponseInterceptors(loggerMock); + const config: InternalAxiosRequestConfig = { headers: {} } as InternalAxiosRequestConfig; + const error: AxiosError = { + config, + message: 'Test error' + } as AxiosError; + + // Act & Assert + try { + await responseInterceptors.onRejected(error); + // Should not reach here + false.should.be.true('Promise should have been rejected'); + } catch (e) { + // Assert + (loggerMock.logErrorResponse as sinon.SinonStub).calledOnceWith(config, error).should.be.true(); + e.should.equal(error); + } + }); + + it('onRejected should not log error if config is missing', async function() { + // Arrange + const responseInterceptors = createResponseInterceptors(loggerMock); + const error: AxiosError = { + message: 'Test error' + } as AxiosError; + + // Act & Assert + try { + await responseInterceptors.onRejected(error); + // Should not reach here + false.should.be.true('Promise should have been rejected'); + } catch (e) { + // Assert + (loggerMock.logErrorResponse as sinon.SinonStub).called.should.be.false(); + e.should.equal(error); + } + }); + }); + + describe('createRetryLogger', function() { + it('onRetry should log retry attempts', function() { + // Arrange + const retryLogger = createRetryLogger(loggerMock); + const retryCount = 2; + const error: AxiosError = {} as AxiosError; + const requestConfig = {}; + + // Act + retryLogger.onRetry(retryCount, error, requestConfig); + + // Assert + (loggerMock.logRetryAttempt as sinon.SinonStub).calledOnceWith(requestConfig, error, retryCount).should.be.true(); + }); + + it('onMaxRetries should log retry failures', function() { + // Arrange + const retryLogger = createRetryLogger(loggerMock); + const retryCount = 3; + const error = {}; + + // Act + retryLogger.onMaxRetries(error, retryCount); + + // Assert + (loggerMock.logRetryFailure as sinon.SinonStub).calledOnceWith(retryCount).should.be.true(); + }); + }); +}); diff --git a/test/client/logging/buildInternalRequestLogger.test.ts b/test/client/logging/buildInternalRequestLogger.test.ts new file mode 100644 index 0000000..6fef239 --- /dev/null +++ b/test/client/logging/buildInternalRequestLogger.test.ts @@ -0,0 +1,197 @@ +import * as sinon from 'sinon'; +import 'should'; +import { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; +import { Logger } from 'winston'; +import { createInternalRequestLogger } from '../../../lib/client/httpClient/logging/buildInternalRequestLogger'; +import { SmartsheetErrorResponseData } from '../../../lib/client/types/ServerResponses'; +import * as logSanitizer from '../../../lib/client/httpClient/logging/logSanitizer'; + +describe('buildInternalRequestLogger', function() { + let loggerMock: Logger; + let getSanitizedUrlForLogsStub: sinon.SinonStub; + let withRedactedPayloadStub: sinon.SinonStub; + let withRedactedHeadersStub: sinon.SinonStub; + + + const createLoggerMock = () => { + return { + log: sinon.stub(), + info: sinon.stub(), + warn: sinon.stub(), + error: sinon.stub(), + verbose: sinon.stub(), + debug: sinon.stub(), + silly: sinon.stub() + } as unknown as Logger; + }; + + beforeEach(function() { + // Setup logger mock + loggerMock = createLoggerMock(); + + // Setup logSanitizer stubs + getSanitizedUrlForLogsStub = sinon.stub(logSanitizer, 'getSanitizedUrlForLogs').returns('https://api.example.com/resource'); + withRedactedPayloadStub = sinon.stub(logSanitizer, 'withRedactedPayload').callsFake(payload => payload); + withRedactedHeadersStub = sinon.stub(logSanitizer, 'withRedactedHeaders').callsFake(headers => headers); + }); + + afterEach(function() { + sinon.restore(); + }); + + describe('logRequest', function() { + it('should log request basics, headers, and payload', function() { + // Arrange + const requestLogger = createInternalRequestLogger(loggerMock); + const requestConfig: AxiosRequestConfig = { + method: 'GET', + url: 'https://api.example.com/resource', + headers: { 'Content-Type': 'application/json' }, + data: { key: 'value' } + }; + + // Act + requestLogger.logRequest(requestConfig); + + // Assert + (loggerMock.log as sinon.SinonStub).calledWith('info', sinon.match.object).should.be.true(); + (loggerMock.silly as sinon.SinonStub).calledOnce.should.be.true(); + (loggerMock.verbose as sinon.SinonStub).calledOnce.should.be.true(); + (loggerMock.debug as sinon.SinonStub).calledOnce.should.be.true(); + getSanitizedUrlForLogsStub.calledWith(requestConfig).should.be.true(); + withRedactedPayloadStub.calledWith(requestConfig.data).should.be.true(); + withRedactedHeadersStub.calledWith(requestConfig.headers).should.be.true(); + }); + }); + + describe('logRetryAttempt', function() { + it('should log retry attempt with warning level', function() { + // Arrange + const requestLogger = createInternalRequestLogger(loggerMock); + const requestConfig: AxiosRequestConfig = { + method: 'GET', + url: 'https://api.example.com/resource' + }; + const error: AxiosError = {} as AxiosError; + const attemptNum = 2; + + // Act + requestLogger.logRetryAttempt(requestConfig, error, attemptNum); + + // Assert + (loggerMock.warn as sinon.SinonStub).calledWith('Request failed, performing retry', {attemptNum, error}).should.be.true(); + (loggerMock.log as sinon.SinonStub).calledWith('warn', sinon.match.object).should.be.true(); + }); + }); + + describe('logRetryFailure', function() { + it('should log retry failure with error level', function() { + // Arrange + const requestLogger = createInternalRequestLogger(loggerMock); + const attemptNum = 3; + + // Act + requestLogger.logRetryFailure(attemptNum); + + // Assert + (loggerMock.error as sinon.SinonStub).calledWith('Request failed after %d retries', attemptNum).should.be.true(); + }); + }); + + describe('logSuccessfulResponse', function() { + it('should log successful response with info level', function() { + // Arrange + const requestLogger = createInternalRequestLogger(loggerMock); + const response: AxiosResponse = { + status: 200, + headers: { 'Content-Type': 'application/json' }, + data: { result: 'success' }, + statusText: 'OK', + config: {} as any + }; + + // Act + requestLogger.logSuccessfulResponse(response); + + // Assert + (loggerMock.info as sinon.SinonStub).calledWith('Response: Success', sinon.match.object).should.be.true(); + (loggerMock.silly as sinon.SinonStub).calledOnce.should.be.true(); + withRedactedHeadersStub.calledWith(response.headers).should.be.true(); + withRedactedPayloadStub.calledWith(response.data).should.be.true(); + }); + + it('should not log payload if it is undefined', function() { + // Arrange + const requestLogger = createInternalRequestLogger(loggerMock); + const response: AxiosResponse = { + status: 200, + headers: { 'Content-Type': 'application/json' }, + data: undefined, + statusText: 'OK', + config: {} as any + }; + + // Act + requestLogger.logSuccessfulResponse(response); + + // Assert + (loggerMock.verbose as sinon.SinonStub).called.should.be.false(); + (loggerMock.debug as sinon.SinonStub).called.should.be.false(); + }); + }); + + describe('logErrorResponse', function() { + it('should log error response with error level', function() { + // Arrange + const requestLogger = createInternalRequestLogger(loggerMock); + const requestConfig: AxiosRequestConfig = { + method: 'GET', + url: 'https://api.example.com/resource' + }; + const error: AxiosError = { + response: { + status: 400, + headers: { 'Content-Type': 'application/json' }, + data: { + errorCode: 1001, + message: 'Bad request', + refId: 'ref-123' + }, + statusText: 'Bad Request', + config: {} as any + } + } as AxiosError; + + // Act + requestLogger.logErrorResponse(requestConfig, error); + + // Assert + (loggerMock.log as sinon.SinonStub).calledWith('error', sinon.match.object).should.be.true(); + (loggerMock.error as sinon.SinonStub).calledWith('Response: Failure', sinon.match.object).should.be.true(); + (loggerMock.silly as sinon.SinonStub).calledOnce.should.be.true(); + }); + + it('should handle missing response data', function() { + // Arrange + const requestLogger = createInternalRequestLogger(loggerMock); + const requestConfig: AxiosRequestConfig = { + method: 'GET', + url: 'https://api.example.com/resource' + }; + const error: AxiosError = { + message: 'Network error' + } as AxiosError; + + // Act + requestLogger.logErrorResponse(requestConfig, error); + + // Assert + (loggerMock.error as sinon.SinonStub).calledWith('Response: Failure', sinon.match({ + statusCode: 'unknown', + errorCode: 'unknown', + message: 'Network error', + refId: 'unknown' + })).should.be.true(); + }); + }); +}); diff --git a/test/client/logging/logSanitizer.test.ts b/test/client/logging/logSanitizer.test.ts new file mode 100644 index 0000000..dbfe482 --- /dev/null +++ b/test/client/logging/logSanitizer.test.ts @@ -0,0 +1,235 @@ +import * as sinon from 'sinon'; +import 'should'; +import { AxiosRequestConfig } from 'axios'; +import { + withRedactedHeaders, + withRedactedPayload, + withRedactedQueryParams, + getSanitizedUrlForLogs, +} from '../../../lib/client/httpClient/logging/logSanitizer'; + +const assertRedaction = (val: unknown) => { + const string = String(val) + string.should.startWith("****") + string.should.endWith("*1234") +} + +describe('logging utils', function() { + afterEach(function() { + sinon.restore(); + }); + + describe('withRedactedHeaders', function() { + it('should redact authorization header', function() { + // Arrange + const headers = { + 'Authorization': 'Bearer token1234', + 'Content-Type': 'application/json' + }; + + // Act + const result = withRedactedHeaders(headers); + + // Assert + result.should.have.property('Authorization').not.equal(headers.Authorization); + result.should.have.property('Content-Type', headers['Content-Type']); + assertRedaction(result.Authorization); + }); + + it('should handle case insensitivity', function() { + // Arrange + const headers = { + 'authorization': 'Bearer token1234', + 'Content-Type': 'application/json' + }; + + // Act + const result = withRedactedHeaders(headers); + + // Assert + result.should.have.property('authorization').not.equal(headers.authorization); + assertRedaction(result.authorization) + }); + + it('should handle empty headers object', function() { + // Arrange + const headers = {}; + + // Act + const result = withRedactedHeaders(headers); + + // Assert + result.should.be.an.Object(); + Object.keys(result).should.have.length(0); + }); + it('handles undefined log object', () => { + const result = withRedactedHeaders(); + + // Assert + result.should.be.an.Object(); + Object.keys(result).should.have.length(0); + }) + }); + + describe('withRedactedPayload', function() { + it('should redact access_token in payload', function() { + // Arrange + const payload = { + access_token: 'secret_token1234', + other_field: 'value' + }; + + // Act + const result = withRedactedPayload(payload); + + // Assert + result.should.have.property('access_token').not.equal(payload.access_token); + assertRedaction(result.access_token) + + result.should.have.property('other_field', payload.other_field); + }); + + it('should redact refresh_token in payload', function() { + // Arrange + const payload = { + refresh_token: 'refresh_token1234', + other_field: 'value' + }; + + // Act + const result = withRedactedPayload(payload); + + // Assert + result.should.have.property('refresh_token').not.equal(payload.refresh_token); + assertRedaction(result.refresh_token) + + result.should.have.property('other_field', payload.other_field); + }); + + it('should handle empty payload object', function() { + // Arrange + const payload = {}; + + // Act + const result = withRedactedPayload(payload); + + // Assert + result.should.be.an.Object(); + Object.keys(result).should.have.length(0); + }); + + it('handles undefined log object', () => { + const result = withRedactedPayload(); + + // Assert + result.should.be.an.Object(); + Object.keys(result).should.have.length(0); + }) + }); + + describe('withRedactedQueryParams', function() { + it('should redact sensitive query parameters', function() { + // Arrange + const params = { + code: 'auth_code1234', + client_id: 'client_id1234', + hash: 'hash1234', + refresh_token: 'refresh_token1234', + other_param: 'value' + }; + + // Act + const result = withRedactedQueryParams(params); + + // Assert + result.should.have.property('code').not.equal(params.code); + result.should.have.property('client_id').not.equal(params.client_id); + result.should.have.property('hash').not.equal(params.hash); + result.should.have.property('refresh_token').not.equal(params.refresh_token); + assertRedaction(result.code) + assertRedaction(result.client_id) + assertRedaction(result.hash) + assertRedaction(result.refresh_token) + + result.should.have.property('other_param', params.other_param); + }); + + it('should handle empty params object', function() { + // Arrange + const params = {}; + + // Act + const result = withRedactedQueryParams(params); + + // Assert + result.should.be.an.Object(); + Object.keys(result).should.have.length(0); + }); + it('handles undefined log object', () => { + const result = withRedactedQueryParams(); + + // Assert + result.should.be.an.Object(); + Object.keys(result).should.have.length(0); + }) + }); + + describe('getSanitizedUrlForLogs', function() { + it('should return url when no params exist', function() { + // Arrange + const requestConfig: AxiosRequestConfig = { + url: 'https://api.example.com/resource' + }; + + // Act + const result = getSanitizedUrlForLogs(requestConfig); + + // Assert + result.should.equal(requestConfig.url); + }); + + it('should append sanitized query parameters to url', function() { + // Arrange + const requestConfig: AxiosRequestConfig = { + url: 'https://api.example.com/resource', + params: { + code: 'auth_code1234', + other_param: 'value' + } + }; + + // Act + const result = getSanitizedUrlForLogs(requestConfig); + + // Assert + result.should.startWith(requestConfig.url + '?'); + result.should.containEql('other_param=value'); + result.should.not.containEql('auth_code'); + result.should.containEql('code=***'); + }); + + it('should handle empty url', function() { + // Arrange + const requestConfig: AxiosRequestConfig = { + params: { + code: 'auth_code1234' + } + }; + + // Act + const result = getSanitizedUrlForLogs(requestConfig); + + // Assert + // The result should start with a question mark and contain the code parameter + result.should.startWith('?'); + result.should.containEql('code=***'); + // It should not contain the original code value + result.should.not.containEql('auth_code1234'); + }); + + it('handles undefined request object', () => { + const newUrl = getSanitizedUrlForLogs({}); + newUrl.should.be.eql('') + }) + }); +}); diff --git a/test/client/types/ServerResponses.test.ts b/test/client/types/ServerResponses.test.ts new file mode 100644 index 0000000..420f71e --- /dev/null +++ b/test/client/types/ServerResponses.test.ts @@ -0,0 +1,50 @@ +import 'should'; +import { errorCodes, SmartsheetErrorResponseData } from '../../../lib/client/types/ServerResponses'; + +describe('ServerResponses types', function() { + describe('errorCodes', function() { + it('should have RATE_LIMIT value', function() { + errorCodes.RATE_LIMIT.should.equal(4001); + }); + + it('should have GATEWAY_TIMEOUT value', function() { + errorCodes.GATEWAY_TIMEOUT.should.equal(4002); + }); + + it('should have INTERNAL_SERVER_ERROR value', function() { + errorCodes.INTERNAL_SERVER_ERROR.should.equal(4003); + }); + + it('should have SERVICE_UNAVAILABLE value', function() { + errorCodes.SERVICE_UNAVAILABLE.should.equal(4004); + }); + }); + + describe('SmartsheetErrorResponseData', function() { + it('should be able to create an instance with required properties', function() { + // This is just a type test, not a runtime test + const errorData: SmartsheetErrorResponseData = { + errorCode: 4001, + message: 'Rate limit exceeded', + refId: 'ref-123', + detail: { additionalInfo: 'Some details' } + }; + + errorData.should.have.property('errorCode', 4001); + errorData.should.have.property('message', 'Rate limit exceeded'); + errorData.should.have.property('refId', 'ref-123'); + errorData.should.have.property('detail'); + (errorData.detail as any).should.have.property('additionalInfo', 'Some details'); + }); + + it('should allow additional properties', function() { + const errorData: SmartsheetErrorResponseData = { + errorCode: 4001, + message: 'Rate limit exceeded', + customProperty: 'custom value' + }; + + errorData.should.have.property('customProperty', 'custom value'); + }); + }); +}); \ No newline at end of file diff --git a/test/client/types/clientConfiguration.test.ts b/test/client/types/clientConfiguration.test.ts new file mode 100644 index 0000000..09998cc --- /dev/null +++ b/test/client/types/clientConfiguration.test.ts @@ -0,0 +1,65 @@ +import 'should'; +import { + ApiHost, + isSupportedLogLevel, + SUPPORTED_LOG_LEVELS, + DEFAULT_LOG_LEVEL, + DEFAULT_RETRY_CONFIG, + DEFAULT_LOGGING_CONFIG +} from '../../../lib/client/types/clientConfiguration'; + +describe('clientConfiguration types', function() { + describe('ApiHost', function() { + it('should have DEFAULT value', function() { + ApiHost.DEFAULT.should.equal('https://api.smartsheet.com/2.0/'); + }); + + it('should have GOV value', function() { + ApiHost.GOV.should.equal('https://api.smartsheetgov.com/2.0/'); + }); + + it('should have EU value', function() { + ApiHost.EU.should.equal('https://api.smartsheet.eu/2.0/'); + }); + }); + + describe('isSupportedLogLevel', function() { + it('should return true for supported log levels', function() { + SUPPORTED_LOG_LEVELS.forEach(level => { + isSupportedLogLevel(level).should.be.true(); + }); + }); + + it('should return false for unsupported log levels', function() { + isSupportedLogLevel('not-a-level').should.be.false(); + }); + }); + + describe('DEFAULT_LOG_LEVEL', function() { + it('should be a supported log level', function() { + SUPPORTED_LOG_LEVELS.should.containEql(DEFAULT_LOG_LEVEL); + }); + + it('should be "warn"', function() { + DEFAULT_LOG_LEVEL.should.equal('warn'); + }); + }); + + describe('DEFAULT_RETRY_CONFIG', function() { + it('should have maxRetries property', function() { + DEFAULT_RETRY_CONFIG.should.have.property('maxRetries'); + (DEFAULT_RETRY_CONFIG.maxRetries as number).should.be.a.Number(); + }); + }); + + describe('DEFAULT_LOGGING_CONFIG', function() { + it('should have logLevel property', function() { + DEFAULT_LOGGING_CONFIG.should.have.property('logLevel'); + (DEFAULT_LOGGING_CONFIG.logLevel as string).should.be.a.String(); + }); + + it('should use DEFAULT_LOG_LEVEL', function() { + (DEFAULT_LOGGING_CONFIG.logLevel as string).should.equal(DEFAULT_LOG_LEVEL); + }); + }); +}); diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..810e7a8 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "outDir": "./dist-test", + "allowJs": true, + "checkJs": true, + "target": "ES6", + "declaration": true, + "moduleResolution": "node16", + "resolveJsonModule": true, + "module": "Node16", + "esModuleInterop": true + }, + "include": ["./lib/**/*", "./test/**/*", "./index.ts", "./package.json"] +}