1use 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#[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 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 pub fn as_str(&self) -> &str {
33 &self.0
34 }
35
36 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 pub fn parse_canonical(value: &str) -> Result<Self, SecretKeyError> {
70 Self::parse(value)?;
72
73 if value.chars().any(|c| c.is_ascii_uppercase()) {
75 return Err(SecretKeyError::Uppercase);
76 }
77
78 if value.ends_with('/') {
80 return Err(SecretKeyError::TrailingSlash);
81 }
82
83 Ok(Self(value.to_owned()))
84 }
85
86 pub fn normalize(value: impl Into<String>) -> Result<Self, SecretKeyError> {
92 let lowered = value.into().to_ascii_lowercase();
93 Self::parse_canonical(&lowered)
94 }
95}
96
97#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
99pub enum SecretKeyError {
100 #[error("secret key must not be empty")]
102 Empty,
103 #[error("secret key must not start with '/'")]
105 LeadingSlash,
106 #[error("secret key must not contain '..' segments")]
108 DotDotSegment,
109 #[error("secret key contains invalid character '{c}'")]
111 InvalidChar {
112 c: char,
114 },
115 #[error("canonical secret key must not contain uppercase letters")]
119 Uppercase,
120 #[error("canonical secret key must not end with '/'")]
122 TrailingSlash,
123}
124
125impl Deref for SecretKey {
126 type Target = str;
127
128 fn deref(&self) -> &Self::Target {
129 &self.0
130 }
131}
132
133impl From<String> for SecretKey {
134 fn from(key: String) -> Self {
135 Self(key)
136 }
137}
138
139impl From<&str> for SecretKey {
140 fn from(key: &str) -> Self {
141 Self(key.to_owned())
142 }
143}
144
145impl From<SecretKey> for String {
146 fn from(key: SecretKey) -> Self {
147 key.0
148 }
149}
150
151#[cfg(feature = "serde")]
152impl<'de> Deserialize<'de> for SecretKey {
153 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
154 where
155 D: serde::Deserializer<'de>,
156 {
157 let value = String::deserialize(deserializer)?;
158 SecretKey::parse(&value).map_err(serde::de::Error::custom)
159 }
160}
161
162#[derive(Clone, Debug, PartialEq, Eq)]
164#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
165#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
166#[cfg_attr(feature = "schemars", derive(JsonSchema))]
167pub struct SecretScope {
168 pub env: String,
170 pub tenant: String,
172 #[cfg_attr(
174 feature = "serde",
175 serde(default, skip_serializing_if = "Option::is_none")
176 )]
177 pub team: Option<String>,
178}
179
180#[derive(Clone, Debug, PartialEq, Eq)]
182#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
183#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
184#[cfg_attr(feature = "schemars", derive(JsonSchema))]
185pub enum SecretFormat {
186 Bytes,
188 Text,
190 Json,
192}
193
194#[non_exhaustive]
196#[derive(Clone, Debug, PartialEq, Eq)]
197#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
198#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
199#[cfg_attr(feature = "schemars", derive(JsonSchema))]
200pub struct SecretRequirement {
201 pub key: SecretKey,
203 #[cfg_attr(
205 feature = "serde",
206 serde(default = "SecretRequirement::default_required")
207 )]
208 pub required: bool,
209 #[cfg_attr(
211 feature = "serde",
212 serde(default, skip_serializing_if = "Option::is_none")
213 )]
214 pub description: Option<String>,
215 #[cfg_attr(
217 feature = "serde",
218 serde(default, skip_serializing_if = "Option::is_none")
219 )]
220 pub scope: Option<SecretScope>,
221 #[cfg_attr(
223 feature = "serde",
224 serde(default, skip_serializing_if = "Option::is_none")
225 )]
226 pub format: Option<SecretFormat>,
227 #[cfg_attr(
229 feature = "serde",
230 serde(default, skip_serializing_if = "Option::is_none")
231 )]
232 pub schema: Option<serde_json::Value>,
233 #[cfg_attr(
235 feature = "serde",
236 serde(default, skip_serializing_if = "Vec::is_empty")
237 )]
238 pub examples: Vec<String>,
239}
240
241impl Default for SecretRequirement {
242 fn default() -> Self {
243 Self {
244 key: SecretKey::default(),
245 required: true,
246 description: None,
247 scope: None,
248 format: None,
249 schema: None,
250 examples: Vec::new(),
251 }
252 }
253}
254
255impl SecretRequirement {
256 const fn default_required() -> bool {
257 true
258 }
259}