Skip to main content

Middleware Design

Middleware Design Document

Project: {{PROJECT_NAME}}Drop Version: {{VERSION}}0.1.0 Date: {{DATE}}2026-02-23 Author: {{AUTHOR}}Platform Architect (AI) Status: Draft | In Review | Approved Reviewers: {{REVIEWERS}}Alem Bašić (CEO)

Document History

Version Date Author Changes
0.1 {{DATE}}2026-02-23 {{AUTHOR}}Platform Architect (AI) Initial draft from source code analysis

1. Middleware Pipeline Overview

Drop

sequenceDiagramhas participanttwo Clientmiddleware participantlayers:

CORS
    as 1. CORS participant Security as 2. Security Headers participant RequestID as 3. Request ID participant RateLimit as 4. Rate Limiter participant Logger as 5. Request Logger participant Auth as 6. Authentication participant Authz as 7. Authorization (RBAC) participant Validate as 8. Validation participant AuditLog as 9. Audit Logger participant Handler as Route Handler Client->>CORS: HTTP Request CORS->>Security: (CORS headers set) Security->>RequestID: (Security headers set) RequestID->>RateLimit: (X-Request-ID injected) RateLimit->>Logger: (Rate check passed) Logger->>Auth: (Request logged) Auth->>Authz: (JWT validated, user attached) Authz->>Validate: (Permissions verified) Validate->>AuditLog: (Input validated & sanitized) AuditLog->>Handler: (Audit record written) Handler-->>Client: Response
  • Framework: {{NestJS / Express / Fastify / Hono}}src/lib/middleware.ts Execution order is strictchangingThe orderactive maymiddleware breakused securityby guarantees.all API routes. Provides requireAuth, requireMerchant, rateLimit, getClientIp, jsonError, CSRF protection, and session revocation.

  • src/lib/middleware/ — A modular middleware library with auth-middleware.ts (Bearer token for mobile), error-handler.ts (AppError class), and validation.ts (input sanitization functions).

  • Both layers are used in production. Routes import from @/lib/middleware (auth, rate limiting) and @/lib/middleware/validation (input validation).


    2. RequestActive LifecycleMiddleware (lib/middleware.ts)

    2.1 CORS MiddlewarerequireAuth(request?)

    Library:Source: middleware.ts:42–80

    Authenticates the current request via cookie-based JWT.

    Returns: { user: User, error: null } | {cors /user: @fastify/cors}null, error: NextResponse }

    Configuration:Steps:

    1. CSRF origin check — if Origin header present, must match allowed origins (NEXT_PUBLIC_APP_URL, http://localhost:3000, http://localhost:3001)
    2. Cookie extraction — reads drop_token from request cookies
    3. JWT verification — validates HS256 signature and expiry using jose library
    4. User lookup — loads user from users table by userId from JWT payload
    5. Session revocation check — verifies at least one non-revoked session exists for this user

    Usage:

    // config/cors.config.ts
    export const corsConfig{ user, error } = {await origin: (origin: string, callback: Function) => {
        const allowedOrigins = [
          'https://app.{{domain.com}}',
          'https://admin.{{domain.com}}',
          ...(process.env.NODE_ENV !== 'production'
            ? ['http://localhost:3000', 'http://localhost:3001']
            : []),
        ]requireAuth(request);
    if (!originerror) ||return allowedOrigins.includes(origin)) {
          callback(null, true);
        } else {
          callback(new Error(`Origin ${origin} not allowed by CORS`));
        }
      },
      methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
      allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
      exposedHeaders: ['X-Request-ID', 'X-RateLimit-Limit', 'X-RateLimit-Remaining'],
      credentials: true,
      maxAge: 86400,error;  // 24hReturns preflightNextResponse cachewith };JSON error
    // user is guaranteed non-null here
    

    PerformanceError impact:responses:

    <
      0.1ms
    • 401 perunauthorized request (headermissing injectioncookie, only)

      invalid JWT, expired token, user not found, all sessions revoked

    2.2 Security Headers MiddlewarerequireMerchant(request?)

    Library:Source: helmetmiddleware.ts:101–108

    Extends requireAuth with a merchant role check.

    app.use(helmet(const { user, error } = await requireMerchant(request);
    if (error) return error;  // Content401 Securityif Policynot contentSecurityPolicy:authenticated, {403 directives:if {not defaultSrc:merchant
    ["

    Returns 403 forbidden if user exists but role !== 'self'"]merchant'.

    Applied to: GET /api/merchants/dashboard, scriptSrc:GET ["'self'"]/api/merchants/qr, styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https://cdn.{{domain.com}}"], connectSrc: ["'self'", "https://api.{{domain.com}}"], frameSrc: ["'none'"], objectSrc: ["'none'"], }, },GET //api/merchants/transactions

    HTTP
    Strict

    2.3 TransportrateLimit(ip, Securitylimit, hsts:windowMs?)

    {

    Source: maxAge:middleware.ts:7–31

    31536000,

    Persistent //IP-based 1rate yearlimiter includeSubDomains:using true,the preload:rate_limits true,database },table.

    //OtherheadersreferrerPolicy:{policy:'same-origin'},frameguard:{action:'deny'},//X-Content-Type-Options:nosniffxssFilter:true,
    Parameter Default Description
    ip Client noSniff:IP true,address
    limit Max //requests X-XSS-Protectionper window
    windowMs60,000msWindow size in milliseconds

    Returns: boolean — true if request is allowed, false if rate limited.

    Implementation:

    • Uses runUpsert for atomic counter creation/update
    • Cleans expired entries on each call (legacyremoves browsers)rows hidePoweredBy:where true,expires_at //< Removenow)
    • X-Powered-By
    • Counter })stored in rate_limits table: (key, count, expires_at)

    Rate limit table schema:

    CREATE TABLE rate_limits (
      key TEXT PRIMARY KEY,      -- IP address
      count INTEGER DEFAULT 1,
      expires_at INTEGER         -- Unix timestamp (ms)
    );
    

    Usage:

    const ip = getClientIp(request);
    if (!(await rateLimit(ip, 10))) {  // 10 req/min
      return jsonError("rate_limited", "Too many requests", 429);
    }
    

    Applied limits:

    EndpointLimitWindow
    /api/auth/bankid/initiate10/min60s
    /api/auth/bankid/callback10/min60s
    /api/auth/register (deprecated)10/min60s
    /api/auth/login (deprecated)10/min60s
    /api/transactions/remittance10/min60s
    /api/transactions/qr-payment10/min60s
    /api/rates120/min60s
    /api/rates/[currency]120/min60s

    2.4 getClientIp(request)

    Source: middleware.ts:33–35

    Extracts the client's real IP address from the x-forwarded-for header (first IP in the chain — the originating client). Falls back to '127.0.0.1' if header not present.

    Note: When behind App Runner (AWS managed proxy), x-forwarded-for is set automatically with the real client IP.


    2.5 jsonError(error, message, status, details?)

    Source: middleware.ts:37–39

    Creates a standardized JSON error NextResponse.

    return jsonError("validation_error", "Validation failed", 422, ["Email required"]);
    // Response body: { "error": "validation_error", "message": "Validation failed", "details": ["Email required"] }
    

    2.6 revokeAllSessions(userId)

    Source: middleware.ts:83–85

    Sets revoked=1 on all sessions for a user. Called by POST /api/auth/logout.

    UPDATE sessions SET revoked = 1 WHERE user_id = $1;
    

    2.7 generateCsrfToken() / validateCsrf(request, token)

    Source: middleware.ts:88–99

    CSRF token generation (32 random bytes hex-encoded) and validation via x-csrf-token header.

    Status: Implemented but not actively required on any route. CSRF protection is handled via:

    • BankID OIDC state parameter (login flow)
    • Origin header validation (in requireAuth)

    3. Middleware Library (lib/middleware/)

    3.1 Error Handler (middleware/error-handler.ts)

    AppError class:

    class AppError extends Error {
      constructor(
        public code: string,
        message: string,
        public status: number = 500,
        public details?: unknown
      ) {}
    }
    

    Predefined error constructors:

    ConstructorCodeHTTP Status
    Errors.unauthorized(msg?)UNAUTHORIZED401
    Errors.forbidden(msg?)FORBIDDEN403
    Errors.notFound(resource)NOT_FOUND404
    Errors.badRequest(msg, details?)BAD_REQUEST400
    Errors.conflict(msg)CONFLICT409
    Errors.tooManyRequests(msg?)RATE_LIMIT_EXCEEDED429
    Errors.internal(msg?)INTERNAL_ERROR500

    Error response format:

    {
      "error": {
        "code": "BAD_REQUEST",
        "message": "Amount must be between 100 and 50000 NOK",
        "details": "validation_error"
      }
    }
    

    Production masking: createErrorResponse() masks internal error messages in production — only returns "An unexpected error occurred" for 500 errors.


    3.2 Auth Middleware (middleware/auth-middleware.ts)

    Alternative auth middleware for mobile clients using Bearer token pattern.

    requireAuth(request):

    • Extracts JWT from Authorization: Bearer <token> header
    • Verifies JWT signature + expiry
    • Returns userId from payload

    In-memory rate limiter (for Bearer token routes):

    • DEFAULT_RATE_LIMIT: 100 req/min
    • STRICT_RATE_LIMIT: 10 req/min
    • Auto-cleanup every 5 minutes
    • Rate limit headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset

    getClientIP(request): Checks X-Forwarded-For → X-Real-IP → falls back to 'unknown'.


    3.3 Validation (middleware/validation.ts)

    Input validation functions — no external dependencies, all custom implementations.

    FunctionDescriptionRules
    validatePhone(phone)International phoneStarts with +, 8–15 digits
    validateAmount(amount)Positive monetary amount> 0, max 2 decimal places
    validateIBAN(iban)European IBANCountry code + alphanumeric, mod-97 checksum
    validatePIN(pin)Card PINExactly 4 digits
    validateEmail(email)Email addressBasic [email protected] pattern
    validateCurrency(currency)ISO 4217 codeWhitelist: EUR, USD, GBP, BAM, CHF, PLN, NOK, RSD, TRY, PKR
    validateDateISO(date)ISO 8601 dateParseable by Date.parse()
    validateName(name)Name field1–100 chars, at least one letter, XSS-safe
    validateLanguage(lang)Language codeWhitelist: nb, en, bs, sq
    sanitizeText(text, maxLength?)Text sanitizationStrips HTML tags + control chars, trims, enforces max length (default 500)
    validate(condition, msg)Assert helperThrows AppError (400) if false
    required(value, name)Required field checkThrows AppError (400) if null/undefined

    Security notes:

    • validateName checks for: <script, javascript:, onerror=, onclick= — blocks XSS injection in name fields
    • sanitizeText removes HTML tags via regex, strips control characters
    • validateIBAN implements full mod-97 checksum algorithm
    • validateAmount rejects NaN, Infinity, negative values

    4. Security Headers set:(Next.js Config)

    Applied to all responses via next.config.ts:

    Header Production ValueDevelopment Value Purpose
    Strict-Transport-Securitymax-age=31536000; includeSubDomains; preloadForce HTTPS
    Content-Security-Policy (seedefault-src above)'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; frame-ancestors 'none'Adds 'unsafe-eval' + 'unsafe-inline' for HMR XSS preventionprotection
    X-Frame-OptionsDENY DENY Clickjacking prevention
    X-Content-Type-Options nosniff nosniffMIME sniffing prevention
    Referrer-Policy same-strict-origin-when-cross-origin SameReferrer privacyleakage prevention
    Permissions-Policycamera=(self), microphone=(), geolocation=(self)SameFeature restriction
    Strict-Transport-Securitymax-age=63072000; includeSubDomains; preloadSameForce HTTPS (2-year HSTS)

    Performance impact: < 0.2ms per request


    2.3 Request ID Middleware

    Purpose: Correlate logs across services for distributed tracing.

    // middleware/request-id.middleware.ts
    export function requestIdMiddleware(req: Request, res: Response, next: NextFunction) {
      const requestId = req.headers['x-request-id'] as string
        || `req_${ulid()}`;
    
      req.requestId = requestId;
      res.setHeader('X-Request-ID', requestId);
    
      // Bind to AsyncLocalStorage for log correlation
      requestContext.run({ requestId }, next);
    }
    

    Format: req_{ulid} — e.g., req_01HX7M2K5N3P4Q5R6S7T8V9W0


    2.4 Authentication Middleware

    Strategy: JWT Bearer token validation

    // guards/jwt.guard.ts
    @Injectable()
    export class JwtGuard implements CanActivate {
      async canActivate(context: ExecutionContext): Promise<boolean> {
        const request = context.switchToHttp().getRequest();
        const token = this.extractToken(request);
    
        if (!token) throw new UnauthorizedException('No token provided');
    
        try {
          const payload = await this.jwtService.verifyAsync(token, {
            secret: this.configService.get('JWT_SECRET'),
            algorithms: ['HS256'],
            clockTolerance: 10, // 10 second clock skew tolerance
          });
    
          // Attach to request for downstream use
          request.user = {
            id: payload.sub,
            email: payload.email,
            role: payload.role,
          };
    
          return true;
        } catch (error) {
          if (error instanceof TokenExpiredError) {
            throw new UnauthorizedException('TOKEN_EXPIRED');
          }
          throw new UnauthorizedException('INVALID_TOKEN');
        }
      }
    
      private extractToken(request: Request): string | null {
        const [type, token] = request.headers.authorization?.split(' ') ?? [];
        return type === 'Bearer' ? token : null;
      }
    }
    

    Performance impact: ~2-5ms (crypto operation + optional DB lookup for token revocation)

    Token revocation check: {{Check Redis blocklist on each request | Check on logout only | Use short TTL — no revocation check}}


    2.5 Authorization

    5. Middleware (RBAC/ABAC)

    Model: {{RBAC (Role-Based) | ABAC (Attribute-Based) | Hybrid}}

    // decorators/roles.decorator.ts
    export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);
    export const RequirePermission = (permission: string) =>
      SetMetadata('permission', permission);
    
    // guards/roles.guard.ts
    @Injectable()
    export class RolesGuard implements CanActivate {
      canActivate(context: ExecutionContext): boolean {
        const requiredRoles = this.reflector.get<Role[]>('roles', context.getHandler());
        const requiredPermission = this.reflector.get<string>('permission', context.getHandler());
    
        if (!requiredRoles && !requiredPermission) return true; // Public route
    
        const { user } = context.switchToHttp().getRequest();
    
        if (requiredRoles && !requiredRoles.includes(user.role)) {
          throw new ForbiddenException(`Requires role: ${requiredRoles.join(' or ')}`);
        }
    
        if (requiredPermission && !this.hasPermission(user, requiredPermission)) {
          throw new ForbiddenException(`Requires permission: ${requiredPermission}`);
        }
    
        return true;
      }
    }
    
    // Usage on controller
    @Get('users')
    @Roles(Role.ADMIN)
    @RequirePermission('users:read')
    async listUsers() { ... }
    

    Role hierarchy:

    admin > manager > user > viewer > public
    
    Matrix access ownorgresources ownresources
    RoleRoute CapabilitiesRate LimitrequireAuthrequireMerchantFeature FlagValidation Functions
    GET admin/api/auth/bankid Full10/min NoNoNo
    GET manager/api/auth/bankid/callback Read/write10/min No No Nostate cookie
    GET user/api/auth/me Read/writeNo Yes NoNo
    POST viewer/api/auth/logout Read-onlyNoYesNoNo
    POST /api/auth/refreshNoYesNoNo
    GET /api/transactionsNoYesNoNo
    POST /api/transactions/remittance10/minYesNoNovalidateAmount
    POST /api/transactions/qr-payment10/minYesNoNovalidateAmount
    GET /api/rates120/minNoNoNo
    POST /api/recipientsNoYesNoNovalidateName, country whitelist
    POST /api/merchants/registerNoYesNoNovalidateName, orgNumber
    GET /api/merchants/dashboardNoYesYesNoperiod whitelist
    GET /api/notificationsNoYesNonotifications
    PATCH /api/notificationsNoYesNonotificationsID format, max 100
    PATCH /api/settingsNoYesNoNocurrency/language whitelist
    POST /api/cards/[id]/physicalNoYesNophysicalCardsaddress min 10 chars
    POST /api/cards/[id]/pinNoYesNocardPinvalidatePIN
    GET/PUT /api/cards/[id]/limitsNoYesNospendingLimitslimitType whitelist

    2.6

    6. ValidationError Middleware

    Spike Detection

    Library:Implemented in class-validator + class-transformersrc/lib/alerts.ts ORas zod

    a
    //middleware-adjacent Global validation pipe (NestJS)
    app.useGlobalPipes(new ValidationPipe({
      whitelist: true,              // Strip unknown properties
      forbidNonWhitelisted: true,  // Throw if unknown properties present
      transform: true,              // Auto-transform to DTO types
      transformOptions: {
        enableImplicitConversion: true,
      },
    }));
    

    Sanitization rules:concern:

    • AllEvery stringHTTP inputs:5xx trimresponse whitespacetriggers trackError() (called in jsonError() middleware for 500 errors)
    • HTMLRolling content:1-minute sanitizewindow withof DOMPurifyerror /timestamps sanitize-htmlmaintained (strip dangerous tags)in-memory
    • SQLWhen parameters:count always> use5 parameterizedin queries60 (ORMseconds handles this)sends critical Slack alert to #drop-ops
    • File10-minute uploads:cooldown validateper MIMEalert typetitle byprevents magic bytes (not just extension)spam

    Validation error format:

    {
      "error": {
        "code": "VALIDATION_ERROR",
        "message": "Request validation failed",
        "details": [
          { "field": "email", "message": "must be an email" },
          { "field": "name", "message": "must be longer than 2 characters" }
        ]
      }
    }
    

    Performance impact:Limitation: <Error 1mscounter is in-memory only — resets on application restart. Redis-backed counter planned for typical DTOs


    2.7 Rate Limiting Middleware

    Library: {{@nestjs/throttler | express-rate-limit | rate-limiter-flexible}} Storage: {{Redis}} (shared across all replicas)

    Algorithms:

    AlgorithmLibraryBest For
    Fixed windowexpress-rate-limitSimple, low overhead
    Sliding windowrate-limiter-flexibleAccurate, no burst at window edge
    Token bucketrate-limiter-flexibleBursty traffic patterns

    Selected algorithm: {{Sliding window}}

    Configuration:

    const rateLimiter = new RateLimiterRedis({
      storeClient: redisClient,
      keyPrefix: 'rl',
      points: 1000,         // Number of points
      duration: 60,         // Per 60 seconds
      blockDuration: 60,    // Block for 60s after exceeded
    });
    
    // Per-route overrides
    const loginLimiter = new RateLimiterRedis({
      points: 5,
      duration: 900,        // 15 minutes
      blockDuration: 900,
    });
    

    Key strategy: {{IP address | User ID (if authenticated) | IP + User ID}}

    Performance impact: ~1-2ms (Redis round-trip)


    2.8 Audit Logging Middleware

    // interceptors/audit.interceptor.ts
    @Injectable()
    export class AuditInterceptor implements NestInterceptor {
      intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        const request = context.switchToHttp().getRequest();
        const { method, path, user, requestId } = request;
    
        // Only audit mutating operations
        if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
          this.auditService.log({
            requestId,
            userId: user?.id,
            method,
            path,
            body: this.sanitizeBody(request.body), // Strip PII
            timestamp: new Date().toISOString(),
          });
        }
    
        return next.handle();
      }
    
      private sanitizeBody(body: Record<string, unknown>) {
        const REDACTED_FIELDS = ['password', 'token', 'creditCard', 'ssn'];
        return Object.fromEntries(
          Object.entries(body).map(([key, value]) =>
            REDACTED_FIELDS.includes(key) ? [key, '[REDACTED]'] : [key, value]
          )
        );
      }
    }
    

    What IS logged:

    • User ID, request ID, timestamp, method, path
    • Response status code, duration
    • Mutation summaries (what changed, not full values)

    What is NEVER logged:

    • Passwords, tokens, API keys
    • Payment card data
    • Full PII fields (log field names but not values for sensitive fields)

    Audit log retention: {{1 year}} (compliance requirement: {{GDPR / SOC2 / internal}})


    2.9 Error Handling Middleware

    // filters/global-exception.filter.ts
    @Catch()
    export class GlobalExceptionFilter implements ExceptionFilter {
      catch(exception: unknown, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const request = ctx.getRequest<Request>();
    
        let status = 500;
        let code = 'INTERNAL_ERROR';
        let message = 'An unexpected error occurred';
        let details: unknown[] = [];
    
        if (exception instanceof HttpException) {
          status = exception.getStatus();
          const exceptionResponse = exception.getResponse() as any;
          code = exceptionResponse.code ?? 'HTTP_ERROR';
          message = exceptionResponse.message ?? exception.message;
          details = exceptionResponse.details ?? [];
        }
    
        // Log 5xx errors (not 4xx — those are client errors)
        if (status >= 500) {
          this.logger.error('Unhandled exception', { exception, requestId: request.requestId });
          this.sentryService.captureException(exception);
        }
    
        response.status(status).json({
          error: {
            code,
            message,
            details,
            requestId: request.requestId,
            timestamp: new Date().toISOString(),
          },
        });
      }
    }
    

    3. Custom Middleware Development Guide

    Template for new middleware:

    // middleware/{{name}}.middleware.ts
    import { Injectable, NestMiddleware } from '@nestjs/common';
    import { Request, Response, NextFunction } from 'express';
    
    @Injectable()
    export class {{Name}}Middleware implements NestMiddleware {
      use(req: Request, res: Response, next: NextFunction): void {
        // 1. Extract needed data from request
        // 2. Perform validation/enrichment/logging
        // 3. Set data on request object if needed
        // 4. Call next() or throw HttpException
    
        next();
      }
    }
    

    Requirements for new middleware:

    •  Handles errors without crashing the process
    •  Calls next() exactly once (or throws)
    •  Does not block async operations without async/await
    •  Performance impact documented
    •  Unit tests covering happy path + error path

    4. Middleware Ordering & Dependencies

    Request → [1] → [2] → [3] → [4] → [5] → [6] → [7] → [8] → [9] → Handler
    
    [1] CORS          — No dependencies
    [2] Security      — No dependencies
    [3] Request ID    — Must be before Logger (Logger reads requestId)
    [4] Rate Limiter  — Must be after Request ID (uses requestId for key)
    [5] Logger        — Must be after Request ID
    [6] Auth          — Must be after Logger (Logger should log auth failures)
    [7] Authorization — MUST be after Auth (requires user on request)
    [8] Validation    — MUST be after Auth (DTOs may reference user context)
    [9] Audit Logger  — MUST be after Auth (logs user ID)
    

    NEVER reorder middleware without reviewing this dependency chain.v1.0.


    MiddlewareAvg Latency AddedP99 Latency AddedNotes
    CORS0.05ms0.1msHeader injection only
    Security Headers (Helmet)0.1ms0.2msHeader injection only
    Request ID0.1ms0.2msID generation
    Rate Limiter1.5ms5msRedis round-trip
    Request Logger0.5ms1msAsync log write
    Authentication (JWT)3ms8msCrypto + optional Redis
    Authorization0.5ms1msIn-memory role check
    Validation0.8ms2msSchema parsing
    Audit Logger0.5ms1msAsync DB write
    Total~7ms~18msMiddleware overhead

    Target: middleware overhead < 10ms P50, < 25ms P99.


    6. Testing Strategy for Middleware

    // Example unit test for Auth middleware
    describe('JwtGuard', () => {
      it('should attach user to request on valid token', async () => {
        const token = generateTestToken({ sub: 'usr_123', role: 'user' });
        const mockRequest = { headers: { authorization: `Bearer ${token}` } };
        const result = await guard.canActivate(createMockContext(mockRequest));
        expect(result).toBe(true);
        expect(mockRequest.user).toMatchObject({ id: 'usr_123', role: 'user' });
      });
    
      it('should throw UnauthorizedException on expired token', async () => {
        const expiredToken = generateExpiredToken();
        const mockRequest = { headers: { authorization: `Bearer ${expiredToken}` } };
        await expect(guard.canActivate(createMockContext(mockRequest)))
          .rejects.toThrow('TOKEN_EXPIRED');
      });
    });
    

    Test coverage requirements:


    7. Configuration Options Per Middleware

    MiddlewareEnvironment VariableDefaultDescription
    CORSCORS_ORIGINSlocalhost:3000Comma-separated allowed origins
    Rate LimitRATE_LIMIT_POINTS1000Requests per window
    Rate LimitRATE_LIMIT_DURATION60Window size in seconds
    Rate Limit (auth)AUTH_RATE_LIMIT_POINTS5Login attempts per window
    Audit LogAUDIT_LOG_RETENTION_DAYS365How long to keep audit records
    Request BodyMAX_REQUEST_BODY_SIZE1mbMax request body size

    Approval

    Alem
    Role Name Date Signature
    Author Platform Architect (AI) 2026-02-23
    Backend LeadReviewer
    Security LeadApprover
    Tech LeadBašić