Skip to main content

soma_auth/upstream/
config.rs

1//! Minimal, self-contained upstream configuration shape.
2//!
3//! soma-auth intentionally does not depend on any gateway/runtime crate for
4//! outbound upstream OAuth. Only the fields the OAuth runtime actually reads
5//! (`name`, `url`, `oauth`) are modeled here — a full gateway config schema
6//! (tool/resource exposure allowlists, proxy flags, priority, import
7//! provenance, ...) belongs to whatever consumer wires this runtime up, not
8//! to the auth crate itself.
9
10use serde::{Deserialize, Serialize};
11
12/// Upstream MCP server identity + outbound OAuth configuration, as needed by
13/// [`crate::upstream::manager::UpstreamOauthManager`].
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct UpstreamConfig {
16    /// Human-readable name for this upstream (used as the OAuth manager/cache key).
17    pub name: String,
18    /// URL of the upstream MCP server. Required when `oauth` is set.
19    #[serde(default)]
20    pub url: Option<String>,
21    /// Outbound OAuth configuration. `None` means no OAuth manager is built
22    /// for this upstream.
23    #[serde(default)]
24    pub oauth: Option<UpstreamOauthConfig>,
25}
26
27impl UpstreamConfig {
28    /// Canonicalized `url` per RFC 3986 §6.2.2 (scheme/host lowercase,
29    /// default port stripped, dot-segment removal, percent-encoding case
30    /// normalization). Trailing slashes are preserved — they are
31    /// semantically significant in HTTP paths.
32    ///
33    /// Returns `None` when `url` is unset, `Some(Err(_))` when it is set but
34    /// not a valid URL.
35    #[must_use]
36    pub fn canonical_url(&self) -> Option<Result<String, url::ParseError>> {
37        self.url
38            .as_deref()
39            .map(|raw| url::Url::parse(raw.trim()).map(|parsed| parsed.to_string()))
40    }
41}
42
43/// Outbound OAuth configuration for a single upstream.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct UpstreamOauthConfig {
46    pub mode: UpstreamOauthMode,
47    #[serde(default)]
48    pub registration: UpstreamOauthRegistration,
49    #[serde(default)]
50    pub scopes: Option<Vec<String>>,
51    /// Compatibility override for older configurations. `true` lets an
52    /// explicit dynamic strategy prefer CIMD when the authorization server
53    /// advertises support. `false` keeps explicit dynamic registration on
54    /// RFC 7591. New configurations should use [`UpstreamOauthRegistration::Auto`].
55    #[serde(default)]
56    pub prefer_client_metadata_document: Option<bool>,
57}
58
59/// Outbound OAuth mode. Currently only `authorization_code_pkce` is supported.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum UpstreamOauthMode {
63    AuthorizationCodePkce,
64}
65
66/// Outbound OAuth client-registration strategy.
67#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(tag = "strategy", rename_all = "snake_case")]
69pub enum UpstreamOauthRegistration {
70    /// Prefer a served Client ID Metadata Document and fall back to DCR only
71    /// when the authorization server does not advertise CIMD support.
72    #[default]
73    Auto,
74    ClientMetadataDocument {
75        url: String,
76    },
77    Preregistered {
78        client_id: String,
79        #[serde(default)]
80        client_secret_env: Option<String>,
81    },
82    Dynamic,
83}