warg-server 0.10.0

A server library for Warg component registries.
Documentation
use std::time::SystemTime;

use anyhow::Context;
use axum::{
    debug_handler,
    extract::{Path, State},
    http::StatusCode,
    response::IntoResponse,
    routing::get,
    Router,
};
use serde::Serialize;
use warg_crypto::{
    hash::{AnyHash, Sha256},
    signing::KeyID,
};
use warg_protocol::{
    package::{LogState, Permission, Release},
    registry::{LogId, PackageName, RecordId},
    Version,
};

use crate::{api::v1::Json, services::CoreService};

#[derive(Clone)]
pub struct Config {
    core_service: CoreService,
}

impl Config {
    pub fn new(core_service: CoreService) -> Self {
        Self { core_service }
    }

    pub fn into_router(self) -> Router {
        Router::new()
            .route("/packages", get(list_package_names))
            .route("/package/:package_name", get(get_package_info))
            .with_state(self)
    }
}

#[debug_handler]
async fn list_package_names(
    State(config): State<Config>,
) -> Result<Json<Vec<PackageName>>, DebugError> {
    let names = config
        .core_service
        .store()
        .debug_list_package_names()
        .await?;
    Ok(Json(names))
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct PackageInfo {
    package_name: PackageName,
    log_id: LogId,
    records: Vec<RecordInfo>,
    releases: Vec<Release>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct RecordInfo {
    record_id: RecordId,
    timestamp: u64,
    entries: Vec<EntryInfo>,
}

#[derive(Default, Serialize)]
#[serde(rename_all = "camelCase")]
struct EntryInfo {
    kind: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    key: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    key_id: Option<KeyID>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    permissions: Vec<Permission>,
    #[serde(skip_serializing_if = "Option::is_none")]
    version: Option<Version>,
    #[serde(skip_serializing_if = "Option::is_none")]
    content: Option<AnyHash>,
}

#[debug_handler]
async fn get_package_info(
    State(config): State<Config>,
    Path(package_name): Path<PackageName>,
) -> Result<Json<PackageInfo>, DebugError> {
    let store = config.core_service.store();

    let checkpoint = store
        .get_latest_checkpoint()
        .await
        .context("get_latest_checkpoint")?;
    let checkpoint_log_length = checkpoint.as_ref().checkpoint.log_length;

    let log_id = LogId::package_log::<Sha256>(&package_name);
    let records = store
        .get_package_records(&log_id, checkpoint_log_length, None, u16::MAX)
        .await
        .context("get_package_records")?;

    let mut package_state = LogState::new();

    let records = records
        .into_iter()
        .map(|record| {
            let state = std::mem::take(&mut package_state);
            package_state = state.validate(&record.envelope).context("validate")?;
            let record_id = RecordId::package_record::<Sha256>(&record.envelope);
            let timestamp = record
                .envelope
                .as_ref()
                .timestamp
                .duration_since(SystemTime::UNIX_EPOCH)
                .context("duration_since")?
                .as_secs();
            let entries = record
                .envelope
                .as_ref()
                .entries
                .iter()
                .map(|entry| {
                    use warg_protocol::package::PackageEntry::*;
                    match entry {
                        Init { key, .. } => EntryInfo {
                            kind: "init",
                            key: Some(key.to_string()),
                            ..Default::default()
                        },
                        GrantFlat { key, permissions } => EntryInfo {
                            kind: "grant",
                            key: Some(key.to_string()),
                            permissions: permissions.clone(),
                            ..Default::default()
                        },
                        RevokeFlat {
                            key_id,
                            permissions,
                        } => EntryInfo {
                            kind: "revoke",
                            key_id: Some(key_id.clone()),
                            permissions: permissions.clone(),
                            ..Default::default()
                        },
                        Release { version, content } => EntryInfo {
                            kind: "release",
                            version: Some(version.clone()),
                            content: Some(content.clone()),
                            ..Default::default()
                        },
                        Yank { version } => EntryInfo {
                            kind: "yank",
                            version: Some(version.clone()),
                            ..Default::default()
                        },
                        _ => EntryInfo {
                            kind: "UNKNOWN",
                            ..Default::default()
                        },
                    }
                })
                .collect();
            Ok(RecordInfo {
                record_id,
                timestamp,
                entries,
            })
        })
        .collect::<Result<_, DebugError>>()?;

    let releases = package_state.releases().cloned().collect();

    Ok(Json(PackageInfo {
        package_name,
        log_id,
        records,
        releases,
    }))
}

struct DebugError(String);

impl From<anyhow::Error> for DebugError {
    fn from(err: anyhow::Error) -> Self {
        Self(format!("{err:#?}"))
    }
}

impl IntoResponse for DebugError {
    fn into_response(self) -> axum::response::Response {
        (StatusCode::INTERNAL_SERVER_ERROR, self.0).into_response()
    }
}