1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct PendingRequest {
8 pub channel: String,
9 pub account_id: String,
10 pub sender_id: String,
11 pub code: String,
12 pub created_at: DateTime<Utc>,
13 #[serde(default)]
14 pub meta: serde_json::Value,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ApprovedRequest {
19 pub channel: String,
20 pub account_id: String,
21 pub sender_id: String,
22 pub approved_at: DateTime<Utc>,
23}
24
25#[derive(Debug, Clone)]
26pub struct UpsertOutcome {
27 pub code: String,
28 pub created: bool,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum Decision {
35 Admit,
37 Challenge { code: String },
39 Drop,
42}
43
44#[derive(Debug, Clone, Default, Deserialize)]
45#[serde(deny_unknown_fields)]
46pub struct PairingPolicy {
47 #[serde(default)]
51 pub auto_challenge: bool,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct SetupCode {
56 pub url: String,
57 pub bootstrap_token: String,
58 pub expires_at: DateTime<Utc>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct TokenClaims {
63 pub profile: String,
64 pub expires_at: DateTime<Utc>,
65 pub nonce: String,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub device_label: Option<String>,
68}
69
70#[derive(Debug, thiserror::Error)]
71pub enum PairingError {
72 #[error("unknown code")]
73 UnknownCode,
74 #[error("expired")]
75 Expired,
76 #[error("invalid signature")]
77 InvalidSignature,
78 #[error("max pending reached for {channel}:{account_id}")]
79 MaxPending { channel: String, account_id: String },
80 #[error("storage: {0}")]
81 Storage(String),
82 #[error("io: {0}")]
83 Io(String),
84 #[error("invalid: {0}")]
85 Invalid(&'static str),
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn pending_round_trip() {
94 let p = PendingRequest {
95 channel: "whatsapp".into(),
96 account_id: "personal".into(),
97 sender_id: "+57111".into(),
98 code: "ABCDEFGH".into(),
99 created_at: Utc::now(),
100 meta: serde_json::json!({"k":"v"}),
101 };
102 let s = serde_json::to_string(&p).unwrap();
103 let p2: PendingRequest = serde_json::from_str(&s).unwrap();
104 assert_eq!(p.code, p2.code);
105 assert_eq!(p.meta, p2.meta);
106 }
107
108 #[test]
109 fn policy_defaults_off() {
110 let p: PairingPolicy = serde_json::from_str("{}").unwrap();
111 assert!(!p.auto_challenge);
112 }
113
114 #[test]
115 fn policy_rejects_unknown_keys() {
116 let res: Result<PairingPolicy, _> = serde_json::from_str("{\"bogus\": true}");
117 assert!(res.is_err());
118 }
119}