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}