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