38 Commits

Author SHA1 Message Date
Dorian
8b88c45262 feat(settings): per-service FIPS/Tor transport preference
Adds a user-configurable toggle for how each peer-to-peer service
reaches federated peers. Three options per service:

- Auto (default) — FIPS preferred, Tor fallback (current behavior).
- FIPS only — fail rather than fall through to Tor.
- Tor only — explicit opt-in to onion anonymity for that service.

Services covered (matching the UI rows):
- Federation — state sync, invites, peer notifications
- Peers — address/DID rotation broadcasts
- Peer Files — content catalog download/browse/preview
- Messaging — archipelago channel + mesh bridge
- Mesh File Sharing — content_ref blob fetches

Implementation:
- settings::transport — persisted struct + process-wide OnceLock handle
  (so deep call sites don't need data_dir threaded through signatures).
  On-disk file: <data_dir>/settings/transport_preferences.json; missing
  or corrupt → defaults (Auto everywhere).
- settings::transport::init() called from main.rs after config load.
- fips::dial::PeerRequest gains a .service(kind) builder; send_* checks
  the preference before choosing a transport. FIPS-only fails loudly
  when FIPS is unavailable (so users who pick it know when something
  falls back).
- Every FIPS-first migration site tags its PeerRequest with the
  matching PeerService so the toggle actually applies.
- transport.preferences + transport.set-preference RPCs added; wired
  into the dispatcher.
- neode-ui/src/views/settings/TransportPrefsCard.vue — standalone card
  with a 5-row Auto/FIPS/Tor tri-state. Not wired into Settings.vue —
  the user places components themselves (see feedback_ui_entry_points).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:44:41 -04:00
Dorian
dbd19006f2 feat(messaging,dwn,mesh): route peer messaging + DWN sync + blob fetch via FIPS first
Migrates the remaining Tor-direct peer call sites to PeerRequest so
FIPS is the default when the peer is federated and running the daemon:

- node_message::send_to_peer / check_peer_reachable: gain a
  fips_npub parameter. Error messages updated to reference both
  transports.
- Callers (api/rpc/network.rs, api/rpc/peers.rs, server health
  loop): look up fips_npub from federation storage by onion and
  pass it.
- mesh::send_typed_wire_via_federation: the spawned background POST
  for the /archipelago/mesh-typed endpoint now uses PeerRequest with
  federation-resolved fips_npub. Signature domain unchanged.
- api/rpc/mesh/typed_messages.rs fetch_blob_from_peer: blob URL
  rebuilt as (base_url, path_with_query) so PeerRequest can append
  the query string after swapping the host. Cap/exp/peer
  parameters are still signed over the content ref itself, so
  transport choice is invisible to the signature.
- network/dwn_sync.rs sync_with_peers: per-peer fips_npub lookup
  before sync_single_peer; health/pull/push each dial through
  PeerRequest, so any DWN peer known to federation gets FIPS.

Left Tor-only on purpose:
- api/rpc/identity/handlers.rs handle_identity_resolve_peer_onion —
  resolving TO a DID, no anchor yet.
- content.browse / preview calls to non-federated peers fall
  through to Tor naturally inside PeerRequest (no fips_npub → skip
  FIPS branch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:36:04 -04:00
Dorian
46350f48b6 chore(fmt): rustfmt drift cleanup across misc crates
Pure formatter output — no semantic changes. Sweeping these into their
own commit so the FIPS integration diff that follows stays scoped to
the actual feature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:57:14 -04:00
Dorian
b614c5c694 chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:

- Applies rustfmt across the tree (the bulk of the diff — untouched
  since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
    container/bitcoin_simulator.rs wildcard-in-or-pattern
    container/manifest.rs from_str rename to parse (reserved name)
    container/podman_client.rs .get(0) -> .first()
    container/runtime.rs manual += collapse
    archipelago/src/constants.rs doc-comment → module-doc
    api/rpc/package/install.rs stray /// comment above a non-item
    container/docker_packages.rs redundant field init
    streaming/advertisement.rs missing Metric import in tests
    tests/orchestration_tests.rs `vec!` in non-Vec contexts
    mesh/listener/dispatch.rs unused store_plain_message import
    api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
  stylistic lints (too_many_arguments, type_complexity, doc indent,
  enum variant prefix, wildcard-in-or, assertions-on-constants,
  drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
  of places with no correctness payoff and have been churning every
  toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
  are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
  rollback compatibility, vpn::get_nostr_vpn_status is surface-area
  for a not-yet-landed RPC.

cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
Dorian
3a52c766ac fix(mesh): single-flight send + spinner + async federation POST
Root cause of the "every bubble shows twice" complaint after the prior
dedup fix: the frontend was firing mesh.send twice per user action. A
held/repeating Enter key on the input fires a keydown per repeat, and
handleSendMessage didn't guard on mesh.sending, so both calls queued
through the store's sendQueue and both executed against the same
contact_id (backend logs show two mesh.send RPCs 13ms apart, same text).
That's why sender and receiver both saw doubles — the envelope actually
was transmitted twice.

Mesh.vue: handleSendMessage now early-returns if mesh.sending or
sendingArch is already set. Send button replaces the `...` placeholder
with a proper spinning ring (`.mesh-send-spinner`) so the held-Enter case
stops looking like the app is ignoring the user.

mesh/mod.rs: send_typed_wire_via_federation no longer blocks on the Tor
POST. Sent MeshMessage is recorded synchronously (UI bubble appears
instantly); the HTTP goes in tokio::spawn. Tor circuit setup was the
1–5s lag the user was seeing on every send to a federation peer. Delivery
failure still shows as `delivered: false` via the read-receipt path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:57:11 -04:00
Dorian
7e4fed7967 fix(mesh): dedup across transports + persistent radio-contact blocklist
Two mesh fixes bundled so the deploy lands them together:

Doubled messages (radio + federation): dedup at store_message now runs
a third cross-transport check keyed on (sender_seq, plaintext, 120s).
The existing (sender_pubkey, sender_seq) match missed the common case
where the same envelope arrives via LoRa radio (sender_pubkey looked
up from the firmware key) and again via Tor federation (sender_pubkey
= archipelago ed25519), because the two lookups disagree. The new
cross-transport match closes that gap without loosening legacy paths.

Stale contacts after clear-all: meshcore's on-device contact table is
persistent and reads back into peers on the next refresh_contacts, so
the previous "nuclear" clear wiped app state for a few seconds before
the old rows reappeared. New persistent `radio_contact_blocklist`
(mesh-ignored-radio-contacts.json) captures the pubkeys present at
clear-time; `refresh_contacts` filters them on read and the filter
survives restart. Federation-synthetic peers are excluded from the
snapshot so the list rebuilds normally on the next gossip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:02:34 -04:00
Dorian
1736f6f99e feat(mesh): server name in adverts + clear-all button + CI fix
- Mesh adverts now use the node's configured server name (e.g. "ThinkPad",
  "Arch Dev") instead of DID key fragments ("Archy-z6MkmkSB")
- Added mesh.clear-all RPC to reset peers, messages, contacts, and history
- Added "Clear All" button in Mesh UI peers panel
- Both glibc and musl builds verified

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 11:53:06 -04:00
Dorian
0c02d06a66 feat: deploy-to-target supports .253 + mesh/federation/VPN updates
- Add deploy_secondary() function for deploying to multiple LAN nodes
- --both now deploys to .198 and .253 (previously .198 only)
- Fleet deploy updated for 3 LAN nodes
- Mesh DM fixes: protocol frame format, DM-via-channel routing
- Federation pending requests, discover modal
- VPN status UI improvements
- Image versions and container specs updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 11:07:08 -04:00
Dorian
6760d11a57 feat(mesh): Telegram primitives pass + attachment transport router
Bundles the Phase 2b/3/4/5 work that accumulated across prior sessions
and the new attachment chunking router from this session. Everything
ships in one shot so the full mesh surface stays coherent on-wire.

Telegram primitives (variants 13–18, 20–22):
- Reply / Reaction / ReadReceipt / Forward / Edit / Delete
- Presence heartbeat + last-seen tracking
- ChannelInvite + ContactCard payload types
- MessageKey (sender_pubkey, sender_seq) as cross-transport identity
- Action menu, reply banner, edit banner, tombstones, (edited) marker
- Debounced auto-read-receipts on scroll + message arrival

Activated prototypes (Phase 4):
- PsbtHash send RPC
- Contacts CRUD (in-memory alias/notes/pinned/blocked)
- Outbox 📤 badge, rotate-prekeys button
- Chunked send fallback (MCIIXXTT framing) as auto-failover inside
  send_typed_wire when a typed wire exceeds the LoRa per-frame budget

Unified inbox (Phase 1):
- conversations.list + conversations.messages RPCs (UI collapse deferred)

Attachment transport router (new this session):
- ContentInline variant 23 + ContentInlinePayload carrying file bytes
  directly in the envelope for small files with no Tor path
- mesh.send-content-inline RPC — mirrors to local BlobStore, rides
  send_typed_wire which auto-chunks over MCIIXXTT framing (~2.3 KB cap)
- mesh.transport-advice RPC as single source of truth for tier
  decisions: auto-mesh / choose / tor-only / impossible
- Receive arm writes inline bytes to local BlobStore so the existing
  content_ref card renderer handles both transports uniformly
- MeshState.blob_store field + order-independent propagation from
  RpcHandler::set_blob_store / set_mesh_service
- Frontend handleAttachFile calls advice first, branches into silent
  auto-send, transport-chooser modal, Tor-only path, or red error
- Transport modal with 📡 mesh / 🧅 Tor options + ETA + disabled
  state when peer has no Tor reachability

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:40:19 -04:00
Dorian
5616bb74e6 fix(mesh): add txt_type + timestamp to CMD_SEND_CHANNEL_TXT_MSG frame
MeshCore firmware frame for cmd 0x03 is
`[cmd][txt_type][channel][timestamp_le32][text]`, not `[cmd][channel][text]`.
Missing txt_type + timestamp caused every channel broadcast to come
back with ERR_UNSUPPORTED, which broke the DM-via-channel path
entirely (nothing was reaching the radio). Bring the frame into
spec — verified against meshcore-dev/MeshCore docs/companion_protocol.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:22:20 -04:00
Dorian
164f938982 fix(mesh): route DM-via-channel on channel 0 (channel 1 unsupported)
Firmware rejected send_channel_text(1, ...) with "Unsupported command"
because channel 1 isn't configured on the device. Revert to channel 0
for the DM wrapper — the 0xD1 marker + dest_prefix header still
disambiguates DMs from plain public-channel text. Also revert
Mesh.vue publicChannel back to index 0 so user-typed broadcasts
target the same (only) working channel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:40:28 -04:00
Dorian
d514e0e5e4 fix(mesh): DM-via-channel tunnel + disable presence spam
Meshcore direct unicast silently drops between our two Archy nodes
(firmware reports flood sends with resp_code=6 but nothing arrives).
Wrap DMs as channel-1 broadcasts with a [0xD1][dest_prefix(6)][inner]
header; receivers filter by prefix and dispatch the inner payload
through the existing typed/base64/chunk ladder. Shrink chunk body to
125B so the wrapper still fits the 160B LoRa budget. Auto-heal
routing: CMD_RESET_PATH (0x0D) any type-1 contact with path_len=0 on
refresh so floods take over. send_text now returns the firmware's
flood/direct mode flag for diagnostics.

Disable the 120s presence heartbeat broadcaster — its CBOR payload
was being re-echoed as plaintext by the shared repeater, spamming
every visible node with garbled "Archy-…: av�…fstatusfonline…"
messages on channel 0. mesh.broadcast-presence RPC stays registered
but no longer transmits. Re-enable only once presence moves off the
shared broadcast path.

Also: MeshState.cmd_tx behind RwLock so stop()→start() cycles don't
fail with "command channel already consumed"; MeshService.send_cmd
helper; drop_message_by_id for control envelopes that shouldn't
appear as Sent bubbles; self_advert_name reflected into MeshStatus
after set; path_len/flags parsed out of RESP_CONTACT.

Frontend: unified inbox merges mesh peers with federation nodes by
DID/pubkey/name; hide presence/read_receipt/edit/channel_invite/
contact_card from chat stream; publicChannel index → 1 to match the
new DM-via-channel routing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:24:27 -04:00
Dorian
8ef7af985d feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:

* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
  21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
  enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
  path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
  mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
  mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
  mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
  mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
  (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
  apply_local_delete (optimistic local echo), and send_chunked_payload — an
  MC-framed base64 splitter that fires as a fallback inside send_typed_wire
  when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
  existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
  (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
  Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
  the target MessageKey pubkey against the sender's advertised pubkey_hex.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
Dorian
002032b7da fix(mesh): resolve ContentRef peer via DID + name-match fallback
Mesh peer pubkeys (LoRa advert ed25519) differ from federation node
pubkeys (archipelago identity), so matching on pubkey always missed
and attachments >160B had no transport. Match on master DID instead;
also accept an explicit peer_onion override from the frontend, which
resolves the peer by display name against federation.list-nodes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:13:36 -04:00
Dorian
bc3729d99f fix(mesh): route ContentRef over federation when >160B
mesh.send-content was failing with "Message too large for LoRa: 624
bytes (max 160)" because a single ContentRef envelope (cid + onion +
cap_token + thumb) dwarfs a LoRa frame. Add a federation Tor fallback:

- New POST /archipelago/mesh-typed endpoint accepts
  {from_pubkey, typed_envelope_b64, signature}, verifies ed25519 over
  the raw wire bytes, and injects the decoded envelope into MeshState
  via a new MeshService::inject_typed_from_federation helper. This
  shares the same dispatch match as LoRa receives via a new pub(crate)
  handle_typed_envelope_direct extracted from handle_typed_message.
- MeshService::send_typed_wire_via_federation POSTs the signed wire to
  a peer's onion over TOR_SOCKS_PROXY and records a local Sent record.
- handle_mesh_send_content looks up the peer's onion in federation
  storage and routes via federation when available, falling back to
  LoRa only when no federation presence is known (still fails on
  oversized — chunking is Phase 4).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:37:48 -04:00
Dorian
a360f90647 feat(mesh): MessageKey + Reply/Reaction variants and sender seq (Phase 2a)
Per-target outbound seq counter on MeshState allocates a monotonic seq
before each typed envelope is encoded; send_typed_wire +
send_channel_typed_wire record it (alongside our own pubkey_hex) on the
Sent MeshMessage so the local store carries the same MessageKey the
receiver will see. TypedEnvelope.with_seq lets the RPC layer stamp the
seq AFTER signing (signature covers t/v/ts only).

New MessageKey struct pairs sender_pubkey+sender_seq as the stable
cross-transport identity. Adds variants 13 Reply and 14 Reaction with
ReplyPayload {target, text} and ReactionPayload {target, emoji}, plus
mesh.send-reply / mesh.send-reaction RPCs and receive-side dispatch
arms that store the payload json for the UI to index.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:19:30 -04:00
Dorian
dce5084451 feat(mesh): ContentRef typed variant + send/fetch RPCs (Phase 3b)
Adds attachment sharing over the mesh: a ContentRef envelope (variant 19)
carries the blob CID, size, mime, optional thumb/caption, and a per-peer
HMAC capability URL so the recipient fetches the full blob out-of-band via
`GET {sender_onion}/blob/{cid}?cap=..&exp=..&peer=..`. BlobStore is shared
from ApiHandler into RpcHandler so mesh.send-content and mesh.fetch-content
(reqwest via TOR_SOCKS_PROXY) hit the same store and cap_key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:10:49 -04:00
Dorian
de1b25cc78 feat(mesh): MessageKey foundation and debug-dump RPC
Adds sender_pubkey + sender_seq fields to MeshMessage so received
messages carry a stable cross-transport identity: (sender_pubkey,
sender_seq) pair. This is the foundation for the upcoming reply,
reaction, edit, and read-receipt variants — they need to target a
message by an ID that is meaningful on every node, not just locally.

Receive-side population lives in dispatch.rs::store_typed_message,
which now looks up the peer's pubkey_hex and copies envelope.seq from
the decoded TypedEnvelope. Sent-side population will land when we
plumb a per-node monotonic seq counter through the RPC layer.

Also adds mesh.debug-dump: a full in-memory state snapshot returning
peers, messages, status, shared-secret peer ids, encrypt_relay flag,
and stego mode — intended for smoke tests and bug investigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:18:01 -04:00
Dorian
3ed9243c50 feat(mesh): rich typed Sent records and echo dedup
Adds message_type + typed_payload (JSON) to MeshMessage so the UI can
render invoice/alert/coordinate/tx/lightning messages as structured
cards in both directions instead of showing raw wire bytes on the
Sent side. RPC handlers now route through send_typed_wire /
send_channel_typed_wire which transmit the binary envelope directly
(no utf8_lossy corruption) and record a rich Sent MeshMessage.

Also: store_message deduplicates echo-back doubles (20-msg lookback,
30s window), from_name is plumbed through the federation Incoming
path, and peer_dest_prefix / send_raw_payload are factored out of
send_message.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:01:10 -04:00
Dorian
18284e1592 chore: remove CLAUDE.md and stale config files 2026-04-12 12:11:00 -04:00
Dorian
19dcfd4f31 feat: BIP-39 master seed for unified key derivation
Replace fragmented random key generation with a single 24-word BIP-39
mnemonic that deterministically derives all node keys: Ed25519 (DID),
secp256k1 (Nostr/Bitcoin), BIP-84 xprv (Bitcoin Core), and LND aezeed
entropy. New onboarding flow: seed generate → word verification → identity
naming. Restore path enabled via 24-word entry. Includes seed RPC handlers,
mock backend support, LND/Bitcoin Core wallet-from-seed integration, and
UI polish across settings and discover views.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 01:41:24 +01:00
Dorian
e4e0ef4f11 bug fixing and deploy and build diagnostics 2026-03-22 03:30:21 +00:00
Dorian
618244eab0 refactor: split package.rs, mod.rs, listener.rs, and lnd.rs into focused submodules
- R35: Split package.rs (1794 lines) into package/{mod,config,validation,lifecycle}.rs
- R36: Split mesh/listener.rs (1799 lines) into listener/{mod,session,frames,decode,dispatch,bitcoin}.rs
- R37: Split rpc/mod.rs into mod.rs + dispatcher.rs, middleware.rs, response.rs (54% reduction)
- R38: Split lnd.rs (1064 lines) into lnd/{mod,info,channels,wallet,payments}.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 02:26:28 +00:00
Dorian
94f2de4a64 refactor: centralize constants, eliminate unwraps, remove dead code, resolve TODOs
- R13+R16: Replace .expect() with .context()? in main.rs and identity.rs
- R17+R18+R19: Fix unwrap() calls in helpers and js-engine
- R20+R21: Remove #[allow(dead_code)] annotations and delete truly dead code
- R22-R26: Create constants.rs module, replace 21 hardcoded values across 12 files
- R28+R29: LND/DWN timeouts already present — verified
- R30-R33: Remove TODO comments, implement marketplace payment check

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 01:54:35 +00:00
Dorian
c3d4a7063b fix: systemd resource limits, Tor rotation transition, unwrap elimination, RPC timeouts
- I2: Add MemoryMax=4G, LimitNOFILE=65535, TasksMax=2048 to systemd service
- I3: Tor rotation keeps old service for 1h transition before cleanup
- R14: Replace .parse().unwrap() with .unwrap_or(localhost) in rate limiter
- R15: Replace 7 unwrap/expect in mesh protocol with proper error propagation
- R27: Add 10s timeouts to mesh Bitcoin RPC calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 01:46:40 +00:00
Dorian
6f5188ef7f fix: rpcauth credentials, reboot survival, system Tor for all containers
- Bitcoin RPC: switch to rpcauth (salted hash in bitcoin.conf, no plaintext
  in config or CLI). Password stable across reboots/restarts/deploys.
- Remove daily-reboot-test.sh cron on both servers
- Enable podman-restart.service for container auto-start after reboot
- System Tor: SocksPort 0.0.0.0:9050 with SocksPolicy for container access
- LND: tor.socks=host.containers.internal:9050 (system Tor, not container)
- Bitcoin: -proxy=host.containers.internal:9050 for Tor outbound
- bitcoin_rpc.rs: reads from secrets file, cached, stable credentials
- package.rs: dynamic rpc_user/rpc_pass, rpcauth hash generation
- network.rs: fix missing send_to_peer args (mesh encryption update)
- first-boot-containers.sh: rpcauth generation, system Tor config
- deploy-to-target.sh: rpcauth credentials, LND config migration
- Mesh: encrypted channel message support (ChaCha20-Poly1305 updates)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 11:56:20 +00:00
Dorian
e4089287a3 fix: bulletproof mesh serial connection — PrivateDevices, auto-detect fallback, backoff
Root cause: systemd PrivateDevices=yes hid /dev/ttyUSB* from the service,
preventing .198 from connecting to its Heltec V3 after the security hardening.

Changes:
- Set PrivateDevices=no in systemd service (serial access needs physical devices;
  other hardening layers remain: NoNewPrivileges, ProtectSystem, RestrictNamespaces)
- Add SupplementaryGroups=dialout for explicit serial permissions
- Add fallback auto-detect when configured serial path fails to open
- Add exponential backoff on reconnect (5s→60s cap) to reduce log spam
- Add pre-open device existence check with actionable error messages
- Add udev rule (99-mesh-radio.rules) for stable /dev/mesh-radio symlink
- Add /dev/mesh-radio to serial candidate list (checked first)
- Add Connect button per detected device in Mesh UI
- Deploy udev rule to both servers and ISO build
- Fix FEDI_HASH unbound variable in deploy script
- Fix deploy binary step to handle hung service stop gracefully

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 10:50:13 +00:00
Dorian
4080d0a92b fix: Phase 8 — mesh hardening: atomic writes, unwrap elimination, GPS opt-out
- Ratchet state: atomic write via tmp + rename to prevent corruption on crash
- Block header decode: replaced .unwrap() with proper error handling on
  untrusted network data (was a crash vector from malicious peers)
- Shutdown channel: replaced .unwrap() with .ok_or_else() error propagation
- Dead man's switch GPS: default changed to opt-out (auto_include_gps=false)
- Alert signature verification: already covered by Phase 4 envelope checks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 01:04:19 +00:00
Dorian
b1e54e3626 feat: Phase 4 — mesh authentication, envelope signature verification, TX validation
- Identity announcements: verify Ed25519 key validity and X25519 consistency
- Envelope signatures: verify Ed25519 signatures on signed messages, drop invalid
- Block header validation: height range, hash length, timestamp sanity checks
- TX relay validation: hex validity, size bounds, version check before broadcast
- Rate limiter struct for per-peer relay operations
- Message sequence number field (seq) added to TypedEnvelope for ordering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 00:49:38 +00:00
Dorian
909ad5f019 feat: Phase 1 — per-installation credential generation, eliminate hardcoded passwords
Generate unique random passwords at first boot for Bitcoin RPC, all database
services (mempool, btcpay, immich, penpot, mysql-root), and Fedimint gateway.
Credentials stored in /var/lib/archipelago/secrets/ with 600 permissions.

Scripts: first-boot-containers.sh, deploy-to-target.sh, deploy-bitcoin-knots.sh,
container-doctor.sh all read from secrets files instead of hardcoded values.

Rust backend: new bitcoin_rpc module reads password from secrets file, env var,
or dev fallback. All .basic_auth() calls and container config strings now use
the shared credential reader instead of hardcoded "archipelago123".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 00:39:52 +00:00
Dorian
d37ec1dea5 feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
  relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
  looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
  (bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha

Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
Dorian
70f1348c15 feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
Dorian
a5d5faf90c feat: Phase 3-4 Weeks 5+6 — off-grid Bitcoin ops + emergency alert system
Bitcoin relay (mesh/bitcoin_relay.rs):
- BlockHeaderCache: stores latest block headers from internet peers for SPV
- RelayTracker: tracks in-flight TX and Lightning relay requests
- Builder functions: block header announcements (Ed25519 signed),
  TX relay request/response, Lightning invoice relay/response
- All amounts as u64 sats, never float
- 4 unit tests

Emergency alerts (mesh/alerts.rs):
- AlertConfig: dead man switch settings, GPS, emergency contacts
- DeadManSwitch: background timer, auto-trigger after configurable interval
  (default 6h), signed alert broadcast with GPS coordinates
- check_in() resets timer, is_triggered() checks elapsed time
- GPS as integer microdegrees (Coordinate type from message_types)
- Disk persistence for config
- 4 unit tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 02:26:07 +00:00
Dorian
de92bb2cd4 feat: Phase 3 Week 3 — typed messages + store-and-forward outbox
- Create mesh/message_types.rs: typed message envelope system
  - MeshMessageType enum: Text, Alert, Invoice, PsbtHash, Coordinate,
    PrekeyBundle, SessionInit, BlockHeader, TxRelay, LightningRelay
  - TypedEnvelope: CBOR wire format with 0x02 prefix, optional Ed25519 sig
  - Payload types: AlertPayload (with AlertType enum), InvoicePayload
    (sats as u64), Coordinate (integer microdegrees, no float),
    PsbtHashPayload, BlockHeaderPayload, TxRelayPayload, LightningRelayPayload
  - Signed envelope creation + verification for alerts/block headers
  - 8 unit tests

- Create mesh/outbox.rs: store-and-forward message queue
  - PendingMessage with TTL (24h default), retry count, relay hops (max 3)
  - MeshOutbox: persistent VecDeque, max 200 messages, expiry, relay support
  - Disk persistence to mesh-outbox.json
  - 6 unit tests: enqueue, deliver, expire, persistence, max size, relay hops

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 02:08:58 +00:00
Dorian
688adc8321 feat: add per-peer ratchet session manager with disk persistence
- Create mesh/session.rs: SessionManager for Double Ratchet state lifecycle
  - Lazy-loads sessions from disk on first message
  - Saves after every encrypt/decrypt (chain key advancement)
  - Per-DID storage at {data_dir}/ratchet/{sha256(did)}.json
  - Session info API for RPC status reporting
  - Zeroize on drop for all key material
- Tests: store+load roundtrip, encrypt/decrypt through manager, session removal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 01:54:26 +00:00
Dorian
e05bb3cc85 feat: Phase 3 Week 2 — Double Ratchet protocol for forward-secret mesh messaging
- Create mesh/ratchet.rs: full Signal-style Double Ratchet implementation
  - DH ratchet with X25519 ephemeral keypairs per step
  - Symmetric-key ratchet via HKDF-SHA256 chain derivation
  - Per-message ChaCha20-Poly1305 encryption with derived message keys
  - Out-of-order delivery via skipped message key cache (max 100)
  - Forward secrecy: old keys zeroized on ratchet step
  - Wire format: 40B header + nonce + ciphertext + tag
- Tests: full conversation, out-of-order, forward secrecy, wire format,
  long conversation (50 messages alternating), message roundtrip

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 01:50:22 +00:00
Dorian
1ced0fdbf3 feat: Phase 3 Week 1 — X3DH key agreement + HKDF foundation
- Add hkdf = "0.12" dependency for Double Ratchet key derivation
- Extend mesh/crypto.rs with hkdf_sha256, hkdf_sha256_32, hkdf_sha256_64,
  and generate_x25519_ephemeral() for DH ratchet steps
- Create mesh/x3dh.rs: full X3DH key agreement protocol
  - PrekeyBundle generation with Ed25519-signed prekeys
  - 3-way (or 4-way) ECDH → HKDF-SHA256 → root key
  - Initiator and responder sides derive identical root key
  - CBOR encoding for mesh transmission
  - Bundle signature verification
  - 5 unit tests: generate+verify, both-sides-same-key,
    without-one-time-prekey, cbor-roundtrip, tamper-detection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 01:28:35 +00:00
Dorian
253c305cc8 backup commit 2026-03-17 00:03:08 +00:00