Skip to main content

brain_edge/port/
mod.rs

1//! The two seams that let one edge serve two deployments.
2//!
3//! `brain-edge` ships a self-host default: forward the caller's bearer token to
4//! Brain (which resolves the identity itself) and meter nothing. The hosted Arc
5//! gateway injects its own [`CredentialResolver`] (API key → tenant identity)
6//! and [`MeteringSink`] (usage analytics) without forking the translation layer.
7//!
8//! Both ports are trait objects held in [`crate::EdgeState`], so the router and
9//! handlers stay non-generic and the host app plugs in behavior at construction.
10
11use axum::http::{HeaderMap, header};
12
13use crate::error::ApiError;
14
15/// The Brain wire credential the edge authenticates with, plus optional resolved
16/// metadata for metering and logging.
17#[derive(Clone, Debug)]
18pub struct ResolvedCredential {
19    /// Opaque token: the connection-pool cache key and — in the self-host
20    /// default — the Brain wire credential forwarded downstream.
21    pub credential: String,
22    /// A resolved tenant label, when the resolver knows it. The gateway fills
23    /// this from the API key (e.g. `namespace/agent`); the bearer default leaves
24    /// it `None` because Brain, not the edge, resolves the identity there.
25    pub tenant: Option<String>,
26}
27
28/// Resolve an incoming request to a Brain credential.
29///
30/// The default is [`BearerResolver`]. The hosted gateway supplies an
31/// implementation that looks an API key up in its key store and returns the
32/// tenant's Brain credential (and, later, a per-request identity).
33#[async_trait::async_trait]
34pub trait CredentialResolver: Send + Sync + 'static {
35    /// Map request headers to a [`ResolvedCredential`], or a `401`/`403` error.
36    async fn resolve(&self, headers: &HeaderMap) -> Result<ResolvedCredential, ApiError>;
37}
38
39/// The outcome of one data-plane operation, for metering.
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum Outcome {
42    /// The operation completed successfully.
43    Ok,
44    /// The operation failed (transport, engine, or validation).
45    Err,
46}
47
48impl Outcome {
49    /// Classify a verb `Result` without inspecting its payload.
50    #[must_use]
51    pub fn of<T, E>(result: &Result<T, E>) -> Self {
52        if result.is_ok() { Self::Ok } else { Self::Err }
53    }
54}
55
56/// One metered event: which tenant ran which op, and whether it succeeded.
57#[derive(Debug)]
58pub struct MeterEvent<'a> {
59    /// The resolved tenant label, if the resolver knew it.
60    pub tenant: Option<&'a str>,
61    /// The verb name (`"encode"`, `"recall"`, …).
62    pub op: &'a str,
63    /// Success or failure.
64    pub outcome: Outcome,
65}
66
67/// Record usage. Called once per data-plane op; implementations must not block
68/// the response path (fire-and-forget).
69pub trait MeteringSink: Send + Sync + 'static {
70    /// Record one operation's outcome.
71    fn record(&self, event: &MeterEvent<'_>);
72}
73
74/// The self-host default resolver: forward `Authorization: Bearer <key>` (or
75/// `X-API-Key`) to Brain, which resolves the identity itself.
76#[derive(Debug)]
77pub struct BearerResolver;
78
79#[async_trait::async_trait]
80impl CredentialResolver for BearerResolver {
81    async fn resolve(&self, headers: &HeaderMap) -> Result<ResolvedCredential, ApiError> {
82        Ok(ResolvedCredential {
83            credential: bearer_token(headers)?,
84            tenant: None,
85        })
86    }
87}
88
89/// The self-host default sink: record nothing.
90#[derive(Debug)]
91pub struct NoopMeter;
92
93impl MeteringSink for NoopMeter {
94    fn record(&self, _event: &MeterEvent<'_>) {}
95}
96
97/// Read the API key from `Authorization: Bearer …` or `X-API-Key`.
98///
99/// # Errors
100/// Returns a `401` if neither header carries a non-empty token.
101pub fn bearer_token(headers: &HeaderMap) -> Result<String, ApiError> {
102    if let Some(raw) = headers
103        .get(header::AUTHORIZATION)
104        .and_then(|v| v.to_str().ok())
105    {
106        let stripped = raw
107            .strip_prefix("Bearer ")
108            .or_else(|| raw.strip_prefix("bearer "));
109        if let Some(tok) = stripped {
110            let tok = tok.trim();
111            if !tok.is_empty() {
112                return Ok(tok.to_string());
113            }
114        }
115    }
116    if let Some(raw) = headers.get("x-api-key").and_then(|v| v.to_str().ok()) {
117        let tok = raw.trim();
118        if !tok.is_empty() {
119            return Ok(tok.to_string());
120        }
121    }
122    Err(ApiError::unauthorized(
123        "missing API key — send `Authorization: Bearer <key>` or `X-API-Key`",
124    ))
125}