little-durable-objects 0.1.10

Standalone regional durable-object control plane, host, and durability runtime
Documentation
use anyhow::{Context, Result, ensure};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::actor::ActorKey;

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SocketAuthorizationRequest {
    pub trigger_id: String,
    pub actor_id: String,
    pub credential: String,
}

#[derive(Clone, Debug)]
pub(crate) struct SocketAuthorization {
    pub actor: ActorKey,
    pub storage_region: String,
    pub metadata: Value,
    pub expires_at: i64,
}

#[derive(Debug)]
pub(crate) enum SocketAuthorizationError {
    Rejected,
    Unavailable(anyhow::Error),
}

impl std::fmt::Display for SocketAuthorizationError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Rejected => formatter.write_str("socket credential was rejected"),
            Self::Unavailable(error) => {
                write!(formatter, "socket authorization is unavailable: {error:#}")
            }
        }
    }
}

impl std::error::Error for SocketAuthorizationError {}

#[async_trait]
pub(crate) trait SocketAuthenticator: Send + Sync {
    async fn authorize(
        &self,
        request: SocketAuthorizationRequest,
    ) -> std::result::Result<SocketAuthorization, SocketAuthorizationError>;
}

pub(crate) struct HttpSocketAuthenticator {
    client: reqwest::Client,
    url: String,
    token: String,
}

impl HttpSocketAuthenticator {
    pub(crate) fn new(url: String, token: String) -> Result<Self> {
        let parsed = reqwest::Url::parse(&url).context("socket authorization URL is invalid")?;
        ensure!(
            matches!(parsed.scheme(), "http" | "https"),
            "socket authorization URL must use HTTP or HTTPS"
        );
        ensure!(
            !token.is_empty(),
            "socket authorization token must not be empty"
        );
        Ok(Self {
            client: reqwest::Client::new(),
            url,
            token,
        })
    }
}

#[async_trait]
impl SocketAuthenticator for HttpSocketAuthenticator {
    async fn authorize(
        &self,
        request: SocketAuthorizationRequest,
    ) -> std::result::Result<SocketAuthorization, SocketAuthorizationError> {
        let response = self
            .client
            .post(&self.url)
            .bearer_auth(&self.token)
            .json(&request)
            .send()
            .await
            .map_err(|error| SocketAuthorizationError::Unavailable(error.into()))?;
        if matches!(response.status().as_u16(), 401 | 403 | 404) {
            return Err(SocketAuthorizationError::Rejected);
        }
        let response = response
            .error_for_status()
            .map_err(|error| SocketAuthorizationError::Unavailable(error.into()))?
            .json::<SocketAuthorizationResponse>()
            .await
            .map_err(|error| SocketAuthorizationError::Unavailable(error.into()))?;
        response
            .into_authorization(&request.actor_id)
            .map_err(SocketAuthorizationError::Unavailable)
    }
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SocketAuthorizationResponse {
    namespace_id: String,
    actor_type: String,
    actor_id: String,
    storage_region: String,
    metadata: Value,
    expires_at: i64,
}

impl SocketAuthorizationResponse {
    fn into_authorization(self, requested_actor_id: &str) -> Result<SocketAuthorization> {
        ensure!(
            self.actor_id == requested_actor_id,
            "socket authorization changed the requested actor ID"
        );
        ensure!(
            !self.storage_region.is_empty(),
            "socket authorization omitted its storage region"
        );
        ensure!(
            self.expires_at > unix_seconds()?,
            "socket authorization has expired"
        );
        let actor = ActorKey {
            namespace_id: self.namespace_id,
            actor_type: self.actor_type,
            actor_id: self.actor_id,
        };
        actor.validate()?;
        Ok(SocketAuthorization {
            actor,
            storage_region: self.storage_region,
            metadata: self.metadata,
            expires_at: self.expires_at,
        })
    }
}

fn unix_seconds() -> Result<i64> {
    Ok(i64::try_from(
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)?
            .as_secs(),
    )?)
}