itsgoin/crates/core/src/lib.rs
Scott Reimers 481e1c8435 Network-wide announcements signed by the bootstrap anchor posting id
New primitive: `VisibilityIntent::Announcement`, a public post whose
author MUST be the hardcoded bootstrap anchor posting identity
(`DEFAULT_ANCHOR_POSTING_ID`) and whose content carries an ed25519
signature by that key. Forged announcements (any other author, or
bad signature) are rejected by `control::receive_post` before storage
— they never enter the DB and never propagate via neighbor-manifest
diffs. Only the real anchor can publish announcements, and it does so
sparingly as part of the release deploy flow.

Uses release announcements to drive an in-app upgrade banner:
- Anchor publishes a signed `{category:release, version, channel,
  download_url, ...}` post during every deploy.
- Clients receive it via the normal CDN; `apply_announcement_if_applicable`
  stores the latest-per-category/channel in the settings kv, keyed
  e.g. `announcement:release:stable`.
- Welcome screen checks storage on startup; if the stored release
  version > CARGO_PKG_VERSION on the user's selected channel, a banner
  appears with a Download button that opens the system browser.
- Settings gets "Updates" section with Stable / Beta radio + Check-now
  button + current status line.

Core:
- `DEFAULT_ANCHOR_POSTING_ID: NodeId` constant (32 bytes, the anchor's
  current posting id — `17af141956ae...`).
- New `VisibilityIntent::Announcement` variant; feed filters in all 6
  `get_feed*` / `list_posts*` query sites updated to also exclude the
  new intent AND the pre-existing `GroupKeyDistribute` intent.
- `types::AnnouncementContent` + `ReleaseAnnouncement` structs.
- `crypto::{sign,verify}_announcement` — length-prefixed field digest
  with a "has release" 1-byte flag.
- New `announcement` module with `verify_announcement_post`,
  `apply_announcement_if_applicable`, `latest_release`,
  `build_announcement_post`, and a `StoredAnnouncement` envelope saved
  to settings so the UI can render without a full post scan.
- `Node::publish_announcement` refuses to run unless the default posting
  id equals the anchor constant — accidental use on client installs
  fails loud.

Wire / receive:
- `control::receive_post` verifies announcement signatures upfront
  alongside Control and Profile. Same pattern; same guarantees.

CLI one-shots (no daemon):
- `itsgoin <data_dir> --print-identity` — prints network_id +
  default_posting_id, exits.
- `itsgoin <data_dir> --announce --ann-category release
  --ann-channel stable --ann-version X --ann-title ... --ann-body ...
  --ann-url https://itsgoin.com/download.html` — builds + stores +
  propagates the signed post, exits.

deploy.sh:
- Now runs the announce one-shot inside the anchor-restart window
  (after binary swap, before start). The DB is free during that gap,
  so the one-shot can write without conflicting with the running
  daemon. The restarted daemon loads all storage on boot and serves
  the new announcement to pulling peers.

Tauri IPC:
- `check_release_announcement(channel)` → Option<ReleaseAnnouncementDto>
  — returns None when up-to-date.
- `get_update_channel` / `set_update_channel(channel)` — persists in
  settings kv key `ui_update_channel`; defaults to stable.
- `open_url_external(url)` — desktop-only (xdg-open / open / cmd start);
  refuses non-http(s) URLs. Android needs the opener plugin — TODO.

Frontend:
- Upgrade banner on the welcome screen, populated by
  `loadUpgradeBanner()`. Hidden when no newer release is known.
- Settings → Updates section with Stable/Beta radio + Check-now button
  + current status line.

Tests: announcement signature roundtrip; non-anchor author rejection;
non-announcement intent is a no-op. 124 / 124 core tests pass.
2026-04-23 01:50:12 -04:00

72 lines
2.3 KiB
Rust

pub mod activity;
pub mod blob;
pub mod connection;
pub mod content;
pub mod control;
pub mod crypto;
pub mod group_key_distribution;
pub mod http;
pub mod export;
pub mod identity;
pub mod import;
pub mod announcement;
pub mod network;
pub mod node;
pub mod profile;
pub mod protocol;
pub mod storage;
pub mod stun;
pub mod types;
pub mod upnp;
pub mod web;
// Re-export iroh types needed by consumers
pub use iroh::{EndpointAddr, EndpointId};
use types::NodeId;
/// Posting-identity public key of the bootstrap anchor. Only announcements
/// authored by this key are accepted by `control::receive_post` for the
/// `VisibilityIntent::Announcement` intent. Hardcoded so clients cannot be
/// tricked into accepting a forged network-wide announcement.
pub const DEFAULT_ANCHOR_POSTING_ID: NodeId = [
0x17, 0xaf, 0x14, 0x19, 0x56, 0xae, 0x0b, 0x50,
0xdc, 0x1c, 0xb9, 0x24, 0x8c, 0xad, 0xf5, 0xfc,
0xa3, 0x71, 0xea, 0x2d, 0x85, 0x31, 0xac, 0x9a,
0xdd, 0x3c, 0x03, 0xca, 0xff, 0xc6, 0x14, 0x41,
];
/// Parse a connect string "nodeid_hex@ip:port" or "nodeid_hex@host:port" or bare "nodeid_hex"
/// into (NodeId, EndpointAddr). Supports DNS hostnames via `ToSocketAddrs`.
/// Shared utility used by CLI, Tauri, and bootstrap.
pub fn parse_connect_string(s: &str) -> anyhow::Result<(NodeId, EndpointAddr)> {
use std::net::ToSocketAddrs;
if let Some((id_hex, addr_str)) = s.split_once('@') {
let nid = parse_node_id_hex(id_hex)?;
let endpoint_id = EndpointId::from_bytes(&nid)?;
let all_addrs: Vec<std::net::SocketAddr> = addr_str
.to_socket_addrs()?
.collect();
if all_addrs.is_empty() {
anyhow::bail!("could not resolve address: {}", addr_str);
}
let mut addr = EndpointAddr::from(endpoint_id);
for sock_addr in all_addrs {
addr = addr.with_ip_addr(sock_addr);
}
Ok((nid, addr))
} else {
let nid = parse_node_id_hex(s)?;
let endpoint_id = EndpointId::from_bytes(&nid)?;
Ok((nid, EndpointAddr::from(endpoint_id)))
}
}
/// Parse a hex-encoded node ID string into NodeId bytes.
pub fn parse_node_id_hex(hex_str: &str) -> anyhow::Result<NodeId> {
let bytes = hex::decode(hex_str)?;
let id: NodeId = bytes
.try_into()
.map_err(|v: Vec<u8>| anyhow::anyhow!("expected 32 bytes, got {}", v.len()))?;
Ok(id)
}