hessra_sdk/
lib.rs

1//! # Hessra SDK
2//!
3//! A Rust client library for interacting with Hessra authentication services.
4//!
5//! The Hessra SDK provides a robust and flexible way to request and verify authentication tokens
6//! for protected resources using mutual TLS (mTLS) for secure client authentication.
7//!
8//! This crate combines functionality from:
9//! - `hessra-token`: Token verification and attestation
10//! - `hessra-config`: Configuration management
11//! - `hessra-api`: HTTP client for the Hessra service
12//!
13//! ## Features
14//!
15//! - **Flexible configuration**: Load configuration from various sources (environment variables, files, etc.)
16//! - **Protocol support**: HTTP/1.1 support with optional HTTP/3 via feature flag
17//! - **Mutual TLS**: Strong security with client and server certificate validation
18//! - **Token management**: Request and verify authorization tokens
19//! - **Local verification**: Retrieve and store public keys for local token verification
20//! - **Service chains**: Support for service chain attestation and verification
21//!
22//! ## Feature Flags
23//!
24//! - `http3`: Enables HTTP/3 protocol support
25//! - `toml`: Enables configuration loading from TOML files
26//! - `wasm`: Enables WebAssembly support for token verification
27
28use std::fs::File;
29use std::io::Read;
30use std::path::Path;
31use thiserror::Error;
32
33// Re-export everything from the component crates
34pub use hessra_token::{
35    // Token attestation
36    add_service_node_attestation,
37    decode_token,
38    encode_token,
39    // Token verification
40    verify_biscuit_local,
41    verify_service_chain_biscuit_local,
42    // Re-exported biscuit types
43    Biscuit,
44    KeyPair,
45    PublicKey,
46    // Service chain types
47    ServiceNode,
48    // Token errors
49    TokenError,
50};
51
52pub use hessra_config::{ConfigError, HessraConfig, Protocol};
53
54pub use hessra_api::{
55    ApiError, HessraClient, HessraClientBuilder, PublicKeyResponse, SignTokenRequest,
56    SignTokenResponse, SignoffInfo, TokenRequest, TokenResponse, VerifyServiceChainTokenRequest,
57    VerifyTokenRequest, VerifyTokenResponse,
58};
59
60/// Errors that can occur in the Hessra SDK
61#[derive(Error, Debug)]
62pub enum SdkError {
63    /// Configuration error
64    #[error("Configuration error: {0}")]
65    Config(#[from] ConfigError),
66
67    /// API error
68    #[error("API error: {0}")]
69    Api(#[from] ApiError),
70
71    /// Token error
72    #[error("Token error: {0}")]
73    Token(#[from] TokenError),
74
75    /// JSON serialization error
76    #[error("JSON error: {0}")]
77    Json(#[from] serde_json::Error),
78
79    /// I/O error
80    #[error("I/O error: {0}")]
81    Io(#[from] std::io::Error),
82
83    /// Generic error
84    #[error("{0}")]
85    Generic(String),
86}
87
88/// A chain of service nodes
89///
90/// Represents an ordered sequence of service nodes that form a processing chain.
91/// The order of nodes in the chain is significant - it defines the expected
92/// order of processing and attestation.
93#[derive(Clone, Debug, Default)]
94pub struct ServiceChain {
95    /// The nodes in the chain, in order
96    nodes: Vec<ServiceNode>,
97}
98
99impl ServiceChain {
100    /// Create a new empty service chain
101    pub fn new() -> Self {
102        Self { nodes: Vec::new() }
103    }
104
105    /// Create a service chain with the given nodes
106    pub fn with_nodes(nodes: Vec<ServiceNode>) -> Self {
107        Self { nodes }
108    }
109
110    /// Create a new service chain builder
111    pub fn builder() -> ServiceChainBuilder {
112        ServiceChainBuilder::new()
113    }
114
115    /// Add a node to the chain
116    pub fn add_node(&mut self, node: ServiceNode) -> &mut Self {
117        self.nodes.push(node);
118        self
119    }
120
121    /// Add a node to the chain (builder style)
122    pub fn with_node(mut self, node: ServiceNode) -> Self {
123        self.nodes.push(node);
124        self
125    }
126
127    /// Get the nodes in the chain
128    pub fn nodes(&self) -> &[ServiceNode] {
129        &self.nodes
130    }
131
132    /// Convert to internal representation for token verification
133    fn to_internal(&self) -> Vec<hessra_token::ServiceNode> {
134        self.nodes.to_vec()
135    }
136
137    /// Load a service chain from a JSON string
138    pub fn from_json(json: &str) -> Result<Self, SdkError> {
139        let nodes: Vec<ServiceNode> = serde_json::from_str(json)?;
140        Ok(Self::with_nodes(nodes))
141    }
142
143    /// Load a service chain from a JSON file
144    pub fn from_json_file(path: impl AsRef<Path>) -> Result<Self, SdkError> {
145        let mut file = File::open(path)?;
146        let mut contents = String::new();
147        file.read_to_string(&mut contents)?;
148        Self::from_json(&contents)
149    }
150
151    /// Load a service chain from a TOML string
152    #[cfg(feature = "toml")]
153    pub fn from_toml(toml_str: &str) -> Result<Self, SdkError> {
154        use serde::Deserialize;
155
156        #[derive(Deserialize)]
157        struct TomlServiceChain {
158            nodes: Vec<ServiceNode>,
159        }
160
161        let chain: TomlServiceChain = toml::from_str(toml_str)
162            .map_err(|e| SdkError::Generic(format!("TOML parse error: {e}")))?;
163
164        Ok(Self::with_nodes(chain.nodes))
165    }
166
167    /// Load a service chain from a TOML file
168    #[cfg(feature = "toml")]
169    pub fn from_toml_file(path: impl AsRef<Path>) -> Result<Self, SdkError> {
170        let mut file = File::open(path)?;
171        let mut contents = String::new();
172        file.read_to_string(&mut contents)?;
173        Self::from_toml(&contents)
174    }
175}
176
177/// Builder for a service chain
178#[derive(Debug, Default)]
179pub struct ServiceChainBuilder {
180    nodes: Vec<ServiceNode>,
181}
182
183impl ServiceChainBuilder {
184    /// Create a new service chain builder
185    pub fn new() -> Self {
186        Self::default()
187    }
188
189    /// Add a node to the chain
190    pub fn add_node(mut self, node: ServiceNode) -> Self {
191        self.nodes.push(node);
192        self
193    }
194
195    /// Build the service chain
196    pub fn build(self) -> ServiceChain {
197        ServiceChain::with_nodes(self.nodes)
198    }
199}
200
201/// Unified SDK for Hessra authentication services
202///
203/// This struct provides a high-level interface combining functionality
204/// from all component crates (config, token, api).
205pub struct Hessra {
206    client: HessraClient,
207    config: HessraConfig,
208}
209
210impl Hessra {
211    /// Create a new Hessra SDK instance from a configuration
212    pub fn new(config: HessraConfig) -> Result<Self, SdkError> {
213        let client = HessraClientBuilder::new()
214            .from_config(&config)
215            .build()
216            .map_err(|e| SdkError::Generic(e.to_string()))?;
217
218        Ok(Self { client, config })
219    }
220
221    /// Create a builder for a Hessra SDK instance
222    pub fn builder() -> HessraBuilder {
223        HessraBuilder::new()
224    }
225
226    /// Setup the SDK with the public key
227    ///
228    /// This will fetch the public key from the Hessra service and set it in the SDK configuration.
229    /// If the public key is already set, it will be overwritten.
230    /// Requires a mutable reference to the SDK instance.
231    pub async fn setup(&mut self) -> Result<(), SdkError> {
232        match self.get_public_key().await {
233            Ok(public_key) => {
234                self.config.public_key = Some(public_key);
235                Ok(())
236            }
237            Err(e) => Err(SdkError::Generic(e.to_string())),
238        }
239    }
240
241    /// Setup the SDK with the public key and return a new instance
242    ///
243    /// This will fetch the public key from the Hessra service and set it in the SDK configuration.
244    /// If the public key is already set, it will be overwritten.
245    pub async fn with_setup(&self) -> Result<Self, SdkError> {
246        match self.get_public_key().await {
247            Ok(public_key) => {
248                let config = self.config.to_builder().public_key(public_key).build()?;
249                Ok(Self::new(config)?)
250            }
251            Err(e) => Err(SdkError::Generic(e.to_string())),
252        }
253    }
254
255    /// Request a token for a resource
256    /// Returns the full TokenResponse which may include pending signoffs for multi-party tokens
257    pub async fn request_token(
258        &self,
259        resource: impl Into<String>,
260        operation: impl Into<String>,
261    ) -> Result<TokenResponse, SdkError> {
262        self.client
263            .request_token(resource.into(), operation.into())
264            .await
265            .map_err(|e| SdkError::Generic(e.to_string()))
266    }
267
268    /// Request a token for a resource (simple version)
269    /// Returns just the token string for backward compatibility
270    pub async fn request_token_simple(
271        &self,
272        resource: impl Into<String>,
273        operation: impl Into<String>,
274    ) -> Result<String, SdkError> {
275        let response = self.request_token(resource, operation).await?;
276        match response.token {
277            Some(token) => Ok(token),
278            None => Err(SdkError::Generic(format!(
279                "Failed to get token: {}",
280                response.response_msg
281            ))),
282        }
283    }
284
285    /// Sign a multi-party token by calling an authorization service's signoff endpoint
286    pub async fn sign_token(
287        &self,
288        token: &str,
289        resource: &str,
290        operation: &str,
291    ) -> Result<SignTokenResponse, SdkError> {
292        self.client
293            .sign_token(token, resource, operation)
294            .await
295            .map_err(|e| SdkError::Generic(e.to_string()))
296    }
297
298    /// Parse an authorization service URL to extract base URL and port
299    /// Handles URLs like "https://hostname:port/path" or "hostname:port/path"
300    /// Returns (base_url, port) where base_url is just the hostname part
301    fn parse_authorization_service_url(url: &str) -> Result<(String, Option<u16>), SdkError> {
302        let url_str = if url.starts_with("http://") || url.starts_with("https://") {
303            url.to_string()
304        } else {
305            // If no protocol, assume https for parsing
306            format!("https://{url}")
307        };
308
309        let parsed_url = url::Url::parse(&url_str).map_err(|e| {
310            SdkError::Generic(format!(
311                "Failed to parse authorization service URL '{url}': {e}"
312            ))
313        })?;
314
315        let host = parsed_url
316            .host_str()
317            .ok_or_else(|| SdkError::Generic(format!("No host found in URL: {url}")))?;
318
319        // For URLs where port is not explicitly specified but the scheme indicates a default port,
320        // we need to check if the original URL had an explicit port
321        let port = if parsed_url.port().is_some() {
322            parsed_url.port()
323        } else if url.contains(':') && !url.starts_with("http://") && !url.starts_with("https://") {
324            // If the original URL has a colon and no protocol, it likely has an explicit port
325            // Try to extract it manually
326            if let Some(host_port) = url.split('/').next() {
327                if let Some(port_str) = host_port.split(':').nth(1) {
328                    port_str.parse::<u16>().ok()
329                } else {
330                    None
331                }
332            } else {
333                None
334            }
335        } else {
336            parsed_url.port()
337        };
338
339        Ok((host.to_string(), port))
340    }
341
342    /// Collect all required signoffs for a multi-party token
343    /// Returns the fully signed token once all signoffs are collected
344    pub async fn collect_signoffs(
345        &self,
346        initial_token_response: TokenResponse,
347        resource: &str,
348        operation: &str,
349    ) -> Result<String, SdkError> {
350        // If no pending signoffs, return the token immediately
351        let pending_signoffs = match &initial_token_response.pending_signoffs {
352            Some(signoffs) if !signoffs.is_empty() => signoffs,
353            _ => {
354                return initial_token_response
355                    .token
356                    .ok_or_else(|| SdkError::Generic("No token in response".to_string()))
357            }
358        };
359
360        let mut current_token = initial_token_response.token.ok_or_else(|| {
361            SdkError::Generic("No initial token to collect signoffs for".to_string())
362        })?;
363
364        // For each SignoffInfo in pending_signoffs, create a client and call sign_token
365        for signoff_info in pending_signoffs {
366            // Parse the authorization service URL to extract base URL and port
367            let (base_url, port) =
368                Self::parse_authorization_service_url(&signoff_info.authorization_service)?;
369
370            // Create a temporary client for this authorization service
371            // Note: This is a simplified approach. In practice, you might want to
372            // have a configuration system for managing multiple service certificates
373            let mut client_builder = HessraClientBuilder::new()
374                .base_url(base_url)
375                .protocol(self.config.protocol.clone())
376                .mtls_cert(self.config.mtls_cert.clone())
377                .mtls_key(self.config.mtls_key.clone())
378                .server_ca(self.config.server_ca.clone());
379
380            if let Some(port) = port {
381                client_builder = client_builder.port(port);
382            }
383
384            let signoff_client = client_builder
385                .build()
386                .map_err(|e| SdkError::Generic(format!("Failed to create signoff client: {e}")))?;
387
388            let sign_response = signoff_client
389                .sign_token(&current_token, resource, operation)
390                .await
391                .map_err(|e| {
392                    SdkError::Generic(format!(
393                        "Signoff failed for {}: {e}",
394                        signoff_info.component
395                    ))
396                })?;
397
398            current_token = sign_response.signed_token.ok_or_else(|| {
399                SdkError::Generic(format!(
400                    "No signed token returned from {}: {}",
401                    signoff_info.component, sign_response.response_msg
402                ))
403            })?;
404        }
405
406        Ok(current_token)
407    }
408
409    /// Request a token and automatically collect any required signoffs
410    /// This is a convenience method that combines token request and signoff collection
411    pub async fn request_token_with_signoffs(
412        &self,
413        resource: &str,
414        operation: &str,
415    ) -> Result<String, SdkError> {
416        let initial_response = self.request_token(resource, operation).await?;
417        self.collect_signoffs(initial_response, resource, operation)
418            .await
419    }
420
421    /// Verify a token
422    ///
423    /// This function verifies a token using either the remote Hessra service or
424    /// locally using the service's public key if one is configured. This will always
425    /// prefer to verify locally if a public key is configured.
426    pub async fn verify_token(
427        &self,
428        token: impl Into<String>,
429        subject: impl Into<String>,
430        resource: impl Into<String>,
431        operation: impl Into<String>,
432    ) -> Result<(), SdkError> {
433        if self.config.public_key.is_some() {
434            self.verify_token_local(
435                token.into(),
436                subject.into(),
437                resource.into(),
438                operation.into(),
439            )
440        } else {
441            self.verify_token_remote(
442                token.into(),
443                subject.into(),
444                resource.into(),
445                operation.into(),
446            )
447            .await
448            .map(|_| ())
449            .map_err(|e| SdkError::Generic(e.to_string()))
450        }
451    }
452
453    /// Verify a token using the remote Hessra service
454    pub async fn verify_token_remote(
455        &self,
456        token: impl Into<String>,
457        subject: impl Into<String>,
458        resource: impl Into<String>,
459        operation: impl Into<String>,
460    ) -> Result<String, SdkError> {
461        self.client
462            .verify_token(
463                token.into(),
464                subject.into(),
465                resource.into(),
466                operation.into(),
467            )
468            .await
469            .map_err(|e| SdkError::Generic(e.to_string()))
470    }
471
472    /// Verify a token locally using cached public keys
473    pub fn verify_token_local(
474        &self,
475        token: impl Into<String>,
476        subject: impl AsRef<str>,
477        resource: impl AsRef<str>,
478        operation: impl AsRef<str>,
479    ) -> Result<(), SdkError> {
480        let public_key_str = match &self.config.public_key {
481            Some(key) => key,
482            None => return Err(SdkError::Generic("Public key not configured".to_string())),
483        };
484
485        let public_key = PublicKey::from_pem(public_key_str.as_str())
486            .map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
487
488        // Convert token to Vec<u8>
489        let token_vec = decode_token(&token.into())?;
490
491        verify_biscuit_local(
492            token_vec,
493            public_key,
494            subject.as_ref().to_string(),
495            resource.as_ref().to_string(),
496            operation.as_ref().to_string(),
497        )
498        .map_err(SdkError::Token)
499    }
500
501    /// Verify a service chain token
502    ///
503    /// This function verifies a service chain token using either the remote Hessra service or
504    /// locally using the service's public key if one is configured. This will always
505    /// prefer to verify locally if a public key is configured and a service chain is provided.
506    pub async fn verify_service_chain_token(
507        &self,
508        token: impl Into<String>,
509        subject: impl Into<String>,
510        resource: impl Into<String>,
511        operation: impl Into<String>,
512        service_chain: Option<&ServiceChain>,
513        component: Option<String>,
514    ) -> Result<(), SdkError> {
515        match (&self.config.public_key, service_chain) {
516            (Some(_), Some(chain)) => self.verify_service_chain_token_local(
517                token.into(),
518                subject.into(),
519                resource.into(),
520                operation.into(),
521                chain,
522                component,
523            ),
524            _ => self
525                .verify_service_chain_token_remote(
526                    token.into(),
527                    subject.into(),
528                    resource.into(),
529                    component,
530                )
531                .await
532                .map(|_| ())
533                .map_err(|e| SdkError::Generic(e.to_string())),
534        }
535    }
536
537    /// Verify a service chain token using the remote Hessra service
538    pub async fn verify_service_chain_token_remote(
539        &self,
540        token: impl Into<String>,
541        subject: impl Into<String>,
542        resource: impl Into<String>,
543        component: Option<String>,
544    ) -> Result<String, SdkError> {
545        self.client
546            .verify_service_chain_token(token.into(), subject.into(), resource.into(), component)
547            .await
548            .map_err(|e| SdkError::Generic(e.to_string()))
549    }
550
551    /// Verify a service chain token locally using cached public keys
552    pub fn verify_service_chain_token_local(
553        &self,
554        token: String,
555        subject: impl AsRef<str>,
556        resource: impl AsRef<str>,
557        operation: impl AsRef<str>,
558        service_chain: &ServiceChain,
559        component: Option<String>,
560    ) -> Result<(), SdkError> {
561        let public_key_str = match &self.config.public_key {
562            Some(key) => key,
563            None => return Err(SdkError::Generic("Public key not configured".to_string())),
564        };
565
566        let public_key = PublicKey::from_pem(public_key_str.as_str())
567            .map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
568
569        // Convert token to Vec<u8>
570        let token_vec = decode_token(&token)?;
571
572        verify_service_chain_biscuit_local(
573            token_vec,
574            public_key,
575            subject.as_ref().to_string(),
576            resource.as_ref().to_string(),
577            operation.as_ref().to_string(),
578            service_chain.to_internal(),
579            component,
580        )
581        .map_err(SdkError::Token)
582    }
583
584    /// Attest a service chain token with a new service node attestation
585    /// Expects a base64 encoded token string and a service name
586    /// Returns a base64 encoded token string
587    pub fn attest_service_chain_token(
588        &self,
589        token: String,
590        service: impl Into<String>,
591    ) -> Result<String, SdkError> {
592        let keypair_str = match &self.config.personal_keypair {
593            Some(keypair) => keypair,
594            None => {
595                return Err(SdkError::Generic(
596                    "Personal keypair not configured".to_string(),
597                ))
598            }
599        };
600
601        let public_key_str = match &self.config.public_key {
602            Some(key) => key,
603            None => return Err(SdkError::Generic("Public key not configured".to_string())),
604        };
605
606        // Parse keypair from string to KeyPair
607        let keypair = KeyPair::from_private_key_pem(keypair_str.as_str())
608            .map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
609
610        // Parse public key from PEM string
611        let public_key = PublicKey::from_pem(public_key_str.as_str())
612            .map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
613
614        // Convert token to Vec<u8>
615        let token_vec = decode_token(&token)?;
616
617        // Convert service to String
618        let service_str = service.into();
619
620        let token_vec = add_service_node_attestation(token_vec, public_key, &service_str, &keypair)
621            .map_err(SdkError::Token)?;
622
623        Ok(encode_token(&token_vec))
624    }
625
626    /// Get the public key from the Hessra service
627    pub async fn get_public_key(&self) -> Result<String, SdkError> {
628        self.client
629            .get_public_key()
630            .await
631            .map_err(|e| SdkError::Generic(e.to_string()))
632    }
633
634    /// Get the client used by this SDK instance
635    pub fn client(&self) -> &HessraClient {
636        &self.client
637    }
638
639    /// Get the configuration used by this SDK instance
640    pub fn config(&self) -> &HessraConfig {
641        &self.config
642    }
643}
644
645/// Builder for Hessra SDK instances
646#[derive(Default)]
647pub struct HessraBuilder {
648    config_builder: hessra_config::HessraConfigBuilder,
649}
650
651impl HessraBuilder {
652    /// Create a new Hessra SDK builder
653    pub fn new() -> Self {
654        Self {
655            config_builder: HessraConfig::builder(),
656        }
657    }
658
659    /// Set the base URL for the Hessra service
660    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
661        self.config_builder = self.config_builder.base_url(base_url);
662        self
663    }
664
665    /// Set the mTLS private key
666    pub fn mtls_key(mut self, mtls_key: impl Into<String>) -> Self {
667        self.config_builder = self.config_builder.mtls_key(mtls_key);
668        self
669    }
670
671    /// Set the mTLS client certificate
672    pub fn mtls_cert(mut self, mtls_cert: impl Into<String>) -> Self {
673        self.config_builder = self.config_builder.mtls_cert(mtls_cert);
674        self
675    }
676
677    /// Set the server CA certificate
678    pub fn server_ca(mut self, server_ca: impl Into<String>) -> Self {
679        self.config_builder = self.config_builder.server_ca(server_ca);
680        self
681    }
682
683    /// Set the port for the Hessra service
684    pub fn port(mut self, port: u16) -> Self {
685        self.config_builder = self.config_builder.port(port);
686        self
687    }
688
689    /// Set the protocol to use
690    pub fn protocol(mut self, protocol: Protocol) -> Self {
691        self.config_builder = self.config_builder.protocol(protocol);
692        self
693    }
694
695    /// Set the public key for token verification
696    pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
697        self.config_builder = self.config_builder.public_key(public_key);
698        self
699    }
700
701    /// Set the personal keypair for service chain attestation
702    pub fn personal_keypair(mut self, keypair: impl Into<String>) -> Self {
703        self.config_builder = self.config_builder.personal_keypair(keypair);
704        self
705    }
706
707    /// Build a Hessra SDK instance
708    pub fn build(self) -> Result<Hessra, SdkError> {
709        let config = self.config_builder.build()?;
710        Hessra::new(config)
711    }
712}
713
714/// Fetch a public key from the Hessra service
715///
716/// This is a convenience function that doesn't require a fully configured client.
717pub async fn fetch_public_key(
718    base_url: impl Into<String>,
719    port: Option<u16>,
720    server_ca: impl Into<String>,
721) -> Result<String, SdkError> {
722    HessraClient::fetch_public_key(base_url, port, server_ca)
723        .await
724        .map_err(|e| SdkError::Generic(e.to_string()))
725}
726
727/// Fetch a public key from the Hessra service using HTTP/3
728///
729/// This is a convenience function that doesn't require a fully configured client.
730#[cfg(feature = "http3")]
731pub async fn fetch_public_key_http3(
732    base_url: impl Into<String>,
733    port: Option<u16>,
734    server_ca: impl Into<String>,
735) -> Result<String, SdkError> {
736    HessraClient::fetch_public_key_http3(base_url, port, server_ca)
737        .await
738        .map_err(|e| SdkError::Generic(e.to_string()))
739}
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744
745    #[test]
746    fn test_service_chain_creation() {
747        // Create a simple service chain with two nodes
748        let json = r#"[
749            {
750                "component": "service1",
751                "public_key": "ed25519/abcdef1234567890"
752            },
753            {
754                "component": "service2",
755                "public_key": "ed25519/0987654321fedcba"
756            }
757        ]"#;
758
759        let service_chain = ServiceChain::from_json(json).unwrap();
760        assert_eq!(service_chain.nodes().len(), 2);
761        assert_eq!(service_chain.nodes()[0].component, "service1");
762        assert_eq!(
763            service_chain.nodes()[0].public_key,
764            "ed25519/abcdef1234567890"
765        );
766        assert_eq!(service_chain.nodes()[1].component, "service2");
767        assert_eq!(
768            service_chain.nodes()[1].public_key,
769            "ed25519/0987654321fedcba"
770        );
771
772        // Test adding a node
773        let mut chain = ServiceChain::new();
774        let node = ServiceNode {
775            component: "service3".to_string(),
776            public_key: "ed25519/1122334455667788".to_string(),
777        };
778        chain.add_node(node);
779        assert_eq!(chain.nodes().len(), 1);
780        assert_eq!(chain.nodes()[0].component, "service3");
781    }
782
783    #[test]
784    fn test_service_chain_builder() {
785        let builder = ServiceChainBuilder::new();
786        let node1 = ServiceNode {
787            component: "auth".to_string(),
788            public_key: "ed25519/auth123".to_string(),
789        };
790        let node2 = ServiceNode {
791            component: "payment".to_string(),
792            public_key: "ed25519/payment456".to_string(),
793        };
794
795        let chain = builder.add_node(node1).add_node(node2).build();
796
797        assert_eq!(chain.nodes().len(), 2);
798        assert_eq!(chain.nodes()[0].component, "auth");
799        assert_eq!(chain.nodes()[1].component, "payment");
800    }
801
802    #[test]
803    fn test_parse_authorization_service_url() {
804        // Test URL with https protocol and path
805        let (base_url, port) =
806            Hessra::parse_authorization_service_url("https://127.0.0.1:4433/sign_token").unwrap();
807        assert_eq!(base_url, "127.0.0.1");
808        assert_eq!(port, Some(4433));
809
810        // Test URL with http protocol
811        let (base_url, port) =
812            Hessra::parse_authorization_service_url("http://example.com:8080/api/sign").unwrap();
813        assert_eq!(base_url, "example.com");
814        assert_eq!(port, Some(8080));
815
816        // Test URL without protocol but with port and path
817        let (base_url, port) =
818            Hessra::parse_authorization_service_url("test.hessra.net:443/sign_token").unwrap();
819        assert_eq!(base_url, "test.hessra.net");
820        assert_eq!(port, Some(443));
821
822        // Test URL without protocol and without port
823        let (base_url, port) =
824            Hessra::parse_authorization_service_url("example.com/api/endpoint").unwrap();
825        assert_eq!(base_url, "example.com");
826        assert_eq!(port, None);
827
828        // Test URL with just hostname and port (no path)
829        let (base_url, port) =
830            Hessra::parse_authorization_service_url("https://localhost:8443").unwrap();
831        assert_eq!(base_url, "localhost");
832        assert_eq!(port, Some(8443));
833
834        // Test hostname only (no protocol, port, or path)
835        let (base_url, port) = Hessra::parse_authorization_service_url("api.example.org").unwrap();
836        assert_eq!(base_url, "api.example.org");
837        assert_eq!(port, None);
838    }
839}