pub mod store;
use std::sync::Arc;
use url::Url;
use webauthn_rs::prelude::*;
use crate::auth::extractor::AuthState;
use crate::error::AppError;
use crate::store::KeyspaceHandle;
pub const ENROLLMENT_CLAIM_WINDOW_SECS: u64 = 300;
pub trait PasskeyState: AuthState {
fn webauthn(&self) -> Option<&Arc<Webauthn>>;
fn acl_ks(&self) -> &KeyspaceHandle;
fn access_token_expiry(&self) -> u64;
fn refresh_token_expiry(&self) -> u64;
fn public_url(&self) -> Option<&str>;
fn enrollment_ttl(&self) -> u64;
}
pub fn build_webauthn(public_url: &str) -> Result<Webauthn, AppError> {
let url = Url::parse(public_url)
.map_err(|e| AppError::Config(format!("invalid public_url '{public_url}': {e}")))?;
let rp_id = url
.domain()
.ok_or_else(|| AppError::Config("public_url has no domain".into()))?
.to_string();
let builder = WebauthnBuilder::new(&rp_id, &url)
.map_err(|e| AppError::Config(format!("failed to build WebauthnBuilder: {e}")))?;
let webauthn = builder
.rp_name("Verifiable Trust Infrastructure")
.build()
.map_err(|e| AppError::Config(format!("failed to build Webauthn: {e}")))?;
Ok(webauthn)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_webauthn_happy_path() {
let w = build_webauthn("https://vtc.example.com").expect("ok");
drop(w);
}
#[test]
fn build_webauthn_rejects_invalid_url() {
let err = build_webauthn("not-a-url").expect_err("invalid URL");
assert!(
matches!(err, AppError::Config(ref m) if m.contains("invalid public_url")),
"got: {err}"
);
}
#[test]
fn build_webauthn_rejects_url_without_domain() {
let err = build_webauthn("file:///tmp/foo").expect_err("no domain");
assert!(
matches!(err, AppError::Config(ref m) if m.contains("no domain")),
"got: {err}"
);
}
}