greentic_types/
secrets.rs

1//! Canonical secret requirement primitives shared across Greentic crates.
2//! All repos must use these helpers; local re-implementation is forbidden.
3
4use crate::{ErrorCode, GResult, GreenticError};
5use alloc::{format, string::String, vec::Vec};
6use core::ops::Deref;
7#[cfg(feature = "schemars")]
8use schemars::JsonSchema;
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12/// Canonical secret identifier used across manifests and bindings.
13#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
14#[cfg_attr(feature = "serde", derive(Serialize))]
15#[cfg_attr(feature = "serde", serde(transparent))]
16#[cfg_attr(feature = "schemars", derive(JsonSchema))]
17pub struct SecretKey(String);
18
19impl SecretKey {
20    /// Constructs a secret key and validates the identifier format.
21    pub fn new(key: impl Into<String>) -> GResult<Self> {
22        let key = key.into();
23        Self::parse(&key).map_err(|err| {
24            GreenticError::new(
25                ErrorCode::InvalidInput,
26                format!("invalid secret key: {err}"),
27            )
28        })
29    }
30
31    /// Returns the key as a string slice.
32    pub fn as_str(&self) -> &str {
33        &self.0
34    }
35
36    /// Parses and validates a secret key string.
37    ///
38    /// Validation rules:
39    /// - must be non-empty
40    /// - allowed characters: ASCII `a-zA-Z0-9._-/`
41    /// - must not start with `/`
42    /// - must not contain a `..` path segment
43    pub fn parse(value: &str) -> Result<Self, SecretKeyError> {
44        if value.is_empty() {
45            return Err(SecretKeyError::Empty);
46        }
47        if value.starts_with('/') {
48            return Err(SecretKeyError::LeadingSlash);
49        }
50        for c in value.chars() {
51            if !(c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | '/')) {
52                return Err(SecretKeyError::InvalidChar { c });
53            }
54        }
55        if value.split('/').any(|segment| segment == "..") {
56            return Err(SecretKeyError::DotDotSegment);
57        }
58        Ok(Self(value.to_owned()))
59    }
60}
61
62/// Validation errors produced by [`SecretKey::parse`].
63#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
64pub enum SecretKeyError {
65    /// Input was empty.
66    #[error("secret key must not be empty")]
67    Empty,
68    /// Input started with `/`.
69    #[error("secret key must not start with '/'")]
70    LeadingSlash,
71    /// Input contained a `..` path segment.
72    #[error("secret key must not contain '..' segments")]
73    DotDotSegment,
74    /// Input contained a disallowed character.
75    #[error("secret key contains invalid character '{c}'")]
76    InvalidChar {
77        /// The offending character.
78        c: char,
79    },
80}
81
82impl Deref for SecretKey {
83    type Target = str;
84
85    fn deref(&self) -> &Self::Target {
86        &self.0
87    }
88}
89
90impl From<String> for SecretKey {
91    fn from(key: String) -> Self {
92        Self(key)
93    }
94}
95
96impl From<&str> for SecretKey {
97    fn from(key: &str) -> Self {
98        Self(key.to_owned())
99    }
100}
101
102impl From<SecretKey> for String {
103    fn from(key: SecretKey) -> Self {
104        key.0
105    }
106}
107
108#[cfg(feature = "serde")]
109impl<'de> Deserialize<'de> for SecretKey {
110    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111    where
112        D: serde::Deserializer<'de>,
113    {
114        let value = String::deserialize(deserializer)?;
115        SecretKey::parse(&value).map_err(serde::de::Error::custom)
116    }
117}
118
119/// Canonical secret scope (environment, tenant, team).
120#[derive(Clone, Debug, PartialEq, Eq)]
121#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
122#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
123#[cfg_attr(feature = "schemars", derive(JsonSchema))]
124pub struct SecretScope {
125    /// Environment identifier (e.g., `dev`, `prod`).
126    pub env: String,
127    /// Tenant identifier within the environment.
128    pub tenant: String,
129    /// Optional team for finer-grained isolation.
130    #[cfg_attr(
131        feature = "serde",
132        serde(default, skip_serializing_if = "Option::is_none")
133    )]
134    pub team: Option<String>,
135}
136
137/// Supported secret content formats.
138#[derive(Clone, Debug, PartialEq, Eq)]
139#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
140#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
141#[cfg_attr(feature = "schemars", derive(JsonSchema))]
142pub enum SecretFormat {
143    /// Arbitrary bytes.
144    Bytes,
145    /// UTF-8 text.
146    Text,
147    /// JSON document.
148    Json,
149}
150
151/// Structured secret requirement used in capabilities, bindings, and deployment plans.
152#[non_exhaustive]
153#[derive(Clone, Debug, PartialEq, Eq)]
154#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
155#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
156#[cfg_attr(feature = "schemars", derive(JsonSchema))]
157pub struct SecretRequirement {
158    /// Logical key the runtime should resolve.
159    pub key: SecretKey,
160    /// Whether the secret is mandatory for execution.
161    #[cfg_attr(
162        feature = "serde",
163        serde(default = "SecretRequirement::default_required")
164    )]
165    pub required: bool,
166    /// Optional description for operator-facing tooling.
167    #[cfg_attr(
168        feature = "serde",
169        serde(default, skip_serializing_if = "Option::is_none")
170    )]
171    pub description: Option<String>,
172    /// Expected scope for resolution (environment/tenant/team).
173    #[cfg_attr(
174        feature = "serde",
175        serde(default, skip_serializing_if = "Option::is_none")
176    )]
177    pub scope: Option<SecretScope>,
178    /// Preferred secret format when known.
179    #[cfg_attr(
180        feature = "serde",
181        serde(default, skip_serializing_if = "Option::is_none")
182    )]
183    pub format: Option<SecretFormat>,
184    /// Optional JSON Schema fragment describing the value shape.
185    #[cfg_attr(
186        feature = "serde",
187        serde(default, skip_serializing_if = "Option::is_none")
188    )]
189    pub schema: Option<serde_json::Value>,
190    /// Example payloads for documentation.
191    #[cfg_attr(
192        feature = "serde",
193        serde(default, skip_serializing_if = "Vec::is_empty")
194    )]
195    pub examples: Vec<String>,
196}
197
198impl Default for SecretRequirement {
199    fn default() -> Self {
200        Self {
201            key: SecretKey::default(),
202            required: true,
203            description: None,
204            scope: None,
205            format: None,
206            schema: None,
207            examples: Vec::new(),
208        }
209    }
210}
211
212impl SecretRequirement {
213    const fn default_required() -> bool {
214        true
215    }
216}