aauth 0.0.2

Rust implementation of the AAuth authorization protocol
Documentation
use std::collections::HashMap;

use axum::http::{HeaderMap, StatusCode};
use serde_json::Value;

use crate::headers::{AAuthRequirementParams, build_aauth_requirement};
use crate::types::{AAuthProtocolError, RequirementLevel, TokenResponseBody};

use super::types::{DeferRequirement, PendingOutcome, PendingSnapshot, PendingStatus};

pub struct AcceptedResponse {
    pub status: StatusCode,
    pub headers: HeaderMap,
    pub body: Value,
}

pub fn build_accepted(
    location: &str,
    requirement: &DeferRequirement,
) -> Result<AcceptedResponse, crate::error::AAuthError> {
    let (level, params) = requirement_to_header(requirement)?;
    let aauth_requirement = build_aauth_requirement(level, params.as_ref())?;

    let body = match requirement {
        DeferRequirement::Clarification { question, timeout } => {
            let mut obj = serde_json::json!({
                "status": "pending",
                "clarification": question,
            });
            if let Some(t) = timeout {
                obj["timeout"] = Value::from(*t);
            }
            obj
        }
        DeferRequirement::Claims { required_claims } => serde_json::json!({
            "status": "pending",
            "required_claims": required_claims,
        }),
        _ => serde_json::json!({ "status": "pending" }),
    };

    let mut headers = HeaderMap::new();
    headers.insert("Location", location.parse().expect("valid location"));
    headers.insert("Retry-After", "0".parse().expect("valid header"));
    headers.insert("Cache-Control", "no-store".parse().expect("valid header"));
    headers.insert(
        "AAuth-Requirement",
        aauth_requirement.parse().expect("valid requirement"),
    );
    headers.insert(
        "Content-Type",
        "application/json".parse().expect("valid content-type"),
    );

    Ok(AcceptedResponse {
        status: StatusCode::ACCEPTED,
        headers,
        body,
    })
}

pub fn build_payment_required_stub(location: &str) -> AcceptedResponse {
    let mut headers = HeaderMap::new();
    headers.insert("Location", location.parse().expect("valid location"));
    headers.insert(
        "Content-Type",
        "application/json".parse().expect("valid content-type"),
    );

    AcceptedResponse {
        status: StatusCode::PAYMENT_REQUIRED,
        headers,
        body: serde_json::json!({
            "status": "pending",
            "error": "payment_required",
            "error_description": "Payment required (stub — settlement not implemented)"
        }),
    }
}

pub enum PollResponse {
    Accepted {
        headers: HeaderMap,
        body: Option<Value>,
    },
    OkAuthToken(TokenResponseBody),
    OkOpaque(String),
    Error {
        status: StatusCode,
        error: AAuthProtocolError,
    },
    Gone,
}

pub fn map_snapshot_to_poll_parts(snapshot: &PendingSnapshot) -> PollResponse {
    if let Some(outcome) = &snapshot.outcome {
        return match outcome {
            PendingOutcome::AuthToken(body) => PollResponse::OkAuthToken(body.clone()),
            PendingOutcome::OpaqueAccess(token) => PollResponse::OkOpaque(token.clone()),
            PendingOutcome::Error(err) => PollResponse::Error {
                status: StatusCode::FORBIDDEN,
                error: err.clone(),
            },
        };
    }

    let mut headers = HeaderMap::new();
    headers.insert("Retry-After", "0".parse().expect("valid header"));
    headers.insert("Cache-Control", "no-store".parse().expect("valid header"));

    let body = if let Some(requirement) = &snapshot.requirement {
        let mut headers_extra = HashMap::new();
        if let Ok(AcceptedResponse {
            headers: defer_headers,
            body,
            ..
        }) = build_accepted("", requirement)
        {
            for (k, v) in defer_headers.iter() {
                headers_extra.insert(k.to_string(), v.to_str().unwrap_or("").to_string());
            }
            for (k, v) in headers_extra {
                if k != "Location" {
                    if let Ok(name) = axum::http::HeaderName::from_bytes(k.as_bytes()) {
                        if let Ok(value) = v.parse() {
                            headers.insert(name, value);
                        }
                    }
                }
            }
            Some(body)
        } else {
            None
        }
    } else {
        None
    };

    let status_body = match snapshot.status {
        PendingStatus::Pending => serde_json::json!({ "status": "pending" }),
        PendingStatus::Interacting => serde_json::json!({ "status": "interacting" }),
    };

    PollResponse::Accepted {
        headers,
        body: Some(body.unwrap_or(status_body)),
    }
}

fn requirement_to_header(
    requirement: &DeferRequirement,
) -> Result<(RequirementLevel, Option<AAuthRequirementParams<'_>>), crate::error::AAuthError> {
    match requirement {
        DeferRequirement::Interaction { url, code } => Ok((
            RequirementLevel::Interaction,
            Some(AAuthRequirementParams {
                url: Some(url),
                code: Some(code),
                resource_token: None,
            }),
        )),
        DeferRequirement::Clarification { .. } => Ok((RequirementLevel::Clarification, None)),
        DeferRequirement::Claims { .. } => Ok((RequirementLevel::Claims, None)),
        DeferRequirement::Approval => Ok((RequirementLevel::Approval, None)),
        DeferRequirement::Payment { .. } => Err(crate::error::AAuthError::Message(
            "payment defer uses 402, not AAuth-Requirement".into(),
        )),
    }
}