passless-rs 0.17.0

FIDO2 security token emulator
Documentation
#[path = "../agent/prompt.rs"]
#[allow(dead_code, unused_imports, unused_variables)]
mod prompt;

use std::process;

use clap::Parser;
use serde::Serialize;

use passless_core::agent::ProfileId;

use prompt::{DesktopPromptHandle, PromptAction, PromptHandle, PromptMode, PromptRequest};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
enum LatencyClass {
    Fast,
    Normal,
    Slow,
}

impl LatencyClass {
    fn from_ms(ms: u64) -> Self {
        if ms < 1000 {
            Self::Fast
        } else if ms <= 5000 {
            Self::Normal
        } else {
            Self::Slow
        }
    }
}

#[derive(Debug, Serialize)]
struct ProbeOutput {
    decision: String,
    error_kind: Option<String>,
    latency_class: LatencyClass,
}

#[derive(Parser, Debug)]
#[command(
    name = "agent-prompt-probe",
    about = "Validation-only production prompt probe"
)]
struct Args {
    #[arg(long, default_value_t = 60)]
    timeout: u64,

    #[arg(long, default_value_t = 1000)]
    min_review_delay: u64,

    #[arg(long, default_value = "authenticate")]
    action: String,

    #[arg(long, default_value = "probe.validation.local")]
    rp_id: String,
}

fn main() {
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn"))
        .format_timestamp(None)
        .init();

    let args = Args::parse();

    let action = match args.action.as_str() {
        "register" => PromptAction::Register,
        "authenticate" => PromptAction::Authenticate,
        other => {
            let output = ProbeOutput {
                decision: "error".to_string(),
                error_kind: Some("internal_error".to_string()),
                latency_class: LatencyClass::Fast,
            };
            eprintln!("invalid action: {other}");
            println!("{}", serde_json::to_string(&output).unwrap());
            process::exit(1);
        }
    };

    let profile_id = match ProfileId::new("validation-probe") {
        Ok(id) => id,
        Err(e) => {
            let output = ProbeOutput {
                decision: "error".to_string(),
                error_kind: Some("internal_error".to_string()),
                latency_class: LatencyClass::Fast,
            };
            eprintln!("failed to construct profile id: {e}");
            println!("{}", serde_json::to_string(&output).unwrap());
            process::exit(1);
        }
    };

    let request = match PromptRequest::builder()
        .profile_id(profile_id)
        .mode(PromptMode::Isolated)
        .action(action)
        .rp_id(&args.rp_id)
        .grant_ttl_secs(300)
        .session_ttl_secs(3600)
        .untrusted_reason("validation probe")
        .build()
    {
        Ok(r) => r,
        Err(e) => {
            let output = ProbeOutput {
                decision: "error".to_string(),
                error_kind: Some("internal_error".to_string()),
                latency_class: LatencyClass::Fast,
            };
            eprintln!("failed to build prompt request: {e}");
            println!("{}", serde_json::to_string(&output).unwrap());
            process::exit(1);
        }
    };

    let handle = DesktopPromptHandle::new(args.timeout, args.min_review_delay);

    let result = handle.prompt(&request);

    let latency_class = LatencyClass::from_ms(result.latency_ms);
    let error_kind = result.error_kind.map(|k| format!("{k}"));

    let output = ProbeOutput {
        decision: format!("{}", result.decision),
        error_kind,
        latency_class,
    };

    println!("{}", serde_json::to_string(&output).unwrap());
}