Skip to main content

mockforge_http/
lib.rs

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