Skip to main content

nexo_pairing/
types.rs

1//! Public types shared by store / gate / setup-code.
2
3use 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/// One row from `pairing_allow_from`. `revoked_at` is `None` for
26/// active entries; populated rows are kept for audit (soft-delete).
27#[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    /// `true` when this call inserted a new pending row, `false` when
42    /// it returned the code from an existing one.
43    pub created: bool,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum Decision {
48    /// Sender is in `allow_from`; publish as normal.
49    Admit,
50    /// Sender is unknown; reply with `code` and drop the message.
51    Challenge { code: String },
52    /// Drop without reply (max-pending exhausted, or `auto_challenge`
53    /// is off and sender is unknown).
54    Drop,
55}
56
57#[derive(Debug, Clone, Default, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct PairingPolicy {
60    /// When `true`, unknown senders trigger a challenge reply. When
61    /// `false`, the gate is a no-op (every message is admitted).
62    /// Default `false` keeps existing setups working without changes.
63    #[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}