//! Length-prefixed JSON over a unix socket: u32 big-endian frame length, //! then the serialized request/response. use serde::{Deserialize, Serialize}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use uuid::Uuid; use crate::BrokerError; const MAX_FRAME: u32 = 1024 * 1024; #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "op", rename_all = "snake_case")] pub enum Request { StoreSecret { workspace_id: Uuid, kind: String, plaintext: String, }, SecretKind { secret_id: Uuid, }, /// Verifies a Slack request signature (v0 HMAC-SHA256) against the /// connection's signing secret — which never leaves the broker. VerifySlackSignature { secret_id: Uuid, timestamp: String, body: String, signature: String, }, /// Performs an HTTP POST with the secret injected as a bearer token. /// Requires consuming the approval's single-use execution grant. InvokeHttp { approval_id: Uuid, secret_id: Uuid, url: String, #[serde(default)] body: serde_json::Value, }, /// Read-only HTTP GET with the secret injected as a bearer token. Returns /// the response body as JSON without ever exposing the credential to the /// caller. Used by low-privilege data-fetching flows (listing an org's /// repositories on GitHub / Gitea / GitLab) that don't need the /// single-use grant `InvokeHttp` demands. The URL scheme is still /// restricted to http(s). FetchAuthorized { secret_id: Uuid, url: String, }, } #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "result", rename_all = "snake_case")] pub enum Response { SecretStored { secret_id: Uuid, }, SecretKind { kind: String, }, HttpDone { status: u16, }, /// Response to a `FetchAuthorized`: HTTP status + parsed JSON body. /// Body is `null` when the response wasn't JSON-parseable. HttpJson { status: u16, body: serde_json::Value, }, Verified { valid: bool, }, Error { kind: ErrorKind, message: String, }, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ErrorKind { GrantRefused, Invalid, NotFound, Internal, } impl Response { pub fn from_error(err: &BrokerError) -> Response { let (kind, message) = match err { BrokerError::GrantRefused => (ErrorKind::GrantRefused, err.to_string()), BrokerError::Invalid(m) => (ErrorKind::Invalid, m.clone()), BrokerError::NotFound => (ErrorKind::NotFound, err.to_string()), BrokerError::Crypto(m) | BrokerError::Io(m) => (ErrorKind::Internal, m.clone()), }; Response::Error { kind, message } } pub fn into_result(self) -> Result { match self { Response::Error { kind, message } => Err(match kind { ErrorKind::GrantRefused => BrokerError::GrantRefused, ErrorKind::Invalid => BrokerError::Invalid(message), ErrorKind::NotFound => BrokerError::NotFound, ErrorKind::Internal => BrokerError::Io(message), }), ok => Ok(ok), } } } pub async fn write_frame(writer: &mut W, value: &T) -> Result<(), BrokerError> where W: AsyncWriteExt + Unpin, T: Serialize, { let payload = serde_json::to_vec(value).map_err(|e| BrokerError::Io(e.to_string()))?; let len = u32::try_from(payload.len()).map_err(|_| BrokerError::Invalid("frame".into()))?; if len > MAX_FRAME { return Err(BrokerError::Invalid("frame too large".into())); } writer .write_all(&len.to_be_bytes()) .await .map_err(|e| BrokerError::Io(e.to_string()))?; writer .write_all(&payload) .await .map_err(|e| BrokerError::Io(e.to_string()))?; Ok(()) } pub async fn read_frame(reader: &mut R) -> Result where R: AsyncReadExt + Unpin, T: for<'de> Deserialize<'de>, { let mut len_bytes = [0u8; 4]; reader .read_exact(&mut len_bytes) .await .map_err(|e| BrokerError::Io(e.to_string()))?; let len = u32::from_be_bytes(len_bytes); if len > MAX_FRAME { return Err(BrokerError::Invalid("frame too large".into())); } let mut payload = vec![0u8; len as usize]; reader .read_exact(&mut payload) .await .map_err(|e| BrokerError::Io(e.to_string()))?; serde_json::from_slice(&payload).map_err(|e| BrokerError::Io(e.to_string())) }