tauri_plugin_finueva_pub_sub/
models.rs1use 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}