release(v1.7.16-alpha): bidirectional + transitive federation, no self-peering

Federation join flow now notifies the inviter with the joiner's name and
immediately bumps state so the Federation UI reloads without a manual
Sync click. Accepting an invite that points back at the local node is
rejected up front (DID/pubkey/onion match). After a peer joins, we spawn
a transitive sync that pulls the new peer's federated peer hints so all
nodes in the federation learn about each other as Observer entries.
Federation.vue polls every 5s while mounted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-04-20 18:12:02 -04:00
parent 0fad7ee431
commit 3cbfcabedf
12 changed files with 357 additions and 28 deletions

2
core/Cargo.lock generated
View File

@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "archipelago"
version = "1.7.15-alpha"
version = "1.7.16-alpha"
dependencies = [
"anyhow",
"archipelago-container",

View File

@ -1,6 +1,6 @@
[package]
name = "archipelago"
version = "1.7.15-alpha"
version = "1.7.16-alpha"
edition = "2021"
description = "Archipelago Bitcoin Node OS - Native backend"
authors = ["Archipelago Team"]

View File

@ -79,6 +79,7 @@ impl RpcHandler {
let local_did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
let local_onion = data.server_info.tor_address.clone().unwrap_or_default();
let local_pubkey = data.server_info.pubkey.clone();
let local_name = data.server_info.name.clone();
let identity_dir = self.config.data_dir.join("identity");
let node_identity = identity::NodeIdentity::load_or_create(&identity_dir).await?;
@ -90,6 +91,7 @@ impl RpcHandler {
&local_onion,
&local_pubkey,
local_fips_npub.as_deref(),
local_name.as_deref(),
|data| node_identity.sign(data),
)
.await?;
@ -447,6 +449,38 @@ impl RpcHandler {
.get("fips_npub")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
// Optional, unsigned: peer's display name. Display-only — identity
// claims are anchored on the signed did/pubkey below.
let incoming_name = params
.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
// Reject self-peering. If somehow our own did / onion / pubkey
// comes back at us (misconfigured invite, gossip loop), adding
// the entry causes sync loops where the node syncs with itself
// forever. Drop it quietly — no useful recovery path.
let (own_data, _) = self.state_manager.get_snapshot().await;
let own_did_result =
identity::did_key_from_pubkey_hex(&own_data.server_info.pubkey).ok();
let own_onion_trim = own_data
.server_info
.tor_address
.as_deref()
.unwrap_or("")
.trim_end_matches(".onion")
.to_string();
let incoming_onion_trim = onion.trim_end_matches(".onion");
if own_did_result.as_deref() == Some(did)
|| pubkey == own_data.server_info.pubkey
|| (!own_onion_trim.is_empty() && own_onion_trim == incoming_onion_trim)
{
tracing::warn!(
peer_did = %did,
"Rejected peer-joined: inbound identity matches this node"
);
anyhow::bail!("Refusing to peer with self");
}
// Verify ed25519 signature to prevent federation spoofing (H2 security fix)
let signature = params.get("signature").and_then(|v| v.as_str());
@ -471,11 +505,12 @@ impl RpcHandler {
let nodes = federation::load_nodes(&self.config.data_dir).await?;
if let Some(existing) = nodes.iter().find(|n| n.did == did) {
// If already known but missing onion/pubkey/fips_npub, update them
// If already known but missing onion/pubkey/fips_npub/name, update them
let needs_onion = existing.onion.is_empty();
let needs_pubkey = existing.pubkey.is_empty();
let needs_fips = existing.fips_npub.is_none() && fips_npub.is_some();
if needs_onion || needs_pubkey || needs_fips {
let needs_name = existing.name.is_none() && incoming_name.is_some();
if needs_onion || needs_pubkey || needs_fips || needs_name {
let mut updated = existing.clone();
if needs_onion && !onion.is_empty() {
updated.onion = onion.to_string();
@ -486,6 +521,9 @@ impl RpcHandler {
if needs_fips {
updated.fips_npub = fips_npub.clone();
}
if needs_name {
updated.name = incoming_name.clone();
}
updated.last_seen = Some(chrono::Utc::now().to_rfc3339());
federation::update_node(&self.config.data_dir, &updated).await?;
info!(peer_did = %did, peer_onion = %onion, "Updated existing peer with fresh identity fields");
@ -497,7 +535,7 @@ impl RpcHandler {
did: did.to_string(),
pubkey: pubkey.to_string(),
onion: onion.to_string(),
name: None,
name: incoming_name.clone(),
trust_level: TrustLevel::Trusted,
added_at: chrono::Utc::now().to_rfc3339(),
last_seen: None,
@ -512,9 +550,38 @@ impl RpcHandler {
// Mirror into mesh state so the inbound peer is addressable from
// the chat UI without waiting for the next mesh restart.
self.register_federation_peer_in_mesh(pubkey, did, None)
self.register_federation_peer_in_mesh(pubkey, did, incoming_name.as_deref())
.await;
// Bump the data-model revision so any Federation view with an
// open WebSocket reloads its node list without waiting for the
// user to click Sync.
let (data, _) = self.state_manager.get_snapshot().await;
self.state_manager.update_data(data).await;
// Transitive discovery: spawn a task that pulls the new peer's
// state (its own federated peers end up as Observer entries on
// our side) so after a join every existing peer in our list is
// aware of the newcomer via the next pair of syncs, without the
// user clicking anything. Best-effort; errors are logged only.
let data_dir = self.config.data_dir.clone();
let new_peer_did = did.to_string();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
if let Err(e) = crate::federation::sync_with_peer_by_did(
&data_dir,
&new_peer_did,
)
.await
{
tracing::debug!(
peer_did = %new_peer_did,
error = %e,
"Transitive sync on peer-joined failed (non-fatal)"
);
}
});
Ok(serde_json::json!({ "accepted": true }))
}

View File

@ -282,6 +282,7 @@ impl RpcHandler {
let local_fips_npub = crate::identity::fips_npub(&identity_dir2)
.await
.unwrap_or(None);
let local_name = data.server_info.name.clone();
match crate::federation::accept_invite(
&self.config.data_dir,
invite_code,
@ -289,6 +290,7 @@ impl RpcHandler {
&local_onion,
&local_pubkey,
local_fips_npub.as_deref(),
local_name.as_deref(),
|bytes| node_identity.sign(bytes),
)
.await

View File

@ -121,6 +121,7 @@ pub async fn accept_invite(
local_onion: &str,
local_pubkey: &str,
local_fips_npub: Option<&str>,
local_name: Option<&str>,
sign_fn: impl FnOnce(&[u8]) -> String,
) -> Result<FederatedNode> {
let ParsedInvite {
@ -131,6 +132,20 @@ pub async fn accept_invite(
fips_npub,
} = parse_invite(code)?;
// Refuse self-peering. If the invite's did / onion / pubkey matches
// our own, adding it pollutes the federation list with a node that
// sees itself as its own peer and causes sync loops. The user
// almost certainly pasted the wrong invite.
if did == local_did || pubkey == local_pubkey || {
let a = onion.trim_end_matches(".onion");
let b = local_onion.trim_end_matches(".onion");
!a.is_empty() && a == b
} {
anyhow::bail!(
"Refusing to federate with self — invite points at this node's own did / onion / pubkey"
);
}
// Make accept idempotent: drop any existing entry that conflicts with
// this invite — same DID (same node, refreshing the link), same onion
// (node rotated identity but kept its hidden service), or same pubkey
@ -190,6 +205,7 @@ pub async fn accept_invite(
local_onion,
local_pubkey,
local_fips_npub,
local_name,
sign_fn,
)
.await;
@ -201,20 +217,22 @@ pub async fn accept_invite(
/// Prefers FIPS (if the remote advertised an npub in their invite) and
/// falls back to Tor. Signs the message with our ed25519 key so the
/// remote peer can verify authenticity regardless of transport.
async fn notify_join(
pub(crate) async fn notify_join(
remote_onion: &str,
remote_fips_npub: Option<&str>,
local_did: &str,
local_onion: &str,
local_pubkey: &str,
local_fips_npub: Option<&str>,
local_name: Option<&str>,
sign_fn: impl FnOnce(&[u8]) -> String,
) -> Result<()> {
// Sign the canonical message: "peer-joined:{did}:{onion}:{pubkey}"
// Signature domain intentionally unchanged — fips_npub is carried
// as an unsigned informational field. The FIPS daemon's own Noise
// handshake authenticates the actual transport session, so a
// stripped/substituted npub here merely downgrades the path to Tor.
// Signature domain intentionally unchanged — fips_npub + name are
// carried as unsigned informational fields. Name is display-only
// (any identity claim is anchored on the signed did/pubkey); the
// FIPS daemon's own Noise handshake authenticates the transport
// session regardless of the advertised npub.
let sign_data = format!("peer-joined:{}:{}:{}", local_did, local_onion, local_pubkey);
let signature = sign_fn(sign_data.as_bytes());
@ -227,6 +245,9 @@ async fn notify_join(
if let Some(npub) = local_fips_npub {
params["fips_npub"] = serde_json::Value::String(npub.to_string());
}
if let Some(name) = local_name {
params["name"] = serde_json::Value::String(name.to_string());
}
let body = serde_json::json!({
"method": "federation.peer-joined",

View File

@ -17,5 +17,5 @@ pub use storage::{
add_node, fips_npub_for_onion, load_nodes, record_peer_transport, remove_node, save_nodes,
set_trust_level, update_node,
};
pub use sync::{build_local_state, deploy_to_peer, sync_with_peer};
pub use sync::{build_local_state, deploy_to_peer, sync_with_peer, sync_with_peer_by_did};
pub use types::{AppStatus, FederatedNode, NodeStateSnapshot, TrustLevel};

View File

@ -82,6 +82,30 @@ pub async fn sync_with_peer(
Ok(state)
}
/// Convenience wrapper: look up a federated peer by DID, derive our
/// own local_did / signing context from the node identity on disk, and
/// call sync_with_peer. Used by transitive-discovery code paths where
/// the caller only knows the peer's DID (e.g. the peer-joined RPC's
/// follow-up task).
pub async fn sync_with_peer_by_did(
data_dir: &Path,
peer_did: &str,
) -> Result<NodeStateSnapshot> {
let nodes = super::storage::load_nodes(data_dir).await?;
let peer = nodes
.into_iter()
.find(|n| n.did == peer_did)
.ok_or_else(|| anyhow::anyhow!("Unknown federation peer: {}", peer_did))?;
let identity_dir = data_dir.join("identity");
let node_identity =
crate::identity::NodeIdentity::load_or_create(&identity_dir).await?;
let local_pubkey_hex = node_identity.pubkey_hex();
let local_did = crate::identity::did_key_from_pubkey_hex(&local_pubkey_hex)?;
sync_with_peer(data_dir, &peer, &local_did, |data| node_identity.sign(data)).await
}
/// Merge peers advertised by a Trusted federated node into our own
/// federation list. New peers are added at `Observer` trust (not
/// Trusted — that requires a direct invite). Existing peers get their

View File

@ -142,7 +142,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { rpcClient } from '@/api/rpc-client'
import { useTransportStore } from '@/stores/transport'
import { useAppStore } from '@/stores/app'
@ -537,6 +537,8 @@ async function rotateDid(password: string) {
}
}
let autoRefreshTimer: ReturnType<typeof setInterval> | null = null
onMounted(async () => {
loadNodes()
loadDwnStatus()
@ -549,5 +551,16 @@ onMounted(async () => {
} catch {
// Self DID not available
}
autoRefreshTimer = setInterval(() => {
loadNodes()
loadPendingRequests()
}, 5000)
})
onUnmounted(() => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer)
autoRefreshTimer = null
}
})
</script>

View File

@ -180,6 +180,207 @@ init()
</button>
</div>
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
<!-- v1.7.16-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.16-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Federation is now bidirectional and instant. When someone joins using your invite code, their node appears on your Federation page automatically no need for the inviter to click Sync or wait for the next poll. Names and node details populate within seconds of the handshake finishing.</p>
<p>New nodes can no longer federate with themselves. Accepting an invite that points back at the local node (by DID, public key, or onion address) is rejected up front, so self-peering no longer clutters the node list with a duplicate card.</p>
<p>Transitive discovery: if nodes A and B are already federated and node C joins A, all three nodes now learn about each other. The new peer is pulled in as an Observer entry on existing federation members, so you can promote to Trusted with one click instead of trading a second invite code.</p>
<p>The Federation page auto-refreshes every five seconds while it's open. Status changes, new peers, and incoming join requests surface on their own clicking Sync remains available for an on-demand pull.</p>
</div>
</div>
<!-- v1.7.15-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.15-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Updates survive network hiccups. Downloads now resume from exactly where a dropped connection left off, and retry up to 6 times with increasing gaps between attempts, instead of restarting from byte zero or giving up.</p>
<p>The download progress bar now shows real progress. Instead of a fake number that creeps to 95% and freezes, you see the actual bytes arriving, and it continues to update correctly even if you navigate away and come back.</p>
<p>Update check itself retries on slow responses. If git.tx1138.com is momentarily overloaded, the node tries three times with a five-second wait between attempts before concluding you're up to date.</p>
</div>
</div>
<!-- v1.7.14-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.14-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Installing an update now shows a full-screen progress overlay with the Archipelago logo, a status message, and an animated bar. The page reloads itself automatically once the new version is up no manual refresh. If something stalls, a 'Reload now' button appears after a few minutes.</p>
<p>Download progress no longer looks frozen near the end. The bar pauses at 95% with a 'Finishing download — verifying checksum…' message and spinner while the last bytes arrive and are hashed.</p>
<p>FIPS Reconnect now genuinely tries to fix the anchor. It runs a proper recovery sequence (stop start wait for the bootstrap window check peers) and tells you the likely reason it's still unreachable — corrupt identity key, seed not unlocked, network blocking UDP, or the anchor server being down — instead of a generic 'try again'.</p>
<p>Healed a latent FIPS identity bug: the public-key file was being written in text form (an 'npub1…' string) on some nodes, which the daemon couldn't parse and silently authenticated with a garbage key. The Reconnect button now rewrites the file in the correct binary format and re-installs the config before restarting — nodes stuck with no peers for 'no reason' should come back online.</p>
<p>AIUI (Claude sidebar) is back. The installer now ships AIUI in the frontend bundle and preserves it across future updates it was being wiped on every OTA because it lived outside the Vue build.</p>
<p>Installing a big app (IndeedHub, Bitcoin, Penpot) no longer gives up early and shows 'didn't work' while the download is still running in the background. The client waits up to 45 minutes for the install pipeline to finish.</p>
<p>'Rollback to Previous' is now labelled 'Rollback Available' clearer that it's a choice you have, not a status you're stuck with.</p>
</div>
</div>
<!-- v1.7.13-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.13-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>App catalog now loads reliably. Before, the Marketplace / Discover page couldn't fetch the catalog of apps because the upstream host wasn't sending the right CORS headers and the node's security policy didn't allow the fallback URL either. The node now fetches the catalog server-side and serves it same-origin to the browser no more blank app lists.</p>
</div>
</div>
<!-- v1.7.12-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.12-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Nothing new version bump so freshly-installed nodes (from the 1.7.11 ISO) have something to OTA down, confirming the end-to-end update pipeline out of the box.</p>
</div>
</div>
<!-- v1.7.11-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.11-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>OTA proof release first version where Install Update should run clean from the UI with no manual steps. Click it and watch the sidebar flip to 1.7.11-alpha on its own.</p>
</div>
</div>
<!-- v1.7.10-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.10-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Install Update actually applies now. The installer had to write into system folders that the backend service was sandboxed out of every earlier 'Failed to apply update' was a layer of that onion. Fixed by running the file swaps in a separate system context.</p>
<p>FIPS status on the Home and Server pages now reflects whether the public anchor is reachable. You'll see 'Active · N peers' (green) when healthy or 'No anchor' (orange) when the network is blocking the bootstrap same signal as the full FIPS card.</p>
<p>Pasting an https:// URL into the profile picture or banner now previews correctly. Before, if the URL failed to load, the UI would silently blank out instead of showing your initial as a placeholder.</p>
<p>Uploaded profile pictures under 64 KB are now embedded directly in your Nostr profile (as a data URL), so any Nostr client can see them not just ones routing over Tor. Larger uploads keep the onion URL for now, with a hint to paste a public URL for wider visibility.</p>
</div>
</div>
<!-- v1.7.9-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.9-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>OTA verification release nothing new to see. Click Install Update, grab a coffee, and watch the sidebar flip to 1.7.9-alpha on its own. If this one works end to end, the pipeline is solid and future updates will flow the same way.</p>
</div>
</div>
<!-- v1.7.8-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.8-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Install Update finally works end-to-end over the air. The installer was trying to overwrite the running backend binary with a tool that fails on in-use files (ETXTBSY) swapped it for an atomic rename, which the kernel allows on a live executable. Every previous 'Failed to apply update' attempt was this one root cause.</p>
</div>
</div>
<!-- v1.7.7-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.7-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Over-the-air update test no feature changes, just a version bump so your node can walk through the whole update flow end-to-end using the new robust installer. Safe to apply; nothing to do afterwards.</p>
</div>
</div>
<!-- v1.7.6-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.6-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Install Update is now more robust. Each install gets its own uniquely-named staging folder and then moves files into place the previous version had a small cleanup step that could hit a transient filesystem hiccup and bail out halfway. You'll also still see a rollback folder after a successful install.</p>
<p>Dev-box OTA: nodes that build archipelago from source can now opt into the standard Download Install flow instead of Pull &amp; Rebuild, by setting ARCHIPELAGO_UPDATE_URL in the service environment. Useful when the dev machine has a checked-out repo but you want to test the regular update path.</p>
</div>
</div>
<!-- v1.7.5-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.5-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Over-the-air update test no feature changes, just a fresh version number so your node can walk through the whole update flow end-to-end: check, download, install, auto-restart. Safe to apply; nothing to do afterwards.</p>
</div>
</div>
<!-- v1.7.4-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.4-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Install Update actually installs now. Before, the final step extracted the new UI into the wrong folder and bailed with 'Failed to apply update' your node ended up backing up cleanly but never swapping in the new files. Fixed.</p>
<p>Download progress no longer overshoots 100%. You'll see the bar climb smoothly to 95% and then jump to 100% when the download actually finishes.</p>
</div>
</div>
<!-- v1.7.3-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.3-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>The version number in the sidebar now always matches the actual running version no more lying to you about being on an older release after an update.</p>
<p>FIPS Mesh card on the server page: cleaner layout on desktop (no more awkward gaps), and a one-click Reconnect button when the public anchor is unreachable it restarts the FIPS daemon so it can re-bootstrap from the anchor.</p>
<p>Profile pictures now show correctly in the identity list and editor. Before, uploaded images silently failed to render because the URL was only reachable over Tor; the UI now rewrites them to a local path while keeping the external URL for other Nostr clients.</p>
<p>Identity rows now show your Display Name first (from your Nostr profile) with the internal identity name beside it in parentheses, so you see the name other people will see not just the one you picked when creating it.</p>
</div>
</div>
<!-- v1.7.2-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.2-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Install Update now actually installs. Before, the button would back up your current version then fail with 'Failed to apply update' because the installer couldn't write into system folders.</p>
<p>The button's also been renamed to 'Install Update' (previously 'Apply Update') and the node restarts itself a moment after you click it no more manual restart step.</p>
<p>Your existing identities now show the generated avatar instead of just their initials same look as freshly created ones.</p>
<p>Everything from 1.7.0-alpha and 1.7.1-alpha carries over (default avatars on creation, one-click Save publishes to Nostr relays, public blob URLs for profile pictures, 30-minute download window, VPN peer restore on reboot, reconciler-only-repairs, filebrowser fix).</p>
</div>
</div>
<!-- v1.7.1-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.1-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Over-the-air update test same features as 1.7.0, just a fresh version number so your node can try the new download-and-apply flow end-to-end. Safe to apply; nothing to do afterwards.</p>
</div>
</div>
<!-- v1.7.0-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.0-alpha</span>
<span class="text-xs text-white/40">Apr 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Every identity now gets a personal avatar the moment it's created. Your main node identity gets a distinctive hexagonal-network icon; other identities get a colourful generated pattern unique to each one.</p>
<p>Profile editor: upload a profile picture and a banner, then tap Save your Nostr profile now goes out to the relays in one step. No more 'Save' vs 'Save &amp; Publish' confusion.</p>
<p>Profile pictures and banners you upload are now reachable by other Nostr clients across the network not just your own browser. Anyone who sees your profile on a relay can load the image.</p>
<p>Update downloads on slow connections no longer cut out right at the end. The client waits up to 30 minutes for each component instead of giving up after 15 seconds.</p>
<p>When you move a node to a new version without going through Check for Updates (for example via a reinstall or manual copy), it now reports the new version correctly instead of endlessly saying 'update available'.</p>
<p>Your VPN peers come back automatically after a reboot. No more rescanning QR codes on your phone or laptop.</p>
<p>Fresh installs stay lean only File Browser is included out of the box. Other apps wait in the Marketplace until you pick them.</p>
<p>File Browser stops rebooting itself every few hours the housekeeper now leaves it alone once it's healthy.</p>
<p>One-click 'Pull &amp; Rebuild' button works for nodes that update from source (the development path), not just the standard download path.</p>
<p>The download progress number is now clean (like 45.23%) instead of 45.270894%.</p>
</div>
</div>
<!-- v1.3.5 -->
<div>
<div class="flex items-center gap-2 mb-3">

View File

@ -1,27 +1,28 @@
{
"version": "1.7.15-alpha",
"version": "1.7.16-alpha",
"release_date": "2026-04-20",
"changelog": [
"Updates survive network hiccups. Downloads now resume from exactly where a dropped connection left off, and retry up to 6 times with increasing gaps between attempts, instead of restarting from byte zero or giving up.",
"The download progress bar now shows real progress. Instead of a fake number that creeps to 95% and freezes, you see the actual bytes arriving, and it continues to update correctly even if you navigate away and come back.",
"Update check itself retries on slow responses. If git.tx1138.com is momentarily overloaded, the node tries three times with a five-second wait between attempts before concluding you're up to date."
"Federation is bidirectional and instant. When someone joins via your invite code, their node appears on your Federation page automatically — no Sync click needed. Names and details populate within seconds of the handshake.",
"Nodes can no longer federate with themselves. Accepting an invite that points back at the local node (by DID, pubkey, or onion) is rejected up front, so self-peering no longer clutters the node list.",
"Transitive discovery: if A and B are federated and C joins A, all three nodes learn about each other. New peers arrive as Observer entries on existing federation members — promote to Trusted with one click instead of trading a second invite code.",
"The Federation page auto-refreshes every five seconds while open. Status changes, new peers, and incoming join requests surface on their own."
],
"components": [
{
"name": "archipelago",
"current_version": "1.7.14-alpha",
"new_version": "1.7.15-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.15-alpha/archipelago",
"sha256": "1070c87fd24fc56b2edcb6ea37f42fa47dfbdc9a4840151f723bbc9c081c162b",
"size_bytes": 40584792
"current_version": "1.7.15-alpha",
"new_version": "1.7.16-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.16-alpha/archipelago",
"sha256": "cd8139f0c133ff4eab9e19b27e549c8be3d150de9acb05e88d51e2158c639e7e",
"size_bytes": 40634592
},
{
"name": "archipelago-frontend-1.7.15-alpha.tar.gz",
"current_version": "1.7.14-alpha",
"new_version": "1.7.15-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.15-alpha/archipelago-frontend-1.7.15-alpha.tar.gz",
"sha256": "8e630ebaddf88ac0e0500eeb80cfea24e6cd87c41c0d6b934e66d7b7f63fd43f",
"size_bytes": 162078068
"name": "archipelago-frontend-1.7.16-alpha.tar.gz",
"current_version": "1.7.15-alpha",
"new_version": "1.7.16-alpha",
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.16-alpha/archipelago-frontend-1.7.16-alpha.tar.gz",
"sha256": "3ed599baa07f6bfe949ea6be151a250beba5dbb7699e5c2ba3783edce1c030bc",
"size_bytes": 162083568
}
]
}

Binary file not shown.