1#![allow(clippy::should_implement_trait)]
2#![allow(clippy::doc_lazy_continuation)]
3use std::collections::BTreeMap;
18use std::fs;
19use std::path::Path;
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum BridgesRegistryKind {
23 Oauth,
24 Clerk,
25 NextAuth,
26 BetterAuth,
27 Webauthn,
28 Tls,
29 Spiffe,
30 Did,
31 Gnap,
32 Mcp,
33 Matrix,
34 Webhook,
35 Grpc,
36 ServiceMesh,
37 A2a,
38 SessionCookie,
39 Aws,
40 Gcp,
41 Azure,
42 Vault,
43 Doppler,
44}
45
46impl BridgesRegistryKind {
47 pub fn parse(s: &str) -> Option<Self> {
48 Some(match s {
49 "oauth" => Self::Oauth,
50 "clerk" => Self::Clerk,
51 "next-auth" => Self::NextAuth,
52 "better-auth" => Self::BetterAuth,
53 "webauthn" => Self::Webauthn,
54 "tls" => Self::Tls,
55 "spiffe" => Self::Spiffe,
56 "did" => Self::Did,
57 "gnap" => Self::Gnap,
58 "mcp" => Self::Mcp,
59 "matrix" => Self::Matrix,
60 "webhook" => Self::Webhook,
61 "grpc" => Self::Grpc,
62 "service-mesh" => Self::ServiceMesh,
63 "a2a" => Self::A2a,
64 "session-cookie" => Self::SessionCookie,
65 "aws" => Self::Aws,
66 "gcp" => Self::Gcp,
67 "azure" => Self::Azure,
68 "vault" => Self::Vault,
69 "doppler" => Self::Doppler,
70 _ => return None,
71 })
72 }
73
74 pub fn as_str(&self) -> &'static str {
75 match self {
76 Self::Oauth => "oauth",
77 Self::Clerk => "clerk",
78 Self::NextAuth => "next-auth",
79 Self::BetterAuth => "better-auth",
80 Self::Webauthn => "webauthn",
81 Self::Tls => "tls",
82 Self::Spiffe => "spiffe",
83 Self::Did => "did",
84 Self::Gnap => "gnap",
85 Self::Mcp => "mcp",
86 Self::Matrix => "matrix",
87 Self::Webhook => "webhook",
88 Self::Grpc => "grpc",
89 Self::ServiceMesh => "service-mesh",
90 Self::A2a => "a2a",
91 Self::SessionCookie => "session-cookie",
92 Self::Aws => "aws",
93 Self::Gcp => "gcp",
94 Self::Azure => "azure",
95 Self::Vault => "vault",
96 Self::Doppler => "doppler",
97 }
98 }
99}
100
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub struct BridgeEntry {
103 pub kind: BridgesRegistryKind,
104 pub issuer_match: Option<String>,
105 pub iss_pattern: Option<String>,
106 pub trust_domain: Option<String>,
107 pub trust_level: Option<String>,
108 pub capability_map: Option<BTreeMap<String, String>>,
109 pub profile: Option<String>,
110}
111
112#[derive(Clone, Debug, Default, PartialEq, Eq)]
113pub struct BridgesRegistry {
114 pub registry_version: String,
115 pub default_profile: Option<String>,
116 pub bridges: Vec<BridgeEntry>,
117}
118
119#[derive(Debug, thiserror::Error)]
120pub enum BridgesRegistryError {
121 #[error("invalid registry: {0}")]
122 Invalid(String),
123 #[error("io: {0}")]
124 Io(#[from] std::io::Error),
125 #[error("parse: {0}")]
126 Parse(String),
127}
128
129const TRUST_LEVELS: &[&str] = &["T0", "T1", "T2", "T3", "T4", "T5", "T6", "T7"];
130
131fn validate_profile(s: &str) -> bool {
132 let mut chars = s.chars();
134 if chars.next() != Some('t') || chars.next() != Some('f') || chars.next() != Some('-') {
135 return false;
136 }
137 let body: String = chars.collect();
138 if !body.ends_with("-compatible") {
139 return false;
140 }
141 let middle = &body[..body.len() - "-compatible".len()];
142 if middle.is_empty() {
143 return false;
144 }
145 let mut it = middle.chars();
146 let first = match it.next() {
147 Some(c) => c,
148 None => return false,
149 };
150 if !first.is_ascii_lowercase() {
151 return false;
152 }
153 for c in it {
154 if !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
155 return false;
156 }
157 }
158 true
159}
160
161fn validate_action_name(s: &str) -> bool {
162 let mut segs = s.split('.');
164 let first = match segs.next() {
165 Some(v) if !v.is_empty() => v,
166 _ => return false,
167 };
168 if !valid_action_segment(first) {
169 return false;
170 }
171 let mut count = 0;
172 for seg in segs {
173 if !valid_action_segment(seg) {
174 return false;
175 }
176 count += 1;
177 }
178 count >= 1
179}
180
181fn valid_action_segment(s: &str) -> bool {
182 let mut it = s.chars();
183 let first = match it.next() {
184 Some(c) => c,
185 None => return false,
186 };
187 if !first.is_ascii_lowercase() {
188 return false;
189 }
190 for c in it {
191 if !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_') {
192 return false;
193 }
194 }
195 true
196}
197
198impl BridgesRegistry {
199 pub fn load(path: impl AsRef<Path>) -> Result<Self, BridgesRegistryError> {
203 let path = path.as_ref();
204 let text = match fs::read_to_string(path) {
205 Ok(t) => t,
206 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
207 return Ok(BridgesRegistry {
208 registry_version: "1".into(),
209 default_profile: None,
210 bridges: Vec::new(),
211 });
212 }
213 Err(e) => return Err(BridgesRegistryError::Io(e)),
214 };
215 Self::from_str(&text)
216 }
217
218 pub fn from_str(text: &str) -> Result<Self, BridgesRegistryError> {
220 let raw: serde_yaml::Value =
221 serde_yaml::from_str(text).map_err(|e| BridgesRegistryError::Parse(format!("{e}")))?;
222 let doc = match raw {
223 serde_yaml::Value::Mapping(m) => m,
224 _ => {
225 return Err(BridgesRegistryError::Invalid(
226 "registry root must be a mapping".into(),
227 ))
228 }
229 };
230 let mut registry_version: Option<String> = None;
231 let mut default_profile: Option<String> = None;
232 let mut bridges_value: Option<serde_yaml::Value> = None;
233 for (k, v) in doc {
234 let key = k
235 .as_str()
236 .ok_or_else(|| BridgesRegistryError::Invalid("non-string key in registry".into()))?
237 .to_string();
238 match key.as_str() {
239 "registry_version" => {
240 let s = v.as_str().ok_or_else(|| {
241 BridgesRegistryError::Invalid("registry_version must be a string".into())
242 })?;
243 registry_version = Some(s.to_string());
244 }
245 "default_profile" => {
246 if let serde_yaml::Value::Null = v {
247 continue;
248 }
249 let s = v.as_str().ok_or_else(|| {
250 BridgesRegistryError::Invalid("default_profile must be a string".into())
251 })?;
252 if !validate_profile(s) {
253 return Err(BridgesRegistryError::Invalid(format!(
254 "default_profile must match ^tf-[a-z][a-z0-9-]*-compatible$, got {s}"
255 )));
256 }
257 default_profile = Some(s.to_string());
258 }
259 "bridges" => {
260 bridges_value = Some(v);
261 }
262 other => {
263 return Err(BridgesRegistryError::Invalid(format!(
264 "unknown registry key: {other}"
265 )));
266 }
267 }
268 }
269 let registry_version = registry_version
270 .ok_or_else(|| BridgesRegistryError::Invalid("registry_version is required".into()))?;
271 if registry_version != "1" {
272 return Err(BridgesRegistryError::Invalid(format!(
273 "registry_version must be \"1\", got {registry_version:?}"
274 )));
275 }
276 let bridges_value = bridges_value
277 .ok_or_else(|| BridgesRegistryError::Invalid("bridges is required".into()))?;
278 let entries = match bridges_value {
279 serde_yaml::Value::Sequence(s) => s,
280 _ => {
281 return Err(BridgesRegistryError::Invalid(
282 "bridges must be a sequence".into(),
283 ))
284 }
285 };
286 let mut bridges = Vec::with_capacity(entries.len());
287 for (i, entry) in entries.into_iter().enumerate() {
288 bridges.push(parse_entry(entry, i)?);
289 }
290 Ok(BridgesRegistry {
291 registry_version: "1".into(),
292 default_profile,
293 bridges,
294 })
295 }
296
297 pub fn resolve_by_issuer(&self, iss: &str) -> Option<&BridgeEntry> {
303 if iss.is_empty() {
304 return None;
305 }
306 for entry in &self.bridges {
307 if let Some(m) = &entry.issuer_match {
308 if m == iss {
309 return Some(entry);
310 }
311 }
312 }
313 for entry in &self.bridges {
314 if let Some(p) = &entry.iss_pattern {
315 if iss.contains(p.as_str()) {
316 return Some(entry);
317 }
318 }
319 }
320 None
321 }
322
323 pub fn resolve_by_kind(&self, kind: &BridgesRegistryKind) -> Option<&BridgeEntry> {
325 self.bridges.iter().find(|e| &e.kind == kind)
326 }
327}
328
329fn parse_entry(
330 value: serde_yaml::Value,
331 index: usize,
332) -> Result<BridgeEntry, BridgesRegistryError> {
333 let map = match value {
334 serde_yaml::Value::Mapping(m) => m,
335 _ => {
336 return Err(BridgesRegistryError::Invalid(format!(
337 "bridges[{index}] must be a mapping"
338 )))
339 }
340 };
341 let mut kind: Option<BridgesRegistryKind> = None;
342 let mut issuer_match: Option<String> = None;
343 let mut iss_pattern: Option<String> = None;
344 let mut trust_domain: Option<String> = None;
345 let mut trust_level: Option<String> = None;
346 let mut capability_map: Option<BTreeMap<String, String>> = None;
347 let mut profile: Option<String> = None;
348 for (k, v) in map {
349 let key = k
350 .as_str()
351 .ok_or_else(|| {
352 BridgesRegistryError::Invalid(format!("bridges[{index}] has non-string key"))
353 })?
354 .to_string();
355 match key.as_str() {
356 "kind" => {
357 let s = v.as_str().ok_or_else(|| {
358 BridgesRegistryError::Invalid(format!("bridges[{index}].kind must be a string"))
359 })?;
360 kind = Some(BridgesRegistryKind::parse(s).ok_or_else(|| {
361 BridgesRegistryError::Invalid(format!("bridges[{index}].kind invalid: {s}"))
362 })?);
363 }
364 "issuer_match" => {
365 if let serde_yaml::Value::Null = v {
366 continue;
367 }
368 let s = v.as_str().ok_or_else(|| {
369 BridgesRegistryError::Invalid(format!(
370 "bridges[{index}].issuer_match must be a string"
371 ))
372 })?;
373 if s.is_empty() {
374 return Err(BridgesRegistryError::Invalid(format!(
375 "bridges[{index}].issuer_match must be non-empty"
376 )));
377 }
378 issuer_match = Some(s.to_string());
379 }
380 "iss_pattern" => {
381 if let serde_yaml::Value::Null = v {
382 continue;
383 }
384 let s = v.as_str().ok_or_else(|| {
385 BridgesRegistryError::Invalid(format!(
386 "bridges[{index}].iss_pattern must be a string"
387 ))
388 })?;
389 if s.is_empty() {
390 return Err(BridgesRegistryError::Invalid(format!(
391 "bridges[{index}].iss_pattern must be non-empty"
392 )));
393 }
394 iss_pattern = Some(s.to_string());
395 }
396 "trust_domain" => {
397 if let serde_yaml::Value::Null = v {
398 continue;
399 }
400 let s = v.as_str().ok_or_else(|| {
401 BridgesRegistryError::Invalid(format!(
402 "bridges[{index}].trust_domain must be a string"
403 ))
404 })?;
405 trust_domain = Some(s.to_string());
406 }
407 "trust_level" => {
408 if let serde_yaml::Value::Null = v {
409 continue;
410 }
411 let s = v.as_str().ok_or_else(|| {
412 BridgesRegistryError::Invalid(format!(
413 "bridges[{index}].trust_level must be a string"
414 ))
415 })?;
416 if !TRUST_LEVELS.contains(&s) {
417 return Err(BridgesRegistryError::Invalid(format!(
418 "bridges[{index}].trust_level must be T0..T7"
419 )));
420 }
421 trust_level = Some(s.to_string());
422 }
423 "capability_map" => {
424 if let serde_yaml::Value::Null = v {
425 continue;
426 }
427 let m = match v {
428 serde_yaml::Value::Mapping(m) => m,
429 _ => {
430 return Err(BridgesRegistryError::Invalid(format!(
431 "bridges[{index}].capability_map must be a mapping"
432 )))
433 }
434 };
435 let mut out = BTreeMap::new();
436 for (mk, mv) in m {
437 let mk = mk.as_str().ok_or_else(|| {
438 BridgesRegistryError::Invalid(format!(
439 "bridges[{index}].capability_map has non-string key"
440 ))
441 })?;
442 let mv = mv.as_str().ok_or_else(|| {
443 BridgesRegistryError::Invalid(format!(
444 "bridges[{index}].capability_map[{mk}] must be a string"
445 ))
446 })?;
447 if !validate_action_name(mv) {
448 return Err(BridgesRegistryError::Invalid(format!(
449 "bridges[{index}].capability_map[{mk}] must be a dotted action name"
450 )));
451 }
452 out.insert(mk.to_string(), mv.to_string());
453 }
454 capability_map = Some(out);
455 }
456 "profile" => {
457 if let serde_yaml::Value::Null = v {
458 continue;
459 }
460 let s = v.as_str().ok_or_else(|| {
461 BridgesRegistryError::Invalid(format!(
462 "bridges[{index}].profile must be a string"
463 ))
464 })?;
465 if !validate_profile(s) {
466 return Err(BridgesRegistryError::Invalid(format!(
467 "bridges[{index}].profile must match ^tf-[a-z][a-z0-9-]*-compatible$"
468 )));
469 }
470 profile = Some(s.to_string());
471 }
472 other => {
473 return Err(BridgesRegistryError::Invalid(format!(
474 "bridges[{index}]: unknown key {other}"
475 )));
476 }
477 }
478 }
479 let kind = kind.ok_or_else(|| {
480 BridgesRegistryError::Invalid(format!("bridges[{index}].kind is required"))
481 })?;
482 Ok(BridgeEntry {
483 kind,
484 issuer_match,
485 iss_pattern,
486 trust_domain,
487 trust_level,
488 capability_map,
489 profile,
490 })
491}