archy/core/archipelago/src/identity_manager.rs

920 lines
34 KiB
Rust
Raw Normal View History

//! 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 std::time::Duration;
use tokio::fs;
use crate::identity::did_key_from_pubkey_hex;
2026-03-12 12:56:59 +00:00
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,
2026-03-12 12:56:59 +00:00
/// Nostr secp256k1 public key in hex format
pub nostr_pubkey: Option<String>,
2026-03-12 12:56:59 +00:00
/// 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>,
/// BIP-39 seed derivation index (if created from seed).
#[serde(default, skip_serializing_if = "Option::is_none")]
derivation_index: Option<u32>,
}
pub struct IdentityManager {
identities_dir: PathBuf,
}
fix(nostr): profile publish broadcasts to ALL enabled relays Previously handle_identity_publish_profile defaulted to a single hard-coded relay (ws://localhost:18081) so the user's kind:0 profile event only ever landed on the local relay — hence "Manage Relays shows N connected, but profile edits don't propagate" from testing. Fix — two-layer change: - identity_manager::publish_profile now takes `&[String]` relays instead of one URL. Adds each relay to the nostr-sdk client, gives 15s for handshakes, publishes, then surfaces per-relay accept/reject in a new ProfilePublishOutcome struct so the UI can show WHICH relays accepted vs. rejected and WHY. - RPC handle_identity_publish_profile no longer defaults to the local relay: pulls the ENABLED list from nostr_relays::list_relays (the same table that powers Manage Relays) and publishes to every entry. Accepts an optional `relays: [...]` override for tests. - At-least-one-accept guarantee: if every relay rejects, the call errors instead of silently reporting published=true. User gets a real error message listing the failures. - Response shape: `{event_id, accepted: [urls], rejected: [[url, reason]], relays_attempted: N, published: bool}` so the UI can show a useful status block after clicking Publish. relay_url_matches is tolerant of trailing-slash / case differences since nostr-sdk canonicalises URLs internally. Covers the publishing half of task #29; avatar/banner upload UI is still open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 04:42:25 -04:00
/// Result of a multi-relay profile broadcast.
#[derive(Debug, Clone, serde::Serialize)]
pub struct ProfilePublishOutcome {
pub event_id: String,
pub accepted: Vec<String>,
pub rejected: Vec<(String, String)>,
}
/// Relay URL equality that tolerates minor normalization differences
/// (trailing slash, case). nostr-sdk canonicalises URLs internally and
/// we compare on the surface strings, so be liberal about what matches.
fn relay_url_matches(a: &str, b: &str) -> bool {
let norm = |s: &str| {
s.trim_end_matches('/')
.trim()
.to_ascii_lowercase()
};
norm(a) == norm(b)
}
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();
feat(identity,update): default avatars, public blobs, long-running downloads Follow-up to 1fb71b4b on the same v1.7.0-alpha line. Identity avatars • New module `avatar.rs` generates two deterministic SVG styles keyed off the pubkey: a 5×5 mirrored identicon for sub-identities and a hexagonal-network motif for the master (seed index 0) identity. Both returned as base64 data URLs, so a fresh identity has a recognisable picture before the user uploads anything. • `IdentityManager::create()` and `create_from_seed()` populate `profile.picture` on creation. Index 0 gets the node SVG; all other seed-derived + ad-hoc identities get the identicon. Blob store — public flag for profile assets • `BlobMeta.public` (default false) added; `BlobStore::put()` takes a `public: bool`. Missing in legacy meta files = false. • `POST /api/blob` now stores uploads with public=true and returns `public_url` alongside `self_test_url`. public_url is `http://<node-onion>/blob/<cid>` (no cap) if Tor has published the archipelago hidden service, else falls back to the local path. • `GET /blob/<cid>` bypasses the HMAC capability check when the requested blob is flagged public — external Nostr clients fetching a kind-0 `picture` URL can't hold a cap. • Mesh callers (content_ref attachments, dispatch rehydration) pin public=false explicitly so nothing leaks out of the mesh path. Profile editor UX • Collapsed Save + Save & Publish into one button — the Save action now persists locally AND publishes the kind-0 metadata event in one step. Uploads store `public_url` into `profile.picture` / `profile.banner` so the published URL is reachable by external clients. Update client — the 15-second cliff • Frontend `rpcClient.call` for `update.download` now has an explicit 30-minute timeout (was falling back to the default 15 s). `update.apply` gets 5 min, `update.git-apply` gets 15 min. Matches what the backend is actually willing to wait for. • Backend `load_state()` reconciles `state.current_version` with `CARGO_PKG_VERSION` on every start. Sideloaded or reflashed nodes were stuck advertising the old version even with a new binary in place, which kept re-offering the same release as an update. Manifest changelog rewritten for fleet readers per the saved feedback (no function names, no file paths). Artefacts refreshed: binary 12f838c5…5ba82d 40381864 frontend dc3b63af…e9a8370 76984288 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:03:38 -04:00
// Every new identity gets a deterministic default avatar derived from
// its pubkey. Non-seed identities aren't the master node, so they use
// the 5×5 identicon (never the hexagonal node silhouette).
let default_profile = IdentityProfile {
picture: Some(crate::avatar::identicon(&pubkey_hex)),
..Default::default()
};
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,
feat(identity,update): default avatars, public blobs, long-running downloads Follow-up to 1fb71b4b on the same v1.7.0-alpha line. Identity avatars • New module `avatar.rs` generates two deterministic SVG styles keyed off the pubkey: a 5×5 mirrored identicon for sub-identities and a hexagonal-network motif for the master (seed index 0) identity. Both returned as base64 data URLs, so a fresh identity has a recognisable picture before the user uploads anything. • `IdentityManager::create()` and `create_from_seed()` populate `profile.picture` on creation. Index 0 gets the node SVG; all other seed-derived + ad-hoc identities get the identicon. Blob store — public flag for profile assets • `BlobMeta.public` (default false) added; `BlobStore::put()` takes a `public: bool`. Missing in legacy meta files = false. • `POST /api/blob` now stores uploads with public=true and returns `public_url` alongside `self_test_url`. public_url is `http://<node-onion>/blob/<cid>` (no cap) if Tor has published the archipelago hidden service, else falls back to the local path. • `GET /blob/<cid>` bypasses the HMAC capability check when the requested blob is flagged public — external Nostr clients fetching a kind-0 `picture` URL can't hold a cap. • Mesh callers (content_ref attachments, dispatch rehydration) pin public=false explicitly so nothing leaks out of the mesh path. Profile editor UX • Collapsed Save + Save & Publish into one button — the Save action now persists locally AND publishes the kind-0 metadata event in one step. Uploads store `public_url` into `profile.picture` / `profile.banner` so the published URL is reachable by external clients. Update client — the 15-second cliff • Frontend `rpcClient.call` for `update.download` now has an explicit 30-minute timeout (was falling back to the default 15 s). `update.apply` gets 5 min, `update.git-apply` gets 15 min. Matches what the backend is actually willing to wait for. • Backend `load_state()` reconciles `state.current_version` with `CARGO_PKG_VERSION` on every start. Sideloaded or reflashed nodes were stuck advertising the old version even with a new binary in place, which kept re-offering the same release as an update. Manifest changelog rewritten for fleet readers per the saved feedback (no function names, no file paths). Artefacts refreshed: binary 12f838c5…5ba82d 40381864 frontend dc3b63af…e9a8370 76984288 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:03:38 -04:00
profile: Some(default_profile),
derivation_index: None,
};
let file_path = self.identities_dir.join(format!("{}.json", id));
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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 (legacy path)
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)
}
/// Create a new identity with keys derived from a BIP-39 master seed.
/// The derivation index is auto-incremented and persisted.
pub async fn create_from_seed(
&self,
name: String,
purpose: IdentityPurpose,
seed: &crate::seed::MasterSeed,
data_dir: &std::path::Path,
) -> Result<IdentityRecord> {
let index = crate::seed::load_identity_index(data_dir).await?;
let signing_key = crate::seed::derive_identity_ed25519(seed, index)?;
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();
// Derive Nostr key from the same seed via BIP-32.
let nostr_keys = crate::seed::derive_nostr_identity_key(seed, index)?;
let nostr_secret_hex = nostr_keys.secret_key().display_secret().to_string();
let nostr_pubkey_hex = nostr_keys.public_key().to_hex();
feat(identity,update): default avatars, public blobs, long-running downloads Follow-up to 1fb71b4b on the same v1.7.0-alpha line. Identity avatars • New module `avatar.rs` generates two deterministic SVG styles keyed off the pubkey: a 5×5 mirrored identicon for sub-identities and a hexagonal-network motif for the master (seed index 0) identity. Both returned as base64 data URLs, so a fresh identity has a recognisable picture before the user uploads anything. • `IdentityManager::create()` and `create_from_seed()` populate `profile.picture` on creation. Index 0 gets the node SVG; all other seed-derived + ad-hoc identities get the identicon. Blob store — public flag for profile assets • `BlobMeta.public` (default false) added; `BlobStore::put()` takes a `public: bool`. Missing in legacy meta files = false. • `POST /api/blob` now stores uploads with public=true and returns `public_url` alongside `self_test_url`. public_url is `http://<node-onion>/blob/<cid>` (no cap) if Tor has published the archipelago hidden service, else falls back to the local path. • `GET /blob/<cid>` bypasses the HMAC capability check when the requested blob is flagged public — external Nostr clients fetching a kind-0 `picture` URL can't hold a cap. • Mesh callers (content_ref attachments, dispatch rehydration) pin public=false explicitly so nothing leaks out of the mesh path. Profile editor UX • Collapsed Save + Save & Publish into one button — the Save action now persists locally AND publishes the kind-0 metadata event in one step. Uploads store `public_url` into `profile.picture` / `profile.banner` so the published URL is reachable by external clients. Update client — the 15-second cliff • Frontend `rpcClient.call` for `update.download` now has an explicit 30-minute timeout (was falling back to the default 15 s). `update.apply` gets 5 min, `update.git-apply` gets 15 min. Matches what the backend is actually willing to wait for. • Backend `load_state()` reconciles `state.current_version` with `CARGO_PKG_VERSION` on every start. Sideloaded or reflashed nodes were stuck advertising the old version even with a new binary in place, which kept re-offering the same release as an update. Manifest changelog rewritten for fleet readers per the saved feedback (no function names, no file paths). Artefacts refreshed: binary 12f838c5…5ba82d 40381864 frontend dc3b63af…e9a8370 76984288 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:03:38 -04:00
// Derivation index 0 is the primary seed-derived identity — the
// "master" node identity — and gets the distinctive hexagonal SVG.
// Later indices get the standard identicon.
let default_profile = IdentityProfile {
picture: Some(crate::avatar::default_picture(&pubkey_hex, index == 0)),
..Default::default()
};
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: Some(nostr_secret_hex),
nostr_pubkey_hex: Some(nostr_pubkey_hex),
feat(identity,update): default avatars, public blobs, long-running downloads Follow-up to 1fb71b4b on the same v1.7.0-alpha line. Identity avatars • New module `avatar.rs` generates two deterministic SVG styles keyed off the pubkey: a 5×5 mirrored identicon for sub-identities and a hexagonal-network motif for the master (seed index 0) identity. Both returned as base64 data URLs, so a fresh identity has a recognisable picture before the user uploads anything. • `IdentityManager::create()` and `create_from_seed()` populate `profile.picture` on creation. Index 0 gets the node SVG; all other seed-derived + ad-hoc identities get the identicon. Blob store — public flag for profile assets • `BlobMeta.public` (default false) added; `BlobStore::put()` takes a `public: bool`. Missing in legacy meta files = false. • `POST /api/blob` now stores uploads with public=true and returns `public_url` alongside `self_test_url`. public_url is `http://<node-onion>/blob/<cid>` (no cap) if Tor has published the archipelago hidden service, else falls back to the local path. • `GET /blob/<cid>` bypasses the HMAC capability check when the requested blob is flagged public — external Nostr clients fetching a kind-0 `picture` URL can't hold a cap. • Mesh callers (content_ref attachments, dispatch rehydration) pin public=false explicitly so nothing leaks out of the mesh path. Profile editor UX • Collapsed Save + Save & Publish into one button — the Save action now persists locally AND publishes the kind-0 metadata event in one step. Uploads store `public_url` into `profile.picture` / `profile.banner` so the published URL is reachable by external clients. Update client — the 15-second cliff • Frontend `rpcClient.call` for `update.download` now has an explicit 30-minute timeout (was falling back to the default 15 s). `update.apply` gets 5 min, `update.git-apply` gets 15 min. Matches what the backend is actually willing to wait for. • Backend `load_state()` reconciles `state.current_version` with `CARGO_PKG_VERSION` on every start. Sideloaded or reflashed nodes were stuck advertising the old version even with a new binary in place, which kept re-offering the same release as an update. Manifest changelog rewritten for fleet readers per the saved feedback (no function names, no file paths). Artefacts refreshed: binary 12f838c5…5ba82d 40381864 frontend dc3b63af…e9a8370 76984288 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:03:38 -04:00
profile: Some(default_profile),
derivation_index: Some(index),
};
let file_path = self.identities_dir.join(format!("{}.json", id));
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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")?;
}
// Increment the derivation index for next identity.
crate::seed::save_identity_index(data_dir, index + 1).await?;
// If first identity, make it the default.
let (existing, _) = self.list().await?;
if existing.len() <= 1 {
self.set_default(&id).await?;
}
let record = self.get(&id).await?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
tracing::info!(
"Created seed-derived identity '{}' ({}) at index {}",
name,
purpose,
index
);
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));
}
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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() {
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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();
2026-03-12 12:56:59 +00:00
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")?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
fs::write(&file_path, json.as_bytes())
.await
.context("Failed to write identity file")?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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));
}
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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")?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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(
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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));
}
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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.
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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.
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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.
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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.
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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));
}
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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")?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
fs::write(&file_path, json.as_bytes())
.await
.context("Failed to write identity file")?;
Ok(())
}
fix(nostr): profile publish broadcasts to ALL enabled relays Previously handle_identity_publish_profile defaulted to a single hard-coded relay (ws://localhost:18081) so the user's kind:0 profile event only ever landed on the local relay — hence "Manage Relays shows N connected, but profile edits don't propagate" from testing. Fix — two-layer change: - identity_manager::publish_profile now takes `&[String]` relays instead of one URL. Adds each relay to the nostr-sdk client, gives 15s for handshakes, publishes, then surfaces per-relay accept/reject in a new ProfilePublishOutcome struct so the UI can show WHICH relays accepted vs. rejected and WHY. - RPC handle_identity_publish_profile no longer defaults to the local relay: pulls the ENABLED list from nostr_relays::list_relays (the same table that powers Manage Relays) and publishes to every entry. Accepts an optional `relays: [...]` override for tests. - At-least-one-accept guarantee: if every relay rejects, the call errors instead of silently reporting published=true. User gets a real error message listing the failures. - Response shape: `{event_id, accepted: [urls], rejected: [[url, reason]], relays_attempted: N, published: bool}` so the UI can show a useful status block after clicking Publish. relay_url_matches is tolerant of trailing-slash / case differences since nostr-sdk canonicalises URLs internally. Covers the publishing half of task #29; avatar/banner upload UI is still open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 04:42:25 -04:00
/// Publish kind 0 (metadata) event to one or more Nostr relays.
///
/// Connects all relays in parallel, broadcasts the signed event to
/// every one of them, and reports back the event id plus per-relay
/// acceptance status. At least one successful relay is required —
/// if every relay rejects the event, this returns an error so the
/// UI can surface "publish failed" instead of silently succeeding.
pub async fn publish_profile(
&self,
id: &str,
relay_urls: &[String],
) -> Result<ProfilePublishOutcome> {
let record = self.get(id).await?;
let keys = self.load_nostr_keys(id).await?;
let profile = record.profile.unwrap_or_default();
fix(nostr): profile publish broadcasts to ALL enabled relays Previously handle_identity_publish_profile defaulted to a single hard-coded relay (ws://localhost:18081) so the user's kind:0 profile event only ever landed on the local relay — hence "Manage Relays shows N connected, but profile edits don't propagate" from testing. Fix — two-layer change: - identity_manager::publish_profile now takes `&[String]` relays instead of one URL. Adds each relay to the nostr-sdk client, gives 15s for handshakes, publishes, then surfaces per-relay accept/reject in a new ProfilePublishOutcome struct so the UI can show WHICH relays accepted vs. rejected and WHY. - RPC handle_identity_publish_profile no longer defaults to the local relay: pulls the ENABLED list from nostr_relays::list_relays (the same table that powers Manage Relays) and publishes to every entry. Accepts an optional `relays: [...]` override for tests. - At-least-one-accept guarantee: if every relay rejects, the call errors instead of silently reporting published=true. User gets a real error message listing the failures. - Response shape: `{event_id, accepted: [urls], rejected: [[url, reason]], relays_attempted: N, published: bool}` so the UI can show a useful status block after clicking Publish. relay_url_matches is tolerant of trailing-slash / case differences since nostr-sdk canonicalises URLs internally. Covers the publishing half of task #29; avatar/banner upload UI is still open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 04:42:25 -04:00
if relay_urls.is_empty() {
anyhow::bail!("No relays configured — add a relay under Manage Relays first");
}
// 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));
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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));
}
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let content_str =
serde_json::to_string(&content).context("Failed to serialize profile content")?;
let client = nostr_sdk::Client::new(keys);
fix(nostr): profile publish broadcasts to ALL enabled relays Previously handle_identity_publish_profile defaulted to a single hard-coded relay (ws://localhost:18081) so the user's kind:0 profile event only ever landed on the local relay — hence "Manage Relays shows N connected, but profile edits don't propagate" from testing. Fix — two-layer change: - identity_manager::publish_profile now takes `&[String]` relays instead of one URL. Adds each relay to the nostr-sdk client, gives 15s for handshakes, publishes, then surfaces per-relay accept/reject in a new ProfilePublishOutcome struct so the UI can show WHICH relays accepted vs. rejected and WHY. - RPC handle_identity_publish_profile no longer defaults to the local relay: pulls the ENABLED list from nostr_relays::list_relays (the same table that powers Manage Relays) and publishes to every entry. Accepts an optional `relays: [...]` override for tests. - At-least-one-accept guarantee: if every relay rejects, the call errors instead of silently reporting published=true. User gets a real error message listing the failures. - Response shape: `{event_id, accepted: [urls], rejected: [[url, reason]], relays_attempted: N, published: bool}` so the UI can show a useful status block after clicking Publish. relay_url_matches is tolerant of trailing-slash / case differences since nostr-sdk canonicalises URLs internally. Covers the publishing half of task #29; avatar/banner upload UI is still open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 04:42:25 -04:00
for url in relay_urls {
if let Err(e) = client.add_relay(url).await {
tracing::warn!(relay = %url, error = %e, "Failed to add relay; continuing");
}
}
// 15s gives each relay a reasonable chance to hand-shake before we
// fire the publish. nostr-sdk's send_event_builder to "all relays"
// will only reach relays that have connected by then — some slow
// relays can miss the first publish but subsequent publishes hit
// them once the connection has settled.
if tokio::time::timeout(Duration::from_secs(15), client.connect())
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
.await
.is_err()
{
fix(nostr): profile publish broadcasts to ALL enabled relays Previously handle_identity_publish_profile defaulted to a single hard-coded relay (ws://localhost:18081) so the user's kind:0 profile event only ever landed on the local relay — hence "Manage Relays shows N connected, but profile edits don't propagate" from testing. Fix — two-layer change: - identity_manager::publish_profile now takes `&[String]` relays instead of one URL. Adds each relay to the nostr-sdk client, gives 15s for handshakes, publishes, then surfaces per-relay accept/reject in a new ProfilePublishOutcome struct so the UI can show WHICH relays accepted vs. rejected and WHY. - RPC handle_identity_publish_profile no longer defaults to the local relay: pulls the ENABLED list from nostr_relays::list_relays (the same table that powers Manage Relays) and publishes to every entry. Accepts an optional `relays: [...]` override for tests. - At-least-one-accept guarantee: if every relay rejects, the call errors instead of silently reporting published=true. User gets a real error message listing the failures. - Response shape: `{event_id, accepted: [urls], rejected: [[url, reason]], relays_attempted: N, published: bool}` so the UI can show a useful status block after clicking Publish. relay_url_matches is tolerant of trailing-slash / case differences since nostr-sdk canonicalises URLs internally. Covers the publishing half of task #29; avatar/banner upload UI is still open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 04:42:25 -04:00
tracing::warn!("Nostr relay connection timed out after 15s, continuing anyway");
}
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let builder =
nostr_sdk::prelude::EventBuilder::new(nostr_sdk::prelude::Kind::Metadata, &content_str);
fix(nostr): profile publish broadcasts to ALL enabled relays Previously handle_identity_publish_profile defaulted to a single hard-coded relay (ws://localhost:18081) so the user's kind:0 profile event only ever landed on the local relay — hence "Manage Relays shows N connected, but profile edits don't propagate" from testing. Fix — two-layer change: - identity_manager::publish_profile now takes `&[String]` relays instead of one URL. Adds each relay to the nostr-sdk client, gives 15s for handshakes, publishes, then surfaces per-relay accept/reject in a new ProfilePublishOutcome struct so the UI can show WHICH relays accepted vs. rejected and WHY. - RPC handle_identity_publish_profile no longer defaults to the local relay: pulls the ENABLED list from nostr_relays::list_relays (the same table that powers Manage Relays) and publishes to every entry. Accepts an optional `relays: [...]` override for tests. - At-least-one-accept guarantee: if every relay rejects, the call errors instead of silently reporting published=true. User gets a real error message listing the failures. - Response shape: `{event_id, accepted: [urls], rejected: [[url, reason]], relays_attempted: N, published: bool}` so the UI can show a useful status block after clicking Publish. relay_url_matches is tolerant of trailing-slash / case differences since nostr-sdk canonicalises URLs internally. Covers the publishing half of task #29; avatar/banner upload UI is still open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 04:42:25 -04:00
let output = match client.send_event_builder(builder).await {
Ok(o) => o,
Err(e) => {
client.disconnect().await;
return Err(anyhow::anyhow!("Publish failed on every relay: {}", e));
}
};
let event_id = output.id().to_hex();
// `Output` has `success: HashSet<RelayUrl>` + `failed: HashMap<RelayUrl, String>`.
// Normalise to string comparisons (RelayUrl trims trailing slashes etc.).
let success_strs: std::collections::HashSet<String> = output
.success
.iter()
.map(|u| u.to_string())
.collect();
let failed_strs: std::collections::HashMap<String, String> = output
.failed
.iter()
.map(|(u, msg)| (u.to_string(), msg.clone()))
.collect();
let mut accepted: Vec<String> = Vec::new();
let mut rejected: Vec<(String, String)> = Vec::new();
for url in relay_urls {
let match_url = success_strs
.iter()
.any(|s| relay_url_matches(s, url));
if match_url {
accepted.push(url.clone());
} else if let Some((_, reason)) = failed_strs
.iter()
.find(|(s, _)| relay_url_matches(s, url))
{
rejected.push((url.clone(), reason.clone()));
} else {
rejected.push((url.clone(), "(no ack from relay)".to_string()));
}
}
client.disconnect().await;
fix(nostr): profile publish broadcasts to ALL enabled relays Previously handle_identity_publish_profile defaulted to a single hard-coded relay (ws://localhost:18081) so the user's kind:0 profile event only ever landed on the local relay — hence "Manage Relays shows N connected, but profile edits don't propagate" from testing. Fix — two-layer change: - identity_manager::publish_profile now takes `&[String]` relays instead of one URL. Adds each relay to the nostr-sdk client, gives 15s for handshakes, publishes, then surfaces per-relay accept/reject in a new ProfilePublishOutcome struct so the UI can show WHICH relays accepted vs. rejected and WHY. - RPC handle_identity_publish_profile no longer defaults to the local relay: pulls the ENABLED list from nostr_relays::list_relays (the same table that powers Manage Relays) and publishes to every entry. Accepts an optional `relays: [...]` override for tests. - At-least-one-accept guarantee: if every relay rejects, the call errors instead of silently reporting published=true. User gets a real error message listing the failures. - Response shape: `{event_id, accepted: [urls], rejected: [[url, reason]], relays_attempted: N, published: bool}` so the UI can show a useful status block after clicking Publish. relay_url_matches is tolerant of trailing-slash / case differences since nostr-sdk canonicalises URLs internally. Covers the publishing half of task #29; avatar/banner upload UI is still open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 04:42:25 -04:00
if accepted.is_empty() {
anyhow::bail!(
"Profile published on 0 relays — {} attempted. Failures: {:?}",
relay_urls.len(),
rejected
);
}
Ok(ProfilePublishOutcome {
event_id,
accepted,
rejected,
})
}
/// 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));
}
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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);
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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")?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let file: IdentityFile =
serde_json::from_slice(&data).context("Failed to parse identity file")?;
2026-03-12 12:56:59 +00:00
// 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())
});
release(v1.7.2-alpha): fix Install Update + identity avatar backfill + label Three user-visible fixes shipped together. 1. update.apply permission-denied apply_update() was doing fs::copy into /usr/local/bin/archipelago and tar xzf into /opt/archipelago as the archipelago user — both root-owned. The backup step succeeded (it wrote to data_dir) but the swap failed with a silent permission denied, wrapped as "Failed to apply archipelago". Now uses `sudo install -m 0755` for the binary and `sudo tar -xzf` for the frontend, plus a post-apply `sudo systemctl --no-block restart archipelago` scheduled 2s after the RPC reply so the UI sees success. 2. Apply → Install label en/es locale strings: applyUpdate / applyTitle / applyNow changed from "Apply" to "Install". Matches the user's mental model and distinguishes the user-facing verb from the internal apply_update() function. 3. Identity avatar backfill Identities created before df83163f had profile=None on disk and so rendered as initials. load_record() now synthesizes an IdentityProfile with a default picture (identicon for regular identities, the hex node SVG for derivation_index=0) when profile is missing. The synthetic profile lives only in the returned record; the file stays untouched so a later explicit Save persists whatever the user actually chose. Artefacts: archipelago 70e5444e…67c589 40381960 archipelago-frontend-1.7.2-alpha.tar.gz 806b027b…358a824 76983699 Changelog rewritten layman-style per saved feedback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:25:10 -04:00
// Backfill a default avatar for identities created before the
// default-avatar feature shipped. The synthetic profile lives only
// in the returned record — we don't rewrite the file on disk,
// since a later explicit save will persist whatever the user
// actually chose. Master identities (seed index 0) get the hex
// node SVG; all other pre-existing identities get the identicon.
let profile = file.profile.or_else(|| {
let is_master = file.derivation_index == Some(0);
Some(IdentityProfile {
picture: Some(crate::avatar::default_picture(&file.pubkey_hex, is_master)),
..Default::default()
})
});
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,
2026-03-12 12:56:59 +00:00
nostr_npub,
release(v1.7.2-alpha): fix Install Update + identity avatar backfill + label Three user-visible fixes shipped together. 1. update.apply permission-denied apply_update() was doing fs::copy into /usr/local/bin/archipelago and tar xzf into /opt/archipelago as the archipelago user — both root-owned. The backup step succeeded (it wrote to data_dir) but the swap failed with a silent permission denied, wrapped as "Failed to apply archipelago". Now uses `sudo install -m 0755` for the binary and `sudo tar -xzf` for the frontend, plus a post-apply `sudo systemctl --no-block restart archipelago` scheduled 2s after the RPC reply so the UI sees success. 2. Apply → Install label en/es locale strings: applyUpdate / applyTitle / applyNow changed from "Apply" to "Install". Matches the user's mental model and distinguishes the user-facing verb from the internal apply_update() function. 3. Identity avatar backfill Identities created before df83163f had profile=None on disk and so rendered as initials. load_record() now synthesizes an IdentityProfile with a default picture (identicon for regular identities, the hex node SVG for derivation_index=0) when profile is missing. The synthetic profile lives only in the returned record; the file stays untouched so a later explicit Save persists whatever the user actually chose. Artefacts: archipelago 70e5444e…67c589 40381960 archipelago-frontend-1.7.2-alpha.tar.gz 806b027b…358a824 76983699 Changelog rewritten layman-style per saved feedback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:25:10 -04:00
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")?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let record = mgr
.create("Nostr".to_string(), IdentityPurpose::Personal)
.await
.unwrap();
let npub = mgr.create_nostr_key(&record.id).await.unwrap();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
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);
}
}