fix(security): bind seq into mesh signatures (v2 preimage), guard DID slice, cfg-gate dev password
- mesh: verify_signature accepts a v2 preimage (t,v,ts,seq) alongside
legacy v1 (t,v,ts); signed_with_seq() is the v2 sender path, not yet
wired — senders stay v1 until the fleet verifies v2 (receivers
hard-drop bad sigs, so flipping send-side first would break
mixed-fleet alerts). Tests: v2 verify, v2 seq-tamper rejection,
v1 sign-then-set-seq compat.
- mesh listener: malformed radio-supplied DID shorter than the
'did:key:' prefix can no longer panic advert_name (slice -> .get()).
- auth: the pre-setup password123 dev login and the constant itself are
now #[cfg(debug_assertions)] — no release binary carries the bypass,
whatever its runtime config says.
- orchestrator: canned host-facts under #[cfg(test)] — awaiting real
subprocesses under tokio's paused test clock deadlocks against
auto-advanced timers (the old blocking detection only worked by never
yielding).
- drop two now-unused std::process::Command imports left by 4c75bb3d.
Tests: mesh 110/110 (incl. 2 new), api 68/68, container 159/159,
archipelago-container check clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 17:49:52 -04:00
|
|
|
use super::RpcHandler;
|
|
|
|
|
#[cfg(debug_assertions)]
|
|
|
|
|
use super::DEV_DEFAULT_PASSWORD;
|
2026-03-04 05:23:42 +00:00
|
|
|
use anyhow::Result;
|
|
|
|
|
|
|
|
|
|
impl RpcHandler {
|
|
|
|
|
pub(super) async fn handle_auth_login(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
|
|
|
|
|
// Companion device-token login: minted via auth.createDeviceToken and
|
|
|
|
|
// carried by the pairing QR. Verified here so it shares the login rate
|
|
|
|
|
// limiter with password attempts.
|
|
|
|
|
if let Some(token) = params.get("token").and_then(|v| v.as_str()) {
|
|
|
|
|
return match crate::device_tokens::verify(&self.config.data_dir, token).await {
|
|
|
|
|
Some(device) => {
|
|
|
|
|
tracing::info!("[onboarding] device-token login ({device})");
|
|
|
|
|
Ok(serde_json::Value::Null)
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
tracing::warn!("[onboarding] device-token login failed");
|
|
|
|
|
Err(anyhow::anyhow!("Invalid device token"))
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 05:23:42 +00:00
|
|
|
let password = params
|
|
|
|
|
.get("password")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing password"))?;
|
|
|
|
|
|
|
|
|
|
let is_setup = self.auth_manager.is_setup().await?;
|
|
|
|
|
if !is_setup {
|
fix(security): bind seq into mesh signatures (v2 preimage), guard DID slice, cfg-gate dev password
- mesh: verify_signature accepts a v2 preimage (t,v,ts,seq) alongside
legacy v1 (t,v,ts); signed_with_seq() is the v2 sender path, not yet
wired — senders stay v1 until the fleet verifies v2 (receivers
hard-drop bad sigs, so flipping send-side first would break
mixed-fleet alerts). Tests: v2 verify, v2 seq-tamper rejection,
v1 sign-then-set-seq compat.
- mesh listener: malformed radio-supplied DID shorter than the
'did:key:' prefix can no longer panic advert_name (slice -> .get()).
- auth: the pre-setup password123 dev login and the constant itself are
now #[cfg(debug_assertions)] — no release binary carries the bypass,
whatever its runtime config says.
- orchestrator: canned host-facts under #[cfg(test)] — awaiting real
subprocesses under tokio's paused test clock deadlocks against
auto-advanced timers (the old blocking detection only worked by never
yielding).
- drop two now-unused std::process::Command imports left by 4c75bb3d.
Tests: mesh 110/110 (incl. 2 new), api 68/68, container 159/159,
archipelago-container check clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 17:49:52 -04:00
|
|
|
// Dev BUILDS only: allow the default password so the UI can log
|
|
|
|
|
// in without running setup. cfg-gated so no release binary can
|
|
|
|
|
// carry the bypass, whatever its runtime config says.
|
|
|
|
|
#[cfg(debug_assertions)]
|
2026-03-04 05:23:42 +00:00
|
|
|
if self.config.dev_mode && password == DEV_DEFAULT_PASSWORD {
|
2026-03-28 11:31:48 +00:00
|
|
|
tracing::info!("[onboarding] login via dev default password");
|
2026-03-04 05:23:42 +00:00
|
|
|
return Ok(serde_json::Value::Null);
|
|
|
|
|
}
|
2026-03-28 11:31:48 +00:00
|
|
|
tracing::warn!("[onboarding] login attempt before setup complete");
|
2026-03-04 05:23:42 +00:00
|
|
|
return Err(anyhow::anyhow!(
|
|
|
|
|
"User not set up. Please complete setup first."
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let valid = self.auth_manager.verify_password(password).await?;
|
|
|
|
|
if !valid {
|
2026-07-22 21:44:39 +01:00
|
|
|
// The companion app sends its device token through the password
|
|
|
|
|
// field (it reuses the whole password auto-login path, including
|
|
|
|
|
// the WebView form). Accept a valid token here so that path works.
|
|
|
|
|
if let Some(device) =
|
|
|
|
|
crate::device_tokens::verify(&self.config.data_dir, password).await
|
|
|
|
|
{
|
|
|
|
|
tracing::info!("[onboarding] device-token login via password field ({device})");
|
|
|
|
|
return Ok(serde_json::Value::Null);
|
|
|
|
|
}
|
2026-03-28 11:31:48 +00:00
|
|
|
tracing::warn!("[onboarding] login failed — wrong password");
|
2026-03-04 05:23:42 +00:00
|
|
|
return Err(anyhow::anyhow!("Password Incorrect"));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 11:31:48 +00:00
|
|
|
tracing::info!("[onboarding] login successful");
|
2026-04-09 20:42:09 +02:00
|
|
|
|
2026-06-14 11:19:56 -04:00
|
|
|
// Best-effort: heal a LOCKED LND wallet created with an unknown/legacy
|
|
|
|
|
// password by rotating it onto the per-node secret, using the password
|
|
|
|
|
// the user just authenticated with as a candidate. Non-blocking so login
|
|
|
|
|
// is never slowed or broken when LND isn't installed / already unlocked.
|
|
|
|
|
let candidate = password.to_string();
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
match crate::container::lnd::migrate_locked_wallet(&[candidate]).await {
|
|
|
|
|
Ok(true) => tracing::info!("[login] LND wallet healed / auto-unlocked"),
|
|
|
|
|
Ok(false) => {} // not locked, or seed-recovery required
|
|
|
|
|
Err(e) => tracing::debug!("[login] LND wallet migration skipped: {e}"),
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-09 20:42:09 +02:00
|
|
|
// Ensure NostrVPN config exists — covers the case where onboardingComplete
|
|
|
|
|
// was never called (e.g., user took the "already set up" shortcut).
|
|
|
|
|
let data_dir = self.config.data_dir.clone();
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
// Quick check: if config.toml already exists, skip
|
|
|
|
|
let config_path = data_dir.join("nostr-vpn/.config/nvpn/config.toml");
|
|
|
|
|
if config_path.exists() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Identity must exist for VPN config
|
|
|
|
|
if !data_dir.join("identity/nostr_pubkey").exists() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
match crate::vpn::configure_nostr_vpn(&data_dir).await {
|
|
|
|
|
Ok(()) => tracing::info!("[login] NostrVPN auto-configured on first login"),
|
|
|
|
|
Err(e) => tracing::debug!("[login] NostrVPN auto-config skipped: {}", e),
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-04 05:23:42 +00:00
|
|
|
Ok(serde_json::Value::Null)
|
|
|
|
|
}
|
|
|
|
|
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
/// Mint a device token for the companion pairing QR. Session-gated by the
|
|
|
|
|
/// dispatcher (not in UNAUTHENTICATED_METHODS), so only a logged-in web UI
|
|
|
|
|
/// can mint one. The plaintext token is returned exactly once.
|
|
|
|
|
pub(super) async fn handle_auth_create_device_token(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
2026-07-24 02:03:47 +01:00
|
|
|
let mut name = params
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|p| p.get("name"))
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.unwrap_or("companion")
|
|
|
|
|
.trim()
|
|
|
|
|
.to_string();
|
|
|
|
|
if name.is_empty() || name.len() > 64 {
|
|
|
|
|
return Err(anyhow::anyhow!("Device name must be 1-64 characters"));
|
|
|
|
|
}
|
2026-07-24 02:03:47 +01:00
|
|
|
// The default name was a single shared slot: every pairing popup
|
|
|
|
|
// replaced the previous phone's token, silently logging out the
|
|
|
|
|
// first phone the moment a second one paired. Default-named mints
|
|
|
|
|
// get a unique suffix so each device keeps its own credential;
|
|
|
|
|
// explicitly named devices keep replace-in-place semantics.
|
|
|
|
|
if name == "companion" {
|
|
|
|
|
name = format!("companion-{}", hex::encode(rand::random::<[u8; 2]>()));
|
|
|
|
|
}
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
let token = crate::device_tokens::create(&self.config.data_dir, &name).await?;
|
|
|
|
|
Ok(serde_json::json!({ "name": name, "token": token }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) async fn handle_auth_list_device_tokens(&self) -> Result<serde_json::Value> {
|
|
|
|
|
let tokens = crate::device_tokens::list(&self.config.data_dir).await;
|
|
|
|
|
Ok(serde_json::json!(tokens
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|t| serde_json::json!({ "name": t.name, "created": t.created }))
|
|
|
|
|
.collect::<Vec<_>>()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) async fn handle_auth_revoke_device_token(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
let name = params
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|p| p.get("name"))
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing name"))?;
|
|
|
|
|
let removed = crate::device_tokens::remove(&self.config.data_dir, name).await?;
|
|
|
|
|
Ok(serde_json::json!({ "removed": removed }))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 05:23:42 +00:00
|
|
|
pub(super) async fn handle_auth_logout(&self) -> Result<serde_json::Value> {
|
2026-03-28 11:31:48 +00:00
|
|
|
tracing::info!("[onboarding] logout");
|
2026-03-04 05:23:42 +00:00
|
|
|
Ok(serde_json::Value::Null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) async fn handle_auth_change_password(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
2026-03-12 00:19:30 +00:00
|
|
|
session_token: &Option<String>,
|
2026-03-04 05:23:42 +00:00
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
let current_password = params
|
|
|
|
|
.get("currentPassword")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing currentPassword"))?;
|
|
|
|
|
let new_password = params
|
|
|
|
|
.get("newPassword")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing newPassword"))?;
|
|
|
|
|
let also_change_ssh = params
|
|
|
|
|
.get("alsoChangeSsh")
|
|
|
|
|
.and_then(|v| v.as_bool())
|
|
|
|
|
.unwrap_or(true);
|
|
|
|
|
|
2026-06-11 00:24:32 -04:00
|
|
|
let outcome = self
|
|
|
|
|
.auth_manager
|
2026-03-04 05:23:42 +00:00
|
|
|
.change_password(current_password, new_password, also_change_ssh)
|
|
|
|
|
.await?;
|
|
|
|
|
|
2026-03-12 00:19:30 +00:00
|
|
|
// Session rotation: invalidate all other sessions, rotate the caller's session
|
|
|
|
|
if let Some(token) = session_token {
|
|
|
|
|
self.session_store.invalidate_all_except(token).await;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 00:24:32 -04:00
|
|
|
Ok(serde_json::json!({
|
|
|
|
|
"success": true,
|
|
|
|
|
"session_rotated": true,
|
|
|
|
|
"ssh_updated": outcome.ssh_updated,
|
|
|
|
|
"ssh_error": outcome.ssh_error,
|
|
|
|
|
}))
|
2026-03-04 05:23:42 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 09:12:16 +00:00
|
|
|
pub(super) async fn handle_auth_is_setup(&self) -> Result<serde_json::Value> {
|
|
|
|
|
let is_setup = self.auth_manager.is_setup().await?;
|
|
|
|
|
Ok(serde_json::json!(is_setup))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) async fn handle_auth_setup(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
// Prevent re-setup if already set up
|
|
|
|
|
let is_setup = self.auth_manager.is_setup().await?;
|
|
|
|
|
if is_setup {
|
2026-03-28 11:31:48 +00:00
|
|
|
tracing::warn!("[onboarding] setup rejected — already set up");
|
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!(
|
|
|
|
|
"Already set up. Use auth.changePassword to change."
|
|
|
|
|
));
|
2026-03-26 09:12:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
let password = params
|
|
|
|
|
.get("password")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing password"))?;
|
|
|
|
|
|
|
|
|
|
if password.len() < 8 {
|
2026-03-28 11:31:48 +00:00
|
|
|
tracing::warn!("[onboarding] setup rejected — password too short");
|
2026-03-26 09:12:16 +00:00
|
|
|
return Err(anyhow::anyhow!("Password must be at least 8 characters"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.auth_manager.setup_user(password).await?;
|
2026-03-28 11:31:48 +00:00
|
|
|
tracing::info!("[onboarding] user setup complete");
|
fix: fresh-ISO feedback bug-bash — onboarding, status truthfulness, recovery, kiosk, logs
Fixes from real fresh-install feedback (Framework node .81) + its log bundle:
Backend:
- websocket: subscribe before initial snapshot — broadcasts in the gap were
silently lost, stranding clients on stale state until a hard refresh
(the "everything needs ctrl-r" bug: My Apps stuck Loading, App Store
stuck Checking, containers-scanned never arriving)
- crash recovery: check the crash marker BEFORE writing our own PID —
recovery had never run on any node (always saw its own PID and skipped);
PID-reuse guard via /proc cmdline
- boot status: pending-boot-starts registry (recovery, stack recovery,
reconciler, adoption) — scanner overlays queued-but-down apps as
Restarting instead of Stopped after a reboot; scanner-authored
Restarting resolves immediately on a settled scan (no transitional wedge)
- install deps: bounded wait (36x5s) when a dependency is installed but
still starting ("Waiting for Bitcoin to start…") instead of instant
rejection; dependency-gate rejections remove the optimistic entry (no
phantom Stopped tile) and surface as a notification
- seed backup: auth.setup persists the onboarding mnemonic as the
encrypted seed backup (reveal previously failed on EVERY node — nothing
ever wrote master_seed.enc); seed.restore stashes too; error sanitizer
lets seed/2FA errors through instead of "Check server logs"
- lnd: bitcoind.rpchost resolved from the running Bitcoin variant
(hardcoded bitcoin-knots broke Core nodes); manifest uses derived_env
- bitcoin status: clean human message for connection-reset/startup; raw
URLs + os-error chains no longer reach the app card
- fedimint-clientd: chown /var/lib/archipelago/fmcd to 1000:1000 (root-
created dir crash-looped the rootless container, EACCES) — first-boot
script + pre-start self-heal
- log volume (>1GB/day on a day-old node): journald caps drop-in (ISO +
bootstrap self-heal), bitcoind -printtoconsole=0 everywhere (90% of the
journal was IBD UpdateTip spam), tracing default debug→info
Frontend:
- Login: Enter advances to confirm field then submits; submit always
clickable with inline errors (was silently disabled on mismatch);
Restart Onboarding needs a confirming second click (the mismatch →
"onboarding restarted" trap)
- sync store: 30s state reconciliation + refetch on re-entrant connect;
20s containers-scanned escape hatch so Checking can never show forever;
fresh empty node reaches the real "no apps yet" state
- intro video: CRF20 re-encode (SSIM 0.988) + faststart — moov was at EOF
so playback needed the full 15MB first (the intro lag)
- backgrounds: 10 heaviest JPEGs → WebP q90 (9.4MB→6.6MB); 7 stayed JPEG
(WebP larger on noisy sources)
- Web5ConnectedNodes: drop unused template ref that failed vue-tsc -b
ISO/kiosk:
- nginx: /assets/ 404s no longer cached immutable for a year; HTTPS block
gained the missing /assets/ location (served index.html as images)
- kiosk: launcher/service spliced from configs/ at ISO build (stale
heredoc force-disabled GPU); MemoryHigh/Max 1200/1500→2200/2800M (kiosk
rode the reclaim throttle = the lag); firmware-intel-graphics +
firmware-amd-graphics (trixie split DMC blobs out of misc-nonfree)
Verified: cargo test 898/898 green, npm run build green with dist
contents confirmed (webp refs, lnd.png, faststart video, new strings).
Handover for ISO build + deploy: docs/HANDOVER-2026-07-02-iso-feedback.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 08:00:39 -04:00
|
|
|
|
2026-07-17 11:31:27 -04:00
|
|
|
// The install-time password must also become the OS login for the
|
|
|
|
|
// archipelago user — otherwise the console/SSH keeps the image default
|
|
|
|
|
// ("archipelago") after the user has picked a real password (#97).
|
|
|
|
|
// Best-effort: a failure here must not break onboarding.
|
|
|
|
|
match crate::auth::change_ssh_password(password).await {
|
|
|
|
|
Ok(()) => tracing::info!("[onboarding] system login password synced"),
|
|
|
|
|
Err(e) => tracing::warn!("[onboarding] system login password sync failed: {e}"),
|
|
|
|
|
}
|
|
|
|
|
|
fix: fresh-ISO feedback bug-bash — onboarding, status truthfulness, recovery, kiosk, logs
Fixes from real fresh-install feedback (Framework node .81) + its log bundle:
Backend:
- websocket: subscribe before initial snapshot — broadcasts in the gap were
silently lost, stranding clients on stale state until a hard refresh
(the "everything needs ctrl-r" bug: My Apps stuck Loading, App Store
stuck Checking, containers-scanned never arriving)
- crash recovery: check the crash marker BEFORE writing our own PID —
recovery had never run on any node (always saw its own PID and skipped);
PID-reuse guard via /proc cmdline
- boot status: pending-boot-starts registry (recovery, stack recovery,
reconciler, adoption) — scanner overlays queued-but-down apps as
Restarting instead of Stopped after a reboot; scanner-authored
Restarting resolves immediately on a settled scan (no transitional wedge)
- install deps: bounded wait (36x5s) when a dependency is installed but
still starting ("Waiting for Bitcoin to start…") instead of instant
rejection; dependency-gate rejections remove the optimistic entry (no
phantom Stopped tile) and surface as a notification
- seed backup: auth.setup persists the onboarding mnemonic as the
encrypted seed backup (reveal previously failed on EVERY node — nothing
ever wrote master_seed.enc); seed.restore stashes too; error sanitizer
lets seed/2FA errors through instead of "Check server logs"
- lnd: bitcoind.rpchost resolved from the running Bitcoin variant
(hardcoded bitcoin-knots broke Core nodes); manifest uses derived_env
- bitcoin status: clean human message for connection-reset/startup; raw
URLs + os-error chains no longer reach the app card
- fedimint-clientd: chown /var/lib/archipelago/fmcd to 1000:1000 (root-
created dir crash-looped the rootless container, EACCES) — first-boot
script + pre-start self-heal
- log volume (>1GB/day on a day-old node): journald caps drop-in (ISO +
bootstrap self-heal), bitcoind -printtoconsole=0 everywhere (90% of the
journal was IBD UpdateTip spam), tracing default debug→info
Frontend:
- Login: Enter advances to confirm field then submits; submit always
clickable with inline errors (was silently disabled on mismatch);
Restart Onboarding needs a confirming second click (the mismatch →
"onboarding restarted" trap)
- sync store: 30s state reconciliation + refetch on re-entrant connect;
20s containers-scanned escape hatch so Checking can never show forever;
fresh empty node reaches the real "no apps yet" state
- intro video: CRF20 re-encode (SSIM 0.988) + faststart — moov was at EOF
so playback needed the full 15MB first (the intro lag)
- backgrounds: 10 heaviest JPEGs → WebP q90 (9.4MB→6.6MB); 7 stayed JPEG
(WebP larger on noisy sources)
- Web5ConnectedNodes: drop unused template ref that failed vue-tsc -b
ISO/kiosk:
- nginx: /assets/ 404s no longer cached immutable for a year; HTTPS block
gained the missing /assets/ location (served index.html as images)
- kiosk: launcher/service spliced from configs/ at ISO build (stale
heredoc force-disabled GPU); MemoryHigh/Max 1200/1500→2200/2800M (kiosk
rode the reclaim throttle = the lag); firmware-intel-graphics +
firmware-amd-graphics (trixie split DMC blobs out of misc-nonfree)
Verified: cargo test 898/898 green, npm run build green with dist
contents confirmed (webp refs, lnd.png, faststart video, new strings).
Handover for ISO build + deploy: docs/HANDOVER-2026-07-02-iso-feedback.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 08:00:39 -04:00
|
|
|
// Persist the pending onboarding seed as the encrypted backup now that
|
|
|
|
|
// a passphrase (the login password) finally exists — otherwise "Reveal
|
|
|
|
|
// recovery phrase" has nothing to decrypt on this node, ever.
|
|
|
|
|
// Best-effort: a failure here must not break password setup.
|
|
|
|
|
match super::seed_rpc::save_pending_seed_encrypted(&self.config.data_dir, password).await {
|
|
|
|
|
Ok(true) => tracing::info!("[onboarding] encrypted seed backup saved"),
|
|
|
|
|
Ok(false) => tracing::info!(
|
|
|
|
|
"[onboarding] no pending mnemonic to back up (restored earlier or legacy node)"
|
|
|
|
|
),
|
|
|
|
|
Err(e) => tracing::warn!("[onboarding] encrypted seed backup failed: {e:#}"),
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 09:12:16 +00:00
|
|
|
Ok(serde_json::json!(true))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 05:23:42 +00:00
|
|
|
pub(super) async fn handle_auth_onboarding_complete(&self) -> Result<serde_json::Value> {
|
|
|
|
|
self.auth_manager.complete_onboarding().await?;
|
2026-03-28 11:31:48 +00:00
|
|
|
tracing::info!("[onboarding] onboarding marked complete");
|
2026-04-07 14:49:34 +01:00
|
|
|
|
|
|
|
|
// Auto-configure NostrVPN with the node's Nostr identity
|
|
|
|
|
let data_dir = self.config.data_dir.clone();
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
match crate::vpn::configure_nostr_vpn(&data_dir).await {
|
|
|
|
|
Ok(()) => tracing::info!("[onboarding] NostrVPN configured and started"),
|
|
|
|
|
Err(e) => tracing::warn!("[onboarding] NostrVPN setup (non-fatal): {}", e),
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-04 05:23:42 +00:00
|
|
|
Ok(serde_json::json!(true))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) async fn handle_auth_is_onboarding_complete(&self) -> Result<serde_json::Value> {
|
|
|
|
|
let complete = self.auth_manager.is_onboarding_complete().await?;
|
2026-03-28 11:31:48 +00:00
|
|
|
tracing::debug!("[onboarding] isOnboardingComplete={}", complete);
|
2026-03-04 05:23:42 +00:00
|
|
|
Ok(serde_json::json!(complete))
|
|
|
|
|
}
|
|
|
|
|
|
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
|
|
|
pub(super) async fn handle_auth_reset_onboarding(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
let password = params
|
|
|
|
|
.get("password")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing password — re-authentication required"))?;
|
|
|
|
|
|
|
|
|
|
let valid = self.auth_manager.verify_password(password).await?;
|
|
|
|
|
if !valid {
|
2026-03-28 11:31:48 +00:00
|
|
|
tracing::warn!("[onboarding] reset rejected — wrong password");
|
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
|
|
|
return Err(anyhow::anyhow!("Password Incorrect"));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 05:23:42 +00:00
|
|
|
self.auth_manager.reset_onboarding().await?;
|
2026-03-28 11:31:48 +00:00
|
|
|
tracing::info!("[onboarding] onboarding reset");
|
2026-03-04 05:23:42 +00:00
|
|
|
Ok(serde_json::json!(true))
|
|
|
|
|
}
|
|
|
|
|
}
|