Skip to main content

systemprompt_config/bootstrap/secrets/
provider.rs

1//! Remote secrets provider seam.
2//!
3//! A provider returns a [`SecretsDocument`] — the raw JSON object holding the
4//! `secrets.json` shape — rather than a parsed [`Secrets`]. Keeping the
5//! untyped document one step longer is what lets per-key overrides be merged
6//! in before a single validation pass decides whether the result is usable.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use std::future::Future;
12
13use systemprompt_manifest::secrets::Secrets;
14
15use super::SecretsBootstrapError;
16
17pub trait SecretsProvider {
18    fn fetch(&self) -> impl Future<Output = Result<SecretsDocument, SecretsBootstrapError>> + Send;
19
20    fn describe(&self) -> String;
21}
22
23#[derive(Debug, Clone, Default)]
24// JSON: `secrets.json` document; keys are operator-defined until parsed.
25pub struct SecretsDocument(serde_json::Map<String, serde_json::Value>);
26
27impl SecretsDocument {
28    #[must_use]
29    // JSON: `secrets.json` document; keys are operator-defined until parsed.
30    pub const fn new(fields: serde_json::Map<String, serde_json::Value>) -> Self {
31        Self(fields)
32    }
33
34    // JSON: `secrets.json` document; keys are operator-defined until parsed.
35    pub fn merge_field(&mut self, key: impl Into<String>, value: serde_json::Value) {
36        self.0.insert(key.into(), value);
37    }
38
39    #[must_use]
40    pub fn key_names(&self) -> Vec<String> {
41        let mut names: Vec<String> = self.0.keys().cloned().collect();
42        names.sort_unstable();
43        names
44    }
45
46    #[must_use]
47    pub fn is_empty(&self) -> bool {
48        self.0.is_empty()
49    }
50
51    pub fn into_secrets(self) -> Result<Secrets, SecretsBootstrapError> {
52        let body = serde_json::to_string(&serde_json::Value::Object(self.0))
53            .map_err(SecretsBootstrapError::DocumentEncode)?;
54        Secrets::parse(&body).map_err(SecretsBootstrapError::InvalidSecretsFile)
55    }
56}