nautilus_architect_ax/common/
credential.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! AX Exchange API credential storage for bearer token authentication.
17
18use core::fmt::Debug;
19
20use zeroize::ZeroizeOnDrop;
21
22/// API credentials required for Ax bearer token authentication.
23///
24/// Ax uses bearer token authentication where the API key and secret
25/// are used to obtain a session token that is then used in the Authorization header.
26#[derive(Clone, ZeroizeOnDrop)]
27pub struct Credential {
28    api_key: Box<str>,
29    api_secret: Box<str>,
30}
31
32impl Debug for Credential {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        f.debug_struct(stringify!(Credential))
35            .field("api_key", &self.masked_api_key())
36            .field("api_secret", &"<redacted>")
37            .finish()
38    }
39}
40
41impl Credential {
42    /// Creates a new [`Credential`] instance from the API key and secret.
43    #[must_use]
44    pub fn new(api_key: impl Into<String>, api_secret: impl Into<String>) -> Self {
45        Self {
46            api_key: api_key.into().into_boxed_str(),
47            api_secret: api_secret.into().into_boxed_str(),
48        }
49    }
50
51    /// Returns the API key associated with this credential.
52    #[must_use]
53    pub fn api_key(&self) -> &str {
54        &self.api_key
55    }
56
57    /// Returns the API secret associated with this credential.
58    ///
59    /// # Safety
60    ///
61    /// The secret should be handled carefully and never logged or exposed.
62    #[must_use]
63    pub fn api_secret(&self) -> &str {
64        &self.api_secret
65    }
66
67    /// Returns a masked version of the API key for logging purposes.
68    ///
69    /// Shows first 4 and last 4 characters with ellipsis in between.
70    /// For keys shorter than 8 characters, shows asterisks only.
71    #[must_use]
72    pub fn masked_api_key(&self) -> String {
73        let key = self.api_key.as_ref();
74        let len = key.len();
75
76        if len <= 8 {
77            "*".repeat(len)
78        } else {
79            format!("{}...{}", &key[..4], &key[len - 4..])
80        }
81    }
82
83    /// Creates an Authorization header value for bearer token authentication.
84    ///
85    /// Returns the value to be used in the `Authorization` HTTP header.
86    #[must_use]
87    pub fn bearer_token(&self, session_token: &str) -> String {
88        format!("Bearer {session_token}")
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use rstest::rstest;
95
96    use super::*;
97
98    const API_KEY: &str = "test_api_key_123";
99    const API_SECRET: &str = "test_secret_456";
100
101    #[rstest]
102    fn test_credential_creation() {
103        let credential = Credential::new(API_KEY, API_SECRET);
104
105        assert_eq!(credential.api_key(), API_KEY);
106        assert_eq!(credential.api_secret(), API_SECRET);
107    }
108
109    #[rstest]
110    fn test_masked_api_key() {
111        let credential = Credential::new(API_KEY, API_SECRET);
112        let masked = credential.masked_api_key();
113
114        assert_eq!(masked, "test..._123");
115        assert!(!masked.contains("api_key"));
116    }
117
118    #[rstest]
119    fn test_masked_api_key_short() {
120        let credential = Credential::new("short", API_SECRET);
121        let masked = credential.masked_api_key();
122
123        assert_eq!(masked, "*****");
124    }
125
126    #[rstest]
127    fn test_bearer_token() {
128        let credential = Credential::new(API_KEY, API_SECRET);
129        let session_token = "abc123def456"; // gitleaks:allow
130        let auth_header = credential.bearer_token(session_token);
131
132        assert_eq!(auth_header, "Bearer abc123def456");
133    }
134
135    #[rstest]
136    fn test_debug_does_not_leak_secret() {
137        let credential = Credential::new(API_KEY, API_SECRET);
138        let debug_string = format!("{credential:?}");
139
140        assert!(!debug_string.contains(API_SECRET));
141        assert!(debug_string.contains("<redacted>"));
142        assert!(debug_string.contains("test..."));
143    }
144}