From b78a107155de1b843699c49336a3b11d4a1a52d7 Mon Sep 17 00:00:00 2001 From: mohiit1502 Date: Fri, 6 Feb 2026 01:44:06 +0530 Subject: [PATCH] Moved server config higher up in global. from global..config --- docs/CONFIG_DRIVEN_BREAKING_CHANGE.md | 344 ++++++++++++ docs/CONFIG_DRIVEN_INITIALIZATION.md | 372 +++++++++++++ docs/ERROR_HANDLING.md | 465 ++++++++++++++++ docs/GLOBAL_LOGGER.md | 370 +++++++++++++ docs/MIGRATION_GUIDE.md | 482 +++++++++++++++++ docs/PLUGINS.md | 735 ++++++++++++++++++++++++++ docs/README.md | 537 +++++++++++++++++++ package.json | 2 +- v2/core/Application.ts | 11 +- v2/types/Context.ts | 10 + 10 files changed, 3323 insertions(+), 5 deletions(-) create mode 100644 docs/CONFIG_DRIVEN_BREAKING_CHANGE.md create mode 100644 docs/CONFIG_DRIVEN_INITIALIZATION.md create mode 100644 docs/ERROR_HANDLING.md create mode 100644 docs/GLOBAL_LOGGER.md create mode 100644 docs/MIGRATION_GUIDE.md create mode 100644 docs/PLUGINS.md create mode 100644 docs/README.md diff --git a/docs/CONFIG_DRIVEN_BREAKING_CHANGE.md b/docs/CONFIG_DRIVEN_BREAKING_CHANGE.md new file mode 100644 index 0000000..57e323d --- /dev/null +++ b/docs/CONFIG_DRIVEN_BREAKING_CHANGE.md @@ -0,0 +1,344 @@ +# Config-Driven Initialization - Breaking Change Summary + +## What Changed + +NSK v2 now supports **config-driven initialization** where `Application.create().build()` automatically loads and instantiates plugins from `armcorc.json` configuration. + +## Problem Solved + +### Before +- Config files only contained plugin **settings** +- You still had to **manually register** plugin instances in code: + ```typescript + App.create(app) + .plugin(createLoggerPlugin()) // Code change required + .plugin(createDatabasePlugin()) // Code change required + .build() + ``` +- Changing plugins required **code changes + rebuild + redeploy** + +### After +- Config file is the **single source of truth** +- `Application.create().build()` auto-loads plugins from config +- Changing plugins only requires **restart** (no rebuild) + +## How It Works + +### 1. Plugin Factory Registry + +When you import `@armco/node-starter-kit`, all built-in plugins automatically register their factory functions: + +```typescript +// Happens automatically when you import NSK +registerPluginFactory('logger', (config) => createLoggerPlugin(config)) +registerPluginFactory('database', (config) => createDatabasePlugin(config)) +registerPluginFactory('cache', (config) => createCachePlugin(config)) +// etc... +``` + +### 2. Hybrid Loading Logic (Config + Manual) + +```typescript +// In Application.build(): +// 1. Config is PRIMARY source of truth +// 2. Manual registration OVERRIDES specific plugins + +if (config has plugins) { + // Auto-load plugins from config + for (pluginName, pluginConfig in config.plugins) { + if (pluginName NOT manually registered && enabled !== false) { + const factory = pluginRegistry.get(pluginName) + const plugin = factory(pluginConfig) + application.plugin(plugin) + } + } + + // Add manually registered plugins (these override config) + for (manualPlugin in manualPlugins) { + application.plugin(manualPlugin) + } +} +``` + +**Key Behavior:** +- Config defines **what** plugins are enabled +- Manual registration **overrides** specific plugin instances +- Manually registered plugins should still be declared in config (for consistency) + +## Usage + +### Approach 1: Pure Config-Driven (Recommended) + +**server.ts:** +```typescript +import express from 'express' +import { Application } from '@armco/node-starter-kit' + +const app = express() +const nsk = await Application.create(app).build() // That's it! + +app.listen(3000) +``` + +**armcorc.json:** +```json +{ + "appName": "auth-core", + "plugins": { + "logger": { "enabled": true, "level": "info" }, + "database": { "enabled": true, "uri": "mongodb://localhost/authdb" }, + "cache": { "enabled": true, "adapter": "redis" } + } +} +``` + +**To change config:** +1. Edit `armcorc.json` +2. Restart the app +3. Done! (no rebuild needed) + +### Approach 2: Programmatic Config (Cloud Config) + +```typescript +import { Application } from '@armco/node-starter-kit' + +// Load from config server, K8s ConfigMap, env vars, etc. +const config = { + appName: 'auth-core', + plugins: { + logger: { + enabled: true, + level: process.env.LOG_LEVEL || 'info' + }, + database: { + enabled: true, + uri: process.env.MONGODB_URI + }, + }, +} + +const nsk = await Application.create(app) + .withConfig(config) + .build() +``` + +### Approach 3: Hybrid (RECOMMENDED for most cases) + +Config is primary source of truth, manual registration for overrides: + +**armcorc.json:** +```json +{ + "appName": "auth-core", + "plugins": { + "logger": { "enabled": true, "level": "info" }, + "database": { "enabled": true, "uri": "mongodb://..." }, + "cache": { "enabled": true }, + "socket": { "enabled": true } + } +} +``` + +**server.ts:** +```typescript +import http from 'http' +import { Application, createSocketPlugin } from '@armco/node-starter-kit' + +const app = express() +const server = http.createServer(app) + +const nsk = await Application.create(app) + // Config auto-loads: logger, database, cache + // Manual registration for socket (requires server instance) + .plugin(createSocketPlugin(server)) + .build() + +server.listen(3000) +``` + +**Benefits:** +- Config declares all plugins (single source of truth) +- Code only handles runtime requirements (e.g., HttpServer for socket) +- Easy restart-based reconfiguration for most plugins + +### Approach 4: Pure Manual (Still Supported) + +```typescript +import { Application, createLoggerPlugin } from '@armco/node-starter-kit' + +// Explicit plugin registration (old way, still works) +const nsk = await Application.create(app) + .plugin(createLoggerPlugin({ level: 'info' })) + .plugin(createDatabasePlugin({ uri: 'mongodb://...' })) + .build() +``` + +## Supported Plugins + +These plugins can be auto-loaded from config: + +| Plugin Name | Config Key | Notes | +|-------------|------------|-------| +| Logger | `logger` | ✅ Fully supported | +| Database | `database` | ✅ Fully supported | +| Cache | `cache` | ✅ Fully supported | +| Scheduler | `scheduler` | ✅ Fully supported | +| Telemetry | `telemetry` or `opentelemetry` | ✅ Fully supported | +| Socket.IO | `socket` | ⚠️ **Requires manual registration** (needs `HttpServer` instance) | + +### Socket.IO Special Case + +Socket.IO plugin requires an `HttpServer` instance, which cannot be provided via JSON config: + +```typescript +import http from 'http' +import { Application, createSocketPlugin } from '@armco/node-starter-kit' + +const app = express() +const server = http.createServer(app) + +const nsk = await Application.create(app) + // Auto-load other plugins from config + .withConfig('./armcorc.json') + // Manually add socket (requires server instance) + .plugin(createSocketPlugin(server, { cors: { origin: '*' } })) + .build() + +server.listen(3000) +``` + +## Configuration Examples + +### Minimal Config +```json +{ + "appName": "my-service", + "plugins": { + "logger": { "enabled": true } + } +} +``` + +### Production Config +```json +{ + "appName": "auth-core", + "env": "production", + "plugins": { + "logger": { + "enabled": true, + "level": "info", + "format": "json" + }, + "database": { + "enabled": true, + "adapter": "mongoose", + "uri": "mongodb://mongo:27017/authdb", + "options": { + "maxPoolSize": 10 + } + }, + "cache": { + "enabled": true, + "adapter": "redis", + "uri": "redis://redis:6379", + "ttl": 3600, + "keyPrefix": "auth:" + }, + "telemetry": { + "enabled": true, + "serviceName": "auth-core", + "serviceVersion": "1.0.0", + "otlpEndpoint": "http://tempo:4318", + "exporters": ["otlp"] + } + } +} +``` + +### Disable a Plugin +```json +{ + "appName": "my-service", + "plugins": { + "logger": { "enabled": true }, + "cache": { "enabled": false } // Disabled + } +} +``` + +## Migration Path + +### If You're Happy with Current Approach + +**No changes needed!** Manual plugin registration still works: + +```typescript +const nsk = await Application.create(app) + .plugin(createLoggerPlugin()) + .build() +``` + +### If You Want Config-Driven + +1. **Create `armcorc.json`** with your plugin configs +2. **Remove manual `.plugin()` calls** +3. **Simplify to** `Application.create(app).build()` + +## Benefits + +1. **Restart-based reconfiguration**: No rebuild or redeploy needed +2. **GitOps friendly**: Config changes tracked in version control +3. **Cloud config integration**: Load from K8s ConfigMaps, AWS Parameter Store, etc. +4. **Environment-specific**: Easy to swap configs per environment +5. **Cleaner code**: Less boilerplate in application code + +## Runtime Reconfiguration Workflow + +```bash +# 1. Edit config +vim armcorc.json + +# 2. Commit (optional, for GitOps) +git commit -am "Enable cache plugin" + +# 3. Restart app (PM2, K8s, systemd, etc.) +pm2 reload my-service +# or +kubectl rollout restart deployment/my-service + +# 4. New config is active (no rebuild!) +``` + +## Implementation Details + +### Files Changed/Added + +**New:** +- `v2/core/PluginFactory.ts` - Plugin factory registry +- `v2/examples/config-driven-initialization.ts` - Usage examples +- `v2/examples/armco.config.full.json` - Full config reference +- `v2/CONFIG_DRIVEN_INITIALIZATION.md` - Comprehensive docs + +**Modified:** +- `v2/core/Application.ts` - Auto-load plugins from config +- `v2/index.ts` - Export PluginFactory +- All plugin `index.ts` files - Register factories on import + +### Type Safety + +Config is still validated using Zod schemas, ensuring type safety at runtime. + +## Questions? + +- See `/v2/examples/config-driven-initialization.ts` for complete examples +- See `/v2/CONFIG_DRIVEN_INITIALIZATION.md` for detailed documentation +- See `/v2/examples/armco.config.full.json` for full config reference + +## Summary + +✅ **Backward compatible** - Old approach still works +✅ **Opt-in** - Use config-driven only if you want +✅ **Restart-based reconfiguration** - No rebuild needed +✅ **Cloud-native** - Integrates with config management systems +✅ **Type-safe** - Still validated with Zod schemas diff --git a/docs/CONFIG_DRIVEN_INITIALIZATION.md b/docs/CONFIG_DRIVEN_INITIALIZATION.md new file mode 100644 index 0000000..4dd9be0 --- /dev/null +++ b/docs/CONFIG_DRIVEN_INITIALIZATION.md @@ -0,0 +1,372 @@ +# Config-Driven Initialization + +NSK v2 now supports **config-driven initialization**, enabling restart-based reconfiguration without code changes or rebuilds. This document explains how it works and how to use it. + +## Overview + +### Previous Approach (Still Supported) +```typescript +import { Application, createLoggerPlugin, createDatabasePlugin } from '@armco/node-starter-kit' + +const nsk = await Application.create(app) + .plugin(createLoggerPlugin({ level: 'info' })) + .plugin(createDatabasePlugin({ uri: process.env.MONGODB_URI })) + .build() +``` + +### New Config-Driven Approach +```typescript +import { Application } from '@armco/node-starter-kit' + +// Just this - plugins auto-loaded from armcorc.json +const nsk = await Application.create(app).build() +``` + +**armcorc.json:** +```json +{ + "appName": "my-service", + "plugins": { + "logger": { "enabled": true, "level": "info" }, + "database": { "enabled": true, "uri": "mongodb://localhost/mydb" } + } +} +``` + +## Benefits + +1. **Restart-based reconfiguration**: Change config → restart app → changes applied (no rebuild) +2. **Single source of truth**: All configuration in one place (JSON/YAML) +3. **Cloud config integration**: Load from config server, env vars, K8s ConfigMaps, etc. +4. **Environment-specific configs**: Easy to swap configs per environment +5. **GitOps friendly**: Config changes tracked in version control + +## How It Works + +### Plugin Factory Registry + +NSK maintains a global registry that maps plugin names to factory functions: + +```typescript +// Internally, when you import @armco/node-starter-kit: +registerPluginFactory('logger', (config) => createLoggerPlugin(config)) +registerPluginFactory('database', (config) => createDatabasePlugin(config)) +registerPluginFactory('cache', (config) => createCachePlugin(config)) +// ... etc +``` + +### Auto-Loading Process + +When `Application.create(app).build()` is called **without** explicit plugins: + +1. NSK searches for `armcorc.json` (or `.armcorc`, `armco.config.js`, etc.) +2. Reads the `plugins` section +3. For each enabled plugin, looks up its factory in the registry +4. Instantiates the plugin with its config +5. Registers and starts all plugins + +## Usage Patterns + +### Pattern 1: Pure Config-Driven (Recommended) + +**server.ts:** +```typescript +import express from 'express' +import { Application } from '@armco/node-starter-kit' + +const app = express() + +// All plugins loaded from armcorc.json +const nsk = await Application.create(app).build() + +app.listen(3000) +``` + +**armcorc.json:** +```json +{ + "appName": "auth-core", + "plugins": { + "logger": { + "enabled": true, + "level": "info", + "format": "json" + }, + "database": { + "enabled": true, + "adapter": "mongoose", + "uri": "mongodb://localhost:27017/authdb" + }, + "cache": { + "enabled": true, + "adapter": "redis", + "uri": "redis://localhost:6379" + } + } +} +``` + +### Pattern 2: Cloud Config Integration + +```typescript +import { Application } from '@armco/node-starter-kit' + +// Load from cloud config store +const config = await fetchConfigFromCloudStore() + +const nsk = await Application.create(app) + .withConfig(config) + .build() +``` + +### Pattern 3: Environment Variables + +```typescript +import { Application } from '@armco/node-starter-kit' + +const config = { + appName: 'auth-core', + plugins: { + logger: { + enabled: true, + level: process.env.LOG_LEVEL || 'info', + }, + database: { + enabled: true, + uri: process.env.MONGODB_URI, + }, + }, +} + +const nsk = await Application.create(app) + .withConfig(config) + .build() +``` + +### Pattern 4: Mixed (Config + Manual) + +Use config for standard plugins, manual registration for special cases: + +```typescript +import { Application } from '@armco/node-starter-kit' +import http from 'http' + +const app = express() +const server = http.createServer(app) + +const nsk = await Application.create(app) + // Auto-load plugins from config + .withConfig('./armcorc.json') + // Manually add socket (requires server instance) + .plugin(createSocketPlugin(server)) + .build() +``` + +## Configuration Reference + +### Config File Locations + +NSK searches for configuration in this order: +1. `armco.config.js` (TypeScript: `armco.config.ts`) +2. `armco.config.json` +3. `.armcorc` +4. `.armcorc.json` +5. `.armcorc.js` +6. `armcorc.json` +7. `package.json` (under `"armco"` key) + +### Plugin Configuration Schema + +Each plugin in `plugins` object follows this structure: + +```json +{ + "plugins": { + "": { + "enabled": true, // Set to false to disable + "priority": 10, // Load order (optional) + "": "..." + } + } +} +``` + +### Supported Plugin Keys + +| Key | Plugin | Config Type | +|-----|--------|-------------| +| `logger` | Logger | `LoggerConfig` | +| `database` | Database | `DatabaseConfig` | +| `cache` | Cache | `CacheConfig` | +| `scheduler` | Scheduler | `SchedulerConfig` | +| `telemetry` or `opentelemetry` | OpenTelemetry | `OpenTelemetryConfig` | + +**Note**: `socket` plugin cannot be auto-loaded (requires `HttpServer` instance). + +## Migration Guide + +### If You're Using Manual Plugin Registration + +**Before:** +```typescript +const nsk = await Application.create(app) + .plugin(createLoggerPlugin({ level: 'info' })) + .plugin(createDatabasePlugin({ uri: 'mongodb://...' })) + .build() +``` + +**After (Option 1 - Config File):** + +Create `armcorc.json`: +```json +{ + "appName": "my-service", + "plugins": { + "logger": { "enabled": true, "level": "info" }, + "database": { "enabled": true, "uri": "mongodb://..." } + } +} +``` + +Update code: +```typescript +const nsk = await Application.create(app).build() +``` + +**After (Option 2 - Inline Config):** +```typescript +const nsk = await Application.create(app) + .withConfig({ + appName: 'my-service', + plugins: { + logger: { enabled: true, level: 'info' }, + database: { enabled: true, uri: 'mongodb://...' }, + }, + }) + .build() +``` + +## Runtime Reconfiguration + +### Pattern: Graceful Reload + +1. **Update config file** (manually, API, or config management tool) +2. **Send SIGTERM** to the process +3. **Process manager restarts** the app (PM2, K8s, systemd, etc.) +4. **NSK loads new config** on startup + +```bash +# Example with PM2 +vim armcorc.json # Edit config +pm2 reload my-service # Graceful reload +``` + +### Pattern: Hot Reload (Advanced) + +For hot reload without restart, you'll need custom logic: + +```typescript +import { watch } from 'fs' +import { Application } from '@armco/node-starter-kit' + +let nsk: Application + +async function loadApp() { + if (nsk) { + await nsk.shutdown() + } + nsk = await Application.create(app).build() +} + +// Initial load +await loadApp() + +// Watch for config changes +watch('./armcorc.json', async () => { + console.log('Config changed, reloading...') + await loadApp() +}) +``` + +## Custom Plugins + +To make your custom plugin auto-loadable: + +```typescript +import { registerPluginFactory, type PluginFactory } from '@armco/node-starter-kit' + +export class MyCustomPlugin extends BasePlugin { + // ... implementation +} + +export function createMyPlugin(config: MyConfig) { + return new MyCustomPlugin(config) +} + +// Register factory +registerPluginFactory('mycustom', (config) => + createMyPlugin(config as unknown as MyConfig) +) +``` + +Then use in config: +```json +{ + "plugins": { + "mycustom": { + "enabled": true, + "myOption": "value" + } + } +} +``` + +## Best Practices + +1. **Use config files for production**: More maintainable than inline configs +2. **Environment-specific configs**: Use separate files (`armcorc.prod.json`, `armcorc.dev.json`) +3. **Sensitive data**: Use env vars, not config files: + ```typescript + const config = await ConfigLoader.load() + config.plugins.database.uri = process.env.MONGODB_URI + ``` +4. **Validate configs**: NSK uses Zod for validation, but add your own checks +5. **Version control**: Commit configs (except secrets) for GitOps workflows +6. **Documentation**: Document your config schema for your team + +## Troubleshooting + +### "Plugin factory not found" + +**Cause**: Plugin not imported, so factory wasn't registered. + +**Solution**: Import the main package: +```typescript +import '@armco/node-starter-kit' // Triggers factory registration +``` + +Or import specific plugin: +```typescript +import '@armco/node-starter-kit/plugins/logger' +``` + +### Config not loading + +**Cause**: Config file not found or invalid. + +**Solution**: Check file name and location. Enable debug: +```typescript +const loader = new ConfigLoader() +const config = loader.load() +console.log('Loaded config:', config) +``` + +### Plugin not initializing + +**Cause**: Missing required config fields or `enabled: false`. + +**Solution**: Check plugin config requirements in type definitions. + +## Examples + +See `/v2/examples/config-driven-initialization.ts` for complete examples. diff --git a/docs/ERROR_HANDLING.md b/docs/ERROR_HANDLING.md new file mode 100644 index 0000000..cbd1611 --- /dev/null +++ b/docs/ERROR_HANDLING.md @@ -0,0 +1,465 @@ +# Error Handling Guide + +## Actionable Error Messages + +Node Starter Kit v2 provides detailed, actionable error messages to help you quickly resolve configuration and runtime issues. + +## Common Errors and Solutions + +### Configuration Errors + +#### Error: "appName is required and cannot be empty" + +**Cause**: Missing or empty `appName` in configuration + +**Solution**: +```typescript +// armco.config.ts +export default { + appName: 'my-app', // ✅ Add this + // ... rest of config +} +``` + +--- + +#### Error: "Database URI is required" + +**Cause**: Database plugin enabled but no URI provided + +**Solution**: +```typescript +export default { + plugins: { + database: { + uri: process.env.MONGO_URI || 'mongodb://localhost:27017/mydb' // ✅ Add URI + } + } +} +``` + +**Check**: +```bash +# Verify environment variable is set +echo $MONGO_URI + +# Set it if missing +export MONGO_URI="mongodb://localhost:27017/mydb" +``` + +--- + +#### Error: "Either secret, secretProvider, or publicKey must be provided" + +**Cause**: JWT middleware configured without authentication credentials + +**Solution**: +```typescript +export default { + middlewares: { + jwt: { + // Option 1: Direct secret (not recommended for production) + secret: process.env.JWT_SECRET, + + // Option 2: Secret provider (recommended) + secretProvider: () => process.env.JWT_SECRET, + + // Option 3: Public key for RS256/ES256 + publicKey: fs.readFileSync('public.pem', 'utf8'), + + algorithms: ['RS256'] + } + } +} +``` + +**Generate secret**: +```bash +# Generate a secure random secret +node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" + +# Add to .env +echo "JWT_SECRET=your_generated_secret" >> .env +``` + +--- + +#### Error: 'Algorithm "none" is not allowed for security reasons' + +**Cause**: JWT configuration includes the insecure "none" algorithm + +**Solution**: +```typescript +export default { + middlewares: { + jwt: { + // Don't do this + algorithms: ['HS256', 'none'], + + // Use secure algorithms only + algorithms: ['RS256', 'ES256', 'HS256'] + } + } +} +``` + +--- + +#### Error: "No configuration found" + +**Cause**: Config file not found or in wrong location + +**Solution**: +1. Create `armco.config.ts` in project root: +```typescript +export default { + appName: 'my-app', + // ... config +} +``` + +2. Or specify path explicitly: +```typescript +const nsk = await Application.create(app) + .withConfig('./path/to/config.ts') + .build() +``` + +**Expected file locations** (searched in order): +- `armco.config.js` +- `armco.config.ts` +- `armco.config.json` +- `.armcorc` +- `.armcorc.json` +- `.armcorc.js` +- `armcorc.json` + +--- + +#### Error: "TypeScript configuration files require ts-node or tsx to be installed" + +**Cause**: Using `.ts` config file without TypeScript loader + +**Solution**: +```bash +# Install tsx (recommended) +npm install --save-dev tsx + +# Or install ts-node +npm install --save-dev ts-node @types/node +``` + +--- + +### Runtime Errors + +#### Error: "Service 'logger' not found in container" + +**Cause**: Trying to resolve a service that wasn't registered + +**Solution**: +```typescript +// Make sure plugin is registered +const nsk = await Application.create(app) + .plugin(createLoggerPlugin()) // Register logger plugin + .build() + +// Then resolve +const logger = nsk.getContainer().resolve('logger') +``` + +**Check what's registered**: +```typescript +const registered = nsk.getContainer().getServiceNames() +console.log('Registered services:', registered) +``` + +--- + +#### Error: "Plugin 'my-plugin' depends on 'logger' which is not registered" + +**Cause**: Plugin dependencies not met + +**Solution**: +```typescript +// Register dependencies first +const nsk = await Application.create(app) + .plugin(createLoggerPlugin()) // Register logger first + .plugin(createDatabasePlugin()) // Database depends on logger + .plugin(new MyCustomPlugin()) // Custom plugin depends on both + .build() +``` + +**Plugin dependency order is automatic** - just register all required plugins. + +--- + +#### Error: "Circular dependency detected involving 'pluginA'" + +**Cause**: Two or more plugins depend on each other + +**Solution**: +```typescript +// Don't do this +class PluginA extends BasePlugin { + dependencies = ['pluginB'] +} +class PluginB extends BasePlugin { + dependencies = ['pluginA'] +} + +// Refactor to remove circular dependency +// Move shared functionality to a separate plugin +class SharedPlugin extends BasePlugin { + name = 'shared' +} +class PluginA extends BasePlugin { + dependencies = ['shared'] +} +class PluginB extends BasePlugin { + dependencies = ['shared'] +} +``` + +--- + +#### Error: "Database circuit breaker opened" + +**Cause**: Database connection failed multiple times + +**What it means**: Circuit breaker is protecting your application from cascading failures + +**Solution**: +1. Check database is running: +```bash +# MongoDB +mongosh --eval "db.adminCommand('ping')" + +# Docker +docker ps | grep mongo +``` + +2. Check connection string: +```bash +echo $MONGO_URI +# Should look like: mongodb://localhost:27017/mydb +``` + +3. Circuit breaker will auto-close after timeout: +```typescript +database: { + circuitBreaker: { + enabled: true, + threshold: 5, // Opens after 5 failures + timeout: 60000, // Tries again after 60 seconds + } +} +``` + +4. Monitor circuit breaker: +```typescript +database: { + circuitBreaker: { + onOpen: () => { + logger.error('Circuit opened - DB unreachable') + // Send alert, update health check, etc. + }, + onClose: () => { + logger.info('Circuit closed - DB recovered') + } + } +} +``` + +--- + +#### Error: "CSRF token missing" + +**Cause**: Request doesn't include CSRF token + +**Solution** (Frontend): +```typescript +// 1. Fetch CSRF token on page load +const response = await fetch('/csrf-token') +const { csrfToken } = await response.json() + +// 2. Include token in requests +await fetch('/api/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken // Add this header + }, + body: JSON.stringify(data) +}) +``` + +**Solution** (Backend - exclude path): +```typescript +csrf: { + excludePaths: ['/api/webhooks', '/auth/login'] // Exclude public endpoints +} +``` + +--- + +#### Error: "Unauthorized Access" (JWT) + +**Cause**: JWT token missing, invalid, or expired + +**Solution**: +1. **Token missing**: Include token in request +```typescript +await fetch('/api/protected', { + headers: { + 'Authorization': `Bearer ${token}` // Add bearer token + } +}) +``` + +2. **Token expired**: Refresh the token +```typescript +// Implement token refresh logic +if (error.status === 401) { + const newToken = await refreshToken() + // Retry request with new token +} +``` + +3. **Token invalid**: Re-authenticate +```typescript +// Redirect to login +window.location.href = '/login' +``` + +4. **Public paths**: Exclude from authentication +```typescript +jwt: { + publicPaths: ['/health', '/auth/login', '/auth/register'] +} +``` + +--- + +### Health Check Errors + +#### Health status: "unhealthy" + +**What to check**: +1. Visit `/health` endpoint to see which check failed +2. Check individual components: + - Database: `/health/ready` + - Memory: Check memory usage + - Custom checks: Review custom check logic + +**Example response**: +```json +{ + "status": "unhealthy", + "checks": { + "database": { + "status": "unhealthy", + "error": "Connection timeout" + }, + "memory": { + "status": "healthy", + "heapUsedPercent": 45 + } + } +} +``` + +**Solution**: Fix the failing component (usually database connection) + +--- + +## Debugging Tips + +### Enable Debug Logging + +```typescript +// armco.config.ts +export default { + plugins: { + logger: { + level: 'debug', // Enable debug logs + format: 'pretty' + } + } +} +``` + +### Check Container Contents + +```typescript +// See what services are registered +console.log('Services:', nsk.getContainer().getServiceNames()) + +// Check if specific service exists +if (nsk.getContainer().has('database')) { + console.log('Database is registered') +} +``` + +### Validate Config Before Starting + +```typescript +import { safeValidateConfig } from '@armco/node-starter-kit/v2' + +const result = safeValidateConfig(rawConfig) +if (!result.success) { + console.error('Config errors:', result.error.format()) + process.exit(1) +} +``` + +### Monitor Plugin Lifecycle + +```typescript +class MyPlugin extends BasePlugin { + async install(ctx) { + console.log('✓ MyPlugin: install') + await super.install(ctx) + } + + async start() { + console.log('✓ MyPlugin: start') + } + + async stop() { + console.log('✓ MyPlugin: stop') + } +} +``` + +--- + +## Getting Help + +If you're stuck: + +1. **Check logs**: Look for detailed error messages with stack traces +2. **Validate config**: Use `safeValidateConfig()` to check configuration +3. **Check examples**: See [examples directory](./examples/) +4. **Read docs**: Check [README](./README.md) and [Migration Guide](./MIGRATION_GUIDE.md) +5. **Open issue**: [GitHub Issues](https://github.com/ReStruct-Corporate-Advantage/node-starter-kit/issues) + +--- + +## Error Message Format + +All NSK errors follow this format: +``` +[Context] Error description + +Suggested actions: +1. Action one +2. Action two + +Examples: +- Example command or code snippet + +Related: link to documentation +``` + +This helps you quickly understand and fix the issue! diff --git a/docs/GLOBAL_LOGGER.md b/docs/GLOBAL_LOGGER.md new file mode 100644 index 0000000..65f25c5 --- /dev/null +++ b/docs/GLOBAL_LOGGER.md @@ -0,0 +1,370 @@ +# Global Logger Access + +NSK v2 provides first-class global access to the logger, matching the legacy NSK v1 behavior. Once NSK is initialized with the logger plugin, the logger is automatically available throughout your application **without requiring imports**. + +## How It Works + +### 1. Automatic Global Injection + +When the logger plugin is installed, NSK automatically makes it available on the global object: + +```typescript +// In your server.ts +import { Application } from '@armco/node-starter-kit' + +const nsk = await Application.create(app) + .withConfig({ + appName: 'my-service', + plugins: { + logger: { enabled: true, level: 'info' } + } + }) + .build() + +// Logger is now globally available! +logger.info('Application started') +``` + +### 2. TypeScript Type Safety + +NSK exports a global namespace augmentation (`globals.d.ts`) that provides full TypeScript type safety for the global logger: + +```typescript +// globals.d.ts (automatically included when you install NSK) +declare global { + var logger: Logger +} +``` + +**No configuration needed in your project!** Just install NSK and TypeScript will automatically recognize the `logger` global. + +## Usage + +### In Route Handlers + +```typescript +app.get('/api/users/:id', (req, res) => { + // No import needed! + logger.info('User request received', { userId: req.params.id }) + + try { + const user = getUserById(req.params.id) + logger.info('User found', { user }) + res.json(user) + } catch (error) { + logger.error('Failed to fetch user', { error }) + res.status(500).json({ error: 'Internal server error' }) + } +}) +``` + +### In Service Classes + +```typescript +class UserService { + async createUser(data: any) { + // Logger is globally available + logger.info('Creating user', { email: data.email }) + + try { + const user = await db.users.create(data) + logger.info('User created', { userId: user.id }) + return user + } catch (error) { + logger.error('Failed to create user', { error }) + throw error + } + } +} +``` + +### In Middleware + +```typescript +function requestLogger(req, res, next) { + const start = Date.now() + + logger.info('Incoming request', { + method: req.method, + path: req.path, + ip: req.ip, + }) + + res.on('finish', () => { + const duration = Date.now() - start + logger.info('Request completed', { + method: req.method, + path: req.path, + statusCode: res.statusCode, + duration: `${duration}ms`, + }) + }) + + next() +} +``` + +### In Error Handlers + +```typescript +function errorHandler(err, req, res, next) { + logger.error('Unhandled error', { + error: err.message, + stack: err.stack, + path: req.path, + }) + + res.status(500).json({ error: 'Internal server error' }) +} +``` + +### In Background Jobs + +```typescript +async function processQueue() { + logger.info('Processing queue') + + while (true) { + const job = await queue.getNext() + + if (!job) { + logger.debug('No jobs in queue') + await sleep(1000) + continue + } + + try { + logger.info('Processing job', { jobId: job.id }) + await processJob(job) + logger.info('Job completed', { jobId: job.id }) + } catch (error) { + logger.error('Job failed', { jobId: job.id, error }) + } + } +} +``` + +## Benefits + +### 1. No Import Boilerplate + +**Before (with imports):** +```typescript +import { logger } from './logger' // Repeated in every file +import { logger } from '../utils/logger' // Different paths +import { getLogger } from '@armco/node-starter-kit' // Different ways +``` + +**After (global):** +```typescript +// No imports needed - logger is just available! +logger.info('Request received') +``` + +### 2. Consistency + +Every file uses the same logger instance automatically: +- No confusion about which logger to import +- No risk of multiple logger instances +- Configuration changes apply everywhere instantly + +### 3. Cleaner Code + +```typescript +// ❌ With imports (verbose) +import express from 'express' +import { logger } from '../utils/logger' + +function handler(req, res) { + logger.info('Processing request') + // ... +} + +// ✅ With global (clean) +import express from 'express' + +function handler(req, res) { + logger.info('Processing request') + // ... +} +``` + +### 4. Migration Compatibility + +Matches legacy NSK v1 behavior - existing code works without changes! + +## TypeScript Configuration + +### No Configuration Needed! ✅ + +When you install `@armco/node-starter-kit`, TypeScript automatically recognizes the global logger through the exported `globals.d.ts` file. + +### How It Works + +1. NSK exports `globals.d.ts` in its package +2. TypeScript automatically picks it up via the triple-slash reference in `index.ts`: + ```typescript + /// + ``` +3. Your project gets full type safety without any configuration! + +### If You Need Manual Configuration (Rare) + +In very rare cases, if TypeScript doesn't pick up the types automatically, you can add this to your `tsconfig.json`: + +```json +{ + "compilerOptions": { + "types": ["@armco/node-starter-kit"] + } +} +``` + +But this is **usually not necessary** - the types are included automatically. + +## Best Practices + +### 1. Initialize NSK Early + +Ensure NSK is initialized before using the logger: + +```typescript +// server.ts +import { Application } from '@armco/node-starter-kit' + +async function main() { + // Initialize NSK first + const nsk = await Application.create(app) + .withConfig('./armcorc.json') + .build() + + // Now logger is available + logger.info('Application initialized') + + // Import other modules (they can now use logger) + const { setupRoutes } = await import('./routes') + setupRoutes(app) +} + +main() +``` + +### 2. Structured Logging + +Use structured logging with context objects: + +```typescript +// ✅ Good: Structured with context +logger.info('User login', { + userId: user.id, + email: user.email, + ip: req.ip, + timestamp: new Date().toISOString(), +}) + +// ❌ Less good: String concatenation +logger.info(`User ${user.id} logged in from ${req.ip}`) +``` + +### 3. Appropriate Log Levels + +Use the right log level for each situation: + +```typescript +logger.debug('Detailed debugging info') // Development only +logger.info('Normal operations') // General info +logger.warn('Something unusual') // Potential issues +logger.error('Something failed', { error }) // Errors +``` + +### 4. Error Logging + +Always include error objects and context: + +```typescript +try { + await riskyOperation() +} catch (error) { + logger.error('Operation failed', { + error, // Full error object + operation: 'riskyOperation', + userId: user.id, + context: { /* additional info */ } + }) +} +``` + +## Comparison: Legacy vs V2 + +### Legacy NSK v1 + +**Required manual namespace declaration in host project:** + +```typescript +// In your project's types/global.d.ts (had to create this) +declare global { + namespace NodeJS { + interface Global { + logger: any + } + } + var logger: any +} +``` + +### NSK v2 + +**No manual configuration needed!** + +Just install NSK and the types are automatically available. The namespace augmentation is exported from NSK itself. + +## Troubleshooting + +### "Cannot find name 'logger'" + +**Cause**: NSK not initialized yet or logger plugin not enabled. + +**Solution**: +1. Ensure logger plugin is enabled in `armcorc.json` +2. Initialize NSK before using the logger +3. Check that NSK initialization completed successfully + +### TypeScript error: "Property 'logger' does not exist" + +**Cause**: TypeScript not picking up the global types (very rare). + +**Solution**: +1. Restart your TypeScript language server +2. Check that `@armco/node-starter-kit` is installed +3. If still not working, add to `tsconfig.json`: + ```json + { + "compilerOptions": { + "types": ["@armco/node-starter-kit"] + } + } + ``` + +### Logger not available in some files + +**Cause**: File is imported/executed before NSK initialization. + +**Solution**: Ensure NSK is initialized before importing other application modules: + +```typescript +// ✅ Good +async function main() { + await initNSK() // Initialize first + const routes = await import('./routes') // Then import +} + +// ❌ Bad +import { routes } from './routes' // Imported before NSK init +async function main() { + await initNSK() +} +``` + +## Examples + +See `/v2/examples/global-logger-usage.ts` for complete working examples. diff --git a/docs/MIGRATION_GUIDE.md b/docs/MIGRATION_GUIDE.md new file mode 100644 index 0000000..d919ea6 --- /dev/null +++ b/docs/MIGRATION_GUIDE.md @@ -0,0 +1,482 @@ +# Migration Guide: v1 → v2 + +## Overview + +Node Starter Kit v2 is a complete architectural overhaul focused on modularity, security, and developer experience. This guide will help you migrate from v1.x to v2.0. + +## Breaking Changes + +### 1. Global Namespace Removed + +**v1:** +```typescript +import initNodeStarterKit from '@armco/node-starter-kit' + +initNodeStarterKit(app) +global.logger.info('Hello') +global.db.collection('users').find() +``` + +**v2:** +```typescript +import { Application, createLoggerPlugin } from '@armco/node-starter-kit/v2' + +const nsk = await Application.create(app) + .plugin(createLoggerPlugin()) + .build() + +const logger = nsk.getContainer().resolve('logger') +const db = nsk.getContainer().resolve('database') + +logger.info('Hello') +``` + +### 2. Configuration File Format + +**v1:** `armcorc.json` +```json +{ + "APP_NAME": "my-app", + "modules": { + "LOG": true, + "DB": { + "development": { + "connection_string": "mongodb://localhost/mydb" + } + } + } +} +``` + +**v2:** `armco.config.ts` (TypeScript with validation) +```typescript +export default { + appName: 'my-app', + plugins: { + logger: { level: 'info' }, + database: { + uri: process.env.MONGO_URI || 'mongodb://localhost/mydb' + } + } +} +``` + +### 3. Initialization API + +**v1:** +```typescript +import initNodeStarterKit from '@armco/node-starter-kit' + +initNodeStarterKit(app, { + server: httpServer, + supersedeModules: { + LOG: true, + DB: true + }, + supersedeMiddlewares: { + helmet: true, + cors: true + } +}) +``` + +**v2:** +```typescript +import { Application } from '@armco/node-starter-kit/v2' +import { initHelmet, initCors } from '@armco/node-starter-kit/v2' + +const nsk = await Application.create(app) + .withConfig('./armco.config.ts') + .build() + +const logger = nsk.getContainer().resolve('logger') + +initHelmet(app, { /* config */ }, logger) +initCors(app, { /* config */ }, logger) +``` + +### 4. CSRF Protection + +**v1:** Used deprecated `csurf` package + +**v2:** Modern double-submit cookie pattern +```typescript +import { initCsrf } from '@armco/node-starter-kit/v2' + +initCsrf(app, { + secret: process.env.CSRF_SECRET, + cookieOptions: { + httpOnly: true, + secure: true, + sameSite: 'strict' + } +}, logger) + +// Client must: +// 1. GET /csrf-token +// 2. Include token in X-CSRF-Token header +``` + +### 5. JWT Authentication + +**v1:** +```typescript +// Config-based with hardcoded secrets +middlewares: { + authentication: { + arOptions: { + secretKey: "my-secret", + algorithm: "HS256" + } + } +} +``` + +**v2:** +```typescript +import { initJwt } from '@armco/node-starter-kit/v2' + +initJwt(app, { + secretProvider: () => process.env.JWT_SECRET, // Enforces env vars + algorithms: ['RS256', 'ES256'], // Secure algorithms only + issuer: 'my-app', + publicPaths: ['/health', '/auth/login'] +}, logger) +``` + +## Migration Steps + +### Step 1: Update Dependencies + +```bash +npm install @armco/node-starter-kit@2.0.0 +npm install cosmiconfig tsx +npm uninstall csurf # Deprecated package +``` + +### Step 2: Create New Config File + +Create `armco.config.ts`: + +```typescript +export default { + appName: 'your-app-name', + + plugins: { + logger: { + level: process.env.LOG_LEVEL || 'info', + format: process.env.NODE_ENV === 'production' ? 'json' : 'pretty', + transports: [ + { type: 'console' }, + { type: 'file', filename: 'app.log' } + ] + }, + + database: { + uri: process.env.MONGO_URI, + circuitBreaker: { + enabled: true, + threshold: 5, + timeout: 60000 + } + } + }, + + health: { + enabled: true, + endpoint: '/health' + } +}) +``` + +### Step 3: Update Application Entry Point + +**Before:** +```typescript +import express from 'express' +import initNodeStarterKit from '@armco/node-starter-kit' + +const app = express() + +initNodeStarterKit(app, { + supersedeModules: { + LOG: true, + DB: true + } +}) + +app.listen(3000, () => { + global.logger.info('Server started') +}) +``` + +**After:** +```typescript +import express from 'express' +import { Application } from '@armco/node-starter-kit/v2' +import { createLoggerPlugin, createDatabasePlugin } from '@armco/node-starter-kit/v2' + +async function main() { + const app = express() + + const nsk = await Application.create(app) + .withConfig('./armco.config.ts') + .plugin(createLoggerPlugin()) + .plugin(createDatabasePlugin({ + uri: process.env.MONGO_URI + })) + .build() + + const logger = nsk.getContainer().resolve('logger') + + app.listen(3000, () => { + logger.info('Server started') + }) + + // Graceful shutdown is automatic +} + +main().catch(console.error) +``` + +### Step 4: Update Middleware Initialization + +**Before:** +```typescript +// Middlewares initialized automatically from config +``` + +**After:** +```typescript +import { + initHelmet, + initCors, + initJwt, + initRateLimiter +} from '@armco/node-starter-kit/v2' + +const logger = nsk.getContainer().resolve('logger') + +initHelmet(app, { + contentSecurityPolicy: { /* config */ } +}, logger) + +initCors(app, { + allowedOrigins: ['http://localhost:3000'], + credentials: true +}, logger) + +initJwt(app, { + secretProvider: () => process.env.JWT_SECRET, + algorithms: ['RS256'], + publicPaths: ['/health', '/auth/login'] +}, logger) + +initRateLimiter(app, { + windowMs: 15 * 60 * 1000, + max: 100 +}, logger) +``` + +### Step 5: Update Service Access + +**Before:** +```typescript +// Anywhere in your code +global.logger.info('Message') +global.db.collection('users').find() +``` + +**After:** +```typescript +// Pass services via DI or parameters +class UserService { + constructor( + private logger: Logger, + private db: Database + ) {} + + async getUsers() { + this.logger.info('Fetching users') + // Use db + } +} + +// In routes +app.get('/users', (req, res) => { + const logger = nsk.getContainer().resolve('logger') + logger.info('Request received') +}) +``` + +### Step 6: Update Health Checks + +**Before:** +```typescript +// Not available in v1 +``` + +**After:** +```typescript +import { HealthChecker } from '@armco/node-starter-kit/v2' + +const healthChecker = new HealthChecker( + nsk.getPluginManager(), + nsk.getConfig().health || {}, + logger +) + +app.use(healthChecker.createRouter()) + +// Available endpoints: +// GET /health - Full health check +// GET /health/live - Liveness probe +// GET /health/ready - Readiness probe +``` + +### Step 7: Update CSRF Protection + +**Before:** +```typescript +// csurf middleware from config +``` + +**After:** +```typescript +import { initCsrf } from '@armco/node-starter-kit/v2' + +initCsrf(app, { + enabled: process.env.NODE_ENV === 'production', + secret: process.env.CSRF_SECRET, + excludePaths: ['/health', '/auth/login'] +}, logger) + +// Client-side: Fetch /csrf-token and include in requests +``` + +### Step 8: Update Tests + +**Before:** +```typescript +// Tests affected by global state +``` + +**After:** +```typescript +import { Application } from '@armco/node-starter-kit/v2' + +describe('App', () => { + let nsk: Application + + beforeEach(async () => { + nsk = await Application.create(express()) + .withConfig({ + appName: 'test-app', + env: 'test', + plugins: { logger: { level: 'silent' } } + }) + .build() + }) + + afterEach(async () => { + await nsk.shutdown() // Clean shutdown + }) + + it('should work', () => { + const logger = nsk.getContainer().resolve('logger') + expect(logger).toBeDefined() + }) +}) +``` + +## Feature Parity + +| Feature | v1 | v2 | +|---------|----|----| +| Logger (Winston) | ✅ | ✅ | +| Database (Mongoose) | ✅ | ✅ | +| Socket.IO | ✅ | 🚧 Coming soon | +| Cron Jobs | ✅ | 🚧 Coming soon | +| Helmet | ✅ | ✅ | +| CORS | ✅ | ✅ | +| JWT Auth | ✅ | ✅ (improved) | +| CSRF | ✅ (deprecated lib) | ✅ (modern) | +| Rate Limiting | ✅ | ✅ | +| Health Checks | ❌ | ✅ | +| Circuit Breaker | ❌ | ✅ | +| DI Container | ❌ | ✅ | +| Plugin System | ❌ | ✅ | +| TypeScript | Partial | Full | + +## New Features in v2 + +### Circuit Breaker +```typescript +database: { + uri: process.env.MONGO_URI, + circuitBreaker: { + enabled: true, + threshold: 5, + timeout: 60000, + onOpen: () => logger.error('Circuit opened!') + } +} +``` + +### Custom Plugins +```typescript +class MyPlugin extends BasePlugin { + name = 'my-plugin' + version = '1.0.0' + + async install(ctx) { + ctx.container.singleton('myService', new MyService()) + } +} +``` + +### Health Checks +```typescript +// Kubernetes-ready probes +GET /health # Overall health +GET /health/live # Liveness probe +GET /health/ready # Readiness probe +``` + +## Troubleshooting + +### "Cannot find module 'cosmiconfig'" +```bash +npm install cosmiconfig +``` + +### "No configuration found" +Create `armco.config.ts` in project root or specify path: +```typescript +.withConfig('./path/to/config.ts') +``` + +### "JWT_SECRET is required" +Set environment variable: +```bash +export JWT_SECRET="your-secret-key" +``` + +### "TypeScript files not loading" +Install tsx or ts-node: +```bash +npm install tsx +``` + +## Getting Help + +- 📖 Read the [v2 README](./README.md) +- 💡 Check [examples](./examples/) +- 🐛 Report issues on [GitHub](https://github.com/ReStruct-Corporate-Advantage/node-starter-kit/issues) +- 💬 Ask questions in discussions + +## Timeline + +- **v1.x**: Maintenance mode (security fixes only) +- **v2.x**: Active development +- **v1 EOL**: 6 months after v2.0 release diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md new file mode 100644 index 0000000..2233b67 --- /dev/null +++ b/docs/PLUGINS.md @@ -0,0 +1,735 @@ +# Node Starter Kit v2 - Plugin Reference + +Complete guide to all available plugins and their configurations. + +--- + +## Core Plugins + +### Logger Plugin + +Winston-based logging with multiple transports and structured logging. + +**Installation:** +```typescript +import { createLoggerPlugin } from '@armco/node-starter-kit/v2' + +const nsk = await Application.create(app) + .plugin(createLoggerPlugin()) + .build() +``` + +**Armco-Specific Configuration:** +```typescript +createLoggerPlugin({ + enabled: true, // Enable/disable the plugin + level: 'info', // Log level: 'debug' | 'info' | 'warn' | 'error' | 'silent' + adapter: 'winston', // Logger adapter to use + format: 'json', // Output format: 'json' | 'pretty' + transports: [ // Array of transport configurations + { type: 'console' }, + { + type: 'file', + filename: 'logs/app.log', + maxsize: 5242880, // 5MB + maxFiles: 5 + }, + { + type: 'mongodb', + uri: process.env.MONGO_URI, + collection: 'logs' + }, + { + type: 'http', + url: 'https://logs.example.com' + } + ], + defaultMeta: { // Metadata added to all logs + app: 'my-app', + env: process.env.NODE_ENV + } +} +``` + +**Usage:** +```typescript +const logger = container.resolve('logger') +logger.info('User logged in', { userId: 123 }) +logger.error('Failed to process', { error }) +``` + +--- + +### Database Plugin + +Mongoose-based MongoDB connection with circuit breaker pattern. + +**Installation:** +```typescript +import { createDatabasePlugin } from '@armco/node-starter-kit/v2' + +const nsk = await Application.create(app) + .plugin(createDatabasePlugin({ uri: process.env.MONGO_URI })) + .build() +``` + +**Armco-Specific Configuration:** +```typescript +createDatabasePlugin({ + enabled: true, + adapter: 'mongoose', // Currently only 'mongoose' supported + uri: process.env.MONGO_URI, // MongoDB connection URI + options: { // Mongoose connection options + // See Mongoose documentation + }, + circuitBreaker: { + enabled: true, + threshold: 5, // Open after 5 failures + timeout: 60000, // Try again after 60 seconds + onOpen: () => { + logger.error('Database circuit breaker opened') + }, + onClose: () => { + logger.info('Database circuit breaker closed') + } + } +}) +``` + +**Usage:** +```typescript +const database = container.resolve('database') +const isConnected = await database.isConnected() +``` + +--- + +### Cache Plugin + +Flexible caching with Redis or in-memory adapters. + +**Installation:** +```typescript +import { createCachePlugin } from '@armco/node-starter-kit/v2' + +const nsk = await Application.create(app) + .plugin(createCachePlugin()) + .build() +``` + +**Armco-Specific Configuration:** +```typescript +createCachePlugin({ + enabled: true, + adapter: 'redis', // 'redis' | 'memory' + ttl: 3600, // Default TTL in seconds + maxItems: 1000, // Max items (memory adapter only) + uri: 'redis://localhost:6379', // Redis URI + keyPrefix: 'myapp:', // Prefix for all cache keys + + circuitBreaker: { // Redis adapter only + enabled: true, + threshold: 5, + timeout: 30000, + onOpen: () => logger.error('Cache circuit breaker opened'), + onClose: () => logger.info('Cache circuit breaker closed') + }, + + serialization: { + enabled: true, // Auto-serialize/deserialize + serialize: (value) => JSON.stringify(value), + deserialize: (value) => JSON.parse(value) + } +}) +``` + +**Usage:** +```typescript +const cache = container.resolve('cache') + +// Set +await cache.set('user:123', { name: 'John' }, 3600) + +// Get +const user = await cache.get('user:123') + +// Get or compute +const result = await cache.getOrSet('expensive', async () => { + return await computeExpensiveValue() +}, 3600) + +// Increment +await cache.increment('page:views') + +// Stats +const stats = await cache.stats() +``` + +--- + +### Socket.IO Plugin + +Real-time WebSocket communication with Socket.IO. + +**Installation:** +```typescript +import http from 'http' +import { createSocketPlugin } from '@armco/node-starter-kit/v2' + +const server = http.createServer(app) + +const nsk = await Application.create(app) + .plugin(createSocketPlugin(server)) + .build() +``` + +**Armco-Specific Configuration:** +```typescript +createSocketPlugin(server, { + enabled: true, + + options: { // Socket.IO server options + // See Socket.IO documentation + }, + + cors: { + origin: ['https://example.com'], + credentials: true + }, + + auth: { + enabled: true, + secret: process.env.JWT_SECRET, + secretProvider: () => process.env.JWT_SECRET + }, + + redis: { // Enable Redis adapter for clustering + enabled: true, + uri: 'redis://localhost:6379', + options: {} + }, + + handlers: { + connection: (socket) => { + console.log('Client connected:', socket.id) + }, + namespaces: { + '/admin': (socket) => { + console.log('Admin connected:', socket.id) + } + } + }, + + middleware: [ // Socket.IO middleware + (socket, next) => { + // Custom middleware + next() + } + ] +}) +``` + +**Usage:** +```typescript +const socket = container.resolve('socket') + +// Emit to all clients +socket.emit('notification', { message: 'Hello!' }) + +// Emit to namespace +socket.emitTo('/admin', 'update', { data }) + +// Emit to room +socket.emitToRoom('room1', 'message', { text: 'Hi' }) + +// Register handlers +socket.onConnection((clientSocket) => { + clientSocket.on('chat', (data) => { + clientSocket.broadcast.emit('chat', data) + }) +}) +``` + +--- + +### OpenTelemetry Plugin + +Distributed tracing and metrics using the OpenTelemetry standard. + +**Installation:** +```typescript +import { createOpenTelemetryPlugin } from '@armco/node-starter-kit/v2' + +const nsk = await Application.create(app) + .plugin(createOpenTelemetryPlugin({ + serviceName: 'auth-core', + otlpEndpoint: 'http://tempo:4318' + })) + .build() +``` + +**Armco-Specific Configuration:** +```typescript +createOpenTelemetryPlugin({ + enabled: true, // Enable/disable the plugin + serviceName: 'auth-core', // Required: Service name for traces + serviceVersion: '1.0.0', // Service version + + // Exporters + exporters: ['otlp', 'console'], // 'otlp' | 'console' | 'jaeger' | 'zipkin' + otlpEndpoint: 'http://tempo:4318', // Grafana Tempo/OTLP endpoint + otlpHeaders: { // Optional auth headers + 'Authorization': 'Bearer token' + }, + + // Features + autoInstrumentation: true, // Auto-instrument Express, MongoDB, Redis, etc. + enableTracing: true, // Enable distributed tracing + enableMetrics: true, // Enable metrics collection + sampleRate: 1.0, // Trace sample rate (0-1) + + // Resource attributes (for filtering in Grafana) + resourceAttributes: { + 'deployment.environment': 'production', + 'service.namespace': 'iam', + 'service.instance.id': process.env.HOSTNAME + }, + + // Advanced + disabledInstrumentations: [ // Disable specific instrumentations + '@opentelemetry/instrumentation-fs' + ] +}) +``` + +**Key Features:** +- ✅ Automatic instrumentation for Express, MongoDB, Redis, PostgreSQL +- ✅ Distributed tracing across microservices +- ✅ OTLP exporter for Grafana Tempo +- ✅ Jaeger and Zipkin exporters +- ✅ Metrics collection (counters, histograms, gauges) +- ✅ Dynamic imports (only loaded when enabled) + +**Usage:** +```typescript +// Auto-instrumentation works automatically - no manual instrumentation needed! +app.get('/users', async (req, res) => { + // This route is automatically traced + const users = await db.users.find() + res.json(users) +}) + +// Manual instrumentation (optional) +const otel = container.resolve('opentelemetry') +const tracer = otel.getTracer() + +const span = tracer.startSpan('complex-operation') +span.setAttribute('user.id', userId) +try { + await doWork() + span.setStatus({ code: 1 }) // OK +} catch (error) { + span.setStatus({ code: 2, message: String(error) }) // ERROR + throw error +} finally { + span.end() +} + +// Custom metrics +const meter = otel.getMeter() +const counter = meter.createCounter('orders_processed') +counter.add(1, { 'order.type': 'premium' }) +``` + +**View Traces in Grafana:** +1. Navigate to Grafana Explore → Tempo +2. Query: `{ service.name="auth-core" }` +3. View flame graphs and trace details + +**For third-party properties**, refer to: +- [OpenTelemetry Node.js Documentation](https://opentelemetry.io/docs/instrumentation/js/) +- [OTLP Exporter Configuration](https://opentelemetry.io/docs/reference/specification/protocol/exporter/) + +--- + +## Middleware Initialization + +Middlewares are not plugins but utility functions to initialize Express middleware. + +### Helmet (Security Headers) + +**Usage:** +```typescript +import { initHelmet } from '@armco/node-starter-kit/v2' + +initHelmet(app, { + enabled: true, + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'"] + } + }, + options: { + // See Helmet documentation + } +}, logger) +``` + +### CORS + +**Usage:** +```typescript +import { initCors } from '@armco/node-starter-kit/v2' + +initCors(app, { + enabled: true, + allowedOrigins: ['https://example.com'], + credentials: true, + allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'], + allowedHeaders: ['Content-Type', 'Authorization'] +}, logger) +``` + +### CSRF Protection + +**Usage:** +```typescript +import { initCsrf } from '@armco/node-starter-kit/v2' + +initCsrf(app, { + enabled: true, + secret: process.env.CSRF_SECRET, + cookieName: '_csrf', + headerName: 'X-CSRF-Token', + cookieOptions: { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict' + }, + excludePaths: ['/auth/login', '/webhooks'] +}, logger) +``` + +### JWT Authentication + +**Usage:** +```typescript +import { initJwt, signToken } from '@armco/node-starter-kit/v2' + +initJwt(app, { + enabled: true, + secretProvider: () => process.env.JWT_SECRET, + algorithms: ['RS256', 'HS256'], // Algorithm allowlist + issuer: 'my-app', + audience: 'my-api', + expiresIn: '1h', + tokenLocations: ['header', 'cookie'], + publicPaths: ['/health', '/auth/login'] +}, logger) + +// Sign a token +const token = signToken({ userId: 123 }, { + secret: process.env.JWT_SECRET, + expiresIn: '1h' +}) +``` + +### Rate Limiting + +**Usage:** +```typescript +import { initRateLimiter } from '@armco/node-starter-kit/v2' + +initRateLimiter(app, { + enabled: true, + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // Max requests per window + standardHeaders: true, + skipPaths: ['/health'], + message: 'Too many requests' +}, logger) +``` + +--- + +## Observability + +### Health Checks + +**Usage:** +```typescript +import { HealthChecker } from '@armco/node-starter-kit/v2' + +const healthChecker = new HealthChecker( + nsk.getPluginManager(), + nsk.getConfig().health || {}, + logger +) + +// Add custom checks +healthChecker.registerCheck('redis', async () => { + const isConnected = await redis.ping() + return { + status: isConnected ? 'healthy' : 'unhealthy', + message: 'Redis connection', + timestamp: new Date() + } +}) + +// Mount routes +app.use(healthChecker.createRouter()) +// GET /health - Overall health +// GET /health/live - Liveness probe +// GET /health/ready - Readiness probe +``` + +### Metrics Collection + +**Usage:** +```typescript +import { initMetrics } from '@armco/node-starter-kit/v2' + +const metrics = initMetrics(app, { + enabled: true, + endpoint: '/metrics', + labels: { + app: 'my-app', + env: process.env.NODE_ENV, + version: '1.0.0' + } +}, logger) + +// Record custom metrics +metrics.incrementCounter('events_processed', { type: 'click' }) +metrics.recordMetric('query_duration', 42, { table: 'users' }) + +// GET /metrics - JSON format +// GET /metrics/prometheus - Prometheus format +``` + +--- + +## Configuration Loading + +**File-based configuration:** + +Create `armco.config.ts`: +```typescript +export default { + appName: 'my-app', + env: process.env.NODE_ENV, + + plugins: { + logger: { + level: 'info', + transports: [{ type: 'console' }] + }, + database: { + uri: process.env.MONGO_URI + }, + cache: { + adapter: 'redis', + uri: process.env.REDIS_URI + } + }, + + middlewares: { + helmet: { enabled: true }, + cors: { + allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') + }, + jwt: { + secretProvider: () => process.env.JWT_SECRET + } + }, + + health: { + enabled: true, + endpoint: '/health' + }, + + metrics: { + enabled: true, + endpoint: '/metrics', + labels: { app: 'my-app' } + }, + + server: { + port: 3000 + } +}) +``` + +**Programmatic configuration:** +```typescript +const nsk = await Application.create(app) + .withConfig({ + appName: 'my-app', + // ... config + }) + .build() +``` + +--- + +## Plugin Development + +### Creating a Custom Plugin + +```typescript +import { BasePlugin } from '@armco/node-starter-kit/v2' +import { ApplicationContext } from '@armco/node-starter-kit/v2' + +export class MyPlugin extends BasePlugin { + constructor(config: MyConfig = {}) { + super('my-plugin', '1.0.0', ['logger'], config) + } + + async install(context: ApplicationContext): Promise { + await super.install(context) + + const logger = context.container.resolve('logger') + logger.info('Installing MyPlugin') + + // Register services + context.container.singleton('myService', new MyService()) + } + + async start(): Promise { + // Initialize resources + } + + async stop(): Promise { + // Cleanup resources + } + + async healthCheck(): Promise { + return { + status: 'healthy', + message: 'MyPlugin is operational', + timestamp: new Date() + } + } +} + +export function createMyPlugin(config?: MyConfig) { + return new MyPlugin(config) +} +``` + +--- + +## Best Practices + +### Plugin Order +Plugins are automatically ordered by dependencies, but you can specify priority: +```typescript +class MyPlugin extends BasePlugin { + readonly priority = 100 // Higher priority = installed first +} +``` + +### Environment Variables +Always use environment variables for secrets: +```typescript +// ❌ Don't hardcode secrets +const plugin = createPlugin({ secret: 'my-secret' }) + +// ✅ Use environment variables +const plugin = createPlugin({ + secretProvider: () => { + const secret = process.env.SECRET + if (!secret) { + throw new Error('SECRET environment variable is required') + } + return secret + } +}) +``` + +### Error Handling +Plugins should handle errors gracefully: +```typescript +async start(): Promise { + try { + await this.resource.connect() + } catch (error) { + this.logger?.error('Failed to start plugin', { error }) + throw error // Re-throw to prevent app startup + } +} +``` + +### Health Checks +Implement meaningful health checks: +```typescript +async healthCheck(): Promise { + try { + await this.database.ping() + const stats = await this.database.stats() + + return { + status: 'healthy', + message: 'Database is connected', + details: { + connections: stats.connections, + responseTime: stats.responseTime + }, + timestamp: new Date() + } + } catch (error) { + return { + status: 'unhealthy', + message: 'Database connection failed', + details: { error: String(error) }, + timestamp: new Date() + } + } +} +``` + +--- + +## Examples + +See the `v2/examples/` directory for complete working examples: +- `basic-usage.ts` - Simple setup +- `with-metrics.ts` - Metrics and observability +- `with-socket-io.ts` - Real-time communication +- `with-cache.ts` - Caching strategies + +--- + +## Troubleshooting + +### Plugin Not Found +``` +Error: Service 'myPlugin' not found in container +``` +**Solution:** Make sure the plugin is registered before trying to resolve it: +```typescript +.plugin(createMyPlugin()) // Register first +``` + +### Circular Dependencies +``` +Error: Circular dependency detected involving 'pluginA' +``` +**Solution:** Refactor to remove circular dependencies or use lazy loading. + +### Type Errors +``` +Error: Cannot find module '@armco/node-starter-kit/v2' +``` +**Solution:** Install dependencies: +```bash +npm install @armco/node-starter-kit +``` + +--- + +For more information, see: +- [README.md](./README.md) - Getting started +- [MIGRATION_GUIDE.md](./MIGRATION_GUIDE.md) - Upgrading from v1 +- [ERROR_HANDLING.md](./ERROR_HANDLING.md) - Common errors and solutions diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..bae2976 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,537 @@ +# Node Starter Kit v2.0 + +> Modern plugin-based starter kit for Node.js applications with TypeScript, security, and observability built-in. + +## 🎉 What's New in v2 + +- **Plugin Architecture**: Modular, extensible design with lifecycle hooks +- **Dependency Injection**: Type-safe container for service management +- **Modern Security**: Replaces deprecated `csurf` with secure double-submit cookie pattern +- **Algorithm Allowlist**: JWT authentication with enforced secure algorithms +- **Circuit Breaker**: Built-in circuit breaker for database connections +- **Health Checks**: Kubernetes-ready liveness and readiness probes +- **TypeScript First**: No `any` types, full type safety +- **Zero Global Pollution**: DI container replaces global state +- **Graceful Shutdown**: Proper cleanup of resources on termination +- **Configuration Validation**: Runtime validation with helpful error messages + +## 📦 Installation + +```bash +npm install @armco/node-starter-kit +``` + +## 🚀 Quick Start + +```typescript +import express from 'express' +import { Application } from '@armco/node-starter-kit/v2' +import { createLoggerPlugin } from '@armco/node-starter-kit/v2' + +const app = express() + +// Method 1: With configuration file (armco.config.ts) +const nsk = await Application.create(app) + .withConfig('./armco.config.ts') // Auto-discovers if not provided + .build() + +// Method 2: Inline configuration +const nsk = await Application.create(app) + .withConfig({ + appName: 'my-app', + plugins: { + logger: { level: 'info', format: 'pretty' } + } + }) + .plugin(createLoggerPlugin()) + .build() + +// Get services from DI container +const logger = nsk.getContainer().resolve('logger') + +app.get('/', (req, res) => { + logger.info('Request received') + res.json({ message: 'Hello World' }) +}) + +app.listen(3000, () => { + logger.info('Server started on port 3000') +}) +``` + +## 🔧 Configuration + +Create `armco.config.ts` in your project root: + +```typescript +export default { + appName: 'my-app', + + plugins: { + logger: { + level: 'info', + adapter: 'winston', + format: 'pretty', + transports: [ + { type: 'console' }, + { type: 'file', filename: 'app.log' } + ] + }, + + database: { + adapter: 'mongoose', + uri: process.env.MONGO_URI, + circuitBreaker: { + enabled: true, + threshold: 5, + timeout: 60000 + } + } + }, + + middlewares: { + helmet: { + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'"] + } + } + }, + + cors: { + allowedOrigins: ['http://localhost:3000'], + credentials: true + }, + + rateLimiter: { + windowMs: 15 * 60 * 1000, + max: 100 + }, + + jwt: { + secretProvider: () => process.env.JWT_SECRET, + algorithms: ['RS256', 'HS256'], + publicPaths: ['/health', '/auth/login'] + } + }, + + health: { + enabled: true, + endpoint: '/health' + } +} +``` + +## 🔌 Plugins + +### Logger Plugin + +```typescript +import { createLoggerPlugin } from '@armco/node-starter-kit/v2' + +nsk.plugin(createLoggerPlugin({ + level: 'info', + format: 'json', + transports: [ + { type: 'console' }, + { type: 'file', filename: 'app.log', maxsize: 10485760 }, + { type: 'mongodb', uri: process.env.MONGO_LOG_URI, collection: 'logs' } + ] +})) + +// Usage +const logger = nsk.getContainer().resolve('logger') +logger.info('Application started', { version: '1.0.0' }) +logger.error('Error occurred', { error }) +``` + +### Database Plugin + +```typescript +import { createDatabasePlugin } from '@armco/node-starter-kit/v2' + +nsk.plugin(createDatabasePlugin({ + adapter: 'mongoose', + uri: process.env.MONGO_URI, + options: { + retryWrites: true + }, + circuitBreaker: { + enabled: true, + threshold: 5, + timeout: 60000, + onOpen: () => logger.error('Circuit breaker opened!') + } +})) + +// Usage +const db = nsk.getContainer().resolve('database') +``` + +### Custom Plugin + +```typescript +import { BasePlugin, ApplicationContext } from '@armco/node-starter-kit/v2' + +class EmailPlugin extends BasePlugin { + name = 'email' + version = '1.0.0' + dependencies = ['logger'] + + async install(context: ApplicationContext) { + await super.install(context) + + const emailService = new EmailService(this.config) + context.container.singleton('email', emailService) + + this.getLogger()?.info('Email plugin installed') + } + + async start() { + this.getLogger()?.info('Email plugin started') + } + + async stop() { + this.getLogger()?.info('Email plugin stopped') + } +} + +nsk.plugin(new EmailPlugin({ apiKey: process.env.SENDGRID_KEY })) +``` + +### Bring Your Own Services (withServices) + +For advanced use cases where you need full control over service initialization (like databases), use `withServices()`: + +```typescript +import { PrismaClient } from '@prisma/client' +import { Application } from '@armco/node-starter-kit/v2' + +// You own the database initialization +const prisma = new PrismaClient({ + datasources: { db: { url: process.env.DATABASE_URL } }, + log: ['query', 'error'] +}) + +await prisma.$connect() + +// Register services directly in the DI container +const nsk = await Application.create(app) + .withServices((container) => { + // Register database + container.singleton('database', prisma) + + // Register repositories + container.singleton('userRepo', new UserRepository(prisma)) + + // Register any custom services + container.singleton('config', { maxRetries: 3 }) + }) + .plugin(createLoggerPlugin()) + .build() + +// Services available via DI +const db = nsk.getContainer().resolve('database') +const userRepo = nsk.getContainer().resolve('userRepo') +``` + +**When to use `withServices()`:** +- Complex database requirements (auth systems, multi-tenancy) +- Need full control over ORM/database client +- Using advanced features (Prisma migrations, Drizzle schemas) +- Existing codebase with established patterns + +**When to use NSK plugins:** +- Quick prototypes and MVPs +- Standard use cases +- Want batteries-included approach + +See [`examples/with-custom-db.ts`](./examples/with-custom-db.ts) for complete example. + +## 🛡️ Security Middlewares + +### Helmet (Security Headers) + +```typescript +import { initHelmet } from '@armco/node-starter-kit/v2' + +initHelmet(app, { + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'] + } + } +}, logger) +``` + +### CORS + +```typescript +import { initCors } from '@armco/node-starter-kit/v2' + +initCors(app, { + allowedOrigins: process.env.ALLOWED_ORIGINS.split(','), + credentials: true, + allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'] +}, logger) +``` + +### CSRF Protection (Modern) + +Replaces deprecated `csurf` with secure double-submit cookie pattern: + +```typescript +import { initCsrf } from '@armco/node-starter-kit/v2' + +initCsrf(app, { + enabled: process.env.NODE_ENV === 'production', + secret: process.env.CSRF_SECRET, + cookieOptions: { + httpOnly: true, + secure: true, + sameSite: 'strict' + }, + excludePaths: ['/health', '/auth/login'] +}, logger) + +// Client usage: +// 1. GET /csrf-token to receive token +// 2. Include token in X-CSRF-Token header for POST/PUT/DELETE +``` + +### JWT Authentication + +```typescript +import { initJwt } from '@armco/node-starter-kit/v2' + +initJwt(app, { + secretProvider: async () => { + // Load from vault, environment, etc. + return process.env.JWT_SECRET + }, + algorithms: ['RS256', 'ES256'], // Secure algorithms only + issuer: 'my-app', + expiresIn: '1h', + publicPaths: ['/health', '/auth/*'] +}, logger) + +// Authenticated routes automatically have req.user and req.token +app.get('/protected', (req, res) => { + res.json({ user: req.user }) +}) +``` + +### Rate Limiter + +```typescript +import { initRateLimiter } from '@armco/node-starter-kit/v2' + +initRateLimiter(app, { + windowMs: 15 * 60 * 1000, + max: 100, + keyGenerator: (req) => req.ip, + skipPaths: ['/health', '/metrics'] +}, logger) +``` + +## 🏥 Health Checks + +```typescript +import { HealthChecker } from '@armco/node-starter-kit/v2' + +const healthChecker = new HealthChecker( + nsk.getPluginManager(), + nsk.getConfig().health || {}, + logger +) + +// Register custom health check +healthChecker.registerCheck('redis', async () => { + const isConnected = await redis.ping() + return { + status: isConnected ? 'healthy' : 'unhealthy', + responseTime: Date.now() + } +}) + +// Add routes +app.use(healthChecker.createRouter()) + +// Endpoints: +// GET /health - Full health check +// GET /health/live - Liveness probe (always 200 if running) +// GET /health/ready - Readiness probe (200 if ready for traffic) +``` + +## 📊 Dependency Injection + +```typescript +// Register services +nsk.getContainer().singleton('cache', new RedisCache()) +nsk.getContainer().transient('requestId', () => uuid()) + +// Resolve services +const cache = nsk.getContainer().resolve('cache') +const logger = nsk.getContainer().resolve('logger') + +// Check if service exists +if (nsk.getContainer().has('database')) { + const db = nsk.getContainer().resolve('database') +} +``` + +## 🔄 Graceful Shutdown + +Automatic handling of SIGTERM and SIGINT: + +```typescript +// Plugins' stop() methods are called in reverse dependency order +process.on('SIGTERM', async () => { + await nsk.shutdown() + process.exit(0) +}) +``` + +## 🧪 Testing + +### Comprehensive Test Suite + +NSK v2 includes a production-grade testing infrastructure with: +- ✅ 82 tests (Unit + Integration) +- ✅ Exhaustive configuration testing +- ✅ Real host application simulation +- ✅ 80% coverage requirement +- ✅ Visual test UI + +### Run Tests + +```bash +# Run all tests +npm test + +# Visual UI +npm run test:ui + +# Coverage report +npm run test:coverage + +# Unit tests only +npm run test:unit + +# Integration tests only +npm run test:integration +``` + +### Test Your App + +```typescript +import { Application } from '@armco/node-starter-kit/v2' +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import express from 'express' +import request from 'supertest' + +describe('My App', () => { + let app: express.Express + let nsk: Application + + beforeEach(async () => { + app = express() + + nsk = await Application.create(app) + .withConfig({ + appName: 'test-app', + env: 'test', + plugins: { + logger: { level: 'silent' } + } + }) + .build() + + app.get('/api/test', (req, res) => res.json({ ok: true })) + }) + + afterEach(async () => { + await nsk.shutdown() + }) + + it('should start successfully', () => { + expect(nsk.getContainer().has('logger')).toBe(true) + }) + + it('should handle requests', async () => { + const response = await request(app).get('/api/test') + expect(response.status).toBe(200) + expect(response.body.ok).toBe(true) + }) +}) +``` + +### Exhaustive Config Testing + +Test files include exhaustive configurations that validate every option: +- `all-middlewares.config.ts` - Every security middleware +- `all-plugins.config.ts` - Every plugin with all options +- `production-like.config.ts` - Real production scenarios + +See [`__tests__/TEST_INFRASTRUCTURE_SUMMARY.md`](./__tests__/TEST_INFRASTRUCTURE_SUMMARY.md) for details. + +## 📝 Migration from v1 + +### Breaking Changes + +1. **Global namespace removed** - Use DI container +2. **Config file format changed** - `armcorc.json` → `armco.config.ts` +3. **Initialization API changed** - See examples above +4. **CSRF library replaced** - New double-submit cookie pattern + +### Migration Steps + +```typescript +// v1 (OLD) +import initNodeStarterKit from '@armco/node-starter-kit' +initNodeStarterKit(app, { + supersedeModules: { LOG: true, DB: true } +}) +global.logger.info('Hello') + +// v2 (NEW) +import { Application, createLoggerPlugin } from '@armco/node-starter-kit/v2' + +const nsk = await Application.create(app) + .plugin(createLoggerPlugin()) + .build() + +const logger = nsk.getContainer().resolve('logger') +logger.info('Hello') +``` + +## 🔒 Security Improvements + +- ✅ Removed deprecated `csurf` package +- ✅ JWT algorithm allowlist (no "none" algorithm) +- ✅ Enforced environment variables for secrets +- ✅ Circuit breaker for database connections +- ✅ Modern CSRF with double-submit cookies +- ✅ Updated dependencies (Mongoose 8, Helmet 7) + +## 📚 Examples + +See `/v2/examples` directory for: +- Basic usage +- Custom plugins +- Configuration examples +- Testing patterns + +## 🤝 Contributing + +Contributions welcome! Please read CONTRIBUTING.md first. + +## 📄 License + +ISC License - see LICENSE file for details + +## 🙏 Acknowledgments + +Built with love by the Armco team for the Node.js community. diff --git a/package.json b/package.json index 80f2945..6ccb5d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@armco/node-starter-kit", - "version": "2.1.1", + "version": "2.1.2", "description": "Modern plugin-based starter kit for Node.js applications with TypeScript, security, and observability", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/v2/core/Application.ts b/v2/core/Application.ts index 02cc751..ff75791 100644 --- a/v2/core/Application.ts +++ b/v2/core/Application.ts @@ -279,15 +279,18 @@ export class ApplicationBuilder { private setupGlobalNamespace(config: AppConfig): void { const appName = config.appName.replace(/[-\s]/g, '_').toUpperCase() const globalObj = global as Record - const rawConfig = config as unknown as Record // Create namespace with config sections globalObj[appName] = { - appConfig: rawConfig.APP_CONFIG || {}, + appConfig: config.APP_CONFIG || {}, config: config, - keys: rawConfig.KEYS || {}, - modules: rawConfig.modules || {}, + keys: config.KEYS || {}, + modules: config.modules || {}, env: config.env || process.env.NODE_ENV || 'development', + server: { + port: config.server?.port || 8081, + host: config.server?.host || '0.0.0.0', + }, } } diff --git a/v2/types/Context.ts b/v2/types/Context.ts index ef7f6d3..68f6e91 100644 --- a/v2/types/Context.ts +++ b/v2/types/Context.ts @@ -37,6 +37,16 @@ export interface AppConfig { health?: HealthConfig metrics?: MetricsConfig server?: ServerConfig + + /** Application-specific configuration (custom per app) */ + APP_CONFIG?: Record + /** Secrets and API keys */ + KEYS?: Record + /** Module configurations */ + modules?: Record + + /** Allow additional properties for extensibility */ + [key: string]: unknown } /**