chore(core): Add @n8n/engine HTTP server and harness (no-changelog) (#29913)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mfsiega 2026-05-08 16:08:11 +02:00 committed by GitHub
parent 8a6e779c6d
commit cd5b2b3762
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 195 additions and 26 deletions

View File

@ -38,3 +38,4 @@
!packages/@n8n/benchmark/** !packages/@n8n/benchmark/**
!packages/@n8n/typescript-config !packages/@n8n/typescript-config
!packages/@n8n/typescript-config/** !packages/@n8n/typescript-config/**

View File

@ -0,0 +1,20 @@
ARG NODE_VERSION=24.14.1
FROM node:${NODE_VERSION}-alpine3.22
ENV NODE_ENV=production
RUN apk add --no-cache tini
WORKDIR /app
# `compiled/` is produced by `pnpm build:docker`. It's a `pnpm deploy --prod`
# output containing package.json, dist/, and a node_modules with only
# production dependencies — no devDeps, no workspace bloat.
COPY --chown=node:node ./compiled /app
USER node
EXPOSE 3000
ENTRYPOINT ["tini", "--"]
CMD ["node", "dist/serve.js"]

View File

@ -0,0 +1,12 @@
import { Config, Env } from '../decorators';
@Config
export class EngineConfig {
/** Port the engine HTTP server listens on. */
@Env('N8N_ENGINE_PORT')
port: number = 3000;
/** Host interface the engine HTTP server binds to. */
@Env('N8N_ENGINE_HOST')
host: string = '0.0.0.0';
}

View File

@ -61,6 +61,7 @@ export {
SsrfProtectionConfig, SsrfProtectionConfig,
SSRF_DEFAULT_BLOCKED_IP_RANGES, SSRF_DEFAULT_BLOCKED_IP_RANGES,
} from './configs/ssrf-protection.config'; } from './configs/ssrf-protection.config';
export { EngineConfig } from './configs/engine.config';
export { ExecutionsConfig } from './configs/executions.config'; export { ExecutionsConfig } from './configs/executions.config';
export { LOG_SCOPES } from './configs/logging.config'; export { LOG_SCOPES } from './configs/logging.config';
export type { LogScope } from './configs/logging.config'; export type { LogScope } from './configs/logging.config';

View File

@ -0,0 +1,3 @@
*
!compiled
!compiled/**

View File

@ -1,4 +1,7 @@
import { defineConfig } from 'eslint/config'; import { defineConfig } from 'eslint/config';
import { nodeConfig } from '@n8n/eslint-config/node'; import { nodeConfig } from '@n8n/eslint-config/node';
export default defineConfig(nodeConfig); export default defineConfig(
{ ignores: ['compiled/**', 'vitest.integration.config.ts'] },
nodeConfig,
);

View File

@ -3,15 +3,18 @@
"version": "0.1.0", "version": "0.1.0",
"description": "n8n workflow execution engine (v2)", "description": "n8n workflow execution engine (v2)",
"scripts": { "scripts": {
"clean": "rimraf dist .turbo", "clean": "rimraf dist .turbo compiled",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json", "build": "tsc -p tsconfig.build.json",
"build:docker": "pnpm build && rimraf compiled && DOCKER_BUILD=true NODE_ENV=production pnpm --filter . --prod --legacy deploy --no-optional compiled && docker build -f ../../../docker/images/engine/Dockerfile -t n8n-engine:local .",
"start": "node dist/serve.js",
"format": "biome format --write src", "format": "biome format --write src",
"format:check": "biome ci src", "format:check": "biome ci src",
"lint": "eslint . --quiet", "lint": "eslint . --quiet",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"test": "vitest run --passWithNoTests", "test": "vitest run --passWithNoTests",
"test:dev": "vitest --watch", "test:dev": "vitest --watch",
"test:integration": "vitest run --config vitest.integration.config.ts",
"watch": "tsc -p tsconfig.build.json --watch" "watch": "tsc -p tsconfig.build.json --watch"
}, },
"main": "dist/index.js", "main": "dist/index.js",
@ -19,10 +22,18 @@
"files": [ "files": [
"dist/**/*" "dist/**/*"
], ],
"dependencies": {}, "dependencies": {
"@n8n/config": "workspace:*",
"@n8n/di": "workspace:*",
"express": "5.1.0",
"reflect-metadata": "catalog:"
},
"devDependencies": { "devDependencies": {
"@n8n/typescript-config": "workspace:*", "@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*", "@n8n/vitest-config": "workspace:*",
"@types/express": "catalog:",
"@types/supertest": "^6.0.3",
"supertest": "^7.1.1",
"vitest": "catalog:" "vitest": "catalog:"
} }
} }

View File

@ -1,5 +1 @@
// Public API of @n8n/engine. export { createEngineServer } from './server';
//
// Intentionally empty for now. The StartExecution API surface and core engine
// interfaces will land in subsequent CAT-2859 sub-tickets.
export {};

View File

@ -0,0 +1,29 @@
import { EngineConfig } from '@n8n/config';
import { Container } from '@n8n/di';
import { createEngineServer } from './server';
const config = Container.get(EngineConfig);
const { app } = createEngineServer();
const server = app.listen(config.port, config.host, () => {
console.log(`engine: listening on http://${config.host}:${config.port}`);
});
let shuttingDown = false;
const shutdown = (signal: string): void => {
if (shuttingDown) return;
shuttingDown = true;
console.log(`engine: received ${signal}, shutting down`);
server.close((error) => {
if (error) {
console.error('engine: error during shutdown', error);
process.exit(1);
}
process.exit(0);
});
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

View File

@ -0,0 +1,11 @@
import express, { type Application } from 'express';
export function createEngineServer(): { app: Application } {
const app = express();
app.get('/healthz', (_req, res) => {
res.status(200).json({ status: 'ok' });
});
return { app };
}

View File

@ -0,0 +1 @@
export { createEngineServer } from './create-engine-server';

View File

@ -0,0 +1,24 @@
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { startEngineServer } from '../start-engine-server';
describe('engine HTTP server (e2e)', () => {
let url: string;
let stop: () => Promise<void>;
beforeAll(async () => {
({ url, stop } = await startEngineServer());
});
afterAll(async () => {
await stop();
});
it('responds to GET /healthz with { status: "ok" }', async () => {
const response = await request(url).get('/healthz');
expect(response.status).toBe(200);
expect(response.body).toEqual({ status: 'ok' });
});
});

View File

@ -0,0 +1 @@
export { startEngineServer } from './start-engine-server';

View File

@ -0,0 +1,33 @@
import type { Server } from 'node:http';
import { createEngineServer } from '../server';
export async function startEngineServer(): Promise<{
url: string;
stop: () => Promise<void>;
}> {
const { app } = createEngineServer();
const server = await new Promise<Server>((resolve, reject) => {
const s = app.listen(0, '127.0.0.1', () => resolve(s));
s.on('error', reject);
});
const address = server.address();
if (address === null || typeof address === 'string') {
throw new Error('Engine server address is not a TCP socket');
}
const url = `http://127.0.0.1:${address.port}`;
const stop = async (): Promise<void> => {
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) reject(error);
else resolve();
});
});
};
return { url, stop };
}

View File

@ -7,7 +7,9 @@
"target": "es2023", "target": "es2023",
"lib": ["es2023"], "lib": ["es2023"],
"types": ["node"], "types": ["node"],
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo" "tsBuildInfoFile": "dist/typecheck.tsbuildinfo",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}, },
"include": ["src/**/*.ts"] "include": ["src/**/*.ts"]
} }

View File

@ -1,3 +1,5 @@
import { createVitestConfig } from '@n8n/vitest-config/node'; import { createVitestConfig } from '@n8n/vitest-config/node';
export default createVitestConfig(); export default createVitestConfig({
exclude: ['**/node_modules/**', '**/dist/**', '**/*.integration.test.ts'],
});

View File

@ -0,0 +1,5 @@
import { createVitestConfig } from '@n8n/vitest-config/node';
export default createVitestConfig({
include: ['**/*.integration.test.ts'],
});

View File

@ -1399,6 +1399,19 @@ importers:
version: link:../typescript-config version: link:../typescript-config
packages/@n8n/engine: packages/@n8n/engine:
dependencies:
'@n8n/config':
specifier: workspace:*
version: link:../config
'@n8n/di':
specifier: workspace:*
version: link:../di
express:
specifier: 5.1.0
version: 5.1.0
reflect-metadata:
specifier: 'catalog:'
version: 0.2.2
devDependencies: devDependencies:
'@n8n/typescript-config': '@n8n/typescript-config':
specifier: workspace:* specifier: workspace:*
@ -1406,6 +1419,15 @@ importers:
'@n8n/vitest-config': '@n8n/vitest-config':
specifier: workspace:* specifier: workspace:*
version: link:../vitest-config version: link:../vitest-config
'@types/express':
specifier: 'catalog:'
version: 5.0.1
'@types/supertest':
specifier: ^6.0.3
version: 6.0.3
supertest:
specifier: ^7.1.1
version: 7.1.1
vitest: vitest:
specifier: 'catalog:' specifier: 'catalog:'
version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(@vitest/browser-playwright@4.0.16)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)) version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(@vitest/browser-playwright@4.0.16)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
@ -17883,10 +17905,6 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
mime-types@3.0.1:
resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
engines: {node: '>= 0.6'}
mime-types@3.0.2: mime-types@3.0.2:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -33948,7 +33966,7 @@ snapshots:
bytes: 3.1.2 bytes: 3.1.2
content-type: 1.0.5 content-type: 1.0.5
debug: 4.4.3(supports-color@8.1.1) debug: 4.4.3(supports-color@8.1.1)
http-errors: 2.0.0 http-errors: 2.0.1
iconv-lite: 0.7.2 iconv-lite: 0.7.2
on-finished: 2.4.1 on-finished: 2.4.1
qs: 6.14.2 qs: 6.14.2
@ -36543,15 +36561,15 @@ snapshots:
content-type: 1.0.5 content-type: 1.0.5
cookie: 0.7.2 cookie: 0.7.2
cookie-signature: 1.2.2 cookie-signature: 1.2.2
debug: 4.4.1(supports-color@8.1.1) debug: 4.4.3(supports-color@8.1.1)
encodeurl: 2.0.0 encodeurl: 2.0.0
escape-html: 1.0.3 escape-html: 1.0.3
etag: 1.8.1 etag: 1.8.1
finalhandler: 2.1.0 finalhandler: 2.1.0
fresh: 2.0.0 fresh: 2.0.0
http-errors: 2.0.0 http-errors: 2.0.1
merge-descriptors: 2.0.0 merge-descriptors: 2.0.0
mime-types: 3.0.1 mime-types: 3.0.2
on-finished: 2.4.1 on-finished: 2.4.1
once: 1.4.0 once: 1.4.0
parseurl: 1.3.3 parseurl: 1.3.3
@ -36561,7 +36579,7 @@ snapshots:
router: 2.2.0 router: 2.2.0
send: 1.2.0 send: 1.2.0
serve-static: 2.2.0 serve-static: 2.2.0
statuses: 2.0.1 statuses: 2.0.2
type-is: 2.0.1 type-is: 2.0.1
vary: 1.1.2 vary: 1.1.2
transitivePeerDependencies: transitivePeerDependencies:
@ -36802,7 +36820,7 @@ snapshots:
escape-html: 1.0.3 escape-html: 1.0.3
on-finished: 2.4.1 on-finished: 2.4.1
parseurl: 1.3.3 parseurl: 1.3.3
statuses: 2.0.1 statuses: 2.0.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -40077,10 +40095,6 @@ snapshots:
dependencies: dependencies:
mime-db: 1.52.0 mime-db: 1.52.0
mime-types@3.0.1:
dependencies:
mime-db: 1.54.0
mime-types@3.0.2: mime-types@3.0.2:
dependencies: dependencies:
mime-db: 1.54.0 mime-db: 1.54.0
@ -43187,12 +43201,12 @@ snapshots:
escape-html: 1.0.3 escape-html: 1.0.3
etag: 1.8.1 etag: 1.8.1
fresh: 2.0.0 fresh: 2.0.0
http-errors: 2.0.0 http-errors: 2.0.1
mime-types: 3.0.2 mime-types: 3.0.2
ms: 2.1.3 ms: 2.1.3
on-finished: 2.4.1 on-finished: 2.4.1
range-parser: 1.2.1 range-parser: 1.2.1
statuses: 2.0.1 statuses: 2.0.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color