Skip to main content

nautilus_coinapi/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//! CoinAPI API key resolution and validation.
17//!
18//! REST and WebSocket requests send the same key in the `X-CoinAPI-Key` header.
19
20use nautilus_core::string::secret::SecretString;
21
22/// Environment variable read when no API key is configured.
23pub const COINAPI_API_KEY: &str = "COINAPI_API_KEY";
24
25/// Resolves the API key from `api_key`, or from `COINAPI_API_KEY` when it is `None`.
26///
27/// # Errors
28///
29/// Returns an error if neither source provides a key, or the key is not non-empty visible ASCII.
30pub fn resolve_api_key(api_key: Option<SecretString>) -> anyhow::Result<SecretString> {
31    let api_key = match api_key {
32        Some(api_key) => api_key,
33        None => std::env::var(COINAPI_API_KEY)
34            .map(SecretString::from)
35            .map_err(|_| {
36                anyhow::anyhow!("CoinAPI API key not configured; set api_key or {COINAPI_API_KEY}")
37            })?,
38    };
39
40    anyhow::ensure!(
41        is_valid_api_key(api_key.expose_secret()),
42        "CoinAPI API key must contain visible ASCII characters"
43    );
44    Ok(api_key)
45}
46
47/// Returns whether an API key is non-empty visible ASCII, as HTTP headers require.
48#[must_use]
49pub fn is_valid_api_key(key: &str) -> bool {
50    !key.is_empty() && key.bytes().all(|b| (33..=126).contains(&b))
51}
52
53#[cfg(test)]
54mod tests {
55    use rstest::rstest;
56
57    use super::*;
58
59    #[rstest]
60    fn resolve_prefers_explicit_key() {
61        let api_key = resolve_api_key(Some(SecretString::from("explicit-key"))).unwrap();
62
63        assert_eq!(api_key.expose_secret(), "explicit-key");
64    }
65
66    #[rstest]
67    #[case("")]
68    #[case("has space")]
69    #[case("tab\tkey")]
70    #[case("key\u{e9}")]
71    fn resolve_rejects_invalid_explicit_key(#[case] api_key: &str) {
72        let error = resolve_api_key(Some(SecretString::from(api_key))).unwrap_err();
73
74        assert_eq!(
75            error.to_string(),
76            "CoinAPI API key must contain visible ASCII characters"
77        );
78    }
79}