ordinary-api 0.10.2

API server for Ordinary
Documentation
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use crate::client::{OrdinaryApiClient, compress_zstd};
use anyhow::bail;
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
use ordinary_config::{OrdinaryConfig, TemplateConfig, TemplateFfiVersion};
use std::path::Path;

pub async fn upload(
    api_client: &OrdinaryApiClient<'_>,
    proj_path: &str,
    template_name: &str,
) -> anyhow::Result<()> {
    let config = OrdinaryConfig::get(proj_path, true)?;

    let correlation_id = api_client
        .correlation_id
        .unwrap_or(uuid::Uuid::new_v4())
        .to_string();

    if let Some(templates) = &config.templates {
        if let Some(template_config) = templates
            .iter()
            .find(|t| t.name_validated() == template_name)
        {
            let access_token = api_client
                .get_access(None, Some(correlation_id.clone()))
                .await?;

            let (query, template_bytes) =
                get_template_bin_and_csp(proj_path, &config, template_config)?;

            let template_idx = template_config.idx_validated();
            let template_name = template_config.name_validated();

            tracing::info!(
                "uploading template '{}' to {} with idx {}...",
                template_name,
                config.domain,
                template_idx
            );
            api_client
                .client
                .put(format!("{}/v1/templates", api_client.addr))
                .body(compress_zstd(&template_bytes[..])?)
                .query(&query)
                .header("x-correlation-id", correlation_id.as_str())
                .header("Content-Encoding", "zstd")
                .header(
                    "Authorization",
                    format!("Bearer {}", b64.encode(&access_token)),
                )
                .send()
                .await?
                .bytes()
                .await?;
            tracing::info!("successfully uploaded template '{}'.", template_name);
        }
    } else {
        tracing::warn!("no \"templates\" field in ordinary.json");
    }

    Ok(())
}

pub async fn upload_all(api_client: &OrdinaryApiClient<'_>, proj_path: &str) -> anyhow::Result<()> {
    let config = OrdinaryConfig::get(proj_path, true)?;

    let correlation_id = api_client
        .correlation_id
        .unwrap_or(uuid::Uuid::new_v4())
        .to_string();

    if let Some(templates) = &config.templates {
        for template_config in templates {
            let access_token = api_client
                .get_access(None, Some(correlation_id.clone()))
                .await?;

            let (query, template_bytes) =
                get_template_bin_and_csp(proj_path, &config, template_config)?;

            let template_idx = template_config.idx_validated();
            let template_name = template_config.name_validated();

            tracing::info!(
                "uploading template '{}' to {} with idx {}...",
                template_name,
                config.domain,
                template_idx
            );
            api_client
                .client
                .put(format!("{}/v1/templates", api_client.addr))
                .body(compress_zstd(&template_bytes[..])?)
                .query(&query)
                .header("x-correlation-id", correlation_id.as_str())
                .header("Content-Encoding", "zstd")
                .header(
                    "Authorization",
                    format!("Bearer {}", b64.encode(&access_token)),
                )
                .send()
                .await?
                .bytes()
                .await?;
        }

        tracing::info!("all templates successfully uploaded.");
    } else {
        tracing::warn!("no \"templates\" field in ordinary.json");
    }

    Ok(())
}

#[allow(clippy::type_complexity)]
fn get_template_bin_and_csp(
    proj_path: &str,
    config: &OrdinaryConfig,
    template_config: &TemplateConfig,
) -> anyhow::Result<(Vec<(String, String)>, Vec<u8>)> {
    let mut template_config = template_config.clone();
    template_config.load_managed();

    let template_idx = template_config.idx_validated();
    let template_name = template_config.name_validated();

    let (bin_path, style_path, script_path) = match template_config.ffi_validated().version {
        TemplateFfiVersion::V1 => {
            tracing::warn!(
                "V1 template FFI will be deprecated in a future version of ordinary(d). Upgrade to V2."
            );
            let bin_path = Path::new(proj_path)
                .join(".ordinary/gen/server")
                .join(template_name)
                .join("target/wasm32-wasip1/release/template.wasm");

            let hash_path = Path::new(proj_path)
                .join(".ordinary")
                .join("gen")
                .join("hashes")
                .join(template_name);

            let style_path = hash_path.join("style-src.json");
            let script_path = hash_path.join("script-src.json");

            (bin_path, style_path, script_path)
        }
        TemplateFfiVersion::V2 => {
            let build_dir = if let Some(reference) = &template_config.r#ref {
                let Some(reference_dir) = Path::new(reference).parent() else {
                    bail!("no parent dir for reference on template '{reference}'");
                };
                reference_dir
            } else {
                Path::new(proj_path)
            };

            let ordinary_dir = build_dir.join(".ordinary");
            let template_dir = ordinary_dir.join(template_name);

            let bin_path = build_dir.join(
                template_config
                    .bin
                    .as_ref()
                    .expect("missing 'bin' path. cannot upload"),
            );

            let style_path = template_dir.join("style-src.json");
            let script_path = template_dir.join("script-src.json");

            (bin_path, style_path, script_path)
        }
    };

    let mut query = vec![
        ("d".to_string(), config.domain.clone()),
        ("i".to_string(), template_idx.to_string()),
    ];

    if style_path.exists() {
        let csp_style_hashes: Vec<String> =
            simd_json::from_slice(fs_err::read(style_path)?.as_mut_slice())?;

        if !csp_style_hashes.is_empty() {
            for hash in csp_style_hashes {
                query.push(("style_csp".to_string(), hash));
            }
        }
    }

    if script_path.exists() {
        let csp_script_hashes: Vec<String> =
            simd_json::from_slice(fs_err::read(script_path)?.as_mut_slice())?;

        if !csp_script_hashes.is_empty() {
            for hash in csp_script_hashes {
                query.push(("script_csp".to_string(), hash));
            }
        }
    }

    let template_bytes = fs_err::read(bin_path)?;
    Ok((query, template_bytes))
}