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, Serialize, Deserialize)]
28pub struct AllowedSender {
29 pub channel: String,
30 pub account_id: String,
31 pub sender_id: String,
32 pub approved_at: DateTime<Utc>,
33 pub approved_via: String,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub revoked_at: Option<DateTime<Utc>>,
36}
37
38#[derive(Debug, Clone)]
39pub struct UpsertOutcome {
40 pub code: String,
41 pub created: bool,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum Decision {
48 Admit,
50 Challenge { code: String },
52 Drop,
55}
56
57#[derive(Debug, Clone, Default, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct PairingPolicy {
60 #[serde(default)]
64 pub auto_challenge: bool,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct SetupCode {
69 pub url: String,
70 pub bootstrap_token: String,
71 pub expires_at: DateTime<Utc>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct TokenClaims {
76 pub profile: String,
77 pub expires_at: DateTime<Utc>,
78 pub nonce: String,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub device_label: Option<String>,
81}
82
83#[derive(Debug, thiserror::Error)]
84pub enum PairingError {
85 #[error("unknown code")]
86 UnknownCode,
87 #[error("expired")]
88 Expired,
89 #[error("invalid signature")]
90 InvalidSignature,
91 #[error("max pending reached for {channel}:{account_id}")]
92 MaxPending { channel: String, account_id: String },
93 #[error("storage: {0}")]
94 Storage(String),
95 #[error("io: {0}")]
96 Io(String),
97 #[error("invalid: {0}")]
98 Invalid(&'static str),
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn pending_round_trip() {
107 let p = PendingRequest {
108 channel: "whatsapp".into(),
109 account_id: "personal".into(),
110 sender_id: "+57111".into(),
111 code: "ABCDEFGH".into(),
112 created_at: Utc::now(),
113 meta: serde_json::json!({"k":"v"}),
114 };
115 let s = serde_json::to_string(&p).unwrap();
116 let p2: PendingRequest = serde_json::from_str(&s).unwrap();
117 assert_eq!(p.code, p2.code);
118 assert_eq!(p.meta, p2.meta);
119 }
120
121 #[test]
122 fn policy_defaults_off() {
123 let p: PairingPolicy = serde_json::from_str("{}").unwrap();
124 assert!(!p.auto_challenge);
125 }
126
127 #[test]
128 fn policy_rejects_unknown_keys() {
129 let res: Result<PairingPolicy, _> = serde_json::from_str("{\"bogus\": true}");
130 assert!(res.is_err());
131 }
132}