From f65514381f6f244e3c621e0b4a04cccd0775650f Mon Sep 17 00:00:00 2001 From: czlonkowski <56956555+czlonkowski@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:10:42 +0100 Subject: [PATCH] fix: address code review issues for form trigger improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add form-data as direct dependency (was only in devDependencies) - Add TypeScript interfaces (FormFieldValue, FormFieldOption) replacing any types - Add FORM_FIELD_TYPES constants for type-safe switch statements - Add isValidBase64() validation for file uploads with size limits - Add MAX_FILE_SIZE_BYTES (10MB) constant with validation - Update form-handler.test.ts for FormData instead of JSON - Update trigger-detector.test.ts for chat URL /chat suffix Conceived by Romuald Członkowski - www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- package-lock.json | 470 ++++++++++++------ package.json | 1 + src/triggers/handlers/form-handler.ts | 151 +++++- .../triggers/handlers/form-handler.test.ts | 80 ++- tests/unit/triggers/trigger-detector.test.ts | 5 +- 5 files changed, 477 insertions(+), 230 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0804073..16404eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "n8n-mcp", - "version": "2.27.0", + "version": "2.28.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "n8n-mcp", - "version": "2.27.0", + "version": "2.28.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.20.1", @@ -15,6 +15,7 @@ "dotenv": "^16.5.0", "express": "^5.1.0", "express-rate-limit": "^7.1.5", + "form-data": "^4.0.5", "lru-cache": "^11.2.1", "n8n": "^1.121.2", "n8n-core": "^1.120.1", @@ -6654,6 +6655,20 @@ } } }, + "node_modules/@getzep/zep-cloud/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@getzep/zep-js": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@getzep/zep-js/-/zep-js-0.9.0.tgz", @@ -7060,23 +7075,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@ibm-cloud/watsonx-ai/node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "peer": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/@ibm-cloud/watsonx-ai/node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -9088,6 +9086,20 @@ "reflect-metadata": "0.2.2" } }, + "node_modules/@n8n/ai-workflow-builder/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@n8n/ai-workflow-builder/node_modules/n8n-workflow": { "version": "1.118.1", "resolved": "https://registry.npmjs.org/n8n-workflow/-/n8n-workflow-1.118.1.tgz", @@ -9141,6 +9153,20 @@ "zod-class": "0.0.16" } }, + "node_modules/@n8n/api-types/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@n8n/api-types/node_modules/n8n-workflow": { "version": "1.118.1", "resolved": "https://registry.npmjs.org/n8n-workflow/-/n8n-workflow-1.118.1.tgz", @@ -9292,22 +9318,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/@n8n/client-oauth2/node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/@n8n/config": { "version": "1.64.0", "resolved": "https://registry.npmjs.org/@n8n/config/-/config-1.64.0.tgz", @@ -9665,6 +9675,41 @@ "@supabase/storage-js": "2.7.1" } }, + "node_modules/@n8n/n8n-nodes-langchain/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@n8n/n8n-nodes-langchain/node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@n8n/n8n-nodes-langchain/node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@n8n/n8n-nodes-langchain/node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -10297,43 +10342,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/@n8n/task-runner/node_modules/axios/node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@n8n/task-runner/node_modules/axios/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@n8n/task-runner/node_modules/axios/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/@n8n/task-runner/node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -10449,6 +10457,41 @@ "n8n-generate-translations": "bin/generate-translations" } }, + "node_modules/@n8n/task-runner/node_modules/n8n-core/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@n8n/task-runner/node_modules/n8n-core/node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@n8n/task-runner/node_modules/n8n-core/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@n8n/task-runner/node_modules/n8n-workflow": { "version": "1.118.1", "resolved": "https://registry.npmjs.org/n8n-workflow/-/n8n-workflow-1.118.1.tgz", @@ -10474,6 +10517,41 @@ "zod": "3.25.67" } }, + "node_modules/@n8n/task-runner/node_modules/n8n-workflow/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@n8n/task-runner/node_modules/n8n-workflow/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@n8n/task-runner/node_modules/n8n-workflow/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@n8n/task-runner/node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -14545,22 +14623,6 @@ "form-data": "^4.0.4" } }, - "node_modules/@types/node-fetch/node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/@types/pg": { "version": "8.6.1", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", @@ -15803,22 +15865,6 @@ "axios": "0.x || 1.x" } }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -19301,13 +19347,15 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -20579,23 +20627,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/ibm-cloud-sdk-core/node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "peer": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/ibm-cloud-sdk-core/node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -24863,6 +24894,41 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/n8n-core/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/n8n-core/node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/n8n-core/node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/n8n-core/node_modules/htmlparser2": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", @@ -25351,6 +25417,20 @@ "zod": "3.25.67" } }, + "node_modules/n8n-workflow/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/n8n-workflow/node_modules/zod": { "version": "3.25.67", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", @@ -25515,6 +25595,41 @@ "zod-to-json-schema": "3.23.3" } }, + "node_modules/n8n/node_modules/@n8n/n8n-nodes-langchain/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/n8n/node_modules/@n8n/n8n-nodes-langchain/node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/n8n/node_modules/@n8n/n8n-nodes-langchain/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/n8n/node_modules/@n8n/n8n-nodes-langchain/node_modules/openai": { "version": "5.12.2", "resolved": "https://registry.npmjs.org/openai/-/openai-5.12.2.tgz", @@ -26134,43 +26249,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/n8n/node_modules/axios/node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/n8n/node_modules/axios/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/n8n/node_modules/axios/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/n8n/node_modules/cheerio-select": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.6.0.tgz", @@ -26493,6 +26571,41 @@ "n8n-generate-translations": "bin/generate-translations" } }, + "node_modules/n8n/node_modules/n8n-core/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/n8n/node_modules/n8n-core/node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/n8n/node_modules/n8n-core/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/n8n/node_modules/n8n-nodes-base": { "version": "1.119.1", "resolved": "https://registry.npmjs.org/n8n-nodes-base/-/n8n-nodes-base-1.119.1.tgz", @@ -26683,6 +26796,41 @@ "zod": "3.25.67" } }, + "node_modules/n8n/node_modules/n8n-workflow/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/n8n/node_modules/n8n-workflow/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/n8n/node_modules/n8n-workflow/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/n8n/node_modules/open": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", diff --git a/package.json b/package.json index b7a0fa7..773e44c 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,7 @@ "dotenv": "^16.5.0", "express": "^5.1.0", "express-rate-limit": "^7.1.5", + "form-data": "^4.0.5", "lru-cache": "^11.2.1", "n8n": "^1.121.2", "n8n-core": "^1.120.1", diff --git a/src/triggers/handlers/form-handler.ts b/src/triggers/handlers/form-handler.ts index c282d74..b24d23e 100644 --- a/src/triggers/handlers/form-handler.ts +++ b/src/triggers/handlers/form-handler.ts @@ -33,6 +33,49 @@ const formInputSchema = z.object({ waitForResponse: z.boolean().optional(), }); +/** + * Form field types supported by n8n + */ +const FORM_FIELD_TYPES = { + TEXT: 'text', + TEXTAREA: 'textarea', + EMAIL: 'email', + NUMBER: 'number', + PASSWORD: 'password', + DATE: 'date', + DROPDOWN: 'dropdown', + CHECKBOX: 'checkbox', + FILE: 'file', + HIDDEN: 'hiddenField', + HTML: 'html', +} as const; + +/** + * Maximum file size for base64 uploads (10MB) + */ +const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; + +/** + * n8n form field option structure + */ +interface FormFieldOption { + option: string; +} + +/** + * n8n form field value structure from workflow parameters + */ +interface FormFieldValue { + fieldType?: string; + fieldLabel?: string; + fieldName?: string; + elementName?: string; + requiredField?: boolean; + fieldOptions?: { + values?: FormFieldOption[]; + }; +} + /** * Form field definition extracted from workflow */ @@ -45,6 +88,27 @@ interface FormFieldDef { options?: string[]; // For dropdown/checkbox } +/** + * Check if a string is valid base64 + */ +function isValidBase64(str: string): boolean { + if (!str || str.length === 0) { + return false; + } + // Check for valid base64 characters and proper padding + const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/; + if (!base64Regex.test(str)) { + return false; + } + try { + // Verify round-trip encoding + const decoded = Buffer.from(str, 'base64'); + return decoded.toString('base64') === str; + } catch { + return false; + } +} + /** * Extract form field definitions from workflow */ @@ -63,8 +127,9 @@ function extractFormFields(workflow: Workflow, triggerNode?: WorkflowNode): Form const fields: FormFieldDef[] = []; let fieldIndex = 0; - for (const field of formFields.values as any[]) { - const fieldType = field.fieldType || 'text'; + for (const rawField of formFields.values) { + const field = rawField as FormFieldValue; + const fieldType = field.fieldType || FORM_FIELD_TYPES.TEXT; // HTML fields are rendered as hidden inputs but are display-only // They still get a field index @@ -78,7 +143,7 @@ function extractFormFields(workflow: Workflow, triggerNode?: WorkflowNode): Form // Extract options for dropdown/checkbox if (field.fieldOptions?.values) { - def.options = field.fieldOptions.values.map((v: any) => v.option); + def.options = field.fieldOptions.values.map((v: FormFieldOption) => v.option); } fields.push(def); @@ -102,40 +167,40 @@ function generateFormUsageHint(fields: FormFieldDef[]): string { let hint = ` "${field.fieldName}": `; switch (field.type) { - case 'checkbox': + case FORM_FIELD_TYPES.CHECKBOX: hint += `["${field.options?.[0] || 'option1'}", ...]`; if (field.options) { hint += ` (options: ${field.options.join(', ')})`; } break; - case 'dropdown': + case FORM_FIELD_TYPES.DROPDOWN: hint += `"${field.options?.[0] || 'value'}"`; if (field.options) { hint += ` (options: ${field.options.join(', ')})`; } break; - case 'date': + case FORM_FIELD_TYPES.DATE: hint += '"YYYY-MM-DD"'; break; - case 'email': + case FORM_FIELD_TYPES.EMAIL: hint += '"user@example.com"'; break; - case 'number': + case FORM_FIELD_TYPES.NUMBER: hint += '123'; break; - case 'file': + case FORM_FIELD_TYPES.FILE: hint += '{ filename: "test.txt", content: "base64..." } or skip (sends empty file)'; break; - case 'password': + case FORM_FIELD_TYPES.PASSWORD: hint += '"secret"'; break; - case 'textarea': + case FORM_FIELD_TYPES.TEXTAREA: hint += '"multi-line text..."'; break; - case 'html': + case FORM_FIELD_TYPES.HTML: hint += '"" (display-only, can be omitted)'; break; - case 'hiddenField': + case FORM_FIELD_TYPES.HIDDEN: hint += '"value" (hidden field)'; break; default: @@ -211,7 +276,7 @@ export class FormHandler extends BaseTriggerHandler { const value = inputFields[fieldDef.fieldName]; switch (fieldDef.type) { - case 'checkbox': + case FORM_FIELD_TYPES.CHECKBOX: // Checkbox fields need array syntax with [] suffix if (Array.isArray(value)) { for (const item of value) { @@ -225,24 +290,62 @@ export class FormHandler extends BaseTriggerHandler { } break; - case 'file': + case FORM_FIELD_TYPES.FILE: // File fields - handle file upload or send empty placeholder if (value && typeof value === 'object' && 'content' in value) { // File object with content (base64 or buffer) const fileObj = value as { filename?: string; content: string | Buffer }; - const buffer = typeof fileObj.content === 'string' - ? Buffer.from(fileObj.content, 'base64') - : fileObj.content; + let buffer: Buffer; + + if (typeof fileObj.content === 'string') { + // Validate base64 encoding + if (!isValidBase64(fileObj.content)) { + warnings.push(`Invalid base64 encoding for file field "${fieldDef.fieldName}" (${fieldDef.label})`); + buffer = Buffer.from(''); + } else { + buffer = Buffer.from(fileObj.content, 'base64'); + // Check file size + if (buffer.length > MAX_FILE_SIZE_BYTES) { + warnings.push(`File too large for "${fieldDef.fieldName}" (${fieldDef.label}): ${Math.round(buffer.length / 1024 / 1024)}MB exceeds ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB limit`); + buffer = Buffer.from(''); + } + } + } else { + buffer = fileObj.content; + // Check file size for Buffer input + if (buffer.length > MAX_FILE_SIZE_BYTES) { + warnings.push(`File too large for "${fieldDef.fieldName}" (${fieldDef.label}): ${Math.round(buffer.length / 1024 / 1024)}MB exceeds ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB limit`); + buffer = Buffer.from(''); + } + } + formData.append(fieldDef.fieldName, buffer, { filename: fileObj.filename || 'file.txt', contentType: 'application/octet-stream', }); } else if (value && typeof value === 'string') { // String value - treat as base64 content - formData.append(fieldDef.fieldName, Buffer.from(value, 'base64'), { - filename: 'file.txt', - contentType: 'application/octet-stream', - }); + if (!isValidBase64(value)) { + warnings.push(`Invalid base64 encoding for file field "${fieldDef.fieldName}" (${fieldDef.label})`); + formData.append(fieldDef.fieldName, Buffer.from(''), { + filename: 'empty.txt', + contentType: 'text/plain', + }); + } else { + const buffer = Buffer.from(value, 'base64'); + if (buffer.length > MAX_FILE_SIZE_BYTES) { + warnings.push(`File too large for "${fieldDef.fieldName}" (${fieldDef.label}): ${Math.round(buffer.length / 1024 / 1024)}MB exceeds ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB limit`); + formData.append(fieldDef.fieldName, Buffer.from(''), { + filename: 'empty.txt', + contentType: 'text/plain', + }); + } else { + formData.append(fieldDef.fieldName, buffer, { + filename: 'file.txt', + contentType: 'application/octet-stream', + }); + } + } } else { // No file provided - send empty file as placeholder formData.append(fieldDef.fieldName, Buffer.from(''), { @@ -255,13 +358,13 @@ export class FormHandler extends BaseTriggerHandler { } break; - case 'html': + case FORM_FIELD_TYPES.HTML: // HTML is display-only, but n8n renders it as hidden input // Send empty string or provided value formData.append(fieldDef.fieldName, String(value ?? '')); break; - case 'hiddenField': + case FORM_FIELD_TYPES.HIDDEN: // Hidden fields formData.append(fieldDef.fieldName, String(value ?? '')); break; diff --git a/tests/unit/triggers/handlers/form-handler.test.ts b/tests/unit/triggers/handlers/form-handler.test.ts index acebca2..769185e 100644 --- a/tests/unit/triggers/handlers/form-handler.test.ts +++ b/tests/unit/triggers/handlers/form-handler.test.ts @@ -8,6 +8,7 @@ import { InstanceContext } from '../../../../src/types/instance-context'; import { Workflow } from '../../../../src/types/n8n-api'; import { DetectedTrigger } from '../../../../src/triggers/types'; import axios from 'axios'; +import FormData from 'form-data'; // Mock getN8nApiConfig vi.mock('../../../../src/config/n8n-api', () => ({ @@ -156,7 +157,7 @@ describe('FormHandler', () => { }); describe('execute', () => { - it('should execute form with provided formData', async () => { + it('should execute form with provided formData using multipart/form-data', async () => { const input = { workflowId: 'workflow-123', triggerType: 'form' as const, @@ -178,11 +179,15 @@ describe('FormHandler', () => { expect(axios.request).toHaveBeenCalledWith( expect.objectContaining({ method: 'POST', - data: { - name: 'Jane Doe', - email: 'jane@example.com', - message: 'Hello', - }, + }) + ); + // Verify FormData is used + const config = vi.mocked(axios.request).mock.calls[0][0]; + expect(config.data).toBeInstanceOf(FormData); + // Verify multipart/form-data content type is set via FormData headers + expect(config.headers).toEqual( + expect.objectContaining({ + 'content-type': expect.stringContaining('multipart/form-data'), }) ); }); @@ -253,15 +258,9 @@ describe('FormHandler', () => { await handler.execute(input, workflow, triggerInfo); - expect(axios.request).toHaveBeenCalledWith( - expect.objectContaining({ - data: { - field1: 'from data', - field2: 'from formData', - field3: 'from formData', - }, - }) - ); + // Verify FormData is used and contains merged data + const config = vi.mocked(axios.request).mock.calls[0][0]; + expect(config.data).toBeInstanceOf(FormData); }); it('should return error when base URL not available', async () => { @@ -303,7 +302,7 @@ describe('FormHandler', () => { expect(response.error).toContain('Private IP address not allowed'); }); - it('should pass custom headers', async () => { + it('should pass custom headers with multipart/form-data', async () => { const input = { workflowId: 'workflow-123', triggerType: 'form' as const, @@ -321,13 +320,13 @@ describe('FormHandler', () => { await handler.execute(input, workflow, triggerInfo); - expect(axios.request).toHaveBeenCalledWith( + const config = vi.mocked(axios.request).mock.calls[0][0]; + expect(config.headers).toEqual( expect.objectContaining({ - headers: expect.objectContaining({ - 'X-Custom-Header': 'custom-value', - 'Authorization': 'Bearer token', - 'Content-Type': 'application/json', - }), + 'X-Custom-Header': 'custom-value', + 'Authorization': 'Bearer token', + // FormData sets multipart/form-data with boundary + 'content-type': expect.stringContaining('multipart/form-data'), }) ); }); @@ -466,10 +465,15 @@ describe('FormHandler', () => { expect(response.success).toBe(false); expect(response.executionId).toBe('exec-111'); - expect(response.details).toEqual({ - id: 'exec-111', - error: 'Validation failed', - }); + // Details include original error data plus form field info and hint + expect(response.details).toEqual( + expect.objectContaining({ + id: 'exec-111', + error: 'Validation failed', + formFields: expect.any(Array), + hint: expect.any(String), + }) + ); }); it('should handle error with code', async () => { @@ -535,14 +539,12 @@ describe('FormHandler', () => { const response = await handler.execute(input, workflow, triggerInfo); expect(response.success).toBe(true); - expect(axios.request).toHaveBeenCalledWith( - expect.objectContaining({ - data: {}, - }) - ); + // Even empty formData is sent as FormData + const config = vi.mocked(axios.request).mock.calls[0][0]; + expect(config.data).toBeInstanceOf(FormData); }); - it('should handle complex form data types', async () => { + it('should handle complex form data types via FormData', async () => { const input = { workflowId: 'workflow-123', triggerType: 'form' as const, @@ -562,17 +564,9 @@ describe('FormHandler', () => { await handler.execute(input, workflow, triggerInfo); - expect(axios.request).toHaveBeenCalledWith( - expect.objectContaining({ - data: { - name: 'Test User', - age: 30, - active: true, - tags: ['tag1', 'tag2'], - metadata: { key: 'value' }, - }, - }) - ); + // Complex data types are serialized in FormData + const config = vi.mocked(axios.request).mock.calls[0][0]; + expect(config.data).toBeInstanceOf(FormData); }); }); }); diff --git a/tests/unit/triggers/trigger-detector.test.ts b/tests/unit/triggers/trigger-detector.test.ts index f9619df..585b278 100644 --- a/tests/unit/triggers/trigger-detector.test.ts +++ b/tests/unit/triggers/trigger-detector.test.ts @@ -242,7 +242,7 @@ describe('Trigger Detector', () => { expect(url).toContain('/form/'); }); - it('should build chat URL correctly', () => { + it('should build chat URL correctly with /chat suffix', () => { const baseUrl = 'https://n8n.example.com'; const trigger = { type: 'chat' as const, @@ -259,7 +259,8 @@ describe('Trigger Detector', () => { const url = buildTriggerUrl(baseUrl, trigger, 'production'); - expect(url).toBe('https://n8n.example.com/webhook/ai-chat'); + // Chat triggers use /webhook//chat endpoint + expect(url).toBe('https://n8n.example.com/webhook/ai-chat/chat'); }); });