Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
604722c96c | ||
|
|
c9a70f9d2e | ||
|
|
7f258b508e | ||
|
|
2786c0727f | ||
|
|
a2ff3502bd | ||
|
|
61b380d266 | ||
|
|
81858ab630 | ||
|
|
c3b532eaf4 | ||
|
|
7fbdabf136 |
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## v1.7.128-alpha (2026-08-10)
|
||||
|
||||
- **The discovery list stops showing ghosts.** Every reinstall of a node mints a new discovery identity, and the old identity's announcement could never be removed from the public relays — nothing holds its key anymore — so the "Discoverable nodes" list slowly filled with entries that led nowhere. Announcements now expire: your node re-announces itself twice a day, each announcement carries a 48-hour expiry that relays honour, anything older than that is ignored when reading, and switching discovery off — or factory-resetting the node — actively overwrites the announcement before it can become a ghost. Old ghosts from earlier versions stop being shown immediately and age off the relays on their own.
|
||||
- **You can name your node when you make it discoverable.** Turning discovery on now asks for an optional display name — it travels inside the public announcement, so other nodes' discovery lists show "Dorian's basement node" instead of a bare npub. The name is public by construction, capped at 32 characters, and blank is fine: you list as npub only. Toggling discovery off and on remembers the name; you can clear it the same way you set it.
|
||||
- **The discoverability panel now shows what the network actually sees: your node's npub.** It previously showed your Tor address — which is precisely the thing the announcement never contains (your address stays private until you approve a peer). The npub, the identity other nodes discover you by and send peering requests to, is now displayed there with a copy button.
|
||||
- **The seed screen stops flashing while the node starts.** During first boot, the lock icon and "server starting" text blinked in and out every few seconds while the node came up — each silent retry briefly emptied the screen. The waiting state now holds steady, with its elapsed timer, until the node answers.
|
||||
- **A node that already has an identity now explains itself on the seed screen.** Reaching seed creation on a provisioned node used to surface a developer message about "the authenticated system.factory-reset". It now says what you can actually do: sign in normally, or factory-reset the node from Settings to start it over.
|
||||
- Known gaps, disclosed rather than buried: three voice-assistant ports remain open without authentication. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release; the changes were verified by operator UAT on a live node.
|
||||
|
||||
## v1.7.127-alpha (2026-08-09)
|
||||
|
||||
- **Your node now has its own assistant.** This is the first release to ship AIUI: a conversational screen that can answer from your node's own content — your films, music and files come first, the open web second — and can act on the node itself: install or remove an app, check what's running, or queue up your media, all through a fixed list of vetted actions rather than free rein. It is off-limits to your data until you say otherwise: every data category starts closed, grants are made in Settings → AI Data Access and live on the node itself, and anything that changes the node asks you to confirm in the dashboard's own chrome first — a declined action stays declined. What leaves the node is screened: your API key is stored encrypted and never written in plain text, credential-shaped strings are scrubbed from app logs before the model sees them, your public address and Wi-Fi name are stripped from network answers, web search is gated behind your login session, and cloud-bound text passes a secret scan on the way out. Three model backends are supported — Anthropic's API, a local Ollama, and pay-per-use Routstr with a hard prepaid budget ceiling — and mesh peers can reach the same loop with `!ai`.
|
||||
|
||||
Generated
+1
-1
@@ -104,7 +104,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.126-alpha"
|
||||
version = "1.7.127-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.127-alpha"
|
||||
version = "1.7.128-alpha"
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
|
||||
@@ -21,7 +21,7 @@ use anyhow::{Context, Result};
|
||||
use nostr_sdk::FromBech32;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const NOSTR_STATE_FILE: &str = "nostr_discovery_state.json";
|
||||
use crate::nostr_handshake::DISCOVERY_STATE_FILE as NOSTR_STATE_FILE;
|
||||
|
||||
/// Runtime override for `Config::nostr_discovery_enabled`. The OS-level
|
||||
/// config file is read once at boot and is OFF by default; this state file
|
||||
@@ -32,6 +32,9 @@ const NOSTR_STATE_FILE: &str = "nostr_discovery_state.json";
|
||||
struct NostrDiscoveryState {
|
||||
#[serde(default)]
|
||||
enabled: bool,
|
||||
/// Operator-chosen display name carried in the presence event.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
async fn load_discovery_state(data_dir: &std::path::Path) -> NostrDiscoveryState {
|
||||
@@ -55,10 +58,16 @@ async fn save_discovery_state(
|
||||
}
|
||||
|
||||
impl RpcHandler {
|
||||
/// Read the current runtime discoverability flag.
|
||||
/// Read the current runtime discoverability flag. Also returns the npub
|
||||
/// this node publishes as (the discoverability UI shows it — that npub,
|
||||
/// not the onion, is what's actually visible on the relays). Load-only:
|
||||
/// null until discovery keys exist.
|
||||
pub(super) async fn handle_nostr_discovery_status(&self) -> Result<serde_json::Value> {
|
||||
let state = load_discovery_state(&self.config.data_dir).await;
|
||||
Ok(serde_json::json!({ "enabled": state.enabled }))
|
||||
let npub = nostr_handshake::own_npub(&self.config.data_dir.join("identity"))
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
Ok(serde_json::json!({ "enabled": state.enabled, "npub": npub, "name": state.name }))
|
||||
}
|
||||
|
||||
/// Set the runtime discoverability flag. If turning ON, publish presence
|
||||
@@ -78,7 +87,22 @@ impl RpcHandler {
|
||||
.and_then(|v| v.as_bool())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing enabled"))?;
|
||||
|
||||
save_discovery_state(&self.config.data_dir, &NostrDiscoveryState { enabled }).await?;
|
||||
// Optional display name. Absent param = keep the stored name (so a
|
||||
// plain off/on toggle doesn't forget it); present-but-empty clears it.
|
||||
let prior = load_discovery_state(&self.config.data_dir).await;
|
||||
let name = match params.get("name") {
|
||||
Some(v) => v.as_str().and_then(nostr_handshake::clean_display_name),
|
||||
None => prior.name,
|
||||
};
|
||||
|
||||
save_discovery_state(
|
||||
&self.config.data_dir,
|
||||
&NostrDiscoveryState {
|
||||
enabled,
|
||||
name: name.clone(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
if enabled && !self.config.nostr_relays.is_empty() {
|
||||
let (data, _) = self.state_manager.get_snapshot().await;
|
||||
@@ -88,11 +112,13 @@ impl RpcHandler {
|
||||
let version = data.server_info.version.clone();
|
||||
let relays = self.handshake_relays().await;
|
||||
let tor_proxy = self.config.nostr_tor_proxy.clone();
|
||||
let publish_name = name.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = nostr_handshake::publish_presence(
|
||||
&identity_dir,
|
||||
&did,
|
||||
&version,
|
||||
publish_name.as_deref(),
|
||||
&relays,
|
||||
tor_proxy.as_deref(),
|
||||
)
|
||||
@@ -101,6 +127,21 @@ impl RpcHandler {
|
||||
tracing::warn!("Initial presence publish failed: {}", e);
|
||||
}
|
||||
});
|
||||
} else if !enabled {
|
||||
// Switching off: overwrite our presence with an empty tombstone so
|
||||
// the node disappears from other nodes' discovery lists now, not
|
||||
// at the next TTL expiry.
|
||||
let identity_dir = self.config.data_dir.join("identity");
|
||||
let relays = self.handshake_relays().await;
|
||||
let tor_proxy = self.config.nostr_tor_proxy.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
nostr_handshake::publish_tombstone(&identity_dir, &relays, tor_proxy.as_deref())
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Presence tombstone publish failed: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(serde_json::json!({ "enabled": enabled }))
|
||||
|
||||
@@ -921,6 +921,29 @@ impl RpcHandler {
|
||||
return Err(anyhow::anyhow!("Password Incorrect"));
|
||||
}
|
||||
|
||||
// Overwrite our Nostr presence with a tombstone BEFORE the wipe: the
|
||||
// discovery keys die with the identity dir, and once they're gone the
|
||||
// stale presence event can never be replaced by anyone — it would
|
||||
// list this dead install to the whole network until relays expire it.
|
||||
// Best-effort with a hard cap so a dead relay can't stall the reset.
|
||||
{
|
||||
let identity_dir = self.config.data_dir.join("identity");
|
||||
let relays = crate::nostr_relays::merged_relay_list(
|
||||
&self.config.data_dir,
|
||||
&self.config.nostr_relays,
|
||||
)
|
||||
.await;
|
||||
let _ = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(15),
|
||||
crate::nostr_handshake::publish_tombstone(
|
||||
&identity_dir,
|
||||
&relays,
|
||||
self.config.nostr_tor_proxy.as_deref(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
tracing::warn!("Factory reset initiated — wiping ALL user data and containers");
|
||||
|
||||
let data_dir = &self.config.data_dir;
|
||||
|
||||
@@ -35,6 +35,59 @@ use tracing::warn;
|
||||
|
||||
const NOSTR_SECRET_FILE: &str = "nostr_secret";
|
||||
|
||||
/// Runtime discoverability override written by the `nostr.set-discovery` RPC.
|
||||
/// Lives here (not api/rpc) so the server's heartbeat can honour the same
|
||||
/// state the toggle writes.
|
||||
pub const DISCOVERY_STATE_FILE: &str = "nostr_discovery_state.json";
|
||||
|
||||
/// How long a presence event stays valid. Published as a NIP-40 expiration
|
||||
/// tag AND enforced client-side in `discover` (relay NIP-40 support varies).
|
||||
/// Must be comfortably longer than the re-publish heartbeat (12h in
|
||||
/// server.rs) so a node that misses one heartbeat doesn't vanish: 48h
|
||||
/// tolerates three misses.
|
||||
pub const PRESENCE_TTL_SECS: u64 = 48 * 3600;
|
||||
|
||||
/// Read the runtime discovery override and the operator-chosen display name.
|
||||
/// Enabled `None` means the toggle has never been used on this node —
|
||||
/// callers fall back to the config flag.
|
||||
pub async fn discovery_overrides(data_dir: &Path) -> (Option<bool>, Option<String>) {
|
||||
let Ok(raw) = fs::read_to_string(data_dir.join(DISCOVERY_STATE_FILE)).await else {
|
||||
return (None, None);
|
||||
};
|
||||
let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {
|
||||
return (None, None);
|
||||
};
|
||||
let enabled = v.get("enabled").and_then(|e| e.as_bool());
|
||||
let name = v
|
||||
.get("name")
|
||||
.and_then(|n| n.as_str())
|
||||
.and_then(clean_display_name);
|
||||
(enabled, name)
|
||||
}
|
||||
|
||||
/// Display names travel in a PUBLIC relay event and come back from untrusted
|
||||
/// peers — normalise both directions: single line, control chars stripped,
|
||||
/// hard length cap, empty collapses to None.
|
||||
pub fn clean_display_name(raw: &str) -> Option<String> {
|
||||
let cleaned: String = raw
|
||||
.chars()
|
||||
.filter(|c| !c.is_control())
|
||||
.take(32)
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.to_string();
|
||||
(!cleaned.is_empty()).then_some(cleaned)
|
||||
}
|
||||
|
||||
/// This node's own published npub (bech32), if discovery keys exist.
|
||||
/// Load-only: never mints keys on a read.
|
||||
pub async fn own_npub(identity_dir: &Path) -> Result<Option<String>> {
|
||||
Ok(load_nostr_keys(identity_dir)
|
||||
.await?
|
||||
.map(|k| k.public_key().to_bech32().unwrap_or_default())
|
||||
.filter(|s| !s.is_empty()))
|
||||
}
|
||||
|
||||
/// Message types exchanged inside NIP-44 encrypted DMs (kind 4).
|
||||
///
|
||||
/// Note: NONE of these variants carry an onion address. The onion is only
|
||||
@@ -130,6 +183,7 @@ pub async fn publish_presence(
|
||||
identity_dir: &Path,
|
||||
did: &str,
|
||||
version: &str,
|
||||
name: Option<&str>,
|
||||
relays: &[String],
|
||||
tor_proxy: Option<&str>,
|
||||
) -> Result<()> {
|
||||
@@ -145,14 +199,20 @@ pub async fn publish_presence(
|
||||
let nostr_npub = keys.public_key().to_bech32().unwrap_or_default();
|
||||
let client = build_client(keys, tor_proxy)?;
|
||||
|
||||
let content = serde_json::json!({
|
||||
let mut fields = serde_json::json!({
|
||||
"did": did,
|
||||
"nostr_pubkey": nostr_pubkey,
|
||||
"nostr_npub": nostr_npub,
|
||||
"version": version,
|
||||
// No onion address — exchanged only via encrypted DM
|
||||
})
|
||||
.to_string();
|
||||
});
|
||||
// Operator-chosen display name (optional, already normalised). Public by
|
||||
// construction: it exists to label this node in other nodes' discovery
|
||||
// lists, so only ever include what clean_display_name lets through.
|
||||
if let Some(n) = name.and_then(clean_display_name) {
|
||||
fields["name"] = serde_json::Value::String(n);
|
||||
}
|
||||
let content = fields.to_string();
|
||||
|
||||
for url in relays {
|
||||
let _ = client.add_relay(url).await;
|
||||
@@ -164,8 +224,13 @@ pub async fn publish_presence(
|
||||
warn!("Nostr relay connection timed out after 10s, continuing anyway");
|
||||
}
|
||||
|
||||
let builder =
|
||||
EventBuilder::new(Kind::Custom(30078), content).tag(Tag::identifier("archipelago-node"));
|
||||
// NIP-40 expiration: relays that honour it garbage-collect the event if
|
||||
// this node stops heartbeating (reinstall, decommission, long outage).
|
||||
// `discover` enforces the same window client-side for relays that don't.
|
||||
let expires = Timestamp::from(Timestamp::now().as_u64() + PRESENCE_TTL_SECS);
|
||||
let builder = EventBuilder::new(Kind::Custom(30078), content)
|
||||
.tag(Tag::identifier("archipelago-node"))
|
||||
.tag(Tag::expiration(expires));
|
||||
let _ = client.send_event_builder(builder).await;
|
||||
client.disconnect().await;
|
||||
|
||||
@@ -176,6 +241,43 @@ pub async fn publish_presence(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Overwrite this node's presence with an empty tombstone (NIP-33: same
|
||||
/// author + kind + d-tag replaces). Called when discovery is switched off
|
||||
/// and — critically — during factory-reset BEFORE the keys are wiped: once
|
||||
/// the secret is gone, nothing can ever replace the stale event.
|
||||
pub async fn publish_tombstone(
|
||||
identity_dir: &Path,
|
||||
relays: &[String],
|
||||
tor_proxy: Option<&str>,
|
||||
) -> Result<()> {
|
||||
if relays.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(keys) = load_nostr_keys(identity_dir).await? else {
|
||||
return Ok(()); // never published — nothing to tombstone
|
||||
};
|
||||
let client = build_client(keys, tor_proxy)?;
|
||||
for url in relays {
|
||||
let _ = client.add_relay(url).await;
|
||||
}
|
||||
if tokio::time::timeout(Duration::from_secs(10), client.connect())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
warn!("Nostr relay connection timed out after 10s, continuing anyway");
|
||||
}
|
||||
// Tombstone also expires: after TTL the relay may drop it entirely,
|
||||
// which is the desired end state (nothing left to list).
|
||||
let expires = Timestamp::from(Timestamp::now().as_u64() + PRESENCE_TTL_SECS);
|
||||
let builder = EventBuilder::new(Kind::Custom(30078), "{}")
|
||||
.tag(Tag::identifier("archipelago-node"))
|
||||
.tag(Tag::expiration(expires));
|
||||
let _ = client.send_event_builder(builder).await;
|
||||
client.disconnect().await;
|
||||
tracing::info!("🔒 Published presence tombstone to {} relays", relays.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Discover other Archipelago nodes (presence-only — no onion addresses).
|
||||
/// Returns Nostr pubkeys and DIDs of discoverable nodes.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -186,6 +288,9 @@ pub struct DiscoverableNode {
|
||||
pub nostr_npub: String,
|
||||
pub did: String,
|
||||
pub version: String,
|
||||
/// Operator-chosen display name from the presence event. Untrusted peer
|
||||
/// input — normalised through `clean_display_name` on the way in.
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn discover_nodes(
|
||||
@@ -221,7 +326,17 @@ pub async fn discover_nodes(
|
||||
client.disconnect().await;
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
let stale_cutoff = Timestamp::from(Timestamp::now().as_u64().saturating_sub(PRESENCE_TTL_SECS));
|
||||
for event in events {
|
||||
// Client-side staleness enforcement: pre-TTL events (and events from
|
||||
// relays that ignore NIP-40) would otherwise list dead installs
|
||||
// forever — every reinstall mints a new key, so the old author can
|
||||
// never replace its own event.
|
||||
if event.created_at < stale_cutoff {
|
||||
continue;
|
||||
}
|
||||
// A tombstone ("{}" content) parses but yields no pubkey — the
|
||||
// nostr_pubkey.is_empty() guard below already drops it.
|
||||
if let Ok(content) = serde_json::from_str::<serde_json::Value>(&event.content) {
|
||||
let nostr_pubkey = content
|
||||
.get("nostr_pubkey")
|
||||
@@ -250,11 +365,16 @@ pub async fn discover_nodes(
|
||||
.ok()
|
||||
.and_then(|pk| pk.to_bech32().ok())
|
||||
.unwrap_or_default();
|
||||
let name = content
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(clean_display_name);
|
||||
nodes.push(DiscoverableNode {
|
||||
nostr_pubkey,
|
||||
nostr_npub,
|
||||
did,
|
||||
version,
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +212,15 @@ impl Server {
|
||||
|
||||
// Publish presence-only to Nostr (DID + Nostr pubkey, NO onion address).
|
||||
// Onion addresses are exchanged privately via NIP-44 encrypted DMs.
|
||||
if config.nostr_discovery_enabled && !config.nostr_relays.is_empty() {
|
||||
//
|
||||
// This is a heartbeat, not a one-shot: presence events carry a NIP-40
|
||||
// expiration of PRESENCE_TTL_SECS, so a node that stops re-publishing
|
||||
// ages out of discovery instead of lingering forever. First tick runs
|
||||
// immediately (preserving the old startup-publish behaviour); the
|
||||
// runtime toggle (nostr.set-discovery) is re-read every tick, so a
|
||||
// node switched on via the UI heartbeats too — not just ones with the
|
||||
// config flag baked in.
|
||||
{
|
||||
let identity_dir = config.data_dir.join("identity");
|
||||
let did =
|
||||
identity::did_key_from_pubkey_hex(&data.server_info.pubkey).unwrap_or_default();
|
||||
@@ -221,21 +229,36 @@ impl Server {
|
||||
// where handshake peers actually read (2026-07-22 unification).
|
||||
let data_dir_for_relays = config.data_dir.clone();
|
||||
let config_relays = config.nostr_relays.clone();
|
||||
let config_flag = config.nostr_discovery_enabled;
|
||||
let tor_proxy = config.nostr_tor_proxy.clone();
|
||||
tokio::spawn(async move {
|
||||
let relays =
|
||||
crate::nostr_relays::merged_relay_list(&data_dir_for_relays, &config_relays)
|
||||
const HEARTBEAT_SECS: u64 = 12 * 3600; // < PRESENCE_TTL_SECS/3
|
||||
loop {
|
||||
let (enabled_override, display_name) =
|
||||
nostr_handshake::discovery_overrides(&data_dir_for_relays).await;
|
||||
let enabled = enabled_override.unwrap_or(config_flag);
|
||||
if enabled {
|
||||
let relays = crate::nostr_relays::merged_relay_list(
|
||||
&data_dir_for_relays,
|
||||
&config_relays,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = nostr_handshake::publish_presence(
|
||||
&identity_dir,
|
||||
&did,
|
||||
&version,
|
||||
&relays,
|
||||
tor_proxy.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::debug!("Nostr presence publish (non-fatal): {}", e);
|
||||
if !relays.is_empty() {
|
||||
if let Err(e) = nostr_handshake::publish_presence(
|
||||
&identity_dir,
|
||||
&did,
|
||||
&version,
|
||||
display_name.as_deref(),
|
||||
&relays,
|
||||
tor_proxy.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::debug!("Nostr presence publish (non-fatal): {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(HEARTBEAT_SECS)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.127-alpha",
|
||||
"version": "1.7.128-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.127-alpha",
|
||||
"version": "1.7.128-alpha",
|
||||
"dependencies": {
|
||||
"@scure/bip39": "^2.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"private": true,
|
||||
"version": "1.7.127-alpha",
|
||||
"version": "1.7.128-alpha",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "./start-dev.sh",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"author": "Bitcoin Knots",
|
||||
"category": "money",
|
||||
"tier": "core",
|
||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:latest",
|
||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260210",
|
||||
"repoUrl": "https://github.com/bitcoinknots/bitcoin"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -884,14 +884,16 @@ class RPCClient {
|
||||
// `handshake.poll` queues inbound requests into the federation pending
|
||||
// inbox for manual approval (it does NOT auto-accept).
|
||||
|
||||
async nostrDiscoveryStatus(): Promise<{ enabled: boolean }> {
|
||||
async nostrDiscoveryStatus(): Promise<{ enabled: boolean; npub?: string | null; name?: string | null }> {
|
||||
return this.call({ method: 'nostr.discovery-status', params: {} })
|
||||
}
|
||||
|
||||
async nostrSetDiscovery(enabled: boolean): Promise<{ enabled: boolean }> {
|
||||
async nostrSetDiscovery(enabled: boolean, name?: string): Promise<{ enabled: boolean }> {
|
||||
// `name` omitted = backend keeps the stored display name; empty string
|
||||
// clears it. Only sent when the caller explicitly provides it.
|
||||
return this.call({
|
||||
method: 'nostr.set-discovery',
|
||||
params: { enabled },
|
||||
params: name === undefined ? { enabled } : { enabled, name },
|
||||
timeout: 30000,
|
||||
})
|
||||
}
|
||||
@@ -902,6 +904,7 @@ class RPCClient {
|
||||
nostr_npub: string
|
||||
did: string
|
||||
version: string
|
||||
name?: string | null
|
||||
}>
|
||||
}> {
|
||||
return this.call({ method: 'handshake.discover', params: {}, timeout: 30000 })
|
||||
|
||||
@@ -443,6 +443,7 @@
|
||||
"nodeVisibility": "Node Visibility",
|
||||
"nodeVisibilityDesc": "Control how other nodes can discover you",
|
||||
"yourTorAddress": "Your Tor address",
|
||||
"yourNodeNpub": "Your node's npub",
|
||||
"discoverableWarning": "Making your node discoverable lets other Archipelago users find and connect with you.",
|
||||
"noPeers": "No peers yet. Add a peer manually or use Discover to find nodes on Nostr.",
|
||||
"noRequests": "No pending connection requests.",
|
||||
@@ -493,6 +494,7 @@
|
||||
"failedToUpdatePrice": "Failed to update price",
|
||||
"failedToConnectPeer": "Failed to connect to peer",
|
||||
"onionAddressCopied": "Onion address copied",
|
||||
"npubCopied": "npub copied",
|
||||
"streamUrlCopied": "Stream URL copied",
|
||||
"playerError": "Unable to load media. The content may only be accessible over Tor.",
|
||||
"connectionAccepted": "Connection accepted",
|
||||
|
||||
@@ -441,6 +441,7 @@
|
||||
"nodeVisibility": "Visibilidad del nodo",
|
||||
"nodeVisibilityDesc": "Controle c\u00f3mo otros nodos pueden descubrirle",
|
||||
"yourTorAddress": "Su direcci\u00f3n Tor",
|
||||
"yourNodeNpub": "El npub de su nodo",
|
||||
"discoverableWarning": "Hacer su nodo descubrible permite que otros usuarios de Archipelago le encuentren y se conecten con usted.",
|
||||
"noPeers": "A\u00fan no hay pares. Agregue un par manualmente o use Descubrir para encontrar nodos en Nostr.",
|
||||
"noRequests": "No hay solicitudes de conexi\u00f3n pendientes.",
|
||||
@@ -491,6 +492,7 @@
|
||||
"failedToUpdatePrice": "Error al actualizar precio",
|
||||
"failedToConnectPeer": "Error al conectar con el par",
|
||||
"onionAddressCopied": "Direcci\u00f3n onion copiada",
|
||||
"npubCopied": "npub copiado",
|
||||
"streamUrlCopied": "URL de transmisi\u00f3n copiada",
|
||||
"playerError": "No se pudo cargar el contenido multimedia. Es posible que solo sea accesible a trav\u00e9s de Tor.",
|
||||
"connectionAccepted": "Conexi\u00f3n aceptada",
|
||||
|
||||
@@ -259,9 +259,11 @@ async function generateSeed() {
|
||||
loading.value = false
|
||||
waitingForServer.value = false
|
||||
} catch (err) {
|
||||
loading.value = false
|
||||
if (isServerStartingError(err)) {
|
||||
// Backend not ready yet — keep waiting, retry silently.
|
||||
// Backend not ready yet — keep waiting, retry silently. `loading` stays
|
||||
// true through the whole retry loop: dropping it here unmounts the lock
|
||||
// icon and status text for the 4s between attempts, which reads as the
|
||||
// screen flashing in and out (reported on a live install test).
|
||||
if (!waitingForServer.value) {
|
||||
waitingForServer.value = true
|
||||
startElapsedTimer()
|
||||
@@ -270,8 +272,16 @@ async function generateSeed() {
|
||||
} else {
|
||||
// Genuine failure — stop the silent loop and surface it with a manual retry.
|
||||
stopTimers()
|
||||
loading.value = false
|
||||
waitingForServer.value = false
|
||||
errorMessage.value = err instanceof Error ? err.message : 'Failed to generate seed'
|
||||
const raw = err instanceof Error ? err.message : 'Failed to generate seed'
|
||||
// The backend's provisioned-guard refusal is precise but written for
|
||||
// developers ("authenticated system.factory-reset"). Operators hit it
|
||||
// when a node that already has an identity lands on this screen —
|
||||
// translate it into what they can actually do about it.
|
||||
errorMessage.value = raw.startsWith('Not supported: this node is already provisioned')
|
||||
? 'This node already has an identity, so a new seed cannot be created. Sign in normally — or to start this node over, run a factory reset from Settings first.'
|
||||
: raw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,6 +362,21 @@ init()
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||
<!-- v1.7.128-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.128-alpha</span>
|
||||
<span class="text-xs text-white/40">August 10, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>**The discovery list stops showing ghosts.** Every reinstall of a node mints a new discovery identity, and the old identity's announcement could never be removed from the public relays — nothing holds its key anymore — so the "Discoverable nodes" list slowly filled with entries that led nowhere. Announcements now expire: your node re-announces itself twice a day, each announcement carries a 48-hour expiry that relays honour, anything older than that is ignored when reading, and switching discovery off — or factory-resetting the node — actively overwrites the announcement before it can become a ghost. Old ghosts from earlier versions stop being shown immediately and age off the relays on their own.</p>
|
||||
<p>**You can name your node when you make it discoverable.** Turning discovery on now asks for an optional display name — it travels inside the public announcement, so other nodes' discovery lists show "Dorian's basement node" instead of a bare npub. The name is public by construction, capped at 32 characters, and blank is fine: you list as npub only. Toggling discovery off and on remembers the name; you can clear it the same way you set it.</p>
|
||||
<p>**The discoverability panel now shows what the network actually sees: your node's npub.** It previously showed your Tor address — which is precisely the thing the announcement never contains (your address stays private until you approve a peer). The npub, the identity other nodes discover you by and send peering requests to, is now displayed there with a copy button.</p>
|
||||
<p>**The seed screen stops flashing while the node starts.** During first boot, the lock icon and "server starting" text blinked in and out every few seconds while the node came up — each silent retry briefly emptied the screen. The waiting state now holds steady, with its elapsed timer, until the node answers.</p>
|
||||
<p>**A node that already has an identity now explains itself on the seed screen.** Reaching seed creation on a provisioned node used to surface a developer message about "the authenticated system.factory-reset". It now says what you can actually do: sign in normally, or factory-reset the node from Settings to start it over.</p>
|
||||
<p>Known gaps, disclosed rather than buried: three voice-assistant ports remain open without authentication. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release; the changes were verified by operator UAT on a live node.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.127-alpha -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
|
||||
@@ -49,14 +49,18 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Onion address (shown when public) -->
|
||||
<div v-if="discoverEnabled && nodeOnionAddress" class="mt-4 p-3 bg-white/5 rounded-lg">
|
||||
<!-- The node's published npub (shown when discoverable) — this, not the
|
||||
onion, is what the presence event actually posts to the relays -->
|
||||
<div v-if="discoverEnabled && nodeNpub" class="mt-4 p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs text-white/50 mb-1">{{ t('web5.yourTorAddress') }}</p>
|
||||
<p class="text-xs font-mono text-white/80 truncate" :title="nodeOnionAddress">{{ nodeOnionAddress }}</p>
|
||||
<p class="text-xs text-white/50 mb-1">{{ t('web5.yourNodeNpub') }}</p>
|
||||
<p v-if="nodeName" class="text-sm text-white/90 truncate mb-0.5">{{ nodeName }}</p>
|
||||
<!-- Middle-ellipsis, never CSS truncate: the tail is the part a
|
||||
human compares against another listing, so it must stay visible -->
|
||||
<p class="text-xs font-mono text-white/80 truncate" :title="nodeNpub">{{ midNpub(nodeNpub) }}</p>
|
||||
</div>
|
||||
<button @click="copyOnionAddress" class="shrink-0 p-2 rounded-lg text-white/50 hover:text-white hover:bg-white/10 transition-colors" title="Copy">
|
||||
<button @click="copyNpub" class="shrink-0 p-2 rounded-lg text-white/50 hover:text-white hover:bg-white/10 transition-colors" title="Copy">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
||||
</svg>
|
||||
@@ -82,7 +86,8 @@
|
||||
class="p-3 bg-white/5 rounded-lg border border-white/10 flex items-start justify-between gap-3"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm text-white truncate">{{ shortNpub(node.nostr_npub) }}</div>
|
||||
<div class="text-sm text-white truncate">{{ node.name || shortNpub(node.nostr_npub) }}</div>
|
||||
<div v-if="node.name" class="text-[11px] text-white/50 font-mono truncate">{{ shortNpub(node.nostr_npub) }}</div>
|
||||
<div class="text-[11px] text-white/40 font-mono truncate">{{ node.did }}</div>
|
||||
<div class="text-[10px] text-white/30 mt-1">version {{ node.version || '?' }}</div>
|
||||
</div>
|
||||
@@ -114,6 +119,32 @@
|
||||
@send="confirmPeerRequest"
|
||||
@cancel="requestModalTarget = null"
|
||||
/>
|
||||
|
||||
<!-- Name prompt on the way to discoverable: the announcement is public,
|
||||
so the name travels with it. Blank is fine — npub-only listing. -->
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
<div v-if="showNameModal" class="fixed inset-0 z-[3000] flex items-center justify-center p-4" @click.self="cancelNameModal">
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div class="glass-card p-6 max-w-md w-full relative z-10">
|
||||
<h3 class="text-lg font-semibold text-white mb-2">Name your node</h3>
|
||||
<p class="text-sm text-white/60 mb-4">Other nodes will see this name next to your npub in their discovery list. It's public. Leave blank to list as npub only.</p>
|
||||
<input
|
||||
v-model="nameInput"
|
||||
type="text"
|
||||
maxlength="32"
|
||||
placeholder="e.g. Dorian's basement node"
|
||||
class="w-full bg-black/30 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder-white/30 focus:outline-none focus:border-orange-500/50 mb-4"
|
||||
@keyup.enter="confirmNameModal"
|
||||
/>
|
||||
<div class="flex gap-3">
|
||||
<button @click="cancelNameModal" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm">Cancel</button>
|
||||
<button @click="confirmNameModal" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm font-medium bg-orange-500/20 border-orange-500/30">Turn on discovery</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -137,16 +168,22 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const nodeVisibility = ref<VisibilityLevel>('hidden')
|
||||
const nodeOnionAddress = ref<string | null>(null)
|
||||
const nodeNpub = ref<string | null>(null)
|
||||
const nodeName = ref<string | null>(null)
|
||||
const visibilityLoading = ref(false)
|
||||
const settingVisibility = ref(false)
|
||||
const discoverEnabled = ref(false)
|
||||
// Name-prompt state: turning discovery ON routes through a small dialog so
|
||||
// the operator can (optionally) name the node before it announces itself.
|
||||
const showNameModal = ref(false)
|
||||
const nameInput = ref('')
|
||||
|
||||
interface DiscoverableNode {
|
||||
nostr_pubkey: string
|
||||
nostr_npub: string
|
||||
did: string
|
||||
version: string
|
||||
name?: string | null
|
||||
}
|
||||
|
||||
const discoveredNodes = ref<DiscoverableNode[]>([])
|
||||
@@ -154,6 +191,12 @@ const discovering = ref(false)
|
||||
const requestingPeer = ref<string | null>(null)
|
||||
const requestedPeers = ref(new Set<string>())
|
||||
|
||||
/** Own-npub display: keep the start and the FULL tail visible, ellipsis in
|
||||
* the middle. (shortNpub below stays as-is — it formats the discovered list.) */
|
||||
function midNpub(npub: string): string {
|
||||
return npub.length > 24 ? `${npub.slice(0, 12)}…${npub.slice(-10)}` : npub
|
||||
}
|
||||
|
||||
function shortNpub(npub: string): string {
|
||||
if (!npub) return 'unknown'
|
||||
return npub.length > 21 ? `${npub.slice(0, 12)}…${npub.slice(-6)}` : npub
|
||||
@@ -173,8 +216,9 @@ async function loadVisibility() {
|
||||
.catch(() => null),
|
||||
])
|
||||
discoverEnabled.value = !!disc.enabled
|
||||
nodeNpub.value = disc.npub || null
|
||||
nodeName.value = disc.name || null
|
||||
nodeVisibility.value = (vis?.visibility as VisibilityLevel) || 'hidden'
|
||||
nodeOnionAddress.value = vis?.onion_address || vis?.tor_address || null
|
||||
if (discoverEnabled.value) void discoverNodes()
|
||||
} catch {
|
||||
discoverEnabled.value = false
|
||||
@@ -185,10 +229,33 @@ async function loadVisibility() {
|
||||
|
||||
async function toggleDiscoverable(enabled: boolean) {
|
||||
if (settingVisibility.value) return
|
||||
if (enabled) {
|
||||
// Turning ON goes through the name dialog: the node is about to announce
|
||||
// itself publicly, and this is the natural moment to (optionally) name it.
|
||||
nameInput.value = nodeName.value || ''
|
||||
showNameModal.value = true
|
||||
return
|
||||
}
|
||||
await applyDiscovery(false)
|
||||
}
|
||||
|
||||
function cancelNameModal() {
|
||||
showNameModal.value = false
|
||||
// The switch never actually flipped server-side; snap the UI back.
|
||||
discoverEnabled.value = false
|
||||
}
|
||||
|
||||
async function confirmNameModal() {
|
||||
showNameModal.value = false
|
||||
// Send exactly what's in the box: text sets the name, blank clears it.
|
||||
await applyDiscovery(true, nameInput.value.trim())
|
||||
}
|
||||
|
||||
async function applyDiscovery(enabled: boolean, name?: string) {
|
||||
settingVisibility.value = true
|
||||
try {
|
||||
// Public means public: the switch drives nostr presence publishing.
|
||||
const res = await rpcClient.nostrSetDiscovery(enabled)
|
||||
const res = await rpcClient.nostrSetDiscovery(enabled, name)
|
||||
discoverEnabled.value = !!res.enabled
|
||||
// Keep the legacy visibility string in sync (cosmetic; best-effort).
|
||||
const level: VisibilityLevel = enabled ? 'public' : 'hidden'
|
||||
@@ -197,8 +264,17 @@ async function toggleDiscoverable(enabled: boolean) {
|
||||
.then(() => { nodeVisibility.value = level })
|
||||
.catch(() => {})
|
||||
emit('toast', enabled ? 'Node is now publicly discoverable' : 'Node hidden from discovery')
|
||||
if (enabled) void discoverNodes()
|
||||
else discoveredNodes.value = []
|
||||
if (enabled) {
|
||||
if (name !== undefined) nodeName.value = name || null
|
||||
// Re-read status so the npub/name shown reflect post-enable state
|
||||
// without a page reload.
|
||||
rpcClient.nostrDiscoveryStatus()
|
||||
.then((s) => { nodeNpub.value = s.npub || null; nodeName.value = s.name || null })
|
||||
.catch(() => {})
|
||||
void discoverNodes()
|
||||
} else {
|
||||
discoveredNodes.value = []
|
||||
}
|
||||
} catch {
|
||||
emit('toast', t('web5.failedToUpdateVisibility'))
|
||||
} finally {
|
||||
@@ -245,10 +321,10 @@ async function requestToPeer(node: DiscoverableNode, message?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function copyOnionAddress() {
|
||||
if (!nodeOnionAddress.value) return
|
||||
safeClipboardWrite(nodeOnionAddress.value)
|
||||
emit('toast', t('web5.onionAddressCopied'))
|
||||
function copyNpub() {
|
||||
if (!nodeNpub.value) return
|
||||
safeClipboardWrite(nodeNpub.value)
|
||||
emit('toast', t('web5.npubCopied'))
|
||||
}
|
||||
|
||||
defineExpose({ loadVisibility })
|
||||
|
||||
+20
-23
@@ -1,35 +1,32 @@
|
||||
{
|
||||
"changelog": [
|
||||
"**Your node now has its own assistant.** This is the first release to ship AIUI: a conversational screen that can answer from your node's own content — your films, music and files come first, the open web second — and can act on the node itself: install or remove an app, check what's running, or queue up your media, all through a fixed list of vetted actions rather than free rein. It is off-limits to your data until you say otherwise: every data category starts closed, grants are made in Settings → AI Data Access and live on the node itself, and anything that changes the node asks you to confirm in the dashboard's own chrome first — a declined action stays declined. What leaves the node is screened: your API key is stored encrypted and never written in plain text, credential-shaped strings are scrubbed from app logs before the model sees them, your public address and Wi-Fi name are stripped from network answers, web search is gated behind your login session, and cloud-bound text passes a secret scan on the way out. Three model backends are supported — Anthropic's API, a local Ollama, and pay-per-use Routstr with a hard prepaid budget ceiling — and mesh peers can reach the same loop with `!ai`.",
|
||||
"**Tor now tells you the truth, heals itself, and the Restart button really restarts it.** Three nodes ran for days with Tor completely dead while the dashboard said \"Connected\" — the indicator was reading a leftover address file, not the daemon, and the restart button reported success without checking. The cause was a configuration line Tor can never bind on our systems; a node could re-break itself from a single settings change. The node now refuses to write that line, checks Tor with a real connection instead of a leftover file, repairs its own Tor configuration at every start, and the Restart button only claims success once Tor is actually answering. Onion addresses that had silently never been published (BTCPay's included) come back with it.",
|
||||
"**Inviting another node as Trusted works again — on every node.** Generating a Trusted invite, or promoting a peer from the dropdown, silently failed everywhere: the security prompt that asks for your node password could never appear, because the message requesting it was being scrubbed out of the reply on its way to your browser. The prompt now opens, and if a trust change fails, the error appears inside the window you are looking at instead of hidden behind it.",
|
||||
"**The mempool explorer actually connects now.** The page loaded but sat empty forever. Three separate causes stacked up: the block index had spent days rebuilding without anything saying so, and then two different layers of the node's plumbing were dropping the live-data connection the page depends on — so everything reported healthy while your screen showed nothing. All three are fixed, and the node's own health checks now test the real connection a browser makes, so this cannot pass unnoticed again.",
|
||||
"**Apps no longer vanish after stopping cleanly.** A stopped app's container is deleted by design, but the restart policy meant an app that exited cleanly was never brought back — it simply disappeared until reinstalled. Backends now restart in every case, the node remembers what you have installed so a missing app is recreated rather than forgotten, and this release repairs the incorrect policy on apps installed by earlier versions.",
|
||||
"**Your Bitcoin node will not silently change software versions anymore.** \"Latest\" previously meant different things in different places — one path installed a newer build that deliberately halts until you make a network-rules decision, which froze one node's sync at a fixed block while it reported itself fully synced. Bitcoin Knots is now pinned to an explicit, known-good version; changing it is a decision you make, never a side effect of an update.",
|
||||
"**Smaller fixes:** the AI data-access settings now say plainly which categories the assistant can see but not act on; the transactions window's tab bar is transparent glass instead of a black block; BTCPay logins no longer fail with a server error when the node is under heavy load right at that moment.",
|
||||
"**You can now replace your Lightning connection keys from Settings, without touching a terminal.** The tokens wallet apps like Zeus use to reach your node are bearer keys: anything that has ever seen one can spend from your node until they are replaced, and there is no way to cancel one individually. Replacing them was previously a script you had to SSH in and run, which in practice meant it never happened. Settings → Lightning credentials now shows when yours were issued, which node they belong to and how many channels must survive, then does the whole job behind your node password — with a step-by-step progress list, and a refusal to call it a success unless it has confirmed your node identity and every channel came back. Your coins and channels are not touched: nothing is closed, and the wallet is never re-created. Afterwards you re-pair Zeus by scanning the Lightning app's QR code again.",
|
||||
"**Replacing those keys no longer silently breaks BTCPay Server.** BTCPay holds its own copy of the key, and that copy cannot repair itself — so a node that replaced its keys ended up with BTCPay running, healthy, and unable to take a single Lightning payment, with nothing anywhere saying why. The dashboard now updates BTCPay's copy as part of the run and restarts it around its existing data, and the Settings screen warns you if it finds a node already stuck in that state. The command-line script fixes the same gap."
|
||||
"**The discovery list stops showing ghosts.** Every reinstall of a node mints a new discovery identity, and the old identity's announcement could never be removed from the public relays — nothing holds its key anymore — so the \"Discoverable nodes\" list slowly filled with entries that led nowhere. Announcements now expire: your node re-announces itself twice a day, each announcement carries a 48-hour expiry that relays honour, anything older than that is ignored when reading, and switching discovery off — or factory-resetting the node — actively overwrites the announcement before it can become a ghost. Old ghosts from earlier versions stop being shown immediately and age off the relays on their own.",
|
||||
"**You can name your node when you make it discoverable.** Turning discovery on now asks for an optional display name — it travels inside the public announcement, so other nodes' discovery lists show \"Dorian's basement node\" instead of a bare npub. The name is public by construction, capped at 32 characters, and blank is fine: you list as npub only. Toggling discovery off and on remembers the name; you can clear it the same way you set it.",
|
||||
"**The discoverability panel now shows what the network actually sees: your node's npub.** It previously showed your Tor address — which is precisely the thing the announcement never contains (your address stays private until you approve a peer). The npub, the identity other nodes discover you by and send peering requests to, is now displayed there with a copy button.",
|
||||
"**The seed screen stops flashing while the node starts.** During first boot, the lock icon and \"server starting\" text blinked in and out every few seconds while the node came up — each silent retry briefly emptied the screen. The waiting state now holds steady, with its elapsed timer, until the node answers.",
|
||||
"**A node that already has an identity now explains itself on the seed screen.** Reaching seed creation on a provisioned node used to surface a developer message about \"the authenticated system.factory-reset\". It now says what you can actually do: sign in normally, or factory-reset the node from Settings to start it over.",
|
||||
"Known gaps, disclosed rather than buried: three voice-assistant ports remain open without authentication. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release; the changes were verified by operator UAT on a live node."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"current_version": "1.7.127-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.7.127-alpha/archipelago",
|
||||
"current_version": "1.7.128-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.7.128-alpha/archipelago",
|
||||
"name": "archipelago",
|
||||
"new_version": "1.7.127-alpha",
|
||||
"sha256": "19c5f4573e49ba5a1339a358f5d422da4c3dbf68fba3391a62588049a52da207",
|
||||
"size_bytes": 59282264
|
||||
"new_version": "1.7.128-alpha",
|
||||
"sha256": "ffc97ab91717323b467d6a8bda62c98275d81796258f5c8f05385001df799b6c",
|
||||
"size_bytes": 59866672
|
||||
},
|
||||
{
|
||||
"current_version": "1.7.127-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.7.127-alpha/archipelago-frontend-1.7.127-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.127-alpha.tar.gz",
|
||||
"new_version": "1.7.127-alpha",
|
||||
"sha256": "bedd662105e53ce800caa610cc099a47d7f0786af5fec601169a8906af760244",
|
||||
"size_bytes": 95433702
|
||||
"current_version": "1.7.128-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.7.128-alpha/archipelago-frontend-1.7.128-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.128-alpha.tar.gz",
|
||||
"new_version": "1.7.128-alpha",
|
||||
"sha256": "b3137a67c02a7cb33333f3f0b6a68cd3ad5c8d35baba3d9e099f441642054e80",
|
||||
"size_bytes": 95429928
|
||||
}
|
||||
],
|
||||
"release_date": "2026-08-09",
|
||||
"signature": "dc418fc08b2b0e288ab0f4b307562d966774d8b5ee73789229d6bef627f61013510054a89d8e314676464a55bcb631e1d1a63e0de394b2d152abd42c90f4120f",
|
||||
"release_date": "2026-08-10",
|
||||
"signature": "eb8c684ef9ebe1046c9abbcdf5a698c37b11f5630fb67062b844ad00ece913e995d41062c9c3ca432b2a47bdbb8e35c38c8ecda60d1a3e82171c8d10d910b108",
|
||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||
"version": "1.7.127-alpha"
|
||||
"version": "1.7.128-alpha"
|
||||
}
|
||||
|
||||
+20
-23
@@ -1,35 +1,32 @@
|
||||
{
|
||||
"changelog": [
|
||||
"**Your node now has its own assistant.** This is the first release to ship AIUI: a conversational screen that can answer from your node's own content — your films, music and files come first, the open web second — and can act on the node itself: install or remove an app, check what's running, or queue up your media, all through a fixed list of vetted actions rather than free rein. It is off-limits to your data until you say otherwise: every data category starts closed, grants are made in Settings → AI Data Access and live on the node itself, and anything that changes the node asks you to confirm in the dashboard's own chrome first — a declined action stays declined. What leaves the node is screened: your API key is stored encrypted and never written in plain text, credential-shaped strings are scrubbed from app logs before the model sees them, your public address and Wi-Fi name are stripped from network answers, web search is gated behind your login session, and cloud-bound text passes a secret scan on the way out. Three model backends are supported — Anthropic's API, a local Ollama, and pay-per-use Routstr with a hard prepaid budget ceiling — and mesh peers can reach the same loop with `!ai`.",
|
||||
"**Tor now tells you the truth, heals itself, and the Restart button really restarts it.** Three nodes ran for days with Tor completely dead while the dashboard said \"Connected\" — the indicator was reading a leftover address file, not the daemon, and the restart button reported success without checking. The cause was a configuration line Tor can never bind on our systems; a node could re-break itself from a single settings change. The node now refuses to write that line, checks Tor with a real connection instead of a leftover file, repairs its own Tor configuration at every start, and the Restart button only claims success once Tor is actually answering. Onion addresses that had silently never been published (BTCPay's included) come back with it.",
|
||||
"**Inviting another node as Trusted works again — on every node.** Generating a Trusted invite, or promoting a peer from the dropdown, silently failed everywhere: the security prompt that asks for your node password could never appear, because the message requesting it was being scrubbed out of the reply on its way to your browser. The prompt now opens, and if a trust change fails, the error appears inside the window you are looking at instead of hidden behind it.",
|
||||
"**The mempool explorer actually connects now.** The page loaded but sat empty forever. Three separate causes stacked up: the block index had spent days rebuilding without anything saying so, and then two different layers of the node's plumbing were dropping the live-data connection the page depends on — so everything reported healthy while your screen showed nothing. All three are fixed, and the node's own health checks now test the real connection a browser makes, so this cannot pass unnoticed again.",
|
||||
"**Apps no longer vanish after stopping cleanly.** A stopped app's container is deleted by design, but the restart policy meant an app that exited cleanly was never brought back — it simply disappeared until reinstalled. Backends now restart in every case, the node remembers what you have installed so a missing app is recreated rather than forgotten, and this release repairs the incorrect policy on apps installed by earlier versions.",
|
||||
"**Your Bitcoin node will not silently change software versions anymore.** \"Latest\" previously meant different things in different places — one path installed a newer build that deliberately halts until you make a network-rules decision, which froze one node's sync at a fixed block while it reported itself fully synced. Bitcoin Knots is now pinned to an explicit, known-good version; changing it is a decision you make, never a side effect of an update.",
|
||||
"**Smaller fixes:** the AI data-access settings now say plainly which categories the assistant can see but not act on; the transactions window's tab bar is transparent glass instead of a black block; BTCPay logins no longer fail with a server error when the node is under heavy load right at that moment.",
|
||||
"**You can now replace your Lightning connection keys from Settings, without touching a terminal.** The tokens wallet apps like Zeus use to reach your node are bearer keys: anything that has ever seen one can spend from your node until they are replaced, and there is no way to cancel one individually. Replacing them was previously a script you had to SSH in and run, which in practice meant it never happened. Settings → Lightning credentials now shows when yours were issued, which node they belong to and how many channels must survive, then does the whole job behind your node password — with a step-by-step progress list, and a refusal to call it a success unless it has confirmed your node identity and every channel came back. Your coins and channels are not touched: nothing is closed, and the wallet is never re-created. Afterwards you re-pair Zeus by scanning the Lightning app's QR code again.",
|
||||
"**Replacing those keys no longer silently breaks BTCPay Server.** BTCPay holds its own copy of the key, and that copy cannot repair itself — so a node that replaced its keys ended up with BTCPay running, healthy, and unable to take a single Lightning payment, with nothing anywhere saying why. The dashboard now updates BTCPay's copy as part of the run and restarts it around its existing data, and the Settings screen warns you if it finds a node already stuck in that state. The command-line script fixes the same gap."
|
||||
"**The discovery list stops showing ghosts.** Every reinstall of a node mints a new discovery identity, and the old identity's announcement could never be removed from the public relays — nothing holds its key anymore — so the \"Discoverable nodes\" list slowly filled with entries that led nowhere. Announcements now expire: your node re-announces itself twice a day, each announcement carries a 48-hour expiry that relays honour, anything older than that is ignored when reading, and switching discovery off — or factory-resetting the node — actively overwrites the announcement before it can become a ghost. Old ghosts from earlier versions stop being shown immediately and age off the relays on their own.",
|
||||
"**You can name your node when you make it discoverable.** Turning discovery on now asks for an optional display name — it travels inside the public announcement, so other nodes' discovery lists show \"Dorian's basement node\" instead of a bare npub. The name is public by construction, capped at 32 characters, and blank is fine: you list as npub only. Toggling discovery off and on remembers the name; you can clear it the same way you set it.",
|
||||
"**The discoverability panel now shows what the network actually sees: your node's npub.** It previously showed your Tor address — which is precisely the thing the announcement never contains (your address stays private until you approve a peer). The npub, the identity other nodes discover you by and send peering requests to, is now displayed there with a copy button.",
|
||||
"**The seed screen stops flashing while the node starts.** During first boot, the lock icon and \"server starting\" text blinked in and out every few seconds while the node came up — each silent retry briefly emptied the screen. The waiting state now holds steady, with its elapsed timer, until the node answers.",
|
||||
"**A node that already has an identity now explains itself on the seed screen.** Reaching seed creation on a provisioned node used to surface a developer message about \"the authenticated system.factory-reset\". It now says what you can actually do: sign in normally, or factory-reset the node from Settings to start it over.",
|
||||
"Known gaps, disclosed rather than buried: three voice-assistant ports remain open without authentication. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release; the changes were verified by operator UAT on a live node."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"current_version": "1.7.127-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.7.127-alpha/archipelago",
|
||||
"current_version": "1.7.128-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.7.128-alpha/archipelago",
|
||||
"name": "archipelago",
|
||||
"new_version": "1.7.127-alpha",
|
||||
"sha256": "19c5f4573e49ba5a1339a358f5d422da4c3dbf68fba3391a62588049a52da207",
|
||||
"size_bytes": 59282264
|
||||
"new_version": "1.7.128-alpha",
|
||||
"sha256": "ffc97ab91717323b467d6a8bda62c98275d81796258f5c8f05385001df799b6c",
|
||||
"size_bytes": 59866672
|
||||
},
|
||||
{
|
||||
"current_version": "1.7.127-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.7.127-alpha/archipelago-frontend-1.7.127-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.127-alpha.tar.gz",
|
||||
"new_version": "1.7.127-alpha",
|
||||
"sha256": "bedd662105e53ce800caa610cc099a47d7f0786af5fec601169a8906af760244",
|
||||
"size_bytes": 95433702
|
||||
"current_version": "1.7.128-alpha",
|
||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.7.128-alpha/archipelago-frontend-1.7.128-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.128-alpha.tar.gz",
|
||||
"new_version": "1.7.128-alpha",
|
||||
"sha256": "b3137a67c02a7cb33333f3f0b6a68cd3ad5c8d35baba3d9e099f441642054e80",
|
||||
"size_bytes": 95429928
|
||||
}
|
||||
],
|
||||
"release_date": "2026-08-09",
|
||||
"signature": "dc418fc08b2b0e288ab0f4b307562d966774d8b5ee73789229d6bef627f61013510054a89d8e314676464a55bcb631e1d1a63e0de394b2d152abd42c90f4120f",
|
||||
"release_date": "2026-08-10",
|
||||
"signature": "eb8c684ef9ebe1046c9abbcdf5a698c37b11f5630fb67062b844ad00ece913e995d41062c9c3ca432b2a47bdbb8e35c38c8ecda60d1a3e82171c8d10d910b108",
|
||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||
"version": "1.7.127-alpha"
|
||||
"version": "1.7.128-alpha"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user