import { Box, Icon, Menu, Throbber } from '@rocket.chat/fuselage';
import {
	useSetModal,
	useMethod,
	useEndpoint,
	useTranslation,
	useRoute,
	useRouteParameter,
	useToastMessageDispatch,
	useCurrentRoute,
} from '@rocket.chat/ui-contexts';
import React, { useMemo, useCallback, useState } from 'react';
import semver from 'semver';

import { Apps } from '../../../../app/apps/client/orchestrator';
import WarningModal from '../../../components/WarningModal';
import AppPermissionsReviewModal from './AppPermissionsReviewModal';
import CloudLoginModal from './CloudLoginModal';
import IframeModal from './IframeModal';
import { appEnabledStatuses, handleAPIError, appButtonProps, warnEnableDisableApp } from './helpers';
import { marketplaceActions } from './helpers/marketplaceActions';

const openIncompatibleModal = async (app, action, cancel, setModal) => {
	try {
		const incompatibleData = await Apps.buildIncompatibleExternalUrl(app.id, app.marketplaceVersion, action);
		setModal(<IframeModal url={incompatibleData.url} cancel={cancel} />);
	} catch (e) {
		handleAPIError(e);
	}
};

function AppMenu({ app, isAppDetailsPage, ...props }) {
	const t = useTranslation();
	const dispatchToastMessage = useToastMessageDispatch();
	const setModal = useSetModal();
	const checkUserLoggedIn = useMethod('cloud:checkUserLoggedIn');

	const [currentRouteName, currentRouteParams] = useCurrentRoute();
	if (!currentRouteName) {
		throw new Error('No current route name');
	}
	const router = useRoute(currentRouteName);

	const context = useRouteParameter('context');
	const currentTab = useRouteParameter('tab');

	const setAppStatus = useEndpoint('POST', `/apps/${app.id}/status`);
	const buildExternalUrl = useEndpoint('GET', '/apps');
	const syncApp = useEndpoint('POST', `/apps/${app.id}/sync`);
	const uninstallApp = useEndpoint('DELETE', `/apps/${app.id}`);

	const [loading, setLoading] = useState(false);

	const canAppBeSubscribed = app.purchaseType === 'subscription';
	const isSubscribed = app.subscriptionInfo && ['active', 'trialing'].includes(app.subscriptionInfo.status);
	const isAppEnabled = appEnabledStatuses.includes(app.status);
	const [isAppPurchased, setPurchased] = useState(app?.isPurchased);

	const button = appButtonProps(app || {});
	const action = button?.action || '';

	const cancelAction = useCallback(() => {
		setModal(null);
		setLoading(false);
	}, [setModal]);

	const confirmAction = useCallback(
		async (permissionsGranted) => {
			setModal(null);

			await marketplaceActions[action]({ ...app, permissionsGranted });

			setLoading(false);
		},
		[setModal, action, app, setLoading],
	);

	const showAppPermissionsReviewModal = useCallback(() => {
		if (!isAppPurchased) {
			setPurchased(true);
		}

		return setModal(<AppPermissionsReviewModal appPermissions={app.permissions} onCancel={cancelAction} onConfirm={confirmAction} />);
	}, [app.permissions, cancelAction, confirmAction, isAppPurchased, setModal, setPurchased]);

	const closeModal = useCallback(() => {
		setModal(null);
		setLoading(false);
	}, [setModal]);

	const handleSubscription = useCallback(async () => {
		if (!(await checkUserLoggedIn())) {
			setModal(<CloudLoginModal />);
			return;
		}

		if (app?.versionIncompatible && !isSubscribed) {
			openIncompatibleModal(app, 'subscribe', closeModal, setModal);
			return;
		}

		let data;
		try {
			data = await buildExternalUrl({
				buildExternalUrl: 'true',
				appId: app.id,
				purchaseType: app.purchaseType,
				details: true,
			});
		} catch (error) {
			handleAPIError(error);
			return;
		}

		const confirm = async () => {
			try {
				await syncApp();
			} catch (error) {
				handleAPIError(error);
			}
		};

		setModal(<IframeModal url={data.url} confirm={confirm} cancel={closeModal} />);
	}, [checkUserLoggedIn, app, setModal, closeModal, isSubscribed, buildExternalUrl, syncApp]);

	const handleAcquireApp = useCallback(async () => {
		setLoading(true);

		const isLoggedIn = await checkUserLoggedIn();

		if (!isLoggedIn) {
			setLoading(false);
			setModal(<CloudLoginModal />);
			return;
		}

		if (app?.versionIncompatible) {
			openIncompatibleModal(app, 'subscribe', closeModal, setModal);
			return;
		}

		if (action === 'purchase' && !isAppPurchased) {
			try {
				const data = await Apps.buildExternalUrl(app.id, app.purchaseType, false);
				setModal(<IframeModal url={data.url} cancel={cancelAction} confirm={showAppPermissionsReviewModal} />);
			} catch (error) {
				handleAPIError(error);
			}
			return;
		}

		showAppPermissionsReviewModal();
	}, [action, app, closeModal, cancelAction, checkUserLoggedIn, isAppPurchased, setModal, showAppPermissionsReviewModal]);

	const handleViewLogs = useCallback(() => {
		router.push({ context, page: 'info', id: app.id, version: app.version, tab: 'logs' });
	}, [app.id, app.version, context, router]);

	const handleDisable = useCallback(() => {
		const confirm = async () => {
			closeModal();
			try {
				const { status } = await setAppStatus({ status: 'manually_disabled' });
				warnEnableDisableApp(app.name, status, 'disable');
			} catch (error) {
				handleAPIError(error);
			}
		};
		setModal(
			<WarningModal close={closeModal} confirm={confirm} text={t('Apps_Marketplace_Deactivate_App_Prompt')} confirmText={t('Yes')} />,
		);
	}, [app.name, closeModal, setAppStatus, setModal, t]);

	const handleEnable = useCallback(async () => {
		try {
			const { status } = await setAppStatus({ status: 'manually_enabled' });
			warnEnableDisableApp(app.name, status, 'enable');
		} catch (error) {
			handleAPIError(error);
		}
	}, [app.name, setAppStatus]);

	const handleUninstall = useCallback(() => {
		const uninstall = async () => {
			closeModal();
			try {
				const { success } = await uninstallApp();
				if (success) {
					dispatchToastMessage({ type: 'success', message: `${app.name} uninstalled` });
					if (context === 'details' && currentTab !== 'details') {
						router.replace({ ...currentRouteParams, tab: 'details' });
					}
				}
			} catch (error) {
				handleAPIError(error);
			}
		};

		if (isSubscribed) {
			const confirm = async () => {
				await handleSubscription();
			};

			setModal(
				<WarningModal
					close={closeModal}
					cancel={uninstall}
					confirm={confirm}
					text={t('Apps_Marketplace_Uninstall_Subscribed_App_Prompt')}
					confirmText={t('Apps_Marketplace_Modify_App_Subscription')}
					cancelText={t('Apps_Marketplace_Uninstall_Subscribed_App_Anyway')}
				/>,
			);
		}

		setModal(
			<WarningModal close={closeModal} confirm={uninstall} text={t('Apps_Marketplace_Uninstall_App_Prompt')} confirmText={t('Yes')} />,
		);
	}, [
		app?.name,
		closeModal,
		context,
		currentTab,
		dispatchToastMessage,
		handleSubscription,
		isSubscribed,
		currentRouteParams,
		router,
		setModal,
		t,
		uninstallApp,
	]);

	const incompatibleIconName = useCallback(
		(app, action) => {
			if (!app.versionIncompatible) {
				if (action === 'update') {
					return 'refresh';
				}

				return 'card';
			}

			// Now we are handling an incompatible app
			if (action === 'subscribe' && !isSubscribed) {
				return 'warning';
			}

			if (action === 'install' || action === 'update') {
				return 'warning';
			}

			return 'card';
		},
		[isSubscribed],
	);

	const handleUpdate = useCallback(async () => {
		setLoading(true);

		if (app?.versionIncompatible) {
			openIncompatibleModal(app, 'update', closeModal, setModal);
			return;
		}

		const isLoggedIn = await checkUserLoggedIn();

		if (!isLoggedIn) {
			setLoading(false);
			setModal(<CloudLoginModal />);
			return;
		}

		showAppPermissionsReviewModal();
	}, [checkUserLoggedIn, app, closeModal, setModal, showAppPermissionsReviewModal]);

	const canUpdate = app.installed && app.version && app.marketplaceVersion && semver.lt(app.version, app.marketplaceVersion);

	const menuOptions = useMemo(() => {
		const bothAppStatusOptions = {
			...(canAppBeSubscribed &&
				isSubscribed && {
					subscribe: {
						label: (
							<Box>
								<Icon name={incompatibleIconName(app, 'subscribe')} size='x16' marginInlineEnd='x4' />
								{t('Subscription')}
							</Box>
						),
						action: handleSubscription,
					},
				}),
		};

		const nonInstalledAppOptions = {
			...(!app.installed && {
				acquire: {
					label: (
						<Box>
							<Icon name={incompatibleIconName(app, 'install')} size='x16' marginInlineEnd='x4' />
							{t(button.label.replace(' ', '_'))}
						</Box>
					),
					action: handleAcquireApp,
				},
			}),
		};

		const installedAppOptions = {
			...(context !== 'details' &&
				app.installed && {
					viewLogs: {
						label: (
							<Box>
								<Icon name='list-alt' size='x16' marginInlineEnd='x4' />
								{t('View_Logs')}
							</Box>
						),
						action: handleViewLogs,
					},
				}),
			...(canUpdate &&
				!isAppDetailsPage && {
					update: {
						label: (
							<Box>
								<Icon name={incompatibleIconName(app, 'update')} size='x16' marginInlineEnd='x4' />
								{t('Update')}
							</Box>
						),
						action: handleUpdate,
					},
				}),
			...(app.installed &&
				isAppEnabled && {
					disable: {
						label: (
							<Box color='on-warning'>
								<Icon name='ban' size='x16' marginInlineEnd='x4' />
								{t('Disable')}
							</Box>
						),
						action: handleDisable,
					},
				}),
			...(app.installed &&
				!isAppEnabled && {
					enable: {
						label: (
							<Box>
								<Icon name='check' size='x16' marginInlineEnd='x4' />
								{t('Enable')}
							</Box>
						),
						action: handleEnable,
					},
				}),
			...(app.installed && {
				divider: {
					type: 'divider',
				},
			}),
			...(app.installed && {
				uninstall: {
					label: (
						<Box color='on-danger'>
							<Icon name='trash' size='x16' marginInlineEnd='x4' />
							{t('Uninstall')}
						</Box>
					),
					action: handleUninstall,
				},
			}),
		};

		return {
			...bothAppStatusOptions,
			...nonInstalledAppOptions,
			...installedAppOptions,
		};
	}, [
		canAppBeSubscribed,
		isSubscribed,
		app,
		t,
		handleSubscription,
		button?.label,
		handleAcquireApp,
		context,
		handleViewLogs,
		canUpdate,
		isAppDetailsPage,
		handleUpdate,
		isAppEnabled,
		handleDisable,
		handleEnable,
		handleUninstall,
		incompatibleIconName,
	]);

	return loading ? <Throbber disabled /> : <Menu options={menuOptions} placement='bottom-start' maxHeight='initial' {...props} />;
}

export default AppMenu;


mailData.browserInfo = `Rocket.Chat App ${app?.bundle || app?.version}`;

mailData.osInfo = `${os.name} ${os.version || ''}`;

import { test, expect } from './utils/test';
import { IS_EE } from './config/constants';

test.skip(!IS_EE, 'Engagemente Dashboard > Enterprise Only');

test.use({ storageState: 'admin-session.json' });

test.describe('engagement-dashboard', () => {
	test.describe.parallel('expect to trigger fallback error component', () => {
		test.beforeEach(async ({ page }) => {
			await page.goto('/admin/engagement-dashboard');
			await page.route('**/api/v1/engagement-dashboard/**', (route) => route.abort());
		});

		test('expect to show 4 fallback errors components inside widget at Users Tab', async ({ page }) => {
			await expect(
				page.locator('[data-qa-id="EngagementDashboardPage-usersTab"][aria-selected="true"]', { hasText: 'Users' }),
			).toBeVisible();

			await page.waitForSelector('[data-qa="EngagementDashboardCardErrorBoundary"]');
			await expect(page.locator('[data-qa="EngagementDashboardCardErrorBoundary"]')).toHaveCount(4);
		});

		test('expect to show 2 fallback errors components inside widget at Messages Tab', async ({ page }) => {
			await page.locator('[data-qa-id="EngagementDashboardPage-messagesTab"]').click();
			await expect(
				page.locator('[data-qa-id="EngagementDashboardPage-messagesTab"][aria-selected="true"]', { hasText: 'Messages' }),
			).toBeVisible();

			await page.waitForSelector('[data-qa="EngagementDashboardCardErrorBoundary"]');
			await expect(page.locator('[data-qa="EngagementDashboardCardErrorBoundary"]')).toHaveCount(2);
		});

		test('expect to show a fallback error component inside widget at Channels Tab', async ({ page }) => {
			await page.locator('[data-qa-id="EngagementDashboardPage-channelsTab"]').click();
			await expect(
				page.locator('[data-qa-id="EngagementDashboardPage-channelsTab"][aria-selected="true"]', { hasText: 'Channels' }),
			).toBeVisible();

			await page.waitForSelector('[data-qa="EngagementDashboardCardErrorBoundary"]');
			await expect(page.locator('[data-qa="EngagementDashboardCardErrorBoundary"]')).toBeVisible();
		});
	});
});


mailData.browserInfo = `Rocket.Chat ${app?.name || browser.name} ${app?.bundle || app?.version || browser.version}`;

mailData.deviceInfo = `Desktop App ${cpu.architecture || ''}`;

mailData.userAgent = connection.httpHeaders['user-agent'] || '';

keep keep keep keep replace keep keep keep keep keep

import { Box, Button, TableRow, TableCell } from '@rocket.chat/fuselage';
import { useMediaQuery } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React from 'react';

import { useFormatDateAndTime } from '../../../../../../client/hooks/useFormatDateAndTime';
import DeviceIcon from '../../../../deviceManagement/components/DeviceIcon';
import { useDeviceLogout } from '../../../../hooks/useDeviceLogout';

type DevicesRowProps = {
	_id: string;
	deviceName?: string;
	deviceType?: string;
	deviceOSName?: string;
	deviceOSVersion?: string;
	loginAt: string;
	onReload: () => void;
};

const DeviceManagementAccountRow = ({
	_id,
	deviceName,
	deviceType = 'browser',
	deviceOSName,
	deviceOSVersion,
	loginAt,
	onReload,
}: DevicesRowProps): ReactElement => {
	const t = useTranslation();
	const formatDateAndTime = useFormatDateAndTime();
	const mediaQuery = useMediaQuery('(min-width: 1024px)');

	const handleDeviceLogout = useDeviceLogout(_id, '/v1/sessions/logout.me');

	return (
		<TableRow key={_id}>
			<TableCell>
				<Box display='flex' alignItems='center'>
					<DeviceIcon deviceType={deviceType} />
					{deviceName && <Box withTruncatedText>{deviceName}</Box>}
				</Box>
			</TableCell>
			<TableCell>{`${deviceOSName || ''} ${deviceOSVersion || ''}`}</TableCell>
			<TableCell>{formatDateAndTime(loginAt)}</TableCell>
			{mediaQuery && <TableCell>{_id}</TableCell>}
			<TableCell align='end'>
				<Button onClick={(): void => handleDeviceLogout(onReload)}>{t('Logout')}</Button>
			</TableCell>
		</TableRow>
	);
};

export default DeviceManagementAccountRow;


import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import _ from 'underscore';
import moment from 'moment';
import { SmarshHistory } from '@rocket.chat/models';

import { settings } from '../../../settings/server';
import { Rooms, Messages, Users } from '../../../models/server';
import { MessageTypes } from '../../../ui-utils';
import { smarsh } from '../lib/rocketchat';
import 'moment-timezone';

const start =
	'

';
const end = '

';
const opentr = '
';
const closetr = '
';
const open20td = '';
const open60td = '';
const closetd = '	';

function _getLink(attachment) {
	const url = attachment.title_link.replace(/ /g, '%20');

	if (url.match(/^(https?:)?\/\//i)) {
		return url;
	}
	return Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX + url;
}

smarsh.generateEml = () => {
	Meteor.defer(() => {
		const smarshMissingEmail = settings.get('Smarsh_MissingEmail_Email');
		const timeZone = settings.get('Smarsh_Timezone');

		Rooms.find().forEach(async (room) => {
			const smarshHistory = await SmarshHistory.findOne({ _id: room._id });
			const query = { rid: room._id };

			if (smarshHistory) {
				query.ts = { $gt: smarshHistory.lastRan };
			}

			const date = new Date();
			const rows = [];
			const data = {
				users: [],
				msgs: 0,
				files: [],
				time: smarshHistory ? moment(date).diff(moment(smarshHistory.lastRan), 'minutes') : moment(date).diff(moment(room.ts), 'minutes'),
				room: room.name ? `#${room.name}` : `Direct Message Between: ${room.usernames.join(' & ')}`,
			};

			Messages.find(query).forEach((message) => {
				rows.push(opentr);

				// The timestamp
				rows.push(open20td);
				rows.push(moment(message.ts).tz(timeZone).format('YYYY-MM-DD HH-mm-ss z'));
				rows.push(closetd);

				// The sender
				rows.push(open20td);
				const sender = Users.findOne({ _id: message.u._id });
				if (data.users.indexOf(sender._id) === -1) {
					data.users.push(sender._id);
				}

				// Get the user's email, can be nothing if it is an unconfigured bot account (like rocket.cat)
				if (sender.emails && sender.emails[0] && sender.emails[0].address) {
					rows.push(`${sender.name} <${sender.emails[0].address}>`);
				} else {
					rows.push(`${sender.name} <${smarshMissingEmail}>`);
				}
				rows.push(closetd);

				// The message
				rows.push(open60td);
				data.msgs++;
				if (message.t) {
					const messageType = MessageTypes.getType(message);
					if (messageType) {
						rows.push(TAPi18n.__(messageType.message, messageType.data ? messageType.data(message) : '', 'en'));
					} else {
						rows.push(`${message.msg} (${message.t})`);
					}
				} else if (message.file) {
					data.files.push(message.file._id);
					rows.push(`${message.attachments[0].title} (${_getLink(message.attachments[0])})`);
				} else if (message.attachments) {
					const attaches = [];
					_.each(message.attachments, function _loopThroughMessageAttachments(a) {
						if (a.image_url) {
							attaches.push(a.image_url);
						}
						// TODO: Verify other type of attachments which need to be handled that aren't file uploads and image urls
						// } else {
						// 	console.log(a);
						// }
					});

					rows.push(`${message.msg} (${attaches.join(', ')})`);
				} else {
					rows.push(message.msg);
				}
				rows.push(closetd);

				rows.push(closetr);
			});

			if (rows.length !== 0) {
				const result = start + rows.join('') + end;

				SmarshHistory.upsert(
					{ _id: room._id },
					{
						_id: room._id,
						lastRan: date,
						lastResult: result,
					},
				);

				smarsh.sendEmail({
					body: result,
					subject: `Rocket.Chat, ${data.users.length} Users, ${data.msgs} Messages, ${data.files.length} Files, ${data.time} Minutes, in ${data.room}`,
					files: data.files,
				});
			}
		});
	});
};


import { useContext } from 'react';

import type { CustomSoundContextValue } from '../CustomSoundContext';
import { CustomSoundContext } from '../CustomSoundContext';

export const useCustomSound = (): CustomSoundContextValue => useContext(CustomSoundContext);


import { Box, Flex, Grid } from '@rocket.chat/fuselage';
import type * as UiKit from '@rocket.chat/ui-kit';
import type { ReactElement } from 'react';
import { memo, useMemo } from 'react';

import type { BlockProps } from '../utils/BlockProps';
import Fields from './SectionBlock.Fields';

type SectionBlockProps = BlockProps<UiKit.SectionBlock>;

const SectionBlock = ({
  className,
  block,
  surfaceRenderer,
}: SectionBlockProps): ReactElement => {
  const { text, fields } = block;

  const accessoryElement = useMemo(
    () =>
      block.accessory
        ? {
            appId: block.appId,
            blockId: block.blockId,
            ...block.accessory,
          }
        : undefined,
    [block.appId, block.blockId, block.accessory]
  );

  return (
    <Grid className={className}>
      <Grid.Item>
        {text && (
          <Box is='span' fontScale='p2' color='default'>
            {surfaceRenderer.text(text)}
          </Box>
        )}
        {fields && <Fields fields={fields} surfaceRenderer={surfaceRenderer} />}
      </Grid.Item>
      {block.accessory && (
        <Flex.Item grow={0}>
          <Grid.Item>
            {accessoryElement
              ? surfaceRenderer.renderSectionAccessoryBlockElement(
                  accessoryElement,
                  0
                )
              : null}
          </Grid.Item>
        </Flex.Item>
      )}
    </Grid>
  );
};

export default memo(SectionBlock);


import type { User } from '@db/entities/User';

import { NodeTypes } from './NodeTypes';

import type { ExecutionMetadata } from '@db/entities/ExecutionMetadata';

import { RoleService } from './services/role.service';

import type { EventPayloadWorkflow } from './eventbus/EventMessageClasses/EventMessageWorkflow';

import { determineFinalExecutionStatus } from './executionLifecycleHooks/shared/sharedHookFunctions';

keep keep keep keep replace keep keep keep

import express from 'express';
import http from 'http';
import type PCancelable from 'p-cancelable';
import { Container } from 'typedi';

import { flags } from '@oclif/command';
import { WorkflowExecute } from 'n8n-core';

import type {
	ExecutionError,
	ExecutionStatus,
	IExecuteResponsePromiseData,
	INodeTypes,
	IRun,
} from 'n8n-workflow';
import { Workflow, NodeOperationError, sleep } from 'n8n-workflow';

import * as Db from '@/Db';
import * as ResponseHelper from '@/ResponseHelper';
import * as WebhookHelpers from '@/WebhookHelpers';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import { PermissionChecker } from '@/UserManagement/PermissionChecker';

import config from '@/config';
import type { Job, JobId, JobResponse, WebhookResponse } from '@/Queue';
import { Queue } from '@/Queue';
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
import { N8N_VERSION } from '@/constants';
import { BaseCommand } from './BaseCommand';
import { ExecutionRepository } from '@db/repositories';
import { OwnershipService } from '@/services/ownership.service';
import type { ICredentialsOverwrite } from '@/Interfaces';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { rawBodyReader, bodyParser } from '@/middlewares';
import { eventBus } from '@/eventbus';
import type { RedisServicePubSubSubscriber } from '@/services/redis/RedisServicePubSubSubscriber';
import { EventMessageGeneric } from '@/eventbus/EventMessageClasses/EventMessageGeneric';
import { IConfig } from '@oclif/config';
import { OrchestrationHandlerWorkerService } from '@/services/orchestration/worker/orchestration.handler.worker.service';
import { OrchestrationWorkerService } from '@/services/orchestration/worker/orchestration.worker.service';
import type { WorkerJobStatusSummary } from '../services/orchestration/worker/types';

export class Worker extends BaseCommand {
	static description = '\nStarts a n8n worker';

	static examples = ['$ n8n worker --concurrency=5'];

	static flags = {
		help: flags.help({ char: 'h' }),
		concurrency: flags.integer({
			default: 10,
			description: 'How many jobs can run in parallel.',
		}),
	};

	static runningJobs: {
		[key: string]: PCancelable<IRun>;
	} = {};

	static runningJobsSummary: {
		[jobId: string]: WorkerJobStatusSummary;
	} = {};

	static jobQueue: Queue;

	redisSubscriber: RedisServicePubSubSubscriber;

	/**
	 * Stop n8n in a graceful way.
	 * Make for example sure that all the webhooks from third party services
	 * get removed.
	 */
	async stopProcess() {
		this.logger.info('Stopping n8n...');

		// Stop accepting new jobs
		await Worker.jobQueue.pause(true);

		try {
			await this.externalHooks.run('n8n.stop', []);

			const maxStopTime = config.getEnv('queue.bull.gracefulShutdownTimeout') * 1000;

			const stopTime = new Date().getTime() + maxStopTime;

			setTimeout(async () => {
				// In case that something goes wrong with shutdown we
				// kill after max. 30 seconds no matter what
				await this.exitSuccessFully();
			}, maxStopTime);

			// Wait for active workflow executions to finish
			let count = 0;
			while (Object.keys(Worker.runningJobs).length !== 0) {
				if (count++ % 4 === 0) {
					const waitLeft = Math.ceil((stopTime - new Date().getTime()) / 1000);
					this.logger.info(
						`Waiting for ${
							Object.keys(Worker.runningJobs).length
						} active executions to finish... (wait ${waitLeft} more seconds)`,
					);
				}

				await sleep(500);
			}
		} catch (error) {
			await this.exitWithCrash('There was an error shutting down n8n.', error);
		}

		await this.exitSuccessFully();
	}

	async runJob(job: Job, nodeTypes: INodeTypes): Promise<JobResponse> {
		const { executionId, loadStaticData } = job.data;
		const fullExecutionData = await Container.get(ExecutionRepository).findSingleExecution(
			executionId,
			{
				includeData: true,
				unflattenData: true,
			},
		);

		if (!fullExecutionData) {
			this.logger.error(
				`Worker failed to find data of execution "${executionId}" in database. Cannot continue.`,
				{ executionId },
			);
			throw new Error(
				`Unable to find data of execution "${executionId}" in database. Aborting execution.`,
			);
		}
		const workflowId = fullExecutionData.workflowData.id!;
		this.logger.info(
			`Start job: ${job.id} (Workflow ID: ${workflowId} | Execution: ${executionId})`,
		);

		const workflowOwner = await Container.get(OwnershipService).getWorkflowOwnerCached(workflowId);

		let { staticData } = fullExecutionData.workflowData;
		if (loadStaticData) {
			const workflowData = await Db.collections.Workflow.findOne({
				select: ['id', 'staticData'],
				where: {
					id: workflowId,
				},
			});
			if (workflowData === null) {
				this.logger.error(
					'Worker execution failed because workflow could not be found in database.',
					{ workflowId, executionId },
				);
				throw new Error(`The workflow with the ID "${workflowId}" could not be found`);
			}
			staticData = workflowData.staticData;
		}

		const workflowSettings = fullExecutionData.workflowData.settings ?? {};

		let workflowTimeout = workflowSettings.executionTimeout ?? config.getEnv('executions.timeout'); // initialize with default

		let executionTimeoutTimestamp: number | undefined;
		if (workflowTimeout > 0) {
			workflowTimeout = Math.min(workflowTimeout, config.getEnv('executions.maxTimeout'));
			executionTimeoutTimestamp = Date.now() + workflowTimeout * 1000;
		}

		const workflow = new Workflow({
			id: workflowId,
			name: fullExecutionData.workflowData.name,
			nodes: fullExecutionData.workflowData.nodes,
			connections: fullExecutionData.workflowData.connections,
			active: fullExecutionData.workflowData.active,
			nodeTypes,
			staticData,
			settings: fullExecutionData.workflowData.settings,
		});

		const additionalData = await WorkflowExecuteAdditionalData.getBase(
			workflowOwner.id,
			undefined,
			executionTimeoutTimestamp,
		);
		additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(
			fullExecutionData.mode,
			job.data.executionId,
			fullExecutionData.workflowData,
			{
				retryOf: fullExecutionData.retryOf as string,
			},
		);

		try {
			await PermissionChecker.check(workflow, workflowOwner.id);
		} catch (error) {
			if (error instanceof NodeOperationError) {
				const failedExecution = generateFailedExecutionFromError(
					fullExecutionData.mode,
					error,
					error.node,
				);
				await additionalData.hooks.executeHookFunctions('workflowExecuteAfter', [failedExecution]);
			}
			return { success: true, error: error as ExecutionError };
		}

		additionalData.hooks.hookFunctions.sendResponse = [
			async (response: IExecuteResponsePromiseData): Promise<void> => {
				const progress: WebhookResponse = {
					executionId,
					response: WebhookHelpers.encodeWebhookResponse(response),
				};
				await job.progress(progress);
			},
		];

		additionalData.executionId = executionId;

		additionalData.setExecutionStatus = (status: ExecutionStatus) => {
			// Can't set the status directly in the queued worker, but it will happen in InternalHook.onWorkflowPostExecute
			this.logger.debug(`Queued worker execution status for ${executionId} is "${status}"`);
		};

		let workflowExecute: WorkflowExecute;
		let workflowRun: PCancelable<IRun>;
		if (fullExecutionData.data !== undefined) {
			workflowExecute = new WorkflowExecute(
				additionalData,
				fullExecutionData.mode,
				fullExecutionData.data,
			);
			workflowRun = workflowExecute.processRunExecutionData(workflow);
		} else {
			// Execute all nodes
			// Can execute without webhook so go on
			workflowExecute = new WorkflowExecute(additionalData, fullExecutionData.mode);
			workflowRun = workflowExecute.run(workflow);
		}

		Worker.runningJobs[job.id] = workflowRun;
		Worker.runningJobsSummary[job.id] = {
			jobId: job.id.toString(),
			executionId,
			workflowId: fullExecutionData.workflowId ?? '',
			workflowName: fullExecutionData.workflowData.name,
			mode: fullExecutionData.mode,
			startedAt: fullExecutionData.startedAt,
			retryOf: fullExecutionData.retryOf ?? '',
			status: fullExecutionData.status,
		};

		// Wait till the execution is finished
		await workflowRun;

		delete Worker.runningJobs[job.id];
		delete Worker.runningJobsSummary[job.id];

		// do NOT call workflowExecuteAfter hook here, since it is being called from processSuccessExecution()
		// already!

		return {
			success: true,
		};
	}

	constructor(argv: string[], cmdConfig: IConfig) {
		super(argv, cmdConfig);
		this.setInstanceType('worker');
		this.setInstanceQueueModeId();
	}

	async init() {
		await this.initCrashJournal();

		this.logger.debug('Starting n8n worker...');
		this.logger.debug(`Queue mode id: ${this.queueModeId}`);

		await super.init();

		await this.initLicense();
		this.logger.debug('License init complete');
		await this.initBinaryDataService();
		this.logger.debug('Binary data service init complete');
		await this.initExternalHooks();
		this.logger.debug('External hooks init complete');
		await this.initExternalSecrets();
		this.logger.debug('External secrets init complete');
		await this.initEventBus();
		this.logger.debug('Event bus init complete');
		await this.initQueue();
		this.logger.debug('Queue init complete');
		await this.initOrchestration();
		this.logger.debug('Orchestration init complete');
		await this.initQueue();

		await Container.get(OrchestrationWorkerService).publishToEventLog(
			new EventMessageGeneric({
				eventName: 'n8n.worker.started',
				payload: {
					workerId: this.queueModeId,
				},
			}),
		);
	}

	async initEventBus() {
		await eventBus.initialize({
			workerId: this.queueModeId,
		});
	}

	/**
	 * Initializes the redis connection
	 * A publishing connection to redis is created to publish events to the event log
	 * A subscription connection to redis is created to subscribe to commands from the main process
	 * The subscription connection adds a handler to handle the command messages
	 */
	async initOrchestration() {
		await Container.get(OrchestrationWorkerService).init();
		await Container.get(OrchestrationHandlerWorkerService).initWithOptions({
			queueModeId: this.queueModeId,
			redisPublisher: Container.get(OrchestrationWorkerService).redisPublisher,
			getRunningJobIds: () => Object.keys(Worker.runningJobs),
			getRunningJobsSummary: () => Object.values(Worker.runningJobsSummary),
		});
	}

	async initQueue() {
		// eslint-disable-next-line @typescript-eslint/no-shadow
		const { flags } = this.parse(Worker);

		const redisConnectionTimeoutLimit = config.getEnv('queue.bull.redis.timeoutThreshold');

		this.logger.debug(
			`Opening Redis connection to listen to messages with timeout ${redisConnectionTimeoutLimit}`,
		);

		Worker.jobQueue = Container.get(Queue);
		await Worker.jobQueue.init();
		this.logger.debug('Queue singleton ready');
		void Worker.jobQueue.process(flags.concurrency, async (job) =>
			this.runJob(job, this.nodeTypes),
		);

		Worker.jobQueue.getBullObjectInstance().on('global:progress', (jobId: JobId, progress) => {
			// Progress of a job got updated which does get used
			// to communicate that a job got canceled.

			if (progress === -1) {
				// Job has to get canceled
				if (Worker.runningJobs[jobId] !== undefined) {
					// Job is processed by current worker so cancel
					Worker.runningJobs[jobId].cancel();
					delete Worker.runningJobs[jobId];
				}
			}
		});

		let lastTimer = 0;
		let cumulativeTimeout = 0;
		Worker.jobQueue.getBullObjectInstance().on('error', (error: Error) => {
			if (error.toString().includes('ECONNREFUSED')) {
				const now = Date.now();
				if (now - lastTimer > 30000) {
					// Means we had no timeout at all or last timeout was temporary and we recovered
					lastTimer = now;
					cumulativeTimeout = 0;
				} else {
					cumulativeTimeout += now - lastTimer;
					lastTimer = now;
					if (cumulativeTimeout > redisConnectionTimeoutLimit) {
						this.logger.error(
							`Unable to connect to Redis after ${redisConnectionTimeoutLimit}. Exiting process.`,
						);
						process.exit(1);
					}
				}
				this.logger.warn('Redis unavailable - trying to reconnect...');
			} else if (error.toString().includes('Error initializing Lua scripts')) {
				// This is a non-recoverable error
				// Happens when worker starts and Redis is unavailable
				// Even if Redis comes back online, worker will be zombie
				this.logger.error('Error initializing worker.');
				process.exit(2);
			} else {
				this.logger.error('Error from queue: ', error);
				throw error;
			}
		});
	}

	async setupHealthMonitor() {
		const port = config.getEnv('queue.health.port');

		const app = express();
		app.disable('x-powered-by');

		const server = http.createServer(app);

		app.get(
			'/healthz',

			async (req: express.Request, res: express.Response) => {
				this.logger.debug('Health check started!');

				const connection = Db.getConnection();

				try {
					if (!connection.isInitialized) {
						// Connection is not active
						throw new Error('No active database connection!');
					}
					// DB ping
					await connection.query('SELECT 1');
				} catch (e) {
					this.logger.error('No Database connection!', e as Error);
					const error = new ResponseHelper.ServiceUnavailableError('No Database connection!');
					return ResponseHelper.sendErrorResponse(res, error);
				}

				// Just to be complete, generally will the worker stop automatically
				// if it loses the connection to redis
				try {
					// Redis ping
					await Worker.jobQueue.ping();
				} catch (e) {
					this.logger.error('No Redis connection!', e as Error);
					const error = new ResponseHelper.ServiceUnavailableError('No Redis connection!');
					return ResponseHelper.sendErrorResponse(res, error);
				}

				// Everything fine
				const responseData = {
					status: 'ok',
				};

				this.logger.debug('Health check completed successfully!');

				ResponseHelper.sendSuccessResponse(res, responseData, true, 200);
			},
		);

		let presetCredentialsLoaded = false;
		const endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
		if (endpointPresetCredentials !== '') {
			// POST endpoint to set preset credentials
			app.post(
				`/${endpointPresetCredentials}`,
				rawBodyReader,
				bodyParser,
				async (req: express.Request, res: express.Response) => {
					if (!presetCredentialsLoaded) {
						const body = req.body as ICredentialsOverwrite;

						if (req.contentType !== 'application/json') {
							ResponseHelper.sendErrorResponse(
								res,
								new Error(
									'Body must be a valid JSON, make sure the content-type is application/json',
								),
							);
							return;
						}

						Container.get(CredentialsOverwrites).setData(body);
						presetCredentialsLoaded = true;
						ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200);
					} else {
						ResponseHelper.sendErrorResponse(res, new Error('Preset credentials can be set once'));
					}
				},
			);
		}

		server.on('error', (error: Error & { code: string }) => {
			if (error.code === 'EADDRINUSE') {
				this.logger.error(
					`n8n's port ${port} is already in use. Do you have the n8n main process running on that port?`,
				);
				process.exit(1);
			}
		});

		await new Promise<void>((resolve) => server.listen(port, () => resolve()));
		await this.externalHooks.run('worker.ready');
		this.logger.info(`\nn8n worker health check via, port ${port}`);
	}

	async run() {
		// eslint-disable-next-line @typescript-eslint/no-shadow
		const { flags } = this.parse(Worker);

		this.logger.info('\nn8n worker is now ready');
		this.logger.info(` * Version: ${N8N_VERSION}`);
		this.logger.info(` * Concurrency: ${flags.concurrency}`);
		this.logger.info('');

		if (config.getEnv('queue.health.active')) {
			await this.setupHealthMonitor();
		}

		// Make sure that the process does not close
		await new Promise(() => {});
	}

	async catch(error: Error) {
		await this.exitWithCrash('Worker exiting due to an error.', error);
	}
}


import type { Variables } from '@db/entities/Variables';
import { CacheService } from '@/services/cache.service';
import Container, { Service } from 'typedi';
import { VariablesRepository } from '@/databases/repositories';
import type { DeepPartial } from 'typeorm';

@Service()
export class VariablesService {
	constructor(
		protected cacheService: CacheService,
		protected variablesRepository: VariablesRepository,
	) {}

	async getAllCached(): Promise<Variables[]> {
		const variables = await this.cacheService.get('variables', {
			async refreshFunction() {
				// TODO: log refresh cache metric
				return Container.get(VariablesService).findAll();
			},
		});
		return (variables as Array<DeepPartial<Variables>>).map((v) =>
			this.variablesRepository.create(v),
		);
	}

	async getCount(): Promise<number> {
		return (await this.getAllCached()).length;
	}

	async getCached(id: string): Promise<Variables | null> {
		const variables = await this.getAllCached();
		const foundVariable = variables.find((variable) => variable.id === id);
		if (!foundVariable) {
			return null;
		}
		return this.variablesRepository.create(foundVariable as DeepPartial<Variables>);
	}

	async delete(id: string): Promise<void> {
		await this.variablesRepository.delete(id);
		await this.updateCache();
	}

	async updateCache(): Promise<void> {
		// TODO: log update cache metric
		const variables = await this.findAll();
		await this.cacheService.set('variables', variables);
	}

	async findAll(): Promise<Variables[]> {
		return this.variablesRepository.find();
	}
}


/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { CliWorkflowOperationError, SubworkflowOperationError } from 'n8n-workflow';
import type { INode } from 'n8n-workflow';
import { STARTING_NODES } from './constants';

/**
 * Returns if the given id is a valid workflow id
 */
export function isWorkflowIdValid(id: string | null | undefined): boolean {
	// TODO: could also check if id only contains nanoId characters
	return typeof id === 'string' && id?.length <= 16;
}

function findWorkflowStart(executionMode: 'integrated' | 'cli') {
	return function (nodes: INode[]) {
		const executeWorkflowTriggerNode = nodes.find(
			(node) => node.type === 'n8n-nodes-base.executeWorkflowTrigger',
		);

		if (executeWorkflowTriggerNode) return executeWorkflowTriggerNode;

		const startNode = nodes.find((node) => STARTING_NODES.includes(node.type));

		if (startNode) return startNode;

		const title = 'Missing node to start execution';
		const description =
			"Please make sure the workflow you're calling contains an Execute Workflow Trigger node";

		if (executionMode === 'integrated') {
			throw new SubworkflowOperationError(title, description);
		}

		throw new CliWorkflowOperationError(title, description);
	};
}

export const findSubworkflowStart = findWorkflowStart('integrated');

export const findCliWorkflowStart = findWorkflowStart('cli');

export const alphabetizeKeys = (obj: INode) =>
	Object.keys(obj)
		.sort()
		.reduce<Partial<INode>>(
			(acc, key) => ({
				...acc,
				// @ts-expect-error @TECH_DEBT Adding index signature to INode causes type issues downstream
				[key]: obj[key],
			}),
			{},
		);

export const separate = <T>(array: T[], test: (element: T) => boolean) => {
	const pass: T[] = [];
	const fail: T[] = [];

	array.forEach((i) => (test(i) ? pass : fail).push(i));

	return [pass, fail];
};

export const webhookNotFoundErrorMessage = (
	path: string,
	httpMethod?: string,
	webhookMethods?: string[],
) => {
	let webhookPath = path;

	if (httpMethod) {
		webhookPath = `${httpMethod} ${webhookPath}`;
	}

	if (webhookMethods?.length && httpMethod) {
		let methods = '';

		if (webhookMethods.length === 1) {
			methods = webhookMethods[0];
		} else {
			const lastMethod = webhookMethods.pop();

			methods = `${webhookMethods.join(', ')} or ${lastMethod as string}`;
		}

		return `This webhook is not registered for ${httpMethod} requests. Did you mean to make a ${methods} request?`;
	} else {
		return `The requested webhook "${webhookPath}" is not registered.`;
	}
};

export const toError = (maybeError: unknown) =>
	// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
	maybeError instanceof Error ? maybeError : new Error(`${maybeError}`);

export function isStringArray(value: unknown): value is string[] {
	return Array.isArray(value) && value.every((item) => typeof item === 'string');
}

export const isIntegerString = (value: string) => /^\d+$/.test(value);

export function isObjectLiteral(item: unknown): item is { [key: string]: string } {
	return typeof item === 'object' && item !== null && !Array.isArray(item);
}


export class InternalHooks implements IInternalHooksClass {

private readonly instanceSettings: InstanceSettings,

keep keep keep keep keep replace keep keep keep

/* eslint-disable @typescript-eslint/no-unsafe-argument */

/* eslint-disable @typescript-eslint/no-unsafe-member-access */

/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */

import type { IProcessMessage } from 'n8n-core';
import { WorkflowExecute } from 'n8n-core';

import type {
	ExecutionError,
	IDeferredPromise,
	IExecuteResponsePromiseData,
	IRun,
	WorkflowExecuteMode,
	WorkflowHooks,
} from 'n8n-workflow';
import {
	ErrorReporterProxy as ErrorReporter,
	Workflow,
	WorkflowOperationError,
} from 'n8n-workflow';

import PCancelable from 'p-cancelable';
import { join as pathJoin } from 'path';
import { fork } from 'child_process';

import { ActiveExecutions } from '@/ActiveExecutions';
import config from '@/config';
import { ExternalHooks } from '@/ExternalHooks';
import type {
	IExecutionResponse,
	IProcessMessageDataHook,
	IWorkflowExecutionDataProcess,
	IWorkflowExecutionDataProcessWithExecution,
} from '@/Interfaces';
import { NodeTypes } from '@/NodeTypes';
import type { Job, JobData, JobResponse } from '@/Queue';

import { Queue } from '@/Queue';
import { decodeWebhookResponse } from '@/helpers/decodeWebhookResponse';
// eslint-disable-next-line import/no-cycle
import * as WorkflowHelpers from '@/WorkflowHelpers';
// eslint-disable-next-line import/no-cycle
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
import { initErrorHandling } from '@/ErrorReporting';
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
import { Push } from '@/push';
import { eventBus } from './eventbus';
import { recoverExecutionDataFromEventLogMessages } from './eventbus/MessageEventBus/recoverEvents';
import { Container } from 'typedi';
import { InternalHooks } from './InternalHooks';
import { ExecutionRepository } from '@db/repositories';
import { Logger } from './Logger';

export class WorkflowRunner {
	logger: Logger;

	activeExecutions: ActiveExecutions;

	push: Push;

	jobQueue: Queue;

	constructor() {
		this.logger = Container.get(Logger);
		this.push = Container.get(Push);
		this.activeExecutions = Container.get(ActiveExecutions);
	}

	/**
	 * The process did send a hook message so execute the appropriate hook
	 */
	async processHookMessage(workflowHooks: WorkflowHooks, hookData: IProcessMessageDataHook) {
		await workflowHooks.executeHookFunctions(hookData.hook, hookData.parameters);
	}

	/**
	 * The process did error
	 */
	async processError(
		error: ExecutionError,
		startedAt: Date,
		executionMode: WorkflowExecuteMode,
		executionId: string,
		hooks?: WorkflowHooks,
	) {
		ErrorReporter.error(error);

		const isQueueMode = config.getEnv('executions.mode') === 'queue';

		// in queue mode, first do a sanity run for the edge case that the execution was not marked as stalled
		// by Bull even though it executed successfully, see https://github.com/OptimalBits/bull/issues/1415

		if (isQueueMode && executionMode !== 'manual') {
			const executionWithoutData = await Container.get(ExecutionRepository).findSingleExecution(
				executionId,
				{
					includeData: false,
				},
			);
			if (executionWithoutData?.finished === true && executionWithoutData?.status === 'success') {
				// false positive, execution was successful
				return;
			}
		}

		const fullRunData: IRun = {
			data: {
				resultData: {
					error: {
						...error,
						message: error.message,
						stack: error.stack,
					},
					runData: {},
				},
			},
			finished: false,
			mode: executionMode,
			startedAt,
			stoppedAt: new Date(),
			status: 'error',
		};

		// The following will attempt to recover runData from event logs
		// Note that this will only work as long as the event logs actually contain the events from this workflow execution
		// Since processError is run almost immediately after the workflow execution has failed, it is likely that the event logs
		// does contain those messages.
		try {
			// Search for messages for this executionId in event logs
			const eventLogMessages = await eventBus.getEventsByExecutionId(executionId);
			// Attempt to recover more better runData from these messages (but don't update the execution db entry yet)
			if (eventLogMessages.length > 0) {
				const eventLogExecutionData = await recoverExecutionDataFromEventLogMessages(
					executionId,
					eventLogMessages,
					false,
				);
				if (eventLogExecutionData) {
					fullRunData.data.resultData.runData = eventLogExecutionData.resultData.runData;
					fullRunData.status = 'crashed';
				}
			}

			const executionFlattedData = await Container.get(ExecutionRepository).findSingleExecution(
				executionId,
				{
					includeData: true,
				},
			);

			if (executionFlattedData) {
				void Container.get(InternalHooks).onWorkflowCrashed(
					executionId,
					executionMode,
					executionFlattedData?.workflowData,
					// TODO: get metadata to be sent here
					// executionFlattedData?.metadata,
				);
			}
		} catch {
			// Ignore errors
		}

		// Remove from active execution with empty data. That will
		// set the execution to failed.
		this.activeExecutions.remove(executionId, fullRunData);

		if (hooks) {
			await hooks.executeHookFunctions('workflowExecuteAfter', [fullRunData]);
		}
	}

	/**
	 * Run the workflow
	 *
	 * @param {boolean} [loadStaticData] If set will the static data be loaded from
	 *                                   the workflow and added to input data
	 */
	async run(
		data: IWorkflowExecutionDataProcess,
		loadStaticData?: boolean,
		realtime?: boolean,
		executionId?: string,
		responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
	): Promise<string> {
		const executionsMode = config.getEnv('executions.mode');
		const executionsProcess = config.getEnv('executions.process');

		await initErrorHandling();

		if (executionsMode === 'queue') {
			this.jobQueue = Container.get(Queue);
		}

		if (executionsMode === 'queue' && data.executionMode !== 'manual') {
			// Do not run "manual" executions in bull because sending events to the
			// frontend would not be possible
			executionId = await this.enqueueExecution(
				data,
				loadStaticData,
				realtime,
				executionId,
				responsePromise,
			);
		} else {
			if (executionsProcess === 'main') {
				executionId = await this.runMainProcess(data, loadStaticData, executionId, responsePromise);
			} else {
				executionId = await this.runSubprocess(data, loadStaticData, executionId, responsePromise);
			}
			void Container.get(InternalHooks).onWorkflowBeforeExecute(executionId, data);
		}

		// only run these when not in queue mode or when the execution is manual,
		// since these calls are now done by the worker directly
		if (executionsMode !== 'queue' || data.executionMode === 'manual') {
			const postExecutePromise = this.activeExecutions.getPostExecutePromise(executionId);
			const externalHooks = Container.get(ExternalHooks);
			postExecutePromise
				.then(async (executionData) => {
					void Container.get(InternalHooks).onWorkflowPostExecute(
						executionId!,
						data.workflowData,
						executionData,
						data.userId,
					);
					if (externalHooks.exists('workflow.postExecute')) {
						try {
							await externalHooks.run('workflow.postExecute', [
								executionData,
								data.workflowData,
								executionId,
							]);
						} catch (error) {
							ErrorReporter.error(error);
							console.error('There was a problem running hook "workflow.postExecute"', error);
						}
					}
				})
				.catch((error) => {
					ErrorReporter.error(error);
					console.error('There was a problem running internal hook "onWorkflowPostExecute"', error);
				});
		}

		return executionId;
	}

	/**
	 * Run the workflow in current process
	 *
	 * @param {boolean} [loadStaticData] If set will the static data be loaded from
	 *                                   the workflow and added to input data
	 */
	async runMainProcess(
		data: IWorkflowExecutionDataProcess,
		loadStaticData?: boolean,
		restartExecutionId?: string,
		responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
	): Promise<string> {
		const workflowId = data.workflowData.id;
		if (loadStaticData === true && workflowId) {
			data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(workflowId);
		}

		const nodeTypes = Container.get(NodeTypes);

		// Soft timeout to stop workflow execution after current running node
		// Changes were made by adding the `workflowTimeout` to the `additionalData`
		// So that the timeout will also work for executions with nested workflows.
		let executionTimeout: NodeJS.Timeout;

		const workflowSettings = data.workflowData.settings ?? {};
		let workflowTimeout = workflowSettings.executionTimeout ?? config.getEnv('executions.timeout'); // initialize with default
		if (workflowTimeout > 0) {
			workflowTimeout = Math.min(workflowTimeout, config.getEnv('executions.maxTimeout'));
		}

		const workflow = new Workflow({
			id: workflowId,
			name: data.workflowData.name,
			nodes: data.workflowData.nodes,
			connections: data.workflowData.connections,
			active: data.workflowData.active,
			nodeTypes,
			staticData: data.workflowData.staticData,
			settings: workflowSettings,
		});
		const additionalData = await WorkflowExecuteAdditionalData.getBase(
			data.userId,
			undefined,
			workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000,
		);
		additionalData.restartExecutionId = restartExecutionId;

		// Register the active execution
		const executionId = await this.activeExecutions.add(data, undefined, restartExecutionId);
		additionalData.executionId = executionId;

		this.logger.verbose(
			`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`,
			{ executionId },
		);
		let workflowExecution: PCancelable<IRun>;

		try {
			this.logger.verbose(
				`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`,
				{ executionId },
			);

			additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(
				data,
				executionId,
				true,
			);

			try {
				await PermissionChecker.check(workflow, data.userId);
			} catch (error) {
				ErrorReporter.error(error);
				// Create a failed execution with the data for the node
				// save it and abort execution
				const failedExecution = generateFailedExecutionFromError(
					data.executionMode,
					error,
					error.node,
				);
				await additionalData.hooks.executeHookFunctions('workflowExecuteAfter', [failedExecution]);
				this.activeExecutions.remove(executionId, failedExecution);
				return executionId;
			}

			additionalData.hooks.hookFunctions.sendResponse = [
				async (response: IExecuteResponsePromiseData): Promise<void> => {
					if (responsePromise) {
						responsePromise.resolve(response);
					}
				},
			];

			additionalData.setExecutionStatus = WorkflowExecuteAdditionalData.setExecutionStatus.bind({
				executionId,
			});

			additionalData.sendDataToUI = WorkflowExecuteAdditionalData.sendDataToUI.bind({
				sessionId: data.sessionId,
			});

			if (data.executionData !== undefined) {
				this.logger.debug(`Execution ID ${executionId} had Execution data. Running with payload.`, {
					executionId,
				});
				const workflowExecute = new WorkflowExecute(
					additionalData,
					data.executionMode,
					data.executionData,
				);
				workflowExecution = workflowExecute.processRunExecutionData(workflow);
			} else if (
				data.runData === undefined ||
				data.startNodes === undefined ||
				data.startNodes.length === 0
			) {
				this.logger.debug(`Execution ID ${executionId} will run executing all nodes.`, {
					executionId,
				});
				// Execute all nodes

				const startNode = WorkflowHelpers.getExecutionStartNode(data, workflow);

				// Can execute without webhook so go on
				const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
				workflowExecution = workflowExecute.run(
					workflow,
					startNode,
					data.destinationNode,
					data.pinData,
				);
			} else {
				this.logger.debug(`Execution ID ${executionId} is a partial execution.`, { executionId });
				// Execute only the nodes between start and destination nodes
				const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
				workflowExecution = workflowExecute.runPartialWorkflow(
					workflow,
					data.runData,
					data.startNodes,
					data.destinationNode,
					data.pinData,
				);
			}

			this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);

			if (workflowTimeout > 0) {
				const timeout = Math.min(workflowTimeout, config.getEnv('executions.maxTimeout')) * 1000; // as seconds
				executionTimeout = setTimeout(() => {
					void this.activeExecutions.stopExecution(executionId, 'timeout');
				}, timeout);
			}

			workflowExecution
				.then((fullRunData) => {
					clearTimeout(executionTimeout);
					if (workflowExecution.isCanceled) {
						fullRunData.finished = false;
					}
					fullRunData.status = this.activeExecutions.getStatus(executionId);
					this.activeExecutions.remove(executionId, fullRunData);
				})
				.catch(async (error) =>
					this.processError(
						error,
						new Date(),
						data.executionMode,
						executionId,
						additionalData.hooks,
					),
				);
		} catch (error) {
			await this.processError(
				error,
				new Date(),
				data.executionMode,
				executionId,
				additionalData.hooks,
			);

			throw error;
		}

		return executionId;
	}

	async enqueueExecution(
		data: IWorkflowExecutionDataProcess,
		loadStaticData?: boolean,
		realtime?: boolean,
		restartExecutionId?: string,
		responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
	): Promise<string> {
		// TODO: If "loadStaticData" is set to true it has to load data new on worker

		// Register the active execution
		const executionId = await this.activeExecutions.add(data, undefined, restartExecutionId);
		if (responsePromise) {
			this.activeExecutions.attachResponsePromise(executionId, responsePromise);
		}

		const jobData: JobData = {
			executionId,
			loadStaticData: !!loadStaticData,
		};

		let priority = 100;
		if (realtime === true) {
			// Jobs which require a direct response get a higher priority
			priority = 50;
		}
		// TODO: For realtime jobs should probably also not do retry or not retry if they are older than x seconds.
		//       Check if they get retried by default and how often.
		const jobOptions = {
			priority,
			removeOnComplete: true,
			removeOnFail: true,
		};
		let job: Job;
		let hooks: WorkflowHooks;
		try {
			job = await this.jobQueue.add(jobData, jobOptions);

			console.log(`Started with job ID: ${job.id.toString()} (Execution ID: ${executionId})`);

			hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerMain(
				data.executionMode,
				executionId,
				data.workflowData,
				{ retryOf: data.retryOf ? data.retryOf.toString() : undefined },
			);

			// Normally also workflow should be supplied here but as it only used for sending
			// data to editor-UI is not needed.
			await hooks.executeHookFunctions('workflowExecuteBefore', []);
		} catch (error) {
			// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
			// "workflowExecuteAfter" which we require.
			const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(
				data.executionMode,
				executionId,
				data.workflowData,
				{ retryOf: data.retryOf ? data.retryOf.toString() : undefined },
			);
			await this.processError(error, new Date(), data.executionMode, executionId, hooks);
			throw error;
		}

		const workflowExecution: PCancelable<IRun> = new PCancelable(
			async (resolve, reject, onCancel) => {
				onCancel.shouldReject = false;
				onCancel(async () => {
					const queue = Container.get(Queue);
					await queue.stopJob(job);

					// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
					// "workflowExecuteAfter" which we require.
					const hooksWorker = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(
						data.executionMode,
						executionId,
						data.workflowData,
						{ retryOf: data.retryOf ? data.retryOf.toString() : undefined },
					);

					const error = new WorkflowOperationError('Workflow-Execution has been canceled!');
					await this.processError(error, new Date(), data.executionMode, executionId, hooksWorker);

					reject(error);
				});

				const jobData: Promise<JobResponse> = job.finished();

				const queueRecoveryInterval = config.getEnv('queue.bull.queueRecoveryInterval');

				const racingPromises: Array<Promise<JobResponse>> = [jobData];

				let clearWatchdogInterval;
				if (queueRecoveryInterval > 0) {
					/** ***********************************************
					 * Long explanation about what this solves:      *
					 * This only happens in a very specific scenario *
					 * when Redis crashes and recovers shortly       *
					 * but during this time, some execution(s)       *
					 * finished. The end result is that the main     *
					 * process will wait indefinitely and never      *
					 * get a response. This adds an active polling to*
					 * the queue that allows us to identify that the *
					 * execution finished and get information from   *
					 * the database.                                 *
					 ************************************************ */
					let watchDogInterval: NodeJS.Timeout | undefined;

					const watchDog: Promise<JobResponse> = new Promise((res) => {
						watchDogInterval = setInterval(async () => {
							const currentJob = await this.jobQueue.getJob(job.id);
							// When null means job is finished (not found in queue)
							if (currentJob === null) {
								// Mimic worker's success message
								res({ success: true });
							}
						}, queueRecoveryInterval * 1000);
					});

					racingPromises.push(watchDog);

					clearWatchdogInterval = () => {
						if (watchDogInterval) {
							clearInterval(watchDogInterval);
							watchDogInterval = undefined;
						}
					};
				}

				let racingPromisesResult: JobResponse = {
					success: false,
				};
				try {
					racingPromisesResult = await Promise.race(racingPromises);
					if (clearWatchdogInterval !== undefined) {
						clearWatchdogInterval();
					}
				} catch (error) {
					ErrorReporter.error(error);
					// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
					// "workflowExecuteAfter" which we require.
					const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(
						data.executionMode,
						executionId,
						data.workflowData,
						{ retryOf: data.retryOf ? data.retryOf.toString() : undefined },
					);
					this.logger.error(`Problem with execution ${executionId}: ${error.message}. Aborting.`);
					if (clearWatchdogInterval !== undefined) {
						clearWatchdogInterval();
					}
					await this.processError(error, new Date(), data.executionMode, executionId, hooks);

					reject(error);
				}

				// optimization: only pull and unflatten execution data from the Db when it is needed
				const executionHasPostExecutionPromises =
					this.activeExecutions.getPostExecutePromiseCount(executionId) > 0;

				if (executionHasPostExecutionPromises) {
					this.logger.debug(
						`Reading execution data for execution ${executionId} from db for PostExecutionPromise.`,
					);
				} else {
					this.logger.debug(
						`Skipping execution data for execution ${executionId} since there are no PostExecutionPromise.`,
					);
				}

				const fullExecutionData = await Container.get(ExecutionRepository).findSingleExecution(
					executionId,
					{
						includeData: executionHasPostExecutionPromises,
						unflattenData: executionHasPostExecutionPromises,
					},
				);
				if (!fullExecutionData) {
					return reject(new Error(`Could not find execution with id "${executionId}"`));
				}

				const runData: IRun = {
					data: {},
					finished: fullExecutionData.finished,
					mode: fullExecutionData.mode,
					startedAt: fullExecutionData.startedAt,
					stoppedAt: fullExecutionData.stoppedAt,
				} as IRun;

				if (executionHasPostExecutionPromises) {
					runData.data = (fullExecutionData as IExecutionResponse).data;
				}

				// NOTE: due to the optimization of not loading the execution data from the db when no post execution promises are present,
				// the execution data in runData.data MAY not be available here.
				// This means that any function expecting with runData has to check if the runData.data defined from this point
				this.activeExecutions.remove(executionId, runData);

				// Normally also static data should be supplied here but as it only used for sending
				// data to editor-UI is not needed.
				await hooks.executeHookFunctions('workflowExecuteAfter', [runData]);
				try {
					// Check if this execution data has to be removed from database
					// based on workflow settings.
					const workflowSettings = data.workflowData.settings ?? {};
					const saveDataErrorExecution =
						workflowSettings.saveDataErrorExecution ?? config.getEnv('executions.saveDataOnError');
					const saveDataSuccessExecution =
						workflowSettings.saveDataSuccessExecution ??
						config.getEnv('executions.saveDataOnSuccess');

					const workflowDidSucceed = !racingPromisesResult.error;
					if (
						(workflowDidSucceed && saveDataSuccessExecution === 'none') ||
						(!workflowDidSucceed && saveDataErrorExecution === 'none')
					) {
						await Container.get(ExecutionRepository).hardDelete({
							workflowId: data.workflowData.id as string,
							executionId,
						});
					}
					// eslint-disable-next-line id-denylist
				} catch (err) {
					// We don't want errors here to crash n8n. Just log and proceed.
					console.log('Error removing saved execution from database. More details: ', err);
				}

				resolve(runData);
			},
		);

		workflowExecution.catch(() => {
			// We `reject` this promise if the execution fails
			// but the error is handled already by processError
			// So we're just preventing crashes here.
		});

		this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
		return executionId;
	}

	/**
	 * Run the workflow
	 *
	 * @param {boolean} [loadStaticData] If set will the static data be loaded from
	 *                                   the workflow and added to input data
	 */
	async runSubprocess(
		data: IWorkflowExecutionDataProcess,
		loadStaticData?: boolean,
		restartExecutionId?: string,
		responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
	): Promise<string> {
		const workflowId = data.workflowData.id;
		let startedAt = new Date();
		const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));

		if (loadStaticData === true && workflowId) {
			data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(workflowId);
		}

		data.restartExecutionId = restartExecutionId;

		// Register the active execution
		const executionId = await this.activeExecutions.add(data, subprocess, restartExecutionId);

		(data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId;

		const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);

		try {
			// Send all data to subprocess it needs to run the workflow
			subprocess.send({ type: 'startWorkflow', data } as IProcessMessage);
		} catch (error) {
			await this.processError(error, new Date(), data.executionMode, executionId, workflowHooks);
			return executionId;
		}

		// Start timeout for the execution
		let executionTimeout: NodeJS.Timeout;

		const workflowSettings = data.workflowData.settings ?? {};
		let workflowTimeout = workflowSettings.executionTimeout ?? config.getEnv('executions.timeout'); // initialize with default

		const processTimeoutFunction = (timeout: number) => {
			void this.activeExecutions.stopExecution(executionId, 'timeout');
			executionTimeout = setTimeout(() => subprocess.kill(), Math.max(timeout * 0.2, 5000)); // minimum 5 seconds
		};

		if (workflowTimeout > 0) {
			workflowTimeout = Math.min(workflowTimeout, config.getEnv('executions.maxTimeout')) * 1000; // as seconds
			// Start timeout already now but give process at least 5 seconds to start.
			// Without it could would it be possible that the workflow executions times out before it even got started if
			// the timeout time is very short as the process start time can be quite long.
			executionTimeout = setTimeout(
				processTimeoutFunction,
				Math.max(5000, workflowTimeout),
				workflowTimeout,
			);
		}

		// Create a list of child spawned executions
		// If after the child process exits we have
		// outstanding executions, we remove them
		const childExecutionIds: string[] = [];

		// Listen to data from the subprocess
		subprocess.on('message', async (message: IProcessMessage) => {
			this.logger.debug(
				`Received child process message of type ${message.type} for execution ID ${executionId}.`,
				{ executionId },
			);
			if (message.type === 'start') {
				// Now that the execution actually started set the timeout again so that does not time out to early.
				startedAt = new Date();
				if (workflowTimeout > 0) {
					clearTimeout(executionTimeout);
					executionTimeout = setTimeout(processTimeoutFunction, workflowTimeout, workflowTimeout);
				}
			} else if (message.type === 'end') {
				clearTimeout(executionTimeout);
				this.activeExecutions.remove(executionId, message.data.runData);
			} else if (message.type === 'sendResponse') {
				if (responsePromise) {
					responsePromise.resolve(decodeWebhookResponse(message.data.response));
				}
			} else if (message.type === 'sendDataToUI') {
				// eslint-disable-next-line @typescript-eslint/no-unsafe-call
				WorkflowExecuteAdditionalData.sendDataToUI.bind({ sessionId: data.sessionId })(
					message.data.type,
					message.data.data,
				);
			} else if (message.type === 'processError') {
				clearTimeout(executionTimeout);
				const executionError = message.data.executionError as ExecutionError;
				await this.processError(
					executionError,
					startedAt,
					data.executionMode,
					executionId,
					workflowHooks,
				);
			} else if (message.type === 'processHook') {
				await this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook);
			} else if (message.type === 'timeout') {
				// Execution timed out and its process has been terminated
				const timeoutError = new WorkflowOperationError('Workflow execution timed out!');

				// No need to add hook here as the subprocess takes care of calling the hooks
				await this.processError(timeoutError, startedAt, data.executionMode, executionId);
			} else if (message.type === 'startExecution') {
				const executionId = await this.activeExecutions.add(message.data.runData);
				childExecutionIds.push(executionId);
				subprocess.send({ type: 'executionId', data: { executionId } } as IProcessMessage);
			} else if (message.type === 'finishExecution') {
				const executionIdIndex = childExecutionIds.indexOf(message.data.executionId);
				if (executionIdIndex !== -1) {
					childExecutionIds.splice(executionIdIndex, 1);
				}

				if (message.data.result === undefined) {
					const noDataError = new WorkflowOperationError('Workflow finished with no result data');
					const subWorkflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(
						data,
						message.data.executionId,
					);
					await this.processError(
						noDataError,
						startedAt,
						data.executionMode,
						message.data?.executionId,
						subWorkflowHooks,
					);
				} else {
					this.activeExecutions.remove(message.data.executionId, message.data.result);
				}
			}
		});

		// Also get informed when the processes does exit especially when it did crash or timed out
		subprocess.on('exit', async (code, signal) => {
			if (signal === 'SIGTERM') {
				this.logger.debug(`Subprocess for execution ID ${executionId} timed out.`, { executionId });
				// Execution timed out and its process has been terminated
				const timeoutError = new WorkflowOperationError('Workflow execution timed out!');

				await this.processError(
					timeoutError,
					startedAt,
					data.executionMode,
					executionId,
					workflowHooks,
				);
			} else if (code !== 0) {
				this.logger.debug(
					`Subprocess for execution ID ${executionId} finished with error code ${code}.`,
					{ executionId },
				);
				// Process did exit with error code, so something went wrong.
				const executionError = new WorkflowOperationError(
					'Workflow execution process crashed for an unknown reason!',
				);

				await this.processError(
					executionError,
					startedAt,
					data.executionMode,
					executionId,
					workflowHooks,
				);
			}

			for (const executionId of childExecutionIds) {
				// When the child process exits, if we still have
				// pending child executions, we mark them as finished
				// They will display as unknown to the user
				// Instead of pending forever as executing when it
				// actually isn't anymore.

				this.activeExecutions.remove(executionId);
			}

			clearTimeout(executionTimeout);
		});

		return executionId;
	}
}


import type { MigrationContext, ReversibleMigration } from '@db/types';

export class AddStatusToExecutions1674138566000 implements ReversibleMigration {
	async up({ queryRunner, tablePrefix }: MigrationContext) {
		await queryRunner.query(
			`ALTER TABLE \`${tablePrefix}execution_entity\` ADD COLUMN "status" varchar`,
		);
	}

	async down({ queryRunner, tablePrefix }: MigrationContext) {
		await queryRunner.query(`ALTER TABLE \`${tablePrefix}execution_entity\` DROP COLUMN "status"`);
	}
}


import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';

import { configurePostgres } from '../transport';
import { configureQueryRunner } from '../helpers/utils';
import type { PostgresType } from './node.type';

import * as database from './database/Database.resource';

export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
	let returnData: INodeExecutionData[] = [];

	const items = this.getInputData();
	const resource = this.getNodeParameter<PostgresType>('resource', 0);
	const operation = this.getNodeParameter('operation', 0);

	const credentials = await this.getCredentials('postgres');
	const options = this.getNodeParameter('options', 0, {});
	options.nodeVersion = this.getNode().typeVersion;
	options.operation = operation;

	const { db, pgp, sshClient } = await configurePostgres(credentials, options);

	const runQueries = configureQueryRunner.call(
		this,
		this.getNode(),
		this.continueOnFail(),
		pgp,
		db,
	);

	const postgresNodeData = {
		resource,
		operation,
	} as PostgresType;

	try {
		switch (postgresNodeData.resource) {
			case 'database':
				returnData = await database[postgresNodeData.operation].execute.call(
					this,
					runQueries,
					items,
					options,
					db,
				);
				break;
			default:
				throw new NodeOperationError(
					this.getNode(),
					`The operation "${operation}" is not supported!`,
				);
		}
	} catch (error) {
		throw error;
	} finally {
		if (sshClient) {
			sshClient.end();
		}

		await db.$pool.end();
	}

	return [returnData];
}