Network/reachability improvements + a relay-privacy fix. Wire-compatible
with v0.7.0/v0.7.1; no protocol changes.
- Replace hand-rolled UPnP (igd-next) with the portmapper crate. All three
protocols (UPnP-IGD / NAT-PMP / PCP) run in parallel, auto-renew
internally. PCP adds IPv6 firewall pinholes and works on iOS without
the multicast entitlement. Pulled in transitively via iroh already, no
net dep growth.
- Android: UPnP/PCP/NAT-PMP attempted on WiFi/Ethernet with a
WifiManager.MulticastLock acquired for the lifetime of the mapping.
Cellular skipped early (no UPnP/PCP gateway, avoid 3s discovery waste).
- TCP port-mapping gate removed for mobile — phones with permissive NAT
can now serve HTTP for direct browser fetches.
- Anchor reachability watcher (bidirectional): clears is_anchor after
>5min of no port mapping; restores it when the mapping comes back.
Network roams self-heal without restart. Mobile never auto-anchors.
- Session relay opt-in restored. relay.session_relay_enabled setting
defaults OFF (anchors included — servers shouldn't silently burn
bandwidth either). Gates both serving (can_accept_relay_pipe) and
using (auto-fallback in node.rs). UI toggle in Settings. Relay-style
signaling (RelayIntroduce / worm_lookup / N1-N3 shares) unaffected.
- URL Phase 1: share links now contain only the post ID
(itsgoin.net/p/<post>). Anchor handler already supported post-ID-only
URLs (author was optional); just dropped the author hex from the
generator. Older URLs with author hex continue to work.
- Quick app close button in header (with confirm) — useful for stopping
network activity between sessions on mobile.
- JNI null-pointer guards on ndk_context handles in android_wifi.rs.
MEMORY rule sharpened to distinguish session relay (byte pipe, opt-in)
from relay-style signaling/discovery (always on).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps:
- crates/{core,cli,tauri-app}/Cargo.toml: 0.6.2 → 0.7.0
- crates/tauri-app/tauri.conf.json: 0.6.2 → 0.7.0
- gen/android/app/tauri.properties: versionName 0.7.0, versionCode 7000
website/download.html: v0.7.0 promoted to top with FoF release notes;
v0.6.2 retained in archive section. Wire-additive notice + link to
design.html#fof for full architecture.
No -beta suffix this cycle: no users on prior version means no need
for the beta carve-out. Will resume beta convention when there are
real users to migrate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end FoFClosed (Mode 1: encrypted body + FoF comments):
Node API:
- create_post_fof_closed(content) -> (PostId, Post, cek)
Builds gating, encrypts body via fof::encrypt_fof_body, base64s it
into post.content, stores with visibility=FoFClosed +
intent=Public, propagates via update_neighbor_manifests_as.
- read_fof_closed_body(post_id) -> Option<String>
Trial-unlocks via find_unlock_for_post, decrypts body, returns
plaintext. Returns None for non-FoFClosed or non-member readers.
Tauri commands:
- create_post_fof_closed, read_fof_closed_body. Registered in
generate_handler!.
Feed rendering:
- PostDto.visibility carries the new "fof-closed" string.
- renderPost(): FoFClosed posts render with a locked placeholder
(data-fof-closed-pending=post_id span). Visual badge added.
- unlockFoFClosedPlaceholders(rootEl): post-render async pass that
scans for placeholder spans and dispatches read_fof_closed_body
for each. Fills in body for FoF readers; falls back to a
"not in this FoF set" notice otherwise.
- Wired into feed-list and my-posts-list render paths.
Compose:
- "Body+Comments: FoF only (Mode 1)" option in comment-perm-select.
Selected → dispatches to create_post_fof_closed.
CLI feed renderer + Tauri feed-DTO match arms updated to handle
FoFClosed.
New end-to-end test brings total to 146:
- fof_closed_body_end_to_end: Alice authors FoFClosed body; Bob (with
Alice's V_me in his keyring) unlocks + decrypts; Carol (no
matching V_x) cannot unlock and sees only ciphertext.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Six phase commits landed for v0.6.2 (2b through 2g) plus three
pre-release fixes from the final audit pass:
- 2b: control-post flow (delete / visibility change) + retire BlobDeleteNotice
- 2c: remove audience primitive + retire PostPush / PostNotification /
AudienceRequest / AudienceResponse
- 2d: profile posts signed by the posting identity
- 2e: rich comments with ref_post_id + signed preview
- 2f: groups as a distinct primitive alongside circles
- 2g: GroupKeyDistribute → encrypted post (last persona-signed
direct push gone)
- audit fix: reject group-key distribution posts where the claimed
admin doesn't match the post author
- audit fix: cap concurrent port-scan hole punches at one (the
10 Mbps-on-VPN storm)
- audit fix: dedup concurrent outgoing connects to the same peer
Wire-breaking fork from v0.6.1. Retired message types 0x42
(PostNotification), 0x43 (PostPush), 0x44 (AudienceRequest), 0x45
(AudienceResponse), 0x95 (BlobDeleteNotice), 0xA0 (GroupKeyDistribute)
are not optional.
121/121 core tests pass.
v0.6.2 wire fork: every persona-identifying direct push is gone. Public posts
propagate only through the CDN (pull + header-diff neighbor propagation).
Encrypted posts propagate only through pull with merged author-or-recipient
match. There is no remaining sender→recipient traffic correlation signal on
the wire for content.
Protocol (network-breaking):
- Retire MessageType 0x42 (PostNotification), 0x43 (PostPush),
0x44 (AudienceRequest), 0x45 (AudienceResponse). Their payload structs are
deleted along with the handlers and senders.
- SocialDisconnectNotice (0x71) / SocialAddressUpdate (0x70) sender
functions targeting audience are deleted; the existing handlers stay
(both already dead code on the send side).
Core removals:
- `push_to_audience`, `notify_post`, `push_delete`,
`push_disconnect_to_audience`, `push_address_update_to_audience`,
`send_audience_request`, `send_audience_response`, `send_to_audience` —
all gone from network.rs.
- `handle_post_notification` removed from connection.rs.
- `request_audience`, `approve_audience`, `deny_audience`,
`remove_audience`, `list_audience_members`, `list_audience` removed from
Node.
- `audience_pushed` step removed from post creation.
- `AudienceDirection`, `AudienceStatus`, `AudienceRecord`,
`AudienceApprovalMode` removed from types.
- Storage: `store_audience`, `list_audience`, `list_audience_members`,
`remove_audience`, `row_to_audience_record`, `audience_crud` test, the
`audience` CREATE TABLE, and the audience-dependent social route rebuild
branch all removed. Upgraded DBs retain the orphan `audience` table;
nothing touches it.
Follow-on cleanups:
- `SocialRelation::Audience` + `::Mutual` collapsed into just `Follow`.
The Display/FromStr impl accepts legacy "audience"/"mutual" strings from
pre-v0.6.2 DBs and maps them to Follow.
- Blob-eviction priority function drops the audience factor; relationship
is now own-author vs followed vs other. Tests updated accordingly.
- `CommentPermission::AudienceOnly` → `FollowersOnly`. Check uses the
author's public follows (`list_public_follows`) rather than a separate
audience table. `ModerationMode::AudienceOnly` similarly renamed.
- Follow/unfollow routines simplified: no audience downgrade logic;
unfollow removes the social route entirely.
UI:
- CLI: `audience*` commands removed.
- Tauri: `AudienceDto`, `list_audience`, `list_audience_outbound`,
`request_audience`, `approve_audience`, `remove_audience` commands
removed from invoke_handler. Frontend: audience panel and audience/mutual
badges removed; compose permission dropdown shows "Followers" instead of
"Audience"; `loadAudience` is a no-op stub that hides any leftover DOM.
Tests: 111 / 111 core tests pass.
Breaking change: v0.6.2 nodes won't interoperate with v0.6.1 for delete
propagation, visibility updates, direct post push, post notifications, or
audience requests. Upgrade both ends.
Phases 1-5 of the Identity Architecture rollout are on master; Phase 6
(rotating DM IDs) is deferred. This bumps the protocol-breaking
release: v0.5 and v0.6 cannot interoperate.
Packaging:
- Cargo + tauri.conf: 0.5.3 -> 0.6.0
- deploy.sh: drop -beta filename suffix
- download.html: single v0.6.0 section with fork notice at the top;
changelog entry summarising the five phases and the schema migration;
Phase 6's rotating-DM-ID status noted as deferred
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users can now hold multiple posting identities on one device and
publish content under any of them. Each persona has its own ed25519
key; peers see them as distinct authors with no link back to the
device's network identity.
Node methods:
- list_posting_identities() -> Vec<PostingIdentity>
- create_posting_identity(display_name) — generates a fresh ed25519
key, persists, auto-follows self
- delete_posting_identity(node_id) — refuses to delete the default
- set_default_posting_identity(node_id) — validates identity exists;
Node's cached default_posting_id/secret picks up on next restart
- create_post_as(posting_id, content, intent, attachments) — routes
through a shared create_post_inner that takes posting_id +
posting_secret as parameters
Post creation pipeline:
- create_post_with_visibility now delegates to create_post_inner
using default_posting_id/secret
- create_post_inner threads posting_id / posting_secret through
every content-signing, encryption, manifest, blob-header, and
CDN-manifest step — the persona is fully honored end to end
- update_neighbor_manifests now takes a posting_id param too, so
posts from persona X only update neighbor manifests for X's own
prior posts
Tauri IPC:
- list_posting_identities / create_posting_identity /
delete_posting_identity / set_default_posting_identity
- create_post_as with posting_id_hex + the same visibility params
as create_post
CLI:
- personas / create-persona <name> / delete-persona <id>
- post-as <posting_id> <text>
Smoke-tested two-persona scenario:
- A creates "Work" persona; posts from default and Work
- B follows both; pulls from A; gets all three posts
- Authors are AB84BA... (Work) and 7CD949... (default) — distinct
on the wire
Frontend UX (Settings > Personas, compose picker, filter pills,
merged feed labels) is scoped as a separate commit.
Co-Authored-By: Claude Opus 4.7 (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>