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}