use std::sync::Arc;
pub mod config;
pub mod embedded;
pub mod engine_api;
pub mod init;
pub mod server;
pub mod state;
pub use assay_auth as auth;
pub use assay_dashboard as dashboard;
pub use assay_domain as core;
pub use assay_workflow as workflow;
pub use config::{
AuthConfig, AuthOidcProviderConfig, AuthPasskeyConfig, AuthSessionConfig, BackendConfig,
DashboardConfig, EngineConfig, ServerConfig,
};
pub use state::{AdminApiKeys, EngineState};
pub async fn run(cfg: EngineConfig) -> anyhow::Result<()> {
let bind_addr = cfg.server.bind_addr.clone();
let engine = embedded::build(cfg).await?;
server::bind_and_serve(&bind_addr, engine.router).await
}
#[cfg(all(feature = "vault", feature = "backend-postgres"))]
async fn build_vault_ctx_pg(
modules: &[String],
pool: &sqlx::PgPool,
) -> anyhow::Result<Option<assay_vault::VaultCtx>> {
if !modules.iter().any(|m| m == "vault") {
return Ok(None);
}
let kek = assay_vault::crypto::kek_store::load_or_init_postgres(pool)
.await
.map_err(|e| anyhow::anyhow!("vault KEK bootstrap (pg): {e}"))?;
let mut ctx = assay_vault::VaultCtx::new()
.with_kek(kek)
.with_kv(assay_vault::store::postgres::PgKvStore::new(pool.clone()))
.with_transit(assay_vault::store::postgres::PgTransitStore::new(
pool.clone(),
));
#[cfg(feature = "vault-sealing-shamir")]
{
ctx = ctx.with_seal_store(assay_vault::store::postgres::PgSealStore::new(pool.clone()));
}
#[cfg(feature = "vault-collections")]
{
ctx = ctx
.with_personal_vaults(assay_vault::store::postgres::PgPersonalVaultStore::new(
pool.clone(),
))
.with_collections(assay_vault::store::postgres::PgCollectionStore::new(
pool.clone(),
))
.with_items(assay_vault::store::postgres::PgItemStore::new(pool.clone()))
.with_folders(assay_vault::store::postgres::PgFolderStore::new(
pool.clone(),
));
}
#[cfg(feature = "vault-share")]
{
let kp = assay_vault::store::postgres::load_or_init_biscuit_root_postgres(pool)
.await
.map_err(|e| anyhow::anyhow!("vault biscuit root bootstrap (pg): {e}"))?;
let revs = std::sync::Arc::new(assay_vault::store::postgres::PgRevocationStore::new(
pool.clone(),
));
let svc = assay_vault::share::ShareService::new(kp, revs);
ctx = ctx.with_share(svc);
}
#[cfg(feature = "vault-dynamic-postgres")]
{
let leases = std::sync::Arc::new(assay_vault::store::postgres::PgLeaseStore::new(
pool.clone(),
));
let registry = assay_vault::dynamic::DynamicCredsRegistry::new();
let svc = assay_vault::dynamic::DynamicCredsService::new(registry, leases);
ctx = ctx.with_dynamic(svc);
}
Ok(Some(ctx))
}
#[cfg(all(feature = "vault", feature = "backend-sqlite"))]
async fn build_vault_ctx_sqlite(
modules: &[String],
pool: &sqlx::SqlitePool,
) -> anyhow::Result<Option<assay_vault::VaultCtx>> {
if !modules.iter().any(|m| m == "vault") {
return Ok(None);
}
let kek = assay_vault::crypto::kek_store::load_or_init_sqlite(pool)
.await
.map_err(|e| anyhow::anyhow!("vault KEK bootstrap (sqlite): {e}"))?;
let mut ctx = assay_vault::VaultCtx::new()
.with_kek(kek)
.with_kv(assay_vault::store::sqlite::SqliteKvStore::new(pool.clone()))
.with_transit(assay_vault::store::sqlite::SqliteTransitStore::new(
pool.clone(),
));
#[cfg(feature = "vault-sealing-shamir")]
{
ctx = ctx.with_seal_store(assay_vault::store::sqlite::SqliteSealStore::new(
pool.clone(),
));
}
#[cfg(feature = "vault-collections")]
{
ctx = ctx
.with_personal_vaults(assay_vault::store::sqlite::SqlitePersonalVaultStore::new(
pool.clone(),
))
.with_collections(assay_vault::store::sqlite::SqliteCollectionStore::new(
pool.clone(),
))
.with_items(assay_vault::store::sqlite::SqliteItemStore::new(
pool.clone(),
))
.with_folders(assay_vault::store::sqlite::SqliteFolderStore::new(
pool.clone(),
));
}
#[cfg(feature = "vault-share")]
{
let kp = assay_vault::store::sqlite::load_or_init_biscuit_root_sqlite(pool)
.await
.map_err(|e| anyhow::anyhow!("vault biscuit root bootstrap (sqlite): {e}"))?;
let revs = std::sync::Arc::new(assay_vault::store::sqlite::SqliteRevocationStore::new(
pool.clone(),
));
let svc = assay_vault::share::ShareService::new(kp, revs);
ctx = ctx.with_share(svc);
}
#[cfg(feature = "vault-dynamic-postgres")]
{
let leases = std::sync::Arc::new(assay_vault::store::sqlite::SqliteLeaseStore::new(
pool.clone(),
));
let registry = assay_vault::dynamic::DynamicCredsRegistry::new();
let svc = assay_vault::dynamic::DynamicCredsService::new(registry, leases);
ctx = ctx.with_dynamic(svc);
}
Ok(Some(ctx))
}
#[cfg(feature = "backend-postgres")]
async fn build_auth_ctx_pg(
cfg: &EngineConfig,
pool: &sqlx::PgPool,
) -> anyhow::Result<assay_auth::AuthCtx> {
use assay_auth::store::{PostgresSessionStore, PostgresUserStore};
let users = PostgresUserStore::new(pool.clone()).into_dyn();
let sessions = PostgresSessionStore::new(pool.clone()).into_dyn();
let mut ctx = assay_auth::AuthCtx::new(users.clone(), sessions);
let biscuit = assay_auth::biscuit::load_or_init_postgres(pool)
.await
.map_err(|e| anyhow::anyhow!("biscuit root key (pg): {e}"))?;
ctx = ctx.with_biscuit(biscuit);
#[cfg(feature = "auth-jwt")]
{
let issuer = effective_issuer(cfg);
let audience = if cfg.auth.audience.is_empty() {
vec![issuer.clone()]
} else {
cfg.auth.audience.clone()
};
let jwt = assay_auth::jwt::JwtConfig::new(issuer.clone(), audience);
if let Err(e) = jwt.load_from_postgres(pool).await {
tracing::warn!(?e, "no JWKS rows yet; rotating to seed first key");
jwt.rotate_postgres(pool)
.await
.map_err(|e| anyhow::anyhow!("seed JWKS (pg): {e}"))?;
}
if jwt.active_kid().is_none() {
jwt.rotate_postgres(pool)
.await
.map_err(|e| anyhow::anyhow!("seed JWKS (pg): {e}"))?;
}
ctx = ctx.with_jwt(jwt);
ctx = ctx.with_external_issuers(discover_external_issuers(cfg).await?);
}
#[cfg(feature = "auth-oidc")]
{
ctx = ctx.with_oidc(assay_auth::oidc::OidcRegistry::new());
}
#[cfg(feature = "auth-passkey")]
if let Some(passkey_mgr) = build_passkey_manager(cfg, users.clone()) {
ctx = ctx.with_passkeys(passkey_mgr);
}
#[cfg(feature = "auth-zanzibar")]
{
let zanzibar: Arc<dyn assay_auth::zanzibar::ZanzibarStore> = Arc::new(
assay_auth::zanzibar::PostgresZanzibarStore::new(pool.clone()),
);
ctx = ctx.with_zanzibar(zanzibar);
}
#[cfg(feature = "auth-oidc-provider")]
if cfg.auth.oidc_provider.enabled {
let issuer = oidc_issuer(cfg);
let public_url = oidc_public_url(cfg)?;
let provider = assay_auth::oidc_provider::OidcProviderConfig::new(
issuer,
public_url,
assay_auth::oidc_provider::PostgresOidcClientStore::new(pool.clone()).into_dyn(),
assay_auth::oidc_provider::PostgresOidcUpstreamStore::new(pool.clone()).into_dyn(),
assay_auth::oidc_provider::PostgresOidcCodeStore::new(pool.clone()).into_dyn(),
assay_auth::oidc_provider::PostgresOidcRefreshStore::new(pool.clone()).into_dyn(),
assay_auth::oidc_provider::PostgresOidcSessionStore::new(pool.clone()).into_dyn(),
assay_auth::oidc_provider::PostgresOidcConsentStore::new(pool.clone()).into_dyn(),
assay_auth::oidc_provider::PostgresOidcUpstreamStateStore::new(pool.clone()).into_dyn(),
)
.with_jwks_source(assay_auth::oidc_provider::JwksSource::Postgres(
pool.clone(),
))
.with_auto_provision(cfg.auth.oidc_provider.auto_provision);
ctx = ctx.with_oidc_provider(provider);
if let (Some(registry), Some(provider)) = (&ctx.oidc, &ctx.oidc_provider) {
match provider.upstream.list().await {
Ok(rows) => {
for row in rows {
assay_auth::oidc_provider::sync_upstream_to_registry(
registry,
&row,
&provider.public_url,
)
.await;
}
}
Err(e) => {
tracing::warn!("failed to list upstream providers at boot: {e}");
}
}
}
}
Ok(ctx)
}
#[cfg(feature = "backend-sqlite")]
async fn build_auth_ctx_sqlite(
cfg: &EngineConfig,
pool: &sqlx::SqlitePool,
) -> anyhow::Result<assay_auth::AuthCtx> {
use assay_auth::store::{SqliteSessionStore, SqliteUserStore};
let users = SqliteUserStore::new(pool.clone()).into_dyn();
let sessions = SqliteSessionStore::new(pool.clone()).into_dyn();
let mut ctx = assay_auth::AuthCtx::new(users.clone(), sessions);
let biscuit = assay_auth::biscuit::load_or_init_sqlite(pool)
.await
.map_err(|e| anyhow::anyhow!("biscuit root key (sqlite): {e}"))?;
ctx = ctx.with_biscuit(biscuit);
#[cfg(feature = "auth-jwt")]
{
let issuer = effective_issuer(cfg);
let audience = if cfg.auth.audience.is_empty() {
vec![issuer.clone()]
} else {
cfg.auth.audience.clone()
};
let jwt = assay_auth::jwt::JwtConfig::new(issuer.clone(), audience);
if let Err(e) = jwt.load_from_sqlite(pool).await {
tracing::warn!(?e, "no JWKS rows yet; rotating to seed first key");
jwt.rotate_sqlite(pool)
.await
.map_err(|e| anyhow::anyhow!("seed JWKS (sqlite): {e}"))?;
}
if jwt.active_kid().is_none() {
jwt.rotate_sqlite(pool)
.await
.map_err(|e| anyhow::anyhow!("seed JWKS (sqlite): {e}"))?;
}
ctx = ctx.with_jwt(jwt);
ctx = ctx.with_external_issuers(discover_external_issuers(cfg).await?);
}
#[cfg(feature = "auth-oidc")]
{
ctx = ctx.with_oidc(assay_auth::oidc::OidcRegistry::new());
}
#[cfg(feature = "auth-passkey")]
if let Some(passkey_mgr) = build_passkey_manager(cfg, users.clone()) {
ctx = ctx.with_passkeys(passkey_mgr);
}
#[cfg(feature = "auth-zanzibar")]
{
let zanzibar: Arc<dyn assay_auth::zanzibar::ZanzibarStore> =
Arc::new(assay_auth::zanzibar::SqliteZanzibarStore::new(pool.clone()));
ctx = ctx.with_zanzibar(zanzibar);
}
#[cfg(feature = "auth-oidc-provider")]
if cfg.auth.oidc_provider.enabled {
let issuer = oidc_issuer(cfg);
let public_url = oidc_public_url(cfg)?;
let provider = assay_auth::oidc_provider::OidcProviderConfig::new(
issuer,
public_url,
assay_auth::oidc_provider::SqliteOidcClientStore::new(pool.clone()).into_dyn(),
assay_auth::oidc_provider::SqliteOidcUpstreamStore::new(pool.clone()).into_dyn(),
assay_auth::oidc_provider::SqliteOidcCodeStore::new(pool.clone()).into_dyn(),
assay_auth::oidc_provider::SqliteOidcRefreshStore::new(pool.clone()).into_dyn(),
assay_auth::oidc_provider::SqliteOidcSessionStore::new(pool.clone()).into_dyn(),
assay_auth::oidc_provider::SqliteOidcConsentStore::new(pool.clone()).into_dyn(),
assay_auth::oidc_provider::SqliteOidcUpstreamStateStore::new(pool.clone()).into_dyn(),
)
.with_jwks_source(assay_auth::oidc_provider::JwksSource::Sqlite(pool.clone()))
.with_auto_provision(cfg.auth.oidc_provider.auto_provision);
ctx = ctx.with_oidc_provider(provider);
if let (Some(registry), Some(provider)) = (&ctx.oidc, &ctx.oidc_provider) {
match provider.upstream.list().await {
Ok(rows) => {
for row in rows {
assay_auth::oidc_provider::sync_upstream_to_registry(
registry,
&row,
&provider.public_url,
)
.await;
}
}
Err(e) => {
tracing::warn!("failed to list upstream providers at boot: {e}");
}
}
}
}
Ok(ctx)
}
#[cfg(feature = "auth-jwt")]
async fn discover_external_issuers(
cfg: &EngineConfig,
) -> anyhow::Result<Vec<assay_auth::external_jwt::ExternalJwtIssuer>> {
let entries = cfg.auth.external_issuers();
let mut out = Vec::with_capacity(entries.len());
for entry in entries {
let verifier = assay_auth::external_jwt::ExternalJwtIssuer::discover(
entry.issuer_url.clone(),
entry.audience.clone(),
entry.jwks_refresh_secs,
)
.await
.map_err(|e| anyhow::anyhow!("discover external issuer `{}`: {e}", entry.issuer_url))?;
tracing::info!(
target: "assay-engine",
issuer = %entry.issuer_url,
audience = ?entry.audience,
"trusted external OIDC issuer for JWT pass-through"
);
out.push(verifier);
}
Ok(out)
}
fn effective_issuer(cfg: &EngineConfig) -> String {
if let Some(issuer) = &cfg.auth.issuer {
return issuer.clone();
}
let base = auth_public_url(cfg).trim_end_matches('/');
format!("{base}/auth")
}
fn auth_public_url(cfg: &EngineConfig) -> &str {
cfg.auth
.public_url
.as_deref()
.unwrap_or(&cfg.server.public_url)
}
fn oidc_issuer(cfg: &EngineConfig) -> String {
cfg.auth
.oidc_provider
.issuer_override
.clone()
.unwrap_or_else(|| effective_issuer(cfg))
}
fn parse_auth_public_url(cfg: &EngineConfig) -> anyhow::Result<url::Url> {
let public_url = auth_public_url(cfg);
url::Url::parse(public_url).map_err(|e| anyhow::anyhow!("auth.public_url {public_url:?}: {e}"))
}
fn oidc_public_url(cfg: &EngineConfig) -> anyhow::Result<url::Url> {
let issuer = oidc_issuer(cfg);
url::Url::parse(&issuer).map_err(|e| anyhow::anyhow!("oidc issuer {issuer:?}: {e}"))
}
#[cfg(feature = "auth-passkey")]
fn build_passkey_manager(
cfg: &EngineConfig,
users: Arc<dyn assay_auth::store::UserStore>,
) -> Option<assay_auth::passkey::PasskeyManager> {
let url = match parse_auth_public_url(cfg) {
Ok(u) => u,
Err(e) => {
tracing::warn!(?e, "passkeys disabled — bad public_url");
return None;
}
};
let host = match url.host_str() {
Some(h) => h.to_string(),
None => {
tracing::warn!("passkeys disabled — public_url has no host");
return None;
}
};
let pk_cfg = assay_auth::passkey::PasskeyConfig {
rp_id: cfg.auth.passkey.rp_id.clone().unwrap_or(host),
rp_name: cfg
.auth
.passkey
.rp_name
.clone()
.unwrap_or_else(|| "Assay".to_string()),
origin: url,
};
match assay_auth::passkey::PasskeyManager::new(pk_cfg, users) {
Ok(m) => Some(m),
Err(e) => {
tracing::warn!(?e, "passkeys disabled — manager construction failed");
None
}
}
}
#[cfg(test)]
mod public_url_tests {
use super::*;
fn config(server_public_url: &str, auth_public_url: Option<&str>) -> EngineConfig {
let auth_public_url = auth_public_url
.map(|url| format!("public_url = \"{url}\""))
.unwrap_or_default();
toml::from_str(&format!(
r#"
[server]
bind_addr = "127.0.0.1:3000"
public_url = "{server_public_url}"
[backend]
type = "sqlite"
data_dir = ":memory:"
[auth]
{auth_public_url}
"#
))
.expect("valid engine config")
}
#[test]
fn auth_origin_defaults_to_engine_public_url() {
let cfg = config("https://engine.example.com", None);
assert_eq!(effective_issuer(&cfg), "https://engine.example.com/auth");
assert_eq!(
parse_auth_public_url(&cfg).unwrap().as_str(),
"https://engine.example.com/"
);
}
#[test]
fn auth_origin_override_drives_default_issuer_and_passkey_origin() {
let cfg = config(
"https://engine.example.com",
Some("https://auth.example.com"),
);
assert_eq!(effective_issuer(&cfg), "https://auth.example.com/auth");
assert_eq!(
parse_auth_public_url(&cfg).unwrap().as_str(),
"https://auth.example.com/"
);
}
#[test]
fn explicit_issuer_takes_precedence_over_auth_origin() {
let mut cfg = config(
"https://engine.example.com",
Some("https://auth.example.com"),
);
cfg.auth.issuer = Some("https://issuer.example.net/oauth".to_string());
assert_eq!(effective_issuer(&cfg), "https://issuer.example.net/oauth");
}
#[test]
fn invalid_auth_origin_is_rejected() {
let cfg = config("https://engine.example.com", Some("not a URL"));
let error = parse_auth_public_url(&cfg).unwrap_err();
assert!(error.to_string().contains("auth.public_url"));
}
}