use crate::runtime::DaemonRuntime;
use crate::server::errors::error;
use crate::server::routes::deploy_providers::oauth::ProviderLogins;
use axum::extract::{Request, State};
use axum::http::{HeaderMap, StatusCode};
use axum::middleware::Next;
use axum::response::Response;
use nomoreide_core::agent_profiles::auth::AuthStates;
use nomoreide_core::approval_broker::ApprovalBroker;
use nomoreide_core::config::ConfigStore;
use nomoreide_core::error_inbox::ErrorInbox;
use nomoreide_core::event_sink::{EventSink, EventSinkError, SharedEventSink};
use nomoreide_core::metrics_store::MetricsStore;
use nomoreide_core::terminal::TerminalManager;
use nomoreide_core::test_runner::TestRunner;
use nomoreide_core::tool_call_store::ToolCallStore;
use nomoreide_core::usage_history::UsageHistory;
use serde_json::Value;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use subtle::ConstantTimeEq;
use tokio::sync::broadcast;
use tokio::sync::mpsc;
#[derive(Clone)]
pub(crate) struct AppState {
pub(crate) credential: String,
pub(crate) owner_id: String,
pub(crate) config_store: ConfigStore,
pub(crate) runtime: Arc<DaemonRuntime>,
pub(crate) errors: ErrorInbox,
pub(crate) shutdown: mpsc::Sender<crate::server::ShutdownRequest>,
pub(crate) terminal: TerminalManager,
pub(crate) events: SharedEventSink,
pub(crate) event_stream: broadcast::Sender<RuntimeEvent>,
pub(crate) session_counter: Arc<AtomicU64>,
pub(crate) tool_calls: ToolCallStore,
pub(crate) tests: TestRunner,
pub(crate) metrics: MetricsStore,
pub(crate) usage_history: Arc<UsageHistory>,
pub(crate) approvals: ApprovalBroker,
pub(crate) registry_auth: AuthStates,
pub(crate) provider_logins: ProviderLogins,
pub(crate) relay: crate::remote::supervisor::RelaySupervisor,
pub(crate) pending_pairing: crate::server::routes::remote::PendingPairing,
}
#[derive(Clone, Debug)]
pub(crate) struct RuntimeEvent {
pub(crate) name: String,
pub(crate) payload: Value,
}
pub(crate) const EVENT_BACKLOG: usize = 256;
pub(crate) struct BroadcastEventSink {
events: broadcast::Sender<RuntimeEvent>,
}
impl BroadcastEventSink {
pub(crate) fn new(events: broadcast::Sender<RuntimeEvent>) -> Self {
Self { events }
}
}
impl EventSink for BroadcastEventSink {
fn emit(&self, event: &str, payload: Value) -> Result<(), EventSinkError> {
let _ = self.events.send(RuntimeEvent {
name: event.to_string(),
payload,
});
Ok(())
}
}
impl AppState {
pub(crate) async fn workspace_cwd(&self) -> String {
let fallback = std::env::current_dir()
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_default();
let Ok(config) = self.config_store.load().await else {
return fallback;
};
nomoreide_core::config::selected_git_cwd(&config, &fallback).await
}
pub(crate) fn next_session_id(&self) -> String {
format!(
"term_{}",
self.session_counter.fetch_add(1, Ordering::Relaxed) + 1
)
}
}
pub(crate) async fn require_credential(
State(state): State<AppState>,
request: Request,
next: Next,
) -> Response {
if !authorized(request.headers(), &state.credential) {
return error(StatusCode::UNAUTHORIZED, "Authentication required.");
}
next.run(request).await
}
fn authorized(headers: &HeaderMap, credential: &str) -> bool {
let bearer = headers
.get(axum::http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "));
let websocket = headers
.get(axum::http::header::SEC_WEBSOCKET_PROTOCOL)
.and_then(|value| value.to_str().ok())
.and_then(|value| {
value
.split(',')
.map(str::trim)
.find_map(|protocol| protocol.strip_prefix("nomoreide-bearer."))
});
bearer
.into_iter()
.chain(websocket)
.any(|candidate| bool::from(candidate.as_bytes().ct_eq(credential.as_bytes())))
}
#[cfg(test)]
mod tests {
use super::authorized;
use axum::http::{header, HeaderMap, HeaderValue};
#[test]
fn accepts_bearer_headers_and_websocket_subprotocols() {
let mut bearer = HeaderMap::new();
bearer.insert(
header::AUTHORIZATION,
HeaderValue::from_static("Bearer secret"),
);
assert!(authorized(&bearer, "secret"));
let mut websocket = HeaderMap::new();
websocket.insert(
header::SEC_WEBSOCKET_PROTOCOL,
HeaderValue::from_static("nomoreide, nomoreide-bearer.secret"),
);
assert!(authorized(&websocket, "secret"));
assert!(!authorized(&websocket, "different"));
}
}