refactor: remove session auth, add DbType enum, native async broker

- Remove `auth`, `sessions-db` features and all tower-sessions/tower-sessions-sqlx-store deps
- Delete `src/session.rs` (InfraSessionStore)
- `ServerConfig`: remove `session_secret` field and `attach_session_layer()` helper
- `db.rs`: add explicit `DbType` enum to `DatabaseConfig`; `connect()` now matches on
  `db_type` instead of sniffing URL prefixes; remove `connect_sqlite()` standalone fn
- `broker/mod.rs`: replace `#[async_trait]` with native async fn (Rust 2024 AFIT)
- `broker/nats.rs`: remove `#[async_trait]` to match trait definition
- `ai/embeddings.rs`: remove sync `generate_embedding()` (wasteful thread spawn);
  keep only `generate_embedding_async()` as the public API
- Default features: `["logging", "db-sqlx", "auth", "http"]` → `["logging"]`
- Bump version to 0.1.11

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 01:36:52 +01:00
parent 7a72f5f54a
commit 0ea9aa7870
9 changed files with 53 additions and 296 deletions

121
Cargo.lock generated
View File

@@ -533,17 +533,6 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -1775,7 +1764,7 @@ dependencies = [
[[package]]
name = "k-core"
version = "0.1.10"
version = "0.1.11"
dependencies = [
"anyhow",
"async-nats",
@@ -1789,12 +1778,9 @@ dependencies = [
"serde",
"sqlx",
"thiserror 2.0.17",
"time",
"tokio",
"tower 0.5.2",
"tower-http",
"tower-sessions",
"tower-sessions-sqlx-store",
"tracing",
"tracing-subscriber",
"uuid",
@@ -1878,7 +1864,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
"serde",
]
[[package]]
@@ -2958,25 +2943,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rmp"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
dependencies = [
"num-traits",
]
[[package]]
name = "rmp-serde"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
dependencies = [
"rmp",
"serde",
]
[[package]]
name = "rsa"
version = "0.9.9"
@@ -3479,7 +3445,6 @@ dependencies = [
"sha2",
"smallvec 1.15.1",
"thiserror 2.0.17",
"time",
"tokio",
"tokio-stream",
"tracing",
@@ -3564,7 +3529,6 @@ dependencies = [
"sqlx-core",
"stringprep",
"thiserror 2.0.17",
"time",
"tracing",
"uuid",
"whoami",
@@ -3604,7 +3568,6 @@ dependencies = [
"sqlx-core",
"stringprep",
"thiserror 2.0.17",
"time",
"tracing",
"uuid",
"whoami",
@@ -3631,7 +3594,6 @@ dependencies = [
"serde_urlencoded",
"sqlx-core",
"thiserror 2.0.17",
"time",
"tracing",
"url",
"uuid",
@@ -4063,22 +4025,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-cookies"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36"
dependencies = [
"axum-core 0.5.6",
"cookie",
"futures-util",
"http",
"parking_lot",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.6.8"
@@ -4110,71 +4056,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tower-sessions"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3"
dependencies = [
"async-trait",
"http",
"time",
"tokio",
"tower-cookies",
"tower-layer",
"tower-service",
"tower-sessions-core",
"tower-sessions-memory-store",
"tracing",
]
[[package]]
name = "tower-sessions-core"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b"
dependencies = [
"async-trait",
"axum-core 0.5.6",
"base64 0.22.1",
"futures",
"http",
"parking_lot",
"rand 0.8.5",
"serde",
"serde_json",
"thiserror 2.0.17",
"time",
"tokio",
"tracing",
]
[[package]]
name = "tower-sessions-memory-store"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242"
dependencies = [
"async-trait",
"time",
"tokio",
"tower-sessions-core",
]
[[package]]
name = "tower-sessions-sqlx-store"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e054622079f57fc1a7d6a6089c9334f963d62028fe21dc9eddd58af9a78480b3"
dependencies = [
"async-trait",
"rmp-serde",
"sqlx",
"thiserror 1.0.69",
"time",
"tower-sessions-core",
]
[[package]]
name = "tracing"
version = "0.1.44"

View File

@@ -1,20 +1,18 @@
[package]
name = "k-core"
version = "0.1.10"
version = "0.1.11"
edition = "2024"
[features]
default = ["logging", "db-sqlx", "auth", "http"]
default = ["logging"]
logging = ["dep:tracing", "dep:tracing-subscriber"]
db-sqlx = ["dep:sqlx"]
postgres = ["db-sqlx", "sqlx/postgres", "tower-sessions-sqlx-store?/postgres"]
sqlite = ["db-sqlx", "sqlx/sqlite", "tower-sessions-sqlx-store?/sqlite"]
auth = ["dep:tower-sessions"]
sessions-db = ["auth", "dep:tower-sessions-sqlx-store"]
postgres = ["db-sqlx", "sqlx/postgres"]
sqlite = ["db-sqlx", "sqlx/sqlite"]
ai = ["dep:fastembed", "dep:qdrant-client"]
broker = []
broker-nats = ["broker", "dep:async-nats", "dep:futures-util", "dep:futures-core"]
http = ["dep:axum", "dep:tower", "dep:tower-http", "dep:time", "logging"]
http = ["dep:axum", "dep:tower", "dep:tower-http", "logging"]
[dependencies]
# Error handling
@@ -22,7 +20,7 @@ thiserror = "2.0.17"
anyhow = "1.0.100"
# Async
tokio = { version = "1.48.0", features = ["full"]}
tokio = { version = "1.48.0", features = ["full"] }
# Database
sqlx = { version = "0.8.6", features = [
@@ -39,10 +37,6 @@ tracing-subscriber = { version = "0.3.22", features = [
"fmt",
], optional = true }
# Auth
tower-sessions = { version = "0.14.0", optional = true }
tower-sessions-sqlx-store = { version = "0.15", optional = true}
# Utils
chrono = { version = "0.4.42", features = ["serde"] }
uuid = { version = "1.19.0", features = ["v4", "serde"] }
@@ -59,8 +53,7 @@ async-nats = { version = "0.45", optional = true }
# HTTP
axum = { version = "0.8.8", features = ["macros"], optional = true }
tower = { version = "0.5.2", optional = true }
tower-http = { version = "0.6.2", features = ["cors", "trace"], optional = true}
time = { version = "0.3", optional = true }
tower-http = { version = "0.6.2", features = ["cors", "trace"], optional = true }
futures-util = { version = "0.3", optional = true }
futures-core = {version = "0.3", optional = true }
futures-core = { version = "0.3", optional = true }

View File

@@ -20,33 +20,7 @@ impl FastEmbedAdapter {
})
}
pub fn generate_embedding(&self, text: &str) -> anyhow::Result<Vec<f32>> {
let model = self.model.clone();
let text = text.to_string();
// FastEmbed is blocking, so we run it in a blocking task if we are in an async context,
// but since this method signature doesn't force async, we wrap the internal logic.
// For strictly async usage in k-core:
let embeddings = std::thread::scope(|s| {
s.spawn(|| {
let mut model = model
.lock()
.map_err(|e| anyhow::anyhow!("Lock error: {}", e))?;
model
.embed(vec![text], None)
.map_err(|e| anyhow::anyhow!("Embed error: {}", e))
})
.join()
.map_err(|_| anyhow::anyhow!("Thread join error"))?
})?;
embeddings
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("No embedding generated"))
}
/// Async wrapper for use in async contexts (like Axum handlers)
/// Generate an embedding asynchronously using a blocking thread pool.
pub async fn generate_embedding_async(&self, text: &str) -> anyhow::Result<Vec<f32>> {
let model = self.model.clone();
let text = text.to_string();

View File

@@ -1,11 +1,9 @@
use async_trait::async_trait;
use futures_core::Stream;
use std::pin::Pin;
#[cfg(feature = "broker-nats")]
pub mod nats;
#[async_trait]
pub trait MessageBroker: Send + Sync {
async fn publish(&self, topic: &str, payload: Vec<u8>) -> anyhow::Result<()>;

View File

@@ -1,5 +1,4 @@
use super::MessageBroker;
use async_trait::async_trait;
use futures_util::StreamExt;
use std::pin::Pin;
@@ -15,7 +14,6 @@ impl NatsBroker {
}
}
#[async_trait]
impl MessageBroker for NatsBroker {
async fn publish(&self, topic: &str, payload: Vec<u8>) -> anyhow::Result<()> {
self.client
@@ -35,7 +33,6 @@ impl MessageBroker for NatsBroker {
.await
.map_err(|e| anyhow::anyhow!("NATS subscribe error: {}", e))?;
// Map NATS Message to generic Vec<u8>
let stream = subscriber.map(|msg| msg.payload.to_vec());
Ok(Box::pin(stream))

View File

@@ -8,9 +8,22 @@ use sqlx::Sqlite;
#[cfg(feature = "postgres")]
use sqlx::Postgres;
/// Explicitly declares which database engine to connect to.
///
/// Using an explicit type avoids URL-prefix sniffing in `connect()`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DbType {
#[default]
#[cfg_attr(not(feature = "sqlite"), allow(dead_code))]
Sqlite,
#[cfg_attr(not(feature = "postgres"), allow(dead_code))]
Postgres,
}
/// Universal Database Configuration
#[derive(Debug, Clone)]
pub struct DatabaseConfig {
pub db_type: DbType,
pub url: String,
pub max_connections: u32,
pub min_connections: u32,
@@ -22,6 +35,7 @@ impl Default for DatabaseConfig {
#[cfg(feature = "sqlite")]
{
Self {
db_type: DbType::Sqlite,
url: "sqlite::memory:".to_string(),
max_connections: 5,
min_connections: 1,
@@ -32,6 +46,7 @@ impl Default for DatabaseConfig {
#[cfg(all(not(feature = "sqlite"), feature = "postgres"))]
{
Self {
db_type: DbType::Postgres,
url: "postgres://localhost:5432/mydb".to_string(),
max_connections: 5,
min_connections: 1,
@@ -41,6 +56,7 @@ impl Default for DatabaseConfig {
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
Self {
db_type: DbType::Sqlite,
url: "".to_string(),
max_connections: 5,
min_connections: 1,
@@ -50,8 +66,9 @@ impl Default for DatabaseConfig {
}
impl DatabaseConfig {
pub fn new(url: impl Into<String>) -> Self {
pub fn new(db_type: DbType, url: impl Into<String>) -> Self {
Self {
db_type,
url: url.into(),
..Default::default()
}
@@ -60,8 +77,9 @@ impl DatabaseConfig {
#[cfg(feature = "sqlite")]
pub fn in_memory() -> Self {
Self {
db_type: DbType::Sqlite,
url: "sqlite::memory:".to_string(),
max_connections: 1, // SQLite in-memory is single-connection
max_connections: 1,
min_connections: 1,
..Default::default()
}
@@ -69,7 +87,6 @@ impl DatabaseConfig {
}
/// A wrapper around various DB pools.
/// The Template uses this type so it doesn't care if it's Sqlite or Postgres.
#[derive(Clone, Debug)]
pub enum DatabasePool {
#[cfg(feature = "sqlite")]
@@ -78,36 +95,33 @@ pub enum DatabasePool {
Postgres(Pool<Postgres>),
}
/// The single entry point for connecting to any DB.
/// Connect to a database using the explicit `db_type` from `DatabaseConfig`.
pub async fn connect(config: &DatabaseConfig) -> Result<DatabasePool, sqlx::Error> {
// 1. Try Postgres if the feature is enabled AND the URL looks like postgres
match config.db_type {
#[cfg(feature = "postgres")]
if config.url.starts_with("postgres://") || config.url.starts_with("postgresql://") {
DbType::Postgres => {
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(config.max_connections)
.acquire_timeout(config.acquire_timeout)
.connect(&config.url)
.await?;
return Ok(DatabasePool::Postgres(pool));
Ok(DatabasePool::Postgres(pool))
}
// 2. Fallback to Sqlite if the feature is enabled
#[cfg(feature = "sqlite")]
{
DbType::Sqlite => {
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(config.max_connections)
.acquire_timeout(config.acquire_timeout)
.connect(&config.url)
.await?;
Ok(DatabasePool::Sqlite(pool))
}
#[cfg(not(feature = "sqlite"))]
{
Err(sqlx::Error::Configuration(
#[allow(unreachable_patterns)]
_ => Err(sqlx::Error::Configuration(
"No supported database features enabled".into(),
))
)),
}
}
@@ -130,7 +144,3 @@ impl DatabasePool {
}
}
}
#[cfg(feature = "sqlite")]
pub async fn connect_sqlite(url: &str) -> Result<Pool<Sqlite>, sqlx::Error> {
sqlx::sqlite::SqlitePoolOptions::new().connect(url).await
}

View File

@@ -1,11 +1,9 @@
use axum::Router;
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
use tower_sessions::{Expiry, SessionManagerLayer, SessionStore};
pub struct ServerConfig {
pub cors_origins: Vec<String>,
pub session_secret: Option<String>,
}
pub fn apply_standard_middleware(app: Router, config: &ServerConfig) -> Router {
@@ -24,7 +22,6 @@ pub fn apply_standard_middleware(app: Router, config: &ServerConfig) -> Router {
])
.allow_credentials(true);
// Configure CORS origins
let mut allowed_origins = Vec::new();
for origin in &config.cors_origins {
if let Ok(value) = origin.parse::<axum::http::HeaderValue>() {
@@ -38,15 +35,3 @@ pub fn apply_standard_middleware(app: Router, config: &ServerConfig) -> Router {
app.layer(cors).layer(TraceLayer::new_for_http())
}
/// Helper to attach a session layer with standard K-Suite configuration
pub fn attach_session_layer<S>(app: Router, store: S) -> Router
where
S: SessionStore + Clone + Send + Sync + 'static,
{
let session_layer = SessionManagerLayer::new(store)
.with_secure(false) // Set to true if you handle HTTPS termination in the app
.with_expiry(Expiry::OnInactivity(time::Duration::days(7)));
app.layer(session_layer)
}

View File

@@ -11,8 +11,6 @@ pub mod ai;
#[cfg(feature = "broker")]
pub mod broker;
#[cfg(feature = "auth")]
pub mod session;
#[cfg(feature = "http")]
pub mod http;

View File

@@ -1,79 +0,0 @@
// Only compile this module if we have the store dependency AND sqlx enabled
#[cfg(all(feature = "sessions-db", feature = "db-sqlx"))]
pub mod store {
use async_trait::async_trait;
use tower_sessions::{
SessionStore,
session::{Id, Record},
session_store,
};
// We only import what is actually enabled by features
#[cfg(feature = "postgres")]
use tower_sessions_sqlx_store::PostgresStore;
#[cfg(feature = "sqlite")]
use tower_sessions_sqlx_store::SqliteStore;
#[derive(Clone, Debug)]
pub enum InfraSessionStore {
#[cfg(feature = "sqlite")]
Sqlite(SqliteStore),
#[cfg(feature = "postgres")]
Postgres(PostgresStore),
// Fallback variant to allow compilation if sessions-db is enabled but no generic DB feature is selected
// (Though in practice you should ensure your config validates this)
#[allow(dead_code)]
Unused,
}
#[async_trait]
impl SessionStore for InfraSessionStore {
async fn save(&self, session_record: &Record) -> session_store::Result<()> {
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite(store) => store.save(session_record).await,
#[cfg(feature = "postgres")]
Self::Postgres(store) => store.save(session_record).await,
_ => Err(session_store::Error::Backend(
"No database feature enabled".to_string(),
)),
}
}
async fn load(&self, session_id: &Id) -> session_store::Result<Option<Record>> {
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite(store) => store.load(session_id).await,
#[cfg(feature = "postgres")]
Self::Postgres(store) => store.load(session_id).await,
_ => Err(session_store::Error::Backend(
"No database feature enabled".to_string(),
)),
}
}
async fn delete(&self, session_id: &Id) -> session_store::Result<()> {
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite(store) => store.delete(session_id).await,
#[cfg(feature = "postgres")]
Self::Postgres(store) => store.delete(session_id).await,
_ => Err(session_store::Error::Backend(
"No database feature enabled".to_string(),
)),
}
}
}
impl InfraSessionStore {
pub async fn migrate(&self) -> anyhow::Result<()> {
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite(store) => store.migrate().await.map_err(|e| anyhow::anyhow!(e)),
#[cfg(feature = "postgres")]
Self::Postgres(store) => store.migrate().await.map_err(|e| anyhow::anyhow!(e)),
_ => Ok(()), // No migration needed for no-op
}
}
}
}