refactor: update dependencies and remove unused code

- Added new dependencies: `adler2`, `crc32fast`, `flate2`, `miniz_oxide`, and `libredox`.
- Updated existing dependencies: `tokio-rustls` to version 0.26.4 and `filetime` to version 0.2.27.
- Removed the `backup.rs` file as it is no longer needed.
- Introduced tests for configuration and credential management.
- Enhanced the `identity` module to generate W3C compliant DID documents.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-12 00:19:30 +00:00
parent fd2a837bea
commit f07ce10b1a
347 changed files with 18703 additions and 46785 deletions

View File

@@ -38,6 +38,7 @@ impl RpcHandler {
pub(super) async fn handle_auth_change_password(
&self,
params: Option<serde_json::Value>,
session_token: &Option<String>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let current_password = params
@@ -57,7 +58,12 @@ impl RpcHandler {
.change_password(current_password, new_password, also_change_ssh)
.await?;
Ok(serde_json::json!({ "success": true }))
// Session rotation: invalidate all other sessions, rotate the caller's session
if let Some(token) = session_token {
self.session_store.invalidate_all_except(token).await;
}
Ok(serde_json::json!({ "success": true, "session_rotated": true }))
}
pub(super) async fn handle_auth_onboarding_complete(&self) -> Result<serde_json::Value> {

View File

@@ -57,13 +57,15 @@ impl RpcHandler {
)
.await?;
let status = if credentials::is_revoked(&vc) { "revoked" } else { "active" };
Ok(serde_json::json!({
"id": vc.id,
"issuer": vc.issuer,
"subject": vc.subject,
"subject": vc.credential_subject.id,
"type": vc.credential_type,
"issued_at": vc.issued_at,
"status": vc.status,
"issued_at": vc.issuance_date,
"status": status,
}))
}
@@ -97,10 +99,12 @@ impl RpcHandler {
})
})?;
let status = if credentials::is_revoked(vc) { "revoked" } else { "active" };
Ok(serde_json::json!({
"id": vc.id,
"valid": valid,
"status": vc.status,
"status": status,
}))
}
@@ -118,15 +122,17 @@ impl RpcHandler {
let items: Vec<serde_json::Value> = creds
.into_iter()
.map(|c| {
let status = if credentials::is_revoked(&c) { "revoked" } else { "active" };
serde_json::json!({
"@context": c.context,
"id": c.id,
"issuer": c.issuer,
"subject": c.subject,
"type": c.credential_type,
"claims": c.claims,
"issued_at": c.issued_at,
"expires_at": c.expires_at,
"status": c.status,
"issuer": c.issuer,
"credentialSubject": c.credential_subject,
"issuanceDate": c.issuance_date,
"expirationDate": c.expiration_date,
"proof": c.proof,
"status": status,
})
})
.collect();
@@ -147,4 +153,86 @@ impl RpcHandler {
credentials::revoke_credential(&self.config.data_dir, id).await?;
Ok(serde_json::json!({ "ok": true }))
}
/// Create a Verifiable Presentation bundling selected credentials.
pub(super) async fn handle_identity_create_presentation(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let holder_id = params
.get("holder_id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing holder_id"))?;
let credential_ids: Vec<&str> = params
.get("credential_ids")
.and_then(|v| v.as_array())
.ok_or_else(|| anyhow::anyhow!("Missing credential_ids array"))?
.iter()
.filter_map(|v| v.as_str())
.collect();
if credential_ids.is_empty() {
return Err(anyhow::anyhow!("credential_ids must not be empty"));
}
let manager = IdentityManager::new(&self.config.data_dir).await?;
let holder_record = manager.get(holder_id).await?;
let holder_did = holder_record.did.clone();
let store = credentials::load_credentials(&self.config.data_dir).await?;
let data_dir = self.config.data_dir.clone();
let sign_id = holder_id.to_string();
let vp = credentials::create_presentation(
&holder_did,
&credential_ids,
&store.credentials,
|bytes| {
let hex_msg = hex::encode(bytes);
tokio::task::block_in_place(|| {
let rt = tokio::runtime::Handle::current();
rt.block_on(async {
let mgr = IdentityManager::new(&data_dir).await?;
mgr.sign(&sign_id, hex_msg.as_bytes()).await
})
})
},
)?;
Ok(serde_json::to_value(&vp)?)
}
/// Verify a Verifiable Presentation: check holder proof and all embedded credentials.
pub(super) async fn handle_identity_verify_presentation(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let presentation = params
.get("presentation")
.ok_or_else(|| anyhow::anyhow!("Missing presentation"))?;
let vp: credentials::VerifiablePresentation =
serde_json::from_value(presentation.clone())?;
let data_dir = self.config.data_dir.clone();
let result = credentials::verify_presentation(&vp, |did, bytes, signature| {
let hex_msg = hex::encode(bytes);
tokio::task::block_in_place(|| {
let rt = tokio::runtime::Handle::current();
rt.block_on(async {
let mgr = IdentityManager::new(&data_dir).await?;
mgr.verify(did, hex_msg.as_bytes(), signature).await
})
})
})?;
Ok(serde_json::json!({
"valid": result.valid,
"holder_valid": result.holder_valid,
"credentials": result.credentials,
}))
}
}

View File

@@ -1,4 +1,5 @@
use super::RpcHandler;
use crate::network::dwn_store::{DwnStore, MessageQuery, ProtocolDefinition};
use crate::network::dwn_sync;
use crate::peers;
use anyhow::Result;
@@ -12,13 +13,18 @@ impl RpcHandler {
version: String::new(),
});
let store = DwnStore::new(&self.config.data_dir).await?;
let stats = store.stats().await?;
Ok(serde_json::json!({
"running": server_status.running,
"version": server_status.version,
"sync_status": sync_state.status,
"last_sync": sync_state.last_sync,
"messages_synced": sync_state.messages_synced,
"storage_bytes": sync_state.storage_bytes,
"storage_bytes": stats.total_bytes,
"message_count": stats.message_count,
"protocol_count": stats.protocol_count,
"registered_protocols": sync_state.registered_protocols,
"peer_sync_targets": sync_state.peer_sync_targets,
}))
@@ -26,7 +32,6 @@ impl RpcHandler {
/// Trigger DWN sync with connected peers.
pub(super) async fn handle_dwn_sync(&self) -> Result<serde_json::Value> {
// Get list of connected peers' onion addresses
let peer_list = peers::load_peers(&self.config.data_dir).await?;
let onions: Vec<String> = peer_list
.iter()
@@ -42,4 +47,97 @@ impl RpcHandler {
"messages_synced": state.messages_synced,
}))
}
/// Register a DWN protocol.
pub(super) async fn handle_dwn_register_protocol(
&self,
params: &serde_json::Value,
) -> Result<serde_json::Value> {
let protocol = params["protocol"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing 'protocol' parameter"))?;
let published = params["published"].as_bool().unwrap_or(false);
let definition = ProtocolDefinition {
protocol: protocol.to_string(),
published,
types: params
.get("types")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default(),
structure: params
.get("structure")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default(),
date_registered: chrono::Utc::now().to_rfc3339(),
};
let store = DwnStore::new(&self.config.data_dir).await?;
store.register_protocol(&definition).await?;
Ok(serde_json::json!({"registered": true, "protocol": protocol}))
}
/// List registered DWN protocols.
pub(super) async fn handle_dwn_list_protocols(&self) -> Result<serde_json::Value> {
let store = DwnStore::new(&self.config.data_dir).await?;
let protocols = store.list_protocols().await?;
Ok(serde_json::json!({"protocols": protocols}))
}
/// Remove a DWN protocol.
pub(super) async fn handle_dwn_remove_protocol(
&self,
params: &serde_json::Value,
) -> Result<serde_json::Value> {
let protocol = params["protocol"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing 'protocol' parameter"))?;
let store = DwnStore::new(&self.config.data_dir).await?;
let removed = store.remove_protocol(protocol).await?;
Ok(serde_json::json!({"removed": removed, "protocol": protocol}))
}
/// Query DWN messages.
pub(super) async fn handle_dwn_query_messages(
&self,
params: &serde_json::Value,
) -> Result<serde_json::Value> {
let query = MessageQuery {
protocol: params["protocol"].as_str().map(|s| s.to_string()),
schema: params["schema"].as_str().map(|s| s.to_string()),
author: params["author"].as_str().map(|s| s.to_string()),
date_from: params["dateFrom"].as_str().map(|s| s.to_string()),
date_to: params["dateTo"].as_str().map(|s| s.to_string()),
limit: params["limit"].as_u64().map(|n| n as usize),
};
let store = DwnStore::new(&self.config.data_dir).await?;
let messages = store.query_messages(&query).await?;
Ok(serde_json::json!({"messages": messages, "count": messages.len()}))
}
/// Write a DWN message.
pub(super) async fn handle_dwn_write_message(
&self,
params: &serde_json::Value,
) -> Result<serde_json::Value> {
let author = params["author"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing 'author' parameter"))?;
let protocol = params["protocol"].as_str();
let schema = params["schema"].as_str();
let data_format = params["dataFormat"].as_str();
let data = params.get("data").cloned();
let store = DwnStore::new(&self.config.data_dir).await?;
let message = store
.write_message(author, protocol, schema, data_format, data)
.await?;
Ok(serde_json::json!({"written": true, "record_id": message.record_id}))
}
}

View File

@@ -0,0 +1,326 @@
use super::RpcHandler;
use crate::federation::{self, FederatedNode, TrustLevel};
use crate::identity;
use anyhow::Result;
use tracing::info;
impl RpcHandler {
/// federation.invite — Generate an invite code containing our DID + onion for a peer.
pub(super) async fn handle_federation_invite(&self) -> Result<serde_json::Value> {
let (data, _) = self.state_manager.get_snapshot().await;
let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
let onion = data
.server_info
.tor_address
.clone()
.unwrap_or_default();
let pubkey = data.server_info.pubkey.clone();
if onion.is_empty() {
anyhow::bail!("Tor address not available. Tor may not be running.");
}
let code = federation::create_invite(&self.config.data_dir, &did, &onion, &pubkey).await?;
info!(did = %did, "Generated federation invite");
Ok(serde_json::json!({
"code": code,
"did": did,
"onion": onion,
}))
}
/// federation.join — Accept an invite code and establish federation with the remote node.
pub(super) async fn handle_federation_join(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let code = params
.get("code")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'code' parameter"))?;
let (data, _) = self.state_manager.get_snapshot().await;
let local_did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
let local_onion = data.server_info.tor_address.clone().unwrap_or_default();
let local_pubkey = data.server_info.pubkey.clone();
let node = federation::accept_invite(
&self.config.data_dir,
code,
&local_did,
&local_onion,
&local_pubkey,
)
.await?;
info!(peer_did = %node.did, "Joined federation with peer");
Ok(serde_json::json!({
"joined": true,
"node": {
"did": node.did,
"onion": node.onion,
"pubkey": node.pubkey,
"trust_level": node.trust_level.to_string(),
}
}))
}
/// federation.list-nodes — List all federated nodes with their status and last state.
pub(super) async fn handle_federation_list_nodes(&self) -> Result<serde_json::Value> {
let nodes = federation::load_nodes(&self.config.data_dir).await?;
let nodes_json: Vec<serde_json::Value> = nodes
.iter()
.map(|n| {
let mut obj = serde_json::json!({
"did": n.did,
"pubkey": n.pubkey,
"onion": n.onion,
"trust_level": n.trust_level.to_string(),
"added_at": n.added_at,
});
if let Some(name) = &n.name {
obj["name"] = serde_json::json!(name);
}
if let Some(last_seen) = &n.last_seen {
obj["last_seen"] = serde_json::json!(last_seen);
}
if let Some(state) = &n.last_state {
obj["last_state"] = serde_json::to_value(state).unwrap_or_default();
}
obj
})
.collect();
Ok(serde_json::json!({ "nodes": nodes_json }))
}
/// federation.remove-node — Remove a node from the federation by DID.
pub(super) async fn handle_federation_remove_node(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let did = params
.get("did")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'did' parameter"))?;
let nodes = federation::remove_node(&self.config.data_dir, did).await?;
info!(did = %did, "Removed node from federation");
Ok(serde_json::json!({
"removed": true,
"nodes_remaining": nodes.len(),
}))
}
/// federation.set-trust — Change trust level for a federated node.
pub(super) async fn handle_federation_set_trust(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let did = params
.get("did")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'did' parameter"))?;
let trust_str = params
.get("trust_level")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'trust_level' parameter"))?;
let trust = match trust_str {
"trusted" => TrustLevel::Trusted,
"observer" => TrustLevel::Observer,
"untrusted" => TrustLevel::Untrusted,
_ => anyhow::bail!("Invalid trust level: {} (expected trusted/observer/untrusted)", trust_str),
};
federation::set_trust_level(&self.config.data_dir, did, trust).await?;
Ok(serde_json::json!({
"updated": true,
"did": did,
"trust_level": trust.to_string(),
}))
}
/// federation.sync-state — Manually trigger state sync with all federated peers.
pub(super) async fn handle_federation_sync_state(&self) -> Result<serde_json::Value> {
let nodes = federation::load_nodes(&self.config.data_dir).await?;
if nodes.is_empty() {
return Ok(serde_json::json!({
"synced": 0,
"failed": 0,
"results": [],
}));
}
let (data, _) = self.state_manager.get_snapshot().await;
let local_did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
let identity_dir = self.config.data_dir.join("identity");
let node_identity = identity::NodeIdentity::load_or_create(&identity_dir).await?;
let mut synced = 0u32;
let mut failed = 0u32;
let mut results = Vec::new();
for node in &nodes {
if node.trust_level == TrustLevel::Untrusted {
continue;
}
let did_clone = local_did.clone();
match federation::sync_with_peer(
&self.config.data_dir,
node,
&did_clone,
|bytes| node_identity.sign(bytes),
)
.await
{
Ok(state) => {
synced += 1;
results.push(serde_json::json!({
"did": node.did,
"status": "ok",
"apps": state.apps.len(),
}));
}
Err(e) => {
failed += 1;
results.push(serde_json::json!({
"did": node.did,
"status": "error",
"error": e.to_string(),
}));
}
}
}
Ok(serde_json::json!({
"synced": synced,
"failed": failed,
"results": results,
}))
}
/// federation.get-state — Return this node's state snapshot (called by peers during sync).
pub(super) async fn handle_federation_get_state(&self) -> Result<serde_json::Value> {
let (data, _) = self.state_manager.get_snapshot().await;
// Build app statuses from package_data
let apps: Vec<federation::AppStatus> = data
.package_data
.iter()
.map(|(id, pkg)| federation::AppStatus {
id: id.clone(),
status: format!("{:?}", pkg.state).to_lowercase(),
version: Some(pkg.manifest.version.clone()),
})
.collect();
let tor_active = data.server_info.tor_address.is_some();
let state = federation::build_local_state(
apps, 0.0, 0, 0, 0, 0, 0, tor_active,
);
Ok(serde_json::to_value(&state)?)
}
/// federation.peer-joined — Called by a remote peer after they accept our invite.
pub(super) async fn handle_federation_peer_joined(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let did = params
.get("did")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'did'"))?;
let onion = params
.get("onion")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'onion'"))?;
let pubkey = params
.get("pubkey")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'pubkey'"))?;
let nodes = federation::load_nodes(&self.config.data_dir).await?;
if nodes.iter().any(|n| n.did == did) {
return Ok(serde_json::json!({ "accepted": true, "already_known": true }));
}
let node = FederatedNode {
did: did.to_string(),
pubkey: pubkey.to_string(),
onion: onion.to_string(),
name: None,
trust_level: TrustLevel::Trusted,
added_at: chrono::Utc::now().to_rfc3339(),
last_seen: None,
last_state: None,
};
federation::add_node(&self.config.data_dir, node).await?;
info!(peer_did = %did, "Peer joined our federation");
Ok(serde_json::json!({ "accepted": true }))
}
/// federation.deploy-app — Deploy an app to a remote federated node.
pub(super) async fn handle_federation_deploy_app(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let peer_did = params
.get("did")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'did' (target node)"))?;
let app_id = params
.get("app_id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'app_id'"))?;
let version = params
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("latest");
let marketplace_url = params
.get("marketplace_url")
.and_then(|v| v.as_str())
.unwrap_or("");
let nodes = federation::load_nodes(&self.config.data_dir).await?;
let peer = nodes
.iter()
.find(|n| n.did == peer_did)
.ok_or_else(|| anyhow::anyhow!("No federated node with DID {}", peer_did))?;
let (data, _) = self.state_manager.get_snapshot().await;
let local_did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
let identity_dir = self.config.data_dir.join("identity");
let node_identity = identity::NodeIdentity::load_or_create(&identity_dir).await?;
let result = federation::deploy_to_peer(
peer,
app_id,
version,
marketplace_url,
&local_did,
|bytes| node_identity.sign(bytes),
)
.await?;
info!(app = %app_id, peer = %peer_did, "Deployed app to federated peer");
Ok(result)
}
}

View File

@@ -0,0 +1,138 @@
use super::RpcHandler;
use crate::{nostr_handshake, peers};
use anyhow::Result;
impl RpcHandler {
/// Discover nodes (presence-only — returns Nostr pubkeys + DIDs, no onion addresses).
pub(super) async fn handle_handshake_discover(&self) -> Result<serde_json::Value> {
let identity_dir = self.config.data_dir.join("identity");
let nodes = nostr_handshake::discover_nodes(
&identity_dir,
&self.config.nostr_relays,
self.config.nostr_tor_proxy.as_deref(),
)
.await?;
Ok(serde_json::json!({ "nodes": nodes }))
}
/// Send encrypted connection request to a peer's Nostr pubkey.
/// Params: { recipient_nostr_pubkey }
pub(super) async fn handle_handshake_connect(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let recipient = params
.get("recipient_nostr_pubkey")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing recipient_nostr_pubkey"))?;
let (data, _) = self.state_manager.get_snapshot().await;
let our_onion = data
.server_info
.tor_address
.as_deref()
.ok_or_else(|| anyhow::anyhow!("No Tor address available — is Tor running?"))?;
let our_node_pubkey = &data.server_info.pubkey;
let our_did = crate::identity::did_key_from_pubkey_hex(our_node_pubkey)
.unwrap_or_default();
let our_version = &data.server_info.version;
let our_name = data.server_info.name.as_deref();
let identity_dir = self.config.data_dir.join("identity");
nostr_handshake::send_connect_request(
&identity_dir,
recipient,
our_onion,
our_node_pubkey,
&our_did,
our_version,
our_name,
&self.config.nostr_relays,
self.config.nostr_tor_proxy.as_deref(),
)
.await?;
Ok(serde_json::json!({ "ok": true, "sent_to": recipient }))
}
/// Poll for incoming encrypted handshake messages (connect requests/responses).
/// Auto-adds peers and auto-responds to requests.
pub(super) async fn handle_handshake_poll(&self) -> Result<serde_json::Value> {
let identity_dir = self.config.data_dir.join("identity");
let handshakes = nostr_handshake::poll_handshakes(
&identity_dir,
&self.config.nostr_relays,
self.config.nostr_tor_proxy.as_deref(),
None, // TODO: track last-seen timestamp to avoid re-processing
)
.await?;
let (data, _) = self.state_manager.get_snapshot().await;
let mut added_peers = Vec::new();
for hs in &handshakes {
let (onion, node_pubkey, name) = match &hs.message {
nostr_handshake::HandshakeMessage::ConnectRequest {
onion,
node_pubkey,
name,
..
} => {
// Auto-respond with our details
if let Some(our_onion) = data.server_info.tor_address.as_deref() {
let our_did = crate::identity::did_key_from_pubkey_hex(
&data.server_info.pubkey,
)
.unwrap_or_default();
let _ = nostr_handshake::send_connect_response(
&identity_dir,
&hs.from_nostr_pubkey,
our_onion,
&data.server_info.pubkey,
&our_did,
&data.server_info.version,
data.server_info.name.as_deref(),
&self.config.nostr_relays,
self.config.nostr_tor_proxy.as_deref(),
)
.await;
}
(onion.clone(), node_pubkey.clone(), name.clone())
}
nostr_handshake::HandshakeMessage::ConnectResponse {
onion,
node_pubkey,
name,
..
} => (onion.clone(), node_pubkey.clone(), name.clone()),
};
// Auto-add as peer
let peer = peers::KnownPeer {
onion,
pubkey: node_pubkey.clone(),
name,
added_at: Some(chrono::Utc::now().to_rfc3339()),
};
let _ = peers::add_peer(&self.config.data_dir, peer).await;
added_peers.push(node_pubkey);
}
let serialized: Vec<serde_json::Value> = handshakes
.iter()
.map(|hs| {
serde_json::json!({
"from_nostr_pubkey": hs.from_nostr_pubkey,
"message": hs.message,
"timestamp": hs.timestamp,
})
})
.collect();
Ok(serde_json::json!({
"handshakes": serialized,
"added_peers": added_peers,
}))
}
}

View File

@@ -2,7 +2,7 @@
use super::RpcHandler;
use crate::identity_manager::{IdentityManager, IdentityPurpose};
use anyhow::Result;
use anyhow::{Context, Result};
impl RpcHandler {
/// List all identities with their default status.
@@ -180,6 +180,101 @@ impl RpcHandler {
Ok(serde_json::json!({ "valid": valid }))
}
/// Resolve a DID to its W3C DID Document.
/// If no DID is provided, returns the node's own DID Document.
pub(super) async fn handle_identity_resolve_did(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.unwrap_or_default();
// If a DID is provided, resolve it; otherwise use the node's DID
let pubkey_hex = if let Some(did) = params.get("did").and_then(|v| v.as_str()) {
// Extract pubkey from did:key format
let pubkey_bytes = crate::identity::pubkey_bytes_from_did_key(did)?;
hex::encode(pubkey_bytes)
} else {
// Use node's own pubkey
let (data, _) = self.state_manager.get_snapshot().await;
data.server_info.pubkey.clone()
};
let document = crate::identity::did_document_from_pubkey_hex(&pubkey_hex)?;
Ok(document)
}
/// Verify a DID Document: validate structure, check key material matches DID.
pub(super) async fn handle_identity_verify_did_document(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let document = params
.get("document")
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: document"))?;
// Validate required fields
let did = document["id"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("DID Document missing 'id' field"))?;
let context = document["@context"]
.as_array()
.ok_or_else(|| anyhow::anyhow!("DID Document missing '@context' array"))?;
let has_did_context = context.iter().any(|c| c.as_str() == Some("https://www.w3.org/ns/did/v1"));
if !has_did_context {
return Ok(serde_json::json!({
"valid": false,
"errors": ["Missing required @context: https://www.w3.org/ns/did/v1"]
}));
}
let verification_methods = document["verificationMethod"]
.as_array()
.ok_or_else(|| anyhow::anyhow!("DID Document missing 'verificationMethod' array"))?;
if verification_methods.is_empty() {
return Ok(serde_json::json!({
"valid": false,
"errors": ["verificationMethod array is empty"]
}));
}
// Verify the DID matches the key material (for did:key method)
let mut errors: Vec<String> = Vec::new();
if did.starts_with("did:key:") {
match crate::identity::pubkey_bytes_from_did_key(did) {
Ok(pubkey_bytes) => {
// Check that at least one verification method has matching key
let pubkey_multibase = format!("z{}", bs58::encode(&pubkey_bytes).into_string());
let has_matching_key = verification_methods.iter().any(|vm| {
vm["publicKeyMultibase"].as_str() == Some(&pubkey_multibase)
});
if !has_matching_key {
errors.push("No verificationMethod matches the DID's public key".to_string());
}
}
Err(e) => {
errors.push(format!("Failed to extract pubkey from DID: {}", e));
}
}
}
// Check authentication is present
if document["authentication"].as_array().map_or(true, |a| a.is_empty()) {
errors.push("Missing or empty 'authentication' field".to_string());
}
Ok(serde_json::json!({
"valid": errors.is_empty(),
"did": did,
"errors": errors,
"verification_methods": verification_methods.len(),
}))
}
/// Create a Nostr keypair linked to an identity.
pub(super) async fn handle_identity_create_nostr_key(
&self,
@@ -221,4 +316,81 @@ impl RpcHandler {
"signature": signature,
}))
}
/// Resolve a remote peer's DID Document over Tor.
/// Queries the peer's /rpc/ endpoint for identity.resolve-did.
pub(super) async fn handle_identity_resolve_remote_did(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let onion = params
.get("onion")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: onion"))?;
// Build URL for peer's RPC endpoint over Tor
let host = if onion.ends_with(".onion") {
onion.to_string()
} else {
format!("{}.onion", onion)
};
let url = format!("http://{}/rpc/", host);
// Use SOCKS5 proxy to reach .onion address
let proxy = reqwest::Proxy::all("socks5h://127.0.0.1:9050")
.context("Failed to create Tor proxy")?;
let client = reqwest::Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(30))
.build()
.context("Failed to build HTTP client")?;
let rpc_body = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "identity.resolve-did",
"params": {}
});
let resp = client
.post(&url)
.json(&rpc_body)
.send()
.await
.context("Failed to connect to peer over Tor")?;
let body: serde_json::Value = resp
.json()
.await
.context("Failed to parse peer response")?;
// Extract the DID Document from the RPC response
let document = body
.get("result")
.ok_or_else(|| anyhow::anyhow!("Peer returned error or missing result"))?;
// Cache the resolved DID locally
let did = document["id"]
.as_str()
.unwrap_or("unknown");
let cache_dir = self.config.data_dir.join("did-cache");
tokio::fs::create_dir_all(&cache_dir).await.ok();
let cache_file = cache_dir.join(format!("{}.json", onion.replace('.', "_")));
let cache_entry = serde_json::json!({
"document": document,
"resolved_at": chrono::Utc::now().to_rfc3339(),
"onion": onion,
});
tokio::fs::write(&cache_file, serde_json::to_string_pretty(&cache_entry).unwrap_or_default())
.await
.ok();
Ok(serde_json::json!({
"document": document,
"did": did,
"resolved_from": onion,
"cached": true,
}))
}
}

View File

@@ -0,0 +1,128 @@
use super::RpcHandler;
use crate::federation;
use crate::marketplace;
use crate::nostr_relays;
use anyhow::Result;
use tracing::info;
impl RpcHandler {
/// marketplace.discover — Query Nostr relays for community app manifests.
pub(super) async fn handle_marketplace_discover(&self) -> Result<serde_json::Value> {
// Load enabled relays
let relay_store = nostr_relays::load_relays(&self.config.data_dir).await?;
let relay_urls: Vec<String> = relay_store
.relays
.iter()
.filter(|r| r.enabled)
.map(|r| r.url.clone())
.collect();
// Load federated DIDs for trust scoring
let fed_nodes = federation::load_nodes(&self.config.data_dir).await.unwrap_or_default();
let federated_dids: Vec<String> = fed_nodes.iter().map(|n| n.did.clone()).collect();
let tor_proxy = std::env::var("ARCHIPELAGO_NOSTR_TOR_PROXY").ok();
let apps = marketplace::discover(
&self.config.data_dir,
&relay_urls,
tor_proxy.as_deref(),
&federated_dids,
)
.await?;
Ok(serde_json::json!({
"apps": apps,
"relay_count": relay_urls.len(),
}))
}
/// marketplace.publish — Publish an app manifest to Nostr relays.
pub(super) async fn handle_marketplace_publish(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let manifest: marketplace::AppManifest =
serde_json::from_value(params).map_err(|e| anyhow::anyhow!("Invalid manifest: {}", e))?;
// Validate before publishing
let issues = marketplace::validate_manifest(&manifest);
if !issues.is_empty() {
return Ok(serde_json::json!({
"ok": false,
"errors": issues,
}));
}
let relay_store = nostr_relays::load_relays(&self.config.data_dir).await?;
let relay_urls: Vec<String> = relay_store
.relays
.iter()
.filter(|r| r.enabled)
.map(|r| r.url.clone())
.collect();
let tor_proxy = std::env::var("ARCHIPELAGO_NOSTR_TOR_PROXY").ok();
let event_id = marketplace::publish(
&self.config.data_dir,
&manifest,
&relay_urls,
tor_proxy.as_deref(),
)
.await?;
info!(app_id = %manifest.app_id, "Published app manifest");
Ok(serde_json::json!({
"ok": true,
"event_id": event_id,
"app_id": manifest.app_id,
"relays": relay_urls.len(),
}))
}
/// marketplace.get-manifest — Get cached manifest for a specific app.
pub(super) async fn handle_marketplace_get_manifest(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let app_id = params
.get("app_id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: app_id"))?;
let cache = marketplace::load_cache(&self.config.data_dir).await?;
let app = cache.apps.iter().find(|a| a.manifest.app_id == app_id);
match app {
Some(discovered) => Ok(serde_json::to_value(discovered)?),
None => Ok(serde_json::json!({ "error": "App not found in cache", "app_id": app_id })),
}
}
/// marketplace.list-published — List manifests published by this node.
pub(super) async fn handle_marketplace_list_published(&self) -> Result<serde_json::Value> {
let manifests = marketplace::list_published(&self.config.data_dir).await?;
Ok(serde_json::json!({ "manifests": manifests }))
}
/// marketplace.verify — Verify a manifest's security compliance.
pub(super) async fn handle_marketplace_verify(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let manifest: marketplace::AppManifest =
serde_json::from_value(params).map_err(|e| anyhow::anyhow!("Invalid manifest: {}", e))?;
let issues = marketplace::validate_manifest(&manifest);
let (trust_score, trust_tier) = marketplace::calculate_trust_score(&manifest, 0, &[]);
Ok(serde_json::json!({
"valid": issues.is_empty(),
"issues": issues,
"trust_score": trust_score,
"trust_tier": trust_tier,
}))
}
}

View File

@@ -0,0 +1,90 @@
use super::RpcHandler;
use crate::{identity, mesh};
use anyhow::Result;
use tracing::info;
impl RpcHandler {
/// mesh.status — Get mesh radio status and detected devices.
pub(super) async fn handle_mesh_status(&self) -> Result<serde_json::Value> {
let config = mesh::load_config(&self.config.data_dir).await?;
let devices = mesh::detect_meshtastic_devices().await;
Ok(serde_json::json!({
"enabled": config.enabled,
"device_path": config.device_path,
"channel_name": config.channel_name,
"broadcast_identity": config.broadcast_identity,
"detected_devices": devices,
}))
}
/// mesh.discover — Discover nodes via mesh radio.
pub(super) async fn handle_mesh_discover(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let device_path = params
.as_ref()
.and_then(|p| p.get("device_path"))
.and_then(|v| v.as_str());
let config = mesh::load_config(&self.config.data_dir).await?;
let effective_device = device_path.or(config.device_path.as_deref());
let nodes = mesh::discover_nodes(effective_device).await?;
Ok(serde_json::json!({
"nodes": nodes,
"count": nodes.len(),
}))
}
/// mesh.broadcast — Broadcast our node identity over mesh.
pub(super) async fn handle_mesh_broadcast(&self) -> Result<serde_json::Value> {
let config = mesh::load_config(&self.config.data_dir).await?;
if !config.enabled {
anyhow::bail!("Mesh networking is not enabled. Configure it first.");
}
let (data, _) = self.state_manager.get_snapshot().await;
let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
let pubkey = &data.server_info.pubkey;
mesh::broadcast_identity(&did, pubkey, config.device_path.as_deref()).await?;
info!("Broadcast identity over mesh");
Ok(serde_json::json!({ "broadcast": true }))
}
/// mesh.configure — Enable/disable mesh and set device path.
pub(super) async fn handle_mesh_configure(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let mut config = mesh::load_config(&self.config.data_dir).await?;
if let Some(enabled) = params.get("enabled").and_then(|v| v.as_bool()) {
config.enabled = enabled;
}
if let Some(device) = params.get("device_path").and_then(|v| v.as_str()) {
config.device_path = Some(device.to_string());
}
if let Some(channel) = params.get("channel_name").and_then(|v| v.as_str()) {
config.channel_name = Some(channel.to_string());
}
if let Some(broadcast) = params.get("broadcast_identity").and_then(|v| v.as_bool()) {
config.broadcast_identity = broadcast;
}
mesh::save_config(&self.config.data_dir, &config).await?;
info!("Mesh config updated");
Ok(serde_json::json!({
"configured": true,
"enabled": config.enabled,
"device_path": config.device_path,
}))
}
}

View File

@@ -6,6 +6,7 @@ mod content;
mod credentials;
mod dwn;
mod federation;
mod handshake;
mod identity;
mod interfaces;
mod marketplace;
@@ -297,6 +298,11 @@ impl RpcHandler {
"node.nostr-pubkey" => self.handle_node_nostr_pubkey().await,
"node-nostr-verify-revoked" => self.handle_node_nostr_verify_revoked().await,
// Encrypted peer handshake (NIP-44)
"handshake.discover" => self.handle_handshake_discover().await,
"handshake.connect" => self.handle_handshake_connect(params).await,
"handshake.poll" => self.handle_handshake_poll().await,
// TOTP 2FA
"auth.totp.setup.begin" => self.handle_totp_setup_begin(params).await,
"auth.totp.setup.confirm" => self.handle_totp_setup_confirm(params).await,

View File

@@ -4,7 +4,6 @@ use crate::data_model::{
};
use crate::port_allocator::PortAllocator;
use anyhow::{Context, Result};
use std::collections::HashMap;
use tokio::io::{AsyncBufReadExt, BufReader};
use tracing::{debug, info};
@@ -752,15 +751,50 @@ printtoconsole=1\n";
let containers_to_remove = get_containers_for_app(package_id).await?;
if containers_to_remove.is_empty() {
tracing::warn!("Uninstall {}: no containers found", package_id);
}
let mut stopped = 0u32;
let mut removed = 0u32;
let mut errors = Vec::new();
for name in &containers_to_remove {
let _ = tokio::process::Command::new("sudo")
.args(["podman", "stop", name])
tracing::info!("Uninstall {}: stopping container {}", package_id, name);
let stop_out = tokio::process::Command::new("sudo")
.args(["podman", "stop", "-t", "10", name])
.output()
.await;
let _ = tokio::process::Command::new("sudo")
match stop_out {
Ok(o) if o.status.success() => stopped += 1,
Ok(o) => {
let stderr = String::from_utf8_lossy(&o.stderr);
tracing::warn!("Uninstall {}: stop {} failed: {}", package_id, name, stderr.trim());
}
Err(e) => {
tracing::warn!("Uninstall {}: stop {} error: {}", package_id, name, e);
}
}
tracing::info!("Uninstall {}: removing container {}", package_id, name);
let rm_out = tokio::process::Command::new("sudo")
.args(["podman", "rm", "-f", name])
.output()
.await;
match rm_out {
Ok(o) if o.status.success() => removed += 1,
Ok(o) => {
let stderr = String::from_utf8_lossy(&o.stderr);
let msg = format!("Failed to remove {}: {}", name, stderr.trim());
tracing::error!("Uninstall {}: {}", package_id, msg);
errors.push(msg);
}
Err(e) => {
let msg = format!("Failed to remove {}: {}", name, e);
tracing::error!("Uninstall {}: {}", package_id, msg);
errors.push(msg);
}
}
}
// Release port allocation
@@ -772,14 +806,31 @@ printtoconsole=1\n";
if !preserve_data {
let data_dirs = get_data_dirs_for_app(package_id);
for dir in &data_dirs {
let _ = tokio::process::Command::new("sudo")
tracing::info!("Uninstall {}: removing data {}", package_id, dir);
let rm_out = tokio::process::Command::new("sudo")
.args(["rm", "-rf", dir])
.output()
.await;
if let Ok(o) = rm_out {
if !o.status.success() {
tracing::warn!("Uninstall {}: rm {} failed", package_id, dir);
}
}
}
}
Ok(serde_json::json!({ "status": "uninstalled" }))
if !errors.is_empty() {
tracing::error!("Uninstall {} completed with errors: {:?}", package_id, errors);
} else {
tracing::info!("Uninstall {} complete: stopped={}, removed={}", package_id, stopped, removed);
}
Ok(serde_json::json!({
"status": if errors.is_empty() { "uninstalled" } else { "partial" },
"stopped": stopped,
"removed": removed,
"errors": errors,
}))
}
/// Start a bundled app (create container from pre-loaded image if needed, then start)

View File

@@ -43,6 +43,101 @@ impl RpcHandler {
Ok(serde_json::json!({ "temperatures": temps }))
}
/// system.detect-usb-devices — scan for known hardware wallet USB devices
pub(super) async fn handle_system_detect_usb_devices(&self) -> Result<serde_json::Value> {
debug!("Scanning for USB hardware wallets");
let devices = detect_usb_hardware_wallets().await.unwrap_or_default();
Ok(serde_json::json!({ "devices": devices }))
}
/// system.disk-status — Disk usage with warning/critical thresholds.
pub(super) async fn handle_system_disk_status(&self) -> Result<serde_json::Value> {
let (used, total) = read_disk_usage().await.unwrap_or((0, 0));
let percent = if total > 0 {
(used as f64 / total as f64) * 100.0
} else {
0.0
};
let percent_rounded = (percent * 10.0).round() / 10.0;
let level = if percent >= 90.0 {
"critical"
} else if percent >= 85.0 {
"warning"
} else {
"ok"
};
Ok(serde_json::json!({
"used_bytes": used,
"total_bytes": total,
"free_bytes": total.saturating_sub(used),
"used_percent": percent_rounded,
"level": level,
}))
}
/// system.disk-cleanup — Remove old container images, stale logs, and temp files.
pub(super) async fn handle_system_disk_cleanup(&self) -> Result<serde_json::Value> {
tracing::info!("Starting disk cleanup");
let mut freed_bytes: u64 = 0;
let mut actions: Vec<String> = Vec::new();
// 1. Prune dangling container images
match prune_container_images().await {
Ok(bytes) => {
if bytes > 0 {
freed_bytes += bytes;
actions.push(format!("Pruned dangling images: {} freed", format_bytes(bytes)));
}
}
Err(e) => actions.push(format!("Image prune failed: {}", e)),
}
// 2. Clean old log files (> 30 days)
match clean_old_logs(30).await {
Ok(bytes) => {
if bytes > 0 {
freed_bytes += bytes;
actions.push(format!("Cleaned old logs: {} freed", format_bytes(bytes)));
}
}
Err(e) => actions.push(format!("Log cleanup failed: {}", e)),
}
// 3. Remove stale temp files
match clean_temp_files().await {
Ok(bytes) => {
if bytes > 0 {
freed_bytes += bytes;
actions.push(format!("Removed temp files: {} freed", format_bytes(bytes)));
}
}
Err(e) => actions.push(format!("Temp cleanup failed: {}", e)),
}
// 4. Prune container build cache
match prune_build_cache().await {
Ok(bytes) => {
if bytes > 0 {
freed_bytes += bytes;
actions.push(format!("Pruned build cache: {} freed", format_bytes(bytes)));
}
}
Err(e) => actions.push(format!("Build cache prune failed: {}", e)),
}
tracing::info!("Disk cleanup complete: {} freed ({} actions)", format_bytes(freed_bytes), actions.len());
Ok(serde_json::json!({
"freed_bytes": freed_bytes,
"freed_human": format_bytes(freed_bytes),
"actions": actions,
}))
}
}
/// Read system uptime from /proc/uptime (seconds since boot).
@@ -226,6 +321,203 @@ async fn read_top_processes() -> Result<Vec<serde_json::Value>> {
Ok(procs)
}
/// Known hardware wallet USB vendor IDs.
const KNOWN_HW_WALLETS: &[(u16, &str)] = &[
(0xd13e, "ColdCard"),
(0x534c, "Trezor"),
(0x2c97, "Ledger"),
(0x1209, "BitBox02"),
];
/// Scan /sys/bus/usb/devices/ for known hardware wallet vendor IDs.
async fn detect_usb_hardware_wallets() -> Result<Vec<serde_json::Value>> {
let usb_dir = std::path::Path::new("/sys/bus/usb/devices");
if !usb_dir.exists() {
return Ok(Vec::new());
}
let mut devices = Vec::new();
let mut entries = tokio::fs::read_dir(usb_dir)
.await
.context("Failed to read /sys/bus/usb/devices")?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
let vendor_path = path.join("idVendor");
let product_path = path.join("idProduct");
if !vendor_path.exists() {
continue;
}
let vid_str = match tokio::fs::read_to_string(&vendor_path).await {
Ok(s) => s.trim().to_string(),
Err(_) => continue,
};
let vid = match u16::from_str_radix(&vid_str, 16) {
Ok(v) => v,
Err(_) => continue,
};
if let Some((_, name)) = KNOWN_HW_WALLETS.iter().find(|(known_vid, _)| *known_vid == vid) {
let pid_str = tokio::fs::read_to_string(&product_path)
.await
.map(|s| s.trim().to_string())
.unwrap_or_default();
let manufacturer = tokio::fs::read_to_string(path.join("manufacturer"))
.await
.map(|s| s.trim().to_string())
.unwrap_or_default();
let product = tokio::fs::read_to_string(path.join("product"))
.await
.map(|s| s.trim().to_string())
.unwrap_or_default();
devices.push(serde_json::json!({
"type": name,
"vendor_id": vid_str,
"product_id": pid_str,
"manufacturer": manufacturer,
"product": product,
"path": path.to_string_lossy(),
}));
}
}
Ok(devices)
}
/// Prune dangling container images via `sudo podman image prune -f`.
/// Returns estimated bytes freed.
async fn prune_container_images() -> Result<u64> {
let output = tokio::process::Command::new("sudo")
.args(["podman", "image", "prune", "-f"])
.output()
.await
.context("Failed to run podman image prune")?;
if !output.status.success() {
anyhow::bail!(
"podman image prune failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
// Podman outputs image IDs, estimate ~100MB per pruned image
let stdout = String::from_utf8_lossy(&output.stdout);
let pruned_count = stdout.lines().filter(|l| !l.trim().is_empty()).count();
Ok(pruned_count as u64 * 100_000_000) // rough estimate
}
/// Prune container build cache via `sudo podman system prune -f`.
async fn prune_build_cache() -> Result<u64> {
// Just prune volumes and build cache (not containers or images — those are handled above)
let output = tokio::process::Command::new("sudo")
.args(["podman", "volume", "prune", "-f"])
.output()
.await
.context("Failed to run podman volume prune")?;
if !output.status.success() {
anyhow::bail!(
"podman volume prune failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let pruned_count = stdout.lines().filter(|l| !l.trim().is_empty()).count();
Ok(pruned_count as u64 * 10_000_000) // rough estimate per volume
}
/// Clean log files older than `max_age_days` from common log directories.
async fn clean_old_logs(max_age_days: u64) -> Result<u64> {
let output = tokio::process::Command::new("sudo")
.args([
"find",
"/var/log",
"-type",
"f",
"-name",
"*.log.*",
"-mtime",
&format!("+{}", max_age_days),
"-delete",
"-print",
])
.output()
.await
.context("Failed to clean old logs")?;
let stdout = String::from_utf8_lossy(&output.stdout);
let deleted_count = stdout.lines().filter(|l| !l.trim().is_empty()).count();
// Also clean rotated/compressed logs
let _ = tokio::process::Command::new("sudo")
.args([
"find",
"/var/log",
"-type",
"f",
"-name",
"*.gz",
"-mtime",
&format!("+{}", max_age_days),
"-delete",
])
.output()
.await;
Ok(deleted_count as u64 * 500_000) // rough estimate per log file
}
/// Remove stale temp files from /tmp and /var/tmp.
async fn clean_temp_files() -> Result<u64> {
let mut freed = 0u64;
for dir in &["/tmp", "/var/tmp"] {
let output = tokio::process::Command::new("sudo")
.args([
"find",
dir,
"-type",
"f",
"-mtime",
"+7",
"-delete",
"-print",
])
.output()
.await;
if let Ok(out) = output {
let stdout = String::from_utf8_lossy(&out.stdout);
let count = stdout.lines().filter(|l| !l.trim().is_empty()).count();
freed += count as u64 * 100_000; // rough estimate per temp file
}
}
Ok(freed)
}
fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.0} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
/// Read temperatures from /sys/class/thermal/thermal_zone*/temp.
async fn read_temperatures() -> Result<Vec<serde_json::Value>> {
let mut temps = Vec::new();

View File

@@ -42,4 +42,52 @@ impl RpcHandler {
update::dismiss_update(&self.config.data_dir).await?;
Ok(serde_json::json!({ "ok": true }))
}
/// Download the available update to staging.
pub(super) async fn handle_update_download(&self) -> Result<serde_json::Value> {
let progress = update::download_update(&self.config.data_dir).await?;
Ok(serde_json::json!({
"total_bytes": progress.total_bytes,
"downloaded_bytes": progress.downloaded_bytes,
"components_downloaded": progress.components_downloaded,
}))
}
/// Apply the staged update.
pub(super) async fn handle_update_apply(&self) -> Result<serde_json::Value> {
update::apply_update(&self.config.data_dir).await?;
Ok(serde_json::json!({ "applied": true, "restart_required": true }))
}
/// Rollback to the previous version.
pub(super) async fn handle_update_rollback(&self) -> Result<serde_json::Value> {
update::rollback_update(&self.config.data_dir).await?;
Ok(serde_json::json!({ "rolled_back": true, "restart_required": true }))
}
/// Get the current update schedule.
pub(super) async fn handle_update_get_schedule(&self) -> Result<serde_json::Value> {
let schedule = update::get_schedule(&self.config.data_dir).await?;
Ok(serde_json::json!({ "schedule": schedule }))
}
/// Set the update schedule. Params: { schedule: "manual" | "daily_check" | "auto_apply" }
pub(super) async fn handle_update_set_schedule(
&self,
params: &serde_json::Value,
) -> Result<serde_json::Value> {
let schedule_str = params["schedule"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing 'schedule' parameter"))?;
let schedule = match schedule_str {
"manual" => update::UpdateSchedule::Manual,
"daily_check" => update::UpdateSchedule::DailyCheck,
"auto_apply" => update::UpdateSchedule::AutoApply,
_ => anyhow::bail!("Invalid schedule: '{}'. Use manual, daily_check, or auto_apply", schedule_str),
};
update::set_schedule(&self.config.data_dir, schedule).await?;
Ok(serde_json::json!({ "schedule": schedule }))
}
}