Switch ALL propagation-decision reads to the flat holder set.
push_manifest_to_downstream now targets file_holders instead of
blob_downstream. ManifestPush receive-side relay likewise — known
holders fan out to up to 5 most-recent peers instead of a directional
tree.
Blob delete notices: single flat fan-out to file_holders; the legacy
upstream_node tree-healing field is emitted as None (wire-stable via
serde default) and ignored on receive — the post-0.6 flat model
doesn't need sender-role distinction. send_blob_delete_notices keeps
its Option<&Upstream> parameter as an unused placeholder for signature
stability with the call sites in this commit.
Other reads migrated:
- blob fetch cascade: step 2 now tries "known holders" (up to 5)
instead of a single upstream
- manifest refresh: downstream_count reported from file_holder_count
- web/http post holder enumeration
- Worm search post/blob holder fallback (both connection.rs paths)
- DeleteRecord fan-out rewires to file_holders
- Under-replication replication check: < 2 holders
Storage additions:
- get_file_holder_count(file_id)
- remove_file_holder(file_id, peer_id)
Legacy upstream/downstream writes are still happening from Phase 2b;
those + the tables themselves go in 2e.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
propagate_engagement_diff now targets the post's flat holder set (up to
5 most-recent) instead of the post_downstream directional tree. The
holder set naturally subsumes the old upstream+downstream partition, so
the separate "also send to upstreams" loops at each engagement call
site are removed (reactions, comments, comment edit/delete, receipt
slots, comment slots).
handle_blob_header_diff on receive:
- records the sending peer as a file holder (an engagement exchange is
proof the peer holds the post)
- re-propagates to the holder set minus the sender
Writes to post_upstream / post_downstream still occur from Phase 2b
(dual-write); those and the legacy tables will be removed in 2e.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Populate the flat holder set alongside every existing post_upstream /
post_downstream / blob_upstream / blob_downstream write so that read
paths can be switched over in the next commit without losing continuity.
Events wired:
- Pull sync receive (3 paths in connection.rs)
- PostPush receive (public posts only after Phase 1)
- PostFetch via notification (discovery pull)
- PostDownstreamRegister
- Replication accept (downstream) + replication-driven pull (upstream)
- Attachment upstream recorded after replication blob fetch
- ManifestPush receive (remote is a CID holder)
- ManifestPush send (downstream peer becomes CID holder)
- Blob fetch fallback (upstream lateral sources)
Direction is tracked as Received vs Sent. Not load-bearing for routing;
retained for future use. LRU cap of 5 enforced on every touch.
Legacy upstream/downstream writes remain in place; they'll go away
together with the table drops at the end of this phase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New flat per-file holder set replaces the directional upstream/downstream
trees. Keyed by 32-byte content-addressed file_id (works for both PostId
and blob CID). LRU-capped at 5 holders per file on touch.
- HolderDirection enum (Sent/Received/Both) — tracked for potential
reuse, not load-bearing for propagation
- touch_file_holder / get_file_holders / delete_file_holders
- seed_file_holders_from_legacy: one-time idempotent seed from
post_upstream, post_downstream, blob_upstream, blob_downstream so
users upgrading from 0.6.0 don't start with empty holder sets
Table and methods land here; call-site refactor and legacy-table drop
follow in subsequent commits within this phase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Encrypted posts now propagate only via the CDN (ManifestPush + neighbor
header updates), eliminating the sender→recipient traffic signal on the
wire. Encrypted DMs are indistinguishable from any other encrypted post.
- Remove push_post_to_recipients entirely from network.rs
- Remove call sites in create_post and re-encrypt-on-revoke
- PostPush handler now ignores non-public visibility (kept for public
audience push path)
Known gap: non-follower DMs won't reach until Phase 3 (merged pull +
recipient-match). Followers receive via the existing CDN path — new
posts trigger neighbor-manifest updates, ManifestPush fans out to
downstream holders, recipients pull missing post IDs from followed
authors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Store the external address reported by peers via your_observed_addr in
initial exchange. Display it in Our Info panel with NAT classification.
Replaces reliance on iroh's pkarr/STUN for external address discovery
while keeping clear_address_lookup() (no dns.iroh.link publishing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When anchor reports duplicate identity, sync pauses and a red banner
shows with a "Continue Anyway" button. Clicking it clears the flag and
starts all sync tasks. Handles false positives from network changes
(WiFi/cellular/VPN switches) without requiring complex staleness checks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Feed pagination:
- Cursor-based pagination: get_feed_page/get_all_posts_page (20 posts/page)
- Batched engagement queries (3 bulk SQL queries instead of 4 per post)
- IntersectionObserver for infinite scroll (sentinel at midpoint)
- Viewport-based media loading (blobs only load when post enters view)
- Pre-fetch next page immediately after current page renders
Duplicate identity detection:
- Anchor detects when a NodeId is already mesh-connected during initial
exchange and sets duplicate_active flag in response
- Client skips sync tasks when duplicate detected
- Frontend shows red warning banner
Privacy:
- Fixed pkarr leak: clear_address_lookup() removes default dns.iroh.link
publishing. Only mDNS (local network) discovery enabled.
Android:
- SAF integration via tauri-plugin-android-fs: exports open native "Save As"
dialog so users can save to Downloads/Drive/etc.
- Download/export paths use app data dir on Android (writable)
- File picker gated behind desktop cfg (blocking_pick not on Android)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Node::open_with_bind no longer runs bootstrap (anchor connect, NAT
probe, referrals). New run_bootstrap() method called from background
task after UI is live.
- Background tasks (pull cycle, diff cycle, etc.) start after bootstrap
completes, not during block_on.
- Feed no longer pre-loaded during welcome screen readiness check.
Ready button enables immediately after get_node_info succeeds.
- Feed loads on tab switch, not during startup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- bundleMediaFramework: true — bundles full GStreamer plugin set in AppImage.
Fixes WebKit hang on video/audio (missing appsink/autoaudiosink plugins).
MSDK/VA plugins removed post-build to avoid Ubuntu DMA assertion crash.
- Import creates complete posts: BlobHeader, VisibilityIntent, pinned blobs,
self-follow. Imported posts now indistinguishable from locally created ones.
- First-run chooser: Start Fresh or Import Identity on fresh install only.
Profile setup shown for existing identities without display name.
- File pickers: native Browse buttons on export (folder) and import (ZIP)
via tauri-plugin-dialog.
- Export path: relative paths resolved against home dir.
- Lightbox close: only on overlay/image click, not inner form content.
- Growth loop skips self as N2 candidate.
- Node shutdown on identity switch (prevents zombie background tasks).
- media-src CSP includes asset protocol.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- First-run: show Start Fresh / Import chooser only when single auto-created
identity with no profile (not on every boot without a display name).
- Identity switch: shut down old node's endpoint before starting new one.
Fixes lockup after multiple switches (zombie background tasks).
- File picker: native Browse buttons on export (folder) and import (ZIP file)
via tauri-plugin-dialog.
- Export path: resolve relative paths against home dir (was using process cwd).
- Lightbox close: only close on overlay/image click, not inner form content.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- handle_pull_request: extracted to static method, conn_mgr lock no longer
held during network I/O (read request + write response)
- handle_address_request: inlined at call site with brief lock to gather
data, response written after lock release
- handle_session_relay: capacity check + target lookup under brief lock,
rejection write moved outside lock scope
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Network Diagnostics: "Our Info" button shows addresses with NAT status,
device role, UPnP, HTTP capability. Addresses stacked for mobile.
- Hole punch race: re-check for existing connection before and after relay
introduction to avoid wasting minutes on redundant punch attempts.
- Relay introduction now carries requester/target NAT mapping+filtering
so hole punch strategy uses fresh profiles instead of stale stored ones.
Critical for phones that switch between WiFi/cellular/VPN.
- STUN fix: filter DNS results to IPv4 (was resolving to IPv6 first on
dual-stack, causing silent send failure and "NAT unknown").
- Welcome screen: Ready button with loading bar for instant feed access.
- LAN addresses show just "LAN" (no misleading punchability label).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merge-with-key: decrypt exported posts using original identity seed, re-create
under current identity with prior_author in BlobHeader for provenance tracking.
Download page restructured with stable (v0.4.4) + beta (v0.5.0-beta) sections.
Version bumped across all crates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Export (export.rs): ZIP archive with auto-chunking at 4GB. Four scopes:
identity only, posts only, posts+identity, everything (posts+key+follows+
profiles+settings). Includes blobs. Manifest JSON tracks metadata.
Import (import.rs): Read ZIP summary without importing (preview).
Import public posts into current identity with new PostIds + original
timestamps. Import as new identity (creates identity subdir from key).
Uses spawn_blocking for ZIP I/O to avoid Send issues with ZipArchive.
Tauri IPC: export_data, import_summary, import_public_posts,
import_as_new_identity commands. IdentityManager.base_dir() getter.
Frontend: Export wizard lightbox with scope radio buttons + output dir.
Import wizard with ZIP path, preview summary, action selection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Security: identity.key written with 0600 permissions (Unix). Docker bridge
IPs (172.17-31.x) filtered from is_shareable_addr to prevent topology
disclosure in relay introductions.
Lock contention: ManifestPush relay and DeleteRecord CDN notify now gather
connections under lock, then send outside lock.
UI: syncBtn null guard prevents crash on hidden element.
Documentation: design.html version badge updated to v0.4.4. Self Last
Encounter threshold corrected from 3h to 4h. Multi-Device Identity section
rewritten for multi-identity-per-device (complete) + multi-device (planned)
+ post merge (planned). MEMORY.md updated to v0.4.4+ status.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New Tauri commands: list_identities, create_identity, switch_identity,
delete_identity, import_identity_key, get_active_identity.
Settings UI: Identities section with list (active indicator, switch/delete
buttons), Create New Identity lightbox, Import Identity Key lightbox.
Export/Import buttons as placeholders for Phase 2/3.
Identity switch does hot-swap: tears down old Node, starts new one with
all background tasks, swaps AppNode RwLock. Frontend reloads after switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New identity.rs: IdentityManager manages per-identity data directories
under identities/{node_id_hex}/. Supports create, list, switch, delete,
import from key. Legacy flat layout auto-migrates on first launch.
Tauri AppState refactored from Arc<Node> to AppNode (Arc<RwLock<Arc<Node>>>)
+ AppIdentity (Arc<Mutex<IdentityManager>>). All 76 IPC commands updated
to get_node(&state).await pattern. Enables hot-swap identity switching.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-peer sync (People tab Sync button) now resets last_sync_ms to 0 so
the responder sends ALL posts, not just posts newer than last sync.
ManifestPush post discovery now fetches blobs alongside discovered posts
while the connection is live, instead of waiting for the next prefetch cycle.
Hole punch: filter_reachable_families() checks endpoint bound sockets and
removes addresses the local endpoint can't reach (IPv4-only won't try IPv6).
Applied to hole_punch_single, hole_punch_parallel, hole_punch_with_scanning.
Relay introduce paths switched from is_publicly_routable to is_shareable_addr
so LAN addresses (192.168.x.x) are included for same-WiFi hole punching.
is_shareable_addr made pub(crate).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sticky header with tabs as one block on desktop. Fixed header + bottom nav on
mobile. Full-width dark header (#0a0a1a) edge-to-edge with 15px fade gradient.
Tab icons on desktop (inline) and mobile (stacked). Safe area inset support for
phone notches. Lightboxes close on tab switch.
Profiles lightbox (name, bio, visibility, circle profiles) and redundancy
lightbox moved from settings to My Posts. Sync All and Stored Anchors moved
into Network Diagnostics popover. Network indicator click opens diagnostics.
Settings streamlined — removed profile editor, diagnostics button, sync,
redundancy, anchor management.
Keepalive fix: tokio::time::sleep in select! never fired; switched to interval.
Auto-reconnect on unexpected disconnect with 3s delay. notify_growth on
disconnect. Tab badge fix preserving icon spans. Feed re-render skip during
media playback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keepalive: tokio::time::sleep inside select! was resetting every iteration —
keepalives never fired. Switched to tokio::time::interval which ticks reliably.
This caused connections to be zombie-reaped (10min timeout with no pings).
Auto-reconnect: unexpected disconnects (stream error, not SocialDisconnectNotice)
now attempt direct reconnect after 3s delay using last known address from peers
table or social route. Falls back to notify_growth() if direct reconnect fails.
Tab icons: updateTabBadge was using textContent which destroyed the icon and
label spans inside tab buttons. Now updates only the .tab-label span and manages
a separate .tab-badge element.
Video playback: feed re-render skipped while any video or audio is actively
playing, preventing echo from DOM destruction and media element recreation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
spawn_with_arc used blocking_lock() which panics inside tokio runtime.
Changed to async lock().await. Reduced StoragePool from 8 to 4 connections
for Android compatibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Eliminate all conn_mgr lock holds during network I/O across 14 actor commands
and bi-stream handlers. PostFetch, TcpPunch, PullFromPeer, FetchEngagement,
ResolveAddress, AnchorProbe use brief locks for data gathering only. WormLookup,
ContentSearch, WormQuery use connection snapshots for lock-free cascade fan-out.
RelayIntroduce extracts forwarding data under brief lock, does I/O outside.
BlobRequest, PostFetchRequest, ManifestRefresh use Arc clones instead of conn_mgr
lock. ConnectionActor hoists shared Arcs (storage, blob_store, endpoint) for
lock-free access. ResolveAddress adds 5s per-query timeout (was unbounded).
Initial exchange failure now aborts mesh upgrade (was silently continuing with
broken connection). connect_to_peer/connect_to_anchor use consistent 15s timeout.
Rebalance connects outside the lock via pending_connects pattern.
StoragePool: 8 concurrent SQLite connections in WAL mode replace single
Mutex<Storage>. Reads run fully parallel; writes serialize at SQLite level only.
PRAGMA busy_timeout=5000 for graceful write contention.
Mobile bottom nav bar (<=768px) with icon tabs. Text sizes: XS/S/M/L/XL
(75%/100%/125%/150%/200%), default M. localStorage persistence for instant
restore. Toast repositioned above mobile nav.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Welcome screen with staggered counters while backend bootstraps. Header status
ticker for new posts/messages/reactions/comments/connection changes. Notification
fallback chain (Tauri plugin → Web API → notify-rust). Responsive text scaling
(Small/Normal/Large, persisted). Diagnostics moved to popover with on-demand
connections. Share details lightbox with QR code. Connect string prefers external
address. Stale N1 fix (disconnected routes excluded). Replication handler actively
fetches posts+blobs from requester. Hole punch registers remote address for relay.
Replication semaphore (3 concurrent). Peer labels show truncated node ID.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Security:
- Reaction signatures: ed25519 sign/verify (sign_reaction, verify_reaction_signature)
Backward-compatible — unsigned reactions from old nodes still accepted
- Comment signature verification: verify_comment_signature now called on receipt
- Reaction removal authorization: only reactor or post author can remove
- BlobHeader author verification: lookup actual author from storage, don't trust payload
Lock contention (4 fixes):
- ManifestPush discovery: cm lock released before PostFetch I/O
- Pull request handler: load under lock, filter without lock, brief re-lock for is_deleted
- Pull sender: split into two brief locks (store posts, then batch upstream+sync)
- Engagement checker: batch all chunk results, single lock for writes
Data cleanup:
- Post deletion cleans post_downstream, post_upstream, seen_engagement tables
- Added TODO-hardening.md documenting remaining DOS/security/lock/data issues
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Encrypted slots in BlobHeader:
- Private posts get noise-prefilled receipt slots (64B, 1 per participant)
and comment slots (256B, ceil(participants/3), expandable)
- Slot key derived from post CEK via BLAKE3 — only participants can read
- CDN relays propagate opaque encrypted bytes without decryption
- 3 new BlobHeaderDiffOps: WriteReceiptSlot, WriteCommentSlot, AddCommentSlots
Receipt system:
- States: empty(0), delivered(1), seen(2), reacted(3)
- Slot index = position in sorted participant NodeId list
- Author can pre-feed emoji reaction at creation time
- 6 new crypto tests for slot encrypt/decrypt/derivation
Node methods:
- write_receipt_slot, write_comment_slot with upstream+downstream propagation
- read_receipt_slots, read_comment_slots with CEK-based decryption
- get_post_cek_and_participants helper for both Encrypted and GroupEncrypted
IPC: write_message_receipt, write_message_comment, get_message_receipts,
get_message_comments
Frontend:
- DM chat bubbles show delivery indicators (check → double → blue → emoji)
- Opening conversation auto-marks incoming messages as seen
- React button on messages with emoji prompt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment edit & delete:
- EditComment/DeleteComment BlobHeaderDiffOps with upstream+downstream propagation
- Trust-based: comment author can edit/delete, post author can delete
- Storage: edit_comment(), delete_comment() methods
- Frontend: inline edit (Enter/Escape), delete with confirm
Native notifications:
- tauri-plugin-notification for system notifications on all platforms
- Triggers for messages, new posts, reactions, and comments
- notif_reacts setting added, button-group toggles replace dropdowns
- _notifReady flag prevents startup spam
Protocol hardening:
- BlobHeaderDiffOp::Unknown variant with #[serde(other)] for forward compatibility
- Old nodes silently skip unknown ops instead of crashing
UI fixes:
- Self removed from Following list
- Offline follows in lightbox popup (auto-show if no one online)
- Sent DMs filtered from My Posts
- Comment threading scoped to closest .post (fixes duplicate ID issue)
- Select dropdown text legible in WebKitGTK (black on white options)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Security & stability:
- Incoming auth-fail rate limiting per source IP (3 attempts, then exponential backoff)
- Schema versioning via PRAGMA user_version with migration framework
Networking:
- IPv6 http_addr fix: advertise actual public IPv6 instead of 0.0.0.0
- N2/N3 TTL reduced from 7 days to 5 hours
- Full N1/N2 state re-broadcast every 4 hours
- Bootstrap isolation recovery: 24h check with sticky N1 advertising
- Bidirectional engagement propagation (upstream + downstream)
- Auto downstream registration on pull sync and push notification
- post_upstream table for CDN tree traversal
Media & UI:
- Video preload="auto" for share links and in-app blob URLs
- Following: Online/Offline split with last-seen timestamps
- DMs filtered from My Posts tab
- Image lightbox, audio player, file attachments with download prompt
- Share link unroutable address filtering
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>