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, ®istry)?;
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, ®istry).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(®istry, 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(®istry, 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(®istry, 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(®istry, 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(®istry, 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(®istry, 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 ®istry_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(®istry_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}