ordinary-api 0.6.0-pre.13

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

use crate::server::APPLICATION;
use axum::extract::{Query, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse;
use bytes::Bytes;
use chacha20poly1305::{
    XChaCha20Poly1305, XNonce,
    aead::{Aead, KeyInit},
};
use ordinary_config::SecretSource;
use serde::Deserialize;
use std::sync::Arc;
use tracing::Instrument;
use utoipa::IntoParams;
use x25519_dalek::PublicKey;

#[derive(Deserialize, IntoParams)]
pub struct StoreParams {
    /// project domain
    d: String,
    /// secret name
    n: String,
}

/// public key, nonce, MAC, at least one byte for secret
const MIN_BODY_LEN: usize = 32 + 24 + 16 + 1;
const ZEROED_KEY: [u8; 32] = [0u8; 32];

#[utoipa::path(
    put,
    path = "/secrets",
    tag = APPLICATION,
    request_body(content = [u8], content_type = "application/octet-stream"),
    params(StoreParams),
    responses(
        (status = 401, description = "unauthorized for operation"),
        (status = 200, description = "update an application secret"),
    ),
    security(
        ("access" = []),
    ),
)]
pub async fn store(
    State(state): State<Arc<crate::server::OrdinaryApiServerState>>,
    Query(StoreParams { d, n }): Query<StoreParams>,
    headers: HeaderMap,
    body: Bytes,
) -> impl IntoResponse {
    let domain = d;

    let span = tracing::info_span!("app", %domain);
    let span = span.in_scope(|| tracing::info_span!("secrets"));
    let span = span.in_scope(|| tracing::info_span!("store"));

    async {
        if body.len() < MIN_BODY_LEN {
            tracing::error!(secret = %n, "too small");
            return StatusCode::BAD_REQUEST.into_response();
        }

        if let Ok(max_size) = usize::try_from(state.config.limits.storage.secrets.max_size)
            && body.len() > max_size + MIN_BODY_LEN - 1
        {
            tracing::error!(secret = %n, "too big");
            return StatusCode::PAYLOAD_TOO_LARGE.into_response();
        }

        let account = match crate::server::check_ordinary_auth(&state, &headers, 0, &domain) {
            Ok(account) => account,
            Err(code) => return code.into_response(),
        };

        let client_public_key: [u8; 32] = match (&body[0..32]).try_into() {
            Ok(v) => v,
            Err(err) => {
                tracing::error!(%err);
                return StatusCode::INTERNAL_SERVER_ERROR.into_response();
            }
        };

        if client_public_key == ZEROED_KEY {
            return StatusCode::BAD_REQUEST.into_response();
        }

        tracing::info!(account, "storing");

        let apps = state.apps.read().await;

        if let Some(wrapped_app) = apps.get(&domain) {
            if wrapped_app.app.config.secrets.is_none() {
                tracing::error!("no secrets configured for app");
                return StatusCode::NOT_FOUND.into_response();
            }

            let mut doesnt_exist = true;

            if let Some(secrets) = &wrapped_app.app.config.secrets {
                for secret in secrets {
                    if let SecretSource::Stored = secret.source
                        && secret.name == n
                    {
                        doesnt_exist = false;
                        break;
                    }
                }
            }

            if doesnt_exist {
                tracing::error!(secret = %n, "not found");
                return StatusCode::NOT_FOUND.into_response();
            }

            let public_key = PublicKey::from(client_public_key);
            let shared_secret = wrapped_app.dh_keypair.0.diffie_hellman(&public_key);

            let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
            let nonce = XNonce::from_slice(&body[32..32 + 24]);

            let Ok(secret) = cipher.decrypt(nonce, &body[32 + 24..]) else {
                return StatusCode::INTERNAL_SERVER_ERROR.into_response();
            };

            if let Err(err) = wrapped_app.app.put_secret(n.as_str(), &secret) {
                tracing::error!(%err, "failed to store secret");
            } else {
                tracing::info!("stored secret");
                return StatusCode::OK.into_response();
            }
        }

        StatusCode::INTERNAL_SERVER_ERROR.into_response()
    }
    .instrument(span)
    .await
}