feat: Add sentry for task runner (no-changelog)

To make sure we capture exceptions from the task runner process.
This commit is contained in:
Tomi Turtiainen 2024-10-25 13:42:37 +03:00
parent c08d23c00f
commit e795d0bae7
12 changed files with 204 additions and 47 deletions

View file

@ -13,7 +13,11 @@
"N8N_RUNNERS_MAX_CONCURRENCY", "N8N_RUNNERS_MAX_CONCURRENCY",
"NODE_FUNCTION_ALLOW_BUILTIN", "NODE_FUNCTION_ALLOW_BUILTIN",
"NODE_FUNCTION_ALLOW_EXTERNAL", "NODE_FUNCTION_ALLOW_EXTERNAL",
"NODE_OPTIONS" "NODE_OPTIONS",
"N8N_SENTRY_DSN",
"N8N_VERSION",
"ENVIRONMENT",
"DEPLOYMENT_NAME"
], ],
"uid": 2000, "uid": 2000,
"gid": 2000 "gid": 2000

View file

@ -37,6 +37,8 @@
"@n8n/config": "workspace:*", "@n8n/config": "workspace:*",
"acorn": "8.14.0", "acorn": "8.14.0",
"acorn-walk": "8.3.4", "acorn-walk": "8.3.4",
"@sentry/integrations": "catalog:",
"@sentry/node": "catalog:",
"n8n-core": "workspace:*", "n8n-core": "workspace:*",
"n8n-workflow": "workspace:*", "n8n-workflow": "workspace:*",
"nanoid": "^3.3.6", "nanoid": "^3.3.6",

View file

@ -2,6 +2,7 @@ import { Config, Nested } from '@n8n/config';
import { BaseRunnerConfig } from './base-runner-config'; import { BaseRunnerConfig } from './base-runner-config';
import { JsRunnerConfig } from './js-runner-config'; import { JsRunnerConfig } from './js-runner-config';
import { SentryConfig } from './sentry-config';
@Config @Config
export class MainConfig { export class MainConfig {
@ -10,4 +11,7 @@ export class MainConfig {
@Nested @Nested
jsRunnerConfig!: JsRunnerConfig; jsRunnerConfig!: JsRunnerConfig;
@Nested
sentryConfig!: SentryConfig;
} }

View file

@ -0,0 +1,21 @@
import { Config, Env } from '@n8n/config';
@Config
export class SentryConfig {
/** Sentry DSN */
@Env('N8N_SENTRY_DSN')
sentryDsn: string = '';
//#region Metadata about the environment
@Env('N8N_VERSION')
n8nVersion: string = '';
@Env('ENVIRONMENT')
environment: string = '';
@Env('DEPLOYMENT_NAME')
deploymentName: string = '';
//#endregion
}

View file

@ -0,0 +1,90 @@
import { RewriteFrames } from '@sentry/integrations';
import { init, setTag, captureException, close } from '@sentry/node';
import * as a from 'assert/strict';
import { createHash } from 'crypto';
import { ApplicationError } from 'n8n-workflow';
import type { SentryConfig } from '@/config/sentry-config';
/**
* Handles error reporting using Sentry
*/
export class ErrorReporting {
private isInitialized = false;
private get dsn() {
return this.sentryConfig.sentryDsn;
}
constructor(private readonly sentryConfig: SentryConfig) {
a.ok(this.dsn, 'Sentry DSN is required to initialize Sentry');
}
async start() {
if (this.isInitialized) return;
// Collect longer stacktraces
Error.stackTraceLimit = 50;
process.on('uncaughtException', (error) => {
captureException(error);
});
const enabledIntegrations = [
'InboundFilters',
'FunctionToString',
'LinkedErrors',
'OnUnhandledRejection',
'ContextLines',
];
const seenErrors = new Set<string>();
init({
dsn: this.dsn,
release: this.sentryConfig.n8nVersion,
environment: this.sentryConfig.environment,
enableTracing: false,
serverName: this.sentryConfig.deploymentName,
beforeBreadcrumb: () => null,
integrations: (integrations) => [
...integrations.filter(({ name }) => enabledIntegrations.includes(name)),
new RewriteFrames({ root: process.cwd() }),
],
async beforeSend(event, { originalException }) {
if (!originalException) return null;
if (originalException instanceof Promise) {
originalException = await originalException.catch((error) => error as Error);
}
if (originalException instanceof ApplicationError) {
const { level, extra, tags } = originalException;
if (level === 'warning') return null;
event.level = level;
if (extra) event.extra = { ...event.extra, ...extra };
if (tags) event.tags = { ...event.tags, ...tags };
}
if (originalException instanceof Error && originalException.stack) {
const eventHash = createHash('sha1').update(originalException.stack).digest('base64');
if (seenErrors.has(eventHash)) return null;
seenErrors.add(eventHash);
}
return event;
},
});
setTag('server_type', 'task_runner');
this.isInitialized = true;
}
async stop() {
if (!this.isInitialized) {
return;
}
await close(1000);
}
}

View file

@ -36,6 +36,12 @@ describe('JsTaskRunner', () => {
...defaultConfig.jsRunnerConfig, ...defaultConfig.jsRunnerConfig,
...opts, ...opts,
}, },
sentryConfig: {
sentryDsn: '',
deploymentName: '',
environment: '',
n8nVersion: '',
},
}); });
const defaultTaskRunner = createRunnerWithOpts(); const defaultTaskRunner = createRunnerWithOpts();

View file

@ -2,10 +2,12 @@ import { ensureError } from 'n8n-workflow';
import Container from 'typedi'; import Container from 'typedi';
import { MainConfig } from './config/main-config'; import { MainConfig } from './config/main-config';
import type { ErrorReporting } from './error-reporting';
import { JsTaskRunner } from './js-task-runner/js-task-runner'; import { JsTaskRunner } from './js-task-runner/js-task-runner';
let runner: JsTaskRunner | undefined; let runner: JsTaskRunner | undefined;
let isShuttingDown = false; let isShuttingDown = false;
let errorReporting: ErrorReporting | undefined;
function createSignalHandler(signal: string) { function createSignalHandler(signal: string) {
return async function onSignal() { return async function onSignal() {
@ -21,10 +23,16 @@ function createSignalHandler(signal: string) {
await runner.stop(); await runner.stop();
runner = undefined; runner = undefined;
} }
if (errorReporting) {
await errorReporting.stop();
errorReporting = undefined;
}
} catch (e) { } catch (e) {
const error = ensureError(e); const error = ensureError(e);
console.error('Error stopping task runner', { error }); console.error('Error stopping task runner', { error });
} finally { } finally {
console.log('Task runner stopped');
process.exit(0); process.exit(0);
} }
}; };
@ -33,6 +41,12 @@ function createSignalHandler(signal: string) {
void (async function start() { void (async function start() {
const config = Container.get(MainConfig); const config = Container.get(MainConfig);
if (config.sentryConfig.sentryDsn) {
const { ErrorReporting } = await import('@/error-reporting');
errorReporting = new ErrorReporting(config.sentryConfig);
await errorReporting.start();
}
runner = new JsTaskRunner(config); runner = new JsTaskRunner(config);
process.on('SIGINT', createSignalHandler('SIGINT')); process.on('SIGINT', createSignalHandler('SIGINT'));

View file

@ -97,8 +97,8 @@
"@n8n_io/license-sdk": "2.13.1", "@n8n_io/license-sdk": "2.13.1",
"@oclif/core": "4.0.7", "@oclif/core": "4.0.7",
"@rudderstack/rudder-sdk-node": "2.0.9", "@rudderstack/rudder-sdk-node": "2.0.9",
"@sentry/integrations": "7.87.0", "@sentry/integrations": "catalog:",
"@sentry/node": "7.87.0", "@sentry/node": "catalog:",
"aws4": "1.11.0", "aws4": "1.11.0",
"axios": "catalog:", "axios": "catalog:",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",

View file

@ -46,9 +46,15 @@ describe('TaskRunnerProcess', () => {
taskRunnerProcess = new TaskRunnerProcess(logger, runnerConfig, authService); taskRunnerProcess = new TaskRunnerProcess(logger, runnerConfig, authService);
}); });
test.each(['PATH', 'NODE_FUNCTION_ALLOW_BUILTIN', 'NODE_FUNCTION_ALLOW_EXTERNAL'])( test.each([
'should propagate %s from env as is', 'PATH',
async (envVar) => { 'NODE_FUNCTION_ALLOW_BUILTIN',
'NODE_FUNCTION_ALLOW_EXTERNAL',
'N8N_SENTRY_DSN',
'N8N_VERSION',
'ENVIRONMENT',
'DEPLOYMENT_NAME',
])('should propagate %s from env as is', async (envVar) => {
jest.spyOn(authService, 'createGrantToken').mockResolvedValue('grantToken'); jest.spyOn(authService, 'createGrantToken').mockResolvedValue('grantToken');
process.env[envVar] = 'custom value'; process.env[envVar] = 'custom value';
@ -61,8 +67,7 @@ describe('TaskRunnerProcess', () => {
[envVar]: 'custom value', [envVar]: 'custom value',
}), }),
); );
}, });
);
it('should pass NODE_OPTIONS env if maxOldSpaceSize is configured', async () => { it('should pass NODE_OPTIONS env if maxOldSpaceSize is configured', async () => {
jest.spyOn(authService, 'createGrantToken').mockResolvedValue('grantToken'); jest.spyOn(authService, 'createGrantToken').mockResolvedValue('grantToken');

View file

@ -59,6 +59,11 @@ export class TaskRunnerProcess extends TypedEmitter<TaskRunnerProcessEventMap> {
'PATH', 'PATH',
'NODE_FUNCTION_ALLOW_BUILTIN', 'NODE_FUNCTION_ALLOW_BUILTIN',
'NODE_FUNCTION_ALLOW_EXTERNAL', 'NODE_FUNCTION_ALLOW_EXTERNAL',
'N8N_SENTRY_DSN',
// Metadata about the environment
'N8N_VERSION',
'ENVIRONMENT',
'DEPLOYMENT_NAME',
] as const; ] as const;
constructor( constructor(

View file

@ -9,6 +9,12 @@ catalogs:
'@langchain/core': '@langchain/core':
specifier: 0.3.15 specifier: 0.3.15
version: 0.3.15 version: 0.3.15
'@sentry/integrations':
specifier: 7.87.0
version: 7.87.0
'@sentry/node':
specifier: 7.87.0
version: 7.87.0
'@types/basic-auth': '@types/basic-auth':
specifier: ^1.1.3 specifier: ^1.1.3
version: 1.1.3 version: 1.1.3
@ -265,7 +271,7 @@ importers:
version: 4.0.7 version: 4.0.7
axios: axios:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.7.4 version: 1.7.4(debug@4.3.7)
dotenv: dotenv:
specifier: 8.6.0 specifier: 8.6.0
version: 8.6.0 version: 8.6.0
@ -333,7 +339,7 @@ importers:
dependencies: dependencies:
axios: axios:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.7.4 version: 1.7.4(debug@4.3.7)
packages/@n8n/codemirror-lang: packages/@n8n/codemirror-lang:
dependencies: dependencies:
@ -407,7 +413,7 @@ importers:
version: 3.666.0(@aws-sdk/client-sts@3.666.0) version: 3.666.0(@aws-sdk/client-sts@3.666.0)
'@getzep/zep-cloud': '@getzep/zep-cloud':
specifier: 1.0.12 specifier: 1.0.12
version: 1.0.12(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.5(7umjwzmwnymi4lyinuvazmp6ki)) version: 1.0.12(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.5(4ubssgvn2k3t3hxnzmxuoc2aja))
'@getzep/zep-js': '@getzep/zep-js':
specifier: 0.9.0 specifier: 0.9.0
version: 0.9.0 version: 0.9.0
@ -434,7 +440,7 @@ importers:
version: 0.3.1(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) version: 0.3.1(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)
'@langchain/community': '@langchain/community':
specifier: 0.3.11 specifier: 0.3.11
version: 0.3.11(simkpjwqw7qnwbripe37u5qu7a) version: 0.3.11(tzffvezibmkr4px5bpuitcp7xu)
'@langchain/core': '@langchain/core':
specifier: 'catalog:' specifier: 'catalog:'
version: 0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)) version: 0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8))
@ -521,7 +527,7 @@ importers:
version: 23.0.1 version: 23.0.1
langchain: langchain:
specifier: 0.3.5 specifier: 0.3.5
version: 0.3.5(7umjwzmwnymi4lyinuvazmp6ki) version: 0.3.5(4ubssgvn2k3t3hxnzmxuoc2aja)
lodash: lodash:
specifier: 'catalog:' specifier: 'catalog:'
version: 4.17.21 version: 4.17.21
@ -642,6 +648,12 @@ importers:
'@n8n/config': '@n8n/config':
specifier: workspace:* specifier: workspace:*
version: link:../config version: link:../config
'@sentry/integrations':
specifier: 'catalog:'
version: 7.87.0
'@sentry/node':
specifier: 'catalog:'
version: 7.87.0
acorn: acorn:
specifier: 8.14.0 specifier: 8.14.0
version: 8.14.0 version: 8.14.0
@ -764,17 +776,17 @@ importers:
specifier: 2.0.9 specifier: 2.0.9
version: 2.0.9(tslib@2.6.2) version: 2.0.9(tslib@2.6.2)
'@sentry/integrations': '@sentry/integrations':
specifier: 7.87.0 specifier: 'catalog:'
version: 7.87.0 version: 7.87.0
'@sentry/node': '@sentry/node':
specifier: 7.87.0 specifier: 'catalog:'
version: 7.87.0 version: 7.87.0
aws4: aws4:
specifier: 1.11.0 specifier: 1.11.0
version: 1.11.0 version: 1.11.0
axios: axios:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.7.4 version: 1.7.4(debug@4.3.7)
bcryptjs: bcryptjs:
specifier: 2.4.3 specifier: 2.4.3
version: 2.4.3 version: 2.4.3
@ -1105,7 +1117,7 @@ importers:
version: 1.11.0 version: 1.11.0
axios: axios:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.7.4 version: 1.7.4(debug@4.3.7)
concat-stream: concat-stream:
specifier: 2.0.0 specifier: 2.0.0
version: 2.0.0 version: 2.0.0
@ -1395,7 +1407,7 @@ importers:
version: 10.11.0(vue@3.5.11(typescript@5.6.2)) version: 10.11.0(vue@3.5.11(typescript@5.6.2))
axios: axios:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.7.4 version: 1.7.4(debug@4.3.7)
bowser: bowser:
specifier: 2.11.0 specifier: 2.11.0
version: 2.11.0 version: 2.11.0
@ -1872,7 +1884,7 @@ importers:
version: 0.15.2 version: 0.15.2
axios: axios:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.7.4 version: 1.7.4(debug@4.3.7)
callsites: callsites:
specifier: 3.1.0 specifier: 3.1.0
version: 3.1.0 version: 3.1.0
@ -14066,7 +14078,7 @@ snapshots:
'@gar/promisify@1.1.3': '@gar/promisify@1.1.3':
optional: true optional: true
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.5(7umjwzmwnymi4lyinuvazmp6ki))': '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.5(4ubssgvn2k3t3hxnzmxuoc2aja))':
dependencies: dependencies:
form-data: 4.0.0 form-data: 4.0.0
node-fetch: 2.7.0(encoding@0.1.13) node-fetch: 2.7.0(encoding@0.1.13)
@ -14075,7 +14087,7 @@ snapshots:
zod: 3.23.8 zod: 3.23.8
optionalDependencies: optionalDependencies:
'@langchain/core': 0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)) '@langchain/core': 0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8))
langchain: 0.3.5(7umjwzmwnymi4lyinuvazmp6ki) langchain: 0.3.5(4ubssgvn2k3t3hxnzmxuoc2aja)
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
@ -14542,7 +14554,7 @@ snapshots:
- aws-crt - aws-crt
- encoding - encoding
'@langchain/community@0.3.11(simkpjwqw7qnwbripe37u5qu7a)': '@langchain/community@0.3.11(tzffvezibmkr4px5bpuitcp7xu)':
dependencies: dependencies:
'@ibm-cloud/watsonx-ai': 1.1.2 '@ibm-cloud/watsonx-ai': 1.1.2
'@langchain/core': 0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)) '@langchain/core': 0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8))
@ -14552,7 +14564,7 @@ snapshots:
flat: 5.0.2 flat: 5.0.2
ibm-cloud-sdk-core: 5.1.0 ibm-cloud-sdk-core: 5.1.0
js-yaml: 4.1.0 js-yaml: 4.1.0
langchain: 0.3.5(7umjwzmwnymi4lyinuvazmp6ki) langchain: 0.3.5(4ubssgvn2k3t3hxnzmxuoc2aja)
langsmith: 0.2.3(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)) langsmith: 0.2.3(openai@4.69.0(encoding@0.1.13)(zod@3.23.8))
uuid: 10.0.0 uuid: 10.0.0
zod: 3.23.8 zod: 3.23.8
@ -14565,7 +14577,7 @@ snapshots:
'@aws-sdk/client-s3': 3.666.0 '@aws-sdk/client-s3': 3.666.0
'@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0) '@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0)
'@azure/storage-blob': 12.18.0(encoding@0.1.13) '@azure/storage-blob': 12.18.0(encoding@0.1.13)
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.5(7umjwzmwnymi4lyinuvazmp6ki)) '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.5(4ubssgvn2k3t3hxnzmxuoc2aja))
'@getzep/zep-js': 0.9.0 '@getzep/zep-js': 0.9.0
'@google-ai/generativelanguage': 2.6.0(encoding@0.1.13) '@google-ai/generativelanguage': 2.6.0(encoding@0.1.13)
'@google-cloud/storage': 7.12.1(encoding@0.1.13) '@google-cloud/storage': 7.12.1(encoding@0.1.13)
@ -15278,7 +15290,7 @@ snapshots:
'@rudderstack/rudder-sdk-node@2.0.9(tslib@2.6.2)': '@rudderstack/rudder-sdk-node@2.0.9(tslib@2.6.2)':
dependencies: dependencies:
axios: 1.7.4 axios: 1.7.4(debug@4.3.7)
axios-retry: 3.7.0 axios-retry: 3.7.0
component-type: 1.2.1 component-type: 1.2.1
join-component: 1.1.0 join-component: 1.1.0
@ -17534,17 +17546,9 @@ snapshots:
'@babel/runtime': 7.24.7 '@babel/runtime': 7.24.7
is-retry-allowed: 2.2.0 is-retry-allowed: 2.2.0
axios@1.7.4:
dependencies:
follow-redirects: 1.15.6(debug@4.3.6)
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
axios@1.7.4(debug@4.3.7): axios@1.7.4(debug@4.3.7):
dependencies: dependencies:
follow-redirects: 1.15.6(debug@4.3.7) follow-redirects: 1.15.6(debug@4.3.6)
form-data: 4.0.0 form-data: 4.0.0
proxy-from-env: 1.1.0 proxy-from-env: 1.1.0
transitivePeerDependencies: transitivePeerDependencies:
@ -19899,7 +19903,7 @@ snapshots:
gaxios@6.6.0(encoding@0.1.13): gaxios@6.6.0(encoding@0.1.13):
dependencies: dependencies:
extend: 3.0.2 extend: 3.0.2
https-proxy-agent: 7.0.2 https-proxy-agent: 7.0.5
is-stream: 2.0.1 is-stream: 2.0.1
node-fetch: 2.7.0(encoding@0.1.13) node-fetch: 2.7.0(encoding@0.1.13)
uuid: 9.0.1 uuid: 9.0.1
@ -21378,7 +21382,7 @@ snapshots:
kuler@2.0.0: {} kuler@2.0.0: {}
langchain@0.3.5(7umjwzmwnymi4lyinuvazmp6ki): langchain@0.3.5(4ubssgvn2k3t3hxnzmxuoc2aja):
dependencies: dependencies:
'@langchain/core': 0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)) '@langchain/core': 0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8))
'@langchain/openai': 0.3.11(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) '@langchain/openai': 0.3.11(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)
@ -21402,7 +21406,7 @@ snapshots:
'@langchain/groq': 0.1.2(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) '@langchain/groq': 0.1.2(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)
'@langchain/mistralai': 0.1.1(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) '@langchain/mistralai': 0.1.1(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)
'@langchain/ollama': 0.1.1(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8))) '@langchain/ollama': 0.1.1(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))
axios: 1.7.4 axios: 1.7.4(debug@4.3.7)
cheerio: 1.0.0 cheerio: 1.0.0
handlebars: 4.7.8 handlebars: 4.7.8
transitivePeerDependencies: transitivePeerDependencies:

View file

@ -5,6 +5,8 @@ packages:
- cypress - cypress
catalog: catalog:
'@sentry/integrations': 7.87.0
'@sentry/node': 7.87.0
'@types/basic-auth': ^1.1.3 '@types/basic-auth': ^1.1.3
'@types/express': ^4.17.21 '@types/express': ^4.17.21
'@types/lodash': ^4.14.195 '@types/lodash': ^4.14.195