Skip to main content

im_core/identity/
dto.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::collections::BTreeMap;
3use std::path::PathBuf;
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub enum IdentitySelector {
7    Default,
8    Id(crate::ids::IdentityId),
9    Did(crate::ids::Did),
10    Handle(crate::ids::Handle),
11    LocalAlias(String),
12}
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct IdentitySummary {
16    pub id: crate::ids::IdentityId,
17    pub did: crate::ids::Did,
18    pub handle: Option<crate::ids::Handle>,
19    pub display_name: Option<String>,
20    pub local_alias: Option<String>,
21    pub device_id: Option<String>,
22    pub is_default: bool,
23    pub readiness: IdentityReadiness,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct IdentityReadiness {
28    pub ready_for_auth: bool,
29    pub ready_for_messaging: bool,
30    pub missing: Vec<IdentityMissingItem>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum IdentitySecretStorageBackend {
36    FileCompat,
37    Vault,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct IdentityVaultStatus {
42    pub identity: IdentitySummary,
43    pub storage_policy: crate::core::IdentitySecretStoragePolicy,
44    pub selected_backend: IdentitySecretStorageBackend,
45    pub vault_available: bool,
46    pub vault_metadata_present: bool,
47    pub vault_metadata_verified: bool,
48    pub workspace_id: Option<String>,
49    pub device_id: Option<String>,
50    pub plaintext_compat_retained: Option<bool>,
51    pub missing: Vec<String>,
52    pub warnings: Vec<String>,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct IdentityVaultMigrationReport {
57    pub identity: IdentitySummary,
58    pub status: IdentityVaultStatus,
59    pub migrated: bool,
60    pub verified: bool,
61    pub plaintext_compat_retained: bool,
62    pub warnings: Vec<String>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct IdentityVaultVerificationReport {
67    pub identity: IdentitySummary,
68    pub status: IdentityVaultStatus,
69    pub verified: bool,
70    pub warnings: Vec<String>,
71}
72
73#[derive(Clone, PartialEq, Eq)]
74pub struct HostedIdentityMaterial {
75    pub identity_id: String,
76    pub did: String,
77    pub handle: Option<String>,
78    pub display_name: Option<String>,
79    pub did_document: serde_json::Value,
80    pub default_signing_private_key_pem: String,
81    pub e2ee_agreement_private_key_pem: String,
82    pub auth_token: Option<String>,
83}
84
85impl std::fmt::Debug for HostedIdentityMaterial {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        f.debug_struct("HostedIdentityMaterial")
88            .field("identity_id", &self.identity_id)
89            .field("did", &self.did)
90            .field("handle", &self.handle)
91            .field("display_name", &self.display_name)
92            .field("did_document", &"<redacted-hosted-did-document>")
93            .field("default_signing_private_key_pem", &"<redacted-private-key>")
94            .field("e2ee_agreement_private_key_pem", &"<redacted-private-key>")
95            .field(
96                "auth_token",
97                &self.auth_token.as_ref().map(|_| "<redacted-token>"),
98            )
99            .finish()
100    }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub enum IdentityMissingItem {
105    DidDocument,
106    PrivateKey,
107    AuthState,
108    Handle,
109    MessageEndpoint,
110    Other(String),
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114pub struct RegisterHandleRequest {
115    pub local_alias: Option<String>,
116    pub requested_handle: crate::ids::Handle,
117    pub verification: VerificationInput,
118    pub invite_code: Option<String>,
119    pub profile: InitialProfile,
120    pub make_default: bool,
121}
122
123pub const DAEMON_SUBKEY_PACKAGE_SCHEMA_V1: &str = "awiki.daemon.user_subkey_package.v1";
124pub const DAEMON_SUBKEY_PACKAGE_SCHEMA_V2: &str = "awiki.daemon.user_subkey_package.v2";
125pub const DAEMON_SUBKEY_PRIVATE_KEY_ENCODING_PEM: &str = "pem";
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct DaemonSubkeyPrivatePackage {
129    pub schema: String,
130    pub user_did: crate::ids::Did,
131    pub verification_method: String,
132    pub key_type: String,
133    pub key_algorithm: Option<String>,
134    pub public_key_multibase: String,
135    pub private_key_encoding: String,
136    pub private_key_pem: String,
137    /// Legacy compatibility field. New JSON serialization writes `private_key_pem`
138    /// instead of this v1 field, but older Rust/Dart callers may still read it.
139    pub private_key_multibase: String,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct DaemonSubkeyAuthorizationRevokeResult {
144    pub user_did: crate::ids::Did,
145    pub verification_method: String,
146    pub updated: bool,
147}
148
149impl DaemonSubkeyPrivatePackage {
150    pub fn new_v2_pem(
151        user_did: crate::ids::Did,
152        verification_method: String,
153        key_type: String,
154        key_algorithm: Option<String>,
155        public_key_multibase: String,
156        private_key_pem: String,
157    ) -> Self {
158        Self {
159            schema: DAEMON_SUBKEY_PACKAGE_SCHEMA_V2.to_owned(),
160            user_did,
161            verification_method,
162            key_type,
163            key_algorithm,
164            public_key_multibase,
165            private_key_encoding: DAEMON_SUBKEY_PRIVATE_KEY_ENCODING_PEM.to_owned(),
166            private_key_multibase: private_key_pem.clone(),
167            private_key_pem,
168        }
169    }
170
171    pub fn private_key_material(&self) -> &str {
172        if !self.private_key_pem.trim().is_empty() {
173            &self.private_key_pem
174        } else {
175            &self.private_key_multibase
176        }
177    }
178
179    pub fn is_v2_pem(&self) -> bool {
180        self.schema == DAEMON_SUBKEY_PACKAGE_SCHEMA_V2
181            && self.private_key_encoding == DAEMON_SUBKEY_PRIVATE_KEY_ENCODING_PEM
182            && !self.private_key_pem.trim().is_empty()
183    }
184}
185
186impl Serialize for DaemonSubkeyPrivatePackage {
187    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
188    where
189        S: Serializer,
190    {
191        #[derive(Serialize)]
192        struct Wire<'a> {
193            schema: &'a str,
194            user_did: &'a crate::ids::Did,
195            verification_method: &'a str,
196            key_type: &'a str,
197            #[serde(skip_serializing_if = "Option::is_none")]
198            key_algorithm: Option<&'a str>,
199            public_key_multibase: &'a str,
200            private_key_encoding: &'a str,
201            private_key_pem: &'a str,
202        }
203
204        let private_key_pem = self.private_key_material();
205        let private_key_encoding = if self.private_key_encoding.trim().is_empty() {
206            DAEMON_SUBKEY_PRIVATE_KEY_ENCODING_PEM
207        } else {
208            self.private_key_encoding.trim()
209        };
210        Wire {
211            schema: DAEMON_SUBKEY_PACKAGE_SCHEMA_V2,
212            user_did: &self.user_did,
213            verification_method: &self.verification_method,
214            key_type: &self.key_type,
215            key_algorithm: self.key_algorithm.as_deref(),
216            public_key_multibase: &self.public_key_multibase,
217            private_key_encoding,
218            private_key_pem,
219        }
220        .serialize(serializer)
221    }
222}
223
224impl<'de> Deserialize<'de> for DaemonSubkeyPrivatePackage {
225    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
226    where
227        D: Deserializer<'de>,
228    {
229        #[derive(Deserialize)]
230        struct Wire {
231            schema: String,
232            user_did: crate::ids::Did,
233            verification_method: String,
234            key_type: String,
235            #[serde(default)]
236            key_algorithm: Option<String>,
237            public_key_multibase: String,
238            #[serde(default)]
239            private_key_encoding: Option<String>,
240            #[serde(default)]
241            private_key_pem: Option<String>,
242            #[serde(default)]
243            private_key_multibase: Option<String>,
244        }
245
246        let wire = Wire::deserialize(deserializer)?;
247        let private_key_pem = wire
248            .private_key_pem
249            .as_deref()
250            .map(str::trim)
251            .filter(|value| !value.is_empty())
252            .map(ToOwned::to_owned)
253            .or_else(|| {
254                wire.private_key_multibase
255                    .as_deref()
256                    .map(str::trim)
257                    .filter(|value| !value.is_empty())
258                    .map(ToOwned::to_owned)
259            })
260            .ok_or_else(|| serde::de::Error::missing_field("private_key_pem"))?;
261        let private_key_encoding = wire
262            .private_key_encoding
263            .as_deref()
264            .map(str::trim)
265            .filter(|value| !value.is_empty())
266            .unwrap_or(DAEMON_SUBKEY_PRIVATE_KEY_ENCODING_PEM)
267            .to_string();
268        Ok(Self {
269            schema: wire.schema,
270            user_did: wire.user_did,
271            verification_method: wire.verification_method,
272            key_type: wire.key_type,
273            key_algorithm: wire.key_algorithm,
274            public_key_multibase: wire.public_key_multibase,
275            private_key_encoding,
276            private_key_multibase: private_key_pem.clone(),
277            private_key_pem,
278        })
279    }
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
283pub enum VerificationInput {
284    Otp {
285        code: String,
286    },
287    Phone {
288        phone: String,
289        otp: Option<String>,
290    },
291    Email {
292        email: String,
293        wait_for_verification: bool,
294    },
295    AlreadyVerified,
296}
297
298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
299pub struct InitialProfile {
300    pub display_name: Option<String>,
301    pub avatar_url: Option<String>,
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
305pub struct HandleRegistrationResult {
306    pub identity: Option<IdentitySummary>,
307    pub handle: crate::ids::Handle,
308    pub method: RegistrationMethod,
309    pub state: HandleRegistrationState,
310    pub default_identity_change: Option<DefaultIdentityChange>,
311    pub warnings: Vec<String>,
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
315pub enum RegistrationMethod {
316    Phone,
317    Email,
318    AlreadyVerified,
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
322pub enum HandleRegistrationState {
323    OtpSent,
324    EmailSent,
325    EmailPending,
326    Registered,
327}
328
329#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
330pub struct DefaultIdentityChange {
331    pub previous: Option<IdentitySummary>,
332    pub next: IdentitySummary,
333    pub requires_default_identity_write: bool,
334    pub warnings: Vec<String>,
335}
336
337#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
338pub struct DeleteLocalIdentityResult {
339    pub deleted: IdentitySummary,
340    pub was_default: bool,
341    pub next_default: Option<IdentitySummary>,
342    pub warnings: Vec<String>,
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
346pub struct Profile {
347    pub subject: crate::ids::Did,
348    pub handle: Option<crate::ids::Handle>,
349    pub display_name: Option<String>,
350    pub bio: Option<String>,
351    pub description: Option<String>,
352    pub tags: Vec<String>,
353    pub markdown: Option<String>,
354    pub avatar_uri: Option<String>,
355    pub avatar_url: Option<String>,
356    pub profile_uri: Option<String>,
357    pub subject_type: Option<String>,
358    pub updated_at: Option<String>,
359    #[serde(
360        default,
361        rename = "versionId",
362        alias = "version_id",
363        skip_serializing_if = "Option::is_none"
364    )]
365    pub version_id: Option<String>,
366    pub ttl: Option<u64>,
367    pub proof: Option<serde_json::Value>,
368    pub metadata: Vec<ProfileAttribute>,
369}
370
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
372pub struct ProfileAttribute {
373    pub key: String,
374    pub value: String,
375}
376
377impl Profile {
378    pub fn new(subject: crate::ids::Did) -> Self {
379        Self {
380            subject,
381            handle: None,
382            display_name: None,
383            bio: None,
384            description: None,
385            tags: Vec::new(),
386            markdown: None,
387            avatar_uri: None,
388            avatar_url: None,
389            profile_uri: None,
390            subject_type: None,
391            updated_at: None,
392            version_id: None,
393            ttl: None,
394            proof: None,
395            metadata: Vec::new(),
396        }
397    }
398
399    pub fn effective_description(&self) -> Option<&String> {
400        self.description.as_ref().or(self.bio.as_ref())
401    }
402
403    pub fn effective_avatar_uri(&self) -> Option<&String> {
404        self.avatar_uri.as_ref().or(self.avatar_url.as_ref())
405    }
406
407    pub fn to_wire_profile_value(&self) -> serde_json::Value {
408        let mut value = serde_json::Map::new();
409        value.insert(
410            "did".to_string(),
411            serde_json::Value::String(self.subject.as_str().to_string()),
412        );
413        value.insert(
414            "subject_did".to_string(),
415            serde_json::Value::String(self.subject.as_str().to_string()),
416        );
417        if let Some(handle) = self.handle.as_ref() {
418            value.insert(
419                "handle".to_string(),
420                serde_json::Value::String(handle.as_str().to_string()),
421            );
422        }
423        if let Some(display_name) = self.display_name.as_ref() {
424            value.insert(
425                "display_name".to_string(),
426                serde_json::Value::String(display_name.clone()),
427            );
428            value.insert(
429                "nick_name".to_string(),
430                serde_json::Value::String(display_name.clone()),
431            );
432        }
433        if let Some(description) = self.effective_description() {
434            value.insert(
435                "description".to_string(),
436                serde_json::Value::String(description.clone()),
437            );
438        }
439        if let Some(bio) = self.bio.as_ref().or(self.description.as_ref()) {
440            value.insert("bio".to_string(), serde_json::Value::String(bio.clone()));
441        }
442        if !self.tags.is_empty() {
443            value.insert("tags".to_string(), serde_json::json!(self.tags));
444        }
445        if let Some(markdown) = self.markdown.as_ref() {
446            value.insert(
447                "profile_md".to_string(),
448                serde_json::Value::String(markdown.clone()),
449            );
450        }
451        if let Some(avatar_uri) = self.effective_avatar_uri() {
452            value.insert(
453                "avatar_uri".to_string(),
454                serde_json::Value::String(avatar_uri.clone()),
455            );
456        }
457        if let Some(avatar_url) = self.avatar_url.as_ref().or(self.avatar_uri.as_ref()) {
458            value.insert(
459                "avatar_url".to_string(),
460                serde_json::Value::String(avatar_url.clone()),
461            );
462        }
463        if let Some(profile_uri) = self.profile_uri.as_ref() {
464            value.insert(
465                "profile_uri".to_string(),
466                serde_json::Value::String(profile_uri.clone()),
467            );
468        }
469        if let Some(subject_type) = self.subject_type.as_ref() {
470            value.insert(
471                "subject_type".to_string(),
472                serde_json::Value::String(subject_type.clone()),
473            );
474        }
475        if let Some(updated_at) = self.updated_at.as_ref() {
476            value.insert(
477                "updated_at".to_string(),
478                serde_json::Value::String(updated_at.clone()),
479            );
480            value.insert(
481                "updated".to_string(),
482                serde_json::Value::String(updated_at.clone()),
483            );
484        }
485        if let Some(version_id) = self.version_id.as_ref() {
486            value.insert(
487                "versionId".to_string(),
488                serde_json::Value::String(version_id.clone()),
489            );
490        }
491        if let Some(ttl) = self.ttl {
492            value.insert("ttl".to_string(), serde_json::json!(ttl));
493        }
494        if let Some(proof) = self.proof.as_ref() {
495            value.insert("proof".to_string(), proof.clone());
496        }
497        if !self.metadata.is_empty() {
498            value.insert(
499                "metadata".to_string(),
500                serde_json::Value::Object(
501                    self.metadata
502                        .iter()
503                        .map(|attribute| {
504                            (
505                                attribute.key.clone(),
506                                serde_json::Value::String(attribute.value.clone()),
507                            )
508                        })
509                        .collect(),
510                ),
511            );
512        }
513        serde_json::Value::Object(value)
514    }
515}
516
517#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
518pub struct ProfilePatch {
519    pub display_name: Option<String>,
520    pub bio: Option<String>,
521    pub tags: Option<Vec<String>>,
522    pub markdown: Option<String>,
523    pub avatar_uri: Option<String>,
524    pub avatar_url: Option<String>,
525}
526
527#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
528pub struct ContactBindingRequest {
529    pub method: ContactBindingMethod,
530    pub wait_for_email_verification: bool,
531}
532
533#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
534pub enum ContactBindingMethod {
535    Phone { phone: String, otp: Option<String> },
536    Email { email: String },
537}
538
539#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
540pub struct ContactBindingResult {
541    pub method: ContactBindingMethodKind,
542    pub target: String,
543    pub state: ContactBindingState,
544    #[serde(skip)]
545    raw_response: Option<serde_json::Value>,
546    pub warnings: Vec<String>,
547}
548
549impl ContactBindingResult {
550    pub(crate) fn with_raw_response(
551        method: ContactBindingMethodKind,
552        target: String,
553        state: ContactBindingState,
554        raw_response: Option<serde_json::Value>,
555        warnings: Vec<String>,
556    ) -> Self {
557        Self {
558            method,
559            target,
560            state,
561            raw_response,
562            warnings,
563        }
564    }
565
566    pub fn response_json(&self) -> Option<&serde_json::Value> {
567        self.raw_response.as_ref()
568    }
569}
570
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
572pub enum ContactBindingMethodKind {
573    Phone,
574    Email,
575}
576
577#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
578pub enum ContactBindingState {
579    OtpSent,
580    EmailSent,
581    Pending,
582    Completed,
583}
584
585#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
586pub struct RecoverHandleRequest {
587    pub handle: crate::ids::Handle,
588    #[serde(default, skip_serializing_if = "Option::is_none")]
589    pub raw_handle: Option<String>,
590    pub phone: String,
591    pub otp: Option<String>,
592    pub generated_identity: Option<RecoverGeneratedIdentity>,
593    #[serde(default, skip_serializing_if = "Option::is_none")]
594    pub local_finalize: Option<RecoverHandleLocalFinalizeRequest>,
595}
596
597#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
598pub struct RecoverGeneratedIdentity {
599    pub did: crate::ids::Did,
600    pub unique_id: String,
601    pub did_document: serde_json::Value,
602}
603
604#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
605pub struct RecoverHandleLocalFinalizeRequest {
606    #[serde(default, skip_serializing_if = "Option::is_none")]
607    pub raw_handle: Option<String>,
608    #[serde(default, skip_serializing_if = "Option::is_none")]
609    pub active_identity_name: Option<String>,
610    #[serde(default, skip_serializing_if = "Option::is_none")]
611    pub config_file_path: Option<PathBuf>,
612}
613
614#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
615pub struct RecoverHandlePlanRequest {
616    pub handle: crate::ids::Handle,
617    #[serde(default, skip_serializing_if = "Option::is_none")]
618    pub raw_handle: Option<String>,
619    pub phone: String,
620    pub otp: Option<String>,
621}
622
623#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
624pub struct RecoverHandlePlan {
625    pub action: String,
626    pub target_handle: String,
627    pub identity_name: String,
628    pub final_identity_name: String,
629    pub temp_identity_name: String,
630    pub same_handle_candidates: Vec<RecoverLocalIdentitySummary>,
631    pub excluded_identities: Vec<RecoverLocalIdentitySummary>,
632    pub backup_path: String,
633    pub phone: String,
634    pub remote_calls: Vec<String>,
635    pub local_writes: Option<Vec<String>>,
636}
637
638#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
639pub struct RecoverHandleResult {
640    pub handle: crate::ids::Handle,
641    pub phone: String,
642    pub state: RecoverHandleState,
643    pub recovered_identity: Option<RecoveredIdentity>,
644    #[serde(default, skip_serializing_if = "Option::is_none")]
645    pub local_recovery: Option<RecoverHandleLocalResult>,
646    #[serde(skip)]
647    raw_response: Option<serde_json::Value>,
648    pub warnings: Vec<String>,
649}
650
651impl RecoverHandleResult {
652    pub(crate) fn with_raw_response(
653        handle: crate::ids::Handle,
654        phone: String,
655        state: RecoverHandleState,
656        recovered_identity: Option<RecoveredIdentity>,
657        local_recovery: Option<RecoverHandleLocalResult>,
658        raw_response: Option<serde_json::Value>,
659        warnings: Vec<String>,
660    ) -> Self {
661        Self {
662            handle,
663            phone,
664            state,
665            recovered_identity,
666            local_recovery,
667            raw_response,
668            warnings,
669        }
670    }
671
672    pub fn response_json(&self) -> Option<&serde_json::Value> {
673        self.raw_response.as_ref()
674    }
675}
676
677#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
678pub enum RecoverHandleState {
679    OtpSent,
680    Recovered,
681}
682
683#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
684pub struct RecoveredIdentity {
685    pub identity: IdentitySummary,
686    pub user_id: Option<String>,
687    pub access_token_present: bool,
688}
689
690#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
691pub struct RecoverHandleLocalResult {
692    pub identity: RecoverLocalIdentitySummary,
693    pub backup_path: String,
694    pub archived_identities: Vec<String>,
695    pub archived_dids: Vec<String>,
696    pub full_handle: String,
697    pub final_identity_name: String,
698    pub store_merge_counts: BTreeMap<String, i64>,
699    pub e2ee_cleanup_counts: BTreeMap<String, i64>,
700    pub default_updated: bool,
701    pub active_config_updated: bool,
702}
703
704#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
705pub struct RecoverLocalIdentitySummary {
706    pub identity_name: String,
707    pub did: String,
708    pub unique_id: String,
709    #[serde(skip_serializing_if = "String::is_empty")]
710    pub display_name: String,
711    #[serde(skip_serializing_if = "String::is_empty")]
712    pub handle: String,
713    #[serde(skip_serializing_if = "String::is_empty")]
714    pub full_handle: String,
715    #[serde(skip_serializing_if = "String::is_empty")]
716    pub created_at: String,
717    pub dir_name: String,
718    pub is_default: bool,
719    pub has_jwt: bool,
720    pub has_did_document: bool,
721    pub has_key1_private: bool,
722    pub has_key1_public: bool,
723    pub has_e2ee_signing_private: bool,
724    pub has_e2ee_agreement_private: bool,
725    pub user_state: RecoverLocalUserState,
726    #[serde(skip)]
727    pub user_id: String,
728}
729
730#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
731pub struct RecoverLocalUserState {
732    pub registration_state: String,
733    pub ready_for_messaging: bool,
734    #[serde(skip_serializing_if = "Vec::is_empty")]
735    pub missing: Vec<String>,
736}
737
738#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
739pub struct ReplaceDidPlanRequest {
740    pub identity: IdentitySummary,
741    pub linked_identity_names: Vec<String>,
742    pub planned_new_did: crate::ids::Did,
743    pub backup_path_preview: String,
744    pub old_dir_name: String,
745    pub is_public: Option<bool>,
746    pub is_agent: Option<bool>,
747    pub role: Option<String>,
748    pub endpoint_url: Option<String>,
749    pub affected_local_state: ReplaceDidAffectedLocalState,
750}
751
752#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
753pub struct ReplaceDidPlan {
754    pub action: String,
755    pub identity: IdentitySummary,
756    pub dangerous: bool,
757    pub risk_summary: Vec<String>,
758    pub backup_plan: ReplaceDidBackupPlan,
759    pub local_rebind_plan: ReplaceDidLocalRebindPlan,
760    pub affected_local_state: ReplaceDidAffectedLocalState,
761    pub remote_replace_did_call_preview: ReplaceDidRemoteCallPreview,
762    pub rollback_notes: Vec<String>,
763    pub local_writes: Vec<String>,
764    pub warnings: Vec<String>,
765}
766
767#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
768pub struct ReplaceDidBackupPlan {
769    pub required: bool,
770    pub backup_path_preview: String,
771    pub manifest_preview: ReplaceDidBackupManifestPreview,
772}
773
774#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
775pub struct ReplaceDidBackupManifestPreview {
776    pub reason: String,
777    pub identity_name: String,
778    pub linked_identity_names: Vec<String>,
779    pub old_did: crate::ids::Did,
780    pub old_dir_name: String,
781    pub planned_new_did: crate::ids::Did,
782}
783
784#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
785pub struct ReplaceDidLocalRebindPlan {
786    pub required: bool,
787    pub old_owner_did: crate::ids::Did,
788    pub new_owner_did: crate::ids::Did,
789    pub destructive: bool,
790    pub dry_run_only: bool,
791}
792
793#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
794pub struct ReplaceDidAffectedLocalState {
795    pub store_rebind_counts: BTreeMap<String, i64>,
796    pub e2ee_cleanup_counts: BTreeMap<String, i64>,
797}
798
799#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
800pub struct ReplaceDidRemoteCallPreview {
801    pub endpoint: String,
802    pub method: String,
803    pub params: serde_json::Value,
804}
805
806#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
807pub struct ReplaceDidGeneratedIdentity {
808    pub did: crate::ids::Did,
809    pub unique_id: String,
810    pub did_document: serde_json::Value,
811}
812
813#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
814pub struct ReplaceDidExecutionRequest {
815    pub plan: ReplaceDidPlan,
816    pub generated_identity: ReplaceDidGeneratedIdentity,
817    pub is_public: Option<bool>,
818    pub is_agent: Option<bool>,
819    pub role: Option<String>,
820    pub endpoint_url: Option<String>,
821}
822
823#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
824pub struct ReplaceDidExecutionResult {
825    pub identity: IdentitySummary,
826    pub old_did: crate::ids::Did,
827    pub new_did: crate::ids::Did,
828    pub backup_path: String,
829    pub backup_manifest: ReplaceDidBackupManifestPreview,
830    pub affected_local_state: ReplaceDidAffectedLocalState,
831    pub remote_result: serde_json::Value,
832    pub warnings: Vec<String>,
833    pub recovery_notes: Vec<String>,
834}
835
836#[cfg(test)]
837mod tests {
838    use super::*;
839    use serde_json::json;
840
841    #[test]
842    fn binding_and_recover_results_keep_raw_response_internal_only() {
843        let binding = ContactBindingResult::with_raw_response(
844            ContactBindingMethodKind::Email,
845            "alice@example.test".to_string(),
846            ContactBindingState::EmailSent,
847            Some(json!({ "provider_state": "sent" })),
848            vec!["queued".to_string()],
849        );
850        let binding_json = serde_json::to_value(&binding).expect("serialize binding result");
851        assert_eq!(
852            binding
853                .response_json()
854                .and_then(|raw| raw.get("provider_state")),
855            Some(&json!("sent"))
856        );
857        assert!(binding_json.get("raw_response").is_none());
858        assert!(binding_json.get("raw_response").is_none());
859        assert!(binding_json.get("raw").is_none());
860
861        let recover = RecoverHandleResult::with_raw_response(
862            crate::ids::Handle::parse("alice", "example.test").expect("handle"),
863            "+15551234567".to_string(),
864            RecoverHandleState::OtpSent,
865            None,
866            None,
867            Some(json!({ "sent": true })),
868            Vec::new(),
869        );
870        let recover_json = serde_json::to_value(&recover).expect("serialize recover result");
871        assert_eq!(
872            recover.response_json().and_then(|raw| raw.get("sent")),
873            Some(&json!(true))
874        );
875        assert!(recover_json.get("raw_response").is_none());
876        assert!(recover_json.get("raw_response").is_none());
877        assert!(recover_json.get("raw").is_none());
878    }
879
880    #[test]
881    fn daemon_subkey_package_writes_v2_pem_without_legacy_private_field() {
882        let package = DaemonSubkeyPrivatePackage::new_v2_pem(
883            crate::ids::Did::parse("did:example:alice").unwrap(),
884            "did:example:alice#daemon-key-1".to_string(),
885            "Multikey/Ed25519".to_string(),
886            Some("Ed25519".to_string()),
887            "zPublic".to_string(),
888            "-----BEGIN PRIVATE KEY-----\nsecret\n-----END PRIVATE KEY-----".to_string(),
889        );
890
891        let value = serde_json::to_value(&package).unwrap();
892
893        assert_eq!(value["schema"], DAEMON_SUBKEY_PACKAGE_SCHEMA_V2);
894        assert_eq!(
895            value["private_key_encoding"],
896            DAEMON_SUBKEY_PRIVATE_KEY_ENCODING_PEM
897        );
898        assert_eq!(value["private_key_pem"], package.private_key_pem);
899        assert!(value.get("private_key_multibase").is_none());
900    }
901
902    #[test]
903    fn daemon_subkey_package_reads_legacy_v1_private_key_multibase() {
904        let package: DaemonSubkeyPrivatePackage = serde_json::from_value(json!({
905            "schema": DAEMON_SUBKEY_PACKAGE_SCHEMA_V1,
906            "user_did": "did:example:alice",
907            "verification_method": "did:example:alice#daemon-key-1",
908            "key_type": "Multikey/Ed25519",
909            "public_key_multibase": "zPublic",
910            "private_key_multibase": "-----BEGIN PRIVATE KEY-----\nsecret\n-----END PRIVATE KEY-----"
911        }))
912        .unwrap();
913
914        assert_eq!(package.schema, DAEMON_SUBKEY_PACKAGE_SCHEMA_V1);
915        assert_eq!(
916            package.private_key_encoding,
917            DAEMON_SUBKEY_PRIVATE_KEY_ENCODING_PEM
918        );
919        assert_eq!(package.private_key_pem, package.private_key_multibase);
920        assert!(package
921            .private_key_material()
922            .starts_with("-----BEGIN PRIVATE KEY-----"));
923    }
924}