Wires the encrypted sync transport into a working replication between one
identity's own devices, reusing the CRDT-ready model (HLC + tombstones +
LWW) the data model was built for.
- SocialSession gains a 5th interface, sync, over the shared connection,
built with a per-install deviceId + the app's inventory namespace.
- DeviceIdStore: a stable random per-install id in the keystore — distinct
from the seed-derived CRDT node id, which is IDENTICAL across a user's
devices (so it couldn't tell them apart / they'd clobber each other's
replaceable record).
- InventorySnapshotIO (implemented by ExportImportService): build/apply the
inventory as the SAME interchange JSON as backups but UNSEALED — the sync
transport does the encryption, so it must not double-seal. Apply = the
existing idempotent LWW importInventory.
- SyncService: on (re)connect publishes this device's snapshot + subscribes
to the others'; a local edit (debounced, off tableUpdates) republishes;
incoming snapshots merge idempotently; skips its own echo; a byte check
skips re-publishing an unchanged snapshot so the ping-pong converges.
Foreground only, like the inbox.
- DI/Bootstrap/switchSocialAccount wire and lifecycle it per identity.
Tests: DeviceIdStore, ExportImportService snapshot round-trip (unsealed +
idempotent), SyncService.handleRemote (applies others', skips own).
Follow-ups: deltas vs full-snapshot pushes; background sync; conflict UI.
Adds pseudonymous, switchable social identities derived from the SAME root
seed via an account index (NostrKeyDerivation.deriveFromSeed(seed, account)).
HKDF is one-way so accounts are unlinkable to the Ğ1 key; account 0 is the
original identity, byte-for-byte unchanged (no rotation for current users),
and every account regenerates from the single seed — so switching adds
nothing to back up.
- SocialAccountStore: keystore-backed active account + max created.
- Per-identity stores (chats, profile, name cache) namespaced by account
scope (account 0 = legacy keys, no migration) so identities never mix.
- switchSocialAccount() re-derives the identity, re-scopes the stores and
restarts the inbox listener; RestartWidget rebuilds the tree to pick up
the new social singletons. DB/inventory untouched.
- Profile 'Your identities' switcher: list, create, switch (with a note
that messages/contacts are kept separate per identity). i18n en/es/pt/ast.
Tests: account-indexed derivation (legacy 0 unchanged, accounts distinct
yet deterministic, negatives rejected); SocialAccountStore; per-identity
store scope isolation. Resolves the flagged 'change identity' decision
(open-decisions §B).
Known: switching resets navigation to home (full tree rebuild).
Drawer 'Profile' is now live.
- commons_core: ProfileTransport + UserProfile + NostrProfileTransport (kind:0
publish/fetch), exposed as SocialSession.profile. Round-trip tested against the
in-process relay (fast dart test).
- app: ProfileStore (local name/about, keystore) + ProfileScreen — shows your
shareable npub with copy, edits display name + 'about', saves locally and (when
online) publishes kind:0 so peers can recognise you instead of a raw key.
- Drawer 'Profile' -> /profile (gated like Market/Chat).
- i18n en/es/pt. ProfileStore + profile transport covered by plain/dart tests.
Analyzer clean (both packages).
Relay-strategy hardening (network-trust §3): don't hang on one relay.
- NostrChannel interface: what the transports need (sign, publish, subscribe,
reqOnce). Implemented by NostrConnection (single relay) and the new RelayPool.
- RelayPool: connects to all configured relays (skips unreachable ones, throws
only when none are reachable), publishes to every live relay (accepted if any
accepts), merges discovery deduped by event id, and survives a relay dropping.
- Transports now depend on NostrChannel, not a concrete connection.
- SocialService.openSession takes ALL configured relays (was relays.first);
createOffersCubit passes the whole list.
commons_core 63 tests green (3 new pool tests over two in-process relays);
app_seeds social/market 18 green.
Third slice: bridge Block 1's root seed to the Block 2 transport foundation.
- SocialService: derives the secp256k1 Nostr identity from the SAME root seed
the recovery QR backs up (no extra backup), exposes npub/pubkey, and opens a
SocialSession = one shared connection carrying all three transports.
- injector.dart: registers SocialService in get_it. Local-first — derivation is
cheap + offline; no relay is contacted at startup. Replaces the old
"nodeId slice of the seed is the social pubkey" placeholder; keeps the CRDT
author id distinct (unifying it would rewrite authorship — a later decision).
- Test: deterministic identity from the seed hex, matches commons_core, rejects
malformed hex. 5 tests green; app_seeds analyzes clean (0 errors).