coven_foundation/
code_envelope.rs1use base64::engine::general_purpose::URL_SAFE_NO_PAD;
8use base64::Engine;
9use serde::de::DeserializeOwned;
10use serde::Serialize;
11
12pub const PREFIX: &str = "coven:";
16
17#[derive(Debug, thiserror::Error)]
20pub enum EnvelopeError {
21 #[error("missing expected code prefix {expected:?}")]
22 MissingPrefix { expected: String },
23 #[error("invalid base64url payload")]
24 InvalidBase64(#[source] base64::DecodeError),
25 #[error("invalid JSON payload")]
26 InvalidJson(#[source] serde_json::Error),
27}
28
29#[derive(Debug, thiserror::Error)]
30pub enum FixedHexError {
31 #[error("{label} is not hex")]
32 InvalidHex {
33 label: String,
34 #[source]
35 source: hex::FromHexError,
36 },
37 #[error("{label} must be {expected_len} bytes, got {actual_len}")]
38 InvalidLength {
39 label: String,
40 expected_len: usize,
41 actual_len: usize,
42 },
43}
44
45pub fn encode_code<T: Serialize>(prefix: &str, code: &T) -> String {
47 let json = serde_json::to_vec(code).expect("code is always serializable");
48 let b64 = URL_SAFE_NO_PAD.encode(&json);
49 format!("{prefix}{b64}")
50}
51
52pub fn decode_code<T: DeserializeOwned>(prefix: &str, s: &str) -> Result<T, EnvelopeError> {
56 let trimmed = s.trim();
57 let payload = trimmed
58 .strip_prefix(prefix)
59 .ok_or_else(|| EnvelopeError::MissingPrefix {
60 expected: prefix.to_string(),
61 })?;
62 let bytes = URL_SAFE_NO_PAD
63 .decode(payload)
64 .map_err(EnvelopeError::InvalidBase64)?;
65 serde_json::from_slice(&bytes).map_err(EnvelopeError::InvalidJson)
66}
67
68pub fn decode_fixed_hex(
70 label: &str,
71 value: &str,
72 expected_len: usize,
73) -> Result<Vec<u8>, FixedHexError> {
74 let bytes = hex::decode(value).map_err(|source| FixedHexError::InvalidHex {
75 label: label.to_string(),
76 source,
77 })?;
78 if bytes.len() != expected_len {
79 return Err(FixedHexError::InvalidLength {
80 label: label.to_string(),
81 expected_len,
82 actual_len: bytes.len(),
83 });
84 }
85 Ok(bytes)
86}