1use crate::service::{ServiceOutcome, VaultOp};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum ExternalSecretField {
10 Username,
11 Password,
12 Title,
13}
14
15impl ExternalSecretField {
16 pub fn as_str(self) -> &'static str {
17 match self {
18 Self::Username => "username",
19 Self::Password => "password",
20 Self::Title => "title",
21 }
22 }
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub enum IpcMessage {
28 GetCredential {
37 domain: String,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 page_url: Option<String>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
44 username: Option<String>,
45 },
46 GetExternalSecret {
47 client_id: String,
48 domain: String,
49 field: ExternalSecretField,
50 purpose: Option<String>,
51 },
52 GetExternalSecretResponse {
53 value: Option<String>,
54 authorized: bool,
55 error: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 locked: Option<bool>,
59 },
60 GetCredentialResponse {
61 username: Option<String>,
62 password: Option<String>,
63 title: Option<String>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 locked: Option<bool>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
70 denied_reason: Option<String>,
71 },
72 ListDomainCredentials {
73 base_domain: String,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 page_url: Option<String>,
76 },
77 ListDomainCredentialsResponse {
78 credentials: Vec<CredentialSummary>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 locked: Option<bool>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 denied_reason: Option<String>,
83 },
84 GetTotpCode {
85 domain: String,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 page_url: Option<String>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
91 username: Option<String>,
92 },
93 GetTotpCodeResponse {
94 code: Option<String>,
95 seconds_remaining: Option<u32>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
97 locked: Option<bool>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 denied_reason: Option<String>,
100 },
101 SaveCredential {
102 domain: String,
103 username: String,
104 password: String,
105 url: Option<String>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
110 save_trigger: Option<String>,
111 },
112 SaveCredentialResponse {
113 success: bool,
114 error: Option<String>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 locked: Option<bool>,
117 },
118 SaveSecret {
121 client_id: String,
122 domain: String,
123 value: String,
124 purpose: Option<String>,
125 },
126 SaveSecretResponse {
127 success: bool,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 locked: Option<bool>,
130 error: Option<String>,
131 },
132 DeleteSecret {
137 client_id: String,
138 domain: String,
139 },
140 DeleteSecretResponse {
141 deleted: bool,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 locked: Option<bool>,
144 error: Option<String>,
145 },
146 UnlockVault {
147 master_password: String,
148 },
149 GrantSitePermission {
154 host: String,
155 allow_insecure: bool,
156 },
157 GrantSitePermissionResponse {
158 success: bool,
159 error: Option<String>,
160 },
161 RevokeSitePermission {
163 host: String,
164 },
165 RevokeSitePermissionResponse {
166 success: bool,
167 removed: bool,
168 error: Option<String>,
169 },
170 ListSitePermissions,
172 ListSitePermissionsResponse {
173 permissions: Vec<SitePermissionSummary>,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 locked: Option<bool>,
176 },
177 UnlockVaultBiometric {
178 prompt_reason: Option<String>,
179 },
180 UnlockVaultResponse {
181 success: bool,
182 error: Option<String>,
183 },
184 CheckVault,
185 VaultStatusResponse {
186 unlocked: bool,
187 #[serde(default)]
191 key_epoch: i64,
192 },
193 LockVault,
194 Shutdown,
195
196 SyncNow,
199 SyncNowResponse {
201 success: bool,
202 pushed: u64,
203 pulled: u64,
204 error: Option<String>,
205 },
206 SyncStatus,
208 SyncStatusResponse {
210 enabled: bool,
211 device_id: Option<String>,
212 device_name: Option<String>,
213 relay_url: Option<String>,
214 last_sync_at: Option<i64>,
215 pending_changes: u64,
216 },
217
218 ServiceCall {
222 op: VaultOp,
223 },
224 ServiceResult {
226 outcome: ServiceOutcome,
227 },
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct CredentialSummary {
233 pub username: String,
234 pub title: Option<String>,
235 pub domain: String,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct SitePermissionSummary {
241 pub host: String,
243 pub allow_insecure: bool,
245 pub granted_at: i64,
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn test_ipc_envelope_round_trip_is_in_envelope_tests() {
255 }
257
258 #[test]
259 fn test_credential_summary_serialization() {
260 let summary = CredentialSummary {
261 username: "user@example.com".to_string(),
262 title: Some("Example Account".to_string()),
263 domain: "example.com".to_string(),
264 };
265
266 let serialized = serde_json::to_string(&summary).unwrap();
267 let deserialized: CredentialSummary = serde_json::from_str(&serialized).unwrap();
268
269 assert_eq!(deserialized.username, summary.username);
270 assert_eq!(deserialized.title, summary.title);
271 assert_eq!(deserialized.domain, summary.domain);
272 }
273
274 #[test]
275 fn test_credential_summary_without_title() {
276 let summary = CredentialSummary {
277 username: "user@example.com".to_string(),
278 title: None,
279 domain: "example.com".to_string(),
280 };
281
282 let serialized = serde_json::to_string(&summary).unwrap();
283 let deserialized: CredentialSummary = serde_json::from_str(&serialized).unwrap();
284
285 assert_eq!(deserialized.username, summary.username);
286 assert_eq!(deserialized.title, None);
287 assert_eq!(deserialized.domain, summary.domain);
288 }
289
290 #[test]
291 fn test_message_types_serialize_correctly() {
292 let messages = vec![
293 IpcMessage::GetCredential {
294 domain: "example.com".to_string(),
295 page_url: Some("https://example.com/login".to_string()),
296 username: None,
297 },
298 IpcMessage::GetExternalSecret {
299 client_id: "victor".to_string(),
300 domain: "anthropic".to_string(),
301 field: ExternalSecretField::Password,
302 purpose: Some("victor-auth".to_string()),
303 },
304 IpcMessage::CheckVault,
305 IpcMessage::LockVault,
306 IpcMessage::Shutdown,
307 IpcMessage::ListDomainCredentials {
308 base_domain: "example.com".to_string(),
309 page_url: None,
310 },
311 ];
312
313 for msg in messages {
314 let serialized = serde_json::to_string(&msg).unwrap();
315 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
316
317 match (&msg, &deserialized) {
319 (
320 IpcMessage::GetCredential {
321 domain: d1,
322 page_url: u1,
323 username: n1,
324 },
325 IpcMessage::GetCredential {
326 domain: d2,
327 page_url: u2,
328 username: n2,
329 },
330 ) => {
331 assert_eq!(d1, d2);
332 assert_eq!(u1, u2);
333 assert_eq!(n1, n2);
334 }
335 (
336 IpcMessage::GetExternalSecret {
337 client_id: c1,
338 domain: d1,
339 field: f1,
340 purpose: p1,
341 },
342 IpcMessage::GetExternalSecret {
343 client_id: c2,
344 domain: d2,
345 field: f2,
346 purpose: p2,
347 },
348 ) => {
349 assert_eq!(c1, c2);
350 assert_eq!(d1, d2);
351 assert_eq!(f1, f2);
352 assert_eq!(p1, p2);
353 }
354 (
355 IpcMessage::ListDomainCredentials {
356 base_domain: b1,
357 page_url: p1,
358 },
359 IpcMessage::ListDomainCredentials {
360 base_domain: b2,
361 page_url: p2,
362 },
363 ) => {
364 assert_eq!(b1, b2);
365 assert_eq!(p1, p2);
366 }
367 (IpcMessage::CheckVault, IpcMessage::CheckVault) => {}
368 (IpcMessage::LockVault, IpcMessage::LockVault) => {}
369 (IpcMessage::Shutdown, IpcMessage::Shutdown) => {}
370 _ => panic!("Message type mismatch during round-trip"),
371 }
372 }
373 }
374
375 #[test]
376 fn test_get_credential_response_serialization() {
377 let response = IpcMessage::GetCredentialResponse {
378 username: Some("user@example.com".to_string()),
379 password: Some("password123".to_string()),
380 title: Some("Example".to_string()),
381 locked: None,
382 denied_reason: None,
383 };
384
385 let serialized = serde_json::to_string(&response).unwrap();
386 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
387
388 match deserialized {
389 IpcMessage::GetCredentialResponse {
390 username,
391 password,
392 title,
393 locked: None,
394 denied_reason: None,
395 } => {
396 assert_eq!(username, Some("user@example.com".to_string()));
397 assert_eq!(password, Some("password123".to_string()));
398 assert_eq!(title, Some("Example".to_string()));
399 }
400 _ => panic!("Wrong response type"),
401 }
402 }
403
404 #[test]
408 fn test_autofill_denied_reason_wire_compat() {
409 let legacy = r#"{
410 "GetCredentialResponse": {
411 "username": null,
412 "password": null,
413 "title": null
414 }
415 }"#;
416 let deserialized: IpcMessage = serde_json::from_str(legacy).unwrap();
417 match deserialized {
418 IpcMessage::GetCredentialResponse { denied_reason, .. } => {
419 assert_eq!(denied_reason, None);
420 }
421 _ => panic!("Wrong response type"),
422 }
423
424 let denied = IpcMessage::GetCredentialResponse {
425 username: None,
426 password: None,
427 title: None,
428 locked: None,
429 denied_reason: Some("insecure-http".to_string()),
430 };
431 let serialized = serde_json::to_string(&denied).unwrap();
432 assert!(serialized.contains("insecure-http"));
433
434 let pre711_request = r#"{"GetCredential": {"domain": "example.com"}}"#;
435 let deserialized: IpcMessage = serde_json::from_str(pre711_request).unwrap();
436 match deserialized {
437 IpcMessage::GetCredential {
438 domain,
439 page_url,
440 username,
441 } => {
442 assert_eq!(domain, "example.com");
443 assert_eq!(page_url, None);
444 assert_eq!(username, None);
445 }
446 _ => panic!("Wrong request type"),
447 }
448 }
449
450 #[test]
451 fn test_get_external_secret_response_serialization() {
452 let response = IpcMessage::GetExternalSecretResponse {
453 value: Some("secret-value".to_string()),
454 authorized: true,
455 error: None,
456 locked: None,
457 };
458
459 let serialized = serde_json::to_string(&response).unwrap();
460 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
461
462 match deserialized {
463 IpcMessage::GetExternalSecretResponse {
464 value,
465 authorized,
466 error,
467 locked: None,
468 } => {
469 assert_eq!(value, Some("secret-value".to_string()));
470 assert!(authorized);
471 assert_eq!(error, None);
472 }
473 _ => panic!("Wrong response type"),
474 }
475 }
476
477 #[test]
478 fn test_list_domain_credentials_response_serialization() {
479 let credentials = vec![
480 CredentialSummary {
481 username: "user1@example.com".to_string(),
482 title: Some("Account 1".to_string()),
483 domain: "example.com".to_string(),
484 },
485 CredentialSummary {
486 username: "user2@example.com".to_string(),
487 title: Some("Account 2".to_string()),
488 domain: "example.com".to_string(),
489 },
490 ];
491
492 let response = IpcMessage::ListDomainCredentialsResponse {
493 credentials: credentials.clone(),
494 locked: None,
495 denied_reason: None,
496 };
497
498 let serialized = serde_json::to_string(&response).unwrap();
499 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
500
501 match deserialized {
502 IpcMessage::ListDomainCredentialsResponse {
503 credentials: decoded,
504 locked: None,
505 denied_reason: None,
506 } => {
507 assert_eq!(decoded.len(), 2);
508 assert_eq!(decoded[0].username, "user1@example.com");
509 assert_eq!(decoded[1].username, "user2@example.com");
510 }
511 _ => panic!("Wrong response type"),
512 }
513 }
514
515 #[test]
516 fn test_save_credential_response_serialization() {
517 let response = IpcMessage::SaveCredentialResponse {
518 success: true,
519 error: None,
520 locked: None,
521 };
522
523 let serialized = serde_json::to_string(&response).unwrap();
524 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
525
526 match deserialized {
527 IpcMessage::SaveCredentialResponse {
528 success,
529 error,
530 locked: None,
531 } => {
532 assert!(success);
533 assert!(error.is_none());
534 }
535 _ => panic!("Wrong response type"),
536 }
537 }
538
539 #[test]
540 fn test_save_credential_error_response_serialization() {
541 let response = IpcMessage::SaveCredentialResponse {
542 success: false,
543 error: Some("Vault is locked".to_string()),
544 locked: None,
545 };
546
547 let serialized = serde_json::to_string(&response).unwrap();
548 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
549
550 match deserialized {
551 IpcMessage::SaveCredentialResponse {
552 success,
553 error,
554 locked,
555 } => {
556 assert!(!success);
557 assert_eq!(error, Some("Vault is locked".to_string()));
558 assert_eq!(locked, None);
559 }
560 _ => panic!("Wrong response type"),
561 }
562 }
563
564 #[test]
565 fn test_unlock_vault_response_serialization() {
566 let response = IpcMessage::UnlockVaultResponse {
567 success: true,
568 error: None,
569 };
570
571 let serialized = serde_json::to_string(&response).unwrap();
572 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
573
574 match deserialized {
575 IpcMessage::UnlockVaultResponse { success, error } => {
576 assert!(success);
577 assert!(error.is_none());
578 }
579 _ => panic!("Wrong response type"),
580 }
581 }
582
583 #[test]
584 fn test_vault_status_response_serialization() {
585 let response = IpcMessage::VaultStatusResponse {
586 unlocked: true,
587 key_epoch: 1,
588 };
589
590 let serialized = serde_json::to_string(&response).unwrap();
591 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
592
593 match deserialized {
594 IpcMessage::VaultStatusResponse {
595 unlocked,
596 key_epoch,
597 } => {
598 assert!(unlocked);
599 assert_eq!(key_epoch, 1);
600 }
601 _ => panic!("Wrong response type"),
602 }
603 }
604
605 #[test]
606 fn test_totp_response_serialization() {
607 let response = IpcMessage::GetTotpCodeResponse {
608 code: Some("123456".to_string()),
609 seconds_remaining: Some(30),
610 locked: None,
611 denied_reason: None,
612 };
613
614 let serialized = serde_json::to_string(&response).unwrap();
615 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
616
617 match deserialized {
618 IpcMessage::GetTotpCodeResponse {
619 code,
620 seconds_remaining,
621 locked: None,
622 denied_reason: None,
623 } => {
624 assert_eq!(code, Some("123456".to_string()));
625 assert_eq!(seconds_remaining, Some(30));
626 }
627 _ => panic!("Wrong response type"),
628 }
629 }
630
631 #[test]
632 fn test_save_credential_message_serialization() {
633 let msg = IpcMessage::SaveCredential {
634 domain: "example.com".to_string(),
635 username: "user@example.com".to_string(),
636 password: "secure_password".to_string(),
637 url: Some("https://example.com".to_string()),
638 save_trigger: Some("password_change".to_string()),
639 };
640
641 let serialized = serde_json::to_string(&msg).unwrap();
642 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
643
644 match deserialized {
645 IpcMessage::SaveCredential {
646 domain,
647 username,
648 password,
649 url,
650 save_trigger,
651 } => {
652 assert_eq!(domain, "example.com");
653 assert_eq!(username, "user@example.com");
654 assert_eq!(password, "secure_password");
655 assert_eq!(url, Some("https://example.com".to_string()));
656 assert_eq!(save_trigger.as_deref(), Some("password_change"));
657 }
658 _ => panic!("Wrong message type"),
659 }
660 }
661
662 #[test]
663 fn test_unlock_vault_message_serialization() {
664 let msg = IpcMessage::UnlockVault {
665 master_password: "test_password".to_string(),
666 };
667
668 let serialized = serde_json::to_string(&msg).unwrap();
669 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
670
671 match deserialized {
672 IpcMessage::UnlockVault { master_password } => {
673 assert_eq!(master_password, "test_password");
674 }
675 _ => panic!("Wrong message type"),
676 }
677 }
678
679 #[test]
680 fn test_unlock_vault_biometric_message_serialization() {
681 let msg = IpcMessage::UnlockVaultBiometric {
682 prompt_reason: Some("Authenticate to unlock".to_string()),
683 };
684
685 let serialized = serde_json::to_string(&msg).unwrap();
686 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
687
688 match deserialized {
689 IpcMessage::UnlockVaultBiometric { prompt_reason } => {
690 assert_eq!(prompt_reason, Some("Authenticate to unlock".to_string()));
691 }
692 _ => panic!("Wrong response type"),
693 }
694 }
695
696 #[test]
697 fn test_empty_credential_list_serialization() {
698 let response = IpcMessage::ListDomainCredentialsResponse {
699 credentials: vec![],
700 locked: None,
701 denied_reason: None,
702 };
703
704 let serialized = serde_json::to_string(&response).unwrap();
705 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
706
707 match deserialized {
708 IpcMessage::ListDomainCredentialsResponse {
709 credentials,
710 locked: None,
711 denied_reason: None,
712 } => {
713 assert!(credentials.is_empty());
714 }
715 _ => panic!("Wrong response type"),
716 }
717 }
718
719 #[test]
720 fn test_get_totp_code_message_serialization() {
721 let msg = IpcMessage::GetTotpCode {
722 domain: "example.com".to_string(),
723 page_url: Some("https://example.com/login".to_string()),
724 username: None,
725 };
726
727 let serialized = serde_json::to_string(&msg).unwrap();
728 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
729
730 match deserialized {
731 IpcMessage::GetTotpCode { domain, .. } => {
732 assert_eq!(domain, "example.com");
733 }
734 _ => panic!("Wrong message type"),
735 }
736 }
737
738 #[test]
739 fn test_sync_status_message_serialization() {
740 let msg = IpcMessage::SyncStatus;
741
742 let serialized = serde_json::to_string(&msg).unwrap();
743 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
744
745 match deserialized {
746 IpcMessage::SyncStatus => {}
747 _ => panic!("Wrong message type"),
748 }
749 }
750
751 #[test]
752 fn test_sync_status_response_serialization() {
753 let response = IpcMessage::SyncStatusResponse {
754 enabled: true,
755 device_id: Some("device-123".to_string()),
756 device_name: Some("Test Device".to_string()),
757 relay_url: Some("https://relay.example.com".to_string()),
758 last_sync_at: Some(1700000000),
759 pending_changes: 5,
760 };
761
762 let serialized = serde_json::to_string(&response).unwrap();
763 let deserialized: IpcMessage = serde_json::from_str(&serialized).unwrap();
764
765 match deserialized {
766 IpcMessage::SyncStatusResponse {
767 enabled,
768 device_id,
769 device_name,
770 relay_url,
771 last_sync_at,
772 pending_changes,
773 } => {
774 assert!(enabled);
775 assert_eq!(device_id, Some("device-123".to_string()));
776 assert_eq!(device_name, Some("Test Device".to_string()));
777 assert_eq!(relay_url, Some("https://relay.example.com".to_string()));
778 assert_eq!(last_sync_at, Some(1700000000));
779 assert_eq!(pending_changes, 5);
780 }
781 _ => panic!("Wrong response type"),
782 }
783 }
784}