Skip to main content

mockforge_http/
lib.rs

1// Issue #555 extracted `mockforge-intelligence` and `mockforge-proxy`
2// out of this crate; both now exist as independent workspace members.
3// AI-touching handlers (ai_studio, forecasting, fidelity, deceptive_canary,
4// consistency, scenario_studio, xray, pr_generation, semantic_drift,
5// threat_modeling, rag_ai_generator, ai_handler) and proxy logic
6// (proxy_server, reality_proxy) all live in their target crates; the
7// files remaining here under those names are thin `pub use ...::*`
8// re-export shims kept for one minor version so external callers don't
9// break. (Issue #656 picked up `ai_handler` after #555's main run.)
10//
11// Deprecation allowances are scoped to individual handler files
12// (handlers/*.rs, middleware/drift_tracking.rs, management/ai_gen.rs)
13// and to specific build_router functions below, rather than being
14// crate-wide.
15//
16// What remains in `mockforge-http`:
17//   - HTTP-native machinery: routing, builders, middleware, SSE, TLS,
18//     fixtures/scenarios/state-machine/time-travel/contract-diff APIs,
19//     management UI, request logging, etc.
20//   - Governance/security HTTP handlers (access_review, change_management,
21//     compliance_dashboard, consent, privileged_access, …) which front
22//     `mockforge_core::security` services and so belong in the same
23//     crate as the rest of the security-services HTTP surface, not in
24//     mockforge-intelligence.
25//   - `handlers::behavioral_cloning` — wired into both
26//     `mockforge_intelligence::behavioral_cloning` (algorithms) and
27//     `mockforge_recorder::database` (trace I/O). Since
28//     `mockforge-recorder` already depends on `mockforge-intelligence`,
29//     intelligence cannot host the orchestration layer without a cycle,
30//     so this handler stays here intentionally.
31//   - `handlers::{contract_health, drift_budget, failure_designer,
32//     incident_replay, protocol_contracts}` — coupled to
33//     `mockforge_chaos`, the chaos crate would need its own #555-style
34//     bucket before these can follow.
35
36//! # MockForge HTTP
37//!
38//! HTTP/REST API mocking library for MockForge.
39//!
40//! This crate provides HTTP-specific functionality for creating mock REST APIs,
41//! including OpenAPI integration, request validation, AI-powered response generation,
42//! and management endpoints.
43//!
44//! ## Overview
45//!
46//! MockForge HTTP enables you to:
47//!
48//! - **Serve OpenAPI specs**: Automatically generate mock endpoints from OpenAPI/Swagger
49//! - **Validate requests**: Enforce schema validation with configurable modes
50//! - **AI-powered responses**: Generate intelligent responses using LLMs
51//! - **Management API**: Real-time monitoring, configuration, and control
52//! - **Request logging**: Comprehensive HTTP request/response logging
53//! - **Metrics collection**: Track performance and usage statistics
54//! - **Server-Sent Events**: Stream logs and metrics to clients
55//!
56//! ## Quick Start
57//!
58//! ### Basic HTTP Server from OpenAPI
59//!
60//! ```rust,no_run
61//! use axum::Router;
62//! use mockforge_openapi::openapi_routes::ValidationMode;
63//! use mockforge_openapi::openapi_routes::ValidationOptions;
64//! use mockforge_http::build_router;
65//!
66//! #[tokio::main]
67//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
68//!     // Build router from OpenAPI specification
69//!     let router = build_router(
70//!         Some("./api-spec.json".to_string()),
71//!         Some(ValidationOptions {
72//!             request_mode: ValidationMode::Enforce,
73//!             ..ValidationOptions::default()
74//!         }),
75//!         None,
76//!     ).await;
77//!
78//!     // Start the server
79//!     let addr: std::net::SocketAddr = "0.0.0.0:3000".parse()?;
80//!     let listener = tokio::net::TcpListener::bind(addr).await?;
81//!     axum::serve(listener, router).await?;
82//!
83//!     Ok(())
84//! }
85//! ```
86//!
87//! ### With Management API
88//!
89//! Enable real-time monitoring and configuration:
90//!
91//! ```rust,no_run
92//! use mockforge_http::{management_router, ManagementState};
93//!
94//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
95//! let state = ManagementState::new(None, None, 3000);
96//!
97//! // Build management router
98//! let mgmt_router = management_router(state);
99//!
100//! // Mount under your main router
101//! let app = axum::Router::new()
102//!     .nest("/__mockforge", mgmt_router);
103//! # Ok(())
104//! # }
105//! ```
106//!
107//! ### AI-Powered Responses
108//!
109//! Generate intelligent responses based on request context:
110//!
111//! ```rust,no_run
112//! use mockforge_data::intelligent_mock::{IntelligentMockConfig, ResponseMode};
113//! use mockforge_http::{process_response_with_ai, AiResponseConfig};
114//! use serde_json::json;
115//!
116//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
117//! let ai_config = AiResponseConfig {
118//!     intelligent: Some(
119//!         IntelligentMockConfig::new(ResponseMode::Intelligent)
120//!             .with_prompt("Generate realistic user data".to_string()),
121//!     ),
122//!     drift: None,
123//! };
124//!
125//! let response = process_response_with_ai(
126//!     Some(json!({"name": "Alice"})),
127//!     ai_config
128//!         .intelligent
129//!         .clone()
130//!         .map(serde_json::to_value)
131//!         .transpose()?,
132//!     ai_config
133//!         .drift
134//!         .clone()
135//!         .map(serde_json::to_value)
136//!         .transpose()?,
137//! )
138//! .await?;
139//! # Ok(())
140//! # }
141//! ```
142//!
143//! ## Key Features
144//!
145//! ### OpenAPI Integration
146//! - Automatic endpoint generation from specs
147//! - Request/response validation
148//! - Schema-based mock data generation
149//!
150//! ### Management & Monitoring
151//! - [`management`]: REST API for server control and monitoring
152//! - [`management_ws`]: WebSocket API for real-time updates
153//! - [`sse`]: Server-Sent Events for log streaming
154//! - [`request_logging`]: Comprehensive request/response logging
155//! - [`metrics_middleware`]: Performance metrics collection
156//!
157//! ### Advanced Features
158//! - [`ai_handler`]: AI-powered response generation
159//! - [`auth`]: Authentication and authorization
160//! - [`chain_handlers`]: Multi-step request workflows
161//! - [`latency_profiles`]: Configurable latency simulation
162//! - [`replay_listing`]: Fixture management
163//!
164//! ## Middleware
165//!
166//! MockForge HTTP includes several middleware layers:
167//!
168//! - **Request Tracing**: [`http_tracing_middleware`] - Distributed tracing integration
169//! - **Metrics Collection**: [`metrics_middleware`] - Prometheus-compatible metrics
170//! - **Operation Metadata**: [`op_middleware`] - OpenAPI operation tracking
171//!
172//! ## Management API Endpoints
173//!
174//! When using the management router, these endpoints are available:
175//!
176//! - `GET /health` - Health check
177//! - `GET /stats` - Server statistics
178//! - `GET /logs` - Request logs (SSE stream)
179//! - `GET /metrics` - Performance metrics
180//! - `GET /fixtures` - List available fixtures
181//! - `POST /config/*` - Update configuration
182//!
183//! ## Examples
184//!
185//! See the [examples directory](https://github.com/SaaSy-Solutions/mockforge/tree/main/examples)
186//! for complete working examples.
187//!
188//! ## Related Crates
189//!
190//! - [`mockforge-core`](https://docs.rs/mockforge-core): Core mocking functionality
191//! - [`mockforge-data`](https://docs.rs/mockforge-data): Synthetic data generation
192//! - [`mockforge-plugin-core`](https://docs.rs/mockforge-plugin-core): Plugin development
193//!
194//! ## Documentation
195//!
196//! - [MockForge Book](https://docs.mockforge.dev/)
197//! - [HTTP Mocking Guide](https://docs.mockforge.dev/user-guide/http-mocking.html)
198//! - [API Reference](https://docs.rs/mockforge-http)
199
200pub mod ai_handler;
201pub mod auth;
202pub mod chain_handlers;
203/// Cross-protocol consistency engine integration for HTTP
204pub mod consistency;
205/// Contract diff retrieval API
206pub mod contract_diff_api;
207/// Contract diff middleware for automatic request capture
208pub mod contract_diff_middleware;
209/// TCP-listener wrapper that bumps the global `record_accept()` counter,
210/// used by the dashboard sampler to derive connections-per-second.
211pub mod counting_listener;
212pub mod coverage;
213/// Database connection wrapper — moved to `mockforge_intelligence::database`
214/// under #555 (prereq for handler moves). Re-exported here so existing
215/// `mockforge_http::database::Database` callers (4 handler files + the
216/// router init code below) keep resolving. Gated by the `database` feature,
217/// which now plumbs `mockforge-intelligence/database` transitively.
218#[cfg(feature = "database")]
219pub mod database {
220    pub use mockforge_intelligence::database::*;
221}
222/// File generation service for creating mock PDF, CSV, JSON files
223pub mod file_generator;
224/// File serving for generated mock files
225pub mod file_server;
226/// Fixtures management API for hosted-mock deployments
227pub mod fixtures_api;
228/// Kubernetes-native health check endpoints (liveness, readiness, startup probes)
229pub mod health;
230pub mod http_tracing_middleware;
231/// Latency profile configuration for HTTP request simulation
232pub mod latency_profiles;
233/// Management API for server control and monitoring
234pub mod management;
235/// WebSocket-based management API for real-time updates
236pub mod management_ws;
237pub mod metrics_middleware;
238pub mod middleware;
239/// Standalone MockAI HTTP API
240pub mod mockai_api;
241/// Runtime network-profile switching API
242pub mod network_profile_runtime;
243pub mod op_middleware;
244/// Unified protocol server lifecycle implementation
245pub mod protocol_server;
246/// Browser/Mobile Proxy Server
247pub mod proxy_server;
248/// Quick mock generation utilities
249pub mod quick_mock;
250/// RAG-powered AI response generation
251pub mod rag_ai_generator;
252/// Reality-slider-driven mock/proxy switching middleware (#222)
253pub mod reality_proxy;
254/// Replay listing and fixture management
255pub mod replay_listing;
256pub mod request_logging;
257/// Runtime route-scoped chaos rules API
258pub mod route_chaos_runtime;
259/// Runtime named-scenario activation API
260#[cfg(feature = "scenario-engine")]
261pub mod scenarios_runtime;
262/// Specification import API for OpenAPI and AsyncAPI
263pub mod spec_import;
264/// Server-Sent Events for streaming logs and metrics
265pub mod sse;
266/// State machine API for scenario state machines
267pub mod state_machine_api;
268/// Time-travel runtime API mirrored from the admin server onto the main HTTP port
269pub mod time_travel_api;
270/// TLS/HTTPS support
271pub mod tls;
272/// Token response utilities
273pub mod token_response;
274/// UI Builder API for low-code mock endpoint creation
275pub mod ui_builder;
276/// Verification API for request verification
277pub mod verification;
278
279// Access review handlers
280pub mod handlers;
281
282// Re-export AI handler utilities
283pub use ai_handler::{process_response_with_ai, AiResponseConfig, AiResponseHandler};
284// Re-export health check utilities
285pub use health::{HealthManager, ServiceStatus};
286
287// Re-export management API utilities
288pub use management::{
289    management_router, management_router_with_ui_builder, ManagementState, MockConfig,
290    ServerConfig, ServerStats,
291};
292
293// Re-export UI Builder utilities
294pub use ui_builder::{create_ui_builder_router, EndpointConfig, UIBuilderState};
295
296// Re-export management WebSocket utilities
297pub use management_ws::{ws_management_router, MockEvent, WsManagementState};
298
299// Re-export verification API utilities
300pub use verification::verification_router;
301
302// Re-export metrics middleware
303pub use metrics_middleware::collect_http_metrics;
304
305// Re-export tracing middleware
306pub use http_tracing_middleware::http_tracing_middleware;
307
308// Re-export coverage utilities
309pub use coverage::{calculate_coverage, CoverageReport, MethodCoverage, RouteCoverage};
310
311/// Helper function to load persona from config file
312/// Tries to load from common config locations: config.yaml, mockforge.yaml, tools/mockforge/config.yaml
313async fn load_persona_from_config() -> Option<Arc<Persona>> {
314    use mockforge_core::config::load_config;
315
316    // Try common config file locations
317    let config_paths = [
318        "config.yaml",
319        "mockforge.yaml",
320        "tools/mockforge/config.yaml",
321        "../tools/mockforge/config.yaml",
322    ];
323
324    for path in &config_paths {
325        if let Ok(config) = load_config(path).await {
326            // Access intelligent_behavior through mockai config
327            // Note: Config structure is mockai.intelligent_behavior.personas
328            if let Some(persona) = config.mockai.intelligent_behavior.personas.get_active_persona()
329            {
330                tracing::info!(
331                    "Loaded active persona '{}' from config file: {}",
332                    persona.name,
333                    path
334                );
335                return Some(Arc::new(persona.clone()));
336            } else {
337                tracing::debug!(
338                    "No active persona found in config file: {} (personas count: {})",
339                    path,
340                    config.mockai.intelligent_behavior.personas.personas.len()
341                );
342            }
343        } else {
344            tracing::debug!("Could not load config from: {}", path);
345        }
346    }
347
348    tracing::debug!("No persona found in config files, persona-based generation will be disabled");
349    None
350}
351
352use axum::body::Body;
353use axum::extract::State;
354use axum::http::Request;
355use axum::middleware::from_fn_with_state;
356use axum::response::Json;
357use axum::Router;
358use mockforge_chaos::core_failure_injection::{FailureConfig, FailureInjector};
359use mockforge_core::intelligent_behavior::config::Persona;
360use mockforge_foundation::latency::LatencyInjector;
361use mockforge_openapi::openapi_routes::OpenApiRouteRegistry;
362use mockforge_openapi::openapi_routes::ValidationOptions;
363use mockforge_openapi::OpenApiSpec;
364use std::sync::Arc;
365use tower_http::cors::{Any, CorsLayer};
366
367#[cfg(feature = "data-faker")]
368use mockforge_data::provider::register_core_faker_provider;
369use mockforge_foundation::latency::LatencyProfile;
370use std::collections::HashMap;
371use std::ffi::OsStr;
372use std::path::Path;
373use tokio::fs;
374use tokio::sync::RwLock;
375use tracing::*;
376
377/// Route info for storing in state
378#[derive(Clone)]
379pub struct RouteInfo {
380    /// HTTP method (GET, POST, PUT, etc.)
381    pub method: String,
382    /// API path pattern (e.g., "/api/users/{id}")
383    pub path: String,
384    /// OpenAPI operation ID if available
385    pub operation_id: Option<String>,
386    /// Operation summary from OpenAPI spec
387    pub summary: Option<String>,
388    /// Operation description from OpenAPI spec
389    pub description: Option<String>,
390    /// List of parameter names for this route
391    pub parameters: Vec<String>,
392}
393
394/// Shared state for tracking OpenAPI routes
395#[derive(Clone)]
396pub struct HttpServerState {
397    /// List of registered routes from OpenAPI spec
398    pub routes: Vec<RouteInfo>,
399    /// Optional global rate limiter for request throttling
400    pub rate_limiter: Option<Arc<middleware::rate_limit::GlobalRateLimiter>>,
401    /// Production headers to add to all responses (for deceptive deploy)
402    pub production_headers: Option<Arc<HashMap<String, String>>>,
403}
404
405impl Default for HttpServerState {
406    fn default() -> Self {
407        Self::new()
408    }
409}
410
411impl HttpServerState {
412    /// Create a new empty HTTP server state
413    pub fn new() -> Self {
414        Self {
415            routes: Vec::new(),
416            rate_limiter: None,
417            production_headers: None,
418        }
419    }
420
421    /// Create HTTP server state with pre-configured routes
422    pub fn with_routes(routes: Vec<RouteInfo>) -> Self {
423        Self {
424            routes,
425            rate_limiter: None,
426            production_headers: None,
427        }
428    }
429
430    /// Add a rate limiter to the HTTP server state
431    pub fn with_rate_limiter(
432        mut self,
433        rate_limiter: Arc<middleware::rate_limit::GlobalRateLimiter>,
434    ) -> Self {
435        self.rate_limiter = Some(rate_limiter);
436        self
437    }
438
439    /// Add production headers to the HTTP server state
440    pub fn with_production_headers(mut self, headers: Arc<HashMap<String, String>>) -> Self {
441        self.production_headers = Some(headers);
442        self
443    }
444}
445
446/// Handler to return OpenAPI routes information
447async fn get_routes_handler(State(state): State<HttpServerState>) -> Json<serde_json::Value> {
448    let route_info: Vec<serde_json::Value> = state
449        .routes
450        .iter()
451        .map(|route| {
452            serde_json::json!({
453                "method": route.method,
454                "path": route.path,
455                "operation_id": route.operation_id,
456                "summary": route.summary,
457                "description": route.description,
458                "parameters": route.parameters
459            })
460        })
461        .collect();
462
463    Json(serde_json::json!({
464        "routes": route_info,
465        "total": state.routes.len()
466    }))
467}
468
469/// Handler to serve the Scalar API docs page
470async fn get_docs_handler() -> axum::response::Html<&'static str> {
471    axum::response::Html(include_str!("../static/docs.html"))
472}
473
474/// Build the base HTTP router, optionally from an OpenAPI spec.
475pub async fn build_router(
476    spec_path: Option<String>,
477    options: Option<ValidationOptions>,
478    failure_config: Option<FailureConfig>,
479) -> Router {
480    build_router_with_multi_tenant(
481        spec_path,
482        options,
483        failure_config,
484        None,
485        None,
486        None,
487        None,
488        None,
489        None,
490        None,
491    )
492    .await
493}
494
495/// Apply CORS middleware to the router based on configuration
496fn apply_cors_middleware(
497    app: Router,
498    cors_config: Option<mockforge_core::config::HttpCorsConfig>,
499) -> Router {
500    use http::Method;
501    use tower_http::cors::AllowOrigin;
502
503    if let Some(config) = cors_config {
504        if !config.enabled {
505            return app;
506        }
507
508        let mut cors_layer = CorsLayer::new();
509        let is_wildcard_origin;
510
511        // Configure allowed origins
512        if config.allowed_origins.contains(&"*".to_string()) {
513            cors_layer = cors_layer.allow_origin(Any);
514            is_wildcard_origin = true;
515        } else if !config.allowed_origins.is_empty() {
516            // Try to parse each origin, fallback to permissive if parsing fails
517            let origins: Vec<_> = config
518                .allowed_origins
519                .iter()
520                .filter_map(|origin| {
521                    origin.parse::<http::HeaderValue>().ok().map(AllowOrigin::exact)
522                })
523                .collect();
524
525            if origins.is_empty() {
526                // If no valid origins, use permissive for development
527                warn!("No valid CORS origins configured, using permissive CORS");
528                cors_layer = cors_layer.allow_origin(Any);
529                is_wildcard_origin = true;
530            } else {
531                // Use the first origin as exact match (tower-http limitation)
532                // For multiple origins, we'd need a custom implementation
533                if origins.len() == 1 {
534                    cors_layer = cors_layer.allow_origin(origins[0].clone());
535                    is_wildcard_origin = false;
536                } else {
537                    // Multiple origins - use permissive for now
538                    warn!(
539                        "Multiple CORS origins configured, using permissive CORS. \
540                        Consider using '*' for all origins."
541                    );
542                    cors_layer = cors_layer.allow_origin(Any);
543                    is_wildcard_origin = true;
544                }
545            }
546        } else {
547            // No origins specified, use permissive for development
548            cors_layer = cors_layer.allow_origin(Any);
549            is_wildcard_origin = true;
550        }
551
552        // Configure allowed methods
553        if !config.allowed_methods.is_empty() {
554            let methods: Vec<Method> =
555                config.allowed_methods.iter().filter_map(|m| m.parse().ok()).collect();
556            if !methods.is_empty() {
557                cors_layer = cors_layer.allow_methods(methods);
558            }
559        } else {
560            // Default to common HTTP methods
561            cors_layer = cors_layer.allow_methods([
562                Method::GET,
563                Method::POST,
564                Method::PUT,
565                Method::DELETE,
566                Method::PATCH,
567                Method::OPTIONS,
568            ]);
569        }
570
571        // Configure allowed headers
572        if !config.allowed_headers.is_empty() {
573            let headers: Vec<_> = config
574                .allowed_headers
575                .iter()
576                .filter_map(|h| h.parse::<http::HeaderName>().ok())
577                .collect();
578            if !headers.is_empty() {
579                cors_layer = cors_layer.allow_headers(headers);
580            }
581        } else {
582            // Default headers
583            cors_layer =
584                cors_layer.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION]);
585        }
586
587        // Configure credentials - cannot allow credentials with wildcard origin
588        // Determine if credentials should be allowed
589        // Cannot allow credentials with wildcard origin per CORS spec
590        let should_allow_credentials = if is_wildcard_origin {
591            // Wildcard origin - credentials must be false
592            false
593        } else {
594            // Specific origins - use config value (defaults to false)
595            config.allow_credentials
596        };
597
598        cors_layer = cors_layer.allow_credentials(should_allow_credentials);
599
600        info!(
601            "CORS middleware enabled with configured settings (credentials: {})",
602            should_allow_credentials
603        );
604        app.layer(cors_layer)
605    } else {
606        // No CORS config provided - use permissive CORS for development
607        // Note: permissive() allows credentials, but since it uses wildcard origin,
608        // we need to disable credentials to avoid CORS spec violation
609        debug!("No CORS config provided, using permissive CORS for development");
610        // Create a permissive CORS layer but disable credentials to avoid CORS spec violation
611        // (cannot combine credentials with wildcard origin)
612        app.layer(CorsLayer::permissive().allow_credentials(false))
613    }
614}
615
616/// Build the base HTTP router with multi-tenant workspace support
617#[allow(clippy::too_many_arguments)]
618#[allow(deprecated)] // uses MockAI, MultiTenantWorkspaceRegistry, WorkspaceRouter, Workspace (all stay in core)
619pub async fn build_router_with_multi_tenant(
620    spec_path: Option<String>,
621    options: Option<ValidationOptions>,
622    failure_config: Option<FailureConfig>,
623    multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
624    _route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
625    cors_config: Option<mockforge_core::config::HttpCorsConfig>,
626    ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
627    smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
628    mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
629    deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
630) -> Router {
631    use std::time::Instant;
632
633    let startup_start = Instant::now();
634
635    // Set up the basic router
636    let mut app = Router::new();
637
638    // Initialize rate limiter with default configuration
639    // Can be customized via environment variables or config
640    let mut rate_limit_config = middleware::RateLimitConfig {
641        requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
642            .ok()
643            .and_then(|v| v.parse().ok())
644            .unwrap_or(1000),
645        burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
646            .ok()
647            .and_then(|v| v.parse().ok())
648            .unwrap_or(2000),
649        per_ip: true,
650        per_endpoint: false,
651    };
652
653    // Apply deceptive deploy configuration if enabled
654    let mut final_cors_config = cors_config;
655    let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
656    // Auth config from deceptive deploy OAuth (if configured)
657    let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
658
659    if let Some(deploy_config) = &deceptive_deploy_config {
660        if deploy_config.enabled {
661            info!("Deceptive deploy mode enabled - applying production-like configuration");
662
663            // Override CORS config if provided
664            if let Some(prod_cors) = &deploy_config.cors {
665                final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
666                    enabled: true,
667                    allowed_origins: prod_cors.allowed_origins.clone(),
668                    allowed_methods: prod_cors.allowed_methods.clone(),
669                    allowed_headers: prod_cors.allowed_headers.clone(),
670                    allow_credentials: prod_cors.allow_credentials,
671                });
672                info!("Applied production-like CORS configuration");
673            }
674
675            // Override rate limit config if provided
676            if let Some(prod_rate_limit) = &deploy_config.rate_limit {
677                rate_limit_config = middleware::RateLimitConfig {
678                    requests_per_minute: prod_rate_limit.requests_per_minute,
679                    burst: prod_rate_limit.burst,
680                    per_ip: prod_rate_limit.per_ip,
681                    per_endpoint: false,
682                };
683                info!(
684                    "Applied production-like rate limiting: {} req/min, burst: {}",
685                    prod_rate_limit.requests_per_minute, prod_rate_limit.burst
686                );
687            }
688
689            // Set production headers
690            if !deploy_config.headers.is_empty() {
691                let headers_map: HashMap<String, String> = deploy_config.headers.clone();
692                production_headers = Some(std::sync::Arc::new(headers_map));
693                info!("Configured {} production headers", deploy_config.headers.len());
694            }
695
696            // Integrate OAuth config from deceptive deploy
697            if let Some(prod_oauth) = &deploy_config.oauth {
698                let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
699                deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
700                    oauth2: Some(oauth2_config),
701                    ..Default::default()
702                });
703                info!("Applied production-like OAuth configuration for deceptive deploy");
704            }
705        }
706    }
707
708    let rate_limit_disabled = middleware::is_rate_limit_disabled();
709    let rate_limiter =
710        std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
711
712    let mut state = HttpServerState::new();
713    if rate_limit_disabled {
714        info!(
715            "HTTP rate limiting disabled (MOCKFORGE_RATE_LIMIT_ENABLED=false or --no-rate-limit)"
716        );
717    } else {
718        state = state.with_rate_limiter(rate_limiter.clone());
719    }
720
721    // Add production headers to state if configured
722    if let Some(headers) = production_headers.clone() {
723        state = state.with_production_headers(headers);
724    }
725
726    // Clone spec_path for later use
727    let spec_path_for_mgmt = spec_path.clone();
728
729    // If an OpenAPI spec is provided, integrate it
730    if let Some(spec_path) = spec_path {
731        tracing::debug!("Processing OpenAPI spec path: {}", spec_path);
732
733        // Measure OpenAPI spec loading
734        let spec_load_start = Instant::now();
735        match OpenApiSpec::from_file(&spec_path).await {
736            Ok(openapi) => {
737                let spec_load_duration = spec_load_start.elapsed();
738                info!(
739                    "Successfully loaded OpenAPI spec from {} (took {:?})",
740                    spec_path, spec_load_duration
741                );
742
743                // Measure route registry creation
744                tracing::debug!("Creating OpenAPI route registry...");
745                let registry_start = Instant::now();
746
747                // Try to load persona from config if available
748                let persona = load_persona_from_config().await;
749
750                let registry = if let Some(opts) = options {
751                    tracing::debug!("Using custom validation options");
752                    if let Some(ref persona) = persona {
753                        tracing::info!("Using persona '{}' for route generation", persona.name);
754                    }
755                    OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
756                } else {
757                    tracing::debug!("Using environment-based options");
758                    if let Some(ref persona) = persona {
759                        tracing::info!("Using persona '{}' for route generation", persona.name);
760                    }
761                    OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
762                };
763                let registry_duration = registry_start.elapsed();
764                info!(
765                    "Created OpenAPI route registry with {} routes (took {:?})",
766                    registry.routes().len(),
767                    registry_duration
768                );
769
770                // Measure route extraction
771                let extract_start = Instant::now();
772                let route_info: Vec<RouteInfo> = registry
773                    .routes()
774                    .iter()
775                    .map(|route| RouteInfo {
776                        method: route.method.clone(),
777                        path: route.path.clone(),
778                        operation_id: route.operation.operation_id.clone(),
779                        summary: route.operation.summary.clone(),
780                        description: route.operation.description.clone(),
781                        parameters: route.parameters.clone(),
782                    })
783                    .collect();
784                state.routes = route_info;
785                let extract_duration = extract_start.elapsed();
786                debug!("Extracted route information (took {:?})", extract_duration);
787
788                // Measure overrides loading
789                let overrides = if std::env::var("MOCKFORGE_HTTP_OVERRIDES_GLOB").is_ok() {
790                    tracing::debug!("Loading overrides from environment variable");
791                    let overrides_start = Instant::now();
792                    match mockforge_core::Overrides::load_from_globs(&[]).await {
793                        Ok(overrides) => {
794                            let overrides_duration = overrides_start.elapsed();
795                            info!(
796                                "Loaded {} override rules (took {:?})",
797                                overrides.rules().len(),
798                                overrides_duration
799                            );
800                            Some(overrides)
801                        }
802                        Err(e) => {
803                            tracing::warn!("Failed to load overrides: {}", e);
804                            None
805                        }
806                    }
807                } else {
808                    None
809                };
810
811                // Measure router building
812                let router_build_start = Instant::now();
813                let overrides_enabled = overrides.is_some();
814                let response_rewriter: Option<
815                    std::sync::Arc<dyn mockforge_openapi::response_rewriter::ResponseRewriter>,
816                > = Some(std::sync::Arc::new(
817                    mockforge_core::openapi_rewriter::CoreResponseRewriter::new(overrides),
818                ));
819                let openapi_router = if let Some(mockai_instance) = &mockai {
820                    tracing::debug!("Building router with MockAI support");
821                    registry.build_router_with_mockai(Some(mockai_instance.clone()))
822                } else if let Some(ai_generator) = &ai_generator {
823                    tracing::debug!("Building router with AI generator support");
824                    registry.build_router_with_ai(Some(ai_generator.clone()))
825                } else if let Some(failure_config) = &failure_config {
826                    tracing::debug!("Building router with failure injection and overrides");
827                    let failure_injector = FailureInjector::new(Some(failure_config.clone()), true);
828                    registry.build_router_with_injectors_and_overrides(
829                        LatencyInjector::default(),
830                        Some(failure_injector),
831                        response_rewriter,
832                        overrides_enabled,
833                    )
834                } else {
835                    tracing::debug!("Building router with overrides");
836                    registry.build_router_with_injectors_and_overrides(
837                        LatencyInjector::default(),
838                        None,
839                        response_rewriter,
840                        overrides_enabled,
841                    )
842                };
843                let router_build_duration = router_build_start.elapsed();
844                debug!("Built OpenAPI router (took {:?})", router_build_duration);
845
846                // Issue #79 — explicitly stamp the body-size limit on the
847                // openapi sub-router BEFORE merging it. The limit is also
848                // applied inside `OpenApiRouteRegistry::build_router_*`,
849                // but merging can drop nested layers in some axum
850                // configurations; redoing it at the merge site is cheap
851                // insurance against the "200 OK before body completes"
852                // bug Srikanth reported.
853                let body_limit_mb = std::env::var("MOCKFORGE_HTTP_BODY_LIMIT_MB")
854                    .ok()
855                    .and_then(|v| v.parse::<usize>().ok())
856                    .unwrap_or(50);
857                let body_limit_bytes = body_limit_mb.saturating_mul(1024 * 1024);
858                let openapi_router =
859                    openapi_router.layer(axum::extract::DefaultBodyLimit::max(body_limit_bytes));
860                tracing::info!(
861                    body_limit_mb = body_limit_mb,
862                    "Merging OpenAPI router with main router"
863                );
864                app = app.merge(openapi_router);
865                tracing::debug!("Router built successfully");
866            }
867            Err(e) => {
868                warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
869            }
870        }
871    }
872
873    // Add basic health check endpoint
874    app = app.route(
875        "/health",
876        axum::routing::get(|| async {
877            use mockforge_core::server_utils::health::HealthStatus;
878            {
879                // HealthStatus should always serialize, but handle errors gracefully
880                match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
881                    Ok(value) => Json(value),
882                    Err(e) => {
883                        // Log error but return a simple healthy response
884                        tracing::error!("Failed to serialize health status: {}", e);
885                        Json(serde_json::json!({
886                            "status": "healthy",
887                            "service": "mockforge-http",
888                            "uptime_seconds": 0
889                        }))
890                    }
891                }
892            }
893        }),
894    )
895    // Add SSE endpoints
896    .merge(sse::sse_router())
897    // Add file serving endpoints for generated mock files
898    .merge(file_server::file_serving_router());
899
900    // Clone state for routes_router since we'll use it for middleware too
901    let state_for_routes = state.clone();
902
903    // Create a router with state for the routes and coverage endpoints
904    let routes_router = Router::new()
905        .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
906        .route("/__mockforge/coverage", axum::routing::get(coverage::get_coverage_handler))
907        .with_state(state_for_routes);
908
909    // Merge the routes router with the main app
910    app = app.merge(routes_router);
911
912    // Add API docs page (Scalar-powered interactive explorer)
913    app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
914
915    // Add static coverage UI
916    // Determine the path to the coverage.html file
917    let coverage_html_path = std::env::var("MOCKFORGE_COVERAGE_UI_PATH")
918        .unwrap_or_else(|_| "crates/mockforge-http/static/coverage.html".to_string());
919
920    // Check if the file exists before serving it
921    if Path::new(&coverage_html_path).exists() {
922        app = app.nest_service(
923            "/__mockforge/coverage.html",
924            tower_http::services::ServeFile::new(&coverage_html_path),
925        );
926        debug!("Serving coverage UI from: {}", coverage_html_path);
927    } else {
928        debug!(
929            "Coverage UI file not found at: {}. Skipping static file serving.",
930            coverage_html_path
931        );
932    }
933
934    // Add management API endpoints
935    // Load spec for ManagementState so /__mockforge/api/spec can serve it
936    let mgmt_spec = if let Some(ref sp) = spec_path_for_mgmt {
937        match OpenApiSpec::from_file(sp).await {
938            Ok(s) => Some(Arc::new(s)),
939            Err(e) => {
940                debug!("Failed to load OpenAPI spec for management API: {}", e);
941                None
942            }
943        }
944    } else {
945        None
946    };
947    let mgmt_port = std::env::var("PORT")
948        .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
949        .ok()
950        .and_then(|p| p.parse().ok())
951        .unwrap_or(3000);
952    let management_state = ManagementState::new(mgmt_spec, spec_path_for_mgmt, mgmt_port);
953
954    // Create WebSocket state and connect it to management state
955    use std::sync::Arc;
956    let ws_state = WsManagementState::new();
957    let ws_broadcast = Arc::new(ws_state.tx.clone());
958    let management_state = management_state.with_ws_broadcast(ws_broadcast);
959
960    // Note: ProxyConfig not available in this build function path
961    // Migration endpoints will work once ProxyConfig is passed to build_router_with_chains_and_multi_tenant
962
963    #[cfg(feature = "smtp")]
964    let management_state = {
965        if let Some(smtp_reg) = smtp_registry {
966            match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
967                Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
968                Err(e) => {
969                    error!(
970                        "Invalid SMTP registry type passed to HTTP management state: {:?}",
971                        e.type_id()
972                    );
973                    management_state
974                }
975            }
976        } else {
977            management_state
978        }
979    };
980    #[cfg(not(feature = "smtp"))]
981    let management_state = management_state;
982    #[cfg(not(feature = "smtp"))]
983    let _ = smtp_registry;
984    let management_state_for_fallback = management_state.clone();
985    app = app.nest("/__mockforge/api", management_router(management_state));
986    // Serve any request that the rest of the router doesn't handle as a dynamic
987    // mock lookup. Lets the Node/Rust SDK register stubs via the management
988    // API and have requests actually reach them. Falls through to 404 when
989    // no mock matches.
990    app = app.fallback_service(
991        axum::routing::any(management::dynamic_mock_fallback)
992            .with_state(management_state_for_fallback),
993    );
994
995    // Add verification API endpoint
996    app = app.merge(verification_router());
997
998    // Add OIDC well-known endpoints
999    use crate::auth::oidc::oidc_router;
1000    app = app.merge(oidc_router());
1001
1002    // Add access review API if enabled
1003    {
1004        use mockforge_core::security::get_global_access_review_service;
1005        if let Some(service) = get_global_access_review_service().await {
1006            use crate::handlers::access_review::{access_review_router, AccessReviewState};
1007            let review_state = AccessReviewState { service };
1008            app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
1009            debug!("Access review API mounted at /api/v1/security/access-reviews");
1010        }
1011    }
1012
1013    // Add privileged access API if enabled
1014    {
1015        use mockforge_core::security::get_global_privileged_access_manager;
1016        if let Some(manager) = get_global_privileged_access_manager().await {
1017            use crate::handlers::privileged_access::{
1018                privileged_access_router, PrivilegedAccessState,
1019            };
1020            let privileged_state = PrivilegedAccessState { manager };
1021            app = app.nest(
1022                "/api/v1/security/privileged-access",
1023                privileged_access_router(privileged_state),
1024            );
1025            debug!("Privileged access API mounted at /api/v1/security/privileged-access");
1026        }
1027    }
1028
1029    // Add change management API if enabled
1030    {
1031        use mockforge_core::security::get_global_change_management_engine;
1032        if let Some(engine) = get_global_change_management_engine().await {
1033            use crate::handlers::change_management::{
1034                change_management_router, ChangeManagementState,
1035            };
1036            let change_state = ChangeManagementState { engine };
1037            app = app.nest("/api/v1/change-management", change_management_router(change_state));
1038            debug!("Change management API mounted at /api/v1/change-management");
1039        }
1040    }
1041
1042    // Add risk assessment API if enabled
1043    {
1044        use mockforge_core::security::get_global_risk_assessment_engine;
1045        if let Some(engine) = get_global_risk_assessment_engine().await {
1046            use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
1047            let risk_state = RiskAssessmentState { engine };
1048            app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
1049            debug!("Risk assessment API mounted at /api/v1/security/risks");
1050        }
1051    }
1052
1053    // Add token lifecycle API
1054    {
1055        use crate::auth::token_lifecycle::TokenLifecycleManager;
1056        use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
1057        let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1058        let lifecycle_state = TokenLifecycleState {
1059            manager: lifecycle_manager,
1060        };
1061        app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
1062        debug!("Token lifecycle API mounted at /api/v1/auth");
1063    }
1064
1065    // Add OAuth2 server endpoints
1066    {
1067        use crate::auth::oidc::load_oidc_state;
1068        use crate::auth::token_lifecycle::TokenLifecycleManager;
1069        use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
1070        // Load OIDC state from configuration (environment variables or config file)
1071        let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
1072        let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1073        let oauth2_state = OAuth2ServerState {
1074            oidc_state,
1075            lifecycle_manager,
1076            auth_codes: Arc::new(RwLock::new(HashMap::new())),
1077            refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
1078        };
1079        app = app.merge(oauth2_server_router(oauth2_state));
1080        debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
1081    }
1082
1083    // Add consent screen endpoints
1084    {
1085        use crate::auth::oidc::load_oidc_state;
1086        use crate::auth::risk_engine::RiskEngine;
1087        use crate::auth::token_lifecycle::TokenLifecycleManager;
1088        use crate::handlers::consent::{consent_router, ConsentState};
1089        use crate::handlers::oauth2_server::OAuth2ServerState;
1090        // Load OIDC state from configuration (environment variables or config file)
1091        let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
1092        let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
1093        let oauth2_state = OAuth2ServerState {
1094            oidc_state: oidc_state.clone(),
1095            lifecycle_manager: lifecycle_manager.clone(),
1096            auth_codes: Arc::new(RwLock::new(HashMap::new())),
1097            refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
1098        };
1099        let risk_engine = Arc::new(RiskEngine::default());
1100        let consent_state = ConsentState {
1101            oauth2_state,
1102            risk_engine,
1103        };
1104        app = app.merge(consent_router(consent_state));
1105        debug!("Consent screen endpoints mounted at /consent");
1106    }
1107
1108    // Add risk simulation API
1109    {
1110        use crate::auth::risk_engine::RiskEngine;
1111        use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
1112        let risk_engine = Arc::new(RiskEngine::default());
1113        let risk_state = RiskSimulationState { risk_engine };
1114        app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
1115        debug!("Risk simulation API mounted at /api/v1/auth/risk");
1116    }
1117
1118    // Add management WebSocket endpoint
1119    app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
1120
1121    // Add request logging middleware to capture all requests
1122    app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
1123
1124    // Add security middleware for security event tracking (after logging, before contract diff)
1125    app = app.layer(axum::middleware::from_fn(middleware::security_middleware));
1126
1127    // Add contract diff middleware for automatic request capture
1128    // This captures requests for contract diff analysis (after logging)
1129    app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
1130
1131    // Add rate limiting middleware (before logging to rate limit early)
1132    app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
1133
1134    // Add production headers middleware if configured
1135    if state.production_headers.is_some() {
1136        app =
1137            app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
1138    }
1139
1140    // Optionally advertise `Connection: keep-alive` + `Keep-Alive: timeout=N,
1141    // max=M` on every response. Useful when MockForge sits behind a reverse
1142    // proxy (F5/Avi/HAProxy/nginx) that pools upstream sockets based on those
1143    // headers — see issue #79 (proxy reset after FIN on HTTP/1.0 upstream).
1144    // Off by default; opt in via `MOCKFORGE_HTTP_KEEPALIVE_HINT=1`.
1145    if middleware::is_keepalive_hint_enabled() {
1146        info!(
1147            "MOCKFORGE_HTTP_KEEPALIVE_HINT enabled — emitting Connection: keep-alive + Keep-Alive headers on all responses (Issue #79 workaround)"
1148        );
1149        app = app.layer(axum::middleware::from_fn(middleware::keepalive_hint_middleware));
1150    }
1151
1152    // Issue #79 (round 5): per-request log line with HTTP version + Connection
1153    // header MockForge actually sees, so users debugging proxy ↔ MockForge
1154    // negotiation can confirm whether their proxy is speaking HTTP/1.1 with
1155    // keep-alive or HTTP/1.0 (which forces hyper to FIN after each response).
1156    if middleware::is_conn_log_enabled() {
1157        info!(
1158            "MOCKFORGE_HTTP_LOG_CONN enabled — logging HTTP version + Connection headers per request (Issue #79 diagnostic)"
1159        );
1160        app = app.layer(axum::middleware::from_fn(middleware::conn_diag_middleware));
1161    }
1162
1163    // Add authentication middleware if OAuth is configured via deceptive deploy
1164    if let Some(auth_config) = deceptive_deploy_auth_config {
1165        use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1166        use std::collections::HashMap;
1167        use std::sync::Arc;
1168        use tokio::sync::RwLock;
1169
1170        // Create OAuth2 client if configured
1171        let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
1172            match create_oauth2_client(oauth2_config) {
1173                Ok(client) => Some(client),
1174                Err(e) => {
1175                    warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
1176                    None
1177                }
1178            }
1179        } else {
1180            None
1181        };
1182
1183        // Create auth state
1184        let auth_state = AuthState {
1185            config: auth_config,
1186            spec: None, // OpenAPI spec not available in this context
1187            oauth2_client,
1188            introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1189        };
1190
1191        // Apply auth middleware
1192        app = app.layer(from_fn_with_state(auth_state, auth_middleware));
1193        info!("Applied OAuth authentication middleware from deceptive deploy configuration");
1194    }
1195
1196    // Add CORS middleware (use final_cors_config which may be overridden by deceptive deploy)
1197    app = apply_cors_middleware(app, final_cors_config);
1198
1199    // Add workspace routing middleware if multi-tenant is enabled
1200    if let Some(mt_config) = multi_tenant_config {
1201        if mt_config.enabled {
1202            use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
1203            use std::sync::Arc;
1204
1205            info!(
1206                "Multi-tenant mode enabled with {} routing strategy",
1207                match mt_config.routing_strategy {
1208                    mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
1209                    mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
1210                    mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
1211                }
1212            );
1213
1214            // Create the multi-tenant workspace registry
1215            let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
1216
1217            // Register the default workspace before wrapping in Arc
1218            let default_workspace =
1219                mockforge_core::Workspace::new(mt_config.default_workspace.clone());
1220            if let Err(e) =
1221                registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
1222            {
1223                warn!("Failed to register default workspace: {}", e);
1224            } else {
1225                info!("Registered default workspace: '{}'", mt_config.default_workspace);
1226            }
1227
1228            // Auto-discover and register workspaces if configured
1229            if mt_config.auto_discover {
1230                if let Some(config_dir) = &mt_config.config_directory {
1231                    let config_path = Path::new(config_dir);
1232                    if config_path.exists() && config_path.is_dir() {
1233                        match fs::read_dir(config_path).await {
1234                            Ok(mut entries) => {
1235                                while let Ok(Some(entry)) = entries.next_entry().await {
1236                                    let path = entry.path();
1237                                    if path.extension() == Some(OsStr::new("yaml")) {
1238                                        match fs::read_to_string(&path).await {
1239                                            Ok(content) => {
1240                                                match serde_yaml::from_str::<
1241                                                    mockforge_core::Workspace,
1242                                                >(
1243                                                    &content
1244                                                ) {
1245                                                    Ok(workspace) => {
1246                                                        if let Err(e) = registry.register_workspace(
1247                                                            workspace.id.clone(),
1248                                                            workspace,
1249                                                        ) {
1250                                                            warn!("Failed to register auto-discovered workspace from {:?}: {}", path, e);
1251                                                        } else {
1252                                                            info!("Auto-registered workspace from {:?}", path);
1253                                                        }
1254                                                    }
1255                                                    Err(e) => {
1256                                                        warn!("Failed to parse workspace from {:?}: {}", path, e);
1257                                                    }
1258                                                }
1259                                            }
1260                                            Err(e) => {
1261                                                warn!(
1262                                                    "Failed to read workspace file {:?}: {}",
1263                                                    path, e
1264                                                );
1265                                            }
1266                                        }
1267                                    }
1268                                }
1269                            }
1270                            Err(e) => {
1271                                warn!("Failed to read config directory {:?}: {}", config_path, e);
1272                            }
1273                        }
1274                    } else {
1275                        warn!(
1276                            "Config directory {:?} does not exist or is not a directory",
1277                            config_path
1278                        );
1279                    }
1280                }
1281            }
1282
1283            // Wrap registry in Arc for shared access
1284            let registry = Arc::new(registry);
1285
1286            // Create workspace router and wrap the app with workspace middleware
1287            let _workspace_router = WorkspaceRouter::new(registry);
1288
1289            // Note: The actual middleware integration would need to be implemented
1290            // in the WorkspaceRouter to work with Axum's middleware system
1291            info!("Workspace routing middleware initialized for HTTP server");
1292        }
1293    }
1294
1295    let total_startup_duration = startup_start.elapsed();
1296    info!("HTTP router startup completed (total time: {:?})", total_startup_duration);
1297
1298    app
1299}
1300
1301/// Build the base HTTP router with authentication and latency support
1302pub async fn build_router_with_auth_and_latency(
1303    spec_path: Option<String>,
1304    _options: Option<()>,
1305    auth_config: Option<mockforge_core::config::AuthConfig>,
1306    latency_injector: Option<LatencyInjector>,
1307) -> Router {
1308    // Build auth router first, then layer latency on top
1309    let mut app = build_router_with_auth(spec_path.clone(), None, auth_config).await;
1310
1311    // Apply latency injection middleware if configured
1312    if let Some(injector) = latency_injector {
1313        let injector = Arc::new(injector);
1314        app = app.layer(axum::middleware::from_fn(move |req, next: axum::middleware::Next| {
1315            let injector = injector.clone();
1316            async move {
1317                let _ = injector.inject_latency(&[]).await;
1318                next.run(req).await
1319            }
1320        }));
1321    }
1322
1323    app
1324}
1325
1326/// Build the base HTTP router with latency injection support
1327pub async fn build_router_with_latency(
1328    spec_path: Option<String>,
1329    options: Option<ValidationOptions>,
1330    latency_injector: Option<LatencyInjector>,
1331) -> Router {
1332    if let Some(spec) = &spec_path {
1333        match OpenApiSpec::from_file(spec).await {
1334            Ok(openapi) => {
1335                let registry = if let Some(opts) = options {
1336                    OpenApiRouteRegistry::new_with_options(openapi, opts)
1337                } else {
1338                    OpenApiRouteRegistry::new_with_env(openapi)
1339                };
1340
1341                if let Some(injector) = latency_injector {
1342                    return registry.build_router_with_latency(injector);
1343                } else {
1344                    return registry.build_router();
1345                }
1346            }
1347            Err(e) => {
1348                warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec, e);
1349            }
1350        }
1351    }
1352
1353    build_router(None, None, None).await
1354}
1355
1356/// Build the base HTTP router with authentication support
1357pub async fn build_router_with_auth(
1358    spec_path: Option<String>,
1359    options: Option<ValidationOptions>,
1360    auth_config: Option<mockforge_core::config::AuthConfig>,
1361) -> Router {
1362    use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
1363    use std::sync::Arc;
1364
1365    // If richer faker is available, register provider once (idempotent)
1366    #[cfg(feature = "data-faker")]
1367    {
1368        register_core_faker_provider();
1369    }
1370
1371    // Set up authentication state
1372    let spec = if let Some(spec_path) = &spec_path {
1373        match OpenApiSpec::from_file(&spec_path).await {
1374            Ok(spec) => Some(Arc::new(spec)),
1375            Err(e) => {
1376                warn!("Failed to load OpenAPI spec for auth: {}", e);
1377                None
1378            }
1379        }
1380    } else {
1381        None
1382    };
1383
1384    // Create OAuth2 client if configured
1385    let oauth2_client = if let Some(auth_config) = &auth_config {
1386        if let Some(oauth2_config) = &auth_config.oauth2 {
1387            match create_oauth2_client(oauth2_config) {
1388                Ok(client) => Some(client),
1389                Err(e) => {
1390                    warn!("Failed to create OAuth2 client: {}", e);
1391                    None
1392                }
1393            }
1394        } else {
1395            None
1396        }
1397    } else {
1398        None
1399    };
1400
1401    let auth_state = AuthState {
1402        config: auth_config.unwrap_or_default(),
1403        spec,
1404        oauth2_client,
1405        introspection_cache: Arc::new(RwLock::new(HashMap::new())),
1406    };
1407
1408    // Set up the basic router with auth state
1409    let mut app = Router::new().with_state(auth_state.clone());
1410
1411    // If an OpenAPI spec is provided, integrate it
1412    if let Some(spec_path) = spec_path {
1413        match OpenApiSpec::from_file(&spec_path).await {
1414            Ok(openapi) => {
1415                info!("Loaded OpenAPI spec from {}", spec_path);
1416                let registry = if let Some(opts) = options {
1417                    OpenApiRouteRegistry::new_with_options(openapi, opts)
1418                } else {
1419                    OpenApiRouteRegistry::new_with_env(openapi)
1420                };
1421
1422                app = registry.build_router();
1423            }
1424            Err(e) => {
1425                warn!("Failed to load OpenAPI spec from {}: {}. Starting without OpenAPI integration.", spec_path, e);
1426            }
1427        }
1428    }
1429
1430    // Add basic health check endpoint
1431    app = app.route(
1432        "/health",
1433        axum::routing::get(|| async {
1434            use mockforge_core::server_utils::health::HealthStatus;
1435            {
1436                // HealthStatus should always serialize, but handle errors gracefully
1437                match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
1438                    Ok(value) => Json(value),
1439                    Err(e) => {
1440                        // Log error but return a simple healthy response
1441                        tracing::error!("Failed to serialize health status: {}", e);
1442                        Json(serde_json::json!({
1443                            "status": "healthy",
1444                            "service": "mockforge-http",
1445                            "uptime_seconds": 0
1446                        }))
1447                    }
1448                }
1449            }
1450        }),
1451    )
1452    // Add SSE endpoints
1453    .merge(sse::sse_router())
1454    // Add file serving endpoints for generated mock files
1455    .merge(file_server::file_serving_router())
1456    // Add authentication middleware (before logging)
1457    .layer(from_fn_with_state(auth_state.clone(), auth_middleware))
1458    // Add request logging middleware
1459    .layer(axum::middleware::from_fn(request_logging::log_http_requests));
1460
1461    app
1462}
1463
1464/// Serve a provided router on the given port.
1465pub async fn serve_router(
1466    port: u16,
1467    app: Router,
1468) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1469    serve_router_with_tls(port, app, None).await
1470}
1471
1472/// Serve a provided router on the given port with optional TLS support.
1473pub async fn serve_router_with_tls(
1474    port: u16,
1475    app: Router,
1476    tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1477) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1478    serve_router_with_tls_notify(port, app, tls_config, None).await
1479}
1480
1481/// Serve a provided router on the given port with optional TLS support, notifying
1482/// the caller via `bound_port_tx` with the actual OS-assigned port once the
1483/// listener has bound. This matters when the requested `port` is `0` (ephemeral),
1484/// because callers such as the `@mockforge-dev/sdk` need to learn the real port
1485/// to connect to it. On the TLS path, `bound_port_tx` is currently ignored and
1486/// the requested `port` is reported verbatim — callers that need dynamic TLS
1487/// ports should bind on a known port.
1488pub async fn serve_router_with_tls_notify(
1489    port: u16,
1490    app: Router,
1491    tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1492    bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1493) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1494    serve_router_with_tls_notify_chaos(port, app, tls_config, bound_port_tx, None).await
1495}
1496
1497/// Serve a router with optional TLS *and* an optional shared chaos config.
1498///
1499/// When `chaos_config` is `Some`, the TCP listener is wrapped with
1500/// [`mockforge_chaos::ChaosTcpListener`], which can inject TCP-level
1501/// connection faults (RST or unclean FIN) before axum/hyper see the socket.
1502/// This is the only way to surface real transport-level failures rather than
1503/// HTTP 503 responses on healthy connections. TLS path does not yet support
1504/// chaos listener wrapping (axum-server uses its own accept loop).
1505pub async fn serve_router_with_tls_notify_chaos(
1506    port: u16,
1507    app: Router,
1508    tls_config: Option<mockforge_core::config::HttpTlsConfig>,
1509    bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
1510    chaos_config: Option<Arc<RwLock<mockforge_chaos::ChaosConfig>>>,
1511) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1512    use std::net::SocketAddr;
1513
1514    let addr = mockforge_core::wildcard_socket_addr(port);
1515
1516    if let Some(ref tls) = tls_config {
1517        if tls.enabled {
1518            info!("HTTPS listening on {}", addr);
1519            if let Some(tx) = bound_port_tx {
1520                let _ = tx.send(port);
1521            }
1522            return serve_with_tls(addr, app, tls).await;
1523        }
1524    }
1525
1526    let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
1527        format!(
1528            "Failed to bind HTTP server to port {}: {}\n\
1529             Hint: The port may already be in use. Try using a different port with --http-port or check if another process is using this port with: lsof -i :{} or netstat -tulpn | grep {}",
1530            port, e, port, port
1531        )
1532    })?;
1533
1534    let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(port);
1535    info!("HTTP listening on {}", listener.local_addr().unwrap_or(addr));
1536    if let Some(tx) = bound_port_tx {
1537        let _ = tx.send(actual_port);
1538    }
1539
1540    // Wrap the Router with OData URI rewrite.
1541    // Router::layer() only applies to matched routes, so we must wrap at the service level
1542    // to rewrite URIs BEFORE route matching occurs.
1543    let odata_app = tower::ServiceBuilder::new()
1544        .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1545        .service(app);
1546    if let Some(cfg) = chaos_config {
1547        info!("HTTP listener wrapped with chaos TCP listener (RST/FIN injection enabled)");
1548        let chaos_listener = mockforge_chaos::ChaosTcpListener::new(listener, cfg);
1549        // Layer that copies ConnectInfo<ChaosClientAddr> → ConnectInfo<SocketAddr>
1550        // so existing handlers using `ConnectInfo<SocketAddr>` keep working.
1551        let app_with_addr_compat = tower::ServiceBuilder::new()
1552            .layer(axum::middleware::from_fn(copy_chaos_addr_to_socketaddr))
1553            .service(odata_app);
1554        let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1555            mockforge_chaos::ChaosClientAddr,
1556        >(app_with_addr_compat);
1557        // Bump the accept counter on each connection that gets through chaos.
1558        let counted = counting_listener::CountingMakeService::new(make_svc);
1559        axum::serve(chaos_listener, counted).await?;
1560    } else {
1561        let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1562            SocketAddr,
1563        >(odata_app);
1564        // Bump the accept counter once per accepted connection so the
1565        // dashboard sampler can derive CPS.
1566        let counted = counting_listener::CountingMakeService::new(make_svc);
1567        axum::serve(listener, counted).await?;
1568    }
1569    Ok(())
1570}
1571
1572/// Mirror `ConnectInfo<ChaosClientAddr>` (set by the chaos listener) onto
1573/// `ConnectInfo<SocketAddr>` so handlers extracting `ConnectInfo<SocketAddr>`
1574/// keep working when chaos TCP wrapping is enabled.
1575async fn copy_chaos_addr_to_socketaddr(
1576    mut req: Request<Body>,
1577    next: axum::middleware::Next,
1578) -> axum::response::Response {
1579    use axum::extract::ConnectInfo;
1580    if let Some(ConnectInfo(chaos_addr)) =
1581        req.extensions().get::<ConnectInfo<mockforge_chaos::ChaosClientAddr>>().copied()
1582    {
1583        let sock: std::net::SocketAddr = *chaos_addr;
1584        req.extensions_mut().insert(ConnectInfo(sock));
1585    }
1586    next.run(req).await
1587}
1588
1589/// Serve router with TLS/HTTPS support using axum-server
1590///
1591/// This function implements native TLS serving using axum-server and tokio-rustls.
1592/// It supports standard TLS and mutual TLS (mTLS) based on the configuration.
1593async fn serve_with_tls(
1594    addr: std::net::SocketAddr,
1595    app: Router,
1596    tls_config: &mockforge_core::config::HttpTlsConfig,
1597) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1598    use axum_server::tls_rustls::RustlsConfig;
1599    use std::net::SocketAddr;
1600
1601    // Initialize the rustls crypto provider (must be called before TLS operations)
1602    tls::init_crypto_provider();
1603
1604    info!("Loading TLS configuration for HTTPS server");
1605
1606    // Load TLS server configuration (supports mTLS)
1607    let server_config = tls::load_tls_server_config(tls_config)?;
1608
1609    // Create RustlsConfig from the ServerConfig
1610    // Note: axum-server's RustlsConfig can be created from a ServerConfig
1611    let rustls_config = RustlsConfig::from_config(server_config);
1612
1613    info!("Starting HTTPS server on {}", addr);
1614
1615    // Wrap the Router with OData URI rewrite (same as the non-TLS path).
1616    // Router::layer() only applies to matched routes, so we must wrap at the service level
1617    // to rewrite URIs BEFORE route matching occurs.
1618    let odata_app = tower::ServiceBuilder::new()
1619        .layer(mockforge_core::odata_rewrite::ODataRewriteLayer)
1620        .service(app);
1621    let make_svc = axum::ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<
1622        SocketAddr,
1623    >(odata_app);
1624    // Bump the accept counter once per successful TLS handshake / connection
1625    // so the dashboard sampler can derive CPS for HTTPS-only setups.
1626    let counted = counting_listener::CountingMakeService::new(make_svc);
1627
1628    // Serve with TLS using axum-server
1629    axum_server::bind_rustls(addr, rustls_config)
1630        .serve(counted)
1631        .await
1632        .map_err(|e| format!("HTTPS server error: {}", e).into())
1633}
1634
1635/// Backwards-compatible start that builds + serves the base router.
1636pub async fn start(
1637    port: u16,
1638    spec_path: Option<String>,
1639    options: Option<ValidationOptions>,
1640) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1641    start_with_latency(port, spec_path, options, None).await
1642}
1643
1644/// Start HTTP server with authentication and latency support
1645pub async fn start_with_auth_and_latency(
1646    port: u16,
1647    spec_path: Option<String>,
1648    options: Option<ValidationOptions>,
1649    auth_config: Option<mockforge_core::config::AuthConfig>,
1650    latency_profile: Option<LatencyProfile>,
1651) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1652    start_with_auth_and_injectors(port, spec_path, options, auth_config, latency_profile, None)
1653        .await
1654}
1655
1656/// Start HTTP server with authentication and injectors support
1657pub async fn start_with_auth_and_injectors(
1658    port: u16,
1659    spec_path: Option<String>,
1660    options: Option<ValidationOptions>,
1661    auth_config: Option<mockforge_core::config::AuthConfig>,
1662    _latency_profile: Option<LatencyProfile>,
1663    _failure_injector: Option<FailureInjector>,
1664) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1665    // For now, ignore latency and failure injectors and just use auth
1666    let app = build_router_with_auth(spec_path, options, auth_config).await;
1667    serve_router(port, app).await
1668}
1669
1670/// Start HTTP server with latency injection support
1671pub async fn start_with_latency(
1672    port: u16,
1673    spec_path: Option<String>,
1674    options: Option<ValidationOptions>,
1675    latency_profile: Option<LatencyProfile>,
1676) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1677    let latency_injector =
1678        latency_profile.map(|profile| LatencyInjector::new(profile, Default::default()));
1679
1680    let app = build_router_with_latency(spec_path, options, latency_injector).await;
1681    serve_router(port, app).await
1682}
1683
1684/// Build the base HTTP router with chaining support
1685pub async fn build_router_with_chains(
1686    spec_path: Option<String>,
1687    options: Option<ValidationOptions>,
1688    circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1689) -> Router {
1690    build_router_with_chains_and_multi_tenant(
1691        spec_path,
1692        options,
1693        circling_config,
1694        None,
1695        None,
1696        None,
1697        None,
1698        None,
1699        None,
1700        None,
1701        false,
1702        None, // health_manager
1703        None, // mockai
1704        None, // deceptive_deploy_config
1705        None, // proxy_config
1706    )
1707    .await
1708}
1709
1710// Template expansion is now handled by mockforge_core::template_expansion
1711// which is explicitly isolated from the templating module to avoid Send issues
1712
1713/// Helper function to apply route chaos injection (fault injection and latency)
1714/// This is extracted to avoid capturing RouteChaosInjector in the closure, which causes Send issues
1715/// Uses mockforge-route-chaos crate which is isolated from mockforge-core to avoid Send issues
1716/// Uses the RouteChaosInjectorTrait from mockforge-core to avoid circular dependency
1717async fn apply_route_chaos(
1718    injector: Option<&dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1719    method: &http::Method,
1720    uri: &http::Uri,
1721) -> Option<axum::response::Response> {
1722    use axum::http::StatusCode;
1723    use axum::response::IntoResponse;
1724
1725    if let Some(injector) = injector {
1726        // Check for fault injection first
1727        if let Some(fault_response) = injector.get_fault_response(method, uri) {
1728            // Return fault response
1729            let mut response = Json(serde_json::json!({
1730                "error": fault_response.error_message,
1731                "fault_type": fault_response.fault_type,
1732            }))
1733            .into_response();
1734            *response.status_mut() = StatusCode::from_u16(fault_response.status_code)
1735                .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
1736            return Some(response);
1737        }
1738
1739        // Inject latency if configured (this is async and may delay the request)
1740        if let Err(e) = injector.inject_latency(method, uri).await {
1741            tracing::warn!("Failed to inject latency: {}", e);
1742        }
1743    }
1744
1745    None // No fault response, processing should continue
1746}
1747
1748/// Build the base HTTP router with chaining and multi-tenant support
1749#[allow(clippy::too_many_arguments)]
1750#[allow(deprecated)] // uses core engines (DriftBudgetEngine, ThreatAnalyzer, Forecaster, ProtocolContractRegistry, MockAI, MultiTenantWorkspaceRegistry, etc.) that stay in core
1751pub async fn build_router_with_chains_and_multi_tenant(
1752    spec_path: Option<String>,
1753    options: Option<ValidationOptions>,
1754    _circling_config: Option<mockforge_core::request_chaining::ChainConfig>,
1755    multi_tenant_config: Option<mockforge_foundation::multi_tenant_types::MultiTenantConfig>,
1756    route_configs: Option<Vec<mockforge_core::config::RouteConfig>>,
1757    cors_config: Option<mockforge_core::config::HttpCorsConfig>,
1758    _ai_generator: Option<Arc<dyn mockforge_openapi::response::AiGenerator + Send + Sync>>,
1759    smtp_registry: Option<Arc<dyn std::any::Any + Send + Sync>>,
1760    mqtt_broker: Option<Arc<dyn std::any::Any + Send + Sync>>,
1761    traffic_shaper: Option<mockforge_core::traffic_shaping::TrafficShaper>,
1762    traffic_shaping_enabled: bool,
1763    health_manager: Option<Arc<HealthManager>>,
1764    mockai: Option<Arc<RwLock<mockforge_core::intelligent_behavior::MockAI>>>,
1765    deceptive_deploy_config: Option<mockforge_core::config::DeceptiveDeployConfig>,
1766    proxy_config: Option<mockforge_proxy::config::ProxyConfig>,
1767) -> Router {
1768    use crate::latency_profiles::LatencyProfiles;
1769    use crate::op_middleware::Shared;
1770    use mockforge_core::Overrides;
1771
1772    // Extract template expansion setting before options is moved (used in OpenAPI routes and custom routes)
1773    let template_expand =
1774        options.as_ref().map(|o| o.response_template_expand).unwrap_or_else(|| {
1775            std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
1776                .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1777                .unwrap_or(false)
1778        });
1779
1780    let _shared = Shared {
1781        profiles: LatencyProfiles::default(),
1782        overrides: Overrides::default(),
1783        failure_injector: None,
1784        traffic_shaper,
1785        overrides_enabled: false,
1786        traffic_shaping_enabled,
1787    };
1788
1789    // Start with basic router
1790    let mut app = Router::new();
1791    let mut include_default_health = true;
1792    let mut captured_routes: Vec<RouteInfo> = Vec::new();
1793
1794    // If an OpenAPI spec is provided, integrate it
1795    if let Some(ref spec) = spec_path {
1796        match OpenApiSpec::from_file(&spec).await {
1797            Ok(openapi) => {
1798                info!("Loaded OpenAPI spec from {}", spec);
1799
1800                // Try to load persona from config if available
1801                let persona = load_persona_from_config().await;
1802
1803                let mut registry = if let Some(opts) = options {
1804                    tracing::debug!("Using custom validation options");
1805                    if let Some(ref persona) = persona {
1806                        tracing::info!("Using persona '{}' for route generation", persona.name);
1807                    }
1808                    OpenApiRouteRegistry::new_with_options_and_persona(openapi, opts, persona)
1809                } else {
1810                    tracing::debug!("Using environment-based options");
1811                    if let Some(ref persona) = persona {
1812                        tracing::info!("Using persona '{}' for route generation", persona.name);
1813                    }
1814                    OpenApiRouteRegistry::new_with_env_and_persona(openapi, persona)
1815                };
1816
1817                // Load custom fixtures if enabled
1818                let fixtures_dir = std::env::var("MOCKFORGE_FIXTURES_DIR")
1819                    .unwrap_or_else(|_| "/app/fixtures".to_string());
1820                let custom_fixtures_enabled = std::env::var("MOCKFORGE_CUSTOM_FIXTURES_ENABLED")
1821                    .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1822                    .unwrap_or(true); // Enabled by default
1823
1824                if custom_fixtures_enabled {
1825                    use mockforge_openapi::CustomFixtureLoader;
1826                    use std::path::PathBuf;
1827                    use std::sync::Arc;
1828
1829                    let fixtures_path = PathBuf::from(&fixtures_dir);
1830                    let mut custom_loader = CustomFixtureLoader::new(fixtures_path, true);
1831
1832                    if let Err(e) = custom_loader.load_fixtures().await {
1833                        tracing::warn!("Failed to load custom fixtures: {}", e);
1834                    } else {
1835                        tracing::info!("Custom fixtures loaded from {}", fixtures_dir);
1836                        registry = registry.with_custom_fixture_loader(Arc::new(custom_loader));
1837                    }
1838                }
1839
1840                if registry
1841                    .routes()
1842                    .iter()
1843                    .any(|route| route.method == "GET" && route.path == "/health")
1844                {
1845                    include_default_health = false;
1846                }
1847                // Capture route info for the /__mockforge/routes endpoint
1848                captured_routes = registry
1849                    .routes()
1850                    .iter()
1851                    .map(|r| RouteInfo {
1852                        method: r.method.clone(),
1853                        path: r.path.clone(),
1854                        operation_id: r.operation.operation_id.clone(),
1855                        summary: r.operation.summary.clone(),
1856                        description: r.operation.description.clone(),
1857                        parameters: r.parameters.clone(),
1858                    })
1859                    .collect();
1860
1861                // Store routes in the global route store so the admin server
1862                // can serve them without proxying back to the HTTP server.
1863                {
1864                    let global_routes: Vec<mockforge_core::request_logger::GlobalRouteInfo> =
1865                        captured_routes
1866                            .iter()
1867                            .map(|r| mockforge_core::request_logger::GlobalRouteInfo {
1868                                method: r.method.clone(),
1869                                path: r.path.clone(),
1870                                operation_id: r.operation_id.clone(),
1871                                summary: r.summary.clone(),
1872                                description: r.description.clone(),
1873                                parameters: r.parameters.clone(),
1874                            })
1875                            .collect();
1876                    mockforge_core::request_logger::set_global_routes(global_routes);
1877                    tracing::info!("Stored {} routes in global route store", captured_routes.len());
1878                }
1879
1880                // Use MockAI if available, otherwise use standard router
1881                let spec_router = if let Some(ref mockai_instance) = mockai {
1882                    tracing::debug!("Building router with MockAI support");
1883                    registry.build_router_with_mockai(Some(mockai_instance.clone()))
1884                } else {
1885                    registry.build_router()
1886                };
1887                // Issue #79 — explicitly stamp the body-size limit on the
1888                // openapi sub-router BEFORE merging. The limit is also
1889                // applied inside `OpenApiRouteRegistry::build_router_*`,
1890                // but axum's `merge` can drop nested layers in some
1891                // configurations; redoing it at the merge site is cheap
1892                // insurance against the "200 OK before body completes"
1893                // bug Srikanth reported on Issue #79.
1894                let body_limit_mb = std::env::var("MOCKFORGE_HTTP_BODY_LIMIT_MB")
1895                    .ok()
1896                    .and_then(|v| v.parse::<usize>().ok())
1897                    .unwrap_or(50);
1898                let body_limit_bytes = body_limit_mb.saturating_mul(1024 * 1024);
1899                let spec_router =
1900                    spec_router.layer(axum::extract::DefaultBodyLimit::max(body_limit_bytes));
1901                tracing::info!(
1902                    body_limit_mb = body_limit_mb,
1903                    "Merging OpenAPI router with main router"
1904                );
1905                app = app.merge(spec_router);
1906            }
1907            Err(e) => {
1908                warn!("Failed to load OpenAPI spec from {:?}: {}. Starting without OpenAPI integration.", spec_path, e);
1909            }
1910        }
1911    }
1912
1913    // Register custom routes from config with advanced routing features
1914    // Create RouteChaosInjector for advanced fault injection and latency
1915    // Store as trait object to avoid circular dependency (RouteChaosInjectorTrait is in mockforge-core)
1916    let route_chaos_injector: Option<
1917        std::sync::Arc<dyn mockforge_core::priority_handler::RouteChaosInjectorTrait>,
1918    > = if let Some(ref route_configs) = route_configs {
1919        if !route_configs.is_empty() {
1920            // Convert to the type expected by RouteChaosInjector
1921            // Note: Both use the same mockforge-core, but we need to ensure type compatibility
1922            let route_configs_converted: Vec<mockforge_core::config::RouteConfig> =
1923                route_configs.to_vec();
1924            match mockforge_route_chaos::RouteChaosInjector::new(route_configs_converted) {
1925                Ok(injector) => {
1926                    info!(
1927                        "Initialized advanced routing features for {} route(s)",
1928                        route_configs.len()
1929                    );
1930                    // RouteChaosInjector implements RouteChaosInjectorTrait, so we can cast it
1931                    // The trait is implemented in mockforge-route-chaos/src/lib.rs
1932                    Some(std::sync::Arc::new(injector)
1933                        as std::sync::Arc<
1934                            dyn mockforge_core::priority_handler::RouteChaosInjectorTrait,
1935                        >)
1936                }
1937                Err(e) => {
1938                    warn!(
1939                        "Failed to initialize advanced routing features: {}. Using basic routing.",
1940                        e
1941                    );
1942                    None
1943                }
1944            }
1945        } else {
1946            None
1947        }
1948    } else {
1949        None
1950    };
1951
1952    if let Some(route_configs) = route_configs {
1953        use axum::http::StatusCode;
1954        use axum::response::IntoResponse;
1955
1956        if !route_configs.is_empty() {
1957            info!("Registering {} custom route(s) from config", route_configs.len());
1958        }
1959
1960        let injector = route_chaos_injector.clone();
1961        for route_config in route_configs {
1962            let status = route_config.response.status;
1963            let body = route_config.response.body.clone();
1964            let headers = route_config.response.headers.clone();
1965            let path = route_config.path.clone();
1966            let method = route_config.method.clone();
1967
1968            // Create handler that returns the configured response with template expansion
1969            // Supports both basic templates ({{uuid}}, {{now}}) and request-aware templates
1970            // ({{request.query.name}}, {{request.path.id}}, {{request.headers.name}})
1971            // Register route using `any()` since we need full Request access for template expansion
1972            let expected_method = method.to_uppercase();
1973            // Clone Arc for the closure - Arc is Send-safe
1974            // Note: RouteChaosInjector is marked as Send+Sync via unsafe impl, so we have to
1975            // clone the Arc rather than move the inner injector into the route handler.
1976            let injector_clone = injector.clone();
1977            app = app.route(
1978                &path,
1979                #[allow(clippy::non_send_fields_in_send_ty)]
1980                axum::routing::any(move |req: Request<Body>| {
1981                    let body = body.clone();
1982                    let headers = headers.clone();
1983                    let expand = template_expand;
1984                    let expected = expected_method.clone();
1985                    let status_code = status;
1986                    // Clone Arc again for the async block
1987                    let injector_for_chaos = injector_clone.clone();
1988
1989                    async move {
1990                        // Check if request method matches expected method
1991                        if req.method().as_str() != expected.as_str() {
1992                            // Return 405 Method Not Allowed for wrong method
1993                            return axum::response::Response::builder()
1994                                .status(StatusCode::METHOD_NOT_ALLOWED)
1995                                .header("Allow", &expected)
1996                                .body(Body::empty())
1997                                .unwrap()
1998                                .into_response();
1999                        }
2000
2001                        // Apply advanced routing features (fault injection and latency) if available
2002                        // Use helper function to avoid capturing RouteChaosInjector in closure
2003                        // Pass the Arc as a reference to the helper function
2004                        if let Some(fault_response) = apply_route_chaos(
2005                            injector_for_chaos.as_deref(),
2006                            req.method(),
2007                            req.uri(),
2008                        )
2009                        .await
2010                        {
2011                            return fault_response;
2012                        }
2013
2014                        // Create JSON response from body, or empty object if None
2015                        let mut body_value = body.unwrap_or(serde_json::json!({}));
2016
2017                        // Apply template expansion if enabled
2018                        // Use mockforge-template-expansion crate which is completely isolated
2019                        // from mockforge-core to avoid Send issues (no rng() in dependency chain)
2020                        if expand {
2021                            use mockforge_template_expansion::RequestContext;
2022                            use serde_json::Value;
2023                            use std::collections::HashMap;
2024
2025                            // Extract request data for template expansion
2026                            let method = req.method().to_string();
2027                            let path = req.uri().path().to_string();
2028
2029                            // Extract query parameters
2030                            let query_params: HashMap<String, Value> = req
2031                                .uri()
2032                                .query()
2033                                .map(|q| {
2034                                    url::form_urlencoded::parse(q.as_bytes())
2035                                        .into_owned()
2036                                        .map(|(k, v)| (k, Value::String(v)))
2037                                        .collect()
2038                                })
2039                                .unwrap_or_default();
2040
2041                            // Extract headers
2042                            let headers: HashMap<String, Value> = req
2043                                .headers()
2044                                .iter()
2045                                .map(|(k, v)| {
2046                                    (
2047                                        k.to_string(),
2048                                        Value::String(v.to_str().unwrap_or_default().to_string()),
2049                                    )
2050                                })
2051                                .collect();
2052
2053                            // Create RequestContext for expand_prompt_template
2054                            // Using RequestContext from mockforge-template-expansion (not mockforge-core)
2055                            // to avoid bringing rng() into scope
2056                            let context = RequestContext {
2057                                method,
2058                                path,
2059                                query_params,
2060                                headers,
2061                                body: None, // Body extraction would require reading the request stream
2062                                path_params: HashMap::new(),
2063                                multipart_fields: HashMap::new(),
2064                                multipart_files: HashMap::new(),
2065                            };
2066
2067                            // Perform template expansion in spawn_blocking to ensure Send safety
2068                            // The template expansion crate is completely isolated from mockforge-core
2069                            // and doesn't have rng() in its dependency chain
2070                            let body_value_clone = body_value.clone();
2071                            let context_clone = context.clone();
2072                            body_value = match tokio::task::spawn_blocking(move || {
2073                                mockforge_template_expansion::expand_templates_in_json(
2074                                    body_value_clone,
2075                                    &context_clone,
2076                                )
2077                            })
2078                            .await
2079                            {
2080                                Ok(result) => result,
2081                                Err(_) => body_value, // Fallback to original on error
2082                            };
2083                        }
2084
2085                        let mut response = Json(body_value).into_response();
2086
2087                        // Set status code
2088                        *response.status_mut() =
2089                            StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK);
2090
2091                        // Add custom headers
2092                        for (key, value) in headers {
2093                            if let Ok(header_name) = http::HeaderName::from_bytes(key.as_bytes()) {
2094                                if let Ok(header_value) = http::HeaderValue::from_str(&value) {
2095                                    response.headers_mut().insert(header_name, header_value);
2096                                }
2097                            }
2098                        }
2099
2100                        response
2101                    }
2102                }),
2103            );
2104
2105            debug!("Registered route: {} {}", method, path);
2106        }
2107    }
2108
2109    // Add health check endpoints
2110    if let Some(health) = health_manager {
2111        // Use comprehensive health check router with all probe endpoints
2112        app = app.merge(health::health_router(health));
2113        info!(
2114            "Health check endpoints enabled: /health, /health/live, /health/ready, /health/startup"
2115        );
2116    } else if include_default_health {
2117        // Fallback to basic health endpoint for backwards compatibility
2118        app = app.route(
2119            "/health",
2120            axum::routing::get(|| async {
2121                use mockforge_core::server_utils::health::HealthStatus;
2122                {
2123                    // HealthStatus should always serialize, but handle errors gracefully
2124                    match serde_json::to_value(HealthStatus::healthy(0, "mockforge-http")) {
2125                        Ok(value) => Json(value),
2126                        Err(e) => {
2127                            // Log error but return a simple healthy response
2128                            tracing::error!("Failed to serialize health status: {}", e);
2129                            Json(serde_json::json!({
2130                                "status": "healthy",
2131                                "service": "mockforge-http",
2132                                "uptime_seconds": 0
2133                            }))
2134                        }
2135                    }
2136                }
2137            }),
2138        );
2139    }
2140
2141    app = app.merge(sse::sse_router());
2142    // Add file serving endpoints for generated mock files
2143    app = app.merge(file_server::file_serving_router());
2144
2145    // Add management API endpoints
2146    // Load spec for ManagementState so /__mockforge/api/spec can serve it
2147    let mgmt_spec = if let Some(ref sp) = spec_path {
2148        match OpenApiSpec::from_file(sp).await {
2149            Ok(s) => Some(Arc::new(s)),
2150            Err(e) => {
2151                debug!("Failed to load OpenAPI spec for management API: {}", e);
2152                None
2153            }
2154        }
2155    } else {
2156        None
2157    };
2158    let spec_path_clone = spec_path.clone();
2159    let mgmt_port = std::env::var("PORT")
2160        .or_else(|_| std::env::var("MOCKFORGE_HTTP_PORT"))
2161        .ok()
2162        .and_then(|p| p.parse().ok())
2163        .unwrap_or(3000);
2164    let management_state = ManagementState::new(mgmt_spec, spec_path_clone, mgmt_port);
2165
2166    // Create WebSocket state and connect it to management state
2167    use std::sync::Arc;
2168    let ws_state = WsManagementState::new();
2169    let ws_broadcast = Arc::new(ws_state.tx.clone());
2170    let management_state = management_state.with_ws_broadcast(ws_broadcast);
2171
2172    // Add proxy config to management state if available
2173    let management_state = if let Some(proxy_cfg) = proxy_config {
2174        use tokio::sync::RwLock;
2175        let proxy_config_arc = Arc::new(RwLock::new(proxy_cfg));
2176        management_state.with_proxy_config(proxy_config_arc)
2177    } else {
2178        management_state
2179    };
2180
2181    #[cfg(feature = "smtp")]
2182    let management_state = {
2183        if let Some(smtp_reg) = smtp_registry {
2184            match smtp_reg.downcast::<mockforge_smtp::SmtpSpecRegistry>() {
2185                Ok(smtp_reg) => management_state.with_smtp_registry(smtp_reg),
2186                Err(e) => {
2187                    error!(
2188                        "Invalid SMTP registry type passed to HTTP management state: {:?}",
2189                        e.type_id()
2190                    );
2191                    management_state
2192                }
2193            }
2194        } else {
2195            management_state
2196        }
2197    };
2198    #[cfg(not(feature = "smtp"))]
2199    let management_state = {
2200        let _ = smtp_registry;
2201        management_state
2202    };
2203    #[cfg(feature = "mqtt")]
2204    let management_state = {
2205        if let Some(broker) = mqtt_broker {
2206            match broker.downcast::<mockforge_mqtt::MqttBroker>() {
2207                Ok(broker) => management_state.with_mqtt_broker(broker),
2208                Err(e) => {
2209                    error!(
2210                        "Invalid MQTT broker passed to HTTP management state: {:?}",
2211                        e.type_id()
2212                    );
2213                    management_state
2214                }
2215            }
2216        } else {
2217            management_state
2218        }
2219    };
2220    #[cfg(not(feature = "mqtt"))]
2221    let management_state = {
2222        let _ = mqtt_broker;
2223        management_state
2224    };
2225    let management_state_for_fallback = management_state.clone();
2226    app = app.nest("/__mockforge/api", management_router(management_state));
2227    // Dynamic-mock fallback; see identical block earlier in this file.
2228    app = app.fallback_service(
2229        axum::routing::any(management::dynamic_mock_fallback)
2230            .with_state(management_state_for_fallback),
2231    );
2232
2233    // Add verification API endpoint
2234    app = app.merge(verification_router());
2235
2236    // Mount the request-chains management API at the prefix the UI's
2237    // ChainsPage expects (`/__mockforge/chains/*`). Without this nest the
2238    // page 404s on every call. The registry/engine are constructed from
2239    // the chain_config param when supplied, otherwise from defaults.
2240    {
2241        use crate::chain_handlers::{chains_router, create_chain_state};
2242        let chain_config = _circling_config.clone().unwrap_or_default();
2243        let chain_registry = Arc::new(mockforge_core::request_chaining::RequestChainRegistry::new(
2244            chain_config.clone(),
2245        ));
2246        let chain_engine = Arc::new(mockforge_core::chain_execution::ChainExecutionEngine::new(
2247            chain_registry.clone(),
2248            chain_config,
2249        ));
2250        app = app.nest(
2251            "/__mockforge/chains",
2252            chains_router(create_chain_state(chain_registry, chain_engine)),
2253        );
2254    }
2255
2256    // Contract-diff retrieval API. The `capture_for_contract_diff`
2257    // middleware (layered earlier) populates the global capture manager
2258    // on every request; this surface lets operators read the captures
2259    // and run the analyser against the current OpenAPI spec.
2260    {
2261        use crate::contract_diff_api::{contract_diff_api_router, ContractDiffApiState};
2262        let cd_state = Arc::new(ContractDiffApiState::new(spec_path.clone()));
2263        app = app.nest("/__mockforge/api/contract-diff", contract_diff_api_router(cd_state));
2264    }
2265
2266    // Fixtures management API. Lives on the admin server too (port 9080),
2267    // but hosted-mock Fly machines only expose port 3000 publicly — so
2268    // we mount the same surface on the main HTTP app. Newly-uploaded
2269    // fixtures land in `MOCKFORGE_FIXTURES_DIR` and are picked up by
2270    // the startup-time loader on the next deploy/restart; live reload
2271    // is a separate concern.
2272    {
2273        use crate::fixtures_api::{fixtures_api_router, FixturesApiState};
2274        let fx_state = FixturesApiState::from_env();
2275        app = app.nest("/__mockforge/fixtures", fixtures_api_router(fx_state));
2276    }
2277
2278    // Standalone MockAI API. Until now MockAI was only invoked from the
2279    // OpenAPI route generator — so a hosted mock without a spec, or a
2280    // user wanting to prototype an "AI persona" mock without registering
2281    // routes, had no way to reach the engine. The endpoint surfaces 503
2282    // when MockAI isn't configured (no API key) so callers get a clear
2283    // signal rather than a silent failure.
2284    {
2285        use crate::mockai_api::{mockai_api_router, MockAiApiState};
2286        let api_state = MockAiApiState::new(mockai.clone());
2287        app = app.nest("/__mockforge/api/mockai", mockai_api_router(api_state));
2288    }
2289
2290    // Time-travel runtime API. The admin server mounts these routes on
2291    // its own port (9080), but hosted-mock Fly machines only expose
2292    // port 3000 publicly — so until now operators couldn't set / advance
2293    // virtual time on a deployed mock. The handlers consult a process-
2294    // wide TimeTravelManager that serve.rs initialises alongside the
2295    // existing admin-server registration; both paths see the same Arc.
2296    app = app.nest("/__mockforge/time-travel", time_travel_api::time_travel_router());
2297
2298    // Runtime route-chaos rules API + middleware. This sits in front of
2299    // the static per-route handlers so operators can add/remove fault and
2300    // latency rules without redeploying. Static rules from the YAML
2301    // config still apply to their routes; runtime rules are additive.
2302    {
2303        use crate::route_chaos_runtime::{
2304            route_chaos_api_router, runtime_route_chaos_middleware, RuntimeRouteChaosState,
2305        };
2306        let runtime_state = RuntimeRouteChaosState::new(Vec::new());
2307        let middleware_state = runtime_state.clone();
2308        app = app.layer(from_fn_with_state(middleware_state, runtime_route_chaos_middleware));
2309        app = app.nest("/__mockforge/api/route-chaos", route_chaos_api_router(runtime_state));
2310    }
2311
2312    // Runtime network-profile switching. Operators activate a named
2313    // catalog profile (e.g., "mobile_3g") and the middleware applies its
2314    // latency to every subsequent request — useful for "what does my
2315    // consumer look like under bad network" probes against a live
2316    // hosted mock without a redeploy.
2317    {
2318        use crate::network_profile_runtime::{
2319            network_profile_api_router, network_profile_middleware, NetworkProfileRuntimeState,
2320        };
2321        let runtime_state = NetworkProfileRuntimeState::new(
2322            mockforge_core::network_profiles::NetworkProfileCatalog::new(),
2323        );
2324        let middleware_state = runtime_state.clone();
2325        app = app.layer(from_fn_with_state(middleware_state, network_profile_middleware));
2326        app = app
2327            .nest("/__mockforge/api/network-profiles", network_profile_api_router(runtime_state));
2328    }
2329
2330    // Add OIDC well-known endpoints
2331    use crate::auth::oidc::oidc_router;
2332    app = app.merge(oidc_router());
2333
2334    // Add access review API if enabled
2335    {
2336        use mockforge_core::security::get_global_access_review_service;
2337        if let Some(service) = get_global_access_review_service().await {
2338            use crate::handlers::access_review::{access_review_router, AccessReviewState};
2339            let review_state = AccessReviewState { service };
2340            app = app.nest("/api/v1/security/access-reviews", access_review_router(review_state));
2341            debug!("Access review API mounted at /api/v1/security/access-reviews");
2342        }
2343    }
2344
2345    // Add privileged access API if enabled
2346    {
2347        use mockforge_core::security::get_global_privileged_access_manager;
2348        if let Some(manager) = get_global_privileged_access_manager().await {
2349            use crate::handlers::privileged_access::{
2350                privileged_access_router, PrivilegedAccessState,
2351            };
2352            let privileged_state = PrivilegedAccessState { manager };
2353            app = app.nest(
2354                "/api/v1/security/privileged-access",
2355                privileged_access_router(privileged_state),
2356            );
2357            debug!("Privileged access API mounted at /api/v1/security/privileged-access");
2358        }
2359    }
2360
2361    // Add change management API if enabled
2362    {
2363        use mockforge_core::security::get_global_change_management_engine;
2364        if let Some(engine) = get_global_change_management_engine().await {
2365            use crate::handlers::change_management::{
2366                change_management_router, ChangeManagementState,
2367            };
2368            let change_state = ChangeManagementState { engine };
2369            app = app.nest("/api/v1/change-management", change_management_router(change_state));
2370            debug!("Change management API mounted at /api/v1/change-management");
2371        }
2372    }
2373
2374    // Add risk assessment API if enabled
2375    {
2376        use mockforge_core::security::get_global_risk_assessment_engine;
2377        if let Some(engine) = get_global_risk_assessment_engine().await {
2378            use crate::handlers::risk_assessment::{risk_assessment_router, RiskAssessmentState};
2379            let risk_state = RiskAssessmentState { engine };
2380            app = app.nest("/api/v1/security", risk_assessment_router(risk_state));
2381            debug!("Risk assessment API mounted at /api/v1/security/risks");
2382        }
2383    }
2384
2385    // Add token lifecycle API
2386    {
2387        use crate::auth::token_lifecycle::TokenLifecycleManager;
2388        use crate::handlers::token_lifecycle::{token_lifecycle_router, TokenLifecycleState};
2389        let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2390        let lifecycle_state = TokenLifecycleState {
2391            manager: lifecycle_manager,
2392        };
2393        app = app.nest("/api/v1/auth", token_lifecycle_router(lifecycle_state));
2394        debug!("Token lifecycle API mounted at /api/v1/auth");
2395    }
2396
2397    // Add OAuth2 server endpoints
2398    {
2399        use crate::auth::oidc::load_oidc_state;
2400        use crate::auth::token_lifecycle::TokenLifecycleManager;
2401        use crate::handlers::oauth2_server::{oauth2_server_router, OAuth2ServerState};
2402        // Load OIDC state from configuration (environment variables or config file)
2403        let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2404        let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2405        let oauth2_state = OAuth2ServerState {
2406            oidc_state,
2407            lifecycle_manager,
2408            auth_codes: Arc::new(RwLock::new(HashMap::new())),
2409            refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2410        };
2411        app = app.merge(oauth2_server_router(oauth2_state));
2412        debug!("OAuth2 server endpoints mounted at /oauth2/authorize and /oauth2/token");
2413    }
2414
2415    // Add consent screen endpoints
2416    {
2417        use crate::auth::oidc::load_oidc_state;
2418        use crate::auth::risk_engine::RiskEngine;
2419        use crate::auth::token_lifecycle::TokenLifecycleManager;
2420        use crate::handlers::consent::{consent_router, ConsentState};
2421        use crate::handlers::oauth2_server::OAuth2ServerState;
2422        // Load OIDC state from configuration (environment variables or config file)
2423        let oidc_state = Arc::new(RwLock::new(load_oidc_state()));
2424        let lifecycle_manager = Arc::new(TokenLifecycleManager::default());
2425        let oauth2_state = OAuth2ServerState {
2426            oidc_state: oidc_state.clone(),
2427            lifecycle_manager: lifecycle_manager.clone(),
2428            auth_codes: Arc::new(RwLock::new(HashMap::new())),
2429            refresh_tokens: Arc::new(RwLock::new(HashMap::new())),
2430        };
2431        let risk_engine = Arc::new(RiskEngine::default());
2432        let consent_state = ConsentState {
2433            oauth2_state,
2434            risk_engine,
2435        };
2436        app = app.merge(consent_router(consent_state));
2437        debug!("Consent screen endpoints mounted at /consent");
2438    }
2439
2440    // Add risk simulation API
2441    {
2442        use crate::auth::risk_engine::RiskEngine;
2443        use crate::handlers::risk_simulation::{risk_simulation_router, RiskSimulationState};
2444        let risk_engine = Arc::new(RiskEngine::default());
2445        let risk_state = RiskSimulationState { risk_engine };
2446        app = app.nest("/api/v1/auth", risk_simulation_router(risk_state));
2447        debug!("Risk simulation API mounted at /api/v1/auth/risk");
2448    }
2449
2450    // Initialize database connection (optional)
2451    #[cfg(feature = "database")]
2452    let database = {
2453        use crate::database::Database;
2454        let database_url = std::env::var("DATABASE_URL").ok();
2455        match Database::connect_optional(database_url.as_deref()).await {
2456            Ok(db) => {
2457                if db.is_connected() {
2458                    // Run migrations if database is connected
2459                    if let Err(e) = db.migrate_if_connected().await {
2460                        warn!("Failed to run database migrations: {}", e);
2461                    } else {
2462                        info!("Database connected and migrations applied");
2463                    }
2464                }
2465                Some(db)
2466            }
2467            Err(e) => {
2468                warn!("Failed to connect to database: {}. Continuing without database support.", e);
2469                None
2470            }
2471        }
2472    };
2473
2474    // Add drift budget and incident management endpoints
2475    // Initialize shared components for drift tracking and protocol contracts
2476    let (drift_engine, incident_manager, drift_config) = {
2477        use mockforge_core::contract_drift::{DriftBudgetConfig, DriftBudgetEngine};
2478        use mockforge_core::incidents::{IncidentManager, IncidentStore};
2479        use std::sync::Arc;
2480
2481        // Initialize drift budget engine with default config
2482        let drift_config = DriftBudgetConfig::default();
2483        let drift_engine = Arc::new(DriftBudgetEngine::new(drift_config.clone()));
2484
2485        // Initialize incident store and manager
2486        let incident_store = Arc::new(IncidentStore::default());
2487        let incident_manager = Arc::new(IncidentManager::new(incident_store.clone()));
2488
2489        (drift_engine, incident_manager, drift_config)
2490    };
2491
2492    {
2493        use crate::handlers::drift_budget::{drift_budget_router, DriftBudgetState};
2494        use crate::middleware::drift_tracking::DriftTrackingState;
2495        use mockforge_contracts::consumer_contracts::{
2496            ConsumerBreakingChangeDetector, UsageRecorder,
2497        };
2498        use mockforge_core::ai_contract_diff::ContractDiffAnalyzer;
2499        use std::sync::Arc;
2500
2501        // Initialize usage recorder and consumer detector
2502        let usage_recorder = Arc::new(UsageRecorder::default());
2503        let consumer_detector =
2504            Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2505
2506        // Initialize contract diff analyzer if enabled
2507        let diff_analyzer = if drift_config.enabled {
2508            match ContractDiffAnalyzer::new(
2509                mockforge_core::ai_contract_diff::ContractDiffConfig::default(),
2510            ) {
2511                Ok(analyzer) => Some(Arc::new(analyzer)),
2512                Err(e) => {
2513                    warn!("Failed to create contract diff analyzer: {}", e);
2514                    None
2515                }
2516            }
2517        } else {
2518            None
2519        };
2520
2521        // Get OpenAPI spec if available
2522        // Note: Load from spec_path if available, or leave as None for manual configuration.
2523        let spec = if let Some(ref spec_path) = spec_path {
2524            match OpenApiSpec::from_file(spec_path).await {
2525                Ok(s) => Some(Arc::new(s)),
2526                Err(e) => {
2527                    debug!("Failed to load OpenAPI spec for drift tracking: {}", e);
2528                    None
2529                }
2530            }
2531        } else {
2532            None
2533        };
2534
2535        // Create drift tracking state
2536        let drift_tracking_state = DriftTrackingState {
2537            diff_analyzer,
2538            spec,
2539            drift_engine: drift_engine.clone(),
2540            incident_manager: incident_manager.clone(),
2541            usage_recorder,
2542            consumer_detector,
2543            enabled: drift_config.enabled,
2544        };
2545
2546        // Add response body buffering middleware (before drift tracking)
2547        app = app.layer(axum::middleware::from_fn(middleware::buffer_response_middleware));
2548
2549        // Add drift tracking middleware (after response buffering)
2550        // Use a wrapper that inserts state into extensions before calling the middleware
2551        let drift_tracking_state_clone = drift_tracking_state.clone();
2552        app = app.layer(axum::middleware::from_fn(
2553            move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2554                let state = drift_tracking_state_clone.clone();
2555                async move {
2556                    // Insert state into extensions if not already present
2557                    if req.extensions().get::<DriftTrackingState>().is_none() {
2558                        req.extensions_mut().insert(state);
2559                    }
2560                    // Call the middleware function
2561                    middleware::drift_tracking::drift_tracking_middleware_with_extensions(req, next)
2562                        .await
2563                }
2564            },
2565        ));
2566
2567        let drift_state = DriftBudgetState {
2568            engine: drift_engine.clone(),
2569            incident_manager: incident_manager.clone(),
2570            gitops_handler: None, // Can be initialized later if GitOps is configured
2571        };
2572
2573        app = app.merge(drift_budget_router(drift_state));
2574        debug!("Drift budget and incident management endpoints mounted at /api/v1/drift");
2575    }
2576
2577    // Add pipeline management endpoints (MockOps)
2578    #[cfg(feature = "pipelines")]
2579    {
2580        use crate::handlers::pipelines::{pipeline_router, PipelineState};
2581
2582        let pipeline_state = PipelineState::new();
2583        app = app.merge(pipeline_router(pipeline_state));
2584        debug!("Pipeline management endpoints mounted at /api/v1/pipelines");
2585    }
2586
2587    // Add governance endpoints (forecasting, semantic drift, threat modeling, contract health)
2588    {
2589        use crate::handlers::contract_health::{contract_health_router, ContractHealthState};
2590        use mockforge_contracts::contract_drift::forecasting::{Forecaster, ForecastingConfig};
2591        use mockforge_core::contract_drift::threat_modeling::ThreatAnalyzer;
2592        use mockforge_core::incidents::semantic_manager::SemanticIncidentManager;
2593        use mockforge_foundation::threat_modeling_types::ThreatModelingConfig;
2594        use mockforge_intelligence::handlers::forecasting::{forecasting_router, ForecastingState};
2595        use mockforge_intelligence::handlers::semantic_drift::{
2596            semantic_drift_router, SemanticDriftState,
2597        };
2598        use mockforge_intelligence::handlers::threat_modeling::{
2599            threat_modeling_router, ThreatModelingState,
2600        };
2601        use std::sync::Arc;
2602
2603        // Initialize forecasting
2604        let forecasting_config = ForecastingConfig::default();
2605        let forecaster = Arc::new(Forecaster::new(forecasting_config));
2606        let forecasting_state = ForecastingState {
2607            forecaster,
2608            #[cfg(feature = "database")]
2609            database: database.clone(),
2610        };
2611
2612        // Initialize semantic drift manager
2613        let semantic_manager = Arc::new(SemanticIncidentManager::new());
2614        let semantic_state = SemanticDriftState {
2615            manager: semantic_manager,
2616            #[cfg(feature = "database")]
2617            database: database.clone(),
2618        };
2619
2620        // Initialize threat analyzer
2621        let threat_config = ThreatModelingConfig::default();
2622        let threat_analyzer = match ThreatAnalyzer::new(threat_config) {
2623            Ok(analyzer) => Arc::new(analyzer),
2624            Err(e) => {
2625                warn!("Failed to create threat analyzer: {}. Using default.", e);
2626                Arc::new(ThreatAnalyzer::new(ThreatModelingConfig::default()).unwrap_or_else(
2627                    |_| {
2628                        // Fallback to a minimal config if default also fails
2629                        ThreatAnalyzer::new(ThreatModelingConfig {
2630                            enabled: false,
2631                            ..Default::default()
2632                        })
2633                        .expect("Failed to create fallback threat analyzer")
2634                    },
2635                ))
2636            }
2637        };
2638        // Load webhook configs from ServerConfig
2639        let mut webhook_configs = Vec::new();
2640        let config_paths = [
2641            "config.yaml",
2642            "mockforge.yaml",
2643            "tools/mockforge/config.yaml",
2644            "../tools/mockforge/config.yaml",
2645        ];
2646
2647        for path in &config_paths {
2648            if let Ok(config) = mockforge_core::config::load_config(path).await {
2649                if !config.incidents.webhooks.is_empty() {
2650                    webhook_configs = config.incidents.webhooks.clone();
2651                    info!("Loaded {} webhook configs from config: {}", webhook_configs.len(), path);
2652                    break;
2653                }
2654            }
2655        }
2656
2657        if webhook_configs.is_empty() {
2658            debug!("No webhook configs found in config files, using empty list");
2659        }
2660
2661        let threat_state = ThreatModelingState {
2662            analyzer: threat_analyzer,
2663            webhook_configs,
2664            #[cfg(feature = "database")]
2665            database: database.clone(),
2666        };
2667
2668        // Initialize contract health state
2669        let contract_health_state = ContractHealthState {
2670            incident_manager: incident_manager.clone(),
2671            semantic_manager: Arc::new(SemanticIncidentManager::new()),
2672            #[cfg(feature = "database")]
2673            database: database.clone(),
2674        };
2675
2676        // Register routers
2677        app = app.merge(forecasting_router(forecasting_state));
2678        debug!("Forecasting endpoints mounted at /api/v1/forecasts");
2679
2680        app = app.merge(semantic_drift_router(semantic_state));
2681        debug!("Semantic drift endpoints mounted at /api/v1/semantic-drift");
2682
2683        app = app.merge(threat_modeling_router(threat_state));
2684        debug!("Threat modeling endpoints mounted at /api/v1/threats");
2685
2686        app = app.merge(contract_health_router(contract_health_state));
2687        debug!("Contract health endpoints mounted at /api/v1/contract-health");
2688    }
2689
2690    // Add protocol contracts endpoints with fitness registry initialization
2691    {
2692        use crate::handlers::protocol_contracts::{
2693            protocol_contracts_router, ProtocolContractState,
2694        };
2695        use mockforge_core::contract_drift::{
2696            ConsumerImpactAnalyzer, FitnessFunctionRegistry, ProtocolContractRegistry,
2697        };
2698        use std::sync::Arc;
2699        use tokio::sync::RwLock;
2700
2701        // Initialize protocol contract registry
2702        let contract_registry = Arc::new(RwLock::new(ProtocolContractRegistry::new()));
2703
2704        // Initialize fitness function registry and load from config
2705        let mut fitness_registry = FitnessFunctionRegistry::new();
2706
2707        // Try to load config and populate fitness rules
2708        let config_paths = [
2709            "config.yaml",
2710            "mockforge.yaml",
2711            "tools/mockforge/config.yaml",
2712            "../tools/mockforge/config.yaml",
2713        ];
2714
2715        let mut config_loaded = false;
2716        for path in &config_paths {
2717            if let Ok(config) = mockforge_core::config::load_config(path).await {
2718                if !config.contracts.fitness_rules.is_empty() {
2719                    if let Err(e) =
2720                        fitness_registry.load_from_config(&config.contracts.fitness_rules)
2721                    {
2722                        warn!("Failed to load fitness rules from config {}: {}", path, e);
2723                    } else {
2724                        info!(
2725                            "Loaded {} fitness rules from config: {}",
2726                            config.contracts.fitness_rules.len(),
2727                            path
2728                        );
2729                        config_loaded = true;
2730                        break;
2731                    }
2732                }
2733            }
2734        }
2735
2736        if !config_loaded {
2737            debug!("No fitness rules found in config files, using empty registry");
2738        }
2739
2740        let fitness_registry = Arc::new(RwLock::new(fitness_registry));
2741
2742        // Reuse drift engine and incident manager from drift budget section
2743
2744        // Initialize consumer impact analyzer
2745        let consumer_mapping_registry =
2746            mockforge_core::contract_drift::ConsumerMappingRegistry::new();
2747        let consumer_analyzer =
2748            Arc::new(RwLock::new(ConsumerImpactAnalyzer::new(consumer_mapping_registry)));
2749
2750        let protocol_state = ProtocolContractState {
2751            registry: contract_registry,
2752            drift_engine: Some(drift_engine.clone()),
2753            incident_manager: Some(incident_manager.clone()),
2754            fitness_registry: Some(fitness_registry),
2755            consumer_analyzer: Some(consumer_analyzer),
2756        };
2757
2758        app = app.nest("/api/v1/contracts", protocol_contracts_router(protocol_state));
2759        debug!("Protocol contracts endpoints mounted at /api/v1/contracts");
2760    }
2761
2762    // Add behavioral cloning middleware (optional - applies learned behavior to requests)
2763    #[cfg(feature = "behavioral-cloning")]
2764    {
2765        use crate::middleware::behavioral_cloning::BehavioralCloningMiddlewareState;
2766        use std::path::PathBuf;
2767
2768        // Determine database path (defaults to ./recordings.db)
2769        let db_path = std::env::var("RECORDER_DATABASE_PATH")
2770            .ok()
2771            .map(PathBuf::from)
2772            .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2773
2774        let bc_middleware_state = if let Some(path) = db_path {
2775            BehavioralCloningMiddlewareState::with_database_path(path)
2776        } else {
2777            BehavioralCloningMiddlewareState::new()
2778        };
2779
2780        // Only enable if BEHAVIORAL_CLONING_ENABLED is set to true
2781        let enabled = std::env::var("BEHAVIORAL_CLONING_ENABLED")
2782            .ok()
2783            .and_then(|v| v.parse::<bool>().ok())
2784            .unwrap_or(false);
2785
2786        if enabled {
2787            let bc_state_clone = bc_middleware_state.clone();
2788            app = app.layer(axum::middleware::from_fn(
2789                move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2790                    let state = bc_state_clone.clone();
2791                    async move {
2792                        // Insert state into extensions if not already present
2793                        if req.extensions().get::<BehavioralCloningMiddlewareState>().is_none() {
2794                            req.extensions_mut().insert(state);
2795                        }
2796                        // Call the middleware function
2797                        middleware::behavioral_cloning::behavioral_cloning_middleware(req, next)
2798                            .await
2799                    }
2800                },
2801            ));
2802            debug!("Behavioral cloning middleware enabled (applies learned behavior to requests)");
2803        }
2804    }
2805
2806    // Add consumer contracts endpoints
2807    {
2808        use crate::handlers::consumer_contracts::{
2809            consumer_contracts_router, ConsumerContractsState,
2810        };
2811        use mockforge_contracts::consumer_contracts::{
2812            ConsumerBreakingChangeDetector, ConsumerRegistry, UsageRecorder,
2813        };
2814        use std::sync::Arc;
2815
2816        // Initialize consumer registry
2817        let registry = Arc::new(ConsumerRegistry::default());
2818
2819        // Initialize usage recorder
2820        let usage_recorder = Arc::new(UsageRecorder::default());
2821
2822        // Initialize breaking change detector
2823        let detector = Arc::new(ConsumerBreakingChangeDetector::new(usage_recorder.clone()));
2824
2825        let consumer_state = ConsumerContractsState {
2826            registry,
2827            usage_recorder,
2828            detector,
2829            violations: Arc::new(RwLock::new(HashMap::new())),
2830        };
2831
2832        app = app.merge(consumer_contracts_router(consumer_state));
2833        debug!("Consumer contracts endpoints mounted at /api/v1/consumers");
2834    }
2835
2836    // Add behavioral cloning endpoints
2837    #[cfg(feature = "behavioral-cloning")]
2838    {
2839        use crate::handlers::behavioral_cloning::{
2840            behavioral_cloning_router, BehavioralCloningState,
2841        };
2842        use std::path::PathBuf;
2843
2844        // Determine database path (defaults to ./recordings.db)
2845        let db_path = std::env::var("RECORDER_DATABASE_PATH")
2846            .ok()
2847            .map(PathBuf::from)
2848            .or_else(|| std::env::current_dir().ok().map(|p| p.join("recordings.db")));
2849
2850        let bc_state = if let Some(path) = db_path {
2851            BehavioralCloningState::with_database_path(path)
2852        } else {
2853            BehavioralCloningState::new()
2854        };
2855
2856        app = app.merge(behavioral_cloning_router(bc_state));
2857        debug!("Behavioral cloning endpoints mounted at /api/v1/behavioral-cloning");
2858    }
2859
2860    // Add consistency engine and cross-protocol state management
2861    {
2862        use crate::consistency::{ConsistencyMiddlewareState, HttpAdapter};
2863        use mockforge_intelligence::consistency::ConsistencyEngine;
2864        use mockforge_intelligence::handlers::consistency::{consistency_router, ConsistencyState};
2865        use std::sync::Arc;
2866
2867        // Initialize consistency engine
2868        let consistency_engine = Arc::new(ConsistencyEngine::new());
2869
2870        // Create and register HTTP adapter
2871        let http_adapter = Arc::new(HttpAdapter::new(consistency_engine.clone()));
2872        consistency_engine.register_adapter(http_adapter.clone()).await;
2873
2874        // Create consistency state for handlers
2875        let consistency_state = ConsistencyState {
2876            engine: consistency_engine.clone(),
2877        };
2878
2879        // Create X-Ray state first (needed for middleware)
2880        use mockforge_intelligence::handlers::xray::XRayState;
2881        let xray_state = Arc::new(XRayState::new(consistency_engine.clone()));
2882
2883        // Create consistency middleware state
2884        let consistency_middleware_state = ConsistencyMiddlewareState {
2885            engine: consistency_engine.clone(),
2886            adapter: http_adapter,
2887            xray_state: Some(xray_state.clone()),
2888        };
2889
2890        // Reality-driven mock/proxy middleware (#222). Must be added
2891        // BEFORE the consistency layer so it ends up inner — that way
2892        // each request hits consistency first (which injects UnifiedState
2893        // including reality_continuum_ratio), then this middleware reads
2894        // that state and either forwards to upstream or falls through to
2895        // the mock chain. Activated only when MOCKFORGE_PROXY_UPSTREAM is
2896        // set; absent that, the layer isn't even installed.
2897        if let Some(reality_cfg) = reality_proxy::RealityProxyConfig::from_env() {
2898            tracing::info!(
2899                upstream = %reality_cfg.upstream_base,
2900                "Reality-driven proxy middleware enabled — requests will be split between mock and upstream based on reality_continuum_ratio"
2901            );
2902            app = app.layer(axum::middleware::from_fn(
2903                move |req: axum::extract::Request, next: axum::middleware::Next| {
2904                    let cfg = reality_cfg.clone();
2905                    async move { reality_proxy::reality_proxy_middleware(cfg, req, next).await }
2906                },
2907            ));
2908        }
2909
2910        // Add consistency middleware (before other middleware to inject state early)
2911        let consistency_middleware_state_clone = consistency_middleware_state.clone();
2912        app = app.layer(axum::middleware::from_fn(
2913            move |mut req: axum::extract::Request, next: axum::middleware::Next| {
2914                let state = consistency_middleware_state_clone.clone();
2915                async move {
2916                    // Insert state into extensions if not already present
2917                    if req.extensions().get::<ConsistencyMiddlewareState>().is_none() {
2918                        req.extensions_mut().insert(state);
2919                    }
2920                    // Call the middleware function
2921                    consistency::middleware::consistency_middleware(req, next).await
2922                }
2923            },
2924        ));
2925
2926        // Add consistency API endpoints
2927        app = app.merge(consistency_router(consistency_state));
2928        debug!("Consistency engine initialized and endpoints mounted at /api/v1/consistency");
2929
2930        // Runtime named-scenario activation API. Locally-installed
2931        // scenarios (from `~/.mockforge/scenario-metadata/*.json`) become
2932        // listable + activatable via HTTP; activation writes through to
2933        // the consistency engine's active_scenario slot for the chosen
2934        // workspace. Failure to load the scenario index is non-fatal —
2935        // the API just shows an empty list, which matches the natural
2936        // state of a fresh deployment with no scenarios installed yet.
2937        #[cfg(feature = "scenario-engine")]
2938        {
2939            use crate::scenarios_runtime::{scenarios_api_router, ScenarioRuntimeState};
2940            let mut scenario_storage = match mockforge_scenarios::ScenarioStorage::new() {
2941                Ok(s) => s,
2942                Err(e) => {
2943                    tracing::warn!(
2944                        error = %e,
2945                        "Failed to init scenario storage; runtime scenarios API will list empty"
2946                    );
2947                    // Construct an isolated tempdir-backed storage so the
2948                    // API still mounts but lists nothing.
2949                    let tmp = std::env::temp_dir().join("mockforge-empty-scenarios");
2950                    mockforge_scenarios::ScenarioStorage::with_dir(&tmp)
2951                        .expect("temp scenario storage")
2952                }
2953            };
2954            if let Err(e) = scenario_storage.load().await {
2955                tracing::warn!(
2956                    error = %e,
2957                    "Failed to load installed scenarios; API will list empty until scenarios are installed"
2958                );
2959            }
2960            let scenarios_state =
2961                ScenarioRuntimeState::new(scenario_storage, consistency_engine.clone());
2962            app = app.nest("/__mockforge/api/scenarios", scenarios_api_router(scenarios_state));
2963            debug!("Scenario runtime API mounted at /__mockforge/api/scenarios");
2964        }
2965
2966        // Add fidelity score endpoints
2967        {
2968            use mockforge_intelligence::handlers::fidelity::{fidelity_router, FidelityState};
2969            let fidelity_state = FidelityState::new();
2970            app = app.merge(fidelity_router(fidelity_state));
2971            debug!("Fidelity score endpoints mounted at /api/v1/workspace/:workspace_id/fidelity");
2972        }
2973
2974        // Add scenario studio endpoints
2975        {
2976            use mockforge_intelligence::handlers::scenario_studio::{
2977                scenario_studio_router, ScenarioStudioState,
2978            };
2979            let scenario_studio_state = ScenarioStudioState::new();
2980            app = app.merge(scenario_studio_router(scenario_studio_state));
2981            debug!("Scenario Studio endpoints mounted at /api/v1/scenario-studio");
2982        }
2983
2984        // Add performance mode endpoints
2985        {
2986            use crate::handlers::performance::{performance_router, PerformanceState};
2987            let performance_state = PerformanceState::new();
2988            app = app.nest("/api/performance", performance_router(performance_state));
2989            debug!("Performance mode endpoints mounted at /api/performance");
2990        }
2991
2992        // Add world state endpoints
2993        {
2994            use crate::handlers::world_state::{world_state_router, WorldStateState};
2995            use mockforge_world_state::WorldStateEngine;
2996            use std::sync::Arc;
2997            use tokio::sync::RwLock;
2998
2999            let world_state_engine = Arc::new(RwLock::new(WorldStateEngine::new()));
3000            let world_state_state = WorldStateState {
3001                engine: world_state_engine,
3002            };
3003            app = app.nest("/api/world-state", world_state_router().with_state(world_state_state));
3004            debug!("World state endpoints mounted at /api/world-state");
3005        }
3006
3007        // Add snapshot management endpoints
3008        {
3009            use crate::handlers::snapshots::{snapshot_router, SnapshotState};
3010            use mockforge_core::snapshots::SnapshotManager;
3011            use std::path::PathBuf;
3012
3013            let snapshot_dir = std::env::var("MOCKFORGE_SNAPSHOT_DIR").ok().map(PathBuf::from);
3014            let snapshot_manager = Arc::new(SnapshotManager::new(snapshot_dir));
3015
3016            let snapshot_state = SnapshotState {
3017                manager: snapshot_manager,
3018                consistency_engine: Some(consistency_engine.clone()),
3019                workspace_persistence: None, // Can be initialized later if workspace persistence is available
3020                vbr_engine: None, // Can be initialized when VBR engine is available in server state
3021                recorder: None,   // Can be initialized when Recorder is available in server state
3022            };
3023
3024            app = app.merge(snapshot_router(snapshot_state));
3025            debug!("Snapshot management endpoints mounted at /api/v1/snapshots");
3026
3027            // Add X-Ray API endpoints for browser extension
3028            {
3029                use mockforge_intelligence::handlers::xray::xray_router;
3030                app = app.merge(xray_router((*xray_state).clone()));
3031                debug!("X-Ray API endpoints mounted at /api/v1/xray");
3032            }
3033        }
3034
3035        // Add A/B testing endpoints and middleware
3036        {
3037            use crate::handlers::ab_testing::{ab_testing_router, ABTestingState};
3038            use crate::middleware::ab_testing::ab_testing_middleware;
3039
3040            let ab_testing_state = ABTestingState::new();
3041
3042            // Add A/B testing middleware (before other response middleware)
3043            let ab_testing_state_clone = ab_testing_state.clone();
3044            app = app.layer(axum::middleware::from_fn(
3045                move |mut req: axum::extract::Request, next: axum::middleware::Next| {
3046                    let state = ab_testing_state_clone.clone();
3047                    async move {
3048                        // Insert state into extensions if not already present
3049                        if req.extensions().get::<ABTestingState>().is_none() {
3050                            req.extensions_mut().insert(state);
3051                        }
3052                        // Call the middleware function
3053                        ab_testing_middleware(req, next).await
3054                    }
3055                },
3056            ));
3057
3058            // Add A/B testing API endpoints
3059            app = app.merge(ab_testing_router(ab_testing_state));
3060            debug!("A/B testing endpoints mounted at /api/v1/ab-tests");
3061        }
3062    }
3063
3064    // Add PR generation endpoints (optional - only if configured)
3065    {
3066        use crate::handlers::pr_generation::{pr_generation_router, PRGenerationState};
3067        use mockforge_intelligence::pr_generation::{PRGenerator, PRProvider};
3068        use std::sync::Arc;
3069
3070        // Load PR generation config from environment or use default
3071        let pr_config = mockforge_intelligence::pr_generation::PRGenerationConfig::from_env();
3072
3073        let generator = if pr_config.enabled && pr_config.token.is_some() {
3074            let token = pr_config.token.as_ref().unwrap().clone();
3075            let generator = match pr_config.provider {
3076                PRProvider::GitHub => PRGenerator::new_github(
3077                    pr_config.owner.clone(),
3078                    pr_config.repo.clone(),
3079                    token,
3080                    pr_config.base_branch.clone(),
3081                ),
3082                PRProvider::GitLab => PRGenerator::new_gitlab(
3083                    pr_config.owner.clone(),
3084                    pr_config.repo.clone(),
3085                    token,
3086                    pr_config.base_branch.clone(),
3087                ),
3088            };
3089            Some(Arc::new(generator))
3090        } else {
3091            None
3092        };
3093
3094        let pr_state = PRGenerationState {
3095            generator: generator.clone(),
3096        };
3097
3098        app = app.merge(pr_generation_router(pr_state));
3099        if generator.is_some() {
3100            debug!(
3101                "PR generation endpoints mounted at /api/v1/pr (configured for {:?})",
3102                pr_config.provider
3103            );
3104        } else {
3105            debug!("PR generation endpoints mounted at /api/v1/pr (not configured - set GITHUB_TOKEN/GITLAB_TOKEN and PR_REPO_OWNER/PR_REPO_NAME)");
3106        }
3107    }
3108
3109    // Add management WebSocket endpoint
3110    app = app.nest("/__mockforge/ws", ws_management_router(ws_state));
3111
3112    // Add workspace routing middleware if multi-tenant is enabled
3113    if let Some(mt_config) = multi_tenant_config {
3114        if mt_config.enabled {
3115            use mockforge_core::{MultiTenantWorkspaceRegistry, WorkspaceRouter};
3116            use std::sync::Arc;
3117
3118            info!(
3119                "Multi-tenant mode enabled with {} routing strategy",
3120                match mt_config.routing_strategy {
3121                    mockforge_foundation::multi_tenant_types::RoutingStrategy::Path => "path-based",
3122                    mockforge_foundation::multi_tenant_types::RoutingStrategy::Port => "port-based",
3123                    mockforge_foundation::multi_tenant_types::RoutingStrategy::Both => "hybrid",
3124                }
3125            );
3126
3127            // Create the multi-tenant workspace registry
3128            let mut registry = MultiTenantWorkspaceRegistry::new(mt_config.clone());
3129
3130            // Register the default workspace before wrapping in Arc
3131            let default_workspace =
3132                mockforge_core::Workspace::new(mt_config.default_workspace.clone());
3133            if let Err(e) =
3134                registry.register_workspace(mt_config.default_workspace.clone(), default_workspace)
3135            {
3136                warn!("Failed to register default workspace: {}", e);
3137            } else {
3138                info!("Registered default workspace: '{}'", mt_config.default_workspace);
3139            }
3140
3141            // Wrap registry in Arc for shared access
3142            let registry = Arc::new(registry);
3143
3144            // Create workspace router
3145            let _workspace_router = WorkspaceRouter::new(registry);
3146            info!("Workspace routing middleware initialized for HTTP server");
3147        }
3148    }
3149
3150    // Apply deceptive deploy configuration if enabled
3151    let mut final_cors_config = cors_config;
3152    let mut production_headers: Option<std::sync::Arc<HashMap<String, String>>> = None;
3153    // Auth config from deceptive deploy OAuth (if configured)
3154    let mut deceptive_deploy_auth_config: Option<mockforge_core::config::AuthConfig> = None;
3155    let mut rate_limit_config = middleware::RateLimitConfig {
3156        requests_per_minute: std::env::var("MOCKFORGE_RATE_LIMIT_RPM")
3157            .ok()
3158            .and_then(|v| v.parse().ok())
3159            .unwrap_or(1000),
3160        burst: std::env::var("MOCKFORGE_RATE_LIMIT_BURST")
3161            .ok()
3162            .and_then(|v| v.parse().ok())
3163            .unwrap_or(2000),
3164        per_ip: true,
3165        per_endpoint: false,
3166    };
3167
3168    if let Some(deploy_config) = &deceptive_deploy_config {
3169        if deploy_config.enabled {
3170            info!("Deceptive deploy mode enabled - applying production-like configuration");
3171
3172            // Override CORS config if provided
3173            if let Some(prod_cors) = &deploy_config.cors {
3174                final_cors_config = Some(mockforge_core::config::HttpCorsConfig {
3175                    enabled: true,
3176                    allowed_origins: prod_cors.allowed_origins.clone(),
3177                    allowed_methods: prod_cors.allowed_methods.clone(),
3178                    allowed_headers: prod_cors.allowed_headers.clone(),
3179                    allow_credentials: prod_cors.allow_credentials,
3180                });
3181                info!("Applied production-like CORS configuration");
3182            }
3183
3184            // Override rate limit config if provided
3185            if let Some(prod_rate_limit) = &deploy_config.rate_limit {
3186                rate_limit_config = middleware::RateLimitConfig {
3187                    requests_per_minute: prod_rate_limit.requests_per_minute,
3188                    burst: prod_rate_limit.burst,
3189                    per_ip: prod_rate_limit.per_ip,
3190                    per_endpoint: false,
3191                };
3192                info!(
3193                    "Applied production-like rate limiting: {} req/min, burst: {}",
3194                    prod_rate_limit.requests_per_minute, prod_rate_limit.burst
3195                );
3196            }
3197
3198            // Set production headers
3199            if !deploy_config.headers.is_empty() {
3200                let headers_map: HashMap<String, String> = deploy_config.headers.clone();
3201                production_headers = Some(std::sync::Arc::new(headers_map));
3202                info!("Configured {} production headers", deploy_config.headers.len());
3203            }
3204
3205            // Integrate OAuth config from deceptive deploy
3206            if let Some(prod_oauth) = &deploy_config.oauth {
3207                let oauth2_config: mockforge_core::config::OAuth2Config = prod_oauth.clone().into();
3208                deceptive_deploy_auth_config = Some(mockforge_core::config::AuthConfig {
3209                    oauth2: Some(oauth2_config),
3210                    ..Default::default()
3211                });
3212                info!("Applied production-like OAuth configuration for deceptive deploy");
3213            }
3214        }
3215    }
3216
3217    // Initialize rate limiter and state
3218    let rate_limit_disabled = middleware::is_rate_limit_disabled();
3219    let rate_limiter =
3220        std::sync::Arc::new(middleware::GlobalRateLimiter::new(rate_limit_config.clone()));
3221
3222    let mut state = HttpServerState::new();
3223    if rate_limit_disabled {
3224        info!(
3225            "HTTP rate limiting disabled (MOCKFORGE_RATE_LIMIT_ENABLED=false or --no-rate-limit)"
3226        );
3227    } else {
3228        state = state.with_rate_limiter(rate_limiter.clone());
3229    }
3230
3231    // Add production headers to state if configured
3232    if let Some(headers) = production_headers.clone() {
3233        state = state.with_production_headers(headers);
3234    }
3235
3236    // Add rate limiting middleware (no-op when state.rate_limiter is None)
3237    app = app.layer(from_fn_with_state(state.clone(), middleware::rate_limit_middleware));
3238
3239    // Add production headers middleware if configured
3240    if state.production_headers.is_some() {
3241        app =
3242            app.layer(from_fn_with_state(state.clone(), middleware::production_headers_middleware));
3243    }
3244
3245    // Optionally advertise `Connection: keep-alive` + `Keep-Alive: timeout=N,
3246    // max=M` on every response (Issue #79 workaround for HTTP/1.0 upstream
3247    // proxies that observe FIN from MockForge after each response). Opt in
3248    // via `MOCKFORGE_HTTP_KEEPALIVE_HINT=1`.
3249    if middleware::is_keepalive_hint_enabled() {
3250        info!(
3251            "MOCKFORGE_HTTP_KEEPALIVE_HINT enabled — emitting Connection: keep-alive + Keep-Alive headers on all responses (Issue #79 workaround)"
3252        );
3253        app = app.layer(axum::middleware::from_fn(middleware::keepalive_hint_middleware));
3254    }
3255
3256    // Issue #79 (round 5): per-request log line with HTTP version + Connection
3257    // header MockForge actually sees, so users debugging proxy ↔ MockForge
3258    // negotiation can confirm whether their proxy is speaking HTTP/1.1 with
3259    // keep-alive or HTTP/1.0 (which forces hyper to FIN after each response).
3260    if middleware::is_conn_log_enabled() {
3261        info!(
3262            "MOCKFORGE_HTTP_LOG_CONN enabled — logging HTTP version + Connection headers per request (Issue #79 diagnostic)"
3263        );
3264        app = app.layer(axum::middleware::from_fn(middleware::conn_diag_middleware));
3265    }
3266
3267    // Add authentication middleware if OAuth is configured via deceptive deploy
3268    if let Some(auth_config) = deceptive_deploy_auth_config {
3269        use crate::auth::{auth_middleware, create_oauth2_client, AuthState};
3270        use std::collections::HashMap;
3271        use std::sync::Arc;
3272        use tokio::sync::RwLock;
3273
3274        // Create OAuth2 client if configured
3275        let oauth2_client = if let Some(oauth2_config) = &auth_config.oauth2 {
3276            match create_oauth2_client(oauth2_config) {
3277                Ok(client) => Some(client),
3278                Err(e) => {
3279                    warn!("Failed to create OAuth2 client from deceptive deploy config: {}", e);
3280                    None
3281                }
3282            }
3283        } else {
3284            None
3285        };
3286
3287        // Create auth state
3288        let auth_state = AuthState {
3289            config: auth_config,
3290            spec: None, // OpenAPI spec not available in this context
3291            oauth2_client,
3292            introspection_cache: Arc::new(RwLock::new(HashMap::new())),
3293        };
3294
3295        // Apply auth middleware
3296        app = app.layer(from_fn_with_state(auth_state, auth_middleware));
3297        info!("Applied OAuth authentication middleware from deceptive deploy configuration");
3298    }
3299
3300    // Add runtime daemon 404 detection middleware (if enabled)
3301    #[cfg(feature = "runtime-daemon")]
3302    {
3303        use mockforge_runtime_daemon::{AutoGenerator, NotFoundDetector, RuntimeDaemonConfig};
3304        use std::sync::Arc;
3305
3306        // Load runtime daemon config from environment
3307        let daemon_config = RuntimeDaemonConfig::from_env();
3308
3309        if daemon_config.enabled {
3310            info!("Runtime daemon enabled - auto-creating mocks from 404s");
3311
3312            // Determine management API URL (default to localhost:3000)
3313            let management_api_url =
3314                std::env::var("MOCKFORGE_MANAGEMENT_API_URL").unwrap_or_else(|_| {
3315                    let port =
3316                        std::env::var("MOCKFORGE_HTTP_PORT").unwrap_or_else(|_| "3000".to_string());
3317                    format!("http://localhost:{}", port)
3318                });
3319
3320            // Create auto-generator
3321            let generator = Arc::new(AutoGenerator::new(daemon_config.clone(), management_api_url));
3322
3323            // Create detector and set generator
3324            let detector = NotFoundDetector::new(daemon_config.clone());
3325            detector.set_generator(generator).await;
3326
3327            // Add middleware layer
3328            let detector_clone = detector.clone();
3329            app = app.layer(axum::middleware::from_fn(
3330                move |req: axum::extract::Request, next: axum::middleware::Next| {
3331                    let detector = detector_clone.clone();
3332                    async move { detector.detect_and_auto_create(req, next).await }
3333                },
3334            ));
3335
3336            debug!("Runtime daemon 404 detection middleware added");
3337        }
3338    }
3339
3340    // Add /__mockforge/routes endpoint so the admin UI can discover registered routes
3341    {
3342        let routes_state = HttpServerState::with_routes(captured_routes);
3343        let routes_router = Router::new()
3344            .route("/__mockforge/routes", axum::routing::get(get_routes_handler))
3345            .with_state(routes_state);
3346        app = app.merge(routes_router);
3347    }
3348
3349    // Add API docs page (Scalar-powered interactive explorer)
3350    app = app.route("/__mockforge/docs", axum::routing::get(get_docs_handler));
3351
3352    // Note: OData URI rewrite is applied at the service level in serve_router_with_tls()
3353    // because Router::layer() only applies to matched routes, not unmatched ones.
3354
3355    // Add request logging middleware to capture all requests for the admin dashboard
3356    app = app.layer(axum::middleware::from_fn(request_logging::log_http_requests));
3357
3358    // Add contract diff middleware for automatic request capture
3359    // This captures requests for contract diff analysis
3360    app = app.layer(axum::middleware::from_fn(contract_diff_middleware::capture_for_contract_diff));
3361
3362    // Add CORS middleware (use final_cors_config which may be overridden by deceptive deploy)
3363    app = apply_cors_middleware(app, final_cors_config);
3364
3365    app
3366}
3367
3368// Note: start_with_traffic_shaping function removed due to compilation issues
3369// Use build_router_with_traffic_shaping_and_multi_tenant directly instead
3370
3371#[test]
3372fn test_route_info_clone() {
3373    let route = RouteInfo {
3374        method: "POST".to_string(),
3375        path: "/users".to_string(),
3376        operation_id: Some("createUser".to_string()),
3377        summary: None,
3378        description: None,
3379        parameters: vec![],
3380    };
3381
3382    let cloned = route.clone();
3383    assert_eq!(route.method, cloned.method);
3384    assert_eq!(route.path, cloned.path);
3385    assert_eq!(route.operation_id, cloned.operation_id);
3386}
3387
3388#[test]
3389fn test_http_server_state_new() {
3390    let state = HttpServerState::new();
3391    assert_eq!(state.routes.len(), 0);
3392}
3393
3394#[test]
3395fn test_http_server_state_with_routes() {
3396    let routes = vec![
3397        RouteInfo {
3398            method: "GET".to_string(),
3399            path: "/users".to_string(),
3400            operation_id: Some("getUsers".to_string()),
3401            summary: None,
3402            description: None,
3403            parameters: vec![],
3404        },
3405        RouteInfo {
3406            method: "POST".to_string(),
3407            path: "/users".to_string(),
3408            operation_id: Some("createUser".to_string()),
3409            summary: None,
3410            description: None,
3411            parameters: vec![],
3412        },
3413    ];
3414
3415    let state = HttpServerState::with_routes(routes.clone());
3416    assert_eq!(state.routes.len(), 2);
3417    assert_eq!(state.routes[0].method, "GET");
3418    assert_eq!(state.routes[1].method, "POST");
3419}
3420
3421#[test]
3422fn test_http_server_state_clone() {
3423    let routes = vec![RouteInfo {
3424        method: "GET".to_string(),
3425        path: "/test".to_string(),
3426        operation_id: None,
3427        summary: None,
3428        description: None,
3429        parameters: vec![],
3430    }];
3431
3432    let state = HttpServerState::with_routes(routes);
3433    let cloned = state.clone();
3434
3435    assert_eq!(state.routes.len(), cloned.routes.len());
3436    assert_eq!(state.routes[0].method, cloned.routes[0].method);
3437}
3438
3439#[tokio::test]
3440async fn test_build_router_without_openapi() {
3441    let _router = build_router(None, None, None).await;
3442    // Should succeed without OpenAPI spec
3443}
3444
3445#[tokio::test]
3446async fn test_build_router_with_nonexistent_spec() {
3447    let _router = build_router(Some("/nonexistent/spec.yaml".to_string()), None, None).await;
3448    // Should succeed but log a warning
3449}
3450
3451#[tokio::test]
3452async fn test_build_router_with_auth_and_latency() {
3453    let _router = build_router_with_auth_and_latency(None, None, None, None).await;
3454    // Should succeed without parameters
3455}
3456
3457#[tokio::test]
3458async fn test_build_router_with_latency() {
3459    let _router = build_router_with_latency(None, None, None).await;
3460    // Should succeed without parameters
3461}
3462
3463#[tokio::test]
3464async fn test_build_router_with_auth() {
3465    let _router = build_router_with_auth(None, None, None).await;
3466    // Should succeed without parameters
3467}
3468
3469#[tokio::test]
3470async fn test_build_router_with_chains() {
3471    let _router = build_router_with_chains(None, None, None).await;
3472    // Should succeed without parameters
3473}
3474
3475#[test]
3476fn test_route_info_with_all_fields() {
3477    let route = RouteInfo {
3478        method: "PUT".to_string(),
3479        path: "/users/{id}".to_string(),
3480        operation_id: Some("updateUser".to_string()),
3481        summary: Some("Update user".to_string()),
3482        description: Some("Updates an existing user".to_string()),
3483        parameters: vec!["id".to_string(), "body".to_string()],
3484    };
3485
3486    assert!(route.operation_id.is_some());
3487    assert!(route.summary.is_some());
3488    assert!(route.description.is_some());
3489    assert_eq!(route.parameters.len(), 2);
3490}
3491
3492#[test]
3493fn test_route_info_with_minimal_fields() {
3494    let route = RouteInfo {
3495        method: "DELETE".to_string(),
3496        path: "/users/{id}".to_string(),
3497        operation_id: None,
3498        summary: None,
3499        description: None,
3500        parameters: vec![],
3501    };
3502
3503    assert!(route.operation_id.is_none());
3504    assert!(route.summary.is_none());
3505    assert!(route.description.is_none());
3506    assert_eq!(route.parameters.len(), 0);
3507}
3508
3509#[test]
3510fn test_http_server_state_empty_routes() {
3511    let state = HttpServerState::with_routes(vec![]);
3512    assert_eq!(state.routes.len(), 0);
3513}
3514
3515#[test]
3516fn test_http_server_state_multiple_routes() {
3517    let routes = vec![
3518        RouteInfo {
3519            method: "GET".to_string(),
3520            path: "/users".to_string(),
3521            operation_id: Some("listUsers".to_string()),
3522            summary: Some("List all users".to_string()),
3523            description: None,
3524            parameters: vec![],
3525        },
3526        RouteInfo {
3527            method: "GET".to_string(),
3528            path: "/users/{id}".to_string(),
3529            operation_id: Some("getUser".to_string()),
3530            summary: Some("Get a user".to_string()),
3531            description: None,
3532            parameters: vec!["id".to_string()],
3533        },
3534        RouteInfo {
3535            method: "POST".to_string(),
3536            path: "/users".to_string(),
3537            operation_id: Some("createUser".to_string()),
3538            summary: Some("Create a user".to_string()),
3539            description: None,
3540            parameters: vec!["body".to_string()],
3541        },
3542    ];
3543
3544    let state = HttpServerState::with_routes(routes);
3545    assert_eq!(state.routes.len(), 3);
3546
3547    // Verify different HTTP methods
3548    let methods: Vec<&str> = state.routes.iter().map(|r| r.method.as_str()).collect();
3549    assert!(methods.contains(&"GET"));
3550    assert!(methods.contains(&"POST"));
3551}
3552
3553#[test]
3554fn test_http_server_state_with_rate_limiter() {
3555    use std::sync::Arc;
3556
3557    let config = middleware::RateLimitConfig::default();
3558    let rate_limiter = Arc::new(middleware::GlobalRateLimiter::new(config));
3559
3560    let state = HttpServerState::new().with_rate_limiter(rate_limiter);
3561
3562    assert!(state.rate_limiter.is_some());
3563    assert_eq!(state.routes.len(), 0);
3564}
3565
3566#[tokio::test]
3567async fn test_build_router_includes_rate_limiter() {
3568    let _router = build_router(None, None, None).await;
3569    // Router should be created successfully with rate limiter initialized
3570}