UNPKG

n8n-nodes-base

Version:

Base nodes of n8n

297 lines 13.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Webhook = void 0; /* eslint-disable n8n-nodes-base/node-execute-block-wrong-error-thrown */ const fs_1 = require("fs"); const promises_1 = require("fs/promises"); const isbot_1 = __importDefault(require("isbot")); const n8n_workflow_1 = require("n8n-workflow"); const promises_2 = require("stream/promises"); const tmp_promise_1 = require("tmp-promise"); const uuid_1 = require("uuid"); const description_1 = require("./description"); const error_1 = require("./error"); const utils_1 = require("./utils"); class Webhook extends n8n_workflow_1.Node { authPropertyName = 'authentication'; description = { displayName: 'Webhook', icon: { light: 'file:webhook.svg', dark: 'file:webhook.dark.svg' }, name: 'webhook', group: ['trigger'], version: [1, 1.1, 2, 2.1], defaultVersion: 2.1, description: 'Starts the workflow when a webhook is called', eventTriggerDescription: 'Waiting for you to call the Test URL', activationMessage: 'You can now make calls to your production webhook URL.', defaults: { name: 'Webhook', }, supportsCORS: true, triggerPanel: { header: '', executionsHelp: { inactive: "Webhooks have two modes: test and production. <br /> <br /> <b>Use test mode while you build your workflow</b>. Click the 'listen' button, then make a request to the test URL. The executions will show up in the editor.<br /> <br /> <b>Use production mode to run your workflow automatically</b>. Publish the workflow, then make requests to the production URL. These executions will show up in the executions list, but not in the editor.", active: 'Webhooks have two modes: test and production. <br /> <br /> <b>Use test mode while you build your workflow</b>. Click the \'listen\' button, then make a request to the test URL. The executions will show up in the editor.<br /> <br /> <b>Use production mode to run your workflow automatically</b>. Since the workflow is activated, you can make requests to the production URL. These executions will show up in the <a data-key="executions">executions list</a>, but not in the editor.', }, activationHint: "Once you've finished building your workflow, run it without having to click this button by using the production webhook URL.", }, inputs: [], outputs: `={{(${utils_1.configuredOutputs})($parameter)}}`, credentials: (0, description_1.credentialsProperty)(this.authPropertyName), webhooks: [description_1.defaultWebhookDescription], sensitiveOutputFields: ['headers.authorization', 'headers.cookie'], properties: [ { displayName: 'Allow Multiple HTTP Methods', name: 'multipleMethods', type: 'boolean', default: false, isNodeSetting: true, description: 'Whether to allow the webhook to listen for multiple HTTP methods', }, { ...description_1.httpMethodsProperty, displayOptions: { show: { multipleMethods: [false], }, }, }, { displayName: 'HTTP Methods', name: 'httpMethod', type: 'multiOptions', options: [ { name: 'DELETE', value: 'DELETE', }, { name: 'GET', value: 'GET', }, { name: 'HEAD', value: 'HEAD', }, { name: 'PATCH', value: 'PATCH', }, { name: 'POST', value: 'POST', }, { name: 'PUT', value: 'PUT', }, ], default: ['GET', 'POST'], description: 'The HTTP methods to listen to', displayOptions: { show: { multipleMethods: [true], }, }, }, { displayName: 'Path', name: 'path', type: 'string', default: '', placeholder: 'webhook', builderHint: { message: 'The webhook path that triggers this workflow', placeholderSupported: false, }, description: "The path to listen to, dynamic values could be specified by using ':', e.g. 'your-path/:dynamic-value'. If dynamic values are set 'webhookId' would be prepended to path.", }, (0, description_1.authenticationProperty)(this.authPropertyName), description_1.responseModeProperty, description_1.responseModePropertyStreaming, { displayName: 'Insert a \'Respond to Webhook\' node to control when and how you respond. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.respondtowebhook/" target="_blank">More details</a>', name: 'webhookNotice', type: 'notice', displayOptions: { show: { responseMode: ['responseNode'], }, }, default: '', }, { displayName: 'Insert a node that supports streaming (e.g. \'AI Agent\') and enable streaming to stream directly to the response while the workflow is executed. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.respondtowebhook/" target="_blank">More details</a>', name: 'webhookStreamingNotice', type: 'notice', displayOptions: { show: { responseMode: ['streaming'], }, }, default: '', }, { ...description_1.responseCodeProperty, displayOptions: { show: { '@version': [1, 1.1], }, hide: { responseMode: ['responseNode'], }, }, }, description_1.responseDataProperty, description_1.responseBinaryPropertyNameProperty, { displayName: 'If you are sending back a response, add a "Content-Type" response header with the appropriate value to avoid unexpected behavior', name: 'contentTypeNotice', type: 'notice', default: '', displayOptions: { show: { responseMode: ['onReceived'], }, }, }, { ...description_1.optionsProperty, options: [...description_1.optionsProperty.options, description_1.responseCodeOption].sort((a, b) => { const nameA = a.displayName.toUpperCase(); const nameB = b.displayName.toUpperCase(); if (nameA < nameB) return -1; if (nameA > nameB) return 1; return 0; }), }, ], }; async webhook(context) { const { typeVersion: nodeVersion, type: nodeType } = context.getNode(); const responseMode = context.getNodeParameter('responseMode', 'onReceived'); if (nodeVersion >= 2 && nodeType === 'n8n-nodes-base.webhook') { (0, utils_1.checkResponseModeConfiguration)(context); } const options = context.getNodeParameter('options', {}); const req = context.getRequestObject(); const resp = context.getResponseObject(); const requestMethod = context.getRequestObject().method; if (!(0, utils_1.isIpAllowed)(options.ipWhitelist, req.ips, req.ip)) { resp.writeHead(403); resp.end('IP is not allowed to access the webhook!'); return { noWebhookResponse: true }; } let validationData; try { if (options.ignoreBots && (0, isbot_1.default)(req.headers['user-agent'])) throw new error_1.WebhookAuthorizationError(403); validationData = await this.validateAuth(context); } catch (error) { if (error instanceof error_1.WebhookAuthorizationError) { resp.writeHead(error.responseCode, { 'WWW-Authenticate': 'Basic realm="Webhook"' }); resp.end(error.message); return { noWebhookResponse: true }; } throw error; } const prepareOutput = (0, utils_1.setupOutputConnection)(context, requestMethod, { jwtPayload: validationData, }); if (options.binaryData) { return await this.handleBinaryData(context, prepareOutput); } if (req.contentType === 'multipart/form-data') { return await (0, utils_1.handleFormData)(context, prepareOutput); } if (nodeVersion > 1 && !req.body && !options.rawBody) { try { return await this.handleBinaryData(context, prepareOutput); } catch (error) { } } if (options.rawBody && !req.rawBody) { await req.readRawBody(); } const response = { json: { headers: req.headers, params: req.params, query: req.query, body: req.body, }, binary: options.rawBody ? { data: { data: (req.rawBody ?? '').toString(n8n_workflow_1.BINARY_ENCODING), mimeType: req.contentType ?? 'application/json', }, } : undefined, }; if (responseMode === 'streaming') { const res = context.getResponseObject(); // Set up streaming response headers res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }); // Flush headers immediately res.flushHeaders(); return { noWebhookResponse: true, workflowData: prepareOutput(response), }; } return { webhookResponse: options.responseData, workflowData: prepareOutput(response), }; } async validateAuth(context) { return await (0, utils_1.validateWebhookAuthentication)(context, this.authPropertyName); } async handleBinaryData(context, prepareOutput) { const req = context.getRequestObject(); const options = context.getNodeParameter('options', {}); // TODO: create empty binaryData placeholder, stream into that path, and then finalize the binaryData const binaryFile = await (0, tmp_promise_1.file)({ prefix: 'n8n-webhook-' }); try { await (0, promises_2.pipeline)(req, (0, fs_1.createWriteStream)(binaryFile.path)); const returnItem = { json: { headers: req.headers, params: req.params, query: req.query, body: {}, }, }; const stats = await (0, promises_1.stat)(binaryFile.path); if (stats.size) { const binaryPropertyName = (options.binaryPropertyName ?? 'data'); const fileName = req.contentDisposition?.filename ?? (0, uuid_1.v4)(); const binaryData = await context.nodeHelpers.copyBinaryFile(binaryFile.path, fileName, req.contentType ?? 'application/octet-stream'); returnItem.binary = { [binaryPropertyName]: binaryData }; } return { workflowData: prepareOutput(returnItem) }; } catch (error) { throw new n8n_workflow_1.NodeOperationError(context.getNode(), error); } finally { await binaryFile.cleanup(); } } } exports.Webhook = Webhook; //# sourceMappingURL=Webhook.node.js.map