bento-n8n-sdk
Version:
n8n community node for Bento integration
1,106 lines • 68.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Bento = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const buffer_1 = require("buffer");
const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
const INPUT_LIMITS = {
EMAIL: 254,
NAME: 50,
SUBJECT: 200,
CUSTOM_FIELD_KEY: 50,
CUSTOM_FIELD_VALUE: 500,
EVENT_NAME: 100,
EVENT_PROPERTY_KEY: 50,
EVENT_PROPERTY_VALUE: 500,
HTML_CONTENT: 50000,
TEXT_CONTENT: 50000,
USER_ID: 254,
IP_ADDRESS: 45,
VALIDATE_NAME: 100,
};
const DANGEROUS_HTML_PATTERNS = [
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
/javascript:/gi,
/on\w+\s*=/gi,
/<iframe\b[^>]*>/gi,
/<object\b[^>]*>/gi,
/<embed\b[^>]*>/gi,
/<form\b[^>]*>/gi,
/<input\b[^>]*>/gi,
/<meta\b[^>]*>/gi,
/<link\b[^>]*>/gi,
/data:(?!image\/)/gi,
/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi,
];
const SECURE_ERROR_MESSAGES = {
AUTHENTICATION_FAILED: 'Authentication failed. Please check your Bento API credentials.',
INVALID_REQUEST: 'Request validation failed. Please check your input parameters.',
API_ERROR: 'Bento API request failed. Please try again or contact support.',
NETWORK_ERROR: 'Network error occurred. Please check your connection and try again.',
VALIDATION_ERROR: 'Input validation failed. Please check your data format.',
RATE_LIMITED: 'Rate limit exceeded. Please wait before making more requests.',
SERVER_ERROR: 'Server error occurred. Please try again later.',
UNKNOWN_ERROR: 'An unexpected error occurred. Please contact support if the issue persists.',
};
const REQUEST_LIMITS = {
MAX_PAYLOAD_SIZE: 1024 * 1024,
DEFAULT_TIMEOUT: 30000,
MAX_RETRIES: 3,
RETRY_DELAY_BASE: 1000,
RATE_LIMIT_DELAY: 60000,
MAX_CONCURRENT_REQUESTS: 5,
};
const RETRYABLE_STATUS_CODES = [429, 500, 502, 503, 504];
const RETRYABLE_ERROR_CODES = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED'];
function isValidEmail(email) {
if (typeof email !== 'string' || email.trim() === '') {
return false;
}
const trimmedEmail = email.trim();
if (trimmedEmail.length > 254) {
return false;
}
if (!EMAIL_REGEX.test(trimmedEmail)) {
return false;
}
if (trimmedEmail.includes('..')) {
return false;
}
if (trimmedEmail.startsWith('.') || trimmedEmail.endsWith('.')) {
return false;
}
const [localPart, domainPart] = trimmedEmail.split('@');
if (localPart.length > 64) {
return false;
}
if (domainPart.length > 253) {
return false;
}
const domainParts = domainPart.split('.');
if (domainParts.length < 2) {
return false;
}
for (const part of domainParts) {
if (part.length === 0 || part.length > 63) {
return false;
}
if (part.startsWith('-') || part.endsWith('-')) {
return false;
}
}
return true;
}
function sanitizeEmail(email) {
if (typeof email !== 'string') {
return '';
}
return email.trim().toLowerCase();
}
function validateHtmlContent(html) {
if (typeof html !== 'string') {
return false;
}
for (const pattern of DANGEROUS_HTML_PATTERNS) {
if (pattern.test(html)) {
return false;
}
}
return true;
}
function sanitizeHtmlContent(html) {
if (typeof html !== 'string') {
return '';
}
let sanitized = html.trim();
for (const pattern of DANGEROUS_HTML_PATTERNS) {
sanitized = sanitized.replace(pattern, '');
}
sanitized = sanitized.replace(/javascript\s*:/gi, '');
sanitized = sanitized.replace(/vbscript\s*:/gi, '');
sanitized = sanitized.replace(/data\s*:/gi, '');
return sanitized;
}
function validateHtmlStructure(html) {
const issues = [];
if (typeof html !== 'string') {
issues.push('HTML content must be a string');
return { isValid: false, issues };
}
if (/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi.test(html)) {
issues.push('Script tags are not allowed');
}
if (/javascript:/gi.test(html)) {
issues.push('JavaScript URLs are not allowed');
}
if (/on\w+\s*=/gi.test(html)) {
issues.push('Event handlers (onclick, onload, etc.) are not allowed');
}
if (/<iframe\b[^>]*>/gi.test(html)) {
issues.push('Iframe tags are not allowed');
}
if (/<form\b[^>]*>/gi.test(html)) {
issues.push('Form tags are not allowed in email content');
}
if (/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi.test(html)) {
issues.push('Style tags are not recommended (use inline styles instead)');
}
const openTags = html.match(/<[^\/][^>]*>/g) || [];
const closeTags = html.match(/<\/[^>]*>/g) || [];
if (openTags.length !== closeTags.length) {
issues.push('HTML may contain unclosed tags');
}
return {
isValid: issues.length === 0,
issues
};
}
function validateInputLength(input, maxLength, fieldName, itemIndex) {
if (typeof input === 'string' && input.length > maxLength) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${fieldName} exceeds maximum length of ${maxLength} characters (current: ${input.length})`, { itemIndex });
}
}
function createSecureErrorMessage(error, operation) {
if (error.statusCode) {
switch (error.statusCode) {
case 400:
return `${SECURE_ERROR_MESSAGES.INVALID_REQUEST} (Operation: ${operation})`;
case 401:
return SECURE_ERROR_MESSAGES.AUTHENTICATION_FAILED;
case 403:
return SECURE_ERROR_MESSAGES.AUTHENTICATION_FAILED;
case 404:
return `${SECURE_ERROR_MESSAGES.API_ERROR} Resource not found.`;
case 429:
return SECURE_ERROR_MESSAGES.RATE_LIMITED;
case 500:
case 502:
case 503:
case 504:
return SECURE_ERROR_MESSAGES.SERVER_ERROR;
default:
return SECURE_ERROR_MESSAGES.API_ERROR;
}
}
if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
return SECURE_ERROR_MESSAGES.NETWORK_ERROR;
}
if (error.message && error.message.includes('validation')) {
return SECURE_ERROR_MESSAGES.VALIDATION_ERROR;
}
return SECURE_ERROR_MESSAGES.UNKNOWN_ERROR;
}
function logSecureError(error, operation, context) {
var _a;
this.logger.error('Bento Node Error', {
operation,
itemIndex: context.itemIndex,
endpoint: context.endpoint,
statusCode: error.statusCode,
errorCode: error.code,
hasMessage: !!error.message,
messageLength: ((_a = error.message) === null || _a === void 0 ? void 0 : _a.length) || 0,
timestamp: new Date().toISOString(),
});
}
function validatePayloadSize(body) {
if (!body)
return true;
const payloadSize = JSON.stringify(body).length;
return payloadSize <= REQUEST_LIMITS.MAX_PAYLOAD_SIZE;
}
function calculateBackoffDelay(attempt, baseDelay = REQUEST_LIMITS.RETRY_DELAY_BASE) {
const exponentialDelay = baseDelay * Math.pow(2, attempt);
const jitter = Math.random() * 0.1 * exponentialDelay;
return Math.min(exponentialDelay + jitter, 30000);
}
function shouldRetryRequest(error, attempt) {
if (attempt >= REQUEST_LIMITS.MAX_RETRIES) {
return false;
}
if (error.statusCode && RETRYABLE_STATUS_CODES.includes(error.statusCode)) {
return true;
}
if (error.code && RETRYABLE_ERROR_CODES.includes(error.code)) {
return true;
}
return false;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const activeRequests = new Map();
const requestQueue = new Map();
async function acquireRequestSlot(nodeId) {
const currentRequests = activeRequests.get(nodeId) || 0;
if (currentRequests >= REQUEST_LIMITS.MAX_CONCURRENT_REQUESTS) {
const queue = requestQueue.get(nodeId) || [];
return new Promise((resolve) => {
queue.push(resolve);
requestQueue.set(nodeId, queue);
});
}
activeRequests.set(nodeId, currentRequests + 1);
}
function releaseRequestSlot(nodeId) {
const currentRequests = activeRequests.get(nodeId) || 0;
const newCount = Math.max(0, currentRequests - 1);
activeRequests.set(nodeId, newCount);
const queue = requestQueue.get(nodeId) || [];
if (queue.length > 0 && newCount < REQUEST_LIMITS.MAX_CONCURRENT_REQUESTS) {
const nextResolve = queue.shift();
if (nextResolve) {
requestQueue.set(nodeId, queue);
activeRequests.set(nodeId, newCount + 1);
nextResolve();
}
}
}
class Bento {
constructor() {
this.description = {
displayName: 'Bento',
name: 'bento',
icon: 'file:bento.svg',
group: ['communication'],
version: 1,
subtitle: '={{$parameter["operation"]}}',
description: 'Native integration for Bento API actions',
defaults: {
name: 'Bento',
},
inputs: ["main"],
outputs: ["main"],
credentials: [
{
name: 'bentoApi',
required: true,
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Create Subscriber',
value: 'createSubscriber',
description: 'Add a new subscriber to your Bento audience with email and profile data',
action: 'Create a subscriber',
},
{
name: 'Get Subscriber',
value: 'getSubscriber',
description: 'Retrieve detailed information about an existing subscriber by email',
action: 'Get a subscriber',
},
{
name: 'Send Transactional Email',
value: 'sendTransactionalEmail',
description: 'Send personalized transactional emails using Bento templates',
action: 'Send a transactional email',
},
{
name: 'Subscriber Command',
value: 'subscriberCommand',
description: 'Execute commands on subscribers (add/remove tags, fields, subscribe/unsubscribe, etc.)',
action: 'Execute a subscriber command',
},
{
name: 'Track Event',
value: 'trackEvent',
description: 'Record custom events and behaviors for subscriber segmentation and automation',
action: 'Track an event',
},
{
name: 'Update Subscriber',
value: 'updateSubscriber',
description: 'Modify subscriber profile information and custom attributes',
action: 'Update a subscriber',
},
{
name: 'Validate Email',
value: 'validateEmail',
description: 'Validate email address for spam/throwaway detection',
action: 'Validate an email address',
},
],
default: 'createSubscriber',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
required: true,
displayOptions: {
show: {
operation: ['createSubscriber', 'getSubscriber', 'updateSubscriber'],
},
},
default: '',
placeholder: 'user@example.com',
description: 'The email address of the subscriber - must be a valid email format',
},
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
displayOptions: {
show: {
operation: ['createSubscriber', 'updateSubscriber'],
},
},
default: '',
placeholder: 'John',
description: 'The first name of the subscriber for personalization',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
displayOptions: {
show: {
operation: ['createSubscriber', 'updateSubscriber'],
},
},
default: '',
placeholder: 'Doe',
description: 'The last name of the subscriber for personalization',
},
{
displayName: 'Custom Fields',
name: 'customFields',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
operation: ['createSubscriber', 'updateSubscriber'],
},
},
default: {},
options: [
{
name: 'field',
displayName: 'Field',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
placeholder: 'company',
description: 'Custom field name',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
placeholder: 'Acme Corp',
description: 'Custom field value',
},
],
},
],
description: 'Additional custom fields to store with the subscriber',
},
{
displayName: 'User ID',
name: 'userId',
type: 'string',
required: true,
displayOptions: {
show: {
operation: ['trackEvent'],
},
},
default: '',
placeholder: 'user123@example.com',
description: 'The unique identifier for the user (typically email address)',
},
{
displayName: 'Event Name',
name: 'eventName',
type: 'string',
required: true,
displayOptions: {
show: {
operation: ['trackEvent'],
},
},
default: '',
placeholder: 'purchase_completed',
description: 'The name of the custom event to track (e.g., purchase_completed, page_viewed)',
},
{
displayName: 'Event Properties',
name: 'eventProperties',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
operation: ['trackEvent'],
},
},
default: {},
options: [
{
name: 'property',
displayName: 'Property',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'Property key',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Property value',
},
],
},
],
description: 'Additional properties and data to attach to the event for segmentation and personalization',
},
{
displayName: 'Recipient Email',
name: 'recipientEmail',
type: 'string',
required: true,
displayOptions: {
show: {
operation: ['sendTransactionalEmail'],
},
},
default: '',
placeholder: 'recipient@example.com',
description: 'The email address of the recipient',
},
{
displayName: 'From Email',
name: 'fromEmail',
type: 'string',
required: true,
displayOptions: {
show: {
operation: ['sendTransactionalEmail'],
},
},
default: '',
placeholder: 'noreply@yourcompany.com',
description: 'The sender email address',
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
required: true,
displayOptions: {
show: {
operation: ['sendTransactionalEmail'],
},
},
default: '',
placeholder: 'Reset Password',
description: 'Email subject line',
},
{
displayName: 'Email Type',
name: 'emailType',
type: 'options',
displayOptions: {
show: {
operation: ['sendTransactionalEmail'],
},
},
options: [
{
name: 'HTML Body',
value: 'html',
description: 'Send HTML formatted email',
},
{
name: 'Text Body',
value: 'text',
description: 'Send plain text email',
},
],
default: 'html',
description: 'Type of email body to send',
},
{
displayName: 'HTML Body',
name: 'htmlBody',
type: 'string',
typeOptions: {
rows: 8,
},
displayOptions: {
show: {
operation: ['sendTransactionalEmail'],
emailType: ['html'],
},
},
default: '',
placeholder: '<p>Here is a link to reset your password ... {{ link }}</p>',
description: 'HTML content of the email (supports template variables like {{ variable_name }})',
},
{
displayName: 'Text Body',
name: 'textBody',
type: 'string',
typeOptions: {
rows: 8,
},
displayOptions: {
show: {
operation: ['sendTransactionalEmail'],
emailType: ['text'],
},
},
default: '',
placeholder: 'Here is a link to reset your password ... {{ link }}',
description: 'Plain text content of the email (supports template variables like {{ variable_name }})',
},
{
displayName: 'Transactional',
name: 'transactional',
type: 'boolean',
displayOptions: {
show: {
operation: ['sendTransactionalEmail'],
},
},
default: false,
description: 'Whether this is a transactional email (affects tracking and analytics)',
},
{
displayName: 'Personalizations',
name: 'personalizations',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
operation: ['sendTransactionalEmail'],
},
},
default: {},
options: [
{
name: 'personalization',
displayName: 'Personalization',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
placeholder: 'link',
description: 'Template variable name (without {{ }})',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
placeholder: 'https://example.com/reset',
description: 'Value to substitute in template',
},
],
},
],
description: 'Template variables to personalize the email (e.g., link, first_name, order_total)',
},
{
displayName: 'Email',
name: 'commandEmail',
type: 'string',
required: true,
displayOptions: {
show: {
operation: ['subscriberCommand'],
},
},
default: '',
placeholder: 'user@example.com',
description: 'The email address of the subscriber to execute the command on',
},
{
displayName: 'Command',
name: 'command',
type: 'options',
required: true,
displayOptions: {
show: {
operation: ['subscriberCommand'],
},
},
options: [
{
name: 'Add Field',
value: 'add_field',
description: 'Add a custom field to the subscriber',
},
{
name: 'Add Tag',
value: 'add_tag',
description: 'Add a tag to the subscriber',
},
{
name: 'Add Tag via Event',
value: 'add_tag_via_event',
description: 'Add a tag to the subscriber via event',
},
{
name: 'Change Email',
value: 'change_email',
description: 'Change the email address of the subscriber',
},
{
name: 'Remove Field',
value: 'remove_field',
description: 'Remove a custom field from the subscriber',
},
{
name: 'Remove Tag',
value: 'remove_tag',
description: 'Remove a tag from the subscriber',
},
{
name: 'Subscribe',
value: 'subscribe',
description: 'Subscribe the email address',
},
{
name: 'Unsubscribe',
value: 'unsubscribe',
description: 'Unsubscribe the email address',
},
],
default: 'add_tag',
description: 'The command to execute on the subscriber',
},
{
displayName: 'Tag/Field Name',
name: 'query',
type: 'string',
displayOptions: {
show: {
operation: ['subscriberCommand'],
command: ['add_tag', 'add_tag_via_event', 'remove_tag', 'remove_field'],
},
},
default: '',
placeholder: 'vip_customer',
description: 'The name of the tag or field to add/remove',
},
{
displayName: 'Field Key',
name: 'fieldKey',
type: 'string',
displayOptions: {
show: {
operation: ['subscriberCommand'],
command: ['add_field'],
},
},
default: '',
placeholder: 'company',
description: 'The key/name of the custom field',
},
{
displayName: 'Field Value',
name: 'fieldValue',
type: 'string',
displayOptions: {
show: {
operation: ['subscriberCommand'],
command: ['add_field'],
},
},
default: '',
placeholder: 'Acme Corp',
description: 'The value of the custom field',
},
{
displayName: 'New Email',
name: 'newEmail',
type: 'string',
displayOptions: {
show: {
operation: ['subscriberCommand'],
command: ['change_email'],
},
},
default: '',
placeholder: 'newemail@example.com',
description: 'The new email address for the subscriber',
},
{
displayName: 'Email',
name: 'validateEmail',
type: 'string',
required: true,
displayOptions: {
show: {
operation: ['validateEmail'],
},
},
default: '',
placeholder: 'test@example.com',
description: 'The email address to validate',
},
{
displayName: 'Name',
name: 'validateName',
type: 'string',
displayOptions: {
show: {
operation: ['validateEmail'],
},
},
default: '',
placeholder: 'John Doe',
description: 'The name associated with the email (optional but recommended for better validation)',
},
{
displayName: 'IP Address',
name: 'validateIp',
type: 'string',
displayOptions: {
show: {
operation: ['validateEmail'],
},
},
default: '',
placeholder: '1.1.1.1',
description: 'The IP address associated with the email (optional but recommended for better validation)',
},
],
};
}
async execute() {
const items = this.getInputData();
const returnData = [];
for (let i = 0; i < items.length; i++) {
try {
const operation = this.getNodeParameter('operation', i);
let responseData;
switch (operation) {
case 'createSubscriber': {
const email = sanitizeEmail(this.getNodeParameter('email', i));
const firstName = this.getNodeParameter('firstName', i);
const lastName = this.getNodeParameter('lastName', i);
const customFields = this.getNodeParameter('customFields', i);
validateInputLength.call(this, email, INPUT_LIMITS.EMAIL, 'Email', i);
validateInputLength.call(this, firstName, INPUT_LIMITS.NAME, 'First Name', i);
validateInputLength.call(this, lastName, INPUT_LIMITS.NAME, 'Last Name', i);
if (!email) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Email is required for creating a subscriber', {
itemIndex: i,
});
}
if (!isValidEmail(email)) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid email format', {
itemIndex: i,
});
}
const details = {
first_name: firstName || '',
last_name: lastName || '',
};
if (customFields.field) {
for (const field of customFields.field) {
if (field.key && field.value) {
validateInputLength.call(this, field.key, INPUT_LIMITS.CUSTOM_FIELD_KEY, 'Custom Field Key', i);
validateInputLength.call(this, field.value, INPUT_LIMITS.CUSTOM_FIELD_VALUE, 'Custom Field Value', i);
details[field.key] = field.value;
}
}
}
const requestBody = {
events: [
{
email: email,
type: '$subscribe',
details: details
}
]
};
try {
const response = await makeBentoRequest.call(this, 'POST', '/api/v1/batch/events', requestBody, i);
responseData = {
operation: 'createSubscriber',
success: true,
subscriber: {
email,
firstName,
lastName,
},
apiResponse: response,
message: 'Subscriber created successfully using Events API',
};
}
catch (error) {
logSecureError.call(this, error, 'createSubscriber', { itemIndex: i });
responseData = {
operation: 'createSubscriber',
success: false,
subscriber: {
email: email ? '[REDACTED]' : undefined,
firstName,
lastName,
},
error: createSecureErrorMessage(error, 'createSubscriber'),
message: 'Failed to create subscriber. Please check your credentials and input data.',
};
}
break;
}
case 'getSubscriber': {
const email = sanitizeEmail(this.getNodeParameter('email', i));
validateInputLength.call(this, email, INPUT_LIMITS.EMAIL, 'Email', i);
if (!email) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Email is required for getting subscriber information', {
itemIndex: i,
});
}
if (!isValidEmail(email)) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid email format', {
itemIndex: i,
});
}
try {
const response = await makeBentoRequest.call(this, 'GET', `/api/v1/fetch/subscribers?email=${encodeURIComponent(email)}`, undefined, i);
responseData = {
operation: 'getSubscriber',
success: true,
email,
subscriber: response,
apiResponse: response,
message: 'Subscriber retrieved successfully',
};
}
catch (error) {
logSecureError.call(this, error, 'getSubscriber', { itemIndex: i });
responseData = {
operation: 'getSubscriber',
success: false,
email: email ? '[REDACTED]' : undefined,
error: createSecureErrorMessage(error, 'getSubscriber'),
message: 'Failed to retrieve subscriber. Please check your credentials and email address.',
};
}
break;
}
case 'updateSubscriber': {
const email = sanitizeEmail(this.getNodeParameter('email', i));
const firstName = this.getNodeParameter('firstName', i);
const lastName = this.getNodeParameter('lastName', i);
const customFields = this.getNodeParameter('customFields', i);
validateInputLength.call(this, email, INPUT_LIMITS.EMAIL, 'Email', i);
validateInputLength.call(this, firstName, INPUT_LIMITS.NAME, 'First Name', i);
validateInputLength.call(this, lastName, INPUT_LIMITS.NAME, 'Last Name', i);
if (!email) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Email is required for updating a subscriber', {
itemIndex: i,
});
}
if (!isValidEmail(email)) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid email format', {
itemIndex: i,
});
}
const subscriberData = {
email: email,
first_name: firstName || '',
last_name: lastName || '',
};
if (customFields.field) {
for (const field of customFields.field) {
if (field.key && field.value) {
validateInputLength.call(this, field.key, INPUT_LIMITS.CUSTOM_FIELD_KEY, 'Custom Field Key', i);
validateInputLength.call(this, field.value, INPUT_LIMITS.CUSTOM_FIELD_VALUE, 'Custom Field Value', i);
subscriberData[field.key] = field.value;
}
}
}
const requestBody = {
subscribers: [subscriberData]
};
try {
const response = await makeBentoRequest.call(this, 'POST', '/api/v1/batch/subscribers', requestBody, i);
responseData = {
operation: 'updateSubscriber',
success: true,
subscriber: {
email,
firstName,
lastName,
},
apiResponse: response,
message: 'Subscriber updated successfully using Import Subscribers API',
};
}
catch (error) {
logSecureError.call(this, error, 'updateSubscriber', { itemIndex: i });
responseData = {
operation: 'updateSubscriber',
success: false,
subscriber: {
email: email ? '[REDACTED]' : undefined,
firstName,
lastName,
},
error: createSecureErrorMessage(error, 'updateSubscriber'),
message: 'Failed to update subscriber. Please check your credentials and input data.',
};
}
break;
}
case 'trackEvent': {
const userId = this.getNodeParameter('userId', i);
const eventName = this.getNodeParameter('eventName', i);
const eventProperties = this.getNodeParameter('eventProperties', i);
validateInputLength.call(this, userId, INPUT_LIMITS.USER_ID, 'User ID', i);
validateInputLength.call(this, eventName, INPUT_LIMITS.EVENT_NAME, 'Event Name', i);
if (!userId || !eventName) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'User ID and event name are required for tracking events', {
itemIndex: i,
});
}
const properties = {};
if (eventProperties.property) {
for (const prop of eventProperties.property) {
if (prop.key && prop.value) {
validateInputLength.call(this, prop.key, INPUT_LIMITS.EVENT_PROPERTY_KEY, 'Event Property Key', i);
validateInputLength.call(this, prop.value, INPUT_LIMITS.EVENT_PROPERTY_VALUE, 'Event Property Value', i);
properties[prop.key] = prop.value;
}
}
}
const requestBody = {
events: [
{
email: userId,
type: eventName,
details: properties,
}
]
};
try {
const response = await makeBentoRequest.call(this, 'POST', '/api/v1/batch/events', requestBody, i);
responseData = {
operation: 'trackEvent',
success: true,
event: {
userId,
eventName,
properties,
},
apiResponse: response,
message: 'Event tracked successfully',
};
}
catch (error) {
logSecureError.call(this, error, 'trackEvent', { itemIndex: i });
responseData = {
operation: 'trackEvent',
success: false,
event: {
userId: userId ? '[REDACTED]' : undefined,
eventName,
properties: Object.keys(properties).length > 0 ? '[REDACTED]' : {},
},
error: createSecureErrorMessage(error, 'trackEvent'),
message: 'Failed to track event. Please check your credentials and event data.',
};
}
break;
}
case 'sendTransactionalEmail': {
const recipientEmail = sanitizeEmail(this.getNodeParameter('recipientEmail', i));
const fromEmail = sanitizeEmail(this.getNodeParameter('fromEmail', i));
const subject = this.getNodeParameter('subject', i);
const emailType = this.getNodeParameter('emailType', i);
const transactional = this.getNodeParameter('transactional', i);
const personalizations = this.getNodeParameter('personalizations', i);
validateInputLength.call(this, recipientEmail, INPUT_LIMITS.EMAIL, 'Recipient Email', i);
validateInputLength.call(this, fromEmail, INPUT_LIMITS.EMAIL, 'From Email', i);
validateInputLength.call(this, subject, INPUT_LIMITS.SUBJECT, 'Subject', i);
let htmlBody = '';
let textBody = '';
if (emailType === 'html') {
htmlBody = this.getNodeParameter('htmlBody', i);
if (htmlBody) {
const htmlValidation = validateHtmlStructure(htmlBody);
if (!htmlValidation.isValid) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `HTML content validation failed: ${htmlValidation.issues.join(', ')}`, { itemIndex: i });
}
htmlBody = sanitizeHtmlContent(htmlBody);
}
}
else {
textBody = this.getNodeParameter('textBody', i);
}
if (emailType === 'html') {
validateInputLength.call(this, htmlBody, INPUT_LIMITS.HTML_CONTENT, 'HTML Body', i);
}
else {
validateInputLength.call(this, textBody, INPUT_LIMITS.TEXT_CONTENT, 'Text Body', i);
}
if (!recipientEmail || !fromEmail || !subject) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Recipient email, from email, and subject are required for sending transactional emails', {
itemIndex: i,
});
}
if (!isValidEmail(recipientEmail)) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid recipient email format', {
itemIndex: i,
});
}
if (!isValidEmail(fromEmail)) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid from email format', {
itemIndex: i,
});
}
if (emailType === 'html' && !htmlBody) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'HTML body is required when email type is HTML', {
itemIndex: i,
});
}
if (emailType === 'text' && !textBody) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Text body is required when email type is Text', {
itemIndex: i,
});
}
if (!htmlBody && !textBody) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Either HTML body or Text body is required', {
itemIndex: i,
});
}
if (emailType === 'html' && htmlBody) {
if (!validateHtmlContent(htmlBody)) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'HTML content contains potentially dangerous elements and cannot be sent', { itemIndex: i });
}
}
const personalizationsData = {};
if (personalizations.personalization) {
for (const item of personalizations.personalization) {
if (item.key && item.value) {
if (typeof item.value === 'string' && item.value.includes('<')) {
if (!validateHtmlContent(item.value)) {
throw new n8n_workflow_1.NodeOperat