UNPKG

n8n

Version:

n8n Workflow Automation Tool

436 lines 21.3 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CommunityPackagesService = void 0; exports.isValidVersionSpecifier = isValidVersionSpecifier; const backend_common_1 = require("@n8n/backend-common"); const constants_1 = require("@n8n/constants"); const decorators_1 = require("@n8n/decorators"); const di_1 = require("@n8n/di"); const axios_1 = __importDefault(require("axios")); const n8n_core_1 = require("n8n-core"); const n8n_workflow_1 = require("n8n-workflow"); const node_child_process_1 = require("node:child_process"); const promises_1 = require("node:fs/promises"); const node_path_1 = require("node:path"); const node_util_1 = require("node:util"); const semver_1 = require("semver"); const constants_2 = require("../../constants"); const feature_not_licensed_error_1 = require("../../errors/feature-not-licensed.error"); const license_1 = require("../../license"); const load_nodes_and_credentials_1 = require("../../load-nodes-and-credentials"); const publisher_service_1 = require("../../scaling/pubsub/publisher.service"); const utils_1 = require("../../utils"); const community_node_types_utils_1 = require("./community-node-types-utils"); const community_packages_config_1 = require("./community-packages.config"); const installed_packages_repository_1 = require("./installed-packages.repository"); const npm_utils_1 = require("./npm-utils"); const asyncExecFile = (0, node_util_1.promisify)(node_child_process_1.execFile); const NPM_DIST_TAG_PATTERN = /^[a-z][a-z0-9-._]*$/; function isValidVersionSpecifier(version) { return (0, semver_1.valid)(version) !== null || NPM_DIST_TAG_PATTERN.test(version); } const DEFAULT_REGISTRY = 'https://registry.npmjs.org'; const { PACKAGE_NAME_NOT_PROVIDED } = constants_2.RESPONSE_ERROR_MESSAGES; const INVALID_OR_SUSPICIOUS_PACKAGE_NAME = /[^0-9a-z@\-._/]/; let CommunityPackagesService = class CommunityPackagesService { constructor(instanceSettings, logger, installedPackageRepository, loadNodesAndCredentials, publisher, license, config) { this.instanceSettings = instanceSettings; this.logger = logger; this.installedPackageRepository = installedPackageRepository; this.loadNodesAndCredentials = loadNodesAndCredentials; this.publisher = publisher; this.license = license; this.config = config; this.missingPackages = []; this.downloadFolder = this.instanceSettings.nodesDownloadDir; this.packageJsonPath = (0, node_path_1.join)(this.downloadFolder, 'package.json'); } async init() { await this.ensurePackageJson(); await this.checkForMissingPackages(); } get hasMissingPackages() { return this.missingPackages.length > 0; } async findInstalledPackage(packageName) { return await this.installedPackageRepository.findOne({ where: { packageName }, relations: ['installedNodes'], }); } async isPackageInstalled(packageName) { return await this.installedPackageRepository.exist({ where: { packageName } }); } async getAllInstalledPackages() { return await this.installedPackageRepository.find({ relations: ['installedNodes'] }); } async removePackageFromDatabase(packageName) { return await this.installedPackageRepository.remove(packageName); } async persistInstalledPackage(packageLoader) { try { return await this.installedPackageRepository.saveInstalledPackageWithNodes(packageLoader); } catch (maybeError) { const error = (0, utils_1.toError)(maybeError); this.logger.error('Failed to save installed packages and nodes', { error, packageName: packageLoader.packageJson.name, }); throw error; } } parseNpmPackageName(rawString) { if (!rawString) throw new n8n_workflow_1.UnexpectedError(PACKAGE_NAME_NOT_PROVIDED); if (INVALID_OR_SUSPICIOUS_PACKAGE_NAME.test(rawString)) { throw new n8n_workflow_1.UnexpectedError('Package name must be a single word'); } const scope = rawString.includes('/') ? rawString.split('/')[0] : undefined; const packageNameWithoutScope = scope ? rawString.replace(`${scope}/`, '') : rawString; if (!packageNameWithoutScope.startsWith(constants_2.NODE_PACKAGE_PREFIX)) { throw new n8n_workflow_1.UnexpectedError(`Package name must start with ${constants_2.NODE_PACKAGE_PREFIX}`); } const version = packageNameWithoutScope.includes('@') ? packageNameWithoutScope.split('@')[1] : undefined; if (version && !isValidVersionSpecifier(version)) { throw new n8n_workflow_1.UnexpectedError(`Invalid version: ${version}`); } const packageName = version ? rawString.replace(`@${version}`, '') : rawString; return { packageName, scope, version, rawString }; } matchPackagesWithUpdates(packages, updates) { if (!updates) return packages; return packages.reduce((acc, cur) => { const publicPackage = { ...cur }; const update = updates[cur.packageName]; if (update) publicPackage.updateAvailable = update.latest; acc.push(publicPackage); return acc; }, []); } matchMissingPackages(installedPackages) { const missingPackagesList = this.missingPackages .map((name) => { try { const parsedPackageData = this.parseNpmPackageName(name); return parsedPackageData.packageName; } catch { return; } }) .filter((i) => i !== undefined); const packages = []; for (const installedPackage of installedPackages) { const pkg = { ...installedPackage }; if (missingPackagesList.includes(pkg.packageName)) { pkg.failedLoading = true; } packages.push(pkg); } return packages; } async checkNpmPackageStatus(packageName) { const N8N_BACKEND_SERVICE_URL = 'https://api.n8n.io/api/package'; try { const response = await axios_1.default.post(N8N_BACKEND_SERVICE_URL, { name: packageName }, { method: 'POST' }); if (response.data.status !== constants_2.NPM_PACKAGE_STATUS_GOOD) return response.data; } catch { } return { status: constants_2.NPM_PACKAGE_STATUS_GOOD }; } hasPackageLoaded(packageName) { if (!this.missingPackages.length) return true; return !this.missingPackages.some((packageNameAndVersion) => packageNameAndVersion.startsWith(packageName) && packageNameAndVersion.replace(packageName, '').startsWith('@')); } removePackageFromMissingList(packageName) { try { this.missingPackages = this.missingPackages.filter((packageNameAndVersion) => !packageNameAndVersion.startsWith(packageName) || !packageNameAndVersion.replace(packageName, '').startsWith('@')); } catch { } } async ensurePackageJson() { try { await (0, promises_1.access)(this.packageJsonPath, promises_1.constants.F_OK); } catch { await (0, promises_1.mkdir)(this.downloadFolder, { recursive: true }); const packageJson = { name: 'installed-nodes', private: true, dependencies: {}, }; await (0, promises_1.writeFile)(this.packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf-8'); } } async checkForMissingPackages() { const installedPackages = await this.getAllInstalledPackages(); const missingPackages = new Set(); installedPackages.forEach((installedPackage) => { installedPackage.installedNodes.forEach((installedNode) => { if (!this.loadNodesAndCredentials.isKnownNode(installedNode.type)) { missingPackages.add({ packageName: installedPackage.packageName, version: installedPackage.installedVersion, }); } }); }); this.missingPackages = []; if (missingPackages.size === 0) return; const { reinstallMissing } = this.config; if (reinstallMissing) { this.logger.info('Attempting to reinstall missing packages', { missingPackages: [...missingPackages], }); const environment = process.env.ENVIRONMENT === 'staging' ? 'staging' : 'production'; const packageNames = [...missingPackages].map((p) => p.packageName); let vettedPackages = []; try { vettedPackages = await (0, community_node_types_utils_1.getCommunityNodeTypes)(environment, { filters: { packageName: { $in: packageNames, }, }, fields: ['packageName', 'npmVersion', 'checksum', 'nodeVersions'], }, this.config.aiNodeSdkVersion); } catch (error) { this.logger.error(`Failed to fetch community packages from Strapi: ${(0, n8n_workflow_1.ensureError)(error).message}`); } for (const missingPackage of missingPackages) { try { const vettedPackage = vettedPackages.find((p) => p.packageName === missingPackage.packageName); let checksum; if (vettedPackage) { if (vettedPackage.npmVersion === missingPackage.version) { checksum = vettedPackage.checksum; } else { checksum = vettedPackage.nodeVersions?.find((v) => v.npmVersion === missingPackage.version)?.checksum; } } await this.installPackage(missingPackage.packageName, missingPackage.version, checksum); missingPackages.delete(missingPackage); } catch (error) { this.logger.error(`Failed to reinstall community package ${missingPackage.packageName}: ${(0, n8n_workflow_1.ensureError)(error).message}`); } } if (missingPackages.size === 0) { this.logger.info('Packages reinstalled successfully. Resuming regular initialization.'); } await this.loadNodesAndCredentials.postProcessLoaders(); this.loadNodesAndCredentials.releaseTypes(); } else { this.logger.warn('n8n detected that some packages are missing. For more information, visit https://docs.n8n.io/integrations/community-nodes/troubleshooting/'); } this.missingPackages = [...missingPackages].map((missingPackage) => `${missingPackage.packageName}@${missingPackage.version}`); } async installPackage(packageName, version, checksum) { return await this.installOrUpdatePackage(packageName, { version, checksum }); } async updatePackage(packageName, installedPackage, version, checksum) { return await this.installOrUpdatePackage(packageName, { installedPackage, version, checksum }); } async removePackage(packageName, installedPackage) { await this.removeNpmPackage(packageName); await this.removePackageFromDatabase(installedPackage); void this.publisher.publishCommand({ command: 'community-package-uninstall', payload: { packageName }, }); } getNpmRegistry() { const { registry } = this.config; if (registry !== DEFAULT_REGISTRY && !this.license.isCustomNpmRegistryEnabled()) { throw new feature_not_licensed_error_1.FeatureNotLicensedError(constants_1.LICENSE_FEATURES.COMMUNITY_NODES_CUSTOM_REGISTRY); } return registry; } getNpmAuthToken() { return this.config.authToken || undefined; } checkInstallPermissions(checksumProvided) { if (!this.config.unverifiedEnabled && !checksumProvided) { throw new n8n_workflow_1.UnexpectedError('Installation of unverified community packages is forbidden!'); } } async installOrUpdatePackage(packageName, options = {}) { const isUpdate = 'installedPackage' in options; const packageVersion = !options.version ? 'latest' : options.version; const shouldValidateChecksum = 'checksum' in options && Boolean(options.checksum); this.checkInstallPermissions(shouldValidateChecksum); const authToken = this.getNpmAuthToken(); if (options.checksum) { await (0, npm_utils_1.verifyIntegrity)(packageName, packageVersion, this.getNpmRegistry(), options.checksum, authToken); } await (0, npm_utils_1.checkIfVersionExistsOrThrow)(packageName, packageVersion, this.getNpmRegistry(), authToken); try { await this.downloadPackage(packageName, packageVersion, authToken); } catch (error) { if (error instanceof Error && error.message === constants_2.RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) { throw new n8n_workflow_1.UserError('npm package not found', { extra: { packageName } }); } throw error; } let loader; try { await this.loadNodesAndCredentials.unloadPackage(packageName); loader = await this.loadNodesAndCredentials.loadPackage(packageName); } catch (error) { try { await this.deletePackageDirectory(packageName); } catch { } throw new n8n_workflow_1.UnexpectedError(constants_2.RESPONSE_ERROR_MESSAGES.PACKAGE_LOADING_FAILED, { cause: error }); } if (loader.loadedNodes.length > 0) { try { if (isUpdate) { await this.removePackageFromDatabase(options.installedPackage); } const installedPackage = await this.persistInstalledPackage(loader); void this.publisher.publishCommand({ command: isUpdate ? 'community-package-update' : 'community-package-install', payload: { packageName, packageVersion }, }); await this.loadNodesAndCredentials.postProcessLoaders(); this.loadNodesAndCredentials.releaseTypes(); this.logger.info(`Community package installed: ${packageName}`); return installedPackage; } catch (error) { throw new n8n_workflow_1.UnexpectedError('Failed to save installed package', { extra: { packageName }, cause: error, }); } } else { try { await this.deletePackageDirectory(packageName); } catch { } throw new n8n_workflow_1.UnexpectedError(constants_2.RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES); } } async handleInstallEvent({ packageName, packageVersion, }) { await this.installOrUpdateNpmPackage(packageName, packageVersion); } async handleUninstallEvent({ packageName }) { await this.removeNpmPackage(packageName); } async installOrUpdateNpmPackage(packageName, packageVersion) { const authToken = this.getNpmAuthToken(); await this.downloadPackage(packageName, packageVersion, authToken); await this.loadNodesAndCredentials.unloadPackage(packageName); await this.loadNodesAndCredentials.loadPackage(packageName); await this.loadNodesAndCredentials.postProcessLoaders(); this.loadNodesAndCredentials.releaseTypes(); this.logger.info(`Community package installed: ${packageName}`); } async removeNpmPackage(packageName) { await this.deletePackageDirectory(packageName); await this.loadNodesAndCredentials.unloadPackage(packageName); await this.loadNodesAndCredentials.postProcessLoaders(); this.loadNodesAndCredentials.releaseTypes(); this.logger.info(`Community package uninstalled: ${packageName}`); } resolvePackageDirectory(packageName) { return `${this.downloadFolder}/node_modules/${packageName}`; } async downloadPackage(packageName, packageVersion, authToken) { const registry = this.getNpmRegistry(); const packageDirectory = this.resolvePackageDirectory(packageName); await this.deletePackageDirectory(packageName); await (0, promises_1.mkdir)(packageDirectory, { recursive: true }); const tarOutput = await (0, npm_utils_1.executeNpmCommand)(['pack', `${packageName}@${packageVersion}`, '--quiet'], { cwd: this.downloadFolder, registry, authToken }); const tarballName = tarOutput?.trim(); try { await asyncExecFile('tar', ['-xzf', tarballName, '-C', packageDirectory, '--strip-components=1'], { cwd: this.downloadFolder }); const packageJsonPath = `${packageDirectory}/package.json`; const packageJsonContent = await (0, promises_1.readFile)(packageJsonPath, 'utf-8'); const { devDependencies, peerDependencies, optionalDependencies, ...packageJson } = JSON.parse(packageJsonContent); await (0, promises_1.writeFile)(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf-8'); await (0, npm_utils_1.executeNpmCommand)([ 'install', '--audit=false', '--fund=false', '--bin-links=false', '--install-strategy=shallow', '--ignore-scripts=true', '--package-lock=false', ], { cwd: packageDirectory, registry, authToken }); await this.updatePackageJsonDependency(packageName, packageJson.version); } finally { await (0, promises_1.rm)((0, node_path_1.join)(this.downloadFolder, tarballName)); } return packageDirectory; } async deletePackageDirectory(packageName) { const packageDirectory = this.resolvePackageDirectory(packageName); await (0, promises_1.rm)(packageDirectory, { recursive: true, force: true }); } async updatePackageJsonDependency(packageName, version) { const existingContent = await (0, promises_1.readFile)(this.packageJsonPath, 'utf-8'); const packageJson = (0, n8n_workflow_1.jsonParse)(existingContent); packageJson.dependencies[packageName] = version; await (0, promises_1.writeFile)(this.packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf-8'); } }; exports.CommunityPackagesService = CommunityPackagesService; __decorate([ (0, decorators_1.OnPubSubEvent)('community-package-install'), (0, decorators_1.OnPubSubEvent)('community-package-update'), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], CommunityPackagesService.prototype, "handleInstallEvent", null); __decorate([ (0, decorators_1.OnPubSubEvent)('community-package-uninstall'), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], CommunityPackagesService.prototype, "handleUninstallEvent", null); exports.CommunityPackagesService = CommunityPackagesService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [n8n_core_1.InstanceSettings, backend_common_1.Logger, installed_packages_repository_1.InstalledPackagesRepository, load_nodes_and_credentials_1.LoadNodesAndCredentials, publisher_service_1.Publisher, license_1.License, community_packages_config_1.CommunityPackagesConfig]) ], CommunityPackagesService); //# sourceMappingURL=community-packages.service.js.map