From 9397320af93bdeb9a03f28ef5790a0c330a55c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 11 Apr 2025 13:58:07 +0200 Subject: [PATCH] refactor(core): Validate all string union config fields (#14527) --- packages/@n8n/config/src/configs/auth.config.ts | 10 ++++++++-- packages/@n8n/config/src/configs/cache.config.ts | 9 +++++++-- .../@n8n/config/src/configs/database.config.ts | 16 ++++++++++++---- .../@n8n/config/src/configs/event-bus.config.ts | 9 +++++++-- .../src/configs/external-storage.config.ts | 10 ++++++++-- .../@n8n/config/src/configs/generic.config.ts | 9 +++++++-- .../@n8n/config/src/configs/logging.config.ts | 15 ++++++++++----- .../config/src/configs/user-management.config.ts | 9 +++++++-- .../@n8n/config/src/configs/workflows.config.ts | 10 +++++++--- packages/@n8n/config/src/custom-types.ts | 2 +- packages/@n8n/config/src/index.ts | 10 ++++++++-- packages/@n8n/config/test/custom-types.test.ts | 8 ++++---- packages/cli/src/modules/modules.config.ts | 4 ++-- 13 files changed, 88 insertions(+), 33 deletions(-) diff --git a/packages/@n8n/config/src/configs/auth.config.ts b/packages/@n8n/config/src/configs/auth.config.ts index ff6628cfa0..51a863494d 100644 --- a/packages/@n8n/config/src/configs/auth.config.ts +++ b/packages/@n8n/config/src/configs/auth.config.ts @@ -1,5 +1,11 @@ +import { z } from 'zod'; + import { Config, Env, Nested } from '../decorators'; +const samesiteSchema = z.enum(['strict', 'lax', 'none']); + +type Samesite = z.infer; + @Config class CookieConfig { /** This sets the `Secure` flag on n8n auth cookie */ @@ -7,8 +13,8 @@ class CookieConfig { secure: boolean = true; /** This sets the `Samesite` flag on n8n auth cookie */ - @Env('N8N_SAMESITE_COOKIE') - samesite: 'strict' | 'lax' | 'none' = 'lax'; + @Env('N8N_SAMESITE_COOKIE', samesiteSchema) + samesite: Samesite = 'lax'; } @Config diff --git a/packages/@n8n/config/src/configs/cache.config.ts b/packages/@n8n/config/src/configs/cache.config.ts index 50c3b8d1b1..be4f1d0667 100644 --- a/packages/@n8n/config/src/configs/cache.config.ts +++ b/packages/@n8n/config/src/configs/cache.config.ts @@ -1,5 +1,10 @@ +import { z } from 'zod'; + import { Config, Env, Nested } from '../decorators'; +const cacheBackendSchema = z.enum(['memory', 'redis', 'auto']); +type CacheBackend = z.infer; + @Config class MemoryConfig { /** Max size of memory cache in bytes */ @@ -25,8 +30,8 @@ class RedisConfig { @Config export class CacheConfig { /** Backend to use for caching. */ - @Env('N8N_CACHE_BACKEND') - backend: 'memory' | 'redis' | 'auto' = 'auto'; + @Env('N8N_CACHE_BACKEND', cacheBackendSchema) + backend: CacheBackend = 'auto'; @Nested memory: MemoryConfig; diff --git a/packages/@n8n/config/src/configs/database.config.ts b/packages/@n8n/config/src/configs/database.config.ts index dc8bdde98d..75dda6ae3b 100644 --- a/packages/@n8n/config/src/configs/database.config.ts +++ b/packages/@n8n/config/src/configs/database.config.ts @@ -1,5 +1,10 @@ +import { z } from 'zod'; + import { Config, Env, Nested } from '../decorators'; +const dbLoggingOptionsSchema = z.enum(['query', 'error', 'schema', 'warn', 'info', 'log', 'all']); +type DbLoggingOptions = z.infer; + @Config class LoggingConfig { /** Whether database logging is enabled. */ @@ -9,8 +14,8 @@ class LoggingConfig { /** * Database logging level. Requires `DB_LOGGING_MAX_EXECUTION_TIME` to be higher than `0`. */ - @Env('DB_LOGGING_OPTIONS') - options: 'query' | 'error' | 'schema' | 'warn' | 'info' | 'log' | 'all' = 'error'; + @Env('DB_LOGGING_OPTIONS', dbLoggingOptionsSchema) + options: DbLoggingOptions = 'error'; /** * Only queries that exceed this time (ms) will be logged. Set `0` to disable. @@ -131,11 +136,14 @@ export class SqliteConfig { executeVacuumOnStartup: boolean = false; } +const dbTypeSchema = z.enum(['sqlite', 'mariadb', 'mysqldb', 'postgresdb']); +type DbType = z.infer; + @Config export class DatabaseConfig { /** Type of database to use */ - @Env('DB_TYPE') - type: 'sqlite' | 'mariadb' | 'mysqldb' | 'postgresdb' = 'sqlite'; + @Env('DB_TYPE', dbTypeSchema) + type: DbType = 'sqlite'; /** Prefix for table names */ @Env('DB_TABLE_PREFIX') diff --git a/packages/@n8n/config/src/configs/event-bus.config.ts b/packages/@n8n/config/src/configs/event-bus.config.ts index b4782555d5..0a68c6e98a 100644 --- a/packages/@n8n/config/src/configs/event-bus.config.ts +++ b/packages/@n8n/config/src/configs/event-bus.config.ts @@ -1,3 +1,5 @@ +import { z } from 'zod'; + import { Config, Env, Nested } from '../decorators'; @Config @@ -15,6 +17,9 @@ class LogWriterConfig { logBaseName: string = 'n8nEventLog'; } +const recoveryModeSchema = z.enum(['simple', 'extensive']); +type RecoveryMode = z.infer; + @Config export class EventBusConfig { /** How often (in ms) to check for unsent event messages. Can in rare cases cause a message to be sent twice. `0` to disable */ @@ -26,6 +31,6 @@ export class EventBusConfig { logWriter: LogWriterConfig; /** Whether to recover execution details after a crash or only mark status executions as crashed. */ - @Env('N8N_EVENTBUS_RECOVERY_MODE') - crashRecoveryMode: 'simple' | 'extensive' = 'extensive'; + @Env('N8N_EVENTBUS_RECOVERY_MODE', recoveryModeSchema) + crashRecoveryMode: RecoveryMode = 'extensive'; } diff --git a/packages/@n8n/config/src/configs/external-storage.config.ts b/packages/@n8n/config/src/configs/external-storage.config.ts index aff2447d40..6a51c263e2 100644 --- a/packages/@n8n/config/src/configs/external-storage.config.ts +++ b/packages/@n8n/config/src/configs/external-storage.config.ts @@ -1,5 +1,11 @@ +import { z } from 'zod'; + import { Config, Env, Nested } from '../decorators'; +const protocolSchema = z.enum(['http', 'https']); + +export type Protocol = z.infer; + @Config class S3BucketConfig { /** Name of the n8n bucket in S3-compatible external storage */ @@ -28,8 +34,8 @@ export class S3Config { @Env('N8N_EXTERNAL_STORAGE_S3_HOST') host: string = ''; - @Env('N8N_EXTERNAL_STORAGE_S3_PROTOCOL') - protocol: 'http' | 'https' = 'https'; + @Env('N8N_EXTERNAL_STORAGE_S3_PROTOCOL', protocolSchema) + protocol: Protocol = 'https'; @Nested bucket: S3BucketConfig; diff --git a/packages/@n8n/config/src/configs/generic.config.ts b/packages/@n8n/config/src/configs/generic.config.ts index f6960b2415..8997a6b1d2 100644 --- a/packages/@n8n/config/src/configs/generic.config.ts +++ b/packages/@n8n/config/src/configs/generic.config.ts @@ -1,13 +1,18 @@ +import { z } from 'zod'; + import { Config, Env } from '../decorators'; +const releaseChannelSchema = z.enum(['stable', 'beta', 'nightly', 'dev']); +type ReleaseChannel = z.infer; + @Config export class GenericConfig { /** Default timezone for the n8n instance. Can be overridden on a per-workflow basis. */ @Env('GENERIC_TIMEZONE') timezone: string = 'America/New_York'; - @Env('N8N_RELEASE_TYPE') - releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev' = 'dev'; + @Env('N8N_RELEASE_TYPE', releaseChannelSchema) + releaseChannel: ReleaseChannel = 'dev'; /** Grace period (in seconds) to wait for components to shut down before process exit. */ @Env('N8N_GRACEFUL_SHUTDOWN_TIMEOUT') diff --git a/packages/@n8n/config/src/configs/logging.config.ts b/packages/@n8n/config/src/configs/logging.config.ts index 75c1836d2d..1b7676cdd0 100644 --- a/packages/@n8n/config/src/configs/logging.config.ts +++ b/packages/@n8n/config/src/configs/logging.config.ts @@ -1,4 +1,6 @@ -import { CommaSeperatedStringArray } from '../custom-types'; +import { z } from 'zod'; + +import { CommaSeparatedStringArray } from '../custom-types'; import { Config, Env, Nested } from '../decorators'; /** Scopes (areas of functionality) to filter logs by. */ @@ -41,6 +43,9 @@ class FileLoggingConfig { location: string = 'logs/n8n.log'; } +const logLevelSchema = z.enum(['error', 'warn', 'info', 'debug', 'silent']); +type LogLevel = z.infer; + @Config export class LoggingConfig { /** @@ -49,8 +54,8 @@ export class LoggingConfig { * * @example `N8N_LOG_LEVEL=info` will output `error`, `warn` and `info` logs, but not `debug`. */ - @Env('N8N_LOG_LEVEL') - level: 'error' | 'warn' | 'info' | 'debug' | 'silent' = 'info'; + @Env('N8N_LOG_LEVEL', logLevelSchema) + level: LogLevel = 'info'; /** * Where to output logs to. Options are: `console` or `file` or both in a comma separated list. @@ -58,7 +63,7 @@ export class LoggingConfig { * @example `N8N_LOG_OUTPUT=console,file` will output to both console and file. */ @Env('N8N_LOG_OUTPUT') - outputs: CommaSeperatedStringArray<'console' | 'file'> = ['console']; + outputs: CommaSeparatedStringArray<'console' | 'file'> = ['console']; @Nested file: FileLoggingConfig; @@ -85,5 +90,5 @@ export class LoggingConfig { * `N8N_LOG_SCOPES=license,waiting-executions` */ @Env('N8N_LOG_SCOPES') - scopes: CommaSeperatedStringArray = []; + scopes: CommaSeparatedStringArray = []; } diff --git a/packages/@n8n/config/src/configs/user-management.config.ts b/packages/@n8n/config/src/configs/user-management.config.ts index 06b3e64fea..7acda93d75 100644 --- a/packages/@n8n/config/src/configs/user-management.config.ts +++ b/packages/@n8n/config/src/configs/user-management.config.ts @@ -1,3 +1,5 @@ +import { z } from 'zod'; + import { Config, Env, Nested } from '../decorators'; @Config @@ -64,11 +66,14 @@ export class TemplateConfig { 'credentials-shared': string = ''; } +const emailModeSchema = z.enum(['', 'smtp']); +type EmailMode = z.infer; + @Config class EmailConfig { /** How to send emails */ - @Env('N8N_EMAIL_MODE') - mode: '' | 'smtp' = 'smtp'; + @Env('N8N_EMAIL_MODE', emailModeSchema) + mode: EmailMode = 'smtp'; @Nested smtp: SmtpConfig; diff --git a/packages/@n8n/config/src/configs/workflows.config.ts b/packages/@n8n/config/src/configs/workflows.config.ts index bebce7ec77..bd50e1bb60 100644 --- a/packages/@n8n/config/src/configs/workflows.config.ts +++ b/packages/@n8n/config/src/configs/workflows.config.ts @@ -1,5 +1,10 @@ +import { z } from 'zod'; + import { Config, Env } from '../decorators'; +const callerPolicySchema = z.enum(['any', 'none', 'workflowsFromAList', 'workflowsFromSameOwner']); +type CallerPolicy = z.infer; + @Config export class WorkflowsConfig { /** Default name for workflow */ @@ -7,9 +12,8 @@ export class WorkflowsConfig { defaultName: string = 'My workflow'; /** Default option for which workflows may call the current workflow */ - @Env('N8N_WORKFLOW_CALLER_POLICY_DEFAULT_OPTION') - callerPolicyDefaultOption: 'any' | 'none' | 'workflowsFromAList' | 'workflowsFromSameOwner' = - 'workflowsFromSameOwner'; + @Env('N8N_WORKFLOW_CALLER_POLICY_DEFAULT_OPTION', callerPolicySchema) + callerPolicyDefaultOption: CallerPolicy = 'workflowsFromSameOwner'; /** How many workflows to activate simultaneously during startup. */ @Env('N8N_WORKFLOW_ACTIVATION_BATCH_SIZE') diff --git a/packages/@n8n/config/src/custom-types.ts b/packages/@n8n/config/src/custom-types.ts index b89cad1597..7925846d88 100644 --- a/packages/@n8n/config/src/custom-types.ts +++ b/packages/@n8n/config/src/custom-types.ts @@ -6,7 +6,7 @@ abstract class StringArray extends Array { } } -export class CommaSeperatedStringArray extends StringArray { +export class CommaSeparatedStringArray extends StringArray { constructor(str: string) { super(str, ','); } diff --git a/packages/@n8n/config/src/index.ts b/packages/@n8n/config/src/index.ts index bc7d88ae60..c5ea38f5f4 100644 --- a/packages/@n8n/config/src/index.ts +++ b/packages/@n8n/config/src/index.ts @@ -1,3 +1,5 @@ +import { z } from 'zod'; + import { AiAssistantConfig } from './configs/aiAssistant.config'; import { AuthConfig } from './configs/auth.config'; import { CacheConfig } from './configs/cache.config'; @@ -38,6 +40,10 @@ export type { LogScope } from './configs/logging.config'; export { WorkflowsConfig } from './configs/workflows.config'; export * from './custom-types'; +const protocolSchema = z.enum(['http', 'https']); + +export type Protocol = z.infer; + @Config export class GlobalConfig { @Nested @@ -99,8 +105,8 @@ export class GlobalConfig { listen_address: string = '0.0.0.0'; /** HTTP Protocol via which n8n can be reached */ - @Env('N8N_PROTOCOL') - protocol: 'http' | 'https' = 'http'; + @Env('N8N_PROTOCOL', protocolSchema) + protocol: Protocol = 'http'; @Nested endpoints: EndpointsConfig; diff --git a/packages/@n8n/config/test/custom-types.test.ts b/packages/@n8n/config/test/custom-types.test.ts index 0e1ca80872..71b8fc3933 100644 --- a/packages/@n8n/config/test/custom-types.test.ts +++ b/packages/@n8n/config/test/custom-types.test.ts @@ -1,13 +1,13 @@ -import { CommaSeperatedStringArray, ColonSeparatedStringArray } from '../src/custom-types'; +import { CommaSeparatedStringArray, ColonSeparatedStringArray } from '../src/custom-types'; -describe('CommaSeperatedStringArray', () => { +describe('CommaSeparatedStringArray', () => { it('should parse comma-separated string into array', () => { - const result = new CommaSeperatedStringArray('a,b,c'); + const result = new CommaSeparatedStringArray('a,b,c'); expect(result).toEqual(['a', 'b', 'c']); }); it('should handle empty strings', () => { - const result = new CommaSeperatedStringArray('a,b,,,'); + const result = new CommaSeparatedStringArray('a,b,,,'); expect(result).toEqual(['a', 'b']); }); }); diff --git a/packages/cli/src/modules/modules.config.ts b/packages/cli/src/modules/modules.config.ts index 3550d2f5ae..013a813597 100644 --- a/packages/cli/src/modules/modules.config.ts +++ b/packages/cli/src/modules/modules.config.ts @@ -1,10 +1,10 @@ -import { CommaSeperatedStringArray, Config, Env } from '@n8n/config'; +import { CommaSeparatedStringArray, Config, Env } from '@n8n/config'; import { UnexpectedError } from 'n8n-workflow'; const moduleNames = ['insights'] as const; type ModuleName = (typeof moduleNames)[number]; -class Modules extends CommaSeperatedStringArray { +class Modules extends CommaSeparatedStringArray { constructor(str: string) { super(str);