Skip to main content

secretspec_ipc/
protocol.rs

1use crate::error::{Error, Result};
2use crate::{ABSOLUTE_MAX_FRAME_BYTES, MAX_IN_FLIGHT, MIN_FRAME_BYTES};
3use serde::{Deserialize, Serialize};
4use std::collections::{BTreeMap, BTreeSet};
5
6pub const RESOLVER_PROTOCOL: &str = "secretspec.resolver";
7pub const PROVIDER_PROTOCOL: &str = "secretspec.provider";
8pub const PROTOCOL_VERSION: u32 = 1;
9
10/// Stands in for a secret in `Debug` output, so logging a protocol message
11/// never prints the value it carries.
12pub(crate) struct Redacted;
13
14impl std::fmt::Debug for Redacted {
15    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        formatter.write_str("<redacted>")
17    }
18}
19
20pub mod rpc {
21    /// Return this endpoint's OpenRPC description without initializing
22    /// application state (0.21+).
23    pub const DISCOVER: &str = "rpc.discover";
24    pub const INITIALIZE: &str = "rpc.initialize";
25    pub const CANCEL: &str = "rpc.cancel";
26    pub const SHUTDOWN: &str = "rpc.shutdown";
27
28    pub const ALL: &[&str] = &[DISCOVER, INITIALIZE, CANCEL, SHUTDOWN];
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct Product {
34    pub name: String,
35    pub version: String,
36}
37
38impl Product {
39    pub fn validate(&self) -> Result<()> {
40        validate_nonempty_bytes("product name", &self.name, 256)?;
41        validate_nonempty_bytes("product version", &self.version, 256)
42    }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct Limits {
48    pub max_frame_bytes: usize,
49    pub max_in_flight: usize,
50}
51
52impl Limits {
53    pub const PRE_NEGOTIATION: Self = Self {
54        max_frame_bytes: ABSOLUTE_MAX_FRAME_BYTES,
55        max_in_flight: 1,
56    };
57
58    pub fn validate(self) -> Result<()> {
59        if !(MIN_FRAME_BYTES..=ABSOLUTE_MAX_FRAME_BYTES).contains(&self.max_frame_bytes) {
60            return Err(Error::Protocol(
61                "max_frame_bytes is outside the version 1 range",
62            ));
63        }
64        if !(1..=MAX_IN_FLIGHT).contains(&self.max_in_flight) {
65            return Err(Error::Protocol(
66                "max_in_flight is outside the version 1 range",
67            ));
68        }
69        Ok(())
70    }
71
72    pub fn select(self, peer: Self) -> Result<Self> {
73        self.validate()?;
74        peer.validate()?;
75        Ok(Self {
76            max_frame_bytes: self.max_frame_bytes.min(peer.max_frame_bytes),
77            max_in_flight: self.max_in_flight.min(peer.max_in_flight),
78        })
79    }
80}
81
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83#[serde(deny_unknown_fields)]
84pub struct InitializeParams<A> {
85    pub protocol: String,
86    pub versions: Vec<u32>,
87    pub client: Product,
88    pub limits: Limits,
89    /// Methods this client can answer when the server calls back on the same
90    /// session (0.21+). Empty, and omitted on the wire, for a client that
91    /// answers none, which is every client before this field existed.
92    ///
93    /// The server's `methods` say what a client may ask for. These say
94    /// what the server may ask of the client, and a server MUST NOT send a
95    /// method that is not listed here. A consumer with no way to reach a person
96    /// therefore advertises nothing and is told so immediately, rather than
97    /// waiting out a deadline on an interaction that was never going to arrive.
98    #[serde(default, skip_serializing_if = "Vec::is_empty")]
99    pub client_methods: Vec<String>,
100    pub application: A,
101}
102
103impl<A> InitializeParams<A> {
104    pub fn validate_common(&self, expected_protocol: &str) -> Result<()> {
105        if self.protocol != expected_protocol {
106            return Err(Error::Protocol(
107                "initialization selected the wrong protocol",
108            ));
109        }
110        if self.versions.is_empty()
111            || self.versions.contains(&0)
112            || self.versions.iter().collect::<BTreeSet<_>>().len() != self.versions.len()
113        {
114            return Err(Error::Protocol(
115                "versions must be distinct positive integers",
116            ));
117        }
118        self.client.validate()?;
119        self.limits.validate()?;
120        validate_capabilities(&self.client_methods)?;
121        Ok(())
122    }
123}
124
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126pub struct InitializeResult<A> {
127    pub protocol: String,
128    pub version: u32,
129    pub server: Product,
130    pub methods: Vec<String>,
131    #[serde(default)]
132    pub capabilities: BTreeMap<String, bool>,
133    pub limits: Limits,
134    pub application: A,
135}
136
137impl<A> InitializeResult<A> {
138    pub fn validate_common(
139        &self,
140        expected_protocol: &str,
141        offered_versions: &[u32],
142        offered_limits: Limits,
143    ) -> Result<()> {
144        if self.protocol != expected_protocol || !offered_versions.contains(&self.version) {
145            return Err(Error::Protocol(
146                "server selected an unsupported protocol version",
147            ));
148        }
149        self.server.validate()?;
150        validate_capabilities(&self.methods)?;
151        validate_feature_capabilities(&self.capabilities)?;
152        self.limits.validate()?;
153        if self.limits.max_frame_bytes > offered_limits.max_frame_bytes
154            || self.limits.max_in_flight > offered_limits.max_in_flight
155        {
156            return Err(Error::Protocol("server selected an unoffered limit"));
157        }
158        Ok(())
159    }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(deny_unknown_fields)]
164pub struct CancelParams {
165    pub id: crate::RequestId,
166}
167
168#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(deny_unknown_fields)]
170pub struct EmptyParams {}
171
172/// Methods a server calls back on the client over the same session (0.21+).
173///
174/// This is the only direction reversal in version 1, and it exists because the
175/// endpoint that knows a value is missing is never the process that can ask a
176/// person for it. A stdio resolver has no terminal at all: its stdin and stdout
177/// are the protocol. Prior art solves this the same way, from D-Bus Secret
178/// Service returning a prompt object the client must drive, to the editor
179/// protocols whose servers request input rather than drawing it.
180///
181/// A server MUST NOT send one of these unless the client advertised it in
182/// [`InitializeParams::client_methods`].
183pub mod callback {
184    pub mod method {
185        /// Ask the client to obtain one secret value from a person (0.21+).
186        pub const PROMPT: &str = "client.prompt";
187        /// Ask the client for one provider authentication credential (0.21+).
188        pub const CREDENTIAL: &str = "client.credential";
189
190        pub const RESOLVER: &[&str] = &[PROMPT];
191        pub const PROVIDER: &[&str] = &[CREDENTIAL];
192        pub const ALL: &[&str] = &[PROMPT, CREDENTIAL];
193    }
194
195    use super::*;
196
197    /// Everything the person answering needs, and nothing else.
198    ///
199    /// There is no free-form message: the text a client shows is built by the
200    /// client from these fields, so a server cannot use the prompt to put
201    /// arbitrary attacker-influenced text in front of a person. The declared
202    /// name and the credential-free provider URI are already known to the
203    /// session that asked.
204    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205    #[serde(deny_unknown_fields)]
206    pub struct PromptParams {
207        /// The declared name whose value is being asked for.
208        pub name: String,
209        /// The active profile, so a person answering can tell which one they
210        /// are provisioning.
211        pub profile: String,
212        /// Credential-free display URI of the provider that will store the
213        /// answer, or absent when the answer is not persisted.
214        #[serde(skip_serializing_if = "Option::is_none")]
215        pub target_provider: Option<String>,
216    }
217
218    impl PromptParams {
219        pub fn validate(&self) -> Result<()> {
220            validate_nonempty_bytes("secret name has an invalid byte length", &self.name, 4096)?;
221            validate_nonempty_bytes("profile has an invalid byte length", &self.profile, 4096)?;
222            validate_optional_bytes(
223                "target provider has an invalid byte length",
224                self.target_provider.as_deref(),
225                32768,
226            )
227        }
228    }
229
230    /// The answer carries a secret and is treated exactly like a resolved
231    /// value: never logged, and dropped as soon as it is copied.
232    #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
233    pub struct PromptResult {
234        pub value: String,
235    }
236
237    impl std::fmt::Debug for PromptResult {
238        fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239            formatter
240                .debug_struct("PromptResult")
241                .field("value", &super::Redacted)
242                .finish()
243        }
244    }
245
246    impl PromptResult {
247        pub fn validate(&self) -> Result<()> {
248            // An empty answer is refused for the same reason `resolver.set`
249            // refuses an empty value: stores disagree about whether one means
250            // absent or present-and-empty. A person who wants to decline
251            // cancels instead.
252            validate_nonempty_bytes(
253                "prompt answer has an invalid byte length",
254                &self.value,
255                ABSOLUTE_MAX_FRAME_BYTES,
256            )
257        }
258    }
259
260    /// One semantic provider credential requested while an endpoint is
261    /// initializing or refreshing its authentication (0.21+).
262    ///
263    /// `scope` is a stable, credential-free account or store identity chosen
264    /// by the provider from its configured URI. The client binds it to the
265    /// already selected provider principal before consulting its credential
266    /// broker, so it cannot address another provider's credentials.
267    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268    #[serde(deny_unknown_fields)]
269    pub struct CredentialParams {
270        pub name: String,
271        pub scope: String,
272        pub required: bool,
273    }
274
275    impl CredentialParams {
276        pub fn validate(&self) -> Result<()> {
277            validate_semantic_name(&self.name)?;
278            validate_nonempty_bytes(
279                "provider credential scope has an invalid byte length",
280                &self.scope,
281                4096,
282            )
283        }
284    }
285
286    /// A broker lookup is either a secret value or an ordinary miss. Missing
287    /// is not a transport failure: the endpoint may try another authentication
288    /// mechanism or return its own actionable authentication error.
289    #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
290    #[serde(tag = "status", rename_all = "snake_case", deny_unknown_fields)]
291    pub enum CredentialResult {
292        Found { value: String },
293        Missing,
294    }
295
296    impl std::fmt::Debug for CredentialResult {
297        fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298            match self {
299                Self::Found { .. } => formatter
300                    .debug_struct("Found")
301                    .field("value", &super::Redacted)
302                    .finish(),
303                Self::Missing => formatter.write_str("Missing"),
304            }
305        }
306    }
307
308    impl CredentialResult {
309        pub fn validate(&self) -> Result<()> {
310            match self {
311                Self::Found { value } => validate_nonempty_bytes(
312                    "provider credential has an invalid byte length",
313                    value,
314                    ABSOLUTE_MAX_FRAME_BYTES,
315                ),
316                Self::Missing => Ok(()),
317            }
318        }
319    }
320}
321
322fn validate_semantic_name(name: &str) -> Result<()> {
323    let mut chars = name.chars();
324    if !matches!(chars.next(), Some('a'..='z'))
325        || chars.any(|character| !matches!(character, 'a'..='z' | '0'..='9' | '_'))
326        || name.len() > 256
327    {
328        return Err(Error::Protocol("invalid credential semantic name"));
329    }
330    Ok(())
331}
332
333pub(crate) fn validate_capabilities(capabilities: &[String]) -> Result<()> {
334    if capabilities.iter().collect::<BTreeSet<_>>().len() != capabilities.len() {
335        return Err(Error::Protocol("capabilities must be distinct"));
336    }
337    for capability in capabilities {
338        validate_nonempty_bytes("capability", capability, 256)?;
339    }
340    Ok(())
341}
342
343pub(crate) fn validate_feature_capabilities(capabilities: &BTreeMap<String, bool>) -> Result<()> {
344    for capability in capabilities.keys() {
345        validate_nonempty_bytes("capability", capability, 256)?;
346    }
347    Ok(())
348}
349
350pub(crate) fn validate_nonempty_bytes(label: &'static str, value: &str, max: usize) -> Result<()> {
351    if value.is_empty() || value.len() > max {
352        Err(Error::Protocol(label))
353    } else {
354        Ok(())
355    }
356}
357
358pub(crate) fn validate_optional_bytes(
359    label: &'static str,
360    value: Option<&str>,
361    max: usize,
362) -> Result<()> {
363    if value.is_some_and(|value| value.len() > max) {
364        Err(Error::Protocol(label))
365    } else {
366        Ok(())
367    }
368}
369
370pub(crate) fn deserialize_required_nullable<'de, D, T>(
371    deserializer: D,
372) -> std::result::Result<Option<T>, D::Error>
373where
374    D: serde::Deserializer<'de>,
375    T: Deserialize<'de>,
376{
377    Option::<T>::deserialize(deserializer)
378}
379
380pub mod resolver {
381    use super::*;
382
383    pub mod method {
384        pub const GET: &str = "resolver.get";
385        pub const RELEASE: &str = "resolver.release";
386        /// Store one declared name (0.21+).
387        pub const SET: &str = "resolver.set";
388        /// Remove one declared name's stored value (0.21+).
389        pub const DELETE: &str = "resolver.delete";
390
391        /// Every method version 1 defines. What an endpoint advertises is a
392        /// subset of this: see [`super::CAPABILITIES`] and
393        /// [`super::MUTATION_CAPABILITIES`].
394        pub const ALL: &[&str] = &[GET, RELEASE, SET, DELETE];
395    }
396
397    /// Methods every version 1 endpoint answers. A client that negotiates a
398    /// session without all of them is talking to something that is not a
399    /// resolver, so it refuses the endpoint rather than degrading.
400    ///
401    pub const CAPABILITIES: &[&str] = &[method::GET, method::RELEASE];
402
403    /// Methods that write to the store (0.21+).
404    ///
405    /// These are optional and separately advertised: resolution is the reason
406    /// the protocol exists, while storage is authority a consumer usually does
407    /// not need. An endpoint serving a read-only consumer, or one an operator
408    /// started read-only, advertises none of them, and the client will not send
409    /// a method that was not advertised.
410    pub const MUTATION_CAPABILITIES: &[&str] = &[method::SET, method::DELETE];
411
412    /// Rejects a capability list that no version 1 endpoint could honor.
413    ///
414    /// Only the base methods are required. Nothing here constrains the
415    /// mutation methods against each other: an endpoint that can store but not
416    /// remove is a store that only appends, which is a real backend and not a
417    /// malformed advertisement.
418    pub fn validate_capabilities(capabilities: &[String]) -> Result<()> {
419        if !CAPABILITIES
420            .iter()
421            .all(|method| capabilities.iter().any(|item| item == method))
422        {
423            return Err(Error::Protocol(
424                "resolver capability set omits a required method",
425            ));
426        }
427        Ok(())
428    }
429
430    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
431    #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
432    pub enum Manifest {
433        Path { path: String },
434        Inline { toml: String, base_dir: String },
435    }
436
437    impl Manifest {
438        pub fn validate(&self) -> Result<()> {
439            match self {
440                Self::Path { path } => validate_absolute_path(path),
441                Self::Inline { toml, base_dir } => {
442                    if toml.len() > ABSOLUTE_MAX_FRAME_BYTES {
443                        return Err(Error::Protocol("inline manifest is too large"));
444                    }
445                    validate_absolute_path(base_dir)
446                }
447            }
448        }
449
450        pub const fn kind(&self) -> &'static str {
451            match self {
452                Self::Path { .. } => "path",
453                Self::Inline { .. } => "inline",
454            }
455        }
456    }
457
458    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
459    #[serde(deny_unknown_fields)]
460    pub struct InitializeApplication {
461        pub manifest: Manifest,
462        #[serde(deserialize_with = "deserialize_required_nullable")]
463        pub provider: Option<String>,
464        #[serde(deserialize_with = "deserialize_required_nullable")]
465        pub profile: Option<String>,
466        #[serde(deserialize_with = "deserialize_required_nullable")]
467        pub scope: Option<String>,
468        #[serde(deserialize_with = "deserialize_required_nullable")]
469        pub reason: Option<String>,
470        /// App-requested authorization lifetime in milliseconds. The provider
471        /// may shorten, extend, or reject this request after user approval.
472        #[serde(default, skip_serializing_if = "Option::is_none")]
473        pub requested_authorization_duration_ms: Option<u64>,
474    }
475
476    impl InitializeApplication {
477        pub fn validate(&self) -> Result<()> {
478            self.manifest.validate()?;
479            self.validate_options()
480        }
481
482        /// Validate client inputs without interpreting paths on the client's OS
483        /// (0.21+). The resolver still performs its native path validation.
484        #[cfg(any(feature = "tokio", feature = "blocking"))]
485        pub(crate) fn validate_for_connection(&self) -> Result<()> {
486            let path = match &self.manifest {
487                Manifest::Path { path } => path,
488                Manifest::Inline { toml, base_dir } => {
489                    if toml.len() > ABSOLUTE_MAX_FRAME_BYTES {
490                        return Err(Error::Protocol("inline manifest is too large"));
491                    }
492                    base_dir
493                }
494            };
495            validate_nonempty_bytes("path has an invalid byte length", path, 32768)?;
496            let bytes = path.as_bytes();
497            let windows_drive = bytes.len() >= 3
498                && bytes[0].is_ascii_alphabetic()
499                && bytes[1] == b':'
500                && matches!(bytes[2], b'/' | b'\\');
501            let windows_unc = path.starts_with("\\\\");
502            let absolute = path.starts_with('/') || windows_drive || windows_unc;
503            let normalized = if windows_drive || windows_unc {
504                !path
505                    .split(['/', '\\'])
506                    .any(|part| matches!(part, "." | ".."))
507            } else {
508                !path.split('/').any(|part| matches!(part, "." | ".."))
509            };
510            if !absolute || !normalized || path.contains('\0') {
511                return Err(Error::Protocol(
512                    "manifest path must be absolute and lexically normalized on the resolver machine",
513                ));
514            }
515            self.validate_options()
516        }
517
518        fn validate_options(&self) -> Result<()> {
519            validate_optional_bytes(
520                "provider override is too long",
521                self.provider.as_deref(),
522                32768,
523            )?;
524            validate_optional_bytes("profile is too long", self.profile.as_deref(), 4096)?;
525            validate_optional_bytes("scope is too long", self.scope.as_deref(), 4096)?;
526            validate_optional_bytes("reason is too long", self.reason.as_deref(), 4096)?;
527            if self.requested_authorization_duration_ms == Some(0) {
528                return Err(Error::Protocol(
529                    "requested authorization duration must be positive",
530                ));
531            }
532            Ok(())
533        }
534    }
535
536    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
537    #[serde(deny_unknown_fields)]
538    pub struct InitializedApplication {
539        pub manifest_kind: String,
540        pub supports_inline_manifest: bool,
541    }
542
543    impl InitializedApplication {
544        pub fn validate(&self) -> Result<()> {
545            if matches!(self.manifest_kind.as_str(), "path" | "inline") {
546                Ok(())
547            } else {
548                Err(Error::Protocol("invalid initialized manifest kind"))
549            }
550        }
551    }
552
553    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
554    #[serde(rename_all = "snake_case")]
555    /// Which shape of value the caller can accept.
556    ///
557    /// Both spellings describe what the returned string contains: the secret
558    /// itself, or a location to read it from. The file the resolver writes for
559    /// the `Path` form is how that location is produced, not something the
560    /// caller selects, which is why it does not appear here.
561    pub enum Representation {
562        Auto,
563        Value,
564        Path,
565    }
566
567    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
568    #[serde(deny_unknown_fields)]
569    pub struct Purpose {
570        pub consumer: String,
571        pub operation: String,
572        #[serde(skip_serializing_if = "Option::is_none")]
573        pub host: Option<String>,
574        #[serde(skip_serializing_if = "Option::is_none")]
575        pub path: Option<String>,
576    }
577
578    impl Purpose {
579        pub fn validate(&self) -> Result<()> {
580            validate_nonempty_bytes(
581                "purpose consumer has an invalid byte length",
582                &self.consumer,
583                256,
584            )?;
585            validate_nonempty_bytes(
586                "purpose operation has an invalid byte length",
587                &self.operation,
588                256,
589            )?;
590            validate_optional_bytes("purpose host is too long", self.host.as_deref(), 4096)?;
591            validate_optional_bytes("purpose path is too long", self.path.as_deref(), 4096)
592        }
593    }
594
595    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
596    #[serde(deny_unknown_fields)]
597    pub struct GetParams {
598        pub name: String,
599        pub representation: Representation,
600        pub purpose: Purpose,
601    }
602
603    impl GetParams {
604        pub fn validate(&self) -> Result<()> {
605            validate_nonempty_bytes("secret name has an invalid byte length", &self.name, 4096)?;
606            self.purpose.validate()
607        }
608    }
609
610    /// Where a resolved value came from.
611    ///
612    /// Provenance, not an authorization input. The set is closed for the
613    /// resolver and open for the client, under the same rule as
614    /// [`crate::error::ErrorKind`]: a later revision naming a new origin, such
615    /// as a dynamically issued credential, must not kill a session with an
616    /// older client. A client that treats provenance as security-relevant reads
617    /// [`Source::Unrecognized`] as "not one of the origins I can vouch for" and
618    /// decides accordingly, which it could not do if the frame failed to parse.
619    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
620    pub enum Source {
621        Provider,
622        Generated,
623        Default,
624        Composed,
625        /// An origin this revision does not define. A resolver never sends it.
626        Unrecognized,
627    }
628
629    impl Source {
630        pub const fn as_str(self) -> &'static str {
631            match self {
632                Self::Provider => "provider",
633                Self::Generated => "generated",
634                Self::Default => "default",
635                Self::Composed => "composed",
636                Self::Unrecognized => "unrecognized",
637            }
638        }
639
640        pub const DEFINED: &'static [Self] = &[
641            Self::Provider,
642            Self::Generated,
643            Self::Default,
644            Self::Composed,
645        ];
646
647        pub fn from_wire(source: &str) -> Self {
648            Self::DEFINED
649                .iter()
650                .copied()
651                .find(|defined| defined.as_str() == source)
652                .unwrap_or(Self::Unrecognized)
653        }
654    }
655
656    impl Serialize for Source {
657        fn serialize<S: serde::Serializer>(
658            &self,
659            serializer: S,
660        ) -> std::result::Result<S::Ok, S::Error> {
661            serializer.serialize_str(self.as_str())
662        }
663    }
664
665    impl<'de> Deserialize<'de> for Source {
666        fn deserialize<D: serde::Deserializer<'de>>(
667            deserializer: D,
668        ) -> std::result::Result<Self, D::Error> {
669            Ok(Self::from_wire(&String::deserialize(deserializer)?))
670        }
671    }
672
673    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
674    pub struct UndeclaredResult {
675        pub status: UndeclaredStatus,
676    }
677
678    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
679    #[serde(rename_all = "snake_case")]
680    pub enum UndeclaredStatus {
681        Undeclared,
682    }
683
684    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
685    pub struct MissingResult {
686        pub status: MissingStatus,
687        pub required: bool,
688    }
689
690    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
691    #[serde(rename_all = "snake_case")]
692    pub enum MissingStatus {
693        Missing,
694    }
695
696    impl std::fmt::Debug for ResolvedValueResult {
697        fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
698            formatter
699                .debug_struct("ResolvedValueResult")
700                .field("status", &self.status)
701                .field("representation", &self.representation)
702                .field("value", &super::Redacted)
703                .field("source", &self.source)
704                .field("source_provider", &self.source_provider)
705                .field("expires_at_unix_ms", &self.expires_at_unix_ms)
706                .field("revision", &self.revision)
707                .field("refresh_at_unix_ms", &self.refresh_at_unix_ms)
708                .finish()
709        }
710    }
711
712    #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
713    pub struct ResolvedValueResult {
714        pub status: ResolvedStatus,
715        pub representation: ValueRepresentation,
716        pub value: String,
717        pub source: Source,
718        #[serde(skip_serializing_if = "Option::is_none")]
719        pub source_provider: Option<String>,
720        #[serde(deserialize_with = "deserialize_required_nullable")]
721        pub expires_at_unix_ms: Option<u64>,
722        /// Opaque revision of the returned logical value (0.21+).
723        #[serde(default, skip_serializing_if = "Option::is_none")]
724        pub revision: Option<crate::Revision>,
725        #[serde(deserialize_with = "deserialize_required_nullable")]
726        pub refresh_at_unix_ms: Option<u64>,
727    }
728
729    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
730    pub struct ResolvedPathResult {
731        pub status: ResolvedStatus,
732        pub representation: PathRepresentation,
733        pub path: String,
734        /// Releases the resolver-owned file behind [`Self::path`]. Named apart
735        /// from a provider credential lease, which is an unrelated concept
736        /// with its own issuance and revocation.
737        pub path_lease_id: String,
738        pub source: Source,
739        #[serde(skip_serializing_if = "Option::is_none")]
740        pub source_provider: Option<String>,
741        #[serde(deserialize_with = "deserialize_required_nullable")]
742        pub expires_at_unix_ms: Option<u64>,
743        /// Opaque revision of the returned logical value (0.21+).
744        #[serde(default, skip_serializing_if = "Option::is_none")]
745        pub revision: Option<crate::Revision>,
746        #[serde(deserialize_with = "deserialize_required_nullable")]
747        pub refresh_at_unix_ms: Option<u64>,
748    }
749
750    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
751    #[serde(rename_all = "snake_case")]
752    pub enum ResolvedStatus {
753        Resolved,
754    }
755
756    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
757    #[serde(rename_all = "snake_case")]
758    pub enum ValueRepresentation {
759        Value,
760    }
761
762    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
763    #[serde(rename_all = "snake_case")]
764    pub enum PathRepresentation {
765        Path,
766    }
767
768    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
769    #[serde(untagged)]
770    pub enum GetResult {
771        Undeclared(UndeclaredResult),
772        Missing(MissingResult),
773        Value(ResolvedValueResult),
774        Path(ResolvedPathResult),
775    }
776
777    #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
778    #[serde(deny_unknown_fields)]
779    pub struct SetParams {
780        pub name: String,
781        pub value: String,
782        pub purpose: Purpose,
783    }
784
785    impl std::fmt::Debug for SetParams {
786        fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
787            formatter
788                .debug_struct("SetParams")
789                .field("name", &self.name)
790                .field("value", &super::Redacted)
791                .field("purpose", &self.purpose)
792                .finish()
793        }
794    }
795
796    impl SetParams {
797        pub fn validate(&self) -> Result<()> {
798            validate_nonempty_bytes("secret name has an invalid byte length", &self.name, 4096)?;
799            // An empty value means "no value" to some stores and "a value that
800            // happens to be empty" to others, and the difference decides
801            // whether a later read finds the secret. It never travels as a
802            // write; a caller that wants the value gone sends `resolver.delete`.
803            validate_nonempty_bytes(
804                "secret value has an invalid byte length",
805                &self.value,
806                ABSOLUTE_MAX_FRAME_BYTES,
807            )?;
808            self.purpose.validate()
809        }
810    }
811
812    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
813    #[serde(rename_all = "snake_case")]
814    pub enum StoredStatus {
815        Stored,
816    }
817
818    /// A write is either accepted or refused, so there is one shape here where
819    /// a read has four. A name the manifest does not declare has no address to
820    /// write to, and unlike an absent value it is not something the caller can
821    /// route around, so it is an error rather than a status.
822    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
823    pub struct SetResult {
824        pub status: StoredStatus,
825        /// Credential-free URI of the provider that took the write, for a
826        /// consumer that reports the destination back to a human. Absent when
827        /// the endpoint does not name it.
828        #[serde(skip_serializing_if = "Option::is_none")]
829        pub target_provider: Option<String>,
830    }
831
832    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
833    #[serde(deny_unknown_fields)]
834    pub struct DeleteParams {
835        pub name: String,
836        pub purpose: Purpose,
837    }
838
839    impl DeleteParams {
840        pub fn validate(&self) -> Result<()> {
841            validate_nonempty_bytes("secret name has an invalid byte length", &self.name, 4096)?;
842            self.purpose.validate()
843        }
844    }
845
846    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
847    #[serde(rename_all = "snake_case")]
848    pub enum DeletedStatus {
849        Deleted,
850    }
851
852    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
853    pub struct DeleteResult {
854        pub status: DeletedStatus,
855        /// `false` when the store held nothing for this name. Removal is
856        /// idempotent, so that is a success and not an error.
857        pub deleted: bool,
858        /// Credential-free URI of the provider the removal was addressed to.
859        #[serde(skip_serializing_if = "Option::is_none")]
860        pub target_provider: Option<String>,
861    }
862
863    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
864    #[serde(deny_unknown_fields)]
865    pub struct ReleaseParams {
866        pub path_lease_ids: Vec<String>,
867    }
868
869    impl ReleaseParams {
870        pub fn validate(&self) -> Result<()> {
871            if self.path_lease_ids.len() > 256 {
872                return Err(Error::Protocol("release contains more than 256 lease IDs"));
873            }
874            for lease in &self.path_lease_ids {
875                validate_nonempty_bytes("lease ID has an invalid byte length", lease, 256)?;
876            }
877            Ok(())
878        }
879    }
880
881    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
882    pub struct ReleaseResult {
883        pub released: usize,
884    }
885
886    fn validate_absolute_path(path: &str) -> Result<()> {
887        validate_nonempty_bytes("path has an invalid byte length", path, 32768)?;
888        let path = std::path::Path::new(path);
889        if !path.is_absolute()
890            || path.components().any(|component| {
891                matches!(
892                    component,
893                    std::path::Component::CurDir | std::path::Component::ParentDir
894                )
895            })
896        {
897            return Err(Error::Protocol(
898                "manifest path must be absolute and lexically normalized",
899            ));
900        }
901        Ok(())
902    }
903}
904
905pub mod provider {
906    use super::*;
907
908    pub mod method {
909        use super::*;
910        use serde::de::DeserializeOwned;
911
912        /// Associates one provider method with its wire parameter and result
913        /// types so callers cannot accidentally deserialize a method into the
914        /// wrong response shape.
915        pub trait Method {
916            const NAME: &'static str;
917            type Params: Serialize;
918            type Result: DeserializeOwned;
919        }
920
921        macro_rules! methods {
922            ($(($type:ident, $constant:ident, $name:literal, $params:ty, $result:ty)),+ $(,)?) => {
923                $(
924                    pub const $constant: &str = $name;
925
926                    #[derive(Debug, Clone, Copy)]
927                    pub struct $type;
928
929                    impl Method for $type {
930                        const NAME: &'static str = $constant;
931                        type Params = $params;
932                        type Result = $result;
933                    }
934                )+
935
936                pub const ALL: &[&str] = &[$($constant),+];
937            };
938        }
939
940        methods!(
941            (
942                ResolveAddress,
943                RESOLVE_ADDRESS,
944                "provider.resolve_address",
945                AddressParams,
946                ResolveAddressResult
947            ),
948            (Get, GET, "provider.get", AddressParams, GetResult),
949            (
950                GetMany,
951                GET_MANY,
952                "provider.get_many",
953                GetManyParams,
954                GetManyResult
955            ),
956            (
957                Exists,
958                EXISTS,
959                "provider.exists",
960                AddressParams,
961                ExistsResult
962            ),
963            (Set, SET, "provider.set", SetParams, StoredResult),
964            (
965                SetExpiring,
966                SET_EXPIRING,
967                "provider.set_expiring",
968                SetExpiringParams,
969                StoredResult
970            ),
971            (
972                Delete,
973                DELETE,
974                "provider.delete",
975                AddressParams,
976                DeletedResult
977            ),
978            (Clear, CLEAR, "provider.clear", ClearParams, ClearResult),
979            (
980                CheckWritable,
981                CHECK_WRITABLE,
982                "provider.check_writable",
983                AddressParams,
984                EmptyResult
985            ),
986            (
987                CheckDeletable,
988                CHECK_DELETABLE,
989                "provider.check_deletable",
990                AddressParams,
991                EmptyResult
992            ),
993            (
994                DescribeWriteTarget,
995                DESCRIBE_WRITE_TARGET,
996                "provider.describe_write_target",
997                AddressParams,
998                DescribeWriteTargetResult
999            ),
1000            (
1001                Reflect,
1002                REFLECT,
1003                "provider.reflect",
1004                ReflectParams,
1005                ReflectResult
1006            ),
1007        );
1008    }
1009
1010    pub const CAPABILITIES: &[&str] = method::ALL;
1011
1012    pub fn validate_capabilities(capabilities: &[String]) -> Result<()> {
1013        let has = |method: &str| capabilities.iter().any(|item| item == method);
1014        if !has(method::RESOLVE_ADDRESS)
1015            || ![method::GET, method::EXISTS, method::SET]
1016                .iter()
1017                .any(|method| has(method))
1018            || (has(method::GET_MANY) && !has(method::GET))
1019            || (has(method::SET_EXPIRING) && !has(method::SET))
1020        {
1021            return Err(Error::Protocol(
1022                "provider capability set violates version 1 dependencies",
1023            ));
1024        }
1025        Ok(())
1026    }
1027
1028    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1029    #[serde(deny_unknown_fields)]
1030    /// Resolver-declared provider session context. Available starting with
1031    /// SecretSpec 0.21.
1032    pub struct ApplicationContext {
1033        #[serde(deserialize_with = "deserialize_required_nullable")]
1034        pub project: Option<String>,
1035        #[serde(deserialize_with = "deserialize_required_nullable")]
1036        pub profile: Option<String>,
1037        #[serde(deserialize_with = "deserialize_required_nullable")]
1038        pub base_dir: Option<String>,
1039        #[serde(deserialize_with = "deserialize_required_nullable")]
1040        pub reason: Option<String>,
1041        /// App-requested authorization lifetime in milliseconds. This is an
1042        /// untrusted default for an approval surface, not an authorization.
1043        #[serde(default, skip_serializing_if = "Option::is_none")]
1044        pub requested_authorization_duration_ms: Option<u64>,
1045    }
1046
1047    impl ApplicationContext {
1048        pub fn validate(&self) -> Result<()> {
1049            for (label, value) in [
1050                ("provider project", self.project.as_deref()),
1051                ("provider profile", self.profile.as_deref()),
1052            ] {
1053                if let Some(value) = value {
1054                    validate_nonempty_bytes(label, value, 4096)?;
1055                }
1056            }
1057            validate_optional_bytes(
1058                "provider base directory is too long",
1059                self.base_dir.as_deref(),
1060                32768,
1061            )?;
1062            validate_optional_bytes("provider reason is too long", self.reason.as_deref(), 4096)?;
1063            if self.requested_authorization_duration_ms == Some(0) {
1064                return Err(Error::Protocol(
1065                    "requested authorization duration must be positive",
1066                ));
1067            }
1068            if let Some(base_dir) = &self.base_dir
1069                && !std::path::Path::new(base_dir).is_absolute()
1070            {
1071                return Err(Error::Protocol("provider base directory must be absolute"));
1072            }
1073            Ok(())
1074        }
1075    }
1076
1077    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1078    #[serde(deny_unknown_fields)]
1079    pub struct InitializeApplication {
1080        pub scheme: String,
1081        pub uri: String,
1082        pub context: ApplicationContext,
1083    }
1084
1085    impl InitializeApplication {
1086        pub fn validate(&self) -> Result<()> {
1087            validate_scheme(&self.scheme)?;
1088            validate_nonempty_bytes("provider URI has an invalid byte length", &self.uri, 32768)?;
1089            self.context.validate()?;
1090            let uri_scheme = self.uri.split_once(':').map(|(scheme, _)| scheme);
1091            if uri_scheme != Some(self.scheme.as_str()) {
1092                return Err(Error::Protocol(
1093                    "provider URI scheme does not match initialization scheme",
1094                ));
1095            }
1096            Ok(())
1097        }
1098    }
1099
1100    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1101    #[serde(rename_all = "snake_case")]
1102    pub enum CoordinateName {
1103        Field,
1104        Vault,
1105        Section,
1106        Version,
1107    }
1108
1109    impl CoordinateName {
1110        pub const fn as_str(self) -> &'static str {
1111            match self {
1112                Self::Field => "field",
1113                Self::Vault => "vault",
1114                Self::Section => "section",
1115                Self::Version => "version",
1116            }
1117        }
1118    }
1119
1120    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1121    #[serde(rename_all = "snake_case")]
1122    pub enum Persistence {
1123        Persist,
1124        Ephemeral,
1125    }
1126
1127    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1128    #[serde(deny_unknown_fields)]
1129    pub struct Metadata {
1130        pub name: String,
1131        pub display_uri: String,
1132        pub supported_coordinates: Vec<CoordinateName>,
1133        pub generated_value_persistence: Persistence,
1134        pub prompted_value_persistence: Persistence,
1135        pub storage_identity: String,
1136        pub entry_container_identity: String,
1137        #[serde(deserialize_with = "deserialize_required_nullable")]
1138        pub physical_store_path: Option<String>,
1139    }
1140
1141    impl Metadata {
1142        pub fn validate(&self) -> Result<()> {
1143            validate_scheme(&self.name)?;
1144            for value in [
1145                &self.display_uri,
1146                &self.storage_identity,
1147                &self.entry_container_identity,
1148            ] {
1149                if value.len() > 32768 {
1150                    return Err(Error::Protocol("provider metadata is too long"));
1151                }
1152            }
1153            if self
1154                .supported_coordinates
1155                .iter()
1156                .collect::<BTreeSet<_>>()
1157                .len()
1158                != self.supported_coordinates.len()
1159            {
1160                return Err(Error::Protocol("supported coordinates must be distinct"));
1161            }
1162            if let Some(path) = &self.physical_store_path
1163                && !std::path::Path::new(path).is_absolute()
1164            {
1165                return Err(Error::Protocol("physical_store_path must be absolute"));
1166            }
1167            Ok(())
1168        }
1169    }
1170
1171    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1172    #[serde(deny_unknown_fields)]
1173    pub struct InitializedApplication {
1174        pub provider: Metadata,
1175    }
1176
1177    #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1178    #[serde(deny_unknown_fields)]
1179    pub struct Coordinates {
1180        pub item: String,
1181        #[serde(default, skip_serializing_if = "Option::is_none")]
1182        pub field: Option<String>,
1183        #[serde(default, skip_serializing_if = "Option::is_none")]
1184        pub vault: Option<String>,
1185        #[serde(default, skip_serializing_if = "Option::is_none")]
1186        pub section: Option<String>,
1187        #[serde(default, skip_serializing_if = "Option::is_none")]
1188        pub version: Option<String>,
1189    }
1190
1191    impl Coordinates {
1192        pub fn validate(&self) -> Result<()> {
1193            validate_nonempty_bytes("item has an invalid byte length", &self.item, 4096)?;
1194            for value in [&self.field, &self.vault, &self.section, &self.version] {
1195                validate_optional_bytes("coordinate is too long", value.as_deref(), 4096)?;
1196            }
1197            Ok(())
1198        }
1199
1200        pub fn unsupported(&self, supported: &[CoordinateName]) -> Option<CoordinateName> {
1201            [
1202                (CoordinateName::Field, self.field.is_some()),
1203                (CoordinateName::Vault, self.vault.is_some()),
1204                (CoordinateName::Section, self.section.is_some()),
1205                (CoordinateName::Version, self.version.is_some()),
1206            ]
1207            .into_iter()
1208            .find_map(|(name, present)| (present && !supported.contains(&name)).then_some(name))
1209        }
1210    }
1211
1212    #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1213    #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1214    pub enum Address {
1215        Convention {
1216            project: String,
1217            profile: String,
1218            key: String,
1219        },
1220        Native {
1221            coordinates: Coordinates,
1222        },
1223    }
1224
1225    impl Address {
1226        pub fn validate(&self) -> Result<()> {
1227            match self {
1228                Self::Convention {
1229                    project,
1230                    profile,
1231                    key,
1232                } => {
1233                    for value in [project, profile, key] {
1234                        if value.len() > 4096 {
1235                            return Err(Error::Protocol(
1236                                "convention address component is too long",
1237                            ));
1238                        }
1239                    }
1240                    Ok(())
1241                }
1242                Self::Native { coordinates } => coordinates.validate(),
1243            }
1244        }
1245    }
1246
1247    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1248    #[serde(deny_unknown_fields)]
1249    pub struct AddressParams {
1250        pub address: Address,
1251    }
1252
1253    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1254    pub struct ResolveAddressResult {
1255        pub coordinates: Coordinates,
1256    }
1257
1258    #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
1259    #[serde(tag = "status", rename_all = "snake_case")]
1260    pub enum GetResult {
1261        Found {
1262            value: String,
1263            #[serde(deserialize_with = "deserialize_required_nullable")]
1264            expires_at_unix_ms: Option<u64>,
1265            #[serde(default, skip_serializing_if = "Option::is_none")]
1266            revision: Option<crate::Revision>,
1267        },
1268        Missing,
1269    }
1270
1271    impl std::fmt::Debug for GetResult {
1272        fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1273            match self {
1274                Self::Found {
1275                    value: _,
1276                    expires_at_unix_ms,
1277                    revision,
1278                } => formatter
1279                    .debug_struct("Found")
1280                    .field("value", &super::Redacted)
1281                    .field("expires_at_unix_ms", expires_at_unix_ms)
1282                    .field("revision", revision)
1283                    .finish(),
1284                Self::Missing => formatter.write_str("Missing"),
1285            }
1286        }
1287    }
1288
1289    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1290    #[serde(deny_unknown_fields)]
1291    pub struct NamedRequest {
1292        pub name: String,
1293        pub address: Address,
1294    }
1295
1296    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1297    #[serde(deny_unknown_fields)]
1298    pub struct GetManyParams {
1299        pub requests: Vec<NamedRequest>,
1300    }
1301
1302    impl GetManyParams {
1303        pub fn validate(&self) -> Result<()> {
1304            if self.requests.len() > 1024 {
1305                return Err(Error::Protocol("get_many contains more than 1024 requests"));
1306            }
1307            let mut names = BTreeSet::new();
1308            for request in &self.requests {
1309                validate_nonempty_bytes(
1310                    "batch name has an invalid byte length",
1311                    &request.name,
1312                    4096,
1313                )?;
1314                if !names.insert(&request.name) {
1315                    return Err(Error::Protocol("batch names must be unique"));
1316                }
1317                request.address.validate()?;
1318            }
1319            Ok(())
1320        }
1321    }
1322
1323    #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1324    pub struct NamedGetResult {
1325        pub name: String,
1326        #[serde(flatten)]
1327        pub outcome: GetResult,
1328    }
1329
1330    impl<'de> Deserialize<'de> for NamedGetResult {
1331        fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1332        where
1333            D: serde::Deserializer<'de>,
1334        {
1335            #[derive(Deserialize)]
1336            struct Found {
1337                name: String,
1338                status: FoundStatus,
1339                value: String,
1340                #[serde(deserialize_with = "deserialize_required_nullable")]
1341                expires_at_unix_ms: Option<u64>,
1342                #[serde(default, skip_serializing_if = "Option::is_none")]
1343                revision: Option<crate::Revision>,
1344            }
1345
1346            #[derive(Deserialize)]
1347            struct Missing {
1348                name: String,
1349                status: MissingStatus,
1350            }
1351
1352            #[derive(Deserialize)]
1353            #[serde(rename_all = "snake_case")]
1354            enum FoundStatus {
1355                Found,
1356            }
1357
1358            #[derive(Deserialize)]
1359            #[serde(rename_all = "snake_case")]
1360            enum MissingStatus {
1361                Missing,
1362            }
1363
1364            #[derive(Deserialize)]
1365            #[serde(untagged)]
1366            enum Repr {
1367                Found(Found),
1368                Missing(Missing),
1369            }
1370
1371            match Repr::deserialize(deserializer)? {
1372                Repr::Found(Found {
1373                    name,
1374                    status: FoundStatus::Found,
1375                    value,
1376                    expires_at_unix_ms,
1377                    revision,
1378                }) => Ok(Self {
1379                    name,
1380                    outcome: GetResult::Found {
1381                        value,
1382                        expires_at_unix_ms,
1383                        revision,
1384                    },
1385                }),
1386                Repr::Missing(Missing {
1387                    name,
1388                    status: MissingStatus::Missing,
1389                }) => Ok(Self {
1390                    name,
1391                    outcome: GetResult::Missing,
1392                }),
1393            }
1394        }
1395    }
1396
1397    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1398    pub struct GetManyResult {
1399        pub results: Vec<NamedGetResult>,
1400    }
1401
1402    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1403    pub struct ExistsResult {
1404        pub exists: bool,
1405    }
1406
1407    #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
1408    #[serde(deny_unknown_fields)]
1409    pub struct SetParams {
1410        pub address: Address,
1411        pub value: String,
1412    }
1413
1414    impl std::fmt::Debug for SetParams {
1415        fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1416            formatter
1417                .debug_struct("SetParams")
1418                .field("address", &self.address)
1419                .field("value", &super::Redacted)
1420                .finish()
1421        }
1422    }
1423
1424    impl SetParams {
1425        pub fn validate(&self) -> Result<()> {
1426            self.address.validate()
1427        }
1428    }
1429
1430    #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
1431    #[serde(deny_unknown_fields)]
1432    pub struct SetExpiringParams {
1433        pub address: Address,
1434        pub value: String,
1435        pub ttl_ms: u64,
1436    }
1437
1438    impl std::fmt::Debug for SetExpiringParams {
1439        fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1440            formatter
1441                .debug_struct("SetExpiringParams")
1442                .field("address", &self.address)
1443                .field("value", &super::Redacted)
1444                .field("ttl_ms", &self.ttl_ms)
1445                .finish()
1446        }
1447    }
1448
1449    impl SetExpiringParams {
1450        pub fn validate(&self) -> Result<()> {
1451            self.address.validate()?;
1452            if self.ttl_ms == 0 {
1453                return Err(Error::Protocol("provider expiry must be positive"));
1454            }
1455            Ok(())
1456        }
1457    }
1458
1459    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1460    pub struct StoredResult {
1461        pub stored: bool,
1462    }
1463
1464    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1465    pub struct DeletedResult {
1466        pub deleted: bool,
1467    }
1468
1469    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1470    pub struct EmptyResult {}
1471
1472    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1473    #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1474    pub enum ClearScope {
1475        All,
1476        Convention { project: String, profile: String },
1477    }
1478
1479    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1480    #[serde(deny_unknown_fields)]
1481    pub struct ClearParams {
1482        pub scope: ClearScope,
1483    }
1484
1485    impl ClearParams {
1486        pub fn validate(&self) -> Result<()> {
1487            match &self.scope {
1488                ClearScope::All => Ok(()),
1489                ClearScope::Convention { project, profile } => {
1490                    validate_optional_bytes("clear project is too long", Some(project), 4096)?;
1491                    validate_optional_bytes("clear profile is too long", Some(profile), 4096)
1492                }
1493            }
1494        }
1495    }
1496
1497    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1498    pub struct ClearResult {
1499        pub cleared: usize,
1500    }
1501
1502    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1503    pub struct DescribeWriteTargetResult {
1504        pub description: String,
1505    }
1506
1507    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1508    #[serde(deny_unknown_fields)]
1509    pub struct ReflectParams {
1510        pub project: String,
1511        pub profile: String,
1512    }
1513
1514    impl ReflectParams {
1515        pub fn validate(&self) -> Result<()> {
1516            validate_optional_bytes("reflection project is too long", Some(&self.project), 4096)?;
1517            validate_optional_bytes("reflection profile is too long", Some(&self.profile), 4096)
1518        }
1519    }
1520
1521    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1522    #[serde(deny_unknown_fields)]
1523    pub struct ReflectedDeclaration {
1524        pub description: String,
1525        pub required: bool,
1526        #[serde(rename = "ref")]
1527        pub reference: Coordinates,
1528    }
1529
1530    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1531    pub struct ReflectResult {
1532        pub schema_version: u32,
1533        pub declarations: BTreeMap<String, ReflectedDeclaration>,
1534    }
1535
1536    impl ReflectResult {
1537        pub fn validate(&self) -> Result<()> {
1538            if self.schema_version != 1 {
1539                return Err(Error::Protocol("unsupported reflection schema version"));
1540            }
1541            for (name, declaration) in &self.declarations {
1542                validate_optional_bytes("reflected name is too long", Some(name), 4096)?;
1543                validate_optional_bytes(
1544                    "reflected description is too long",
1545                    Some(&declaration.description),
1546                    4096,
1547                )?;
1548                declaration.reference.validate()?;
1549            }
1550            Ok(())
1551        }
1552    }
1553
1554    fn validate_scheme(scheme: &str) -> Result<()> {
1555        let mut chars = scheme.chars();
1556        let first = chars.next();
1557        if !matches!(first, Some('a'..='z'))
1558            || chars.any(|character| !matches!(character, 'a'..='z' | '0'..='9' | '-'))
1559        {
1560            return Err(Error::Protocol("invalid provider scheme"));
1561        }
1562        Ok(())
1563    }
1564}
1565
1566#[cfg(test)]
1567mod tests {
1568    use super::*;
1569
1570    #[test]
1571    fn debug_never_prints_secret_values() {
1572        const SECRET: &str = "hunter2";
1573        let address = provider::Address::Convention {
1574            project: "app".into(),
1575            profile: "default".into(),
1576            key: "TOKEN".into(),
1577        };
1578        let value = resolver::ResolvedValueResult {
1579            status: resolver::ResolvedStatus::Resolved,
1580            representation: resolver::ValueRepresentation::Value,
1581            value: SECRET.into(),
1582            source: resolver::Source::Provider,
1583            source_provider: None,
1584            expires_at_unix_ms: None,
1585            revision: None,
1586            refresh_at_unix_ms: None,
1587        };
1588        let rendered = [
1589            format!(
1590                "{:?}",
1591                callback::PromptResult {
1592                    value: SECRET.into()
1593                }
1594            ),
1595            format!(
1596                "{:?}",
1597                callback::CredentialResult::Found {
1598                    value: SECRET.into()
1599                }
1600            ),
1601            format!("{:?}", resolver::GetResult::Value(value)),
1602            format!(
1603                "{:?}",
1604                resolver::SetParams {
1605                    name: "TOKEN".into(),
1606                    value: SECRET.into(),
1607                    purpose: resolver::Purpose {
1608                        consumer: "test".into(),
1609                        operation: "store".into(),
1610                        host: None,
1611                        path: None,
1612                    },
1613                }
1614            ),
1615            format!(
1616                "{:?}",
1617                provider::GetResult::Found {
1618                    value: SECRET.into(),
1619                    expires_at_unix_ms: None,
1620                    revision: None,
1621                }
1622            ),
1623            format!(
1624                "{:?}",
1625                provider::SetParams {
1626                    address: address.clone(),
1627                    value: SECRET.into(),
1628                }
1629            ),
1630            format!(
1631                "{:?}",
1632                provider::SetExpiringParams {
1633                    address,
1634                    value: SECRET.into(),
1635                    ttl_ms: 1,
1636                }
1637            ),
1638            format!("{:?}", crate::provider::SecretValue::new(SECRET.into())),
1639        ];
1640        for debug in rendered {
1641            assert!(!debug.contains(SECRET), "{debug}");
1642            assert!(debug.contains("<redacted>"), "{debug}");
1643        }
1644    }
1645
1646    #[test]
1647    fn limit_selection_never_increases_an_offer() {
1648        let selected = Limits {
1649            max_frame_bytes: 32_768,
1650            max_in_flight: 16,
1651        }
1652        .select(Limits {
1653            max_frame_bytes: 8_192,
1654            max_in_flight: 4,
1655        })
1656        .unwrap();
1657        assert_eq!(selected.max_frame_bytes, 8_192);
1658        assert_eq!(selected.max_in_flight, 4);
1659    }
1660
1661    #[test]
1662    fn provider_uri_scheme_must_match() {
1663        let application = provider::InitializeApplication {
1664            scheme: "factorseal".into(),
1665            uri: "other://default".into(),
1666            context: provider::ApplicationContext {
1667                project: Some("demo".into()),
1668                profile: Some("default".into()),
1669                base_dir: None,
1670                reason: None,
1671                requested_authorization_duration_ms: None,
1672            },
1673        };
1674        assert!(application.validate().is_err());
1675    }
1676
1677    #[test]
1678    fn provider_credential_requests_use_bounded_semantic_names_and_scopes() {
1679        let valid = callback::CredentialParams {
1680            name: "client_secret_2".into(),
1681            scope: "https://service.example/account/team-a".into(),
1682            required: true,
1683        };
1684        assert!(valid.validate().is_ok());
1685        for name in ["", "ClientSecret", "2fa", "client-secret"] {
1686            assert!(
1687                callback::CredentialParams {
1688                    name: name.into(),
1689                    ..valid.clone()
1690                }
1691                .validate()
1692                .is_err()
1693            );
1694        }
1695        assert!(
1696            callback::CredentialParams {
1697                scope: String::new(),
1698                ..valid.clone()
1699            }
1700            .validate()
1701            .is_err()
1702        );
1703        assert!(
1704            callback::CredentialParams {
1705                scope: "x".repeat(4097),
1706                ..valid
1707            }
1708            .validate()
1709            .is_err()
1710        );
1711    }
1712
1713    #[test]
1714    fn provider_credential_results_distinguish_a_miss_from_an_empty_secret() {
1715        assert!(callback::CredentialResult::Missing.validate().is_ok());
1716        assert!(
1717            callback::CredentialResult::Found {
1718                value: "token".into()
1719            }
1720            .validate()
1721            .is_ok()
1722        );
1723        assert!(
1724            callback::CredentialResult::Found {
1725                value: String::new()
1726            }
1727            .validate()
1728            .is_err()
1729        );
1730    }
1731
1732    #[test]
1733    fn named_provider_batch_results_are_flattened_and_closed() {
1734        let found = serde_json::from_value::<provider::NamedGetResult>(serde_json::json!({
1735            "name": "token",
1736            "status": "found",
1737            "value": "secret",
1738            "expires_at_unix_ms": null
1739        }))
1740        .unwrap();
1741        assert_eq!(
1742            found,
1743            provider::NamedGetResult {
1744                name: "token".into(),
1745                outcome: provider::GetResult::Found {
1746                    value: "secret".into(),
1747                    expires_at_unix_ms: None,
1748                    revision: None,
1749                }
1750            }
1751        );
1752        assert!(
1753            serde_json::from_value::<provider::NamedGetResult>(serde_json::json!({
1754                "name": "token",
1755                "status": "missing",
1756                "extra": true
1757            }))
1758            .is_ok()
1759        );
1760    }
1761
1762    #[test]
1763    fn provider_empty_results_ignore_future_members() {
1764        assert!(serde_json::from_value::<provider::EmptyResult>(serde_json::json!({})).is_ok());
1765        assert!(
1766            serde_json::from_value::<provider::EmptyResult>(serde_json::json!({"stored": true}))
1767                .is_ok()
1768        );
1769    }
1770
1771    #[test]
1772    fn schema_required_nullable_members_cannot_be_omitted() {
1773        assert!(
1774            serde_json::from_value::<resolver::InitializeApplication>(serde_json::json!({
1775                "manifest": {"kind": "path", "path": "/tmp/secretspec.toml"},
1776                "provider": null,
1777                "profile": null,
1778                "scope": null
1779            }))
1780            .is_err()
1781        );
1782        assert!(
1783            serde_json::from_value::<provider::InitializeApplication>(serde_json::json!({
1784                "scheme": "factorseal",
1785                "uri": "factorseal://default",
1786                "context": {
1787                    "project": "demo",
1788                    "profile": "default",
1789                    "base_dir": null
1790                }
1791            }))
1792            .is_err()
1793        );
1794        assert!(
1795            serde_json::from_value::<provider::GetResult>(serde_json::json!({
1796                "status": "found",
1797                "value": "secret"
1798            }))
1799            .is_err()
1800        );
1801        assert!(
1802            serde_json::from_value::<resolver::ResolvedValueResult>(serde_json::json!({
1803                "status": "resolved",
1804                "representation": "value",
1805                "value": "secret",
1806                "source": "provider",
1807                "source_provider": null,
1808                "expires_at_unix_ms": null
1809            }))
1810            .is_err()
1811        );
1812    }
1813
1814    #[test]
1815    fn requested_authorization_duration_is_optional_but_must_be_positive() {
1816        let without_request =
1817            serde_json::from_value::<resolver::InitializeApplication>(serde_json::json!({
1818                "manifest": {"kind": "path", "path": "/tmp/secretspec.toml"},
1819                "provider": null,
1820                "profile": null,
1821                "scope": null,
1822                "reason": null
1823            }))
1824            .unwrap();
1825        assert_eq!(without_request.requested_authorization_duration_ms, None);
1826
1827        let zero = resolver::InitializeApplication {
1828            requested_authorization_duration_ms: Some(0),
1829            ..without_request
1830        };
1831        assert!(zero.validate().is_err());
1832    }
1833}