diff --git a/packages/cli/src/CommunityNodes/helpers.ts b/packages/cli/src/CommunityNodes/helpers.ts index 38544efdb57c7..cb634a4aa0399 100644 --- a/packages/cli/src/CommunityNodes/helpers.ts +++ b/packages/cli/src/CommunityNodes/helpers.ts @@ -4,7 +4,7 @@ import { promisify } from 'util'; import { exec } from 'child_process'; import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises'; - +import { createContext, Script } from 'vm'; import axios from 'axios'; import { UserSettings } from 'n8n-core'; import { LoggerProxy, PublicInstalledPackage } from 'n8n-workflow'; @@ -235,3 +235,10 @@ export const isClientError = (error: Error): boolean => { export function isNpmError(error: unknown): error is { code: number; stdout: string } { return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error; } + +const context = createContext({ require }); +export const loadClassInIsolation = (filePath: string, className: string) => { + const script = new Script(`new (require('${filePath}').${className})()`); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return script.runInContext(context); +}; diff --git a/packages/cli/src/LoadNodesAndCredentials.ts b/packages/cli/src/LoadNodesAndCredentials.ts index 8f5c62da9bc35..f302638d07156 100644 --- a/packages/cli/src/LoadNodesAndCredentials.ts +++ b/packages/cli/src/LoadNodesAndCredentials.ts @@ -37,7 +37,7 @@ import config from '../config'; import { NodeTypes } from '.'; import { InstalledPackages } from './databases/entities/InstalledPackages'; import { InstalledNodes } from './databases/entities/InstalledNodes'; -import { executeCommand } from './CommunityNodes/helpers'; +import { executeCommand, loadClassInIsolation } from './CommunityNodes/helpers'; import { RESPONSE_ERROR_MESSAGES } from './constants'; import { persistInstalledPackageData, @@ -46,6 +46,14 @@ import { const CUSTOM_NODES_CATEGORY = 'Custom Nodes'; +function toJSON() { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...this, + authenticate: typeof this.authenticate === 'function' ? {} : this.authenticate, + }; +} + class LoadNodesAndCredentialsClass { nodeTypes: INodeTypeData = {}; @@ -104,10 +112,8 @@ class LoadNodesAndCredentialsClass { await fsAccess(checkPath); // Folder exists, so use it. return path.dirname(checkPath); - } catch (error) { + } catch (_) { // Folder does not exist so get next one - // eslint-disable-next-line no-continue - continue; } } throw new Error('Could not find "node_modules" folder!'); @@ -144,8 +150,7 @@ class LoadNodesAndCredentialsClass { if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV]!.split(';'); - // eslint-disable-next-line prefer-spread - customDirectories.push.apply(customDirectories, customExtensionFolders); + customDirectories.push(...customExtensionFolders); } for (const directory of customDirectories) { @@ -192,26 +197,16 @@ class LoadNodesAndCredentialsClass { * @param {string} filePath The file to read credentials from * @returns {Promise} */ - async loadCredentialsFromFile(credentialName: string, filePath: string): Promise { - // eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires - const tempModule = require(filePath); - + loadCredentialsFromFile(credentialName: string, filePath: string): void { let tempCredential: ICredentialType; try { + tempCredential = loadClassInIsolation(filePath, credentialName); + // Add serializer method "toJSON" to the class so that authenticate method (if defined) // gets mapped to the authenticate attribute before it is sent to the client. // The authenticate property is used by the client to decide whether or not to // include the credential type in the predefined credentials (HTTP node) - // eslint-disable-next-line func-names - tempModule[credentialName].prototype.toJSON = function () { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return { - ...this, - authenticate: typeof this.authenticate === 'function' ? {} : this.authenticate, - }; - }; - - tempCredential = new tempModule[credentialName]() as ICredentialType; + Object.assign(tempCredential, { toJSON }); if (tempCredential.icon && tempCredential.icon.startsWith('file:')) { // If a file icon gets used add the full path @@ -353,19 +348,16 @@ class LoadNodesAndCredentialsClass { * @param {string} filePath The file to read node from * @returns {Promise} */ - async loadNodeFromFile( + loadNodeFromFile( packageName: string, nodeName: string, filePath: string, - ): Promise { + ): INodeTypeNameVersion | undefined { let tempNode: INodeType | INodeVersionedType; - let fullNodeName: string; let nodeVersion = 1; try { - // eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires - const tempModule = require(filePath); - tempNode = new tempModule[nodeName](); + tempNode = loadClassInIsolation(filePath, nodeName); this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' }); } catch (error) { // eslint-disable-next-line no-console, @typescript-eslint/restrict-template-expressions @@ -373,8 +365,7 @@ class LoadNodesAndCredentialsClass { throw error; } - // eslint-disable-next-line prefer-const - fullNodeName = `${packageName}.${tempNode.description.name}`; + const fullNodeName = `${packageName}.${tempNode.description.name}`; tempNode.description.name = fullNodeName; if (tempNode.description.icon !== undefined && tempNode.description.icon.startsWith('file:')) { @@ -385,13 +376,6 @@ class LoadNodesAndCredentialsClass { )}`; } - if (tempNode.hasOwnProperty('executeSingle')) { - this.logger.warn( - `"executeSingle" will get deprecated soon. Please update the code of node "${packageName}.${nodeName}" to use "execute" instead!`, - { filePath }, - ); - } - if (tempNode.hasOwnProperty('nodeVersions')) { const versionedNodeType = (tempNode as INodeVersionedType).getNodeType(); this.addCodex({ node: versionedNodeType, filePath, isCustom: packageName === 'CUSTOM' }); @@ -491,8 +475,7 @@ class LoadNodesAndCredentialsClass { node.description.codex = codex; } catch (_) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - this.logger.debug(`No codex available for: ${filePath.split('/').pop()}`); + this.logger.debug(`No codex available for: ${filePath.split('/').pop() ?? ''}`); if (isCustom) { node.description.codex = { @@ -512,22 +495,15 @@ class LoadNodesAndCredentialsClass { async loadDataFromDirectory(setPackageName: string, directory: string): Promise { const files = await glob(path.join(directory, '**/*.@(node|credentials).js')); - let fileName: string; - let type: string; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const loadPromises: any[] = []; for (const filePath of files) { - [fileName, type] = path.parse(filePath).name.split('.'); + const [fileName, type] = path.parse(filePath).name.split('.'); if (type === 'node') { - loadPromises.push(this.loadNodeFromFile(setPackageName, fileName, filePath)); + this.loadNodeFromFile(setPackageName, fileName, filePath); } else if (type === 'credentials') { - loadPromises.push(this.loadCredentialsFromFile(fileName, filePath)); + this.loadCredentialsFromFile(fileName, filePath); } } - - await Promise.all(loadPromises); } async readPackageJson(packagePath: string): Promise { @@ -545,26 +521,20 @@ class LoadNodesAndCredentialsClass { async loadDataFromPackage(packagePath: string): Promise { // Get the absolute path of the package const packageFile = await this.readPackageJson(packagePath); - // if (!packageFile.hasOwnProperty('n8n')) { if (!packageFile.n8n) { return []; } const packageName = packageFile.name; - - let tempPath: string; - let filePath: string; - + const { nodes, credentials } = packageFile.n8n; const returnData: INodeTypeNameVersion[] = []; // Read all node types - let fileName: string; - let type: string; - if (packageFile.n8n.hasOwnProperty('nodes') && Array.isArray(packageFile.n8n.nodes)) { - for (filePath of packageFile.n8n.nodes) { - tempPath = path.join(packagePath, filePath); - [fileName, type] = path.parse(filePath).name.split('.'); - const loadData = await this.loadNodeFromFile(packageName, fileName, tempPath); + if (Array.isArray(nodes)) { + for (const filePath of nodes) { + const tempPath = path.join(packagePath, filePath); + const [fileName] = path.parse(filePath).name.split('.'); + const loadData = this.loadNodeFromFile(packageName, fileName, tempPath); if (loadData) { returnData.push(loadData); } @@ -572,15 +542,10 @@ class LoadNodesAndCredentialsClass { } // Read all credential types - if ( - packageFile.n8n.hasOwnProperty('credentials') && - Array.isArray(packageFile.n8n.credentials) - ) { - for (filePath of packageFile.n8n.credentials) { - tempPath = path.join(packagePath, filePath); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - [fileName, type] = path.parse(filePath).name.split('.'); - // eslint-disable-next-line @typescript-eslint/no-floating-promises + if (Array.isArray(credentials)) { + for (const filePath of credentials) { + const tempPath = path.join(packagePath, filePath); + const [fileName] = path.parse(filePath).name.split('.'); this.loadCredentialsFromFile(fileName, tempPath); } } diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index 477caf98b2cfa..2c2b015ff79f6 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -49,6 +49,7 @@ import { getLogger } from './Logger'; import config from '../config'; import { InternalHooksManager } from './InternalHooksManager'; import { checkPermissionsForExecution } from './UserManagement/UserManagementHelper'; +import { loadClassInIsolation } from './CommunityNodes/helpers'; export class WorkflowRunnerProcess { data: IWorkflowExecutionDataProcessWithExecution | undefined; @@ -92,41 +93,30 @@ export class WorkflowRunnerProcess { workflowId: this.data.workflowData.id, }); - let className: string; - let tempNode: INodeType; - let tempCredential: ICredentialType; - let filePath: string; - this.startedAt = new Date(); // Load the required nodes const nodeTypesData: INodeTypeData = {}; // eslint-disable-next-line no-restricted-syntax for (const nodeTypeName of Object.keys(this.data.nodeTypeData)) { - className = this.data.nodeTypeData[nodeTypeName].className; - - filePath = this.data.nodeTypeData[nodeTypeName].sourcePath; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires - const tempModule = require(filePath); + let tempNode: INodeType; + const { className, sourcePath } = this.data.nodeTypeData[nodeTypeName]; try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const nodeObject = new tempModule[className](); + const nodeObject = loadClassInIsolation(sourcePath, className); if (nodeObject.getNodeType !== undefined) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call tempNode = nodeObject.getNodeType(); } else { tempNode = nodeObject; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - tempNode = new tempModule[className]() as INodeType; } catch (error) { - throw new Error(`Error loading node "${nodeTypeName}" from: "${filePath}"`); + throw new Error(`Error loading node "${nodeTypeName}" from: "${sourcePath}"`); } nodeTypesData[nodeTypeName] = { type: tempNode, - sourcePath: filePath, + sourcePath, }; } @@ -137,22 +127,18 @@ export class WorkflowRunnerProcess { const credentialsTypeData: ICredentialTypeData = {}; // eslint-disable-next-line no-restricted-syntax for (const credentialTypeName of Object.keys(this.data.credentialsTypeData)) { - className = this.data.credentialsTypeData[credentialTypeName].className; - - filePath = this.data.credentialsTypeData[credentialTypeName].sourcePath; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires - const tempModule = require(filePath); + let tempCredential: ICredentialType; + const { className, sourcePath } = this.data.credentialsTypeData[credentialTypeName]; try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - tempCredential = new tempModule[className]() as ICredentialType; + tempCredential = loadClassInIsolation(sourcePath, className); } catch (error) { - throw new Error(`Error loading credential "${credentialTypeName}" from: "${filePath}"`); + throw new Error(`Error loading credential "${credentialTypeName}" from: "${sourcePath}"`); } credentialsTypeData[credentialTypeName] = { type: tempCredential, - sourcePath: filePath, + sourcePath, }; }