2026-04-13 08:48:48 -04:00
|
|
|
mod blob;
|
2026-03-22 03:30:21 +00:00
|
|
|
mod content;
|
|
|
|
|
mod dwn;
|
|
|
|
|
mod node_message;
|
|
|
|
|
mod proxy;
|
2026-04-01 23:37:55 +01:00
|
|
|
mod remote_input;
|
2026-04-02 11:01:38 +01:00
|
|
|
mod remote_relay;
|
2026-03-22 03:30:21 +00:00
|
|
|
mod websocket;
|
|
|
|
|
|
|
|
|
|
use crate::api::rpc::RpcHandler;
|
2026-04-13 08:48:48 -04:00
|
|
|
use crate::blobs::BlobStore;
|
2026-03-22 03:30:21 +00:00
|
|
|
use crate::config::Config;
|
|
|
|
|
use crate::monitoring::MetricsStore;
|
|
|
|
|
use crate::session::{self, SessionStore};
|
|
|
|
|
use crate::state::StateManager;
|
|
|
|
|
use anyhow::Result;
|
|
|
|
|
use hyper::{Method, Request, Response, StatusCode};
|
2026-04-13 08:48:48 -04:00
|
|
|
use sha2::{Digest, Sha256};
|
2026-03-22 03:30:21 +00:00
|
|
|
use std::sync::Arc;
|
2026-04-02 10:34:58 +01:00
|
|
|
use tokio::sync::broadcast;
|
2026-03-22 03:30:21 +00:00
|
|
|
use tracing::debug;
|
|
|
|
|
|
|
|
|
|
/// Build an HTTP response without unwrap. Falls back to a plain 500 if builder fails.
|
|
|
|
|
// Used by handler submodules after unwrap elimination
|
|
|
|
|
#[allow(dead_code)]
|
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(super) fn build_response(
|
|
|
|
|
status: StatusCode,
|
|
|
|
|
content_type: &str,
|
|
|
|
|
body: hyper::Body,
|
|
|
|
|
) -> Response<hyper::Body> {
|
2026-03-22 03:30:21 +00:00
|
|
|
Response::builder()
|
|
|
|
|
.status(status)
|
|
|
|
|
.header("Content-Type", content_type)
|
|
|
|
|
.body(body)
|
|
|
|
|
.unwrap_or_else(|_| Response::new(hyper::Body::from("Internal error")))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct ApiHandler {
|
|
|
|
|
config: Config,
|
|
|
|
|
rpc_handler: Arc<RpcHandler>,
|
|
|
|
|
state_manager: Arc<StateManager>,
|
|
|
|
|
metrics_store: Arc<MetricsStore>,
|
|
|
|
|
session_store: SessionStore,
|
2026-04-02 10:34:58 +01:00
|
|
|
/// Broadcast channel for relaying companion app input to remote browsers.
|
|
|
|
|
input_relay_tx: broadcast::Sender<String>,
|
2026-04-13 08:48:48 -04:00
|
|
|
/// Content-addressed blob store for attachments shared over mesh/federation.
|
|
|
|
|
blob_store: Arc<BlobStore>,
|
|
|
|
|
/// Our own node pubkey (hex) — used to self-sign debug/test capabilities.
|
|
|
|
|
self_pubkey_hex: String,
|
2026-03-22 03:30:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ApiHandler {
|
|
|
|
|
pub async fn new(
|
|
|
|
|
config: Config,
|
|
|
|
|
state_manager: Arc<StateManager>,
|
|
|
|
|
metrics_store: Arc<MetricsStore>,
|
|
|
|
|
) -> Result<Self> {
|
|
|
|
|
let session_store = SessionStore::new().await;
|
|
|
|
|
let rpc_handler = Arc::new(
|
|
|
|
|
RpcHandler::new(
|
|
|
|
|
config.clone(),
|
|
|
|
|
state_manager.clone(),
|
|
|
|
|
metrics_store.clone(),
|
|
|
|
|
session_store.clone(),
|
|
|
|
|
)
|
|
|
|
|
.await?,
|
|
|
|
|
);
|
2026-04-02 10:34:58 +01:00
|
|
|
let (input_relay_tx, _) = broadcast::channel(64);
|
2026-03-22 03:30:21 +00:00
|
|
|
|
2026-04-13 08:48:48 -04:00
|
|
|
// Derive a blob-store capability key from the node's Ed25519 signing
|
|
|
|
|
// key. SHA-256 domain-separated so rotating the identity rotates
|
|
|
|
|
// every outstanding capability token (intentional — prevents a
|
|
|
|
|
// replaced node from honouring old caps).
|
|
|
|
|
let identity_dir = config.data_dir.join("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
|
|
|
let identity = crate::identity::NodeIdentity::load_or_create(&identity_dir).await?;
|
2026-04-13 08:48:48 -04:00
|
|
|
let mut hasher = Sha256::new();
|
|
|
|
|
hasher.update(identity.signing_key().to_bytes());
|
|
|
|
|
hasher.update(b"|archipelago-blob-cap-v1");
|
|
|
|
|
let mut cap_key = [0u8; 32];
|
|
|
|
|
cap_key.copy_from_slice(&hasher.finalize());
|
|
|
|
|
let blob_store = Arc::new(BlobStore::open(&config.data_dir, cap_key).await?);
|
|
|
|
|
let self_pubkey_hex = hex::encode(identity.signing_key().verifying_key().as_bytes());
|
|
|
|
|
|
2026-04-13 11:10:49 -04:00
|
|
|
// Share blob store with the RPC layer so mesh.send-content /
|
|
|
|
|
// mesh.fetch-content can reach the same instance (single cap_key,
|
|
|
|
|
// single on-disk root) without re-opening it.
|
|
|
|
|
rpc_handler
|
|
|
|
|
.set_blob_store(blob_store.clone(), self_pubkey_hex.clone())
|
|
|
|
|
.await;
|
|
|
|
|
|
2026-03-22 03:30:21 +00:00
|
|
|
Ok(Self {
|
|
|
|
|
config,
|
|
|
|
|
rpc_handler,
|
|
|
|
|
state_manager,
|
|
|
|
|
metrics_store,
|
|
|
|
|
session_store,
|
2026-04-02 10:34:58 +01:00
|
|
|
input_relay_tx,
|
2026-04-13 08:48:48 -04:00
|
|
|
blob_store,
|
|
|
|
|
self_pubkey_hex,
|
2026-03-22 03:30:21 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Access the RPC handler (for service initialization after construction).
|
|
|
|
|
pub fn rpc_handler(&self) -> &Arc<RpcHandler> {
|
|
|
|
|
&self.rpc_handler
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Check if the request has a valid session cookie.
|
|
|
|
|
async fn is_authenticated(&self, headers: &hyper::HeaderMap) -> bool {
|
|
|
|
|
match session::extract_session_cookie(headers) {
|
|
|
|
|
Some(token) => self.session_store.validate(&token).await,
|
|
|
|
|
None => false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 15:43:45 -04:00
|
|
|
/// Server-side fetch of the upstream app catalog so the browser can
|
|
|
|
|
/// load it without fighting CORS (git.tx1138.com emits no ACAO) or
|
|
|
|
|
/// CSP (the fallback IP-port URL isn't in `connect-src`). Tries the
|
|
|
|
|
/// upstream URLs in the same order the frontend used, returns the
|
|
|
|
|
/// first 2xx response. 15s total timeout.
|
|
|
|
|
async fn handle_app_catalog_proxy() -> Result<Response<hyper::Body>> {
|
|
|
|
|
const UPSTREAMS: &[&str] = &[
|
|
|
|
|
"http://23.182.128.160:3000/lfg2025/app-catalog/raw/branch/main/catalog.json",
|
release(v1.7.29-alpha): VPS as default app registry + settings UI
- New Settings → App registries page (/dashboard/settings/registries)
that mirrors the update-mirrors experience: list of configured
registries, test reachability, set primary, add/remove. New
registry.set-primary RPC; existing registry.{list,add,remove,test}
reused.
- Default RegistryConfig flipped: VPS (23.182.128.160:3000/lfg2025) is
now Server 1 (primary), tx1138 is Server 2 (fallback).
- Install pipeline now rewrites the first pull to the primary registry
URL before attempting it. Before this, installs always hit whichever
registry the image was hardcoded to, so changing the primary didn't
actually affect where images came from. On failure, the existing
fallback walk skips the primary (already tried) and walks the rest.
- App catalog proxy UPSTREAMS order flipped so the catalog follows the
same VPS-first rule.
- Reboot overlay: animated "a" logo now sits in the center of the ring
(matches the screensaver composition). Extracted the logo-wrapper
pattern inline.
7/7 registry tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:54:07 -04:00
|
|
|
"https://git.tx1138.com/lfg2025/app-catalog/raw/branch/main/catalog.json",
|
2026-04-20 15:43:45 -04:00
|
|
|
];
|
|
|
|
|
let client = match reqwest::Client::builder()
|
|
|
|
|
.timeout(std::time::Duration::from_secs(15))
|
|
|
|
|
.build()
|
|
|
|
|
{
|
|
|
|
|
Ok(c) => c,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
return Ok(build_response(
|
|
|
|
|
hyper::StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
"text/plain",
|
|
|
|
|
hyper::Body::from(format!("client build failed: {}", e)),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
for url in UPSTREAMS {
|
|
|
|
|
match client.get(*url).send().await {
|
|
|
|
|
Ok(resp) if resp.status().is_success() => {
|
|
|
|
|
if let Ok(bytes) = resp.bytes().await {
|
|
|
|
|
return Ok(Response::builder()
|
|
|
|
|
.status(hyper::StatusCode::OK)
|
|
|
|
|
.header("Content-Type", "application/json")
|
|
|
|
|
.header("Cache-Control", "public, max-age=3600")
|
|
|
|
|
.body(hyper::Body::from(bytes))
|
|
|
|
|
.unwrap_or_else(|_| {
|
|
|
|
|
Response::new(hyper::Body::from("proxy response build failed"))
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => continue,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(build_response(
|
|
|
|
|
hyper::StatusCode::BAD_GATEWAY,
|
|
|
|
|
"text/plain",
|
|
|
|
|
hyper::Body::from("all upstream catalog URLs failed"),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 03:30:21 +00:00
|
|
|
/// Build a 401 Unauthorized JSON response.
|
|
|
|
|
fn unauthorized() -> Response<hyper::Body> {
|
|
|
|
|
let body = serde_json::json!({ "error": "Unauthorized" });
|
|
|
|
|
let body_bytes = serde_json::to_vec(&body).unwrap_or_default();
|
|
|
|
|
Response::builder()
|
|
|
|
|
.status(StatusCode::UNAUTHORIZED)
|
|
|
|
|
.header("Content-Type", "application/json")
|
|
|
|
|
.body(hyper::Body::from(body_bytes))
|
|
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Allowed CORS origins derived from the config host IP.
|
|
|
|
|
fn allowed_origins(&self) -> Vec<String> {
|
|
|
|
|
let mut origins = vec![
|
|
|
|
|
format!("http://{}", self.config.host_ip),
|
|
|
|
|
format!("https://{}", self.config.host_ip),
|
|
|
|
|
];
|
|
|
|
|
if self.config.dev_mode {
|
|
|
|
|
origins.push("http://localhost:8100".to_string()); // Vite dev server
|
|
|
|
|
}
|
|
|
|
|
origins
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Validate the Origin header against allowed origins.
|
|
|
|
|
/// Returns the matched origin if valid, None if cross-origin is not allowed.
|
|
|
|
|
fn validate_origin(&self, headers: &hyper::HeaderMap) -> Option<String> {
|
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 origin = headers.get("origin").and_then(|v| v.to_str().ok())?;
|
2026-03-22 03:30:21 +00:00
|
|
|
let allowed = self.allowed_origins();
|
|
|
|
|
if allowed.iter().any(|a| a == origin) {
|
|
|
|
|
Some(origin.to_string())
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 12:58:03 -04:00
|
|
|
/// Permissive origin check for the share-to-mesh iframe intent: any scheme
|
|
|
|
|
/// http(s):// followed by the configured host_ip, optionally `:port`. Apps
|
|
|
|
|
/// proxied under other ports (APP_PORTS) call this from within the same
|
|
|
|
|
/// node, so they share host_ip but not port. The session cookie still has
|
|
|
|
|
/// to be valid — this is a sanity check, not the primary auth.
|
|
|
|
|
fn validate_app_origin(&self, headers: &hyper::HeaderMap) -> Option<String> {
|
|
|
|
|
let origin = headers.get("origin").and_then(|v| v.to_str().ok())?;
|
|
|
|
|
// Allow localhost dev server too so the Vite frontend can exercise it.
|
|
|
|
|
if self.config.dev_mode && origin == "http://localhost:8100" {
|
|
|
|
|
return Some(origin.to_string());
|
|
|
|
|
}
|
|
|
|
|
let host_ip = &self.config.host_ip;
|
|
|
|
|
let matches = |scheme: &str| -> bool {
|
|
|
|
|
let prefix = format!("{}{}", scheme, host_ip);
|
|
|
|
|
if origin == prefix {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
let with_port = format!("{}:", prefix);
|
|
|
|
|
origin.starts_with(&with_port)
|
|
|
|
|
&& origin[with_port.len()..]
|
|
|
|
|
.bytes()
|
|
|
|
|
.all(|b| b.is_ascii_digit())
|
|
|
|
|
};
|
|
|
|
|
if matches("http://") || matches("https://") {
|
|
|
|
|
Some(origin.to_string())
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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 handle_request(&self, req: Request<hyper::Body>) -> Result<Response<hyper::Body>> {
|
2026-03-22 03:30:21 +00:00
|
|
|
let path = req.uri().path().to_string();
|
|
|
|
|
let method = req.method().clone();
|
|
|
|
|
|
|
|
|
|
// Handle CORS preflight for all routes
|
|
|
|
|
if method == Method::OPTIONS {
|
|
|
|
|
let mut builder = Response::builder()
|
|
|
|
|
.status(StatusCode::NO_CONTENT)
|
|
|
|
|
.header("Vary", "Origin");
|
|
|
|
|
if let Some(origin) = self.validate_origin(req.headers()) {
|
|
|
|
|
builder = builder
|
|
|
|
|
.header("Access-Control-Allow-Origin", &origin)
|
|
|
|
|
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
|
|
|
.header("Access-Control-Allow-Headers", "Content-Type, X-CSRF-Token")
|
|
|
|
|
.header("Access-Control-Allow-Credentials", "true");
|
|
|
|
|
}
|
|
|
|
|
return Ok(builder.body(hyper::Body::empty()).unwrap());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WebSocket upgrade — validate session before upgrading
|
|
|
|
|
if method == Method::GET && path == "/ws/db" {
|
|
|
|
|
if !self.is_authenticated(req.headers()).await {
|
|
|
|
|
tracing::warn!("401 WebSocket /ws/db — session invalid or missing");
|
|
|
|
|
return Ok(Self::unauthorized());
|
|
|
|
|
}
|
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 Self::handle_websocket(
|
|
|
|
|
req,
|
|
|
|
|
self.state_manager.clone(),
|
|
|
|
|
self.metrics_store.clone(),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
2026-03-22 03:30:21 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-01 23:37:55 +01:00
|
|
|
// Remote input WebSocket — companion app sends keyboard/mouse events
|
|
|
|
|
if method == Method::GET && path == "/ws/remote-input" {
|
|
|
|
|
if !self.is_authenticated(req.headers()).await {
|
|
|
|
|
tracing::warn!("401 WebSocket /ws/remote-input — session invalid or missing");
|
|
|
|
|
return Ok(Self::unauthorized());
|
|
|
|
|
}
|
2026-04-02 10:34:58 +01:00
|
|
|
return Self::handle_remote_input(req, self.input_relay_tx.clone()).await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remote relay WebSocket — browser receives companion input events
|
|
|
|
|
if method == Method::GET && path == "/ws/remote-relay" {
|
|
|
|
|
if !self.is_authenticated(req.headers()).await {
|
|
|
|
|
tracing::warn!("401 WebSocket /ws/remote-relay — session invalid or missing");
|
|
|
|
|
return Ok(Self::unauthorized());
|
|
|
|
|
}
|
|
|
|
|
return Self::handle_remote_relay(req, self.input_relay_tx.subscribe()).await;
|
2026-04-01 23:37:55 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-22 03:30:21 +00:00
|
|
|
// Convert body to bytes for non-WS routes
|
|
|
|
|
let headers = req.headers().clone();
|
2026-04-13 08:48:48 -04:00
|
|
|
let query_string = req.uri().query().map(|s| s.to_string()).unwrap_or_default();
|
2026-03-22 03:30:21 +00:00
|
|
|
let (parts, body) = req.into_parts();
|
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 body_bytes = hyper::body::to_bytes(body)
|
|
|
|
|
.await
|
2026-03-22 03:30:21 +00:00
|
|
|
.map_err(|e| anyhow::anyhow!("Failed to read body: {}", e))?;
|
|
|
|
|
let req_with_bytes = Request::from_parts(parts, hyper::Body::from(body_bytes.clone()));
|
|
|
|
|
|
|
|
|
|
debug!("{} {}", method, path);
|
|
|
|
|
|
|
|
|
|
match (method, path.as_str()) {
|
|
|
|
|
// RPC — auth is handled inside rpc handler per-method
|
|
|
|
|
(Method::POST, "/rpc/v1") => self.rpc_handler.handle(req_with_bytes).await,
|
|
|
|
|
|
|
|
|
|
// Health — unauthenticated, returns JSON with service status
|
|
|
|
|
(Method::GET, "/health") => {
|
|
|
|
|
let recovery_complete = crate::crash_recovery::is_recovery_complete();
|
|
|
|
|
let uptime = crate::crash_recovery::uptime_seconds();
|
|
|
|
|
let health_status = if recovery_complete { "ok" } else { "degraded" };
|
|
|
|
|
let status = serde_json::json!({
|
|
|
|
|
"status": health_status,
|
|
|
|
|
"crash_recovery_complete": recovery_complete,
|
|
|
|
|
"uptime_seconds": uptime,
|
|
|
|
|
"version": env!("CARGO_PKG_VERSION"),
|
|
|
|
|
"services": {
|
|
|
|
|
"rpc": true,
|
|
|
|
|
"sessions": true,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header("Content-Type", "application/json")
|
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
|
|
|
.body(hyper::Body::from(
|
|
|
|
|
serde_json::to_vec(&status).unwrap_or_default(),
|
|
|
|
|
))
|
2026-03-22 03:30:21 +00:00
|
|
|
.unwrap())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Node message — P2P endpoint (authenticated by source validation, not cookie)
|
|
|
|
|
(Method::POST, "/archipelago/node-message") => {
|
|
|
|
|
Self::handle_node_message(body_bytes).await
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 13:37:48 -04:00
|
|
|
// Mesh typed envelope relay over federation — peers POST
|
|
|
|
|
// pre-encoded TypedEnvelope wire bytes here when the envelope is
|
|
|
|
|
// too large for a single LoRa frame (primarily ContentRef). No
|
|
|
|
|
// session auth: the body carries a pubkey + ed25519 signature
|
|
|
|
|
// over the wire bytes which we verify before dispatching.
|
|
|
|
|
(Method::POST, "/archipelago/mesh-typed") => {
|
|
|
|
|
Self::handle_mesh_typed_relay(self.rpc_handler.clone(), body_bytes).await
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 08:48:48 -04:00
|
|
|
// Blob upload — local/session use only. Session-authenticated so
|
|
|
|
|
// only the node owner can push attachments into the blob store.
|
|
|
|
|
(Method::POST, "/api/blob") => {
|
|
|
|
|
if !self.is_authenticated(&headers).await {
|
|
|
|
|
return Ok(Self::unauthorized());
|
|
|
|
|
}
|
|
|
|
|
Self::handle_blob_upload(
|
|
|
|
|
&self.blob_store,
|
|
|
|
|
&self.self_pubkey_hex,
|
2026-04-20 10:03:38 -04:00
|
|
|
&self.config.data_dir,
|
2026-04-13 08:48:48 -04:00
|
|
|
&headers,
|
|
|
|
|
body_bytes,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 12:58:03 -04:00
|
|
|
// Share-to-mesh intent — marketplace app iframes POST a file here
|
|
|
|
|
// to stage it as a mesh attachment. Same body format as /api/blob
|
|
|
|
|
// (raw bytes + X-Blob-Mime/X-Blob-Filename headers). The app is
|
|
|
|
|
// expected to postMessage `{type:'share-to-mesh', cid, ...}` to
|
|
|
|
|
// its parent window afterwards so the Mesh view can pick it up.
|
|
|
|
|
// Authenticated by session cookie + a relaxed Origin check (any
|
|
|
|
|
// port on the archipelago host is allowed, so proxied apps on
|
|
|
|
|
// their own ports can reach it with credentials:'include').
|
|
|
|
|
(Method::POST, "/api/share-to-mesh") => {
|
|
|
|
|
if !self.is_authenticated(&headers).await {
|
|
|
|
|
return Ok(Self::unauthorized());
|
|
|
|
|
}
|
|
|
|
|
let origin = match self.validate_app_origin(&headers) {
|
|
|
|
|
Some(o) => o,
|
|
|
|
|
None => {
|
|
|
|
|
return Ok(build_response(
|
|
|
|
|
StatusCode::FORBIDDEN,
|
|
|
|
|
"text/plain",
|
|
|
|
|
hyper::Body::from("origin not allowed"),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
Self::handle_share_to_mesh(
|
|
|
|
|
&self.blob_store,
|
|
|
|
|
&self.self_pubkey_hex,
|
|
|
|
|
&headers,
|
|
|
|
|
body_bytes,
|
|
|
|
|
&origin,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 08:48:48 -04:00
|
|
|
// Blob download — peer-facing. No session required; authenticated
|
|
|
|
|
// by HMAC capability token signed when the blob ref was shared.
|
|
|
|
|
(Method::GET, p) if p.starts_with("/blob/") => {
|
|
|
|
|
Self::handle_blob_download(&self.blob_store, p, &query_string).await
|
|
|
|
|
}
|
|
|
|
|
|
feat: botfights, discover, mobile gamepad, content handler, package config updates
Miscellaneous improvements: botfights manifest, discover page curated
apps, mobile gamepad enhancements, content HTTP handler, package
install config updates, health monitor tweaks, shared content UI,
container specs and image version updates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:11:41 -04:00
|
|
|
// Content preview — degraded previews for paid content (no auth, no payment)
|
|
|
|
|
(Method::GET, p) if p.starts_with("/content/") && p.ends_with("/preview") => {
|
|
|
|
|
Self::handle_content_preview(p, &self.config).await
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 03:30:21 +00:00
|
|
|
// Content serving — peers access shared content over Tor (no session auth)
|
|
|
|
|
(Method::GET, p) if p.starts_with("/content/") => {
|
|
|
|
|
Self::handle_content_request(p, &headers, &self.config).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Content catalog — list available content (no session auth, for peers)
|
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
|
|
|
(Method::GET, "/content") => Self::handle_content_catalog(&self.config).await,
|
2026-03-22 03:30:21 +00:00
|
|
|
|
|
|
|
|
// Electrs status — unauthenticated (read-only sync status)
|
|
|
|
|
(Method::GET, "/electrs-status") => Self::handle_electrs_status().await,
|
|
|
|
|
|
2026-04-20 15:43:45 -04:00
|
|
|
// App-catalog proxy — fetches catalog.json from the configured
|
|
|
|
|
// upstream URLs server-side so the browser doesn't hit CORS
|
|
|
|
|
// (git.tx1138.com has no ACAO header) or CSP (IP-port upstream
|
|
|
|
|
// falls outside `connect-src`). Session-authenticated so only
|
|
|
|
|
// the logged-in node owner can spin up fetches.
|
|
|
|
|
(Method::GET, "/api/app-catalog") => {
|
|
|
|
|
if !self.is_authenticated(&headers).await {
|
|
|
|
|
return Ok(Self::unauthorized());
|
|
|
|
|
}
|
|
|
|
|
Self::handle_app_catalog_proxy().await
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 03:30:21 +00:00
|
|
|
// LND connect info — nginx validates session cookie (presence check),
|
|
|
|
|
// backend is bound to 127.0.0.1 so only nginx can reach it.
|
|
|
|
|
// No backend auth check here because the LND UI iframe fetches this
|
|
|
|
|
// endpoint and the session cookie flow is validated at the nginx layer.
|
|
|
|
|
(Method::GET, "/lnd-connect-info") => {
|
|
|
|
|
Self::handle_lnd_connect_info(self.rpc_handler.clone()).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Container logs — requires session
|
|
|
|
|
(Method::GET, path) if path.starts_with("/api/container/logs") => {
|
|
|
|
|
if !self.is_authenticated(&headers).await {
|
|
|
|
|
return Ok(Self::unauthorized());
|
|
|
|
|
}
|
|
|
|
|
let origin = self.validate_origin(&headers).unwrap_or_default();
|
|
|
|
|
Self::handle_container_logs_http(self.rpc_handler.clone(), path, &origin).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LND proxy — requires session
|
|
|
|
|
(Method::GET, path) if path.starts_with("/proxy/lnd/") => {
|
|
|
|
|
if !self.is_authenticated(&headers).await {
|
|
|
|
|
return Ok(Self::unauthorized());
|
|
|
|
|
}
|
|
|
|
|
let origin = self.validate_origin(&headers).unwrap_or_default();
|
|
|
|
|
Self::handle_lnd_proxy(path, &origin).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DWN health — unauthenticated
|
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
|
|
|
(Method::GET, "/dwn/health") => Self::handle_dwn_health(&self.config).await,
|
2026-03-22 03:30:21 +00:00
|
|
|
|
|
|
|
|
// DWN message processing — peers access over Tor for sync (no session auth)
|
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
|
|
|
(Method::POST, "/dwn") => Self::handle_dwn_message(body_bytes, &self.config).await,
|
2026-03-22 03:30:21 +00:00
|
|
|
|
|
|
|
|
_ => Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::NOT_FOUND)
|
|
|
|
|
.body(hyper::Body::from("Not Found"))
|
|
|
|
|
.unwrap()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Validate that an app ID matches the safe pattern: lowercase alphanumeric + hyphens.
|
|
|
|
|
fn is_valid_app_id(id: &str) -> bool {
|
|
|
|
|
!id.is_empty()
|
|
|
|
|
&& id.len() <= 64
|
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
|
|
|
&& id
|
|
|
|
|
.bytes()
|
|
|
|
|
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
|
2026-03-22 03:30:21 +00:00
|
|
|
&& id.as_bytes()[0] != b'-'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Validate that a pubkey is a 64-char hex string.
|
|
|
|
|
fn is_valid_pubkey_hex(s: &str) -> bool {
|
|
|
|
|
s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Strip newlines and ANSI escape sequences from strings before logging.
|
|
|
|
|
fn sanitize_log_string(s: &str) -> String {
|
|
|
|
|
s.replace('\n', "\\n")
|
|
|
|
|
.replace('\r', "\\r")
|
|
|
|
|
.replace('\x1b', "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Strip HTML-sensitive characters to prevent XSS when stored/rendered.
|
|
|
|
|
fn sanitize_html(s: &str) -> String {
|
|
|
|
|
s.replace('&', "&")
|
|
|
|
|
.replace('<', "<")
|
|
|
|
|
.replace('>', ">")
|
|
|
|
|
.replace('"', """)
|
|
|
|
|
.replace('\'', "'")
|
|
|
|
|
}
|