Skip to main content

assay_registry/
types.rs

1//! API response types for the registry protocol.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// Pack metadata returned by the registry.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct PackMeta {
9    /// Pack name (e.g., "eu-ai-act-baseline").
10    pub name: String,
11
12    /// Semantic version (e.g., "1.2.0").
13    pub version: String,
14
15    /// Pack description.
16    #[serde(default)]
17    pub description: Option<String>,
18
19    /// Content digest (sha256:...).
20    pub digest: String,
21
22    /// Size in bytes.
23    #[serde(default)]
24    pub size: Option<u64>,
25
26    /// When the pack was published.
27    #[serde(default)]
28    pub published_at: Option<DateTime<Utc>>,
29
30    /// Whether the pack is signed.
31    #[serde(default)]
32    pub signed: bool,
33
34    /// Key ID used to sign (if signed).
35    #[serde(default)]
36    pub key_id: Option<String>,
37
38    /// Whether the pack is deprecated.
39    #[serde(default)]
40    pub deprecated: bool,
41
42    /// Deprecation message (if deprecated).
43    #[serde(default)]
44    pub deprecation_message: Option<String>,
45}
46
47/// Response from GET /packs/{name}/versions.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct VersionsResponse {
50    /// Pack name.
51    pub name: String,
52
53    /// Available versions, sorted newest first.
54    pub versions: Vec<VersionInfo>,
55}
56
57/// Version information.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct VersionInfo {
60    /// Semantic version.
61    pub version: String,
62
63    /// Content digest.
64    pub digest: String,
65
66    /// When published.
67    #[serde(default)]
68    pub published_at: Option<DateTime<Utc>>,
69
70    /// Whether deprecated.
71    #[serde(default)]
72    pub deprecated: bool,
73}
74
75/// Headers returned with pack content.
76#[derive(Debug, Clone)]
77pub struct PackHeaders {
78    /// X-Pack-Digest header value.
79    pub digest: Option<String>,
80
81    /// X-Pack-Signature header value (Base64 DSSE envelope).
82    pub signature: Option<String>,
83
84    /// X-Pack-Key-Id header value.
85    pub key_id: Option<String>,
86
87    /// ETag for caching.
88    pub etag: Option<String>,
89
90    /// Cache-Control header.
91    pub cache_control: Option<String>,
92
93    /// Content-Length.
94    pub content_length: Option<u64>,
95}
96
97impl PackHeaders {
98    /// Parse headers from a response.
99    pub fn from_headers(headers: &reqwest::header::HeaderMap) -> Self {
100        Self {
101            digest: headers
102                .get("x-pack-digest")
103                .and_then(|v| v.to_str().ok())
104                .map(String::from),
105            signature: headers
106                .get("x-pack-signature")
107                .and_then(|v| v.to_str().ok())
108                .map(String::from),
109            key_id: headers
110                .get("x-pack-key-id")
111                .and_then(|v| v.to_str().ok())
112                .map(String::from),
113            etag: headers
114                .get(reqwest::header::ETAG)
115                .and_then(|v| v.to_str().ok())
116                .map(String::from),
117            cache_control: headers
118                .get(reqwest::header::CACHE_CONTROL)
119                .and_then(|v| v.to_str().ok())
120                .map(String::from),
121            content_length: headers
122                .get(reqwest::header::CONTENT_LENGTH)
123                .and_then(|v| v.to_str().ok())
124                .and_then(|v| v.parse().ok()),
125        }
126    }
127}
128
129/// Response from GET /keys manifest.
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct KeysManifest {
132    /// Schema version.
133    pub version: u8,
134
135    /// List of trusted keys.
136    pub keys: Vec<TrustedKey>,
137
138    /// When the manifest expires.
139    #[serde(default)]
140    pub expires_at: Option<DateTime<Utc>>,
141}
142
143/// A trusted signing key.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct TrustedKey {
146    /// Key ID (sha256:...).
147    pub key_id: String,
148
149    /// Algorithm (always "Ed25519" for now).
150    pub algorithm: String,
151
152    /// Public key (SPKI DER, Base64).
153    pub public_key: String,
154
155    /// Human-readable description.
156    #[serde(default)]
157    pub description: Option<String>,
158
159    /// When the key was added.
160    #[serde(default)]
161    pub added_at: Option<DateTime<Utc>>,
162
163    /// When the key expires (if any).
164    #[serde(default)]
165    pub expires_at: Option<DateTime<Utc>>,
166
167    /// Whether the key is revoked.
168    #[serde(default)]
169    pub revoked: bool,
170}
171
172/// Result of fetching a pack.
173#[derive(Debug, Clone)]
174pub struct FetchResult {
175    /// Pack YAML content.
176    pub content: String,
177
178    /// Headers from the response.
179    pub headers: PackHeaders,
180
181    /// Computed digest of the content.
182    pub computed_digest: String,
183}
184
185/// DSSE envelope structure.
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct DsseEnvelope {
188    /// Payload type (e.g., "application/vnd.assay.pack+yaml;v=1").
189    #[serde(rename = "payloadType")]
190    pub payload_type: String,
191
192    /// Base64-encoded payload.
193    pub payload: String,
194
195    /// Signatures.
196    pub signatures: Vec<DsseSignature>,
197}
198
199/// DSSE signature.
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct DsseSignature {
202    /// Key ID.
203    #[serde(rename = "keyid")]
204    pub key_id: String,
205
206    /// Base64-encoded signature.
207    #[serde(rename = "sig")]
208    pub signature: String,
209}
210
211/// Registry configuration.
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct RegistryConfig {
214    /// Base URL for the registry.
215    #[serde(default = "default_registry_url")]
216    pub url: String,
217
218    /// Authentication token.
219    #[serde(default)]
220    pub token: Option<String>,
221
222    /// Whether to allow unsigned packs.
223    #[serde(default)]
224    pub allow_unsigned: bool,
225
226    /// Request timeout in seconds.
227    #[serde(default = "default_timeout")]
228    pub timeout_secs: u64,
229
230    /// Maximum retries for transient failures.
231    #[serde(default = "default_max_retries")]
232    pub max_retries: u32,
233}
234
235fn default_registry_url() -> String {
236    "https://registry.getassay.dev/v1".to_string()
237}
238
239fn default_timeout() -> u64 {
240    30
241}
242
243fn default_max_retries() -> u32 {
244    3
245}
246
247impl Default for RegistryConfig {
248    fn default() -> Self {
249        Self {
250            url: default_registry_url(),
251            token: None,
252            allow_unsigned: false,
253            timeout_secs: default_timeout(),
254            max_retries: default_max_retries(),
255        }
256    }
257}
258
259impl RegistryConfig {
260    /// Create config from environment variables.
261    ///
262    /// | Variable | Description |
263    /// |----------|-------------|
264    /// | `ASSAY_REGISTRY_URL` | Registry base URL |
265    /// | `ASSAY_REGISTRY_TOKEN` | Authentication token |
266    /// | `ASSAY_ALLOW_UNSIGNED_PACKS` | Allow unsigned packs (dev only) |
267    pub fn from_env() -> Self {
268        Self {
269            url: std::env::var("ASSAY_REGISTRY_URL").unwrap_or_else(|_| default_registry_url()),
270            token: std::env::var("ASSAY_REGISTRY_TOKEN").ok(),
271            allow_unsigned: std::env::var("ASSAY_ALLOW_UNSIGNED_PACKS")
272                .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
273                .unwrap_or(false),
274            timeout_secs: std::env::var("ASSAY_REGISTRY_TIMEOUT")
275                .ok()
276                .and_then(|v| v.parse().ok())
277                .unwrap_or_else(default_timeout),
278            max_retries: std::env::var("ASSAY_REGISTRY_MAX_RETRIES")
279                .ok()
280                .and_then(|v| v.parse().ok())
281                .unwrap_or_else(default_max_retries),
282        }
283    }
284
285    /// Set the token.
286    pub fn with_token(mut self, token: impl Into<String>) -> Self {
287        self.token = Some(token.into());
288        self
289    }
290
291    /// Set the base URL.
292    pub fn with_url(mut self, url: impl Into<String>) -> Self {
293        self.url = url.into();
294        self
295    }
296
297    /// Allow unsigned packs.
298    pub fn with_allow_unsigned(mut self, allow: bool) -> Self {
299        self.allow_unsigned = allow;
300        self
301    }
302}