Skip to main content

im_core/identity/
registry.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use serde::{Deserialize, Deserializer, Serialize};
7use serde_json::Value;
8
9pub struct IdentityRegistry<'a> {
10    core: &'a crate::core::ImCore,
11}
12
13impl<'a> IdentityRegistry<'a> {
14    pub(crate) fn new(core: &'a crate::core::ImCore) -> Self {
15        Self { core }
16    }
17
18    pub fn list(&self) -> crate::ImResult<Vec<super::IdentitySummary>> {
19        Ok(self
20            .load_registry()?
21            .entries
22            .into_iter()
23            .map(|entry| entry.summary)
24            .collect())
25    }
26
27    pub async fn list_async(&self) -> crate::ImResult<Vec<super::IdentitySummary>> {
28        Ok(self
29            .load_registry_async()
30            .await?
31            .entries
32            .into_iter()
33            .map(|entry| entry.summary)
34            .collect())
35    }
36
37    pub fn default_identity(&self) -> crate::ImResult<Option<super::IdentitySummary>> {
38        Ok(self.load_registry()?.default_identity())
39    }
40
41    pub async fn default_identity_async(&self) -> crate::ImResult<Option<super::IdentitySummary>> {
42        Ok(self.load_registry_async().await?.default_identity())
43    }
44
45    pub fn delete_local_identity(
46        &self,
47        selector: super::IdentitySelector,
48    ) -> crate::ImResult<super::DeleteLocalIdentityResult> {
49        let paths = &self.core.inner().sdk_paths().identities;
50        let mut registry = self.load_registry()?;
51        let deleted_index = registry.find_index(selector)?;
52        let deleted_entry = registry.entries.remove(deleted_index);
53        let deleted = deleted_entry.summary.clone();
54        let was_default = deleted.is_default
55            || registry.default_alias.as_deref() == deleted_entry.local_alias.as_deref();
56
57        let mut warnings = Vec::new();
58        if let Some(identity_dir_name) = deleted_entry.identity_dir_name() {
59            let identity_dir = local_identity_dir(&paths.identity_root_dir, &identity_dir_name)?;
60            match fs::remove_dir_all(&identity_dir) {
61                Ok(()) => {}
62                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
63                    warnings.push(format!(
64                        "local identity directory was already missing: {}",
65                        identity_dir.display()
66                    ));
67                }
68                Err(err) => return Err(crate::ImError::from(err)),
69            }
70        } else {
71            warnings.push(format!(
72                "local identity {} did not include a usable directory name",
73                deleted.id.as_str()
74            ));
75        }
76
77        if was_default {
78            registry.default_alias = registry
79                .entries
80                .first()
81                .and_then(|entry| entry.local_alias.clone());
82        }
83        registry.apply_default_flags();
84        let next_default = registry.default_identity();
85        write_registry(&paths.registry_path, &registry)?;
86        write_default_identity(
87            paths.default_identity_path.as_deref(),
88            registry.default_alias.as_deref(),
89        )?;
90
91        Ok(super::DeleteLocalIdentityResult {
92            deleted,
93            was_default,
94            next_default,
95            warnings,
96        })
97    }
98
99    pub async fn delete_local_identity_async(
100        &self,
101        selector: super::IdentitySelector,
102    ) -> crate::ImResult<super::DeleteLocalIdentityResult> {
103        let paths = &self.core.inner().sdk_paths().identities;
104        let mut registry = self.load_registry_async().await?;
105        let deleted_index = registry.find_index(selector)?;
106        let deleted_entry = registry.entries.remove(deleted_index);
107        let deleted = deleted_entry.summary.clone();
108        let was_default = deleted.is_default
109            || registry.default_alias.as_deref() == deleted_entry.local_alias.as_deref();
110
111        let mut warnings = Vec::new();
112        if let Some(identity_dir_name) = deleted_entry.identity_dir_name() {
113            let identity_dir = local_identity_dir(&paths.identity_root_dir, &identity_dir_name)?;
114            match tokio::fs::remove_dir_all(&identity_dir).await {
115                Ok(()) => {}
116                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
117                    warnings.push(format!(
118                        "local identity directory was already missing: {}",
119                        identity_dir.display()
120                    ));
121                }
122                Err(err) => return Err(crate::ImError::from(err)),
123            }
124        } else {
125            warnings.push(format!(
126                "local identity {} did not include a usable directory name",
127                deleted.id.as_str()
128            ));
129        }
130
131        if was_default {
132            registry.default_alias = registry
133                .entries
134                .first()
135                .and_then(|entry| entry.local_alias.clone());
136        }
137        registry.apply_default_flags();
138        let next_default = registry.default_identity();
139        write_registry_async(&paths.registry_path, &registry).await?;
140        write_default_identity_async(
141            paths.default_identity_path.clone(),
142            registry.default_alias.clone(),
143        )
144        .await?;
145
146        Ok(super::DeleteLocalIdentityResult {
147            deleted,
148            was_default,
149            next_default,
150            warnings,
151        })
152    }
153
154    pub fn resolve(
155        &self,
156        selector: super::IdentitySelector,
157    ) -> crate::ImResult<super::IdentitySummary> {
158        let registry = self.load_registry()?;
159        self.resolve_from_snapshot(&registry, selector)
160    }
161
162    pub async fn resolve_async(
163        &self,
164        selector: super::IdentitySelector,
165    ) -> crate::ImResult<super::IdentitySummary> {
166        let registry = self.load_registry_async().await?;
167        self.resolve_from_snapshot(&registry, selector)
168    }
169
170    pub fn vault_status(
171        &self,
172        selector: super::IdentitySelector,
173    ) -> crate::ImResult<super::IdentityVaultStatus> {
174        let registry = self.load_registry()?;
175        if registry.entries.is_empty() {
176            let summary = self.resolve_from_snapshot(&registry, selector)?;
177            return Ok(self.identity_vault_status(&summary, None));
178        }
179        let entry = registry.find_entry(selector)?;
180        Ok(self.identity_vault_status(&entry.summary, Some(entry)))
181    }
182
183    pub async fn vault_status_async(
184        &self,
185        selector: super::IdentitySelector,
186    ) -> crate::ImResult<super::IdentityVaultStatus> {
187        let registry = self.load_registry_async().await?;
188        if registry.entries.is_empty() {
189            let summary = self.resolve_from_snapshot(&registry, selector)?;
190            return Ok(self.identity_vault_status(&summary, None));
191        }
192        let entry = registry.find_entry(selector)?;
193        Ok(self.identity_vault_status(&entry.summary, Some(entry)))
194    }
195
196    pub fn migrate_identity_vault(
197        &self,
198        selector: super::IdentitySelector,
199    ) -> crate::ImResult<super::IdentityVaultMigrationReport> {
200        let context = self.core.inner().identity_vault().cloned().ok_or_else(|| {
201            crate::ImError::LocalStateUnavailable {
202                detail: "identity vault migration requires identity secret vault open options"
203                    .to_owned(),
204            }
205        })?;
206        let registry = self.load_registry()?;
207        let entry = registry.find_entry(selector)?;
208        let local_alias =
209            entry
210                .local_alias
211                .clone()
212                .ok_or_else(|| crate::ImError::IdentityNotFound {
213                    selector: entry.summary.id.as_str().to_owned(),
214                })?;
215        crate::internal::identity_store::IdentityStore::new(
216            &self.core.inner().sdk_paths().identities,
217        )
218        .migrate_identity_to_vault(
219            &local_alias,
220            context.workspace_id(),
221            context.device_id(),
222            context.vault().as_ref(),
223        )?;
224        let status = self.vault_status(super::IdentitySelector::LocalAlias(local_alias))?;
225        self.verify_identity_vault_status(status, true)
226            .map(|verification| super::IdentityVaultMigrationReport {
227                plaintext_compat_retained: verification
228                    .status
229                    .plaintext_compat_retained
230                    .unwrap_or(false),
231                warnings: verification.warnings,
232                identity: verification.identity,
233                status: verification.status,
234                migrated: true,
235                verified: verification.verified,
236            })
237    }
238
239    pub async fn migrate_identity_vault_async(
240        &self,
241        selector: super::IdentitySelector,
242    ) -> crate::ImResult<super::IdentityVaultMigrationReport> {
243        let context = self.core.inner().identity_vault().cloned().ok_or_else(|| {
244            crate::ImError::LocalStateUnavailable {
245                detail: "identity vault migration requires identity secret vault open options"
246                    .to_owned(),
247            }
248        })?;
249        let registry = self.load_registry_async().await?;
250        let entry = registry.find_entry(selector)?;
251        let local_alias =
252            entry
253                .local_alias
254                .clone()
255                .ok_or_else(|| crate::ImError::IdentityNotFound {
256                    selector: entry.summary.id.as_str().to_owned(),
257                })?;
258        let paths = self.core.inner().sdk_paths().identities.clone();
259        let workspace_id = context.workspace_id().to_owned();
260        let device_id = context.device_id().to_owned();
261        let vault = context.vault();
262        let local_alias_for_migration = local_alias.clone();
263        crate::internal::runtime::worker::run_blocking(move || {
264            crate::internal::identity_store::IdentityStore::new(&paths).migrate_identity_to_vault(
265                &local_alias_for_migration,
266                &workspace_id,
267                &device_id,
268                vault.as_ref(),
269            )
270        })
271        .await
272        .map_err(|err| crate::ImError::Internal {
273            message: err.to_string(),
274        })??;
275        let status = self
276            .vault_status_async(super::IdentitySelector::LocalAlias(local_alias))
277            .await?;
278        self.verify_identity_vault_status(status, true)
279            .map(|verification| super::IdentityVaultMigrationReport {
280                plaintext_compat_retained: verification
281                    .status
282                    .plaintext_compat_retained
283                    .unwrap_or(false),
284                warnings: verification.warnings,
285                identity: verification.identity,
286                status: verification.status,
287                migrated: true,
288                verified: verification.verified,
289            })
290    }
291
292    pub fn verify_identity_vault(
293        &self,
294        selector: super::IdentitySelector,
295    ) -> crate::ImResult<super::IdentityVaultVerificationReport> {
296        let status = self.vault_status(selector)?;
297        self.verify_identity_vault_status(status, true)
298    }
299
300    pub async fn verify_identity_vault_async(
301        &self,
302        selector: super::IdentitySelector,
303    ) -> crate::ImResult<super::IdentityVaultVerificationReport> {
304        let status = self.vault_status_async(selector).await?;
305        self.verify_identity_vault_status(status, true)
306    }
307
308    pub fn load_daemon_subkey_package(
309        &self,
310        selector: super::IdentitySelector,
311    ) -> crate::ImResult<super::DaemonSubkeyPrivatePackage> {
312        let registry = self.load_registry()?;
313        let entry = registry.find_entry(selector)?;
314        let dir_name =
315            entry
316                .identity_dir_name()
317                .ok_or_else(|| crate::ImError::IdentityNotFound {
318                    selector: entry.summary.id.as_str().to_string(),
319                })?;
320        crate::internal::identity_store::IdentityStore::new(
321            &self.core.inner().sdk_paths().identities,
322        )
323        .load_daemon_subkey_package_vault_aware(
324            &dir_name,
325            &entry.summary.did,
326            entry.vault_migration.as_ref(),
327            self.core.inner().identity_vault(),
328            self.core.inner().identity_secret_storage_policy(),
329        )
330    }
331
332    pub async fn load_daemon_subkey_package_async(
333        &self,
334        selector: super::IdentitySelector,
335    ) -> crate::ImResult<super::DaemonSubkeyPrivatePackage> {
336        let registry = self.load_registry_async().await?;
337        let entry = registry.find_entry(selector)?;
338        let dir_name =
339            entry
340                .identity_dir_name()
341                .ok_or_else(|| crate::ImError::IdentityNotFound {
342                    selector: entry.summary.id.as_str().to_string(),
343                })?;
344        let paths = self.core.inner().sdk_paths().identities.clone();
345        let did = entry.summary.did.clone();
346        let metadata = entry.vault_migration.clone();
347        let context = self.core.inner().identity_vault().cloned();
348        let policy = self.core.inner().identity_secret_storage_policy();
349        crate::internal::runtime::worker::run_blocking(move || {
350            crate::internal::identity_store::IdentityStore::new(&paths)
351                .load_daemon_subkey_package_vault_aware(
352                    &dir_name,
353                    &did,
354                    metadata.as_ref(),
355                    context.as_ref(),
356                    policy,
357                )
358        })
359        .await
360        .map_err(|err| crate::ImError::Internal {
361            message: err.to_string(),
362        })?
363    }
364
365    pub fn ensure_daemon_subkey_package(
366        &self,
367        selector: super::IdentitySelector,
368    ) -> crate::ImResult<super::DaemonSubkeyPrivatePackage> {
369        let registry = self.load_registry()?;
370        let entry = registry.find_entry(selector)?;
371        let prepared = self.prepare_daemon_subkey_ensure(entry)?;
372        match prepared {
373            EnsureDaemonSubkeyPrepared::Ready { package } => Ok(package),
374            EnsureDaemonSubkeyPrepared::UpdateRequired {
375                dir_name,
376                identity_id,
377                did_document,
378                package,
379                selector,
380            } => {
381                let client = self.core.client(selector)?;
382                let call =
383                    crate::internal::identity_wire::update_document::build_update_document_rpc_call(
384                        crate::internal::identity_wire::UpdateDocumentRpcParams {
385                            did_document: did_document.clone(),
386                            is_public: None,
387                            is_agent: None,
388                            role: None,
389                            endpoint_url: None,
390                        },
391                    );
392                use crate::internal::transport::AuthenticatedRpcTransport;
393                let mut transport = crate::internal::transport::CoreHttpTransport::new(&client);
394                transport.authenticated_rpc(call.endpoint, call.method, call.params)?;
395                crate::internal::identity_store::IdentityStore::new(
396                    &self.core.inner().sdk_paths().identities,
397                )
398                .save_did_document(&dir_name, &did_document)?;
399                crate::internal::identity_store::IdentityStore::new(
400                    &self.core.inner().sdk_paths().identities,
401                )
402                .save_daemon_subkey_package_with_secret_storage(
403                    &dir_name,
404                    &identity_id,
405                    &package,
406                    crate::internal::identity_store::SaveIdentitySecretStorage::from_core(
407                        self.core,
408                    )?,
409                )?;
410                Ok(package)
411            }
412        }
413    }
414
415    pub async fn ensure_daemon_subkey_package_async(
416        &self,
417        selector: super::IdentitySelector,
418    ) -> crate::ImResult<super::DaemonSubkeyPrivatePackage> {
419        let registry = self.load_registry_async().await?;
420        let entry = registry.find_entry(selector)?;
421        let core = (*self.core).clone();
422        let entry = entry.clone();
423        let prepared = crate::internal::runtime::worker::run_blocking(move || {
424            IdentityRegistry::new(&core).prepare_daemon_subkey_ensure(&entry)
425        })
426        .await
427        .map_err(|err| crate::ImError::Internal {
428            message: err.to_string(),
429        })??;
430        match prepared {
431            EnsureDaemonSubkeyPrepared::Ready { package } => Ok(package),
432            EnsureDaemonSubkeyPrepared::UpdateRequired {
433                dir_name,
434                identity_id,
435                did_document,
436                package,
437                selector,
438            } => {
439                let client = self.core.client_async(selector).await?;
440                let call =
441                    crate::internal::identity_wire::update_document::build_update_document_rpc_call(
442                        crate::internal::identity_wire::UpdateDocumentRpcParams {
443                            did_document: did_document.clone(),
444                            is_public: None,
445                            is_agent: None,
446                            role: None,
447                            endpoint_url: None,
448                        },
449                    );
450                use crate::internal::transport::AsyncAuthenticatedRpcTransport;
451                let mut transport = crate::internal::transport::CoreHttpTransport::new(&client);
452                transport
453                    .authenticated_rpc(call.endpoint, call.method, call.params)
454                    .await?;
455                let paths = self.core.inner().sdk_paths().identities.clone();
456                let package_to_save = package.clone();
457                let secret_storage =
458                    crate::internal::identity_store::SaveIdentitySecretStorage::from_core(
459                        self.core,
460                    )?;
461                crate::internal::runtime::worker::run_blocking(move || {
462                    let store = crate::internal::identity_store::IdentityStore::new(&paths);
463                    store.save_did_document(&dir_name, &did_document)?;
464                    store.save_daemon_subkey_package_with_secret_storage(
465                        &dir_name,
466                        &identity_id,
467                        &package_to_save,
468                        secret_storage,
469                    )?;
470                    Ok::<(), crate::ImError>(())
471                })
472                .await
473                .map_err(|err| crate::ImError::Internal {
474                    message: err.to_string(),
475                })??;
476                Ok(package)
477            }
478        }
479    }
480
481    pub fn revoke_daemon_subkey_authorization(
482        &self,
483        selector: super::IdentitySelector,
484    ) -> crate::ImResult<super::DaemonSubkeyAuthorizationRevokeResult> {
485        let registry = self.load_registry()?;
486        let entry = registry.find_entry(selector)?;
487        let prepared = self.prepare_daemon_subkey_revoke(entry)?;
488        match prepared {
489            RevokeDaemonSubkeyPrepared::AlreadyRevoked {
490                did,
491                verification_method,
492            } => Ok(super::DaemonSubkeyAuthorizationRevokeResult {
493                user_did: did,
494                verification_method,
495                updated: false,
496            }),
497            RevokeDaemonSubkeyPrepared::UpdateRequired {
498                dir_name,
499                did,
500                verification_method,
501                did_document,
502                selector,
503            } => {
504                let client = self.core.client(selector)?;
505                let call =
506                    crate::internal::identity_wire::update_document::build_update_document_rpc_call(
507                        crate::internal::identity_wire::UpdateDocumentRpcParams {
508                            did_document: did_document.clone(),
509                            is_public: None,
510                            is_agent: None,
511                            role: None,
512                            endpoint_url: None,
513                        },
514                    );
515                use crate::internal::transport::AuthenticatedRpcTransport;
516                let mut transport = crate::internal::transport::CoreHttpTransport::new(&client);
517                transport.authenticated_rpc(call.endpoint, call.method, call.params)?;
518                crate::internal::identity_store::IdentityStore::new(
519                    &self.core.inner().sdk_paths().identities,
520                )
521                .save_did_document(&dir_name, &did_document)?;
522                Ok(super::DaemonSubkeyAuthorizationRevokeResult {
523                    user_did: did,
524                    verification_method,
525                    updated: true,
526                })
527            }
528        }
529    }
530
531    pub async fn revoke_daemon_subkey_authorization_async(
532        &self,
533        selector: super::IdentitySelector,
534    ) -> crate::ImResult<super::DaemonSubkeyAuthorizationRevokeResult> {
535        let registry = self.load_registry_async().await?;
536        let entry = registry.find_entry(selector)?;
537        let core = (*self.core).clone();
538        let entry = entry.clone();
539        let prepared = crate::internal::runtime::worker::run_blocking(move || {
540            IdentityRegistry::new(&core).prepare_daemon_subkey_revoke(&entry)
541        })
542        .await
543        .map_err(|err| crate::ImError::Internal {
544            message: err.to_string(),
545        })??;
546        match prepared {
547            RevokeDaemonSubkeyPrepared::AlreadyRevoked {
548                did,
549                verification_method,
550            } => Ok(super::DaemonSubkeyAuthorizationRevokeResult {
551                user_did: did,
552                verification_method,
553                updated: false,
554            }),
555            RevokeDaemonSubkeyPrepared::UpdateRequired {
556                dir_name,
557                did,
558                verification_method,
559                did_document,
560                selector,
561            } => {
562                let client = self.core.client_async(selector).await?;
563                let call =
564                    crate::internal::identity_wire::update_document::build_update_document_rpc_call(
565                        crate::internal::identity_wire::UpdateDocumentRpcParams {
566                            did_document: did_document.clone(),
567                            is_public: None,
568                            is_agent: None,
569                            role: None,
570                            endpoint_url: None,
571                        },
572                    );
573                use crate::internal::transport::AsyncAuthenticatedRpcTransport;
574                let mut transport = crate::internal::transport::CoreHttpTransport::new(&client);
575                transport
576                    .authenticated_rpc(call.endpoint, call.method, call.params)
577                    .await?;
578                let paths = self.core.inner().sdk_paths().identities.clone();
579                crate::internal::runtime::worker::run_blocking(move || {
580                    crate::internal::identity_store::IdentityStore::new(&paths)
581                        .save_did_document(&dir_name, &did_document)
582                })
583                .await
584                .map_err(|err| crate::ImError::Internal {
585                    message: err.to_string(),
586                })??;
587                Ok(super::DaemonSubkeyAuthorizationRevokeResult {
588                    user_did: did,
589                    verification_method,
590                    updated: true,
591                })
592            }
593        }
594    }
595
596    fn prepare_daemon_subkey_ensure(
597        &self,
598        entry: &RegistryEntry,
599    ) -> crate::ImResult<EnsureDaemonSubkeyPrepared> {
600        let dir_name =
601            entry
602                .identity_dir_name()
603                .ok_or_else(|| crate::ImError::IdentityNotFound {
604                    selector: entry.summary.id.as_str().to_string(),
605                })?;
606        let store = crate::internal::identity_store::IdentityStore::new(
607            &self.core.inner().sdk_paths().identities,
608        );
609        let did = entry.summary.did.clone();
610        let mut did_document = store.load_did_document(&dir_name)?;
611        let document_id = did_document
612            .get("id")
613            .and_then(Value::as_str)
614            .unwrap_or_default();
615        if document_id != did.as_str() {
616            return Err(crate::ImError::IdentityNotReady {
617                identity: did.as_str().to_string(),
618                missing: vec!["did_document_identity_mismatch".to_string()],
619            });
620        }
621        if let Some(package) = match store.load_daemon_subkey_package_vault_aware(
622            &dir_name,
623            &did,
624            entry.vault_migration.as_ref(),
625            self.core.inner().identity_vault(),
626            self.core.inner().identity_secret_storage_policy(),
627        ) {
628            Ok(package) => Some(package),
629            Err(crate::ImError::IdentityNotFound { .. })
630                if !matches!(
631                    self.core.inner().identity_secret_storage_policy(),
632                    crate::core::IdentitySecretStoragePolicy::VaultRequired
633                ) =>
634            {
635                store.load_daemon_subkey_package_or_legacy(&dir_name, &did, &did_document)?
636            }
637            Err(crate::ImError::IdentityNotFound { .. }) => None,
638            Err(err) => return Err(err),
639        } {
640            if package.user_did != did {
641                return Err(crate::ImError::IdentityNotReady {
642                    identity: did.as_str().to_string(),
643                    missing: vec!["daemon_subkey_did_mismatch".to_string()],
644                });
645            }
646            crate::internal::identity_daemon_subkey::validate_package_against_did_document(
647                &package,
648                &did_document,
649            )?;
650            return Ok(EnsureDaemonSubkeyPrepared::Ready { package });
651        }
652        if crate::internal::identity_daemon_subkey::did_document_references_daemon_subkey(
653            &did_document,
654            &did,
655        ) {
656            return Err(crate::ImError::IdentityNotReady {
657                identity: did.as_str().to_string(),
658                missing: vec!["daemon_subkey_private_missing".to_string()],
659            });
660        }
661        let key1_private_pem = self.identity_root_private_for_entry(entry, &dir_name, &did)?;
662        let subkey = crate::internal::identity_daemon_subkey::generate_for_did(&did);
663        crate::internal::identity_daemon_subkey::apply_to_did_document(
664            &mut did_document,
665            &did,
666            &subkey,
667        )?;
668        crate::internal::identity_daemon_subkey::resign_did_document_with_key1(
669            &mut did_document,
670            &did,
671            &key1_private_pem,
672        )?;
673        let package = crate::internal::identity_daemon_subkey::package_from_parts(
674            did.clone(),
675            subkey.verification_method,
676            subkey.public_key_multibase,
677            subkey.private_key_pem,
678        );
679        Ok(EnsureDaemonSubkeyPrepared::UpdateRequired {
680            dir_name,
681            identity_id: entry.summary.id.as_str().to_string(),
682            did_document,
683            package,
684            selector: super::IdentitySelector::Did(did),
685        })
686    }
687
688    fn prepare_daemon_subkey_revoke(
689        &self,
690        entry: &RegistryEntry,
691    ) -> crate::ImResult<RevokeDaemonSubkeyPrepared> {
692        let dir_name =
693            entry
694                .identity_dir_name()
695                .ok_or_else(|| crate::ImError::IdentityNotFound {
696                    selector: entry.summary.id.as_str().to_string(),
697                })?;
698        let store = crate::internal::identity_store::IdentityStore::new(
699            &self.core.inner().sdk_paths().identities,
700        );
701        let did = entry.summary.did.clone();
702        let verification_method =
703            crate::internal::identity_daemon_subkey::expected_verification_method(&did);
704        let mut did_document = store.load_did_document(&dir_name)?;
705        let document_id = did_document
706            .get("id")
707            .and_then(Value::as_str)
708            .unwrap_or_default();
709        if document_id != did.as_str() {
710            return Err(crate::ImError::IdentityNotReady {
711                identity: did.as_str().to_string(),
712                missing: vec!["did_document_identity_mismatch".to_string()],
713            });
714        }
715        if !crate::internal::identity_daemon_subkey::remove_from_did_document(
716            &mut did_document,
717            &did,
718        )? {
719            return Ok(RevokeDaemonSubkeyPrepared::AlreadyRevoked {
720                did,
721                verification_method,
722            });
723        }
724        let key1_private_pem = self.identity_root_private_for_entry(entry, &dir_name, &did)?;
725        crate::internal::identity_daemon_subkey::resign_did_document_with_key1(
726            &mut did_document,
727            &did,
728            &key1_private_pem,
729        )?;
730        Ok(RevokeDaemonSubkeyPrepared::UpdateRequired {
731            dir_name,
732            did: did.clone(),
733            verification_method,
734            did_document,
735            selector: super::IdentitySelector::Did(did),
736        })
737    }
738
739    fn identity_root_private_for_entry(
740        &self,
741        entry: &RegistryEntry,
742        dir_name: &str,
743        did: &crate::ids::Did,
744    ) -> crate::ImResult<String> {
745        let store = crate::internal::identity_store::IdentityStore::new(
746            &self.core.inner().sdk_paths().identities,
747        );
748        let policy = self.core.inner().identity_secret_storage_policy();
749        if let Some(metadata) = entry.vault_migration.as_ref() {
750            if vault_metadata_is_verified(metadata) {
751                if let Some(context) = self.core.inner().identity_vault() {
752                    if vault_context_matches_metadata(context, metadata) {
753                        return store.load_key1_private_pem_from_vault(
754                            dir_name,
755                            did,
756                            metadata,
757                            context.workspace_id(),
758                            context.device_id(),
759                            context.vault().as_ref(),
760                        );
761                    }
762                    if matches!(
763                        policy,
764                        crate::core::IdentitySecretStoragePolicy::VaultRequired
765                    ) || !metadata.plaintext_compat_retained
766                    {
767                        return Err(crate::ImError::IdentityNotReady {
768                            identity: did.as_str().to_string(),
769                            missing: vec!["identity_vault_context_mismatch".to_string()],
770                        });
771                    }
772                } else if matches!(
773                    policy,
774                    crate::core::IdentitySecretStoragePolicy::VaultRequired
775                ) || !metadata.plaintext_compat_retained
776                {
777                    return Err(crate::ImError::LocalStateUnavailable {
778                        detail:
779                            "identity root private key is stored in vault but no identity secret vault was provided"
780                                .to_owned(),
781                    });
782                }
783            } else if matches!(
784                policy,
785                crate::core::IdentitySecretStoragePolicy::VaultRequired
786            ) {
787                return Err(crate::ImError::IdentityNotReady {
788                    identity: did.as_str().to_string(),
789                    missing: vec!["identity_vault_metadata_verified".to_string()],
790                });
791            }
792        } else if matches!(
793            policy,
794            crate::core::IdentitySecretStoragePolicy::VaultRequired
795        ) {
796            return Err(crate::ImError::IdentityNotReady {
797                identity: did.as_str().to_string(),
798                missing: vec!["identity_vault_metadata".to_string()],
799            });
800        }
801        store
802            .load_key1_private_pem(dir_name)
803            .map_err(|err| match err {
804                crate::ImError::CredentialFileUnreadable { .. } => {
805                    crate::ImError::IdentityNotReady {
806                        identity: did.as_str().to_string(),
807                        missing: vec!["key1_private".to_string()],
808                    }
809                }
810                other => other,
811            })
812    }
813
814    fn resolve_from_snapshot(
815        &self,
816        registry: &RegistrySnapshot,
817        selector: super::IdentitySelector,
818    ) -> crate::ImResult<super::IdentitySummary> {
819        match selector {
820            super::IdentitySelector::Default => registry
821                .default_identity()
822                .ok_or(crate::ImError::DefaultIdentityMissing),
823            super::IdentitySelector::LocalAlias(alias) => {
824                let alias = alias.trim();
825                if alias.is_empty() {
826                    return Err(crate::ImError::invalid_input(
827                        Some("identity".to_string()),
828                        "local alias must not be empty",
829                    ));
830                }
831                registry
832                    .find(|entry| entry.local_alias.as_deref() == Some(alias))
833                    .map(|entry| entry.summary.clone())
834                    .map_or_else(
835                        || {
836                            if registry.entries.is_empty() {
837                                self.summary_for_local_alias(alias.to_string())
838                            } else {
839                                Err(crate::ImError::IdentityNotFound {
840                                    selector: alias.to_string(),
841                                })
842                            }
843                        },
844                        Ok,
845                    )
846            }
847            super::IdentitySelector::Did(did) => registry
848                .find(|entry| entry.summary.did == did)
849                .map(|entry| entry.summary.clone())
850                .map_or_else(
851                    || {
852                        if registry.entries.is_empty() {
853                            self.summary_for_did(did)
854                        } else {
855                            Err(crate::ImError::IdentityNotFound {
856                                selector: did.as_str().to_string(),
857                            })
858                        }
859                    },
860                    Ok,
861                ),
862            super::IdentitySelector::Id(id) => registry
863                .find(|entry| entry.summary.id == id)
864                .map(|entry| entry.summary.clone())
865                .ok_or_else(|| crate::ImError::IdentityNotFound {
866                    selector: id.as_str().to_string(),
867                }),
868            super::IdentitySelector::Handle(handle) => registry
869                .find(|entry| entry.summary.handle.as_ref() == Some(&handle))
870                .map(|entry| entry.summary.clone())
871                .ok_or_else(|| crate::ImError::IdentityNotFound {
872                    selector: handle.as_str().to_string(),
873                }),
874        }
875    }
876
877    fn summary_for_local_alias(&self, alias: String) -> crate::ImResult<super::IdentitySummary> {
878        let alias = alias.trim();
879        if alias.is_empty() {
880            return Err(crate::ImError::invalid_input(
881                Some("identity".to_string()),
882                "local alias must not be empty",
883            ));
884        }
885        let did = crate::ids::Did::parse(format!(
886            "did:awiki:{}",
887            alias
888                .chars()
889                .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '-' })
890                .collect::<String>()
891        ))?;
892        Ok(super::IdentitySummary {
893            id: crate::ids::IdentityId::parse(alias)?,
894            did,
895            handle: None,
896            display_name: None,
897            local_alias: Some(alias.to_string()),
898            device_id: None,
899            is_default: false,
900            readiness: super::IdentityReadiness {
901                ready_for_auth: false,
902                ready_for_messaging: false,
903                missing: vec![
904                    super::IdentityMissingItem::DidDocument,
905                    super::IdentityMissingItem::PrivateKey,
906                    super::IdentityMissingItem::AuthState,
907                ],
908            },
909        })
910    }
911
912    fn summary_for_did(&self, did: crate::ids::Did) -> crate::ImResult<super::IdentitySummary> {
913        let id = did.as_str().replace(':', "-");
914        Ok(super::IdentitySummary {
915            id: crate::ids::IdentityId::parse(id)?,
916            did,
917            handle: None,
918            display_name: None,
919            local_alias: None,
920            device_id: None,
921            is_default: false,
922            readiness: super::IdentityReadiness {
923                ready_for_auth: false,
924                ready_for_messaging: false,
925                missing: vec![
926                    super::IdentityMissingItem::DidDocument,
927                    super::IdentityMissingItem::PrivateKey,
928                    super::IdentityMissingItem::AuthState,
929                ],
930            },
931        })
932    }
933
934    pub fn register_handle(
935        &self,
936        request: super::RegisterHandleRequest,
937    ) -> crate::ImResult<super::HandleRegistrationResult> {
938        crate::internal::identity_registration_runtime::IdentityRegistrationRuntime::new(
939            self.core,
940            crate::internal::transport::CorePlainTransport::new(self.core),
941        )
942        .register_handle(request)
943        .map(|result| result.sdk_result)
944    }
945
946    pub async fn register_handle_async(
947        &self,
948        request: super::RegisterHandleRequest,
949    ) -> crate::ImResult<super::HandleRegistrationResult> {
950        crate::internal::identity_registration_runtime::IdentityRegistrationRuntime::new(
951            self.core,
952            crate::internal::transport::CorePlainTransport::new(self.core),
953        )
954        .register_handle_async(request)
955        .await
956        .map(|result| result.sdk_result)
957    }
958
959    #[cfg(feature = "mcp-trusted-registration")]
960    pub async fn register_handle_with_service_bearer_async(
961        &self,
962        request: super::RegisterHandleRequest,
963        bearer_token: impl Into<String>,
964    ) -> crate::ImResult<super::HandleRegistrationResult> {
965        crate::internal::identity_registration_runtime::IdentityRegistrationRuntime::new(
966            self.core,
967            crate::internal::transport::CorePlainTransport::new_with_register_bearer_token(
968                self.core,
969                bearer_token,
970            ),
971        )
972        .register_handle_async(request)
973        .await
974        .map(|result| result.sdk_result)
975    }
976}
977
978impl IdentityRegistry<'_> {
979    pub fn recover_handle(
980        &self,
981        request: super::RecoverHandleRequest,
982    ) -> crate::ImResult<super::RecoverHandleResult> {
983        let prepared = crate::internal::identity_recovery_runtime::prepare_recover_handle_request(
984            self.core, request,
985        )?;
986        crate::internal::identity_recovery_runtime::IdentityRecoveryRuntime::new_with_core(
987            self.core,
988            crate::internal::transport::CorePlainTransport::new(self.core),
989        )
990        .recover_handle(prepared.request)
991        .and_then(|result| {
992            crate::internal::identity_recovery_runtime::finalize_recover_handle_result(
993                self.core,
994                prepared.local_store,
995                result,
996            )
997        })
998    }
999
1000    pub async fn recover_handle_async(
1001        &self,
1002        request: super::RecoverHandleRequest,
1003    ) -> crate::ImResult<super::RecoverHandleResult> {
1004        let prepared = crate::internal::identity_recovery_runtime::prepare_recover_handle_request(
1005            self.core, request,
1006        )?;
1007        crate::internal::identity_recovery_runtime::IdentityRecoveryRuntime::new_with_core(
1008            self.core,
1009            crate::internal::transport::CorePlainTransport::new(self.core),
1010        )
1011        .recover_handle_async(prepared.request)
1012        .await
1013        .and_then(|result| {
1014            crate::internal::identity_recovery_runtime::finalize_recover_handle_result(
1015                self.core,
1016                prepared.local_store,
1017                result,
1018            )
1019        })
1020    }
1021
1022    pub fn recover_handle_plan(
1023        &self,
1024        request: super::RecoverHandlePlanRequest,
1025    ) -> crate::ImResult<super::RecoverHandlePlan> {
1026        let phone = crate::internal::identity_wire::normalize_phone(&request.phone)?;
1027        let plan = crate::internal::identity_recovery_local::plan_recover_handle(
1028            &self.core.inner().sdk_paths().identities,
1029            &request.handle,
1030            request.raw_handle.as_deref(),
1031            &self.core.inner().sdk_config().did_domain,
1032        )?;
1033        Ok(plan.public_plan(&phone, request.otp.as_deref()))
1034    }
1035
1036    pub async fn recover_handle_plan_async(
1037        &self,
1038        request: super::RecoverHandlePlanRequest,
1039    ) -> crate::ImResult<super::RecoverHandlePlan> {
1040        self.recover_handle_plan(request)
1041    }
1042
1043    pub fn plan_default_identity_change(
1044        &self,
1045        selector: super::IdentitySelector,
1046    ) -> crate::ImResult<super::DefaultIdentityChange> {
1047        let previous = self.default_identity()?;
1048        let next = self.resolve(selector)?;
1049        Ok(super::DefaultIdentityChange {
1050            previous,
1051            next,
1052            requires_default_identity_write: true,
1053            warnings: Vec::new(),
1054        })
1055    }
1056
1057    pub async fn plan_default_identity_change_async(
1058        &self,
1059        selector: super::IdentitySelector,
1060    ) -> crate::ImResult<super::DefaultIdentityChange> {
1061        let previous = self.default_identity_async().await?;
1062        let next = self.resolve_async(selector).await?;
1063        Ok(super::DefaultIdentityChange {
1064            previous,
1065            next,
1066            requires_default_identity_write: true,
1067            warnings: Vec::new(),
1068        })
1069    }
1070
1071    pub(crate) fn load_runtime(
1072        &self,
1073        selector: super::IdentitySelector,
1074    ) -> crate::ImResult<crate::internal::identity_runtime::ClientIdentityRuntime> {
1075        let registry = self.load_registry()?;
1076        let (summary, entry) = if registry.entries.is_empty() {
1077            (self.resolve_from_snapshot(&registry, selector)?, None)
1078        } else {
1079            let entry = registry.find_entry(selector)?;
1080            (entry.summary.clone(), Some(entry))
1081        };
1082        let identity_root = &self.core.inner().sdk_paths().identities.identity_root_dir;
1083        let identity_dir_name = entry
1084            .and_then(|entry| entry.dir_name.as_deref())
1085            .or(summary.local_alias.as_deref())
1086            .unwrap_or_else(|| summary.id.as_str());
1087        let identity_dir = identity_root.join(identity_dir_name);
1088        let key_provider = self.key_provider_for_entry(identity_dir.clone(), entry, &summary)?;
1089        Ok(crate::internal::identity_runtime::ClientIdentityRuntime {
1090            summary: summary.clone(),
1091            did_document_path: first_existing_path(
1092                &identity_dir,
1093                &["did.json", "did_document.json"],
1094            ),
1095            private_key_path: first_existing_path(
1096                &identity_dir,
1097                &["private.key", "key-1-private.pem"],
1098            ),
1099            e2ee_agreement_private_key_path: first_existing_path(
1100                &identity_dir,
1101                &["e2ee-agreement-private.pem", "key-3-private.pem"],
1102            ),
1103            auth_state_path: identity_dir.join("auth.json"),
1104            key_provider,
1105            owner: crate::internal::identity_runtime::LocalOwnerContext {
1106                identity_id: summary.id,
1107                current_did: summary.did,
1108            },
1109        })
1110    }
1111
1112    pub(crate) async fn load_runtime_async(
1113        &self,
1114        selector: super::IdentitySelector,
1115    ) -> crate::ImResult<crate::internal::identity_runtime::ClientIdentityRuntime> {
1116        let registry = self.load_registry_async().await?;
1117        let (summary, entry) = if registry.entries.is_empty() {
1118            (self.resolve_from_snapshot(&registry, selector)?, None)
1119        } else {
1120            let entry = registry.find_entry(selector)?;
1121            (entry.summary.clone(), Some(entry))
1122        };
1123        let identity_root = &self.core.inner().sdk_paths().identities.identity_root_dir;
1124        let identity_dir_name = entry
1125            .and_then(|entry| entry.dir_name.as_deref())
1126            .or(summary.local_alias.as_deref())
1127            .unwrap_or_else(|| summary.id.as_str());
1128        let identity_dir = identity_root.join(identity_dir_name);
1129        let key_provider = self.key_provider_for_entry(identity_dir.clone(), entry, &summary)?;
1130        Ok(crate::internal::identity_runtime::ClientIdentityRuntime {
1131            summary: summary.clone(),
1132            did_document_path: first_existing_path_async(
1133                &identity_dir,
1134                &["did.json", "did_document.json"],
1135            )
1136            .await,
1137            private_key_path: first_existing_path_async(
1138                &identity_dir,
1139                &["private.key", "key-1-private.pem"],
1140            )
1141            .await,
1142            e2ee_agreement_private_key_path: first_existing_path_async(
1143                &identity_dir,
1144                &["e2ee-agreement-private.pem", "key-3-private.pem"],
1145            )
1146            .await,
1147            auth_state_path: identity_dir.join("auth.json"),
1148            key_provider,
1149            owner: crate::internal::identity_runtime::LocalOwnerContext {
1150                identity_id: summary.id,
1151                current_did: summary.did,
1152            },
1153        })
1154    }
1155
1156    fn load_registry(&self) -> crate::ImResult<RegistrySnapshot> {
1157        let paths = &self.core.inner().sdk_paths().identities;
1158        let mut snapshot = match fs::read(&paths.registry_path) {
1159            Ok(raw) => parse_registry(&raw)?,
1160            Err(err) if err.kind() == std::io::ErrorKind::NotFound => RegistrySnapshot {
1161                default_alias: default_alias_from_file(paths.default_identity_path.as_deref())?,
1162                entries: Vec::new(),
1163            },
1164            Err(err) => {
1165                return Err(crate::ImError::CredentialFileUnreadable {
1166                    path_kind: "identity_registry".to_string(),
1167                    detail: err.to_string(),
1168                });
1169            }
1170        };
1171        if let Some(default_alias) =
1172            default_alias_from_file(paths.default_identity_path.as_deref())?
1173        {
1174            snapshot.default_alias = Some(default_alias);
1175        }
1176        snapshot.apply_default_flags();
1177        snapshot.validate()?;
1178        Ok(snapshot)
1179    }
1180
1181    async fn load_registry_async(&self) -> crate::ImResult<RegistrySnapshot> {
1182        let paths = &self.core.inner().sdk_paths().identities;
1183        let mut snapshot = match tokio::fs::read(&paths.registry_path).await {
1184            Ok(raw) => parse_registry(&raw)?,
1185            Err(err) if err.kind() == std::io::ErrorKind::NotFound => RegistrySnapshot {
1186                default_alias: default_alias_from_file_async(paths.default_identity_path.clone())
1187                    .await?,
1188                entries: Vec::new(),
1189            },
1190            Err(err) => {
1191                return Err(crate::ImError::CredentialFileUnreadable {
1192                    path_kind: "identity_registry".to_string(),
1193                    detail: err.to_string(),
1194                });
1195            }
1196        };
1197        if let Some(default_alias) =
1198            default_alias_from_file_async(paths.default_identity_path.clone()).await?
1199        {
1200            snapshot.default_alias = Some(default_alias);
1201        }
1202        snapshot.apply_default_flags();
1203        snapshot.validate()?;
1204        Ok(snapshot)
1205    }
1206
1207    fn key_provider_for_entry(
1208        &self,
1209        identity_dir: PathBuf,
1210        entry: Option<&RegistryEntry>,
1211        summary: &super::IdentitySummary,
1212    ) -> crate::ImResult<Arc<dyn crate::internal::key_provider::KeyMaterialProvider>> {
1213        let policy = self.core.inner().identity_secret_storage_policy();
1214        let metadata = entry.and_then(|entry| entry.vault_migration.as_ref());
1215        if let Some(metadata) = metadata {
1216            if vault_metadata_is_verified(metadata) {
1217                if let Some(context) = self.core.inner().identity_vault() {
1218                    if vault_context_matches_metadata(context, metadata) {
1219                        return Ok(Arc::new(
1220                            crate::internal::key_provider::vault::VaultBackedKeyMaterialProvider::new(
1221                                identity_dir,
1222                                context.vault(),
1223                                metadata.key_material_refs(),
1224                            ),
1225                        ));
1226                    }
1227                    if matches!(
1228                        policy,
1229                        crate::core::IdentitySecretStoragePolicy::VaultRequired
1230                    ) {
1231                        return Err(crate::ImError::IdentityNotReady {
1232                            identity: summary.did.as_str().to_owned(),
1233                            missing: vec!["identity_vault_context_mismatch".to_owned()],
1234                        });
1235                    }
1236                } else if matches!(
1237                    policy,
1238                    crate::core::IdentitySecretStoragePolicy::VaultRequired
1239                ) {
1240                    return Err(crate::ImError::LocalStateUnavailable {
1241                        detail:
1242                            "identity has verified vault metadata but no identity secret vault was provided"
1243                                .to_owned(),
1244                    });
1245                }
1246            } else if matches!(
1247                policy,
1248                crate::core::IdentitySecretStoragePolicy::VaultRequired
1249            ) {
1250                return Err(crate::ImError::IdentityNotReady {
1251                    identity: summary.did.as_str().to_owned(),
1252                    missing: vec!["identity_vault_metadata_verified".to_owned()],
1253                });
1254            }
1255        } else if matches!(
1256            policy,
1257            crate::core::IdentitySecretStoragePolicy::VaultRequired
1258        ) {
1259            return Err(crate::ImError::IdentityNotReady {
1260                identity: summary.did.as_str().to_owned(),
1261                missing: vec!["identity_vault_metadata".to_owned()],
1262            });
1263        }
1264        Ok(Arc::new(
1265            crate::internal::key_provider::FileBackedKeyMaterialProvider::new(identity_dir),
1266        ))
1267    }
1268
1269    fn verify_identity_vault_status(
1270        &self,
1271        status: super::IdentityVaultStatus,
1272        require_vault_backend: bool,
1273    ) -> crate::ImResult<super::IdentityVaultVerificationReport> {
1274        if require_vault_backend
1275            && status.selected_backend != super::IdentitySecretStorageBackend::Vault
1276        {
1277            return Err(crate::ImError::IdentityNotReady {
1278                identity: status.identity.did.as_str().to_owned(),
1279                missing: if status.missing.is_empty() {
1280                    vec!["identity_vault_backend".to_owned()]
1281                } else {
1282                    status.missing.clone()
1283                },
1284            });
1285        }
1286        let runtime = self.load_runtime(super::IdentitySelector::Id(status.identity.id.clone()))?;
1287        let _ = runtime.key_provider.optional_did_document()?;
1288        let _ = runtime.key_provider.default_signing_private_pem()?;
1289        let _ = runtime.key_provider.e2ee_agreement_private_pem()?;
1290        let _ = runtime.key_provider.auth_state()?;
1291        let mut warnings = status.warnings.clone();
1292        if status.plaintext_compat_retained.unwrap_or(false) {
1293            warnings.push("identity plaintext compatibility files are still retained".to_owned());
1294        }
1295        Ok(super::IdentityVaultVerificationReport {
1296            identity: status.identity.clone(),
1297            status,
1298            verified: true,
1299            warnings,
1300        })
1301    }
1302
1303    fn identity_vault_status(
1304        &self,
1305        summary: &super::IdentitySummary,
1306        entry: Option<&RegistryEntry>,
1307    ) -> super::IdentityVaultStatus {
1308        let policy = self.core.inner().identity_secret_storage_policy();
1309        let context = self.core.inner().identity_vault();
1310        let metadata = entry.and_then(|entry| entry.vault_migration.as_ref());
1311        let vault_metadata_present = metadata.is_some();
1312        let vault_metadata_verified = metadata.map(vault_metadata_is_verified).unwrap_or(false);
1313        let vault_context_matches = metadata
1314            .zip(context)
1315            .map(|(metadata, context)| vault_context_matches_metadata(context, metadata))
1316            .unwrap_or(false);
1317        let selected_backend =
1318            if vault_metadata_verified && context.is_some() && vault_context_matches {
1319                super::IdentitySecretStorageBackend::Vault
1320            } else {
1321                super::IdentitySecretStorageBackend::FileCompat
1322            };
1323        let mut missing = Vec::new();
1324        let mut warnings = Vec::new();
1325        if !vault_metadata_present {
1326            missing.push("identity_vault_metadata".to_owned());
1327        }
1328        if vault_metadata_present && !vault_metadata_verified {
1329            missing.push("identity_vault_metadata_verified".to_owned());
1330        }
1331        if !context.is_some() {
1332            missing.push("identity_secret_vault".to_owned());
1333        }
1334        if vault_metadata_present && context.is_some() && !vault_context_matches {
1335            missing.push("identity_vault_context_match".to_owned());
1336            warnings.push(
1337                "identity vault metadata workspace/device does not match provided vault context"
1338                    .to_owned(),
1339            );
1340        }
1341        if metadata
1342            .map(|metadata| metadata.plaintext_compat_retained)
1343            .unwrap_or(false)
1344        {
1345            warnings.push("identity plaintext compatibility files are still retained".to_owned());
1346        }
1347        if matches!(policy, crate::core::IdentitySecretStoragePolicy::FileCompat) {
1348            warnings.push("identity secret storage policy is file_compat".to_owned());
1349        }
1350        super::IdentityVaultStatus {
1351            identity: summary.clone(),
1352            storage_policy: policy,
1353            selected_backend,
1354            vault_available: context.is_some(),
1355            vault_metadata_present,
1356            vault_metadata_verified,
1357            workspace_id: metadata.map(|metadata| metadata.workspace_id.clone()),
1358            device_id: metadata.map(|metadata| metadata.device_id.clone()),
1359            plaintext_compat_retained: metadata.map(|metadata| metadata.plaintext_compat_retained),
1360            missing,
1361            warnings,
1362        }
1363    }
1364}
1365
1366fn first_existing_path(identity_dir: &Path, names: &[&str]) -> std::path::PathBuf {
1367    names
1368        .iter()
1369        .map(|name| identity_dir.join(name))
1370        .find(|path| path.exists())
1371        .unwrap_or_else(|| identity_dir.join(names[0]))
1372}
1373
1374async fn first_existing_path_async(identity_dir: &Path, names: &[&str]) -> std::path::PathBuf {
1375    for name in names {
1376        let path = identity_dir.join(name);
1377        if tokio::fs::try_exists(&path).await.unwrap_or(false) {
1378            return path;
1379        }
1380    }
1381    identity_dir.join(names[0])
1382}
1383
1384#[derive(Debug, Clone)]
1385struct RegistrySnapshot {
1386    default_alias: Option<String>,
1387    entries: Vec<RegistryEntry>,
1388}
1389
1390impl RegistrySnapshot {
1391    fn default_identity(&self) -> Option<super::IdentitySummary> {
1392        self.find(|entry| entry.summary.is_default)
1393            .map(|entry| entry.summary.clone())
1394            .or_else(|| {
1395                self.default_alias.as_deref().and_then(|alias| {
1396                    self.find(|entry| entry.local_alias.as_deref() == Some(alias))
1397                        .map(|entry| entry.summary.clone())
1398                })
1399            })
1400    }
1401
1402    fn find(&self, predicate: impl Fn(&RegistryEntry) -> bool) -> Option<&RegistryEntry> {
1403        self.entries.iter().find(|entry| predicate(entry))
1404    }
1405
1406    fn find_index(&self, selector: super::IdentitySelector) -> crate::ImResult<usize> {
1407        match selector {
1408            super::IdentitySelector::Default => {
1409                let default = self
1410                    .default_identity()
1411                    .ok_or(crate::ImError::DefaultIdentityMissing)?;
1412                self.entries
1413                    .iter()
1414                    .position(|entry| entry.summary == default)
1415                    .ok_or_else(|| crate::ImError::IdentityNotFound {
1416                        selector: "default".to_string(),
1417                    })
1418            }
1419            super::IdentitySelector::LocalAlias(alias) => {
1420                let alias = alias.trim();
1421                if alias.is_empty() {
1422                    return Err(crate::ImError::invalid_input(
1423                        Some("identity".to_string()),
1424                        "local alias must not be empty",
1425                    ));
1426                }
1427                self.entries
1428                    .iter()
1429                    .position(|entry| entry.local_alias.as_deref() == Some(alias))
1430                    .ok_or_else(|| crate::ImError::IdentityNotFound {
1431                        selector: alias.to_string(),
1432                    })
1433            }
1434            super::IdentitySelector::Did(did) => self
1435                .entries
1436                .iter()
1437                .position(|entry| entry.summary.did == did)
1438                .ok_or_else(|| crate::ImError::IdentityNotFound {
1439                    selector: did.as_str().to_string(),
1440                }),
1441            super::IdentitySelector::Id(id) => self
1442                .entries
1443                .iter()
1444                .position(|entry| entry.summary.id == id)
1445                .ok_or_else(|| crate::ImError::IdentityNotFound {
1446                    selector: id.as_str().to_string(),
1447                }),
1448            super::IdentitySelector::Handle(handle) => self
1449                .entries
1450                .iter()
1451                .position(|entry| entry.summary.handle.as_ref() == Some(&handle))
1452                .ok_or_else(|| crate::ImError::IdentityNotFound {
1453                    selector: handle.as_str().to_string(),
1454                }),
1455        }
1456    }
1457
1458    fn find_entry(&self, selector: super::IdentitySelector) -> crate::ImResult<&RegistryEntry> {
1459        let index = self.find_index(selector)?;
1460        self.entries
1461            .get(index)
1462            .ok_or_else(|| crate::ImError::IdentityNotFound {
1463                selector: index.to_string(),
1464            })
1465    }
1466
1467    fn apply_default_flags(&mut self) {
1468        let default_alias = self.default_alias.clone();
1469        for entry in &mut self.entries {
1470            if let Some(alias) = default_alias.as_deref() {
1471                entry.summary.is_default = entry.local_alias.as_deref() == Some(alias);
1472            }
1473        }
1474    }
1475
1476    fn validate(&self) -> crate::ImResult<()> {
1477        let mut identity_ids = BTreeSet::new();
1478        let mut live_dids = BTreeSet::new();
1479        let mut aliases = BTreeSet::new();
1480        let mut handles = BTreeSet::new();
1481        let mut default_count = 0usize;
1482
1483        for entry in &self.entries {
1484            validate_unique_registry_value(
1485                &mut identity_ids,
1486                "identity_id",
1487                entry.summary.id.as_str(),
1488            )?;
1489            validate_unique_registry_value(&mut live_dids, "live DID", entry.summary.did.as_str())?;
1490            if let Some(alias) = entry.local_alias.as_deref() {
1491                validate_unique_registry_value(&mut aliases, "local alias", alias)?;
1492            }
1493            if let Some(handle) = entry.summary.handle.as_ref() {
1494                validate_unique_registry_value(&mut handles, "handle", handle.as_str())?;
1495            }
1496            if entry.summary.is_default {
1497                default_count += 1;
1498            }
1499        }
1500
1501        if default_count > 1 {
1502            return Err(registry_invariant_error(
1503                "registry must not contain more than one default identity",
1504            ));
1505        }
1506
1507        if let Some(default_alias) = self.default_alias.as_deref() {
1508            let default_alias = default_alias.trim();
1509            if !default_alias.is_empty()
1510                && !self.entries.is_empty()
1511                && !self.entries.iter().any(|entry| {
1512                    entry
1513                        .local_alias
1514                        .as_deref()
1515                        .map(str::trim)
1516                        .is_some_and(|alias| alias == default_alias)
1517                })
1518            {
1519                return Err(registry_invariant_error(format!(
1520                    "default identity alias `{default_alias}` does not match any registry identity"
1521                )));
1522            }
1523        }
1524
1525        Ok(())
1526    }
1527}
1528
1529fn resolve_from_registry(
1530    registry: &RegistrySnapshot,
1531    selector: super::IdentitySelector,
1532) -> crate::ImResult<super::IdentitySummary> {
1533    match selector {
1534        super::IdentitySelector::Default => registry
1535            .default_identity()
1536            .ok_or(crate::ImError::DefaultIdentityMissing),
1537        super::IdentitySelector::LocalAlias(alias) => {
1538            let alias = alias.trim();
1539            if alias.is_empty() {
1540                return Err(crate::ImError::invalid_input(
1541                    Some("identity".to_string()),
1542                    "local alias must not be empty",
1543                ));
1544            }
1545            registry
1546                .find(|entry| entry.local_alias.as_deref() == Some(alias))
1547                .map(|entry| entry.summary.clone())
1548                .ok_or_else(|| crate::ImError::IdentityNotFound {
1549                    selector: alias.to_string(),
1550                })
1551        }
1552        super::IdentitySelector::Did(did) => registry
1553            .find(|entry| entry.summary.did == did)
1554            .map(|entry| entry.summary.clone())
1555            .ok_or_else(|| crate::ImError::IdentityNotFound {
1556                selector: did.as_str().to_string(),
1557            }),
1558        super::IdentitySelector::Id(id) => registry
1559            .find(|entry| entry.summary.id == id)
1560            .map(|entry| entry.summary.clone())
1561            .ok_or_else(|| crate::ImError::IdentityNotFound {
1562                selector: id.as_str().to_string(),
1563            }),
1564        super::IdentitySelector::Handle(handle) => registry
1565            .find(|entry| entry.summary.handle.as_ref() == Some(&handle))
1566            .map(|entry| entry.summary.clone())
1567            .ok_or_else(|| crate::ImError::IdentityNotFound {
1568                selector: handle.as_str().to_string(),
1569            }),
1570    }
1571}
1572
1573#[derive(Debug, Clone)]
1574struct RegistryEntry {
1575    local_alias: Option<String>,
1576    dir_name: Option<String>,
1577    summary: super::IdentitySummary,
1578    vault_migration: Option<crate::internal::identity_store::IdentityVaultMigrationMetadata>,
1579}
1580
1581#[derive(Debug, Clone)]
1582enum EnsureDaemonSubkeyPrepared {
1583    Ready {
1584        package: super::DaemonSubkeyPrivatePackage,
1585    },
1586    UpdateRequired {
1587        dir_name: String,
1588        identity_id: String,
1589        did_document: Value,
1590        package: super::DaemonSubkeyPrivatePackage,
1591        selector: super::IdentitySelector,
1592    },
1593}
1594
1595#[derive(Debug, Clone)]
1596enum RevokeDaemonSubkeyPrepared {
1597    AlreadyRevoked {
1598        did: crate::ids::Did,
1599        verification_method: String,
1600    },
1601    UpdateRequired {
1602        dir_name: String,
1603        did: crate::ids::Did,
1604        verification_method: String,
1605        did_document: Value,
1606        selector: super::IdentitySelector,
1607    },
1608}
1609
1610impl RegistryEntry {
1611    fn identity_dir_name(&self) -> Option<String> {
1612        self.dir_name
1613            .as_deref()
1614            .or(self.local_alias.as_deref())
1615            .or_else(|| Some(self.summary.id.as_str()))
1616            .map(str::trim)
1617            .filter(|value| !value.is_empty())
1618            .map(str::to_string)
1619    }
1620}
1621
1622#[derive(Debug, Deserialize, Serialize)]
1623struct SdkRegistryFile {
1624    #[serde(default)]
1625    #[serde(skip_serializing_if = "Option::is_none")]
1626    default_identity: Option<String>,
1627    #[serde(default)]
1628    identities: Vec<SdkIdentityRecord>,
1629}
1630
1631#[derive(Debug, Deserialize, Serialize)]
1632struct SdkIdentityRecord {
1633    id: String,
1634    did: String,
1635    #[serde(default)]
1636    #[serde(skip_serializing_if = "Option::is_none")]
1637    dir_name: Option<String>,
1638    #[serde(default)]
1639    #[serde(skip_serializing_if = "Option::is_none")]
1640    handle: Option<String>,
1641    #[serde(default)]
1642    #[serde(skip_serializing_if = "Option::is_none")]
1643    display_name: Option<String>,
1644    #[serde(default)]
1645    #[serde(skip_serializing_if = "Option::is_none")]
1646    local_alias: Option<String>,
1647    #[serde(default)]
1648    #[serde(skip_serializing_if = "Option::is_none")]
1649    device_id: Option<String>,
1650    #[serde(default)]
1651    is_default: bool,
1652    #[serde(default)]
1653    ready_for_auth: bool,
1654    #[serde(default)]
1655    ready_for_messaging: bool,
1656    #[serde(default)]
1657    missing: Vec<String>,
1658    #[serde(default)]
1659    #[serde(skip_serializing_if = "Option::is_none")]
1660    vault_migration: Option<crate::internal::identity_store::IdentityVaultMigrationMetadata>,
1661}
1662
1663#[derive(Debug, Deserialize)]
1664struct LegacyRegistryFile {
1665    #[serde(default)]
1666    default_credential_name: String,
1667    #[serde(default)]
1668    credentials: BTreeMap<String, LegacyIdentityRecord>,
1669}
1670
1671#[derive(Debug, Deserialize)]
1672struct LegacyIdentityRecord {
1673    #[serde(default)]
1674    credential_name: String,
1675    #[serde(default)]
1676    dir_name: String,
1677    #[serde(default)]
1678    did: String,
1679    #[serde(default)]
1680    unique_id: String,
1681    #[serde(default, deserialize_with = "deserialize_string_lossy")]
1682    user_id: String,
1683    #[serde(default)]
1684    name: String,
1685    #[serde(default)]
1686    handle: String,
1687    #[serde(default)]
1688    full_handle: String,
1689    #[serde(default)]
1690    is_default: bool,
1691    #[serde(default)]
1692    vault_migration: Option<crate::internal::identity_store::IdentityVaultMigrationMetadata>,
1693}
1694
1695fn parse_registry(raw: &[u8]) -> crate::ImResult<RegistrySnapshot> {
1696    if let Ok(value) = serde_json::from_slice::<serde_json::Value>(raw) {
1697        if value.as_object().is_some_and(|object| {
1698            object.contains_key("identities") || object.contains_key("default_identity")
1699        }) {
1700            let file: SdkRegistryFile =
1701                serde_json::from_value(value).map_err(|err| crate::ImError::Serialization {
1702                    detail: err.to_string(),
1703                })?;
1704            return sdk_registry_snapshot(file);
1705        }
1706    }
1707    let file: LegacyRegistryFile =
1708        serde_json::from_slice(raw).map_err(|err| crate::ImError::Serialization {
1709            detail: err.to_string(),
1710        })?;
1711    legacy_registry_snapshot(file)
1712}
1713
1714fn sdk_registry_snapshot(file: SdkRegistryFile) -> crate::ImResult<RegistrySnapshot> {
1715    let mut entries = Vec::with_capacity(file.identities.len());
1716    for record in file.identities {
1717        let local_alias = record
1718            .local_alias
1719            .clone()
1720            .or_else(|| Some(record.id.clone()).filter(|value| !value.trim().is_empty()));
1721        let dir_name = record
1722            .dir_name
1723            .clone()
1724            .or_else(|| local_alias.clone())
1725            .or_else(|| Some(record.id.clone()).filter(|value| !value.trim().is_empty()));
1726        entries.push(RegistryEntry {
1727            local_alias,
1728            dir_name,
1729            summary: super::IdentitySummary {
1730                id: crate::ids::IdentityId::parse(record.id)?,
1731                did: crate::ids::Did::parse(record.did)?,
1732                handle: optional_handle(record.handle)?,
1733                display_name: record.display_name,
1734                local_alias: record.local_alias,
1735                device_id: record.device_id,
1736                is_default: record.is_default,
1737                readiness: super::IdentityReadiness {
1738                    ready_for_auth: record.ready_for_auth,
1739                    ready_for_messaging: record.ready_for_messaging,
1740                    missing: record
1741                        .missing
1742                        .into_iter()
1743                        .map(identity_missing_item)
1744                        .collect(),
1745                },
1746            },
1747            vault_migration: record.vault_migration,
1748        });
1749    }
1750    let mut snapshot = RegistrySnapshot {
1751        default_alias: optional_trimmed_string(file.default_identity),
1752        entries,
1753    };
1754    snapshot.apply_default_flags();
1755    snapshot.validate()?;
1756    Ok(snapshot)
1757}
1758
1759fn write_registry(path: &Path, registry: &RegistrySnapshot) -> crate::ImResult<()> {
1760    if let Some(parent) = path.parent() {
1761        fs::create_dir_all(parent)?;
1762    }
1763    let file = SdkRegistryFile {
1764        default_identity: registry.default_alias.clone(),
1765        identities: registry
1766            .entries
1767            .iter()
1768            .map(|entry| SdkIdentityRecord {
1769                id: entry.summary.id.as_str().to_string(),
1770                did: entry.summary.did.as_str().to_string(),
1771                dir_name: entry.dir_name.clone(),
1772                handle: entry
1773                    .summary
1774                    .handle
1775                    .as_ref()
1776                    .map(|handle| handle.as_str().to_string()),
1777                display_name: entry.summary.display_name.clone(),
1778                local_alias: entry
1779                    .local_alias
1780                    .clone()
1781                    .or_else(|| entry.summary.local_alias.clone()),
1782                device_id: entry.summary.device_id.clone(),
1783                is_default: entry.summary.is_default,
1784                ready_for_auth: entry.summary.readiness.ready_for_auth,
1785                ready_for_messaging: entry.summary.readiness.ready_for_messaging,
1786                missing: entry
1787                    .summary
1788                    .readiness
1789                    .missing
1790                    .iter()
1791                    .map(identity_missing_item_to_string)
1792                    .collect(),
1793                vault_migration: entry.vault_migration.clone(),
1794            })
1795            .collect(),
1796    };
1797    let raw = serde_json::to_vec_pretty(&file).map_err(|err| crate::ImError::Serialization {
1798        detail: err.to_string(),
1799    })?;
1800    fs::write(path, raw)?;
1801    Ok(())
1802}
1803
1804async fn write_registry_async(path: &Path, registry: &RegistrySnapshot) -> crate::ImResult<()> {
1805    if let Some(parent) = path.parent() {
1806        tokio::fs::create_dir_all(parent).await?;
1807    }
1808    let file = SdkRegistryFile {
1809        default_identity: registry.default_alias.clone(),
1810        identities: registry
1811            .entries
1812            .iter()
1813            .map(|entry| SdkIdentityRecord {
1814                id: entry.summary.id.as_str().to_string(),
1815                did: entry.summary.did.as_str().to_string(),
1816                dir_name: entry.dir_name.clone(),
1817                handle: entry
1818                    .summary
1819                    .handle
1820                    .as_ref()
1821                    .map(|handle| handle.as_str().to_string()),
1822                display_name: entry.summary.display_name.clone(),
1823                local_alias: entry
1824                    .local_alias
1825                    .clone()
1826                    .or_else(|| entry.summary.local_alias.clone()),
1827                device_id: entry.summary.device_id.clone(),
1828                is_default: entry.summary.is_default,
1829                ready_for_auth: entry.summary.readiness.ready_for_auth,
1830                ready_for_messaging: entry.summary.readiness.ready_for_messaging,
1831                missing: entry
1832                    .summary
1833                    .readiness
1834                    .missing
1835                    .iter()
1836                    .map(identity_missing_item_to_string)
1837                    .collect(),
1838                vault_migration: entry.vault_migration.clone(),
1839            })
1840            .collect(),
1841    };
1842    let raw = serde_json::to_vec_pretty(&file).map_err(|err| crate::ImError::Serialization {
1843        detail: err.to_string(),
1844    })?;
1845    tokio::fs::write(path, raw).await?;
1846    Ok(())
1847}
1848
1849fn write_default_identity(path: Option<&Path>, default_alias: Option<&str>) -> crate::ImResult<()> {
1850    let Some(path) = path else {
1851        return Ok(());
1852    };
1853    match default_alias {
1854        Some(alias) => {
1855            if let Some(parent) = path.parent() {
1856                fs::create_dir_all(parent)?;
1857            }
1858            fs::write(path, format!("{alias}\n"))?;
1859        }
1860        None => match fs::remove_file(path) {
1861            Ok(()) => {}
1862            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
1863            Err(err) => return Err(crate::ImError::from(err)),
1864        },
1865    }
1866    Ok(())
1867}
1868
1869async fn write_default_identity_async(
1870    path: Option<PathBuf>,
1871    default_alias: Option<String>,
1872) -> crate::ImResult<()> {
1873    let Some(path) = path else {
1874        return Ok(());
1875    };
1876    match default_alias {
1877        Some(alias) => {
1878            if let Some(parent) = path.parent() {
1879                tokio::fs::create_dir_all(parent).await?;
1880            }
1881            tokio::fs::write(path, format!("{alias}\n")).await?;
1882        }
1883        None => match tokio::fs::remove_file(path).await {
1884            Ok(()) => {}
1885            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
1886            Err(err) => return Err(crate::ImError::from(err)),
1887        },
1888    }
1889    Ok(())
1890}
1891
1892fn local_identity_dir(root: &Path, dir_name: &str) -> crate::ImResult<PathBuf> {
1893    let relative = Path::new(dir_name);
1894    if relative.is_absolute()
1895        || relative
1896            .components()
1897            .any(|component| !matches!(component, std::path::Component::Normal(_)))
1898    {
1899        return Err(crate::ImError::invalid_input(
1900            Some("identity".to_string()),
1901            "local identity directory name must be a simple relative path segment",
1902        ));
1903    }
1904    Ok(root.join(relative))
1905}
1906
1907fn legacy_registry_snapshot(file: LegacyRegistryFile) -> crate::ImResult<RegistrySnapshot> {
1908    let mut entries = Vec::with_capacity(file.credentials.len());
1909    for (alias, record) in file.credentials {
1910        let id = first_non_empty([&record.unique_id, &record.credential_name, &alias])
1911            .unwrap_or(&alias)
1912            .to_string();
1913        let handle = first_non_empty([&record.full_handle, &record.handle, ""]);
1914        let dir_name = first_non_empty([&record.dir_name, &record.unique_id, &alias])
1915            .unwrap_or(&alias)
1916            .to_string();
1917        let missing = legacy_readiness_missing(&record, handle);
1918        entries.push(RegistryEntry {
1919            local_alias: Some(alias.clone()),
1920            dir_name: Some(dir_name),
1921            summary: super::IdentitySummary {
1922                id: crate::ids::IdentityId::parse(id)?,
1923                did: crate::ids::Did::parse(record.did)?,
1924                handle: optional_handle(handle.map(str::to_string))?,
1925                display_name: Some(record.name).filter(|value| !value.trim().is_empty()),
1926                local_alias: Some(alias),
1927                device_id: None,
1928                is_default: record.is_default,
1929                readiness: super::IdentityReadiness {
1930                    ready_for_auth: true,
1931                    ready_for_messaging: missing.is_empty(),
1932                    missing,
1933                },
1934            },
1935            vault_migration: record.vault_migration,
1936        });
1937    }
1938    let mut snapshot = RegistrySnapshot {
1939        default_alias: optional_trimmed_string(Some(file.default_credential_name)),
1940        entries,
1941    };
1942    snapshot.apply_default_flags();
1943    snapshot.validate()?;
1944    Ok(snapshot)
1945}
1946
1947fn legacy_readiness_missing(
1948    record: &LegacyIdentityRecord,
1949    handle: Option<&str>,
1950) -> Vec<super::IdentityMissingItem> {
1951    let mut missing = Vec::new();
1952    if record.user_id.trim().is_empty() {
1953        missing.push(super::IdentityMissingItem::Other(
1954            "registration".to_string(),
1955        ));
1956    }
1957    if handle.map(str::trim).unwrap_or_default().is_empty() {
1958        missing.push(super::IdentityMissingItem::Handle);
1959    }
1960    missing
1961}
1962
1963fn vault_metadata_is_verified(
1964    metadata: &crate::internal::identity_store::IdentityVaultMigrationMetadata,
1965) -> bool {
1966    matches!(
1967        metadata.status,
1968        crate::internal::identity_store::IdentityVaultMigrationStatus::Verified
1969    )
1970}
1971
1972fn vault_context_matches_metadata(
1973    context: &crate::core::options::IdentityVaultContext,
1974    metadata: &crate::internal::identity_store::IdentityVaultMigrationMetadata,
1975) -> bool {
1976    context.workspace_id() == metadata.workspace_id && context.device_id() == metadata.device_id
1977}
1978
1979fn default_alias_from_file(path: Option<&Path>) -> crate::ImResult<Option<String>> {
1980    let Some(path) = path else {
1981        return Ok(None);
1982    };
1983    match fs::read_to_string(path) {
1984        Ok(value) => Ok(Some(value.trim().to_string()).filter(|value| !value.is_empty())),
1985        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
1986        Err(err) => Err(crate::ImError::CredentialFileUnreadable {
1987            path_kind: "default_identity".to_string(),
1988            detail: err.to_string(),
1989        }),
1990    }
1991}
1992
1993async fn default_alias_from_file_async(
1994    path: Option<std::path::PathBuf>,
1995) -> crate::ImResult<Option<String>> {
1996    let Some(path) = path else {
1997        return Ok(None);
1998    };
1999    match tokio::fs::read_to_string(path).await {
2000        Ok(value) => Ok(Some(value.trim().to_string()).filter(|value| !value.is_empty())),
2001        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
2002        Err(err) => Err(crate::ImError::CredentialFileUnreadable {
2003            path_kind: "default_identity".to_string(),
2004            detail: err.to_string(),
2005        }),
2006    }
2007}
2008
2009fn optional_handle(value: Option<String>) -> crate::ImResult<Option<crate::ids::Handle>> {
2010    value
2011        .map(|value| value.trim().trim_start_matches('@').to_string())
2012        .filter(|value| !value.is_empty())
2013        .map(|value| crate::ids::Handle::parse(value, ""))
2014        .transpose()
2015}
2016
2017fn identity_missing_item(value: String) -> super::IdentityMissingItem {
2018    match value.trim() {
2019        "did_document" | "DidDocument" => super::IdentityMissingItem::DidDocument,
2020        "private_key" | "PrivateKey" => super::IdentityMissingItem::PrivateKey,
2021        "auth_state" | "AuthState" => super::IdentityMissingItem::AuthState,
2022        "handle" | "Handle" => super::IdentityMissingItem::Handle,
2023        "message_endpoint" | "MessageEndpoint" => super::IdentityMissingItem::MessageEndpoint,
2024        other => super::IdentityMissingItem::Other(other.to_string()),
2025    }
2026}
2027
2028fn identity_missing_item_to_string(value: &super::IdentityMissingItem) -> String {
2029    match value {
2030        super::IdentityMissingItem::DidDocument => "did_document".to_string(),
2031        super::IdentityMissingItem::PrivateKey => "private_key".to_string(),
2032        super::IdentityMissingItem::AuthState => "auth_state".to_string(),
2033        super::IdentityMissingItem::Handle => "handle".to_string(),
2034        super::IdentityMissingItem::MessageEndpoint => "message_endpoint".to_string(),
2035        super::IdentityMissingItem::Other(value) => value.clone(),
2036    }
2037}
2038
2039fn deserialize_string_lossy<'de, D>(deserializer: D) -> Result<String, D::Error>
2040where
2041    D: Deserializer<'de>,
2042{
2043    let value = Option::<Value>::deserialize(deserializer)?;
2044    Ok(value
2045        .and_then(|value| value.as_str().map(ToString::to_string))
2046        .unwrap_or_default())
2047}
2048
2049fn first_non_empty<const N: usize>(values: [&str; N]) -> Option<&str> {
2050    values.into_iter().find(|value| !value.trim().is_empty())
2051}
2052
2053fn validate_unique_registry_value(
2054    seen: &mut BTreeSet<String>,
2055    label: &str,
2056    value: &str,
2057) -> crate::ImResult<()> {
2058    let value = value.trim();
2059    if value.is_empty() {
2060        return Ok(());
2061    }
2062    if !seen.insert(value.to_owned()) {
2063        return Err(registry_invariant_error(format!(
2064            "duplicate {label} `{value}` in identity registry"
2065        )));
2066    }
2067    Ok(())
2068}
2069
2070fn registry_invariant_error(message: impl Into<String>) -> crate::ImError {
2071    crate::ImError::invalid_input(Some("identity_registry".to_owned()), message.into())
2072}
2073
2074fn optional_trimmed_string(value: Option<String>) -> Option<String> {
2075    value
2076        .map(|value| value.trim().to_owned())
2077        .filter(|value| !value.is_empty())
2078}
2079
2080#[cfg(test)]
2081mod tests {
2082    use super::*;
2083    use crate::internal::platform_secret::{DeviceVaultRootKey, SecretBytes};
2084    use crate::internal::secret_vault::{
2085        FileSecretVault, FileSecretVaultStore, SealSecretRequest, SecretAccessPolicy, SecretKind,
2086        SecretMetadata, SecretVault,
2087    };
2088    use serde_json::json;
2089
2090    #[test]
2091    fn identity_registry_rejects_duplicate_live_did() {
2092        let err = parse_registry(
2093            br#"{
2094              "identities": [
2095                {"id":"alice-id","did":"did:example:shared","local_alias":"alice"},
2096                {"id":"bob-id","did":"did:example:shared","local_alias":"bob"}
2097              ]
2098            }"#,
2099        )
2100        .unwrap_err();
2101
2102        assert_registry_error_contains(err, "duplicate live DID");
2103    }
2104
2105    #[test]
2106    fn identity_registry_rejects_duplicate_identity_id() {
2107        let err = parse_registry(
2108            br#"{
2109              "identities": [
2110                {"id":"same-id","did":"did:example:alice","local_alias":"alice"},
2111                {"id":"same-id","did":"did:example:bob","local_alias":"bob"}
2112              ]
2113            }"#,
2114        )
2115        .unwrap_err();
2116
2117        assert_registry_error_contains(err, "duplicate identity_id");
2118    }
2119
2120    #[test]
2121    fn identity_registry_rejects_duplicate_alias() {
2122        let err = parse_registry(
2123            br#"{
2124              "identities": [
2125                {"id":"alice-id","did":"did:example:alice","local_alias":"shared"},
2126                {"id":"bob-id","did":"did:example:bob","local_alias":"shared"}
2127              ]
2128            }"#,
2129        )
2130        .unwrap_err();
2131
2132        assert_registry_error_contains(err, "duplicate local alias");
2133    }
2134
2135    #[test]
2136    fn identity_registry_rejects_duplicate_handle() {
2137        let err = parse_registry(
2138            br#"{
2139              "identities": [
2140                {"id":"alice-id","did":"did:example:alice","local_alias":"alice","handle":"shared.awiki.test"},
2141                {"id":"bob-id","did":"did:example:bob","local_alias":"bob","handle":"shared.awiki.test"}
2142              ]
2143            }"#,
2144        )
2145        .unwrap_err();
2146
2147        assert_registry_error_contains(err, "duplicate handle");
2148    }
2149
2150    #[test]
2151    fn identity_registry_rejects_missing_default_alias() {
2152        let err = parse_registry(
2153            br#"{
2154              "default_identity": "missing",
2155              "identities": [
2156                {"id":"alice-id","did":"did:example:alice","local_alias":"alice"}
2157              ]
2158            }"#,
2159        )
2160        .unwrap_err();
2161
2162        assert_registry_error_contains(err, "default identity alias");
2163    }
2164
2165    #[test]
2166    fn identity_vault_status_and_runtime_use_verified_matching_vault_context() {
2167        let root = tempfile::tempdir().unwrap();
2168        let identity_dir = root.path().join("identities").join("alice-id");
2169        std::fs::create_dir_all(&identity_dir).unwrap();
2170        std::fs::write(
2171            identity_dir.join("did_document.json"),
2172            serde_json::to_vec(&json!({"id": "did:example:alice"})).unwrap(),
2173        )
2174        .unwrap();
2175        std::fs::write(
2176            identity_dir.join("key-1-private.pem"),
2177            "file-signing-secret",
2178        )
2179        .unwrap();
2180        std::fs::write(
2181            identity_dir.join("e2ee-agreement-private.pem"),
2182            "file-agreement-secret",
2183        )
2184        .unwrap();
2185        std::fs::write(
2186            identity_dir.join("auth.json"),
2187            serde_json::to_vec(&json!({"jwt_token": "file-token-secret"})).unwrap(),
2188        )
2189        .unwrap();
2190        let vault_dir = root.path().join("vault");
2191        let vault = FileSecretVault::new(
2192            DeviceVaultRootKey::from_bytes([31_u8; 32]),
2193            FileSecretVaultStore::new(&vault_dir),
2194        );
2195        let signing_ref = vault
2196            .seal(SealSecretRequest {
2197                metadata: test_secret_metadata(
2198                    "workspace-a",
2199                    "device-a",
2200                    "alice-id",
2201                    "did:example:alice",
2202                    SecretKind::IdentityRootPrivate,
2203                    "key-1",
2204                ),
2205                plaintext: SecretBytes::from_vec(b"vault-signing-secret".to_vec()),
2206            })
2207            .unwrap();
2208        let agreement_ref = vault
2209            .seal(SealSecretRequest {
2210                metadata: test_secret_metadata(
2211                    "workspace-a",
2212                    "device-a",
2213                    "alice-id",
2214                    "did:example:alice",
2215                    SecretKind::IdentityE2eeAgreementPrivate,
2216                    "key-3",
2217                ),
2218                plaintext: SecretBytes::from_vec(b"vault-agreement-secret".to_vec()),
2219            })
2220            .unwrap();
2221        let auth_ref = vault
2222            .seal(SealSecretRequest {
2223                metadata: test_secret_metadata(
2224                    "workspace-a",
2225                    "device-a",
2226                    "alice-id",
2227                    "did:example:alice",
2228                    SecretKind::AuthJwt,
2229                    "auth.json",
2230                ),
2231                plaintext: SecretBytes::from_vec(
2232                    crate::internal::auth::state::auth_state_json_for_token("vault-token-secret")
2233                        .unwrap(),
2234                ),
2235            })
2236            .unwrap();
2237        let registry_path = root.path().join("identities").join("registry.json");
2238        std::fs::write(
2239            &registry_path,
2240            serde_json::to_vec_pretty(&json!({
2241                "default_credential_name": "alice",
2242                "credentials": {
2243                    "alice": {
2244                        "credential_name": "alice",
2245                        "dir_name": "alice-id",
2246                        "did": "did:example:alice",
2247                        "unique_id": "alice-id",
2248                        "user_id": "user-1",
2249                        "name": "Alice",
2250                        "handle": "alice",
2251                        "full_handle": "alice.example",
2252                        "is_default": true,
2253                        "vault_migration": {
2254                            "schema_version": 1,
2255                            "status": "verified",
2256                            "backend": "vault",
2257                            "unlock_policy": "explicit_root_key",
2258                            "migrated_at": "2026-07-03T00:00:00Z",
2259                            "workspace_id": "workspace-a",
2260                            "device_id": "device-a",
2261                            "plaintext_compat_retained": true,
2262                            "refs": {
2263                                "default_signing_private": signing_ref,
2264                                "e2ee_agreement_private": agreement_ref,
2265                                "auth_jwt": auth_ref
2266                            }
2267                        }
2268                    }
2269                }
2270            }))
2271            .unwrap(),
2272        )
2273        .unwrap();
2274        let core = crate::ImCore::new_with_options(
2275            test_config(),
2276            test_paths(root.path()),
2277            crate::ImCoreOpenOptions::default().with_identity_secret_vault(
2278                crate::IdentitySecretStoragePolicy::VaultRequired,
2279                crate::ImCoreSecretVaultOptions::new(
2280                    DeviceVaultRootKey::from_bytes([31_u8; 32]),
2281                    vault_dir,
2282                    "workspace-a",
2283                    "device-a",
2284                ),
2285            ),
2286        )
2287        .unwrap();
2288
2289        let status = core
2290            .identities()
2291            .vault_status(crate::identity::IdentitySelector::Default)
2292            .unwrap();
2293        assert_eq!(
2294            status.selected_backend,
2295            crate::identity::IdentitySecretStorageBackend::Vault
2296        );
2297        assert!(status.vault_available);
2298        assert!(status.vault_metadata_present);
2299        assert!(status.vault_metadata_verified);
2300        assert_eq!(status.workspace_id.as_deref(), Some("workspace-a"));
2301        assert_eq!(status.device_id.as_deref(), Some("device-a"));
2302        assert_eq!(status.plaintext_compat_retained, Some(true));
2303        assert!(status.missing.is_empty());
2304
2305        let runtime = core
2306            .identities()
2307            .load_runtime(crate::identity::IdentitySelector::Default)
2308            .unwrap();
2309        assert_eq!(
2310            runtime.key_provider.default_signing_private_pem().unwrap(),
2311            "vault-signing-secret"
2312        );
2313        assert_eq!(
2314            runtime.key_provider.e2ee_agreement_private_pem().unwrap(),
2315            "vault-agreement-secret"
2316        );
2317        assert_eq!(
2318            runtime.key_provider.valid_auth_token().unwrap().as_deref(),
2319            Some("vault-token-secret")
2320        );
2321
2322        let written = parse_registry(&std::fs::read(&registry_path).unwrap()).unwrap();
2323        let rewritten = root
2324            .path()
2325            .join("identities")
2326            .join("rewritten-registry.json");
2327        write_registry(&rewritten, &written).unwrap();
2328        let reparsed = parse_registry(&std::fs::read(rewritten).unwrap()).unwrap();
2329        assert!(reparsed.entries[0].vault_migration.is_some());
2330    }
2331
2332    #[test]
2333    fn identity_vault_migrate_and_verify_public_api_use_vault_provider() {
2334        let root = tempfile::tempdir().unwrap();
2335        let paths = test_paths(root.path());
2336        let store = crate::internal::identity_store::IdentityStore::new(&paths.identities);
2337        store
2338            .save_identity(crate::internal::identity_store::SaveIdentityInput {
2339                local_alias: "alice".to_owned(),
2340                did: crate::ids::Did::parse("did:example:alice").unwrap(),
2341                unique_id: "alice-id".to_owned(),
2342                user_id: "user-1".to_owned(),
2343                display_name: "Alice".to_owned(),
2344                handle: "alice".to_owned(),
2345                full_handle: "alice.example".to_owned(),
2346                jwt_token: "jwt-secret-value".to_owned(),
2347                did_document: Some(json!({"id": "did:example:alice"})),
2348                key1_private_pem: "signing-private-secret".to_owned(),
2349                key1_public_pem: "signing-public".to_owned(),
2350                e2ee_signing_private_pem: String::new(),
2351                e2ee_agreement_private_pem: "e2ee-agreement-secret".to_owned(),
2352                daemon_subkey_package: None,
2353                make_default: true,
2354            })
2355            .unwrap();
2356        let vault_dir = root.path().join("vault");
2357        let core = crate::ImCore::new_with_options(
2358            test_config(),
2359            paths,
2360            crate::ImCoreOpenOptions::default().with_identity_secret_vault(
2361                crate::IdentitySecretStoragePolicy::VaultRequired,
2362                crate::ImCoreSecretVaultOptions::new(
2363                    DeviceVaultRootKey::from_bytes([41_u8; 32]),
2364                    &vault_dir,
2365                    "workspace-a",
2366                    "device-a",
2367                ),
2368            ),
2369        )
2370        .unwrap();
2371
2372        let report = core
2373            .identities()
2374            .migrate_identity_vault(crate::identity::IdentitySelector::LocalAlias(
2375                "alice".to_owned(),
2376            ))
2377            .unwrap();
2378
2379        assert!(report.migrated);
2380        assert!(report.verified);
2381        assert!(report.plaintext_compat_retained);
2382        assert_eq!(
2383            report.status.selected_backend,
2384            crate::identity::IdentitySecretStorageBackend::Vault
2385        );
2386        assert_eq!(report.identity.did.as_str(), "did:example:alice");
2387
2388        let verification = core
2389            .identities()
2390            .verify_identity_vault(crate::identity::IdentitySelector::LocalAlias(
2391                "alice".to_owned(),
2392            ))
2393            .unwrap();
2394        assert!(verification.verified);
2395        assert_eq!(
2396            verification.status.selected_backend,
2397            crate::identity::IdentitySecretStorageBackend::Vault
2398        );
2399    }
2400
2401    #[test]
2402    fn vault_required_without_vault_options_fails_closed() {
2403        let root = tempfile::tempdir().unwrap();
2404        let err = match crate::ImCore::new_with_options(
2405            test_config(),
2406            test_paths(root.path()),
2407            crate::ImCoreOpenOptions {
2408                identity_secret_storage_policy: crate::IdentitySecretStoragePolicy::VaultRequired,
2409                identity_secret_vault: None,
2410            },
2411        ) {
2412            Ok(_) => panic!("VaultRequired without vault options should fail"),
2413            Err(err) => err,
2414        };
2415
2416        let message = err.to_string();
2417        assert!(message.contains("VaultRequired"));
2418        assert!(!message.contains("root key"));
2419    }
2420
2421    fn assert_registry_error_contains(err: crate::ImError, expected: &str) {
2422        match err {
2423            crate::ImError::InvalidInput {
2424                field: Some(field),
2425                message,
2426            } => {
2427                assert_eq!(field, "identity_registry");
2428                assert!(
2429                    message.contains(expected),
2430                    "expected `{message}` to contain `{expected}`"
2431                );
2432            }
2433            other => panic!("unexpected error: {other:?}"),
2434        }
2435    }
2436
2437    fn test_secret_metadata(
2438        workspace_id: &str,
2439        device_id: &str,
2440        identity_id: &str,
2441        did: &str,
2442        kind: SecretKind,
2443        key_id: &str,
2444    ) -> SecretMetadata {
2445        SecretMetadata {
2446            workspace_id: workspace_id.to_owned(),
2447            device_id: device_id.to_owned(),
2448            identity_id: Some(identity_id.to_owned()),
2449            did: Some(did.to_owned()),
2450            kind,
2451            key_id: key_id.to_owned(),
2452            key_version: 1,
2453            policy: SecretAccessPolicy::no_prompt_local_secret(),
2454        }
2455    }
2456
2457    fn test_config() -> crate::ImCoreConfig {
2458        crate::ImCoreConfig {
2459            service_base_url: crate::ServiceEndpoint::parse("https://example.test").unwrap(),
2460            did_domain: "awiki.test".to_owned(),
2461            user_service_endpoint: None,
2462            message_service_endpoint: None,
2463            mail_service_endpoint: None,
2464            anp_service_endpoint: None,
2465            anp_service_did: None,
2466            ca_bundle: None,
2467            transport_policy: crate::MessageTransportPolicy::HttpOnly,
2468        }
2469    }
2470
2471    fn test_paths(root: &Path) -> crate::ImCorePaths {
2472        crate::ImCorePaths {
2473            identities: crate::IdentityRegistryPaths {
2474                identity_root_dir: root.join("identities"),
2475                registry_path: root.join("identities").join("registry.json"),
2476                default_identity_path: Some(root.join("identities").join("default")),
2477            },
2478            local_state: crate::LocalStatePaths {
2479                sqlite_path: root.join("local").join("im.sqlite"),
2480            },
2481            runtime: crate::RuntimePaths {
2482                cache_dir: root.join("cache"),
2483                temp_dir: root.join("tmp"),
2484            },
2485        }
2486    }
2487}