archy/core/archipelago/src/identity_manager.rs
Dorian 30164fd12a feat: bitcoin-ui CSS fix, HTTPS proxy support, deploy script improvements
Bitcoin UI:
- Replace cdn.tailwindcss.com with locally bundled tailwind.css (CSP blocks external scripts)
- Make all asset paths relative for nginx proxy compatibility
- Add bitcoin-ui build/deploy to deploy-to-target.sh (was missing entirely)
- Use --network host (bitcoin-ui proxies Bitcoin RPC at 127.0.0.1:8332)

HTTPS mixed content fix:
- Add HTTPS_PROXY_PATHS in AppSession.vue — when parent page is HTTPS,
  iframe loads through nginx proxy instead of direct HTTP port
- Prevents browser blocking HTTP iframes inside HTTPS pages
- All Tailscale servers use HTTPS, this was breaking all app iframes

Deploy & first-boot improvements:
- first-boot-containers.sh auto-detects disk size for pruning vs txindex
- first-boot-containers.sh checks fallback source path for UI containers
- Added mempool-electrs to APP_PORTS mapping
- ElectrumX container creation in first-boot
- Podman doctor/fix/uptime skills added

Also includes: session persistence, identity management, LND transactions,
ElectrumX status UI, nostr-provider improvements, Web5 enhancements

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 12:58:35 +00:00

608 lines
24 KiB
Rust

//! Multi-identity manager: multiple Ed25519 identities with DID support.
//! Each identity has a keypair, display name, purpose tag, and DID:key.
//! Identities are stored as JSON files encrypted with the node's master key.
use anyhow::{Context, Result};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tokio::fs;
use crate::identity::did_key_from_pubkey_hex;
use nostr_sdk::ToBech32;
const IDENTITIES_DIR: &str = "identities";
const DEFAULT_MARKER: &str = ".default";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum IdentityPurpose {
Personal,
Business,
Anonymous,
}
impl std::fmt::Display for IdentityPurpose {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IdentityPurpose::Personal => write!(f, "personal"),
IdentityPurpose::Business => write!(f, "business"),
IdentityPurpose::Anonymous => write!(f, "anonymous"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentityRecord {
pub id: String,
pub name: String,
pub purpose: IdentityPurpose,
pub pubkey_hex: String,
pub did: String,
/// did:dht identifier (published to Mainline DHT for discoverability)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dht_did: Option<String>,
pub created_at: String,
/// Nostr secp256k1 public key in hex format
pub nostr_pubkey: Option<String>,
/// Nostr public key in bech32 npub format (NIP-19)
pub nostr_npub: Option<String>,
/// Nostr profile metadata (NIP-01 kind 0)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<IdentityProfile>,
}
/// Nostr profile metadata fields (NIP-01 kind 0 + NIP-24 extra fields).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct IdentityProfile {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub about: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub picture: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub banner: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub website: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nip05: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lud16: Option<String>,
}
/// On-disk format for identity storage (includes secret key bytes).
#[derive(Serialize, Deserialize)]
struct IdentityFile {
id: String,
name: String,
purpose: IdentityPurpose,
secret_key: Vec<u8>,
pubkey_hex: String,
did: String,
created_at: String,
#[serde(default)]
nostr_secret_hex: Option<String>,
#[serde(default)]
nostr_pubkey_hex: Option<String>,
/// Nostr profile metadata
#[serde(default)]
profile: Option<IdentityProfile>,
}
pub struct IdentityManager {
identities_dir: PathBuf,
}
impl IdentityManager {
pub async fn new(data_dir: &Path) -> Result<Self> {
let identities_dir = data_dir.join(IDENTITIES_DIR);
fs::create_dir_all(&identities_dir)
.await
.context("Failed to create identities directory")?;
Ok(Self { identities_dir })
}
/// List all identities (without secret keys).
pub async fn list(&self) -> Result<(Vec<IdentityRecord>, Option<String>)> {
let default_id = self.get_default_id().await;
let mut identities = Vec::new();
let mut entries = fs::read_dir(&self.identities_dir)
.await
.context("Failed to read identities directory")?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
match self.load_record(&path).await {
Ok(record) => identities.push(record),
Err(e) => {
tracing::warn!("Skipping corrupt identity file {:?}: {}", path, e);
}
}
}
identities.sort_by(|a, b| a.created_at.cmp(&b.created_at));
Ok((identities, default_id))
}
/// Create a new identity.
pub async fn create(&self, name: String, purpose: IdentityPurpose) -> Result<IdentityRecord> {
let signing_key = SigningKey::generate(&mut OsRng);
let pubkey_hex = hex::encode(signing_key.verifying_key().as_bytes());
let did = did_key_from_pubkey_hex(&pubkey_hex)?;
let id = uuid::Uuid::new_v4().to_string();
let created_at = chrono::Utc::now().to_rfc3339();
let identity_file = IdentityFile {
id: id.clone(),
name: name.clone(),
purpose: purpose.clone(),
secret_key: signing_key.to_bytes().to_vec(),
pubkey_hex: pubkey_hex.clone(),
did: did.clone(),
created_at: created_at.clone(),
nostr_secret_hex: None,
nostr_pubkey_hex: None,
profile: None,
};
let file_path = self.identities_dir.join(format!("{}.json", id));
let json = serde_json::to_string_pretty(&identity_file)
.context("Failed to serialize identity")?;
fs::write(&file_path, json.as_bytes())
.await
.context("Failed to write identity file")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&file_path, std::fs::Permissions::from_mode(0o600))
.await
.context("Failed to set identity file permissions")?;
}
// If this is the first identity, make it the default
let (existing, _) = self.list().await?;
if existing.len() <= 1 {
self.set_default(&id).await?;
}
// Auto-generate Nostr keypair so every identity has both key types
let _ = self.create_nostr_key(&id).await;
// Re-read to pick up the Nostr keys
let record = self.get(&id).await?;
tracing::info!("Created identity '{}' ({})", name, purpose);
Ok(record)
}
/// Get a single identity by ID (without secret key).
pub async fn get(&self, id: &str) -> Result<IdentityRecord> {
let file_path = self.identities_dir.join(format!("{}.json", id));
if !file_path.exists() {
return Err(anyhow::anyhow!("Identity not found: {}", id));
}
self.load_record(&file_path).await
}
/// Delete an identity.
pub async fn delete(&self, id: &str) -> Result<()> {
let file_path = self.identities_dir.join(format!("{}.json", id));
if !file_path.exists() {
return Err(anyhow::anyhow!("Identity not found: {}", id));
}
fs::remove_file(&file_path)
.await
.context("Failed to delete identity file")?;
// If this was the default, clear the marker
if let Some(default_id) = self.get_default_id().await {
if default_id == id {
let marker = self.identities_dir.join(DEFAULT_MARKER);
let _ = fs::remove_file(marker).await;
// Set a new default if other identities exist
let (remaining, _) = self.list().await?;
if let Some(first) = remaining.first() {
self.set_default(&first.id).await?;
}
}
}
tracing::info!("Deleted identity {}", id);
Ok(())
}
/// Set the default identity.
pub async fn set_default(&self, id: &str) -> Result<()> {
// Verify it exists
let file_path = self.identities_dir.join(format!("{}.json", id));
if !file_path.exists() {
return Err(anyhow::anyhow!("Identity not found: {}", id));
}
let marker = self.identities_dir.join(DEFAULT_MARKER);
fs::write(&marker, id.as_bytes())
.await
.context("Failed to write default identity marker")?;
Ok(())
}
/// Get the Ed25519 signing key for an identity (for DHT publication).
pub async fn get_signing_key(&self, id: &str) -> Result<SigningKey> {
self.load_signing_key(id).await
}
/// Sign data with a specific identity.
pub async fn sign(&self, id: &str, data: &[u8]) -> Result<String> {
let signing_key = self.load_signing_key(id).await?;
Ok(hex::encode(signing_key.sign(data).to_bytes()))
}
/// Verify a signature against a DID's public key.
/// Works for any valid did:key (not just local identities).
pub async fn verify(&self, did: &str, data: &[u8], sig_hex: &str) -> Result<bool> {
// Extract pubkey from did:key directly — no local lookup needed
let pubkey_bytes = pubkey_bytes_from_did_key(did)?;
let verifying_key = VerifyingKey::from_bytes(
pubkey_bytes
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid pubkey length"))?,
)?;
let sig_bytes = hex::decode(sig_hex).context("Invalid signature hex")?;
let sig = Signature::from_bytes(
sig_bytes
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid signature length"))?,
);
Ok(verifying_key.verify(data, &sig).is_ok())
}
/// Create a Nostr keypair for an identity.
pub async fn create_nostr_key(&self, id: &str) -> Result<String> {
let file_path = self.identities_dir.join(format!("{}.json", id));
if !file_path.exists() {
return Err(anyhow::anyhow!("Identity not found: {}", id));
}
let data = fs::read(&file_path).await.context("Failed to read identity file")?;
let mut file: IdentityFile = serde_json::from_slice(&data).context("Failed to parse identity file")?;
if file.nostr_secret_hex.is_some() {
return Err(anyhow::anyhow!("Nostr key already exists for this identity"));
}
let keys = nostr_sdk::Keys::generate();
let secret_hex = keys.secret_key().display_secret().to_string();
let pubkey_hex = keys.public_key().to_hex();
let npub = keys.public_key().to_bech32().unwrap_or_default();
file.nostr_secret_hex = Some(secret_hex);
file.nostr_pubkey_hex = Some(pubkey_hex.clone());
let json = serde_json::to_string_pretty(&file).context("Failed to serialize identity")?;
fs::write(&file_path, json.as_bytes()).await.context("Failed to write identity file")?;
tracing::info!("Created Nostr key for identity {} (npub: {})", id, &npub[..20.min(npub.len())]);
Ok(pubkey_hex)
}
/// Sign a Nostr event (NIP-01) with an identity's Nostr key.
/// Returns the signature hex string.
pub async fn nostr_sign(&self, id: &str, event_hash_hex: &str) -> Result<String> {
let file_path = self.identities_dir.join(format!("{}.json", id));
if !file_path.exists() {
return Err(anyhow::anyhow!("Identity not found: {}", id));
}
let data = fs::read(&file_path).await.context("Failed to read identity file")?;
let file: IdentityFile = serde_json::from_slice(&data).context("Failed to parse identity file")?;
let secret_hex = file.nostr_secret_hex
.ok_or_else(|| anyhow::anyhow!("No Nostr key for this identity"))?;
let keys = nostr_sdk::Keys::parse(&secret_hex).context("Invalid Nostr secret key")?;
let hash_bytes = hex::decode(event_hash_hex).context("Invalid event hash hex")?;
if hash_bytes.len() != 32 {
return Err(anyhow::anyhow!("Event hash must be 32 bytes"));
}
let message = nostr_sdk::secp256k1::Message::from_digest(
hash_bytes.try_into().map_err(|_| anyhow::anyhow!("Invalid hash length"))?,
);
let sig = keys.sign_schnorr(&message);
Ok(sig.to_string())
}
/// Load the Nostr secret key for an identity, returning the parsed Keys.
async fn load_nostr_keys(&self, id: &str) -> Result<nostr_sdk::Keys> {
let file_path = self.identities_dir.join(format!("{}.json", id));
if !file_path.exists() {
return Err(anyhow::anyhow!("Identity not found: {}", id));
}
let data = fs::read(&file_path).await.context("Failed to read identity file")?;
let file: IdentityFile = serde_json::from_slice(&data).context("Failed to parse identity file")?;
let secret_hex = file.nostr_secret_hex
.ok_or_else(|| anyhow::anyhow!("No Nostr key for this identity"))?;
nostr_sdk::Keys::parse(&secret_hex).context("Invalid Nostr secret key")
}
/// NIP-04 encrypt plaintext for a peer pubkey.
pub async fn nostr_encrypt_nip04(&self, id: &str, peer_pubkey_hex: &str, plaintext: &str) -> Result<String> {
let keys = self.load_nostr_keys(id).await?;
let peer_pk = nostr_sdk::PublicKey::from_hex(peer_pubkey_hex)
.context("Invalid peer pubkey hex")?;
nostr_sdk::nips::nip04::encrypt(keys.secret_key(), &peer_pk, plaintext)
.context("NIP-04 encryption failed")
}
/// NIP-04 decrypt ciphertext from a peer pubkey.
pub async fn nostr_decrypt_nip04(&self, id: &str, peer_pubkey_hex: &str, ciphertext: &str) -> Result<String> {
let keys = self.load_nostr_keys(id).await?;
let peer_pk = nostr_sdk::PublicKey::from_hex(peer_pubkey_hex)
.context("Invalid peer pubkey hex")?;
nostr_sdk::nips::nip04::decrypt(keys.secret_key(), &peer_pk, ciphertext)
.context("NIP-04 decryption failed")
}
/// NIP-44 encrypt plaintext for a peer pubkey.
pub async fn nostr_encrypt_nip44(&self, id: &str, peer_pubkey_hex: &str, plaintext: &str) -> Result<String> {
let keys = self.load_nostr_keys(id).await?;
let peer_pk = nostr_sdk::PublicKey::from_hex(peer_pubkey_hex)
.context("Invalid peer pubkey hex")?;
nostr_sdk::nips::nip44::encrypt(keys.secret_key(), &peer_pk, plaintext, nostr_sdk::nips::nip44::Version::V2)
.context("NIP-44 encryption failed")
}
/// NIP-44 decrypt ciphertext from a peer pubkey.
pub async fn nostr_decrypt_nip44(&self, id: &str, peer_pubkey_hex: &str, ciphertext: &str) -> Result<String> {
let keys = self.load_nostr_keys(id).await?;
let peer_pk = nostr_sdk::PublicKey::from_hex(peer_pubkey_hex)
.context("Invalid peer pubkey hex")?;
nostr_sdk::nips::nip44::decrypt(keys.secret_key(), &peer_pk, ciphertext)
.context("NIP-44 decryption failed")
}
/// Update the profile metadata for an identity.
pub async fn update_profile(&self, id: &str, profile: IdentityProfile) -> Result<()> {
let file_path = self.identities_dir.join(format!("{}.json", id));
if !file_path.exists() {
return Err(anyhow::anyhow!("Identity not found: {}", id));
}
let data = fs::read(&file_path).await.context("Failed to read identity file")?;
let mut file: IdentityFile = serde_json::from_slice(&data).context("Failed to parse identity file")?;
file.profile = Some(profile);
let json = serde_json::to_string_pretty(&file).context("Failed to serialize identity")?;
fs::write(&file_path, json.as_bytes()).await.context("Failed to write identity file")?;
Ok(())
}
/// Publish kind 0 (metadata) event to a Nostr relay.
pub async fn publish_profile(&self, id: &str, relay_url: &str) -> Result<String> {
let record = self.get(id).await?;
let keys = self.load_nostr_keys(id).await?;
let profile = record.profile.unwrap_or_default();
// Build kind 0 content JSON (NIP-01 + NIP-24)
let mut content = serde_json::Map::new();
content.insert("name".to_string(), serde_json::json!(record.name));
if let Some(v) = &profile.display_name { content.insert("display_name".to_string(), serde_json::json!(v)); }
if let Some(v) = &profile.about { content.insert("about".to_string(), serde_json::json!(v)); }
if let Some(v) = &profile.picture { content.insert("picture".to_string(), serde_json::json!(v)); }
if let Some(v) = &profile.banner { content.insert("banner".to_string(), serde_json::json!(v)); }
if let Some(v) = &profile.website { content.insert("website".to_string(), serde_json::json!(v)); }
if let Some(v) = &profile.nip05 { content.insert("nip05".to_string(), serde_json::json!(v)); }
if let Some(v) = &profile.lud16 { content.insert("lud16".to_string(), serde_json::json!(v)); }
let content_str = serde_json::to_string(&content).context("Failed to serialize profile content")?;
let client = nostr_sdk::Client::new(keys);
client.add_relay(relay_url).await.context("Failed to add relay")?;
client.connect().await;
let builder = nostr_sdk::prelude::EventBuilder::new(
nostr_sdk::prelude::Kind::Metadata,
&content_str,
);
let output = client.send_event_builder(builder).await.context("Failed to publish profile")?;
client.disconnect().await;
Ok(output.id().to_hex())
}
/// Export all keys for an identity (SENSITIVE — only call after password verification).
pub async fn export_keys(&self, id: &str) -> Result<serde_json::Value> {
let file_path = self.identities_dir.join(format!("{}.json", id));
if !file_path.exists() {
return Err(anyhow::anyhow!("Identity not found: {}", id));
}
let data = fs::read(&file_path).await.context("Failed to read identity file")?;
let file: IdentityFile = serde_json::from_slice(&data).context("Failed to parse identity file")?;
let ed25519_secret_hex = hex::encode(&file.secret_key);
let nostr_nsec = file.nostr_secret_hex.as_ref().and_then(|h| {
nostr_sdk::SecretKey::from_hex(h)
.ok()
.and_then(|sk| sk.to_bech32().ok())
});
Ok(serde_json::json!({
"ed25519_secret_hex": ed25519_secret_hex,
"nostr_secret_hex": file.nostr_secret_hex,
"nostr_nsec": nostr_nsec,
}))
}
// --- internal helpers ---
}
/// Extract Ed25519 pubkey bytes from a did:key string.
/// Format: did:key:z<base58btc(0xed01 + 32-byte-pubkey)>
fn pubkey_bytes_from_did_key(did: &str) -> Result<Vec<u8>> {
let z_part = did
.strip_prefix("did:key:z")
.ok_or_else(|| anyhow::anyhow!("Invalid did:key format: {}", did))?;
let decoded = bs58::decode(z_part)
.into_vec()
.context("Invalid base58 in did:key")?;
if decoded.len() != 34 || decoded[0] != 0xed || decoded[1] != 0x01 {
return Err(anyhow::anyhow!("Invalid Ed25519 did:key multicodec prefix"));
}
Ok(decoded[2..].to_vec())
}
impl IdentityManager {
async fn get_default_id(&self) -> Option<String> {
let marker = self.identities_dir.join(DEFAULT_MARKER);
fs::read_to_string(&marker).await.ok().map(|s| s.trim().to_string())
}
async fn load_record(&self, path: &Path) -> Result<IdentityRecord> {
let data = fs::read(path)
.await
.context("Failed to read identity file")?;
let file: IdentityFile = serde_json::from_slice(&data)
.context("Failed to parse identity file")?;
// Derive npub (bech32) from hex pubkey if available
let nostr_npub = file.nostr_pubkey_hex.as_ref().and_then(|hex| {
nostr_sdk::PublicKey::from_hex(hex)
.ok()
.and_then(|pk| pk.to_bech32().ok())
});
Ok(IdentityRecord {
id: file.id,
name: file.name,
purpose: file.purpose,
pubkey_hex: file.pubkey_hex,
did: file.did,
dht_did: None,
created_at: file.created_at,
nostr_pubkey: file.nostr_pubkey_hex,
nostr_npub,
profile: file.profile,
})
}
async fn load_signing_key(&self, id: &str) -> Result<SigningKey> {
let file_path = self.identities_dir.join(format!("{}.json", id));
if !file_path.exists() {
return Err(anyhow::anyhow!("Identity not found: {}", id));
}
let data = fs::read(&file_path)
.await
.context("Failed to read identity file")?;
let file: IdentityFile = serde_json::from_slice(&data)
.context("Failed to parse identity file")?;
let arr: [u8; 32] = file
.secret_key
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid secret key length"))?;
Ok(SigningKey::from_bytes(&arr))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn test_create_identity_did_key_format() {
let dir = tempdir().unwrap();
let mgr = IdentityManager::new(dir.path()).await.unwrap();
let record = mgr.create("Test".to_string(), IdentityPurpose::Personal).await.unwrap();
assert!(record.did.starts_with("did:key:z6Mk"), "DID should be did:key:z6Mk..., got {}", record.did);
assert!(!record.id.is_empty());
assert_eq!(record.name, "Test");
}
#[tokio::test]
async fn test_create_nostr_key_npub_format() {
let dir = tempdir().unwrap();
let mgr = IdentityManager::new(dir.path()).await.unwrap();
let record = mgr.create("Nostr".to_string(), IdentityPurpose::Personal).await.unwrap();
let npub = mgr.create_nostr_key(&record.id).await.unwrap();
assert!(npub.starts_with("npub1"), "npub should start with npub1, got {}", npub);
}
#[tokio::test]
async fn test_sign_and_verify() {
let dir = tempdir().unwrap();
let mgr = IdentityManager::new(dir.path()).await.unwrap();
let record = mgr.create("Signer".to_string(), IdentityPurpose::Personal).await.unwrap();
let data = b"hello archipelago";
let sig = mgr.sign(&record.id, data).await.unwrap();
let valid = mgr.verify(&record.did, data, &sig).await.unwrap();
assert!(valid, "Signature should verify");
}
#[tokio::test]
async fn test_sign_verify_wrong_data() {
let dir = tempdir().unwrap();
let mgr = IdentityManager::new(dir.path()).await.unwrap();
let record = mgr.create("Signer".to_string(), IdentityPurpose::Personal).await.unwrap();
let sig = mgr.sign(&record.id, b"correct").await.unwrap();
let valid = mgr.verify(&record.did, b"wrong", &sig).await.unwrap();
assert!(!valid, "Signature should not verify with wrong data");
}
#[tokio::test]
async fn test_list_identities() {
let dir = tempdir().unwrap();
let mgr = IdentityManager::new(dir.path()).await.unwrap();
mgr.create("One".to_string(), IdentityPurpose::Personal).await.unwrap();
mgr.create("Two".to_string(), IdentityPurpose::Business).await.unwrap();
let (list, _) = mgr.list().await.unwrap();
assert_eq!(list.len(), 2);
}
#[tokio::test]
async fn test_delete_identity() {
let dir = tempdir().unwrap();
let mgr = IdentityManager::new(dir.path()).await.unwrap();
let r1 = mgr.create("First".to_string(), IdentityPurpose::Personal).await.unwrap();
let r2 = mgr.create("Second".to_string(), IdentityPurpose::Business).await.unwrap();
mgr.delete(&r1.id).await.unwrap();
let (list, _) = mgr.list().await.unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0].id, r2.id);
}
#[tokio::test]
async fn test_set_and_get_default() {
let dir = tempdir().unwrap();
let mgr = IdentityManager::new(dir.path()).await.unwrap();
let r1 = mgr.create("First".to_string(), IdentityPurpose::Personal).await.unwrap();
let r2 = mgr.create("Second".to_string(), IdentityPurpose::Business).await.unwrap();
mgr.set_default(&r2.id).await.unwrap();
let (_, default_id) = mgr.list().await.unwrap();
assert_eq!(default_id, Some(r2.id.clone()));
assert_ne!(default_id, Some(r1.id));
}
#[tokio::test]
async fn test_delete_default_shifts() {
let dir = tempdir().unwrap();
let mgr = IdentityManager::new(dir.path()).await.unwrap();
let r1 = mgr.create("First".to_string(), IdentityPurpose::Personal).await.unwrap();
mgr.create("Second".to_string(), IdentityPurpose::Business).await.unwrap();
mgr.set_default(&r1.id).await.unwrap();
mgr.delete(&r1.id).await.unwrap();
let (list, _) = mgr.list().await.unwrap();
assert_eq!(list.len(), 1);
}
}