diff --git a/tsconfig.json b/tsconfig.json index 89d06d7..537aff7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,6 @@ "compilerOptions": { "declaration": true, "declarationDir": "dist", - "ignoreDeprecations": "6.0", "target": "ES2020", "module": "commonjs", "moduleResolution": "node", diff --git a/v2/core/Application.ts b/v2/core/Application.ts index 7f42442..3964c80 100644 --- a/v2/core/Application.ts +++ b/v2/core/Application.ts @@ -276,12 +276,8 @@ export class ApplicationBuilder { * Build and return the application */ async build(): Promise { - // Import middleware initializers - const { initHelmet } = await import('../middlewares/security/helmet') - const { initCors } = await import('../middlewares/security/cors') - const { initRateLimiter } = await import('../middlewares/security/rateLimiter') - const { initCookieParser } = await import('../middlewares/utils/cookieParser') - const { initBodyParser } = await import('../middlewares/utils/bodyParser') + // Import middleware registry (this triggers all middleware self-registrations) + const { middlewareRegistry } = await import('../core/MiddlewareFactory') // Load configuration let finalConfig: AppConfig @@ -363,32 +359,10 @@ export class ApplicationBuilder { // Auto-inject middlewares from config (AFTER plugins so logger is available) const logger = application.getContainer().tryResolve('logger') - // Inject middlewares in order (IMPORTANT: body parsers first, then security, then auth) + // Initialize all configured middlewares using registry + // The registry automatically handles order, logging, and initialization if (finalConfig.middlewares) { - // 1. Body parser (must be early to parse request bodies) - if (finalConfig.middlewares.bodyParser) { - initBodyParser(this.app, finalConfig.middlewares.bodyParser as any, logger) - } - - // 2. Cookie parser (must be early to parse cookies) - if (finalConfig.middlewares.cookieParser) { - initCookieParser(this.app, finalConfig.middlewares.cookieParser as any, logger) - } - - // 3. Security middlewares - if (finalConfig.middlewares.helmet) { - initHelmet(this.app, finalConfig.middlewares.helmet as any, logger) - } - - if (finalConfig.middlewares.cors) { - initCors(this.app, finalConfig.middlewares.cors as any, logger) - } - - // 4. Rate limiting (after CORS) - const rateLimitConfig = finalConfig.middlewares.rateLimit || finalConfig.middlewares.rateLimiter - if (rateLimitConfig) { - initRateLimiter(this.app, rateLimitConfig as any, logger) - } + middlewareRegistry.initializeFromConfig(this.app, finalConfig.middlewares, logger) } return application diff --git a/v2/core/MiddlewareFactory.ts b/v2/core/MiddlewareFactory.ts new file mode 100644 index 0000000..feeb581 --- /dev/null +++ b/v2/core/MiddlewareFactory.ts @@ -0,0 +1,128 @@ +import { Application } from 'express' +import { Logger } from '../types/Logger' + +/** + * Middleware initializer function type + * Takes app, config, and logger, and registers the middleware + */ +export type MiddlewareInitializer = (app: Application, config: any, logger?: Logger) => void + +/** + * Middleware registration metadata + */ +export interface MiddlewareMetadata { + name: string + category: 'security' | 'utils' | 'auth' | 'custom' + order: number // Lower = earlier in middleware chain + initializer: MiddlewareInitializer +} + +/** + * Global registry for middleware initializers + * Maps middleware names (from config) to their initializer functions + */ +class MiddlewareFactoryRegistry { + private middlewares = new Map() + + /** + * Register a middleware initializer + * @param metadata - Middleware metadata including name and initializer function + */ + register(metadata: MiddlewareMetadata): void { + this.middlewares.set(metadata.name, metadata) + } + + /** + * Get a middleware by name + */ + get(name: string): MiddlewareMetadata | undefined { + return this.middlewares.get(name) + } + + /** + * Check if a middleware is registered + */ + has(name: string): boolean { + return this.middlewares.has(name) + } + + /** + * Get all registered middleware names + */ + getRegisteredNames(): string[] { + return Array.from(this.middlewares.keys()) + } + + /** + * Get middlewares sorted by execution order + */ + getSortedMiddlewares(): MiddlewareMetadata[] { + return Array.from(this.middlewares.values()).sort((a, b) => a.order - b.order) + } + + /** + * Initialize middlewares from config + * @param app - Express application + * @param middlewaresConfig - Middleware configuration from AppConfig + * @param logger - Optional logger instance + */ + initializeFromConfig( + app: Application, + middlewaresConfig: Record, + logger?: Logger + ): void { + // Get middlewares sorted by order + const sortedMiddlewares = this.getSortedMiddlewares() + + for (const middleware of sortedMiddlewares) { + const config = middlewaresConfig[middleware.name] + + // Skip if not configured + if (!config) { + continue + } + + // Skip if explicitly disabled + if (config.enabled === false) { + logger?.debug(`[NSK][${middleware.name.toUpperCase()}] Middleware disabled`) + continue + } + + try { + logger?.info(`[NSK][${middleware.name.toUpperCase()}] Initializing...`) + middleware.initializer(app, config, logger) + logger?.info(`[NSK][${middleware.name.toUpperCase()}] Initialized`) + } catch (error) { + logger?.error(`[NSK][${middleware.name.toUpperCase()}] Failed to initialize:`, error) + throw new Error( + `Middleware initialization failed for '${middleware.name}': ${ + error instanceof Error ? error.message : String(error) + }` + ) + } + } + } +} + +/** + * Global middleware factory registry instance + */ +export const middlewareRegistry = new MiddlewareFactoryRegistry() + +/** + * Register a middleware initializer (convenience function) + * + * @example + * ```typescript + * // In your middleware file: + * registerMiddleware({ + * name: 'cors', + * category: 'security', + * order: 30, + * initializer: initCors + * }) + * ``` + */ +export function registerMiddleware(metadata: MiddlewareMetadata): void { + middlewareRegistry.register(metadata) +} diff --git a/v2/index.ts b/v2/index.ts index 72fa64e..ed88ed9 100644 --- a/v2/index.ts +++ b/v2/index.ts @@ -40,7 +40,7 @@ export { validateConfig, safeValidateConfig, formatValidationError, AppConfigSch // Testing utilities export * from './testing' -// Middleware exports +// Middleware exports (importing these files triggers their self-registration) export { initHelmet, type HelmetConfig } from './middlewares/security/helmet' export { initCors, type CorsConfig } from './middlewares/security/cors' export { initCsrf, type CsrfConfig } from './middlewares/security/csrf' @@ -49,6 +49,9 @@ export { initJwt, signToken, type JwtConfig } from './middlewares/auth/jwt' export { initCookieParser, type CookieParserConfig } from './middlewares/utils/cookieParser' export { initBodyParser, type BodyParserConfig } from './middlewares/utils/bodyParser' +// Middleware registry for advanced use cases +export { middlewareRegistry, registerMiddleware, type MiddlewareMetadata } from './core/MiddlewareFactory' + // Re-export Application.create as default export { Application as default } from './core/Application' diff --git a/v2/middlewares/README.md b/v2/middlewares/README.md new file mode 100644 index 0000000..71759c5 --- /dev/null +++ b/v2/middlewares/README.md @@ -0,0 +1,167 @@ +# NSK Middleware System + +NSK uses a **middleware registry pattern** for scalable middleware management. + +## How to Add a New Middleware + +### ✅ Changes Required: **1-2 Places** + +1. **Create middleware file** (e.g., `v2/middlewares/security/myMiddleware.ts`) +2. **(Optional)** Export in `v2/index.ts` if needed for external use + +That's it! The middleware will automatically: +- Register itself when imported +- Initialize from `armcorc.json` if configured +- Log with consistent `[NSK][MIDDLEWARE]` prefixes +- Execute in the correct order + +--- + +## Example: Adding a New Middleware + +### Step 1: Create the Middleware File + +```typescript +// v2/middlewares/security/compression.ts +import compression from 'compression' +import { Application } from 'express' +import { Logger } from '../../types/Logger' +import { registerMiddleware } from '../../core/MiddlewareFactory' + +export interface CompressionConfig { + enabled?: boolean + level?: number + threshold?: number +} + +/** + * Initialize compression middleware + * Compresses response bodies + */ +export function initCompression(app: Application, config: CompressionConfig, logger?: Logger): void { + const options = { + level: config.level || 6, + threshold: config.threshold || 1024 + } + + app.use(compression(options)) + logger?.debug('[NSK][COMPRESSION] Config:', options) +} + +// Self-register this middleware +registerMiddleware({ + name: 'compression', // Name used in armcorc.json + category: 'security', // Category for organization + order: 35, // Execution order (lower = earlier) + initializer: initCompression +}) +``` + +### Step 2: (Optional) Export in index.ts + +```typescript +// v2/index.ts +export { initCompression, type CompressionConfig } from './middlewares/security/compression' +``` + +### Step 3: Configure in armcorc.json + +```json +{ + "middlewares": { + "compression": { + "enabled": true, + "level": 6, + "threshold": 1024 + } + } +} +``` + +**Done!** The middleware will automatically initialize when `Application.build()` is called. + +--- + +## Middleware Execution Order + +Middlewares execute based on their `order` value (lower = earlier): + +- **10** - bodyParser (parse request bodies first) +- **20** - cookieParser (parse cookies) +- **30** - helmet (security headers) +- **40** - cors (cross-origin handling) +- **50** - rateLimit (rate limiting) +- **60+** - Custom middlewares + +--- + +## Architecture Benefits + +### Before (Manual Import Pattern) +Adding a middleware required changes in **4 places**: +1. Create middleware file +2. Export in index.ts +3. Add schema in ConfigSchema.ts +4. Import + call in Application.ts + +### After (Registry Pattern) +Adding a middleware requires changes in **1-2 places**: +1. Create middleware file with self-registration +2. (Optional) Export in index.ts + +### Key Features + +- ✅ **Single source of truth**: Each middleware is self-contained +- ✅ **Automatic initialization**: From armcorc.json config +- ✅ **Consistent logging**: `[NSK][MIDDLEWARE]` prefix automatically added +- ✅ **Order management**: Defined once in middleware file +- ✅ **Enabled/disabled checks**: Handled by registry +- ✅ **Type-safe**: Full TypeScript support + +--- + +## Registry API + +```typescript +// Register a middleware +registerMiddleware({ + name: 'myMiddleware', + category: 'security', + order: 35, + initializer: initMyMiddleware +}) + +// Get registered middleware +const middleware = middlewareRegistry.get('cors') + +// Check if registered +const exists = middlewareRegistry.has('helmet') + +// Get all middleware names +const names = middlewareRegistry.getRegisteredNames() + +// Initialize all from config (automatically called by Application.build) +middlewareRegistry.initializeFromConfig(app, config.middlewares, logger) +``` + +--- + +## Logging + +The registry automatically wraps initialization with consistent logging: + +``` +[NSK][BODYPARSER] Initializing... +[NSK][BODYPARSER] JSON parser enabled +[NSK][BODYPARSER] URL-encoded parser enabled +[NSK][BODYPARSER] Initialized + +[NSK][HELMET] Initializing... +[NSK][HELMET] Initialized + +[NSK][CORS] Initializing... +[NSK][CORS] Allowed origins: ["http://localhost:3000", ...] +[NSK][CORS] Initialized +``` + +Your middleware's logger calls use the same prefix, maintaining consistency. diff --git a/v2/middlewares/security/cors.ts b/v2/middlewares/security/cors.ts index 2b6ddd1..93aa118 100644 --- a/v2/middlewares/security/cors.ts +++ b/v2/middlewares/security/cors.ts @@ -1,6 +1,7 @@ import cors, { CorsOptions } from 'cors' import { Application, RequestHandler } from 'express' import { Logger } from '../../types/Logger' +import { registerMiddleware } from '../../core/MiddlewareFactory' export interface CorsConfig { enabled?: boolean @@ -18,13 +19,6 @@ export interface CorsConfig { * Handles cross-origin requests with validation */ export function initCors(app: Application, config: CorsConfig, logger?: Logger): void { - if (config.enabled === false) { - logger?.debug('[NSK][CORS] Middleware disabled') - return - } - - logger?.info('[NSK][CORS] Initializing...') - const corsOptions: CorsOptions = { credentials: config.credentials ?? true, maxAge: config.maxAge, @@ -44,12 +38,12 @@ export function initCors(app: Application, config: CorsConfig, logger?: Logger): if (config.allowedOrigins!.includes(origin)) { callback(null, true) } else { - logger?.warn(`Blocked CORS request from origin: ${origin}`) + logger?.warn(`[NSK][CORS] Blocked request from origin: ${origin}`) callback(new Error(`Origin ${origin} not allowed by CORS`)) } } - logger?.info('[NSK][CORS] Allowed origins:', { origins: config.allowedOrigins }) + logger?.debug('[NSK][CORS] Allowed origins:', { origins: config.allowedOrigins }) } else { // Default: allow all origins corsOptions.origin = true @@ -69,6 +63,12 @@ export function initCors(app: Application, config: CorsConfig, logger?: Logger): } app.use(cors(corsOptions) as RequestHandler) - - logger?.info('[NSK][CORS] Initialized') } + +// Self-register this middleware +registerMiddleware({ + name: 'cors', + category: 'security', + order: 40, // After helmet + initializer: initCors +}) diff --git a/v2/middlewares/security/helmet.ts b/v2/middlewares/security/helmet.ts index 5ad8f8f..660838a 100644 --- a/v2/middlewares/security/helmet.ts +++ b/v2/middlewares/security/helmet.ts @@ -1,6 +1,7 @@ import helmet, { HelmetOptions } from 'helmet' import { Application, RequestHandler } from 'express' import { Logger } from '../../types/Logger' +import { registerMiddleware } from '../../core/MiddlewareFactory' export interface HelmetConfig { enabled?: boolean @@ -16,13 +17,6 @@ export interface HelmetConfig { * Sets various security headers */ export function initHelmet(app: Application, config: HelmetConfig, logger?: Logger): void { - if (config.enabled === false) { - logger?.debug('[NSK][HELMET] Middleware disabled') - return - } - - logger?.info('[NSK][HELMET] Initializing...') - const options: HelmetOptions = config.options || {} // Handle CSP configuration @@ -39,6 +33,12 @@ export function initHelmet(app: Application, config: HelmetConfig, logger?: Logg } app.use(helmet(options) as RequestHandler) - - logger?.info('[NSK][HELMET] Initialized') } + +// Self-register this middleware +registerMiddleware({ + name: 'helmet', + category: 'security', + order: 30, // After parsers + initializer: initHelmet +}) diff --git a/v2/middlewares/security/rateLimiter.ts b/v2/middlewares/security/rateLimiter.ts index 5e934e5..054ac4d 100644 --- a/v2/middlewares/security/rateLimiter.ts +++ b/v2/middlewares/security/rateLimiter.ts @@ -1,6 +1,7 @@ import rateLimit, { Options } from 'express-rate-limit' import { Application, Request, Response } from 'express' import { Logger } from '../../types/Logger' +import { registerMiddleware } from '../../core/MiddlewareFactory' export interface RateLimiterConfig { enabled?: boolean @@ -20,13 +21,6 @@ export interface RateLimiterConfig { * Protects against brute force and DoS attacks */ export function initRateLimiter(app: Application, config: RateLimiterConfig, logger?: Logger): void { - if (config.enabled === false) { - logger?.debug('[NSK][RATELIMITER] Middleware disabled') - return - } - - logger?.info('[NSK][RATELIMITER] Initializing...') - const skipPaths = config.skipPaths || [] const options: Partial = { @@ -58,8 +52,24 @@ export function initRateLimiter(app: Application, config: RateLimiterConfig, log app.use(rateLimit(options)) - logger?.info('[NSK][RATELIMITER] Initialized', { + logger?.debug('[NSK][RATELIMITER] Config:', { windowMs: options.windowMs, max: options.max, }) } + +// Self-register this middleware +registerMiddleware({ + name: 'rateLimit', + category: 'security', + order: 50, // After CORS + initializer: initRateLimiter +}) + +// Also register as 'rateLimiter' alias for backward compatibility +registerMiddleware({ + name: 'rateLimiter', + category: 'security', + order: 50, + initializer: initRateLimiter +}) diff --git a/v2/middlewares/utils/bodyParser.ts b/v2/middlewares/utils/bodyParser.ts index ebd0ad8..77c610f 100644 --- a/v2/middlewares/utils/bodyParser.ts +++ b/v2/middlewares/utils/bodyParser.ts @@ -1,5 +1,6 @@ import express, { Application } from 'express' import { Logger } from '../../types/Logger' +import { registerMiddleware } from '../../core/MiddlewareFactory' export interface BodyParserConfig { json?: { @@ -17,8 +18,6 @@ export interface BodyParserConfig { * Parses request body as JSON and URL-encoded */ export function initBodyParser(app: Application, config: BodyParserConfig, logger?: Logger): void { - logger?.info('[NSK][BODYPARSER] Initializing...') - // JSON parser if (config.json?.enabled !== false) { const jsonOptions = config.json?.options || {} @@ -32,6 +31,12 @@ export function initBodyParser(app: Application, config: BodyParserConfig, logge app.use(express.urlencoded(urlencodedOptions)) logger?.debug('[NSK][BODYPARSER] URL-encoded parser enabled') } - - logger?.info('[NSK][BODYPARSER] Initialized') } + +// Self-register this middleware +registerMiddleware({ + name: 'bodyParser', + category: 'utils', + order: 10, // Early in chain (body parsing should be first) + initializer: initBodyParser +}) diff --git a/v2/middlewares/utils/cookieParser.ts b/v2/middlewares/utils/cookieParser.ts index 2509c1b..35764fe 100644 --- a/v2/middlewares/utils/cookieParser.ts +++ b/v2/middlewares/utils/cookieParser.ts @@ -1,6 +1,7 @@ import cookieParser from 'cookie-parser' import { Application, RequestHandler } from 'express' import { Logger } from '../../types/Logger' +import { registerMiddleware } from '../../core/MiddlewareFactory' export interface CookieParserConfig { enabled?: boolean @@ -13,18 +14,19 @@ export interface CookieParserConfig { * Parses cookies from request headers */ export function initCookieParser(app: Application, config: CookieParserConfig, logger?: Logger): void { - if (config.enabled === false) { - logger?.debug('[NSK][COOKIEPARSER] Middleware disabled') - return - } - - logger?.info('[NSK][COOKIEPARSER] Initializing...') - if (config.secret) { app.use(cookieParser(config.secret, config.options) as RequestHandler) - logger?.info('[NSK][COOKIEPARSER] Initialized with secret') + logger?.debug('[NSK][COOKIEPARSER] Using secret') } else { app.use(cookieParser(undefined, config.options) as RequestHandler) - logger?.info('[NSK][COOKIEPARSER] Initialized without secret') + logger?.debug('[NSK][COOKIEPARSER] No secret') } } + +// Self-register this middleware +registerMiddleware({ + name: 'cookieParser', + category: 'utils', + order: 20, // After body parser + initializer: initCookieParser +})