Skip to main content

tauri_plugin_finueva_pub_sub/
models.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6pub(crate) const WEBSOCKET_PROTOCOL: &str = "finueva.pubsub.v1";
7pub(crate) const MAX_EVENT_MESSAGE_BYTES: usize = 34 * 1_024;
8pub(crate) const MAX_RESPONSE_BYTES: usize = 16 * 1_024;
9
10const REDACTED: &str = "[REDACTED]";
11
12#[derive(Clone, Deserialize, Serialize)]
13#[serde(rename_all = "camelCase")]
14pub struct ConnectRequest {
15    pub operation_id: String,
16    pub service_origin: String,
17    pub user_id: String,
18    pub ticket: String,
19}
20
21impl fmt::Debug for ConnectRequest {
22    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
23        formatter
24            .debug_struct("ConnectRequest")
25            .field("operation_id", &self.operation_id)
26            .field("service_origin", &REDACTED)
27            .field("user_id", &self.user_id)
28            .field("ticket", &REDACTED)
29            .finish()
30    }
31}
32
33#[derive(Clone, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct InstallationRequest {
36    pub operation_id: String,
37    pub service_origin: String,
38    pub user_id: String,
39    pub installation_id: String,
40    pub ticket: String,
41    pub token: Option<String>,
42}
43
44impl fmt::Debug for InstallationRequest {
45    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46        formatter
47            .debug_struct("InstallationRequest")
48            .field("operation_id", &self.operation_id)
49            .field("service_origin", &REDACTED)
50            .field("user_id", &self.user_id)
51            .field("installation_id", &self.installation_id)
52            .field("ticket", &REDACTED)
53            .field("token", &self.token.as_ref().map(|_| REDACTED))
54            .finish()
55    }
56}
57
58#[derive(Clone, Deserialize, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct PubSubPushContent {
61    pub title: String,
62    pub body: String,
63}
64
65impl fmt::Debug for PubSubPushContent {
66    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67        formatter
68            .debug_struct("PubSubPushContent")
69            .field("title", &REDACTED)
70            .field("body", &REDACTED)
71            .finish()
72    }
73}
74
75#[derive(Clone, Deserialize, Serialize)]
76#[serde(rename_all = "camelCase")]
77pub struct PubSubEvent {
78    pub id: String,
79    #[serde(rename = "type")]
80    pub event_type: String,
81    pub occurred_at: String,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub data: Option<Value>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub push: Option<PubSubPushContent>,
86}
87
88impl fmt::Debug for PubSubEvent {
89    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90        formatter
91            .debug_struct("PubSubEvent")
92            .field("id", &self.id)
93            .field("event_type", &self.event_type)
94            .field("occurred_at", &self.occurred_at)
95            .field("data", &self.data.as_ref().map(|_| REDACTED))
96            .field("push", &self.push)
97            .finish()
98    }
99}
100
101#[derive(Clone, Debug, Deserialize)]
102pub(crate) struct EventEnvelope {
103    pub kind: String,
104    pub event: PubSubEvent,
105}
106
107#[derive(Clone, Debug, Serialize)]
108#[serde(tag = "kind", rename_all = "camelCase")]
109pub enum ConnectionMessage {
110    Event {
111        event: PubSubEvent,
112    },
113    Closed {
114        cause: ConnectionCloseCause,
115        #[serde(skip_serializing_if = "Option::is_none")]
116        error: Option<crate::Error>,
117    },
118}
119
120#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
121#[serde(rename_all = "kebab-case")]
122pub enum ConnectionCloseCause {
123    Aborted,
124    ClosedByCaller,
125    Remote,
126    Error,
127}
128
129#[derive(Clone, Debug, Serialize)]
130#[serde(rename_all = "camelCase")]
131pub struct PutInstallationResult {
132    pub installation_id: String,
133    pub status: InstallationStatus,
134}
135
136#[derive(Clone, Copy, Debug, Serialize)]
137#[serde(rename_all = "lowercase")]
138pub enum InstallationStatus {
139    Created,
140    Replaced,
141}
142
143#[derive(Clone, Debug, Deserialize, Serialize)]
144pub struct DeleteInstallationResult {
145    pub removed: bool,
146}
147
148#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
149#[serde(rename_all = "lowercase")]
150pub enum PushPermissionState {
151    Granted,
152    Denied,
153    Prompt,
154}
155
156#[derive(Clone, Debug, Deserialize, Serialize)]
157#[serde(rename_all = "camelCase")]
158pub struct PushPermissionResponse {
159    #[serde(alias = "notifications", alias = "permissionState")]
160    pub state: PushPermissionState,
161}
162
163#[derive(Clone, Deserialize, Serialize)]
164#[serde(rename_all = "camelCase")]
165pub struct PushTokenResponse {
166    #[serde(alias = "deviceToken")]
167    pub token: String,
168}
169
170impl fmt::Debug for PushTokenResponse {
171    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
172        formatter
173            .debug_struct("PushTokenResponse")
174            .field("token", &REDACTED)
175            .finish()
176    }
177}
178
179#[derive(Clone, Debug, Deserialize, Serialize)]
180#[serde(rename_all = "camelCase")]
181pub struct PushMetadata {
182    pub delivery_id: String,
183    pub event_id: String,
184    pub event_type: String,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub title: Option<String>,
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub body: Option<String>,
189}
190
191#[derive(Clone, Deserialize, Serialize)]
192#[serde(rename_all = "camelCase")]
193pub struct PushTokenRefresh {
194    pub delivery_id: String,
195    pub token: String,
196}
197
198impl fmt::Debug for PushTokenRefresh {
199    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
200        formatter
201            .debug_struct("PushTokenRefresh")
202            .field("delivery_id", &self.delivery_id)
203            .field("token", &REDACTED)
204            .finish()
205    }
206}
207
208#[derive(Clone, Deserialize, Serialize)]
209#[serde(rename_all = "camelCase")]
210pub struct PendingPushEvent {
211    pub event: String,
212    pub payload: Value,
213}
214
215impl fmt::Debug for PendingPushEvent {
216    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
217        formatter
218            .debug_struct("PendingPushEvent")
219            .field("event", &self.event)
220            .field("payload", &REDACTED)
221            .finish()
222    }
223}
224
225#[derive(Clone, Debug, Deserialize, Serialize)]
226#[serde(rename_all = "camelCase")]
227pub struct PushListenerRequest {
228    pub event: String,
229    pub active: Option<bool>,
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn sensitive_debug_output_is_redacted() {
238        let connect = ConnectRequest {
239            operation_id: "operation_1".into(),
240            service_origin: "https://origin-user:origin-password@pub-sub.example.com".into(),
241            user_id: "user_1".into(),
242            ticket: "secret-connect-ticket".into(),
243        };
244        let installation = InstallationRequest {
245            operation_id: "operation_2".into(),
246            service_origin: "https://other-user:other-password@pub-sub.example.com".into(),
247            user_id: "user_1".into(),
248            installation_id: "mobile_primary".into(),
249            ticket: "secret-installation-ticket".into(),
250            token: Some("secret-fcm-token".into()),
251        };
252        let event = PubSubEvent {
253            id: "event_1".into(),
254            event_type: "order.updated".into(),
255            occurred_at: "2026-08-17T08:00:00Z".into(),
256            data: Some(serde_json::json!({ "secret": "event-data" })),
257            push: Some(PubSubPushContent {
258                title: "private title".into(),
259                body: "private body".into(),
260            }),
261        };
262        let token = PushTokenResponse {
263            token: "secret-response-token".into(),
264        };
265        let pending = PendingPushEvent {
266            event: "pushTokenRefresh".into(),
267            payload: serde_json::json!({ "token": "secret-pending-token" }),
268        };
269
270        let output = format!("{connect:?} {installation:?} {event:?} {token:?} {pending:?}");
271        for secret in [
272            "secret-connect-ticket",
273            "secret-installation-ticket",
274            "secret-fcm-token",
275            "origin-user",
276            "origin-password",
277            "other-user",
278            "other-password",
279            "event-data",
280            "private title",
281            "private body",
282            "secret-response-token",
283            "secret-pending-token",
284        ] {
285            assert!(!output.contains(secret));
286        }
287        assert!(output.contains(REDACTED));
288    }
289}