Skip to main content

boxxkite_client/
secrets.rs

1//! Org-scoped secrets for the proxy-substitution secrets broker
2//! (`docs/SECRETS-DESIGN.md`): `POST/GET/DELETE /v1/secrets*`. Mirrors
3//! `sdk-python`'s `create_secret`/`list_secrets`/`delete_secret`.
4//!
5//! The raw value is write-only: accepted on [`Client::create_secret`],
6//! never returned by any route here (list/create-response both omit it).
7
8use reqwest::Method;
9use serde::{Deserialize, Serialize};
10
11use crate::client::Client;
12use crate::error::BoxxkiteError;
13
14/// Builder for `POST /v1/secrets`'s optional fields.
15#[derive(Debug, Clone, Default, Serialize)]
16pub struct CreateSecretOptions {
17    #[serde(skip_serializing_if = "Option::is_none")]
18    trust_tier: Option<String>,
19}
20
21impl CreateSecretOptions {
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// Only meaningful for wallet/private-key-style secrets
27    /// (`docs/WALLET-SECRETS-DESIGN.md`) -- omit for an ordinary
28    /// API-key-style secret. The only accepted value today is
29    /// `"testnet"`; `"mainnet"` is refused (422).
30    pub fn trust_tier(mut self, trust_tier: impl Into<String>) -> Self {
31        self.trust_tier = Some(trust_tier.into());
32        self
33    }
34}
35
36/// A secret's metadata (`SecretOut`/`SecretCreatedResponse`). The raw value
37/// is never included here or anywhere else after creation.
38#[derive(Debug, Clone, Deserialize)]
39pub struct Secret {
40    pub id: String,
41    pub name: String,
42    pub allowed_hosts: Vec<String>,
43    pub trust_tier: Option<String>,
44    pub created_at: String,
45    pub last_used_at: Option<String>,
46}
47
48impl Client {
49    /// `POST /v1/secrets` -- register a new org-scoped secret.
50    ///
51    /// `name` must be unique per account; `value` is the real credential
52    /// value, write-only (never returned by this or any other route);
53    /// `allowed_hosts` are the destination hostnames this secret may be
54    /// used against via `http_request` -- required, not optional, since an
55    /// unscoped secret usable against any destination defeats the point of
56    /// this feature. A host that resolves to a private/link-local/
57    /// loopback/metadata address is rejected at creation time (a
58    /// best-effort backstop; see `docs/SECRETS-DESIGN.md` section 5 for why
59    /// the real control is the sidecar's request-time check).
60    pub async fn create_secret(
61        &self,
62        name: &str,
63        value: &str,
64        allowed_hosts: &[String],
65        options: CreateSecretOptions,
66    ) -> Result<Secret, BoxxkiteError> {
67        #[derive(Serialize)]
68        struct Body<'a> {
69            name: &'a str,
70            value: &'a str,
71            allowed_hosts: &'a [String],
72            #[serde(skip_serializing_if = "Option::is_none")]
73            trust_tier: Option<&'a str>,
74        }
75        let body = Body {
76            name,
77            value,
78            allowed_hosts,
79            trust_tier: options.trust_tier.as_deref(),
80        };
81        let builder = self.request(Method::POST, "/v1/secrets").json(&body);
82        self.send(builder).await
83    }
84
85    /// `GET /v1/secrets` -- secrets registered for this account. Raw values
86    /// are never returned here.
87    pub async fn list_secrets(&self) -> Result<Vec<Secret>, BoxxkiteError> {
88        let builder = self.request(Method::GET, "/v1/secrets");
89        self.send_or_default(builder).await
90    }
91
92    /// `DELETE /v1/secrets/{id}` -- delete a secret owned by this account.
93    /// 404s if already gone or never owned by this account.
94    pub async fn delete_secret(&self, secret_id: &str) -> Result<(), BoxxkiteError> {
95        let builder = self.request(Method::DELETE, &format!("/v1/secrets/{secret_id}"));
96        self.send_no_content(builder).await
97    }
98}