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
10pub(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 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 #[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
172pub mod callback {
184 pub mod method {
185 pub const PROMPT: &str = "client.prompt";
187 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 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205 #[serde(deny_unknown_fields)]
206 pub struct PromptParams {
207 pub name: String,
209 pub profile: String,
212 #[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 #[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 validate_nonempty_bytes(
253 "prompt answer has an invalid byte length",
254 &self.value,
255 ABSOLUTE_MAX_FRAME_BYTES,
256 )
257 }
258 }
259
260 #[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 #[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 pub const SET: &str = "resolver.set";
388 pub const DELETE: &str = "resolver.delete";
390
391 pub const ALL: &[&str] = &[GET, RELEASE, SET, DELETE];
395 }
396
397 pub const CAPABILITIES: &[&str] = &[method::GET, method::RELEASE];
402
403 pub const MUTATION_CAPABILITIES: &[&str] = &[method::SET, method::DELETE];
411
412 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 #[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 #[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 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 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
620 pub enum Source {
621 Provider,
622 Generated,
623 Default,
624 Composed,
625 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 #[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 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 #[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 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 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
823 pub struct SetResult {
824 pub status: StoredStatus,
825 #[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 pub deleted: bool,
858 #[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 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 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 #[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}