1use std::{collections::BTreeSet, convert::Infallible, fmt, str::FromStr};
4
5use lexe_common::time::TimestampMs;
6use serde::{Deserialize, Serialize, Serializer};
7use serde_with::DeserializeFromStr;
8
9use crate::request::OneTimeSecret;
10
11#[derive(Debug, thiserror::Error)]
12pub enum ResponseError {
13 #[error("Response is not valid JSON: {0}")]
14 Json(#[from] serde_json::Error),
15 #[error("Response must set exactly one of credential or error")]
16 Malformed,
17}
18
19#[derive(Clone, Debug, Eq, PartialEq)]
26pub struct CredentialResponse {
27 pub result: CredentialResult,
28 pub one_time_secret: OneTimeSecret,
30 pub account: Option<String>,
32 pub metadata: Option<String>,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub enum CredentialResult {
38 Granted(Grant),
39 Error(CredentialError),
40}
41
42#[derive(Clone, Debug, Eq, PartialEq)]
44pub struct Grant {
45 pub credential: String,
47 pub scopes: BTreeSet<String>,
49 pub permissions: BTreeSet<String>,
51 pub expires_at: Option<TimestampMs>,
53}
54
55#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct CredentialError {
57 pub code: ErrorCode,
58 pub message: Option<String>,
60}
61
62#[derive(Copy, Clone, Debug, Eq, PartialEq)]
66#[derive(DeserializeFromStr)]
67pub enum ErrorCode {
68 UserRejected,
69 Other,
70}
71
72#[serde_with::skip_serializing_none]
78#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
79pub struct CredentialResponseWire {
80 pub credential: Option<String>,
81 pub error: Option<ErrorCode>,
82 pub error_message: Option<String>,
83 pub one_time_secret: OneTimeSecret,
84 pub account: Option<String>,
85 pub metadata: Option<String>,
86 pub scopes: Option<BTreeSet<String>>,
87 pub permissions: Option<BTreeSet<String>>,
88 pub expires_at: Option<TimestampMs>,
89}
90
91impl CredentialResponse {
92 pub fn from_json(json: &[u8]) -> Result<Self, ResponseError> {
94 let wire = serde_json::from_slice::<CredentialResponseWire>(json)?;
95 Self::try_from_wire(wire)
96 }
97
98 pub fn try_from_wire(
99 wire: CredentialResponseWire,
100 ) -> Result<Self, ResponseError> {
101 let result = match (wire.credential, wire.error) {
102 (Some(credential), None) => CredentialResult::Granted(Grant {
103 credential,
104 scopes: wire.scopes.unwrap_or_default(),
105 permissions: wire.permissions.unwrap_or_default(),
106 expires_at: wire.expires_at,
107 }),
108 (None, Some(code)) => CredentialResult::Error(CredentialError {
109 code,
110 message: wire.error_message,
111 }),
112 _ => return Err(ResponseError::Malformed),
113 };
114
115 Ok(Self {
116 result,
117 one_time_secret: wire.one_time_secret,
118 account: wire.account,
119 metadata: wire.metadata,
120 })
121 }
122
123 pub fn to_json(self) -> Vec<u8> {
125 serde_json::to_vec(&CredentialResponseWire::from(self))
126 .expect("JSON serialization is infallible")
127 }
128}
129
130impl From<CredentialResponse> for CredentialResponseWire {
131 fn from(response: CredentialResponse) -> Self {
132 let mut wire = Self {
133 credential: None,
134 error: None,
135 error_message: None,
136 one_time_secret: response.one_time_secret,
137 account: response.account,
138 metadata: response.metadata,
139 scopes: None,
140 permissions: None,
141 expires_at: None,
142 };
143
144 match response.result {
145 CredentialResult::Granted(grant) => {
146 wire.credential = Some(grant.credential);
147 wire.scopes = Some(grant.scopes);
148 wire.permissions = Some(grant.permissions);
149 wire.expires_at = grant.expires_at;
150 }
151 CredentialResult::Error(error) => {
152 wire.error = Some(error.code);
153 wire.error_message = error.message;
154 }
155 }
156
157 wire
158 }
159}
160
161impl ErrorCode {
162 fn as_str(self) -> &'static str {
163 match self {
164 Self::UserRejected => "user_rejected",
165 Self::Other => "other",
166 }
167 }
168}
169
170impl Serialize for ErrorCode {
171 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
172 s.serialize_str(self.as_str())
173 }
174}
175
176impl fmt::Display for ErrorCode {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 f.write_str(self.as_str())
179 }
180}
181
182impl FromStr for ErrorCode {
183 type Err = Infallible;
184
185 fn from_str(s: &str) -> Result<Self, Self::Err> {
186 Ok(match s {
187 "user_rejected" => Self::UserRejected,
188 _ => Self::Other,
189 })
190 }
191}
192
193#[cfg(test)]
194mod test {
195 use super::*;
196
197 const SPEC_SUCCESS: &str = r#"{
199 "credential": "<client credential>",
200 "one_time_secret": "000102030405060708090a0b0c0d0e0f",
201 "account": "@janedoe",
202 "metadata": "<metadata echoed from the request>",
203 "scopes": ["read_info", "read_payments", "receive", "spend"],
204 "permissions": ["cancel_payment"],
205 "expires_at": 1821484800000,
206 "budget_limit": "20",
207 "budget_currency": "usd",
208 "budget_period": "month",
209 "budget_period_multiple": 1,
210 "budget_first_reset": 1790838000000,
211 "budget_utc_offset_secs": -25200
212 }"#;
213
214 const SPEC_ERROR: &str = r#"{
215 "error": "user_rejected",
216 "error_message": "<human-readable message>",
217 "one_time_secret": "000102030405060708090a0b0c0d0e0f",
218 "account": "@janedoe",
219 "metadata": "<metadata echoed from the request>"
220 }"#;
221
222 #[test]
223 fn spec_examples() {
224 let from_json =
225 |json: &str| CredentialResponse::from_json(json.as_bytes());
226 let success = from_json(SPEC_SUCCESS).unwrap();
227 let CredentialResult::Granted(grant) = &success.result else {
228 panic!("Expected a grant");
229 };
230 assert_eq!(grant.credential, "<client credential>");
231 assert_eq!(
232 grant.scopes,
233 ["read_info", "read_payments", "receive", "spend"]
234 .map(String::from)
235 .into()
236 );
237 assert_eq!(grant.permissions, ["cancel_payment".to_owned()].into());
238 assert_eq!(
239 grant.expires_at,
240 Some(TimestampMs::from_millis(1821484800000).unwrap())
241 );
242 assert_eq!(success.account.as_deref(), Some("@janedoe"));
243
244 let error = from_json(SPEC_ERROR).unwrap();
245 let CredentialResult::Error(err) = &error.result else {
246 panic!("Expected an error");
247 };
248 assert_eq!(err.code, ErrorCode::UserRejected);
249 assert_eq!(err.message.as_deref(), Some("<human-readable message>"));
250
251 for resp in [success, error] {
253 let json = String::from_utf8(resp.clone().to_json()).unwrap();
254 assert_eq!(from_json(&json).unwrap(), resp);
255 }
256 let json = SPEC_ERROR.replace("user_rejected", "new_code");
257 let CredentialResult::Error(err) = from_json(&json).unwrap().result
258 else {
259 panic!("Expected an error");
260 };
261 assert_eq!(err.code, ErrorCode::Other);
262
263 let secret_only =
265 r#"{"one_time_secret": "000102030405060708090a0b0c0d0e0f"}"#;
266 assert!(matches!(
267 from_json(secret_only),
268 Err(ResponseError::Malformed)
269 ));
270 let both = SPEC_ERROR
271 .replace("\"error\":", "\"credential\": \"c\", \"error\":");
272 assert!(matches!(from_json(&both), Err(ResponseError::Malformed)));
273 }
274
275 #[test]
276 fn serialized_shape_is_flat() {
277 let resp = CredentialResponse {
278 result: CredentialResult::Error(CredentialError {
279 code: ErrorCode::Other,
280 message: None,
281 }),
282 one_time_secret: "000102030405060708090a0b0c0d0e0f"
283 .parse()
284 .unwrap(),
285 account: None,
286 metadata: None,
287 };
288 assert_eq!(
289 String::from_utf8(resp.to_json()).unwrap(),
290 r#"{"error":"other","one_time_secret":"000102030405060708090a0b0c0d0e0f"}"#
291 );
292 }
293}