diff --git a/CHANGELOG.md b/CHANGELOG.md index 194df106aedda..2b3291495cbd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## [1.37.3](https://github.com/n8n-io/n8n/compare/n8n@1.37.2...n8n@1.37.3) (2024-04-18) + + +### Bug Fixes + +* **core:** Don't create multiple owners when importing credentials or workflows ([#9112](https://github.com/n8n-io/n8n/issues/9112)) ([32db869](https://github.com/n8n-io/n8n/commit/32db869ef45f674c1ec9a8dbabe28c4545192792)) +* **core:** Exclude oAuth callback urls from browser-id checks ([#9158](https://github.com/n8n-io/n8n/issues/9158)) ([58b6a9d](https://github.com/n8n-io/n8n/commit/58b6a9d4aeebfb7ff93bff275af2d558b3e67046)) +* **core:** Improve browserId checks, and add logging ([#9161](https://github.com/n8n-io/n8n/issues/9161)) ([cff50fb](https://github.com/n8n-io/n8n/commit/cff50fb59dc6800f0fbbc4804262be8aaa78beef)) +* Fix issue with Crowdstrike credential not working correctly ([#9108](https://github.com/n8n-io/n8n/issues/9108)) ([d11ca79](https://github.com/n8n-io/n8n/commit/d11ca79adbaf7cba86213723be92b7da769c89f5)) + + + ## [1.37.2](https://github.com/n8n-io/n8n/compare/n8n@1.37.1...n8n@1.37.2) (2024-04-17) ### Bug Fixes diff --git a/package.json b/package.json index a8779d1c0255d..02b57cfaadf37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-monorepo", - "version": "1.37.2", + "version": "1.37.3", "private": true, "homepage": "https://n8n.io/", "engines": { @@ -94,6 +94,7 @@ "@sentry/cli@2.17.0": "patches/@sentry__cli@2.17.0.patch", "pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch", "pyodide@0.23.4": "patches/pyodide@0.23.4.patch", + "@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch", "@types/ws@8.5.4": "patches/@types__ws@8.5.4.patch", "vite-plugin-checker@0.6.4": "patches/vite-plugin-checker@0.6.4.patch" } diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index cddde47f1575d..5c57e355df444 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/n8n-nodes-langchain", - "version": "1.37.2", + "version": "1.37.3", "description": "", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io/", @@ -118,7 +118,7 @@ "devDependencies": { "@aws-sdk/types": "3.357.0", "@types/basic-auth": "^1.1.3", - "@types/express": "^4.17.6", + "@types/express": "^4.17.21", "@types/html-to-text": "^9.0.1", "@types/json-schema": "^7.0.15", "@types/temp": "^0.9.1", @@ -140,7 +140,7 @@ "@langchain/openai": "^0.0.16", "@langchain/pinecone": "^0.0.3", "@langchain/redis": "^0.0.2", - "@n8n/typeorm": "0.3.20-7", + "@n8n/typeorm": "0.3.20-8", "@n8n/vm2": "3.9.20", "@pinecone-database/pinecone": "2.1.0", "@qdrant/js-client-rest": "1.7.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index d7fa5e1ac33ad..83a4b469930a2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "1.37.2", + "version": "1.37.3", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io/", @@ -98,7 +98,7 @@ "@n8n/localtunnel": "2.1.0", "@n8n/n8n-nodes-langchain": "workspace:*", "@n8n/permissions": "workspace:*", - "@n8n/typeorm": "0.3.20-7", + "@n8n/typeorm": "0.3.20-8", "@n8n_io/license-sdk": "2.10.0", "@oclif/core": "3.18.1", "@rudderstack/rudder-sdk-node": "2.0.7", diff --git a/packages/cli/src/auth/auth.service.ts b/packages/cli/src/auth/auth.service.ts index f8e332cbfe781..636c5a27abff0 100644 --- a/packages/cli/src/auth/auth.service.ts +++ b/packages/cli/src/auth/auth.service.ts @@ -41,7 +41,11 @@ const skipBrowserIdCheckEndpoints = [ `/${restEndpoint}/push`, // We need to exclude binary-data downloading endpoint because we can't send custom headers on `` tags - `/${restEndpoint}/binary-data`, + `/${restEndpoint}/binary-data/`, + + // oAuth callback urls aren't called by the frontend. therefore we can't send custom header on these requests + `/${restEndpoint}/oauth1-credential/callback`, + `/${restEndpoint}/oauth2-credential/callback`, ]; @Service() @@ -127,12 +131,20 @@ export class AuthService { // or, If the user has been deactivated (i.e. LDAP users) user.disabled || // or, If the email or password has been updated - jwtPayload.hash !== this.createJWTHash(user) || - // If the token was issued for another browser session - (!skipBrowserIdCheckEndpoints.includes(req.baseUrl) && - jwtPayload.browserId && - (!req.browserId || jwtPayload.browserId !== this.hash(req.browserId))) + jwtPayload.hash !== this.createJWTHash(user) + ) { + throw new AuthError('Unauthorized'); + } + + // Check if the token was issued for another browser session, ignoring the endpoints that can't send custom headers + const endpoint = req.route ? `${req.baseUrl}${req.route.path}` : req.baseUrl; + if (req.method === 'GET' && skipBrowserIdCheckEndpoints.includes(endpoint)) { + this.logger.debug(`Skipped browserId check on ${endpoint}`); + } else if ( + jwtPayload.browserId && + (!req.browserId || jwtPayload.browserId !== this.hash(req.browserId)) ) { + this.logger.warn(`browserId check failed on ${endpoint}`); throw new AuthError('Unauthorized'); } diff --git a/packages/cli/src/commands/import/credentials.ts b/packages/cli/src/commands/import/credentials.ts index 2ff971b2d2826..52440e1149bf7 100644 --- a/packages/cli/src/commands/import/credentials.ts +++ b/packages/cli/src/commands/import/credentials.ts @@ -64,67 +64,25 @@ export class ImportCredentialsCommand extends BaseCommand { } } - let totalImported = 0; - - const cipher = Container.get(Cipher); const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner(); - if (flags.separate) { - let { input: inputPath } = flags; - - if (process.platform === 'win32') { - inputPath = inputPath.replace(/\\/g, '/'); - } - - const files = await glob('*.json', { - cwd: inputPath, - absolute: true, - }); - - totalImported = files.length; - - await Db.getConnection().transaction(async (transactionManager) => { - this.transactionManager = transactionManager; - for (const file of files) { - const credential = jsonParse( - fs.readFileSync(file, { encoding: 'utf8' }), - ); - if (typeof credential.data === 'object') { - // plain data / decrypted input. Should be encrypted first. - credential.data = cipher.encrypt(credential.data); - } - await this.storeCredential(credential, user); - } - }); + const credentials = await this.readCredentials(flags.input, flags.separate); - this.reportSuccess(totalImported); - return; - } - - const credentials = jsonParse( - fs.readFileSync(flags.input, { encoding: 'utf8' }), - ); + await Db.getConnection().transaction(async (transactionManager) => { + this.transactionManager = transactionManager; - totalImported = credentials.length; + const result = await this.checkRelations(credentials, flags.userId); - if (!Array.isArray(credentials)) { - throw new ApplicationError( - 'File does not seem to contain credentials. Make sure the credentials are contained in an array.', - ); - } + if (!result.success) { + throw new ApplicationError(result.message); + } - await Db.getConnection().transaction(async (transactionManager) => { - this.transactionManager = transactionManager; for (const credential of credentials) { - if (typeof credential.data === 'object') { - // plain data / decrypted input. Should be encrypted first. - credential.data = cipher.encrypt(credential.data); - } await this.storeCredential(credential, user); } }); - this.reportSuccess(totalImported); + this.reportSuccess(credentials.length); } async catch(error: Error) { @@ -142,15 +100,23 @@ export class ImportCredentialsCommand extends BaseCommand { private async storeCredential(credential: Partial, user: User) { const result = await this.transactionManager.upsert(CredentialsEntity, credential, ['id']); - await this.transactionManager.upsert( - SharedCredentials, - { - credentialsId: result.identifiers[0].id as string, - userId: user.id, - role: 'credential:owner', - }, - ['credentialsId', 'userId'], - ); + + const sharingExists = await this.transactionManager.existsBy(SharedCredentials, { + credentialsId: credential.id, + role: 'credential:owner', + }); + + if (!sharingExists) { + await this.transactionManager.upsert( + SharedCredentials, + { + credentialsId: result.identifiers[0].id as string, + userId: user.id, + role: 'credential:owner', + }, + ['credentialsId', 'userId'], + ); + } } private async getOwner() { @@ -162,6 +128,84 @@ export class ImportCredentialsCommand extends BaseCommand { return owner; } + private async checkRelations(credentials: ICredentialsEncrypted[], userId?: string) { + if (!userId) { + return { + success: true as const, + message: undefined, + }; + } + + for (const credential of credentials) { + if (credential.id === undefined) { + continue; + } + + if (!(await this.credentialExists(credential.id))) { + continue; + } + + const ownerId = await this.getCredentialOwner(credential.id); + if (!ownerId) { + continue; + } + + if (ownerId !== userId) { + return { + success: false as const, + message: `The credential with id "${credential.id}" is already owned by the user with the id "${ownerId}". It can't be re-owned by the user with the id "${userId}"`, + }; + } + } + + return { + success: true as const, + message: undefined, + }; + } + + private async readCredentials(path: string, separate: boolean): Promise { + const cipher = Container.get(Cipher); + + if (process.platform === 'win32') { + path = path.replace(/\\/g, '/'); + } + + let credentials: ICredentialsEncrypted[]; + + if (separate) { + const files = await glob('*.json', { + cwd: path, + absolute: true, + }); + + credentials = files.map((file) => + jsonParse(fs.readFileSync(file, { encoding: 'utf8' })), + ); + } else { + const credentialsUnchecked = jsonParse( + fs.readFileSync(path, { encoding: 'utf8' }), + ); + + if (!Array.isArray(credentialsUnchecked)) { + throw new ApplicationError( + 'File does not seem to contain credentials. Make sure the credentials are contained in an array.', + ); + } + + credentials = credentialsUnchecked; + } + + return credentials.map((credential) => { + if (typeof credential.data === 'object') { + // plain data / decrypted input. Should be encrypted first. + credential.data = cipher.encrypt(credential.data); + } + + return credential; + }); + } + private async getAssignee(userId: string) { const user = await Container.get(UserRepository).findOneBy({ id: userId }); @@ -171,4 +215,17 @@ export class ImportCredentialsCommand extends BaseCommand { return user; } + + private async getCredentialOwner(credentialsId: string) { + const sharedCredential = await this.transactionManager.findOneBy(SharedCredentials, { + credentialsId, + role: 'credential:owner', + }); + + return sharedCredential?.userId; + } + + private async credentialExists(credentialId: string) { + return await this.transactionManager.existsBy(CredentialsEntity, { id: credentialId }); + } } diff --git a/packages/cli/src/commands/import/workflow.ts b/packages/cli/src/commands/import/workflow.ts index 21c3d82501cc4..40404482fb455 100644 --- a/packages/cli/src/commands/import/workflow.ts +++ b/packages/cli/src/commands/import/workflow.ts @@ -13,6 +13,7 @@ import { WorkflowRepository } from '@db/repositories/workflow.repository'; import type { IWorkflowToImport } from '@/Interfaces'; import { ImportService } from '@/services/import.service'; import { BaseCommand } from '../BaseCommand'; +import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] { if (!Array.isArray(workflows)) { @@ -78,53 +79,52 @@ export class ImportWorkflowsCommand extends BaseCommand { } } - const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner(); + const owner = await this.getOwner(); - let totalImported = 0; + const workflows = await this.readWorkflows(flags.input, flags.separate); - if (flags.separate) { - let { input: inputPath } = flags; + const result = await this.checkRelations(workflows, flags.userId); + if (!result.success) { + throw new ApplicationError(result.message); + } - if (process.platform === 'win32') { - inputPath = inputPath.replace(/\\/g, '/'); - } + this.logger.info(`Importing ${workflows.length} workflows...`); - const files = await glob('*.json', { - cwd: inputPath, - absolute: true, - }); + await Container.get(ImportService).importWorkflows(workflows, flags.userId ?? owner.id); - totalImported = files.length; - this.logger.info(`Importing ${totalImported} workflows...`); + this.reportSuccess(workflows.length); + } - for (const file of files) { - const workflow = jsonParse(fs.readFileSync(file, { encoding: 'utf8' })); - if (!workflow.id) { - workflow.id = generateNanoId(); - } + private async checkRelations(workflows: WorkflowEntity[], userId: string | undefined) { + if (!userId) { + return { + success: true as const, + message: undefined, + }; + } - const _workflow = Container.get(WorkflowRepository).create(workflow); + for (const workflow of workflows) { + if (!(await this.workflowExists(workflow))) { + continue; + } - await Container.get(ImportService).importWorkflows([_workflow], user.id); + const ownerId = await this.getWorkflowOwner(workflow); + if (!ownerId) { + continue; } - this.reportSuccess(totalImported); - process.exit(); + if (ownerId !== userId) { + return { + success: false as const, + message: `The credential with id "${workflow.id}" is already owned by the user with the id "${ownerId}". It can't be re-owned by the user with the id "${userId}"`, + }; + } } - const workflows = jsonParse( - fs.readFileSync(flags.input, { encoding: 'utf8' }), - ); - - const _workflows = workflows.map((w) => Container.get(WorkflowRepository).create(w)); - - assertHasWorkflowsToImport(workflows); - - totalImported = workflows.length; - - await Container.get(ImportService).importWorkflows(_workflows, user.id); - - this.reportSuccess(totalImported); + return { + success: true as const, + message: undefined, + }; } async catch(error: Error) { @@ -145,13 +145,48 @@ export class ImportWorkflowsCommand extends BaseCommand { return owner; } - private async getAssignee(userId: string) { - const user = await Container.get(UserRepository).findOneBy({ id: userId }); + private async getWorkflowOwner(workflow: WorkflowEntity) { + const sharing = await Container.get(SharedWorkflowRepository).findOneBy({ + workflowId: workflow.id, + role: 'workflow:owner', + }); + + return sharing?.userId; + } + + private async workflowExists(workflow: WorkflowEntity) { + return await Container.get(WorkflowRepository).existsBy({ id: workflow.id }); + } - if (!user) { - throw new ApplicationError('Failed to find user', { extra: { userId } }); + private async readWorkflows(path: string, separate: boolean): Promise { + if (process.platform === 'win32') { + path = path.replace(/\\/g, '/'); } - return user; + if (separate) { + const files = await glob('*.json', { + cwd: path, + absolute: true, + }); + const workflowInstances = files.map((file) => { + const workflow = jsonParse(fs.readFileSync(file, { encoding: 'utf8' })); + if (!workflow.id) { + workflow.id = generateNanoId(); + } + + const workflowInstance = Container.get(WorkflowRepository).create(workflow); + + return workflowInstance; + }); + + return workflowInstances; + } else { + const workflows = jsonParse(fs.readFileSync(path, { encoding: 'utf8' })); + + const workflowInstances = workflows.map((w) => Container.get(WorkflowRepository).create(w)); + assertHasWorkflowsToImport(workflows); + + return workflowInstances; + } } } diff --git a/packages/cli/src/services/import.service.ts b/packages/cli/src/services/import.service.ts index 32f6894f9b844..11b726225663f 100644 --- a/packages/cli/src/services/import.service.ts +++ b/packages/cli/src/services/import.service.ts @@ -53,14 +53,18 @@ export class ImportService { this.logger.info(`Deactivating workflow "${workflow.name}". Remember to activate later.`); } - const upsertResult = await tx.upsert(WorkflowEntity, workflow, ['id']); + const exists = workflow.id ? await tx.existsBy(WorkflowEntity, { id: workflow.id }) : false; + const upsertResult = await tx.upsert(WorkflowEntity, workflow, ['id']); const workflowId = upsertResult.identifiers.at(0)?.id as string; - await tx.upsert(SharedWorkflow, { workflowId, userId, role: 'workflow:owner' }, [ - 'workflowId', - 'userId', - ]); + // Create relationship if the workflow was inserted instead of updated. + if (!exists) { + await tx.upsert(SharedWorkflow, { workflowId, userId, role: 'workflow:owner' }, [ + 'workflowId', + 'userId', + ]); + } if (!workflow.tags?.length) continue; diff --git a/packages/cli/test/integration/commands/credentials.cmd.test.ts b/packages/cli/test/integration/commands/credentials.cmd.test.ts index 1ec68f72633cd..730a0cd5dd1da 100644 --- a/packages/cli/test/integration/commands/credentials.cmd.test.ts +++ b/packages/cli/test/integration/commands/credentials.cmd.test.ts @@ -6,10 +6,17 @@ import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { mockInstance } from '../../shared/mocking'; import * as testDb from '../shared/testDb'; -import { getAllCredentials } from '../shared/db/credentials'; +import { getAllCredentials, getAllSharedCredentials } from '../shared/db/credentials'; +import { createMember, createOwner } from '../shared/db/users'; const oclifConfig = new Config({ root: __dirname }); +async function importCredential(argv: string[]) { + const importer = new ImportCredentialsCommand(argv, oclifConfig); + await importer.init(); + await importer.run(); +} + beforeAll(async () => { mockInstance(InternalHooks); mockInstance(LoadNodesAndCredentials); @@ -17,7 +24,7 @@ beforeAll(async () => { }); beforeEach(async () => { - await testDb.truncate(['Credentials']); + await testDb.truncate(['Credentials', 'SharedCredentials', 'User']); }); afterAll(async () => { @@ -25,25 +32,202 @@ afterAll(async () => { }); test('import:credentials should import a credential', async () => { - const before = await getAllCredentials(); - expect(before.length).toBe(0); - const importer = new ImportCredentialsCommand( - ['--input=./test/integration/commands/importCredentials/credentials.json'], - oclifConfig, + // + // ARRANGE + // + const owner = await createOwner(); + + // + // ACT + // + await importCredential([ + '--input=./test/integration/commands/importCredentials/credentials.json', + ]); + + // + // ASSERT + // + const after = { + credentials: await getAllCredentials(), + sharings: await getAllSharedCredentials(), + }; + expect(after).toMatchObject({ + credentials: [expect.objectContaining({ id: '123', name: 'cred-aws-test' })], + sharings: [ + expect.objectContaining({ credentialsId: '123', userId: owner.id, role: 'credential:owner' }), + ], + }); +}); + +test('import:credentials should import a credential from separated files', async () => { + // + // ARRANGE + // + const owner = await createOwner(); + + // + // ACT + // + // import credential the first time, assigning it to the owner + await importCredential([ + '--separate', + '--input=./test/integration/commands/importCredentials/separate', + ]); + + // + // ASSERT + // + const after = { + credentials: await getAllCredentials(), + sharings: await getAllSharedCredentials(), + }; + + expect(after).toMatchObject({ + credentials: [ + expect.objectContaining({ + id: '123', + name: 'cred-aws-test', + }), + ], + sharings: [ + expect.objectContaining({ + credentialsId: '123', + userId: owner.id, + role: 'credential:owner', + }), + ], + }); +}); + +test('`import:credentials --userId ...` should fail if the credential exists already and is owned by somebody else', async () => { + // + // ARRANGE + // + const owner = await createOwner(); + const member = await createMember(); + + // import credential the first time, assigning it to the owner + await importCredential([ + '--input=./test/integration/commands/importCredentials/credentials.json', + `--userId=${owner.id}`, + ]); + + // making sure the import worked + const before = { + credentials: await getAllCredentials(), + sharings: await getAllSharedCredentials(), + }; + expect(before).toMatchObject({ + credentials: [expect.objectContaining({ id: '123', name: 'cred-aws-test' })], + sharings: [ + expect.objectContaining({ + credentialsId: '123', + userId: owner.id, + role: 'credential:owner', + }), + ], + }); + + // + // ACT + // + + // Import again while updating the name we try to assign the + // credential to another user. + await expect( + importCredential([ + '--input=./test/integration/commands/importCredentials/credentials-updated.json', + `--userId=${member.id}`, + ]), + ).rejects.toThrowError( + `The credential with id "123" is already owned by the user with the id "${owner.id}". It can't be re-owned by the user with the id "${member.id}"`, ); - const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { - throw new Error('process.exit'); + + // + // ASSERT + // + const after = { + credentials: await getAllCredentials(), + sharings: await getAllSharedCredentials(), + }; + + expect(after).toMatchObject({ + credentials: [ + expect.objectContaining({ + id: '123', + // only the name was updated + name: 'cred-aws-test', + }), + ], + sharings: [ + expect.objectContaining({ + credentialsId: '123', + userId: owner.id, + role: 'credential:owner', + }), + ], }); +}); - await importer.init(); - try { - await importer.run(); - } catch (error) { - expect(error.message).toBe('process.exit'); - } - const after = await getAllCredentials(); - expect(after.length).toBe(1); - expect(after[0].name).toBe('cred-aws-test'); - expect(after[0].id).toBe('123'); - mockExit.mockRestore(); +test("only update credential, don't create or update owner if `--userId` is not passed", async () => { + // + // ARRANGE + // + await createOwner(); + const member = await createMember(); + + // import credential the first time, assigning it to a member + await importCredential([ + '--input=./test/integration/commands/importCredentials/credentials.json', + `--userId=${member.id}`, + ]); + + // making sure the import worked + const before = { + credentials: await getAllCredentials(), + sharings: await getAllSharedCredentials(), + }; + expect(before).toMatchObject({ + credentials: [expect.objectContaining({ id: '123', name: 'cred-aws-test' })], + sharings: [ + expect.objectContaining({ + credentialsId: '123', + userId: member.id, + role: 'credential:owner', + }), + ], + }); + + // + // ACT + // + // Import again only updating the name and omitting `--userId` + await importCredential([ + '--input=./test/integration/commands/importCredentials/credentials-updated.json', + ]); + + // + // ASSERT + // + const after = { + credentials: await getAllCredentials(), + sharings: await getAllSharedCredentials(), + }; + + expect(after).toMatchObject({ + credentials: [ + expect.objectContaining({ + id: '123', + // only the name was updated + name: 'cred-aws-prod', + }), + ], + sharings: [ + expect.objectContaining({ + credentialsId: '123', + userId: member.id, + role: 'credential:owner', + }), + ], + }); }); diff --git a/packages/cli/test/integration/commands/import.cmd.test.ts b/packages/cli/test/integration/commands/import.cmd.test.ts index 211fde564156e..e65325c47153c 100644 --- a/packages/cli/test/integration/commands/import.cmd.test.ts +++ b/packages/cli/test/integration/commands/import.cmd.test.ts @@ -6,10 +6,17 @@ import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { mockInstance } from '../../shared/mocking'; import * as testDb from '../shared/testDb'; -import { getAllWorkflows } from '../shared/db/workflows'; +import { getAllSharedWorkflows, getAllWorkflows } from '../shared/db/workflows'; +import { createMember, createOwner } from '../shared/db/users'; const oclifConfig = new Config({ root: __dirname }); +async function importWorkflow(argv: string[]) { + const importer = new ImportWorkflowsCommand(argv, oclifConfig); + await importer.init(); + await importer.run(); +} + beforeAll(async () => { mockInstance(InternalHooks); mockInstance(LoadNodesAndCredentials); @@ -17,7 +24,7 @@ beforeAll(async () => { }); beforeEach(async () => { - await testDb.truncate(['Workflow']); + await testDb.truncate(['Workflow', 'SharedWorkflow', 'User']); }); afterAll(async () => { @@ -25,53 +32,186 @@ afterAll(async () => { }); test('import:workflow should import active workflow and deactivate it', async () => { - const before = await getAllWorkflows(); - expect(before.length).toBe(0); - const importer = new ImportWorkflowsCommand( - ['--separate', '--input=./test/integration/commands/importWorkflows/separate'], - oclifConfig, - ); - const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { - throw new Error('process.exit'); - }); + // + // ARRANGE + // + const owner = await createOwner(); - await importer.init(); - try { - await importer.run(); - } catch (error) { - expect(error.message).toBe('process.exit'); - } - const after = await getAllWorkflows(); - expect(after.length).toBe(2); - expect(after[0].name).toBe('active-workflow'); - expect(after[0].active).toBe(false); - expect(after[1].name).toBe('inactive-workflow'); - expect(after[1].active).toBe(false); - mockExit.mockRestore(); + // + // ACT + // + await importWorkflow([ + '--separate', + '--input=./test/integration/commands/importWorkflows/separate', + ]); + + // + // ASSERT + // + const after = { + workflows: await getAllWorkflows(), + sharings: await getAllSharedWorkflows(), + }; + expect(after).toMatchObject({ + workflows: [ + expect.objectContaining({ name: 'active-workflow', active: false }), + expect.objectContaining({ name: 'inactive-workflow', active: false }), + ], + sharings: [ + expect.objectContaining({ workflowId: '998', userId: owner.id, role: 'workflow:owner' }), + expect.objectContaining({ workflowId: '999', userId: owner.id, role: 'workflow:owner' }), + ], + }); }); test('import:workflow should import active workflow from combined file and deactivate it', async () => { - const before = await getAllWorkflows(); - expect(before.length).toBe(0); - const importer = new ImportWorkflowsCommand( - ['--input=./test/integration/commands/importWorkflows/combined/combined.json'], - oclifConfig, + // + // ARRANGE + // + const owner = await createOwner(); + + // + // ACT + // + await importWorkflow([ + '--input=./test/integration/commands/importWorkflows/combined/combined.json', + ]); + + // + // ASSERT + // + const after = { + workflows: await getAllWorkflows(), + sharings: await getAllSharedWorkflows(), + }; + expect(after).toMatchObject({ + workflows: [ + expect.objectContaining({ name: 'active-workflow', active: false }), + expect.objectContaining({ name: 'inactive-workflow', active: false }), + ], + sharings: [ + expect.objectContaining({ workflowId: '998', userId: owner.id, role: 'workflow:owner' }), + expect.objectContaining({ workflowId: '999', userId: owner.id, role: 'workflow:owner' }), + ], + }); +}); + +test('`import:workflow --userId ...` should fail if the workflow exists already and is owned by somebody else', async () => { + // + // ARRANGE + // + const owner = await createOwner(); + const member = await createMember(); + + // Import workflow the first time, assigning it to a member. + await importWorkflow([ + '--input=./test/integration/commands/importWorkflows/combined-with-update/original.json', + `--userId=${owner.id}`, + ]); + + const before = { + workflows: await getAllWorkflows(), + sharings: await getAllSharedWorkflows(), + }; + // Make sure the workflow and sharing have been created. + expect(before).toMatchObject({ + workflows: [expect.objectContaining({ id: '998', name: 'active-workflow' })], + sharings: [ + expect.objectContaining({ + workflowId: '998', + userId: owner.id, + role: 'workflow:owner', + }), + ], + }); + + // + // ACT + // + // Import the same workflow again, with another name but the same ID, and try + // to assign it to the member. + await expect( + importWorkflow([ + '--input=./test/integration/commands/importWorkflows/combined-with-update/updated.json', + `--userId=${member.id}`, + ]), + ).rejects.toThrowError( + `The credential with id "998" is already owned by the user with the id "${owner.id}". It can't be re-owned by the user with the id "${member.id}"`, ); - const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { - throw new Error('process.exit'); + + // + // ASSERT + // + const after = { + workflows: await getAllWorkflows(), + sharings: await getAllSharedWorkflows(), + }; + // Make sure there is no new sharing and that the name DID NOT change. + expect(after).toMatchObject({ + workflows: [expect.objectContaining({ id: '998', name: 'active-workflow' })], + sharings: [ + expect.objectContaining({ + workflowId: '998', + userId: owner.id, + role: 'workflow:owner', + }), + ], }); +}); - await importer.init(); - try { - await importer.run(); - } catch (error) { - expect(error.message).toBe('process.exit'); - } - const after = await getAllWorkflows(); - expect(after.length).toBe(2); - expect(after[0].name).toBe('active-workflow'); - expect(after[0].active).toBe(false); - expect(after[1].name).toBe('inactive-workflow'); - expect(after[1].active).toBe(false); - mockExit.mockRestore(); +test("only update the workflow, don't create or update the owner if `--userId` is not passed", async () => { + // + // ARRANGE + // + await createOwner(); + const member = await createMember(); + + // Import workflow the first time, assigning it to a member. + await importWorkflow([ + '--input=./test/integration/commands/importWorkflows/combined-with-update/original.json', + `--userId=${member.id}`, + ]); + + const before = { + workflows: await getAllWorkflows(), + sharings: await getAllSharedWorkflows(), + }; + // Make sure the workflow and sharing have been created. + expect(before).toMatchObject({ + workflows: [expect.objectContaining({ id: '998', name: 'active-workflow' })], + sharings: [ + expect.objectContaining({ + workflowId: '998', + userId: member.id, + role: 'workflow:owner', + }), + ], + }); + + // + // ACT + // + // Import the same workflow again, with another name but the same ID. + await importWorkflow([ + '--input=./test/integration/commands/importWorkflows/combined-with-update/updated.json', + ]); + + // + // ASSERT + // + const after = { + workflows: await getAllWorkflows(), + sharings: await getAllSharedWorkflows(), + }; + // Make sure there is no new sharing and that the name changed. + expect(after).toMatchObject({ + workflows: [expect.objectContaining({ id: '998', name: 'active-workflow updated' })], + sharings: [ + expect.objectContaining({ + workflowId: '998', + userId: member.id, + role: 'workflow:owner', + }), + ], + }); }); diff --git a/packages/cli/test/integration/commands/importCredentials/credentials-updated.json b/packages/cli/test/integration/commands/importCredentials/credentials-updated.json new file mode 100644 index 0000000000000..67fad38ef79de --- /dev/null +++ b/packages/cli/test/integration/commands/importCredentials/credentials-updated.json @@ -0,0 +1,14 @@ +[ + { + "createdAt": "2023-07-10T14:50:49.193Z", + "updatedAt": "2023-10-27T13:34:42.917Z", + "id": "123", + "name": "cred-aws-prod", + "data": { + "region": "eu-west-1", + "accessKeyId": "999999999999", + "secretAccessKey": "aaaaaaaaaaaaa" + }, + "type": "aws" + } +] diff --git a/packages/cli/test/integration/commands/importCredentials/separate/separate-credential.json b/packages/cli/test/integration/commands/importCredentials/separate/separate-credential.json new file mode 100644 index 0000000000000..24ce8467ed757 --- /dev/null +++ b/packages/cli/test/integration/commands/importCredentials/separate/separate-credential.json @@ -0,0 +1,12 @@ +{ + "createdAt": "2023-07-10T14:50:49.193Z", + "updatedAt": "2023-10-27T13:34:42.917Z", + "id": "123", + "name": "cred-aws-test", + "data": { + "region": "eu-west-1", + "accessKeyId": "999999999999", + "secretAccessKey": "aaaaaaaaaaaaa" + }, + "type": "aws" +} diff --git a/packages/cli/test/integration/commands/importWorkflows/combined-with-update/original.json b/packages/cli/test/integration/commands/importWorkflows/combined-with-update/original.json new file mode 100644 index 0000000000000..bbef96a0a9e07 --- /dev/null +++ b/packages/cli/test/integration/commands/importWorkflows/combined-with-update/original.json @@ -0,0 +1,81 @@ +[ + { + "name": "active-workflow", + "nodes": [ + { + "parameters": { + "path": "e20b4873-fcf7-4bce-88fc-a1a56d66b138", + "responseMode": "responseNode", + "options": {} + }, + "id": "c26d8782-bd57-43d0-86dc-0c618a7e4024", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [800, 580], + "webhookId": "e20b4873-fcf7-4bce-88fc-a1a56d66b138" + }, + { + "parameters": { + "values": { + "boolean": [ + { + "name": "hooked", + "value": true + } + ] + }, + "options": {} + }, + "id": "9701b1ef-9ab0-432a-b086-cf76981b097d", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [1020, 580] + }, + { + "parameters": { + "options": {} + }, + "id": "d0f086b8-c2b2-4404-b347-95d3f91e555a", + "name": "Respond to Webhook", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1, + "position": [1240, 580] + } + ], + "pinData": {}, + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set": { + "main": [ + [ + { + "node": "Respond to Webhook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": {}, + "versionId": "40a70df1-740f-47e7-8e16-50a0bcd5b70f", + "id": "998", + "meta": { + "instanceId": "95977dc4769098fc608439605527ee75d23f10d551aed6b87a3eea1a252c0ba9" + }, + "tags": [] + } +] diff --git a/packages/cli/test/integration/commands/importWorkflows/combined-with-update/updated.json b/packages/cli/test/integration/commands/importWorkflows/combined-with-update/updated.json new file mode 100644 index 0000000000000..fc1ddbf3ead48 --- /dev/null +++ b/packages/cli/test/integration/commands/importWorkflows/combined-with-update/updated.json @@ -0,0 +1,81 @@ +[ + { + "name": "active-workflow updated", + "nodes": [ + { + "parameters": { + "path": "e20b4873-fcf7-4bce-88fc-a1a56d66b138", + "responseMode": "responseNode", + "options": {} + }, + "id": "c26d8782-bd57-43d0-86dc-0c618a7e4024", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [800, 580], + "webhookId": "e20b4873-fcf7-4bce-88fc-a1a56d66b138" + }, + { + "parameters": { + "values": { + "boolean": [ + { + "name": "hooked", + "value": true + } + ] + }, + "options": {} + }, + "id": "9701b1ef-9ab0-432a-b086-cf76981b097d", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [1020, 580] + }, + { + "parameters": { + "options": {} + }, + "id": "d0f086b8-c2b2-4404-b347-95d3f91e555a", + "name": "Respond to Webhook", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1, + "position": [1240, 580] + } + ], + "pinData": {}, + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set": { + "main": [ + [ + { + "node": "Respond to Webhook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": {}, + "versionId": "40a70df1-740f-47e7-8e16-50a0bcd5b70f", + "id": "998", + "meta": { + "instanceId": "95977dc4769098fc608439605527ee75d23f10d551aed6b87a3eea1a252c0ba9" + }, + "tags": [] + } +] diff --git a/packages/cli/test/integration/import.service.test.ts b/packages/cli/test/integration/import.service.test.ts index 4809e58138aeb..2cfdbe4080411 100644 --- a/packages/cli/test/integration/import.service.test.ts +++ b/packages/cli/test/integration/import.service.test.ts @@ -12,8 +12,13 @@ import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflo import * as testDb from './shared/testDb'; import { mockInstance } from '../shared/mocking'; -import { createOwner } from './shared/db/users'; -import { createWorkflow, getWorkflowById } from './shared/db/workflows'; +import { createMember, createOwner } from './shared/db/users'; +import { + createWorkflow, + getAllSharedWorkflows, + getWorkflowById, + newWorkflow, +} from './shared/db/workflows'; import type { User } from '@db/entities/User'; @@ -57,7 +62,7 @@ describe('ImportService', () => { }); test('should make user owner of imported workflow', async () => { - const workflowToImport = await createWorkflow(); + const workflowToImport = newWorkflow(); await importService.importWorkflows([workflowToImport], owner.id); @@ -68,6 +73,23 @@ describe('ImportService', () => { expect(dbSharing.userId).toBe(owner.id); }); + test('should not change the owner if it already exists', async () => { + const member = await createMember(); + const workflowToImport = await createWorkflow(undefined, owner); + + await importService.importWorkflows([workflowToImport], member.id); + + const sharings = await getAllSharedWorkflows(); + + expect(sharings).toMatchObject([ + expect.objectContaining({ + workflowId: workflowToImport.id, + userId: owner.id, + role: 'workflow:owner', + }), + ]); + }); + test('should deactivate imported workflow if active', async () => { const workflowToImport = await createWorkflow({ active: true }); diff --git a/packages/cli/test/integration/shared/db/credentials.ts b/packages/cli/test/integration/shared/db/credentials.ts index 4e77b2fcc2322..85b46d26a3e6d 100644 --- a/packages/cli/test/integration/shared/db/credentials.ts +++ b/packages/cli/test/integration/shared/db/credentials.ts @@ -91,3 +91,7 @@ export async function getAllCredentials() { export const getCredentialById = async (id: string) => await Container.get(CredentialsRepository).findOneBy({ id }); + +export async function getAllSharedCredentials() { + return await Container.get(SharedCredentialsRepository).find(); +} diff --git a/packages/cli/test/integration/shared/db/workflows.ts b/packages/cli/test/integration/shared/db/workflows.ts index f0758088f1667..18a97a693b6ba 100644 --- a/packages/cli/test/integration/shared/db/workflows.ts +++ b/packages/cli/test/integration/shared/db/workflows.ts @@ -19,12 +19,7 @@ export async function createManyWorkflows( return await Promise.all(workflowRequests); } -/** - * Store a workflow in the DB (without a trigger) and optionally assign it to a user. - * @param attributes workflow attributes - * @param user user to assign the workflow to - */ -export async function createWorkflow(attributes: Partial = {}, user?: User) { +export function newWorkflow(attributes: Partial = {}): WorkflowEntity { const { active, name, nodes, connections, versionId } = attributes; const workflowEntity = Container.get(WorkflowRepository).create({ @@ -45,7 +40,16 @@ export async function createWorkflow(attributes: Partial = {}, u ...attributes, }); - const workflow = await Container.get(WorkflowRepository).save(workflowEntity); + return workflowEntity; +} + +/** + * Store a workflow in the DB (without a trigger) and optionally assign it to a user. + * @param attributes workflow attributes + * @param user user to assign the workflow to + */ +export async function createWorkflow(attributes: Partial = {}, user?: User) { + const workflow = await Container.get(WorkflowRepository).save(newWorkflow(attributes)); if (user) { await Container.get(SharedWorkflowRepository).save({ @@ -121,5 +125,9 @@ export async function getAllWorkflows() { return await Container.get(WorkflowRepository).find(); } +export async function getAllSharedWorkflows() { + return await Container.get(SharedWorkflowRepository).find(); +} + export const getWorkflowById = async (id: string) => await Container.get(WorkflowRepository).findOneBy({ id }); diff --git a/packages/core/package.json b/packages/core/package.json index 64d5c0b93b22f..43f3237f14836 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "1.37.2", + "version": "1.37.3", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io/", @@ -37,7 +37,7 @@ "@types/aws4": "^1.5.1", "@types/concat-stream": "^2.0.0", "@types/cron": "~1.7.1", - "@types/express": "^4.17.6", + "@types/express": "^4.17.21", "@types/lodash": "^4.14.195", "@types/mime-types": "^2.1.0", "@types/uuid": "^8.3.2", diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 0367d292566db..3193f3bd81fbf 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -518,8 +518,9 @@ export async function parseRequestObject(requestObject: IRequestOptions) { if (typeof requestObject.proxy === 'string') { try { const url = new URL(requestObject.proxy); + const host = url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname; axiosConfig.proxy = { - host: url.hostname, + host, port: parseInt(url.port, 10), protocol: url.protocol, }; @@ -544,8 +545,9 @@ export async function parseRequestObject(requestObject: IRequestOptions) { const [userpass, hostport] = requestObject.proxy.split('@'); const [username, password] = userpass.split(':'); const [hostname, port] = hostport.split(':'); + const host = hostname.startsWith('[') ? hostname.slice(1, -1) : hostname; axiosConfig.proxy = { - host: hostname, + host, port: parseInt(port, 10), protocol: 'http', auth: { diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index f0dcee5c55f33..dea39ba711574 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "1.37.1", + "version": "1.37.2", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io/", diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index 849b8d7bcd7ad..8ad550167da08 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -1,6 +1,6 @@ { "name": "n8n-node-dev", - "version": "1.36.0", + "version": "1.36.1", "description": "CLI to simplify n8n credentials/node development", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io/", diff --git a/packages/nodes-base/credentials/CrowdStrikeOAuth2Api.credentials.ts b/packages/nodes-base/credentials/CrowdStrikeOAuth2Api.credentials.ts index ca2aec474dec7..ccf20224477a2 100644 --- a/packages/nodes-base/credentials/CrowdStrikeOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/CrowdStrikeOAuth2Api.credentials.ts @@ -63,9 +63,11 @@ export class CrowdStrikeOAuth2Api implements ICredentialType { const url = credentials.url as string; const { access_token } = (await this.helpers.httpRequest({ method: 'POST', - url: `${url.endsWith('/') ? url.slice(0, -1) : url}/oauth2/token?client_id=${ - credentials.clientId - }&client_secret=${credentials.clientSecret}`, + url: `${url.endsWith('/') ? url.slice(0, -1) : url}/oauth2/token`, + body: { + client_id: credentials.clientId, + client_secret: credentials.clientSecret, + }, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, diff --git a/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts b/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts index 3b781ed8053a7..12febc14e5205 100644 --- a/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts +++ b/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts @@ -144,7 +144,7 @@ export class GitlabTrigger implements INodeType { default: '', required: true, placeholder: 'n8n-io', - description: 'Owner of the repsitory', + description: 'Owner of the repository', }, { displayName: 'Repository Name', @@ -153,7 +153,7 @@ export class GitlabTrigger implements INodeType { default: '', required: true, placeholder: 'n8n', - description: 'The name of the repsitory', + description: 'The name of the repository', }, { displayName: 'Events', diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 1909e172669b4..c765178b9619b 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "1.37.2", + "version": "1.37.3", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io/", @@ -810,7 +810,7 @@ "@types/cheerio": "^0.22.15", "@types/cron": "~1.7.1", "@types/eventsource": "^1.1.2", - "@types/express": "^4.17.6", + "@types/express": "^4.17.21", "@types/html-to-text": "^9.0.1", "@types/gm": "^1.25.0", "@types/js-nacl": "^1.3.0", diff --git a/packages/workflow/package.json b/packages/workflow/package.json index fa4c61e04e6e7..8ad5f173004e5 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "n8n-workflow", - "version": "1.37.2", + "version": "1.37.3", "description": "Workflow base code of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io/", @@ -40,7 +40,7 @@ ], "devDependencies": { "@types/deep-equal": "^1.0.1", - "@types/express": "^4.17.6", + "@types/express": "^4.17.21", "@types/jmespath": "^0.15.0", "@types/lodash": "^4.14.195", "@types/luxon": "^3.2.0", diff --git a/patches/@types__express-serve-static-core@4.17.43.patch b/patches/@types__express-serve-static-core@4.17.43.patch new file mode 100644 index 0000000000000..05882f0b97f5d --- /dev/null +++ b/patches/@types__express-serve-static-core@4.17.43.patch @@ -0,0 +1,13 @@ +diff --git a/index.d.ts b/index.d.ts +index 5cc36f5760c806a76ee839bfb67c419c9cb48901..8ef0bf74f0f31741b564fe37f040144526e98eb5 100644 +--- a/index.d.ts ++++ b/index.d.ts +@@ -646,7 +646,7 @@ export interface Request< + + query: ReqQuery; + +- route: any; ++ route?: Pick; + + signedCookies: any; + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c329d7ffb6d94..05ac220ed5f42 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ patchedDependencies: '@sentry/cli@2.17.0': hash: nchnoezkq6p37qaiku3vrpwraq path: patches/@sentry__cli@2.17.0.patch + '@types/express-serve-static-core@4.17.43': + hash: 5orrj4qleu2iko5t27vl44u4we + path: patches/@types__express-serve-static-core@4.17.43.patch '@types/ws@8.5.4': hash: nbzuqaoyqbrfwipijj5qriqqju path: patches/@types__ws@8.5.4.patch @@ -257,8 +260,8 @@ importers: specifier: ^0.0.2 version: 0.0.2 '@n8n/typeorm': - specifier: 0.3.20-7 - version: 0.3.20-7(pg@8.11.3)(redis@4.6.12)(sqlite3@5.1.7) + specifier: 0.3.20-8 + version: 0.3.20-8(pg@8.11.3)(redis@4.6.12)(sqlite3@5.1.7) '@n8n/vm2': specifier: 3.9.20 version: 3.9.20 @@ -345,8 +348,8 @@ importers: specifier: ^1.1.3 version: 1.1.3 '@types/express': - specifier: ^4.17.6 - version: 4.17.14 + specifier: ^4.17.21 + version: 4.17.21 '@types/html-to-text': specifier: ^9.0.1 version: 9.0.4 @@ -488,8 +491,8 @@ importers: specifier: workspace:* version: link:../@n8n/permissions '@n8n/typeorm': - specifier: 0.3.20-7 - version: 0.3.20-7(@sentry/node@7.87.0)(ioredis@5.3.2)(mysql2@2.3.3)(pg@8.11.3)(sqlite3@5.1.7) + specifier: 0.3.20-8 + version: 0.3.20-8(@sentry/node@7.87.0)(ioredis@5.3.2)(mysql2@2.3.3)(pg@8.11.3)(sqlite3@5.1.7) '@n8n_io/license-sdk': specifier: 2.10.0 version: 2.10.0 @@ -904,8 +907,8 @@ importers: specifier: ~1.7.1 version: 1.7.3 '@types/express': - specifier: ^4.17.6 - version: 4.17.14 + specifier: ^4.17.21 + version: 4.17.21 '@types/lodash': specifier: ^4.14.195 version: 4.14.195 @@ -1478,8 +1481,8 @@ importers: specifier: ^1.1.2 version: 1.1.9 '@types/express': - specifier: ^4.17.6 - version: 4.17.14 + specifier: ^4.17.21 + version: 4.17.21 '@types/gm': specifier: ^1.25.0 version: 1.25.0 @@ -1611,8 +1614,8 @@ importers: specifier: ^1.0.1 version: 1.0.1 '@types/express': - specifier: ^4.17.6 - version: 4.17.14 + specifier: ^4.17.21 + version: 4.17.21 '@types/jmespath': specifier: ^0.15.0 version: 0.15.0 @@ -5339,7 +5342,7 @@ packages: dependencies: string-width: 5.1.2 string-width-cjs: /string-width@4.2.3 - strip-ansi: 7.0.1 + strip-ansi: 7.1.0 strip-ansi-cjs: /strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: /wrap-ansi@7.0.0 @@ -6619,8 +6622,8 @@ packages: recast: 0.22.0 dev: false - /@n8n/typeorm@0.3.20-7(@sentry/node@7.87.0)(ioredis@5.3.2)(mysql2@2.3.3)(pg@8.11.3)(sqlite3@5.1.7): - resolution: {integrity: sha512-f4A9RGOnB3kCkusNAr1QDCGOVq1HU1YCBKoIGr2of+P3CVS3I+1vW7neOhlr/ic5S1F14Qy5TU8Lb78mRBYRSw==} + /@n8n/typeorm@0.3.20-8(@sentry/node@7.87.0)(ioredis@5.3.2)(mysql2@2.3.3)(pg@8.11.3)(sqlite3@5.1.7): + resolution: {integrity: sha512-WJFa9Pg6BJVS1dEe1xFRQcLtvjKx2O1KTgI6pFrTTcH7zZMy3qNww7A3HIrW/LvzCu0+rnSfHU4GvDg5/oJhlg==} engines: {node: '>=16.13.0'} hasBin: true peerDependencies: @@ -6702,14 +6705,14 @@ packages: sqlite3: 5.1.7 tarn: 3.0.2 tslib: 2.6.2 - uuid: 9.0.0 + uuid: 9.0.1 yargs: 17.7.2 transitivePeerDependencies: - supports-color dev: false - /@n8n/typeorm@0.3.20-7(pg@8.11.3)(redis@4.6.12)(sqlite3@5.1.7): - resolution: {integrity: sha512-f4A9RGOnB3kCkusNAr1QDCGOVq1HU1YCBKoIGr2of+P3CVS3I+1vW7neOhlr/ic5S1F14Qy5TU8Lb78mRBYRSw==} + /@n8n/typeorm@0.3.20-8(pg@8.11.3)(redis@4.6.12)(sqlite3@5.1.7): + resolution: {integrity: sha512-WJFa9Pg6BJVS1dEe1xFRQcLtvjKx2O1KTgI6pFrTTcH7zZMy3qNww7A3HIrW/LvzCu0+rnSfHU4GvDg5/oJhlg==} engines: {node: '>=16.13.0'} hasBin: true peerDependencies: @@ -6789,7 +6792,7 @@ packages: sqlite3: 5.1.7 tarn: 3.0.2 tslib: 2.6.2 - uuid: 9.0.0 + uuid: 9.0.1 yargs: 17.7.2 transitivePeerDependencies: - supports-color @@ -9213,7 +9216,7 @@ packages: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.4.21(typescript@5.4.2) - vue-component-type-helpers: 2.0.12 + vue-component-type-helpers: 2.0.13 transitivePeerDependencies: - encoding - supports-color @@ -9534,7 +9537,7 @@ packages: /@types/connect-history-api-fallback@1.3.5: resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==} dependencies: - '@types/express-serve-static-core': 4.17.43 + '@types/express-serve-static-core': 4.17.43(patch_hash=5orrj4qleu2iko5t27vl44u4we) '@types/node': 18.16.16 dev: true @@ -9622,28 +9625,20 @@ packages: resolution: {integrity: sha512-F3K4oyM12o8W9jxuJmW+1sc8kdw0Hj0t+26urwkcolPJTgkfppEfIdftdcXmUU2QPBIwcrYO6diqgIqgCDf1FA==} dev: true - /@types/express-serve-static-core@4.17.43: + /@types/express-serve-static-core@4.17.43(patch_hash=5orrj4qleu2iko5t27vl44u4we): resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} dependencies: '@types/node': 18.16.16 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 '@types/send': 0.17.4 - - /@types/express@4.17.14: - resolution: {integrity: sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==} - dependencies: - '@types/body-parser': 1.19.2 - '@types/express-serve-static-core': 4.17.43 - '@types/qs': 6.9.7 - '@types/serve-static': 1.15.0 - dev: true + patched: true /@types/express@4.17.21: resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} dependencies: '@types/body-parser': 1.19.2 - '@types/express-serve-static-core': 4.17.43 + '@types/express-serve-static-core': 4.17.43(patch_hash=5orrj4qleu2iko5t27vl44u4we) '@types/qs': 6.9.7 '@types/serve-static': 1.15.0 @@ -24011,7 +24006,7 @@ packages: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.0.1 + strip-ansi: 7.1.0 /string.prototype.startswith@1.0.0: resolution: {integrity: sha512-VHhsDkuf8gsw4JNRK9cIZjYe6r7PsVUutVohaBhqYAoPaRADoQH+mMgUg7Cs/TgQeDGEvI+PzPEMOdvdsCMvpg==} @@ -24094,18 +24089,11 @@ packages: dependencies: ansi-regex: 5.0.1 - /strip-ansi@7.0.1: - resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} - engines: {node: '>=12'} - dependencies: - ansi-regex: 6.0.1 - /strip-ansi@7.1.0: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} dependencies: ansi-regex: 6.0.1 - dev: true /strip-bom@2.0.0: resolution: {integrity: sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==} @@ -24414,7 +24402,7 @@ packages: https-proxy-agent: 5.0.1 node-fetch: 2.7.0(encoding@0.1.13) stream-events: 1.0.5 - uuid: 9.0.0 + uuid: 9.0.1 transitivePeerDependencies: - encoding - supports-color @@ -25907,8 +25895,8 @@ packages: resolution: {integrity: sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==} dev: true - /vue-component-type-helpers@2.0.12: - resolution: {integrity: sha512-iVJugClQdu3ZyF0N4CF3Egi+gWYfnxlIPPGtFXZG29rF3kQIuziP+k7rVGCCHiibIOQ1SlspKjrh+LRYzMpwTA==} + /vue-component-type-helpers@2.0.13: + resolution: {integrity: sha512-xNO5B7DstNWETnoYflLkVgh8dK8h2ZDgxY1M2O0zrqGeBNq5yAZ8a10yCS9+HnixouNGYNX+ggU9MQQq86HTpg==} dev: true /vue-demi@0.14.5(vue@3.4.21): @@ -26434,7 +26422,7 @@ packages: dependencies: ansi-styles: 6.2.1 string-width: 5.1.2 - strip-ansi: 7.0.1 + strip-ansi: 7.1.0 /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}