1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! Client configuration and credential discovery.
//!
//! The SDK reads no settings file. Applications, such as the CLI, read their own
//! configuration and pass endpoints and credentials to [`ConfigBuilder`].
use std::time::Duration;
use reqwest::redirect::Policy;
use reqwest::{Client as ReqwestClient, Url};
use crate::Error;
use crate::auth::{TokenSource, default_auth_url, default_credentials};
use crate::client::endpoint;
use crate::http::http_client_builder;
/// The production control-plane endpoint.
pub const DEFAULT_API_URL: &str = "https://api.spiraldb.dev";
/// The production Spiral file-system endpoint, for the embedded runtime.
pub const DEFAULT_SPFS_URL: &str = "https://spfs.spiraldb.dev";
/// Resolved endpoints, HTTP pool and credentials shared by SDK clients.
#[derive(Clone)]
pub struct Config {
pub(crate) api_url: Url,
pub(crate) auth_url: Url,
pub(crate) scandal_url: Option<Url>,
pub(crate) auth: TokenSource,
pub(crate) http: ReqwestClient,
}
impl Config {
/// The default endpoints and the noninteractive default credential chain.
pub async fn load() -> Result<Self, Error> {
Self::builder().load().await
}
/// Configure endpoints and credentials.
pub fn builder() -> ConfigBuilder {
ConfigBuilder::default()
}
/// The resolved control-plane endpoint, including the token's resource when discovered.
pub fn api_url(&self) -> &Url {
&self.api_url
}
/// The remote scandal endpoint, when configured; Table operations then run there.
pub fn scandal_url(&self) -> Option<&Url> {
self.scandal_url.as_ref()
}
/// The shared credential provider, consulted before each authenticated request.
pub fn token_source(&self) -> &TokenSource {
&self.auth
}
}
/// Endpoints and credentials for SDK clients. Unset endpoints use the production defaults.
#[derive(Default)]
pub struct ConfigBuilder {
api_url: Option<String>,
scandal_url: Option<String>,
auth_url: Option<String>,
auth: Option<TokenSource>,
http: Option<ReqwestClient>,
fallback: Option<TokenSource>,
}
impl ConfigBuilder {
/// Set the API endpoint explicitly; credential discovery cannot replace it.
pub fn api_url(mut self, url: impl Into<String>) -> Self {
self.api_url = Some(url.into());
self
}
/// Select remote scandal at this endpoint.
pub fn scandal_url(mut self, url: impl Into<String>) -> Self {
self.scandal_url = Some(url.into());
self
}
/// Set the central authorization server used by discovered credentials.
pub fn auth_url(mut self, url: impl Into<String>) -> Self {
self.auth_url = Some(url.into());
self
}
/// Use caller-owned credentials instead of the default credential chain.
pub fn token_source(mut self, provider: TokenSource) -> Self {
self.auth = Some(provider);
self
}
/// Use a fixed bearer credential instead of discovered credentials.
pub fn bearer_token(self, token: impl Into<String>) -> Self {
self.token_source(TokenSource::fixed(token.into().into()))
}
/// Share a caller-owned HTTP pool across the resource clients.
///
/// Disable redirects on this pool to protect bearer credentials.
pub fn http_client(mut self, http: ReqwestClient) -> Self {
self.http = Some(http);
self
}
/// Add a final credential source after the SDK's environment and workload providers.
///
/// The caller controls this provider's refresh and interactive behavior.
pub fn fallback_token_source(mut self, provider: TokenSource) -> Self {
self.fallback = Some(provider);
self
}
/// Resolve credentials and discover the API endpoint before constructing clients.
///
/// Without a caller-owned provider, the default credential chain considers `SPIRAL_TOKEN`,
/// an environment private key, a legacy secret, workload identities and an optional caller
/// fallback, in that order. Errors stop discovery. Without an explicit API endpoint, the
/// client uses the endpoint returned with the credential.
pub async fn load(self) -> Result<Config, Error> {
let api_explicit = self.api_url.is_some();
let mut api_url = endpoint(self.api_url.as_deref().unwrap_or(DEFAULT_API_URL))?;
let scandal_url = self.scandal_url.as_deref().map(endpoint).transpose()?;
let auth_url = match self.auth_url.as_deref() {
Some(url) => endpoint(url)?,
None => default_auth_url(),
};
let auth = match self.auth {
Some(auth) => auth,
None => default_credentials(&api_url, &auth_url, self.fallback)?,
};
let token = auth.token().await?.ok_or(Error::CredentialsNotFound)?;
if !api_explicit && let Some(resource) = token.resource() {
api_url = endpoint(resource)?;
}
let http = match self.http {
Some(http) => http,
None => http_client_builder()
.redirect(Policy::none())
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(60))
.build()?,
};
Ok(Config {
api_url,
auth_url,
scandal_url,
auth,
http,
})
}
}