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>
Clear All now deletes messages.json, mesh-contacts.json, sessions.json,
mesh-outbox.json and clears shared secrets for a truly clean slate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
- 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>
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>
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>
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>
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>
* Replaces click-anywhere-on-bubble with a tiny ⋯ trigger in the meta row
that fades in on hover (always visible on touch devices). Outside-click
closes the menu, bubble gets a `menu-open` class so the trigger stays lit.
* Action menu gains Forward (any message) + Edit + Delete (own messages
only, delete is red). Reaction spinner + reply preview upgraded to handle
typed targets (attachment/invoice/location/alert) via summarizeForPreview.
* Pending-edit banner with ✎ icon mirrors the reply banner; Send flushes as
mesh.edit-message when pendingEdit is set.
* Forwarded bubbles render "↪ Forwarded from {orig_name}" header; tombstone
+ (edited) markers; pending-reply close button upsized (28px, red hover).
* Scroll + message-arrival watcher fires a debounced 400ms read receipt
with per-peer seq dedup so we never double-ack.
* Chat header: ⟲ rotate-prekeys button next to the shield badge; 📤 outbox
count when mesh.outbox reports queued messages. Blob-store test widget
removed and chat list now sorts by most-recent message timestamp.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
Tap a bubble to open an action menu with Reply + 6 quick reactions.
Reply stashes the target MessageKey and flips the Send button to
"Reply" mode, routing through mesh.send-reply. Reactions call
mesh.send-reaction immediately and render as chips under the target
bubble, collapsed per emoji with a count and self-highlight. Reaction
messages are filtered out of the main chat stream so they don't create
standalone bubbles. Reply bubbles show a "↳ quoted snippet" header
when the target is still in the local window.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
App.vue listens for postMessage({type:'share-to-mesh',cid,...}) from
marketplace app iframes, stashes the payload in sessionStorage, and
routes to /mesh. Mesh.vue reads the stash on mount (and on a synthetic
'archipelago:share-to-mesh' event when already on the view), showing a
pending-attachment banner in the compose area. Send becomes Share and
flushes the CID via mesh.send-content with the input text as caption.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Marketplace app iframes (Penpot, Gitea, IndeedHub, ...) can POST a file
to /api/share-to-mesh and postMessage the returned CID to the parent
window. The endpoint mirrors /api/blob's body format but adds CORS for
the requesting app origin (any port on host_ip) so proxied apps can
reach it with credentials:'include'. Session cookie is still the primary
auth; the origin check is a sanity guard.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Compose row gains a 📎 attach button that uploads the file via /api/blob
and calls mesh.send-content for the selected peer. Received content_ref
bubbles render as a caption+filename card with either an inline image
preview or a Download button that calls mesh.fetch-content and swaps in
the returned local_url.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Plumbs the BlobStore from blobs.rs into ApiHandler. The HMAC capability
key is derived from the node's Ed25519 signing key via a domain-separated
SHA-256 — rotating the identity rotates every outstanding cap (intentional
so a replaced node cannot honour old tokens).
New routes (added to nginx config in both server blocks):
- POST /api/blob — session-authenticated raw upload, returns
{cid, size, mime, filename, self_test_url}. The self_test_url is a
pre-signed cap pointing at the local node so the UI can verify the
round-trip without needing a peer pubkey.
- GET /blob/<cid>?cap=<hex>&exp=<epoch>&peer=<pubkey> — peer-facing,
HMAC-verified in constant time, expiry-checked, then streams bytes.
Mesh.vue gets a minimal "Attachment test (blob store)" section: file
picker → upload → cid display → "Verify round-trip" and "Open in new
tab" buttons. This validates Phase 3a end-to-end before we layer the
ContentRef typed envelope variant on top.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds core/archipelago/src/blobs.rs: a SHA-256 content-addressed store
that writes bytes to ${data_dir}/blobs/<cid> with a sibling <cid>.meta
JSON file (mime, filename, size, created_at, optional tiny thumbnail).
BlobStore::put is idempotent, max 64 MiB per blob, and issues HMAC-SHA256
capability tokens scoped to (cid, peer_pubkey_hex, expiry_epoch). Tokens
are verified in constant time and rejected on expiry. This is the
foundation piece for the mesh ContentRef typed envelope — the /blob/<cid>
HTTP route and ContentRef variant will land in a follow-up increment
once the HMAC key is plumbed from node identity.
No consumer yet, so the module compiles with dead_code warnings; these
will clear when the HTTP handler and ApiHandler state wiring land next.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
IndeedHub, Bitcoin, and Penpot installs routinely exceed the default
RPC timeout on first pull. Bump package.install specifically to
900s so the frontend doesn't drop the request while the backend is
still downloading images.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds renderers for tx_relay, tx_relay_response, tx_confirmation,
lightning_relay, and lightning_relay_response message types so these
appear as rich cards in the chat stream. sendArchMessage now looks up
our own onion via getTorAddress and skips federation peers that match,
preventing the duplicate "echoed back to self" message we were seeing
on single-node test federations. Empty-federation error message is
also clearer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Swapped all registry references: image-versions.sh, marketplaceData.ts,
curatedApps.ts, catalog.json. git.tx1138.com is now fallback only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Registry fallback now only tries DIFFERENT registries (skips original
that already failed). 120s timeout per fallback attempt. WireGuard
keys generated on unbundled first-boot. Gitea ROOT_URL uses port 3001.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gitea image pushed to Archipelago registry. PROXY_APPS stays empty
per user preference - direct port only. Gitea config uses
INSTALL_LOCK + dark theme.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Installs all 7 containers (postgres, redis, minio, relay, api,
ffmpeg, frontend) on indeedhub-net with proper env vars and volumes.
Fixes pull timeout to cover stderr reader. Catalog registry set to
23.182.128.160:3000.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Timeout now wraps stderr reader + wait (was only wrapping wait, so
hung pulls were never killed). 23.182.128.160:3000 is now primary
registry since git.tx1138.com is unreachable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous timeout used ExitStatus::default() which is success on Linux,
so the fallback never triggered. Now properly kills process, awaits
exit, and forces fallback path on timeout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Image pulls now timeout after 60s and fall through to dynamic registry
fallback instead of hanging forever when primary is unreachable.
Gitea external port corrected to 3001. WireGuard key generation
added to first-boot for fresh installs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. registries.conf includes docker.io search + fallback 23.182.128.160
2. First-boot pull_with_fallback() tries primary then fallback registry
3. FileBrowser created with noauth config on persistent volume
4. Backend dynamic registries.json pre-created in ISO
5. Filebrowser password secret created for token flow
Fixes: apps stuck at 0% download, filebrowser not working, dynamic
catalog not loading on fresh installs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
App catalog served from Gitea repos (app-catalog) with 35 apps.
Nodes fetch catalog dynamically — new apps appear without frontend
rebuild. Test app added and removed to verify pipeline.
Gitea manifest updated with internal_port/nginx_proxy for iframe.
Updated catalog.json, nginx configs, app session configs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Configurable registry list persisted to config/registries.json.
Image pulls try all registries in priority order — if primary fails,
fallback registries are attempted automatically. RPC endpoints:
registry.list, registry.add, registry.remove, registry.test.
Replaces hardcoded fallback logic with extensible registry system.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added gitea to PROXY_APPS so it always routes through /app/gitea/
nginx proxy (same origin as parent page). Fixes X-Frame-Options
SAMEORIGIN rejection when loading via direct port.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When primary registry (git.tx1138.com) fails, image pull automatically
retries from Gitea registry at 23.182.128.160:3000. Tags pulled image
with original name so install continues seamlessly. Gitea added as
external app in app session config.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Videos and audio now stream directly via URL with auth token query
param instead of downloading entire file into a JS blob. Fixes
playback of large videos (170MB+ was timing out). Images still use
blob URLs. streamUrl() added to filebrowser client and cloud store.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cloud subpages (Music, Photos, etc.) now show bg-cloud.jpg instead
of falling through to bg-home.jpg.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Video thumbnail in card is pointer-events-none so clicks pass through
to the play handler. Better error messages when preview fetch fails.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cookie was scoped to /app/filebrowser but Cloud page reads it from
/dashboard/cloud — cookie was invisible. Changed to path=/ so the
auth token is accessible from any page for fetchBlobUrl calls.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Video fills entire viewport with no padding/border-radius. Double-click
toggles native fullscreen. Reduced padding for all media types.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fedimint can use a remote Bitcoin RPC (e.g., over Tailscale or Tor).
Dependency check now logs info instead of blocking installation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MediaLightbox: full glassmorphic redesign with dark backdrop, smooth
transitions, proper video/audio/image support. FileBrowser: noauth
config on persistent volume. Deploy script: fixed sed quoting.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cashu ecash protocol (BDHKE blind signatures, cashuA token format,
mint HTTP client) replacing the stub wallet. TollGate-inspired streaming
data payment system with step-based pricing (bytes/time/requests),
session management with incremental top-ups, usage metering, and
Nostr kind 10021 service advertisements.
13 new streaming.* RPC endpoints. Content server now verifies real
Cashu tokens. Profits tracking includes streaming revenue.
Frontend: GlobalAudioPlayer (persistent bottom bar across all pages),
video lightbox with full controls, audio in MediaLightbox, free file
previews (no blur), paid 10% audio/video previews, separated play
vs download buttons in PeerFiles.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents duplicate resolver directive error when both
nginx-archipelago.conf and external-app-proxies.conf are loaded
at http context level.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>