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