saorsa_core/
api.rs

1// Copyright 2024 Saorsa Labs Limited
2//
3// This software is dual-licensed under:
4// - GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
5// - Commercial License
6//
7// For AGPL-3.0 license, see LICENSE-AGPL-3.0
8// For commercial licensing, contact: saorsalabs@gmail.com
9
10//! Clean API implementation for saorsa-core
11//!
12//! This module provides the simplified public API for:
13//! - Identity registration and management
14//! - Presence and device management
15//! - Storage with saorsa-seal and saorsa-fec
16
17use crate::auth::Sig;
18use crate::fwid::{Key, compute_key, fw_check, fw_to_key};
19use crate::types::{
20    Device, DeviceId, Endpoint, Identity, IdentityHandle, MlDsaKeyPair, Presence, PresenceReceipt,
21    StorageHandle, StorageStrategy,
22};
23use anyhow::{Context, Result};
24use serde::{Deserialize, Serialize};
25use std::collections::HashMap;
26use std::sync::Arc;
27use tokio::sync::RwLock;
28// tracing not currently used in this module
29
30// Mock DHT for fallback when no global DHT client is installed
31struct MockDht {
32    storage: HashMap<Key, Vec<u8>>,
33}
34
35impl MockDht {
36    fn new() -> Self {
37        Self {
38            storage: HashMap::new(),
39        }
40    }
41
42    async fn put(&mut self, key: Key, value: Vec<u8>) -> Result<()> {
43        self.storage.insert(key, value);
44        Ok(())
45    }
46
47    async fn get(&self, key: &Key) -> Result<Vec<u8>> {
48        self.storage
49            .get(key)
50            .cloned()
51            .ok_or_else(|| anyhow::anyhow!("Key not found"))
52    }
53}
54
55// Global DHT instance for testing
56static DHT: once_cell::sync::Lazy<Arc<RwLock<MockDht>>> =
57    once_cell::sync::Lazy::new(|| Arc::new(RwLock::new(MockDht::new())));
58
59// Optional global DHT client (real engine). If not set, we fall back to MockDht.
60static GLOBAL_DHT_CLIENT: once_cell::sync::OnceCell<Arc<crate::dht::client::DhtClient>> =
61    once_cell::sync::OnceCell::new();
62
63/// Install a process-global DHT client for API operations.
64pub fn set_dht_client(client: crate::dht::client::DhtClient) -> bool {
65    GLOBAL_DHT_CLIENT.set(Arc::new(client)).is_ok()
66}
67
68fn get_dht_client() -> Option<Arc<crate::dht::client::DhtClient>> {
69    GLOBAL_DHT_CLIENT.get().cloned()
70}
71
72async fn dht_put_bytes(key: &Key, value: Vec<u8>) -> Result<()> {
73    if let Some(client) = get_dht_client() {
74        let k = hex::encode(key.as_bytes());
75        let _ = client
76            .put(k, value)
77            .await
78            .context("Failed to store data in DHT client")?;
79        Ok(())
80    } else {
81        let mut dht = DHT.write().await;
82        dht.put(key.clone(), value).await
83    }
84}
85
86async fn dht_get_bytes(key: &Key) -> Result<Vec<u8>> {
87    if let Some(client) = get_dht_client() {
88        let k = hex::encode(key.as_bytes());
89        match client.get(k).await.context("DHT get failed")? {
90            Some(v) => Ok(v),
91            None => anyhow::bail!("Key not found"),
92        }
93    } else {
94        let dht = DHT.read().await;
95        dht.get(key).await
96    }
97}
98
99// =============================================================================
100// API-visible record types (minimal, per AGENTS_API.md)
101// =============================================================================
102
103/// Minimal identity packet compatible with Communitas group flows
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct IdentityPacketV1 {
106    pub v: u8,
107    pub words: [String; 4],
108    pub id: Key,
109    pub pk: Vec<u8>,
110    pub sig: Option<Vec<u8>>, // optional when registered locally
111    pub device_set_root: Key,
112}
113
114/// Member reference for group identities
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct MemberRef {
117    pub member_id: Key,
118    pub member_pk: Vec<u8>,
119}
120
121/// Group identity packet (canonical)
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct GroupIdentityPacketV1 {
124    pub v: u8,
125    pub words: [String; 4],
126    pub id: Key,
127    pub group_pk: Vec<u8>,
128    pub group_sig: Vec<u8>,
129    pub members: Vec<MemberRef>,
130    pub membership_root: Key,
131    pub created_at: u64,
132    pub mls_ciphersuite: Option<u16>,
133}
134
135/// Keypair for group signatures
136#[derive(Clone)]
137pub struct GroupKeyPair {
138    pub group_pk: crate::quantum_crypto::MlDsaPublicKey,
139    pub group_sk: crate::quantum_crypto::MlDsaSecretKey,
140}
141
142impl std::fmt::Debug for GroupKeyPair {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        write!(
145            f,
146            "GroupKeyPair {{ group_pk: <{} bytes>, group_sk: <hidden> }}",
147            self.group_pk.as_bytes().len()
148        )
149    }
150}
151
152// ============================================================================
153// IDENTITY API
154// ============================================================================
155
156/// Register a new identity on the network
157///
158/// # Arguments
159/// * `words` - Four-word identifier (must be valid dictionary words)
160/// * `keypair` - ML-DSA keypair for signing
161///
162/// # Returns
163/// * `IdentityHandle` - Handle for identity operations
164pub async fn register_identity(words: [&str; 4], keypair: &MlDsaKeyPair) -> Result<IdentityHandle> {
165    // Convert to owned strings
166    let words_owned: [String; 4] = [
167        words[0].to_string(),
168        words[1].to_string(),
169        words[2].to_string(),
170        words[3].to_string(),
171    ];
172
173    // Validate words
174    if !fw_check(words_owned.clone()) {
175        anyhow::bail!("Invalid word in identity");
176    }
177
178    // Generate key from words
179    let key = fw_to_key(words_owned.clone())?;
180
181    // Check if already registered
182    let dht = DHT.read().await;
183    if dht.get(&key).await.is_ok() {
184        anyhow::bail!("Identity already registered");
185    }
186    drop(dht);
187
188    // Create identity (typed) and store packet for compatibility
189    let identity = Identity {
190        words: words_owned.clone(),
191        key: key.clone(),
192        public_key: keypair.public_key.clone(),
193    };
194
195    let packet = IdentityPacketV1 {
196        v: 1,
197        words: words_owned.clone(),
198        id: key.clone(),
199        pk: keypair.public_key.clone(),
200        sig: None,
201        device_set_root: compute_key("device-set", key.as_bytes()),
202    };
203
204    dht_put_bytes(&key, serde_json::to_vec(&packet)?).await?;
205
206    Ok(IdentityHandle::new(identity, keypair.clone()))
207}
208
209/// Get an identity by its key
210///
211/// # Arguments
212/// * `key` - Identity key (derived from four-word address)
213///
214/// # Returns
215/// * `Identity` - The identity information
216pub async fn get_identity(key: Key) -> Result<Identity> {
217    // Try to read the identity packet and map back to Identity struct
218    let data = dht_get_bytes(&key).await.context("Identity not found")?;
219    if let Ok(pkt) = serde_json::from_slice::<IdentityPacketV1>(&data) {
220        let identity = Identity {
221            words: pkt.words,
222            key: pkt.id,
223            public_key: pkt.pk,
224        };
225        return Ok(identity);
226    }
227    // Fallback: legacy storage of Identity
228    let identity: Identity = serde_json::from_slice(&data)?;
229    Ok(identity)
230}
231
232/// Fetch identity packet in canonical format
233pub async fn identity_fetch(key: Key) -> Result<IdentityPacketV1> {
234    let data = dht_get_bytes(&key).await.context("Identity not found")?;
235    let pkt: IdentityPacketV1 = serde_json::from_slice(&data)?;
236    Ok(pkt)
237}
238
239// ============================================================================
240// PRESENCE API
241// ============================================================================
242
243/// Register presence on the network
244///
245/// # Arguments
246/// * `handle` - Identity handle
247/// * `devices` - List of devices for this identity
248/// * `active_device` - Currently active device ID
249///
250/// # Returns
251/// * `PresenceReceipt` - Receipt of presence registration
252pub async fn register_presence(
253    handle: &IdentityHandle,
254    devices: Vec<Device>,
255    active_device: DeviceId,
256) -> Result<PresenceReceipt> {
257    // Validate active device is in list
258    if !devices.iter().any(|d| d.id == active_device) {
259        anyhow::bail!("Active device not in device list");
260    }
261
262    // Create presence packet
263    let presence = Presence {
264        identity: handle.key(),
265        devices,
266        active_device: Some(active_device),
267        timestamp: std::time::SystemTime::now()
268            .duration_since(std::time::UNIX_EPOCH)?
269            .as_secs(),
270        signature: vec![], // Will be filled
271    };
272
273    // Sign presence
274    let presence_bytes = serde_json::to_vec(&presence)?;
275    let signature = handle.sign(&presence_bytes)?;
276
277    let mut signed_presence = presence;
278    signed_presence.signature = signature;
279
280    // Store in DHT with presence key
281    let presence_key = derive_presence_key(handle.key());
282    let mut dht = DHT.write().await;
283    dht.put(presence_key, serde_json::to_vec(&signed_presence)?)
284        .await?;
285
286    // Create receipt
287    let receipt = PresenceReceipt {
288        identity: handle.key(),
289        timestamp: signed_presence.timestamp,
290        storing_nodes: vec![Key::from([0u8; 32])], // Mock node
291    };
292
293    Ok(receipt)
294}
295
296/// Get presence information for an identity
297///
298/// # Arguments
299/// * `identity_key` - Key of the identity
300///
301/// # Returns
302/// * `Presence` - Current presence information
303pub async fn get_presence(identity_key: Key) -> Result<Presence> {
304    let presence_key = derive_presence_key(identity_key);
305    let dht = DHT.read().await;
306    let data = dht.get(&presence_key).await.context("Presence not found")?;
307    let presence: Presence = serde_json::from_slice(&data)?;
308    Ok(presence)
309}
310
311/// Register a headless storage node
312///
313/// # Arguments
314/// * `handle` - Identity handle
315/// * `storage_gb` - Storage capacity in GB
316/// * `endpoint` - Network endpoint
317///
318/// # Returns
319/// * `DeviceId` - ID of the registered headless node
320pub async fn register_headless(
321    handle: &IdentityHandle,
322    storage_gb: u32,
323    endpoint: Endpoint,
324) -> Result<DeviceId> {
325    // Get current presence
326    let mut presence = get_presence(handle.key()).await?;
327
328    // Create headless device
329    let device = Device {
330        id: DeviceId::generate(),
331        device_type: crate::types::presence::DeviceType::Headless,
332        storage_gb: storage_gb as u64,
333        endpoint,
334        capabilities: crate::types::presence::DeviceCapabilities {
335            storage_bytes: storage_gb as u64 * 1_000_000_000,
336            always_online: true,
337            supports_fec: true,
338            supports_seal: true,
339            ..Default::default()
340        },
341    };
342
343    let device_id = device.id;
344    presence.devices.push(device);
345
346    // Update presence
347    let active = presence.active_device.unwrap_or(device_id);
348    register_presence(handle, presence.devices, active).await?;
349
350    Ok(device_id)
351}
352
353/// Set the active device for an identity
354///
355/// # Arguments
356/// * `handle` - Identity handle
357/// * `device_id` - Device to make active
358pub async fn set_active_device(handle: &IdentityHandle, device_id: DeviceId) -> Result<()> {
359    // Get current presence
360    let presence = get_presence(handle.key()).await?;
361
362    // Validate device exists
363    if !presence.devices.iter().any(|d| d.id == device_id) {
364        anyhow::bail!("Device not found in presence");
365    }
366
367    // Update with new active device
368    register_presence(handle, presence.devices, device_id).await?;
369    Ok(())
370}
371
372// ============================================================================
373// STORAGE API
374// ============================================================================
375
376/// Store data on the network
377///
378/// # Arguments
379/// * `handle` - Identity handle
380/// * `data` - Data to store
381/// * `group_size` - Size of the group (affects storage strategy)
382///
383/// # Returns
384/// * `StorageHandle` - Handle to retrieve the data
385pub async fn store_data(
386    handle: &IdentityHandle,
387    data: Vec<u8>,
388    group_size: usize,
389) -> Result<StorageHandle> {
390    // Select strategy based on group size
391    let strategy = StorageStrategy::from_group_size(group_size);
392
393    match strategy {
394        StorageStrategy::Direct => store_direct(handle, data).await,
395        StorageStrategy::FullReplication { replicas } => {
396            store_replicated(handle, data, replicas).await
397        }
398        StorageStrategy::FecEncoded {
399            data_shards,
400            parity_shards,
401            ..
402        } => store_with_fec(handle, data, data_shards, parity_shards).await,
403    }
404}
405
406/// Store data for a dyad (2-person group)
407///
408/// # Arguments
409/// * `handle1` - First identity handle
410/// * `handle2_key` - Key of second identity
411/// * `data` - Data to store
412///
413/// # Returns
414/// * `StorageHandle` - Handle to retrieve the data
415pub async fn store_dyad(
416    handle1: &IdentityHandle,
417    _handle2_key: Key,
418    data: Vec<u8>,
419) -> Result<StorageHandle> {
420    // For dyads, use full replication (2 copies)
421    store_replicated(handle1, data, 2).await
422}
423
424/// Store data with custom FEC parameters
425///
426/// # Arguments
427/// * `handle` - Identity handle
428/// * `data` - Data to store
429/// * `data_shards` - Number of data shards (k)
430/// * `parity_shards` - Number of parity shards (m)
431///
432/// # Returns
433/// * `StorageHandle` - Handle to retrieve the data
434pub async fn store_with_fec(
435    handle: &IdentityHandle,
436    data: Vec<u8>,
437    data_shards: usize,
438    parity_shards: usize,
439) -> Result<StorageHandle> {
440    // Generate storage ID
441    let storage_id = Key::from(*blake3::hash(&data).as_bytes());
442
443    // TODO: Actual FEC encoding with saorsa-fec
444    // For now, just store the data directly
445
446    // Create shard map (mock)
447    let mut shard_map = crate::types::storage::ShardMap::new();
448
449    // Get presence to find devices
450    let presence = get_presence(handle.key()).await?;
451
452    // Prefer headless nodes for storage
453    let mut devices = presence.devices.clone();
454    devices.sort_by_key(|d| match d.device_type {
455        crate::types::presence::DeviceType::Headless => 0,
456        crate::types::presence::DeviceType::Active => 1,
457        crate::types::presence::DeviceType::Mobile => 2,
458    });
459
460    // Assign shards to devices
461    let total_shards = data_shards + parity_shards;
462    for (i, device) in devices.iter().take(total_shards).enumerate() {
463        shard_map.assign_shard(device.id, i as u32);
464    }
465
466    // Store data in DHT
467    let mut dht = DHT.write().await;
468    dht.put(storage_id.clone(), data.clone()).await?;
469
470    // Create storage handle
471    let handle = StorageHandle {
472        id: storage_id,
473        size: data.len() as u64,
474        strategy: StorageStrategy::FecEncoded {
475            data_shards,
476            parity_shards,
477            shard_size: 65536,
478        },
479        shard_map,
480        sealed_key: Some(vec![0u8; 32]), // Mock sealed key
481    };
482
483    Ok(handle)
484}
485
486/// Retrieve data from the network
487///
488/// # Arguments
489/// * `handle` - Storage handle
490///
491/// # Returns
492/// * `Vec<u8>` - The retrieved data
493pub async fn get_data(handle: &StorageHandle) -> Result<Vec<u8>> {
494    // TODO: Handle different strategies (FEC decoding, unsealing, etc.)
495    // For now, just retrieve from DHT
496
497    let dht = DHT.read().await;
498    let data = dht.get(&handle.id).await.context("Data not found")?;
499    Ok(data)
500}
501
502// ============================================================================
503// HELPER FUNCTIONS
504// ============================================================================
505
506/// Derive presence key from identity key
507fn derive_presence_key(identity_key: Key) -> Key {
508    let mut hasher = blake3::Hasher::new();
509    hasher.update(b"presence:");
510    hasher.update(identity_key.as_bytes());
511    Key::from(*hasher.finalize().as_bytes())
512}
513
514/// Store data directly (no redundancy)
515async fn store_direct(handle: &IdentityHandle, data: Vec<u8>) -> Result<StorageHandle> {
516    let storage_id = Key::from(*blake3::hash(&data).as_bytes());
517
518    // Store in DHT
519    let mut dht = DHT.write().await;
520    dht.put(storage_id.clone(), data.clone()).await?;
521
522    // Get single device
523    let presence = get_presence(handle.key()).await?;
524    let device = presence.devices.first().context("No devices available")?;
525
526    let mut shard_map = crate::types::storage::ShardMap::new();
527    shard_map.assign_shard(device.id, 0);
528
529    Ok(StorageHandle {
530        id: storage_id,
531        size: data.len() as u64,
532        strategy: StorageStrategy::Direct,
533        shard_map,
534        sealed_key: Some(vec![0u8; 32]),
535    })
536}
537
538// ============================================================================
539// GROUP API (per AGENTS_API.md, minimal subset used by Communitas)
540// ============================================================================
541
542/// Canonical bytes for group identity signing: b"saorsa-group:identity:v1" || id || membership_root
543pub fn group_identity_canonical_sign_bytes(id: &Key, membership_root: &Key) -> Vec<u8> {
544    let mut out = Vec::with_capacity(16 + 32 + 32);
545    out.extend_from_slice(b"saorsa-group:identity:v1");
546    out.extend_from_slice(id.as_bytes());
547    out.extend_from_slice(membership_root.as_bytes());
548    out
549}
550
551fn compute_membership_root(members: &[MemberRef]) -> Key {
552    let mut ids: Vec<[u8; 32]> = members.iter().map(|m| *m.member_id.as_bytes()).collect();
553    ids.sort_unstable();
554    let mut hasher = blake3::Hasher::new();
555    for id in ids {
556        hasher.update(&id);
557    }
558    Key::from(*hasher.finalize().as_bytes())
559}
560
561/// Create a canonical group identity and keypair
562pub fn group_identity_create(
563    words: [String; 4],
564    members: Vec<MemberRef>,
565) -> Result<(GroupIdentityPacketV1, GroupKeyPair)> {
566    // Validate words and id
567    if !fw_check(words.clone()) {
568        anyhow::bail!("Invalid group words");
569    }
570    let id = fw_to_key(words.clone())?;
571
572    // Generate ML-DSA group keypair
573    use crate::quantum_crypto::{MlDsa65, MlDsaOperations};
574    let ml = MlDsa65::new();
575    let (group_pk, group_sk) = ml
576        .generate_keypair()
577        .map_err(|e| anyhow::anyhow!("group keypair generation failed: {e:?}"))?;
578
579    // Compute membership root and sign canonical bytes
580    let membership_root = compute_membership_root(&members);
581    let msg = group_identity_canonical_sign_bytes(&id, &membership_root);
582    let sig = ml
583        .sign(&group_sk, &msg)
584        .map_err(|e| anyhow::anyhow!("group sign failed: {e:?}"))?;
585
586    let pkt = GroupIdentityPacketV1 {
587        v: 1,
588        words,
589        id: id.clone(),
590        group_pk: group_pk.as_bytes().to_vec(),
591        group_sig: sig.0.to_vec(),
592        members,
593        membership_root,
594        created_at: std::time::SystemTime::now()
595            .duration_since(std::time::UNIX_EPOCH)
596            .unwrap_or_default()
597            .as_secs(),
598        mls_ciphersuite: None,
599    };
600
601    Ok((pkt, GroupKeyPair { group_pk, group_sk }))
602}
603
604/// Publish a group identity packet under its id key
605pub async fn group_identity_publish(packet: GroupIdentityPacketV1) -> Result<()> {
606    // Basic validation: recompute root and signature check
607    let root = compute_membership_root(&packet.members);
608    if root != packet.membership_root {
609        anyhow::bail!("membership_root mismatch");
610    }
611    // Verify signature
612    use crate::quantum_crypto::{MlDsa65, MlDsaOperations, MlDsaPublicKey, MlDsaSignature};
613    const SIG_LEN: usize = 3309;
614    if packet.group_sig.len() != SIG_LEN {
615        anyhow::bail!("invalid signature length");
616    }
617    let mut sig_arr = [0u8; SIG_LEN];
618    sig_arr.copy_from_slice(&packet.group_sig);
619    let sig = MlDsaSignature(Box::new(sig_arr));
620    let pk = MlDsaPublicKey::from_bytes(&packet.group_pk)
621        .map_err(|_| anyhow::anyhow!("invalid group_pk"))?;
622    let ml = MlDsa65::new();
623    let msg = group_identity_canonical_sign_bytes(&packet.id, &packet.membership_root);
624    let ok = ml
625        .verify(&pk, &msg, &sig)
626        .map_err(|e| anyhow::anyhow!("verify failed: {e:?}"))?;
627    if !ok {
628        anyhow::bail!("group signature invalid");
629    }
630    dht_put_bytes(&packet.id, serde_json::to_vec(&packet)?).await
631}
632
633/// Fetch a group identity by id key
634pub async fn group_identity_fetch(id_key: Key) -> Result<GroupIdentityPacketV1> {
635    let data = dht_get_bytes(&id_key).await.context("Group not found")?;
636    let pkt: GroupIdentityPacketV1 = serde_json::from_slice(&data)?;
637    Ok(pkt)
638}
639
640/// Update group members with signature verification over canonical bytes
641pub async fn group_identity_update_members_signed(
642    id_key: Key,
643    new_members: Vec<MemberRef>,
644    group_pk: Vec<u8>,
645    group_sig: Sig,
646) -> Result<()> {
647    // Compute new root and verify signature
648    let new_root = compute_membership_root(&new_members);
649    use crate::quantum_crypto::{MlDsa65, MlDsaOperations, MlDsaPublicKey, MlDsaSignature};
650    const SIG_LEN: usize = 3309;
651    let sig_bytes = group_sig.as_bytes();
652    if sig_bytes.len() != SIG_LEN {
653        anyhow::bail!("invalid signature length");
654    }
655    let mut sig_arr = [0u8; SIG_LEN];
656    sig_arr.copy_from_slice(sig_bytes);
657    let sig = MlDsaSignature(Box::new(sig_arr));
658    let pk =
659        MlDsaPublicKey::from_bytes(&group_pk).map_err(|_| anyhow::anyhow!("invalid group_pk"))?;
660    let ml = MlDsa65::new();
661    let msg = group_identity_canonical_sign_bytes(&id_key, &new_root);
662    let ok = ml
663        .verify(&pk, &msg, &sig)
664        .map_err(|e| anyhow::anyhow!("verify failed: {e:?}"))?;
665    if !ok {
666        anyhow::bail!("group signature invalid");
667    }
668
669    // Fetch current (if exists) to preserve metadata
670    let mut pkt = match group_identity_fetch(id_key.clone()).await {
671        Ok(p) => p,
672        Err(_) => GroupIdentityPacketV1 {
673            v: 1,
674            words: [String::new(), String::new(), String::new(), String::new()],
675            id: id_key.clone(),
676            group_pk: group_pk.clone(),
677            group_sig: sig.0.clone().to_vec(),
678            members: Vec::new(),
679            membership_root: new_root.clone(),
680            created_at: std::time::SystemTime::now()
681                .duration_since(std::time::UNIX_EPOCH)
682                .unwrap_or_default()
683                .as_secs(),
684            mls_ciphersuite: None,
685        },
686    };
687
688    pkt.members = new_members;
689    pkt.membership_root = new_root;
690    pkt.group_pk = group_pk;
691    pkt.group_sig = sig.0.to_vec();
692
693    group_identity_publish(pkt).await
694}
695
696/// Store data with full replication
697async fn store_replicated(
698    handle: &IdentityHandle,
699    data: Vec<u8>,
700    replicas: usize,
701) -> Result<StorageHandle> {
702    let storage_id = Key::from(*blake3::hash(&data).as_bytes());
703
704    // Store in DHT
705    let mut dht = DHT.write().await;
706    dht.put(storage_id.clone(), data.clone()).await?;
707
708    // Get devices for replicas
709    let presence = get_presence(handle.key()).await?;
710    let mut shard_map = crate::types::storage::ShardMap::new();
711
712    for (i, device) in presence.devices.iter().take(replicas).enumerate() {
713        shard_map.assign_shard(device.id, i as u32);
714    }
715
716    Ok(StorageHandle {
717        id: storage_id,
718        size: data.len() as u64,
719        strategy: StorageStrategy::FullReplication { replicas },
720        shard_map,
721        sealed_key: Some(vec![0u8; 32]),
722    })
723}
724
725// =============================================================================
726// ENTITY API
727// =============================================================================
728
729use crate::entities::{
730    ENTITY_REGISTRY, Entity, EntityHandle, EntityId, EntityInfo, EntitySettings, EntityType,
731    FourWordAddress, create_entity as internal_create_entity,
732};
733
734/// Create a new entity (Individual, Group, Channel, Project, Organization)
735///
736/// # Arguments
737/// * `entity_type` - Type of entity to create
738/// * `name` - Human-readable name for the entity
739/// * `four_words` - Four-word address for the entity
740/// * `description` - Optional description
741/// * `settings` - Optional custom settings
742///
743/// # Returns
744/// * `EntityHandle` - Handle to the created entity
745///
746/// # Example
747/// ```
748/// let handle = create_entity(
749///     EntityType::Individual,
750///     "Alice",
751///     ["eagle", "forest", "river", "mountain"],
752///     Some("Alice's personal entity"),
753///     None,
754/// ).await?;
755/// ```
756pub async fn create_entity(
757    entity_type: EntityType,
758    name: &str,
759    four_words: [&str; 4],
760    description: Option<String>,
761    settings: Option<EntitySettings>,
762) -> Result<EntityHandle> {
763    // Convert four words to owned strings
764    let words = [
765        four_words[0].to_string(),
766        four_words[1].to_string(),
767        four_words[2].to_string(),
768        four_words[3].to_string(),
769    ];
770
771    // Validate four-word address
772    if !fw_check(words.clone()) {
773        anyhow::bail!("Invalid four-word address");
774    }
775
776    // Create entity
777    let mut entity = internal_create_entity(entity_type, name.to_string(), words).await?;
778
779    // Apply optional settings
780    if let Some(desc) = description {
781        entity.core.metadata.description = Some(desc);
782    }
783    if let Some(custom_settings) = settings {
784        entity.core.metadata.settings = custom_settings;
785    }
786
787    // Register entity
788    ENTITY_REGISTRY.register(entity).await
789}
790
791/// Get an entity by its ID
792///
793/// # Arguments
794/// * `id` - Entity ID
795///
796/// # Returns
797/// * `Option<Arc<Entity>>` - The entity if found
798pub async fn get_entity(id: &EntityId) -> Option<Arc<Entity>> {
799    ENTITY_REGISTRY.get(id).await
800}
801
802/// Get an entity by its four-word address
803///
804/// # Arguments
805/// * `four_words` - Four-word address array
806///
807/// # Returns
808/// * `Option<Arc<Entity>>` - The entity if found
809pub async fn get_entity_by_address(four_words: [&str; 4]) -> Option<Arc<Entity>> {
810    let words = [
811        four_words[0].to_string(),
812        four_words[1].to_string(),
813        four_words[2].to_string(),
814        four_words[3].to_string(),
815    ];
816
817    match FourWordAddress::from_words(words) {
818        Ok(address) => ENTITY_REGISTRY.get_by_address(&address).await,
819        Err(_) => None,
820    }
821}
822
823/// List all entities in the registry
824///
825/// # Returns
826/// * `Vec<EntityInfo>` - List of entity information
827pub async fn list_entities() -> Vec<EntityInfo> {
828    ENTITY_REGISTRY.list().await
829}
830
831/// Write data to an entity's virtual disk
832///
833/// # Arguments
834/// * `entity_handle` - Handle to the entity
835/// * `path` - Path within the virtual disk
836/// * `content` - Data to write
837/// * `is_public` - Whether to write to public or private disk
838///
839/// # Returns
840/// * `Result<()>` - Success or error
841pub async fn entity_disk_write(
842    entity_handle: &EntityHandle,
843    path: &str,
844    content: Vec<u8>,
845    is_public: bool,
846) -> Result<()> {
847    use crate::virtual_disk::{FileMetadata, disk_write};
848
849    let disk = if is_public {
850        entity_handle.public_disk()
851    } else {
852        entity_handle.private_disk()
853    };
854
855    let metadata = FileMetadata {
856        mime_type: Some("application/octet-stream".to_string()),
857        attributes: HashMap::new(),
858        permissions: 0o644,
859    };
860
861    disk_write(disk, path, &content, metadata).await?;
862    Ok(())
863}
864
865/// Read data from an entity's virtual disk
866///
867/// # Arguments
868/// * `entity_handle` - Handle to the entity
869/// * `path` - Path within the virtual disk
870/// * `is_public` - Whether to read from public or private disk
871///
872/// # Returns
873/// * `Result<Vec<u8>>` - The file content
874pub async fn entity_disk_read(
875    entity_handle: &EntityHandle,
876    path: &str,
877    is_public: bool,
878) -> Result<Vec<u8>> {
879    use crate::virtual_disk::disk_read;
880
881    let disk = if is_public {
882        entity_handle.public_disk()
883    } else {
884        entity_handle.private_disk()
885    };
886
887    disk_read(disk, path).await
888}
889
890/// List files in an entity's virtual disk
891///
892/// # Arguments
893/// * `entity_handle` - Handle to the entity
894/// * `path` - Directory path to list
895/// * `recursive` - Whether to list recursively
896/// * `is_public` - Whether to list public or private disk
897///
898/// # Returns
899/// * `Result<Vec<FileEntry>>` - List of files
900pub async fn entity_disk_list(
901    entity_handle: &EntityHandle,
902    path: &str,
903    recursive: bool,
904    is_public: bool,
905) -> Result<Vec<crate::virtual_disk::FileEntry>> {
906    use crate::virtual_disk::disk_list;
907
908    let disk = if is_public {
909        entity_handle.public_disk()
910    } else {
911        entity_handle.private_disk()
912    };
913
914    disk_list(disk, path, recursive).await
915}
916
917/// Enable or disable website publishing for an entity
918///
919/// # Arguments
920/// * `entity_handle` - Handle to the entity
921/// * `enabled` - Whether to enable website publishing
922///
923/// # Returns
924/// * `Result<()>` - Success or error
925pub async fn entity_set_website(_entity_handle: &EntityHandle, _enabled: bool) -> Result<()> {
926    // TODO: This would update the entity's settings in the registry
927    // Need to implement a proper update mechanism that modifies
928    // the entity in the registry and persists changes to DHT
929
930    // For now, just return Ok as a placeholder
931    Ok(())
932}
933
934/// Get the website URL for an entity (if enabled)
935///
936/// # Arguments
937/// * `entity_handle` - Handle to the entity
938///
939/// # Returns
940/// * `Option<String>` - The website URL if enabled
941pub fn entity_website_url(entity_handle: &EntityHandle) -> Option<String> {
942    entity_handle.website_url()
943}