1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum ExternalSecretField {
9 Username,
10 Password,
11 Title,
12}
13
14impl ExternalSecretField {
15 pub fn as_str(self) -> &'static str {
16 match self {
17 Self::Username => "username",
18 Self::Password => "password",
19 Self::Title => "title",
20 }
21 }
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub enum IpcMessage {
27 GetCredential {
28 domain: String,
29 },
30 GetExternalSecret {
31 client_id: String,
32 domain: String,
33 field: ExternalSecretField,
34 purpose: Option<String>,
35 },
36 GetExternalSecretResponse {
37 value: Option<String>,
38 authorized: bool,
39 error: Option<String>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
42 locked: Option<bool>,
43 },
44 GetCredentialResponse {
45 username: Option<String>,
46 password: Option<String>,
47 title: Option<String>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 locked: Option<bool>,
50 },
51 ListDomainCredentials {
52 base_domain: String,
53 },
54 ListDomainCredentialsResponse {
55 credentials: Vec<CredentialSummary>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 locked: Option<bool>,
58 },
59 GetTotpCode {
60 domain: String,
61 },
62 GetTotpCodeResponse {
63 code: Option<String>,
64 seconds_remaining: Option<u32>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 locked: Option<bool>,
67 },
68 SaveCredential {
69 domain: String,
70 username: String,
71 password: String,
72 url: Option<String>,
73 },
74 SaveCredentialResponse {
75 success: bool,
76 error: Option<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 locked: Option<bool>,
79 },
80 SaveSecret {
83 client_id: String,
84 domain: String,
85 value: String,
86 purpose: Option<String>,
87 },
88 SaveSecretResponse {
89 success: bool,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 locked: Option<bool>,
92 error: Option<String>,
93 },
94 DeleteSecret {
98 client_id: String,
99 domain: String,
100 },
101 DeleteSecretResponse {
102 deleted: bool,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 locked: Option<bool>,
105 error: Option<String>,
106 },
107 UnlockVault {
108 master_password: String,
109 },
110 UnlockVaultBiometric {
111 prompt_reason: Option<String>,
112 },
113 UnlockVaultResponse {
114 success: bool,
115 error: Option<String>,
116 },
117 CheckVault,
118 VaultStatusResponse {
119 unlocked: bool,
120 },
121 LockVault,
122 Shutdown,
123
124 SyncNow,
127 SyncNowResponse {
129 success: bool,
130 pushed: u64,
131 pulled: u64,
132 error: Option<String>,
133 },
134 SyncStatus,
136 SyncStatusResponse {
138 enabled: bool,
139 device_id: Option<String>,
140 device_name: Option<String>,
141 relay_url: Option<String>,
142 last_sync_at: Option<i64>,
143 pending_changes: u64,
144 },
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct CredentialSummary {
150 pub username: String,
151 pub title: Option<String>,
152 pub domain: String,
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_ipc_envelope_round_trip_is_in_envelope_tests() {
161 }
163
164 #[test]
165 fn test_credential_summary_serialization() {
166 let summary = CredentialSummary {
167 username: "user@example.com".to_string(),
168 title: Some("Example Account".to_string()),
169 domain: "example.com".to_string(),
170 };
171
172 let serialized = serde_json::to_string(&summary).unwrap();
173 let deserialized: CredentialSummary = serde_json::from_str(&serialized).unwrap();
174
175 assert_eq!(deserialized.username, summary.username);
176 assert_eq!(deserialized.title, summary.title);
177 assert_eq!(deserialized.domain, summary.domain);
178 }
179
180 #[test]
181 fn test_credential_summary_without_title() {
182 let summary = CredentialSummary {
183 username: "user@example.com".to_string(),
184 title: None,
185 domain: "example.com".to_string(),
186 };
187
188 let serialized = serde_json::to_string(&summary).unwrap();
189 let deserialized: CredentialSummary = serde_json::from_str(&serialized).unwrap();
190
191 assert_eq!(deserialized.username, summary.username);
192 assert_eq!(deserialized.title, None);
193 assert_eq!(deserialized.domain, summary.domain);
194 }
195
196 #[test]
197 fn test_message_types_serialize_correctly() {
198 let messages = vec![
199 IpcMessage::GetCredential {
200 domain: "example.com".to_string(),
201 },
202 IpcMessage::GetExternalSecret {
203 client_id: "victor".to_string(),
204 domain: "anthropic".to_string(),
205 field: ExternalSecretField::Password,
206 purpose: Some("victor-auth".to_string()),
207 },
208 IpcMessage::CheckVault,
209 IpcMessage::LockVault,
210 IpcMessage::Shutdown,
211 IpcMessage::ListDomainCredentials {
212 base_domain: "example.com".to_string(),
213 },
214 ];
215
216 for msg in messages {
217 let serialized = serde_json::to_string(&msg).unwrap();
218 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
219
220 match (&msg, &deserialized) {
222 (
223 IpcMessage::GetCredential { domain: d1 },
224 IpcMessage::GetCredential { domain: d2 },
225 ) => {
226 assert_eq!(d1, d2);
227 }
228 (
229 IpcMessage::GetExternalSecret {
230 client_id: c1,
231 domain: d1,
232 field: f1,
233 purpose: p1,
234 },
235 IpcMessage::GetExternalSecret {
236 client_id: c2,
237 domain: d2,
238 field: f2,
239 purpose: p2,
240 },
241 ) => {
242 assert_eq!(c1, c2);
243 assert_eq!(d1, d2);
244 assert_eq!(f1, f2);
245 assert_eq!(p1, p2);
246 }
247 (
248 IpcMessage::ListDomainCredentials { base_domain: b1 },
249 IpcMessage::ListDomainCredentials { base_domain: b2 },
250 ) => {
251 assert_eq!(b1, b2);
252 }
253 (IpcMessage::CheckVault, IpcMessage::CheckVault) => {}
254 (IpcMessage::LockVault, IpcMessage::LockVault) => {}
255 (IpcMessage::Shutdown, IpcMessage::Shutdown) => {}
256 _ => panic!("Message type mismatch during round-trip"),
257 }
258 }
259 }
260
261 #[test]
262 fn test_get_credential_response_serialization() {
263 let response = IpcMessage::GetCredentialResponse {
264 username: Some("user@example.com".to_string()),
265 password: Some("password123".to_string()),
266 title: Some("Example".to_string()),
267 locked: None,
268 };
269
270 let serialized = serde_json::to_string(&response).unwrap();
271 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
272
273 match deserialized {
274 IpcMessage::GetCredentialResponse {
275 username,
276 password,
277 title,
278 locked: None,
279 } => {
280 assert_eq!(username, Some("user@example.com".to_string()));
281 assert_eq!(password, Some("password123".to_string()));
282 assert_eq!(title, Some("Example".to_string()));
283 }
284 _ => panic!("Wrong response type"),
285 }
286 }
287
288 #[test]
289 fn test_get_external_secret_response_serialization() {
290 let response = IpcMessage::GetExternalSecretResponse {
291 value: Some("secret-value".to_string()),
292 authorized: true,
293 error: None,
294 locked: None,
295 };
296
297 let serialized = serde_json::to_string(&response).unwrap();
298 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
299
300 match deserialized {
301 IpcMessage::GetExternalSecretResponse {
302 value,
303 authorized,
304 error,
305 locked: None,
306 } => {
307 assert_eq!(value, Some("secret-value".to_string()));
308 assert!(authorized);
309 assert_eq!(error, None);
310 }
311 _ => panic!("Wrong response type"),
312 }
313 }
314
315 #[test]
316 fn test_list_domain_credentials_response_serialization() {
317 let credentials = vec![
318 CredentialSummary {
319 username: "user1@example.com".to_string(),
320 title: Some("Account 1".to_string()),
321 domain: "example.com".to_string(),
322 },
323 CredentialSummary {
324 username: "user2@example.com".to_string(),
325 title: Some("Account 2".to_string()),
326 domain: "example.com".to_string(),
327 },
328 ];
329
330 let response = IpcMessage::ListDomainCredentialsResponse {
331 credentials: credentials.clone(),
332 locked: None,
333 };
334
335 let serialized = serde_json::to_string(&response).unwrap();
336 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
337
338 match deserialized {
339 IpcMessage::ListDomainCredentialsResponse {
340 credentials: decoded,
341 locked: None,
342 } => {
343 assert_eq!(decoded.len(), 2);
344 assert_eq!(decoded[0].username, "user1@example.com");
345 assert_eq!(decoded[1].username, "user2@example.com");
346 }
347 _ => panic!("Wrong response type"),
348 }
349 }
350
351 #[test]
352 fn test_save_credential_response_serialization() {
353 let response = IpcMessage::SaveCredentialResponse {
354 success: true,
355 error: None,
356 locked: None,
357 };
358
359 let serialized = serde_json::to_string(&response).unwrap();
360 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
361
362 match deserialized {
363 IpcMessage::SaveCredentialResponse {
364 success,
365 error,
366 locked: None,
367 } => {
368 assert!(success);
369 assert!(error.is_none());
370 }
371 _ => panic!("Wrong response type"),
372 }
373 }
374
375 #[test]
376 fn test_save_credential_error_response_serialization() {
377 let response = IpcMessage::SaveCredentialResponse {
378 success: false,
379 error: Some("Vault is locked".to_string()),
380 locked: None,
381 };
382
383 let serialized = serde_json::to_string(&response).unwrap();
384 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
385
386 match deserialized {
387 IpcMessage::SaveCredentialResponse {
388 success,
389 error,
390 locked,
391 } => {
392 assert!(!success);
393 assert_eq!(error, Some("Vault is locked".to_string()));
394 assert_eq!(locked, None);
395 }
396 _ => panic!("Wrong response type"),
397 }
398 }
399
400 #[test]
401 fn test_unlock_vault_response_serialization() {
402 let response = IpcMessage::UnlockVaultResponse {
403 success: true,
404 error: None,
405 };
406
407 let serialized = serde_json::to_string(&response).unwrap();
408 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
409
410 match deserialized {
411 IpcMessage::UnlockVaultResponse { success, error } => {
412 assert!(success);
413 assert!(error.is_none());
414 }
415 _ => panic!("Wrong response type"),
416 }
417 }
418
419 #[test]
420 fn test_vault_status_response_serialization() {
421 let response = IpcMessage::VaultStatusResponse { unlocked: true };
422
423 let serialized = serde_json::to_string(&response).unwrap();
424 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
425
426 match deserialized {
427 IpcMessage::VaultStatusResponse { unlocked } => {
428 assert!(unlocked);
429 }
430 _ => panic!("Wrong response type"),
431 }
432 }
433
434 #[test]
435 fn test_totp_response_serialization() {
436 let response = IpcMessage::GetTotpCodeResponse {
437 code: Some("123456".to_string()),
438 seconds_remaining: Some(30),
439 locked: None,
440 };
441
442 let serialized = serde_json::to_string(&response).unwrap();
443 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
444
445 match deserialized {
446 IpcMessage::GetTotpCodeResponse {
447 code,
448 seconds_remaining,
449 locked: None,
450 } => {
451 assert_eq!(code, Some("123456".to_string()));
452 assert_eq!(seconds_remaining, Some(30));
453 }
454 _ => panic!("Wrong response type"),
455 }
456 }
457
458 #[test]
459 fn test_save_credential_message_serialization() {
460 let msg = IpcMessage::SaveCredential {
461 domain: "example.com".to_string(),
462 username: "user@example.com".to_string(),
463 password: "secure_password".to_string(),
464 url: Some("https://example.com".to_string()),
465 };
466
467 let serialized = serde_json::to_string(&msg).unwrap();
468 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
469
470 match deserialized {
471 IpcMessage::SaveCredential {
472 domain,
473 username,
474 password,
475 url,
476 } => {
477 assert_eq!(domain, "example.com");
478 assert_eq!(username, "user@example.com");
479 assert_eq!(password, "secure_password");
480 assert_eq!(url, Some("https://example.com".to_string()));
481 }
482 _ => panic!("Wrong message type"),
483 }
484 }
485
486 #[test]
487 fn test_unlock_vault_message_serialization() {
488 let msg = IpcMessage::UnlockVault {
489 master_password: "test_password".to_string(),
490 };
491
492 let serialized = serde_json::to_string(&msg).unwrap();
493 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
494
495 match deserialized {
496 IpcMessage::UnlockVault { master_password } => {
497 assert_eq!(master_password, "test_password");
498 }
499 _ => panic!("Wrong message type"),
500 }
501 }
502
503 #[test]
504 fn test_unlock_vault_biometric_message_serialization() {
505 let msg = IpcMessage::UnlockVaultBiometric {
506 prompt_reason: Some("Authenticate to unlock".to_string()),
507 };
508
509 let serialized = serde_json::to_string(&msg).unwrap();
510 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
511
512 match deserialized {
513 IpcMessage::UnlockVaultBiometric { prompt_reason } => {
514 assert_eq!(prompt_reason, Some("Authenticate to unlock".to_string()));
515 }
516 _ => panic!("Wrong response type"),
517 }
518 }
519
520 #[test]
521 fn test_empty_credential_list_serialization() {
522 let response = IpcMessage::ListDomainCredentialsResponse {
523 credentials: vec![],
524 locked: None,
525 };
526
527 let serialized = serde_json::to_string(&response).unwrap();
528 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
529
530 match deserialized {
531 IpcMessage::ListDomainCredentialsResponse {
532 credentials,
533 locked: None,
534 } => {
535 assert!(credentials.is_empty());
536 }
537 _ => panic!("Wrong response type"),
538 }
539 }
540
541 #[test]
542 fn test_get_totp_code_message_serialization() {
543 let msg = IpcMessage::GetTotpCode {
544 domain: "example.com".to_string(),
545 };
546
547 let serialized = serde_json::to_string(&msg).unwrap();
548 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
549
550 match deserialized {
551 IpcMessage::GetTotpCode { domain } => {
552 assert_eq!(domain, "example.com");
553 }
554 _ => panic!("Wrong message type"),
555 }
556 }
557
558 #[test]
559 fn test_sync_status_message_serialization() {
560 let msg = IpcMessage::SyncStatus;
561
562 let serialized = serde_json::to_string(&msg).unwrap();
563 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
564
565 match deserialized {
566 IpcMessage::SyncStatus => {}
567 _ => panic!("Wrong message type"),
568 }
569 }
570
571 #[test]
572 fn test_sync_status_response_serialization() {
573 let response = IpcMessage::SyncStatusResponse {
574 enabled: true,
575 device_id: Some("device-123".to_string()),
576 device_name: Some("Test Device".to_string()),
577 relay_url: Some("https://relay.example.com".to_string()),
578 last_sync_at: Some(1700000000),
579 pending_changes: 5,
580 };
581
582 let serialized = serde_json::to_string(&response).unwrap();
583 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
584
585 match deserialized {
586 IpcMessage::SyncStatusResponse {
587 enabled,
588 device_id,
589 device_name,
590 relay_url,
591 last_sync_at,
592 pending_changes,
593 } => {
594 assert!(enabled);
595 assert_eq!(device_id, Some("device-123".to_string()));
596 assert_eq!(device_name, Some("Test Device".to_string()));
597 assert_eq!(relay_url, Some("https://relay.example.com".to_string()));
598 assert_eq!(last_sync_at, Some(1700000000));
599 assert_eq!(pending_changes, 5);
600 }
601 _ => panic!("Wrong response type"),
602 }
603 }
604}