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