Identity redesign Phase 2: server-authoritative surrogate user id (client) - #37
Merged
Conversation
Adds EventType::IdentityChanged, SyncEvent::IdentityChanged, the emit/register/dispatch surface, a C-FFI IdentityEventCallback + replicant_register_identity_callback, and regenerates the cbindgen C header. The emit is wired into both adoption paths in client.rs. Claude-Session: https://claude.ai/code/session_01VpgSNzaVPefKfHKjERxTSQ
Drives the real Client through the join-reply adoption path against a live server: pre-seeds a provisional id + document, then asserts the document is re-stamped to the server's canonical id, identity_adopted flips, and the in-memory id updates. Gated behind RUN_INTEGRATION_TESTS. Claude-Session: https://claude.ai/code/session_01VpgSNzaVPefKfHKjERxTSQ
adopt_identity now rejects a nil canonical_id, old_id == canonical_id, and a second adoption attempt. The user_config UPDATE is scoped to the expected old_id and errors if rows_affected != 1, rolling back the transaction.
… join The canonical user id (delivered by enrollment claim) is now passed into Client construction and adopted before any WebSocket work, so adoption cannot race live document creation. The provisional-topic bootstrap join and post-connect adoption paths are removed; the join reply's user_id is a drift check that refuses to sync on mismatch. Sync is attempted only with credentials and an adopted identity; account switch on an adopted database is rejected. Document payloads without an ownership envelope are rejected instead of stamped with the local owner.
- interop harness: fold boot-script removal into cleanup() — the second trap EXIT was replacing the server-cleanup trap, leaking the booted server on :4000 after every successful run; seed step stderr now goes to the server log instead of /dev/null - client: user_id is write-once after construction — replace the RwLock with a plain field and correct the stale doc comment; document adopt_identity error conditions - replicant-seed: require --user-id (REPLICANT_USER_ID) and pass it as the canonical id — passing None left the client unadopted/local-only, so nothing was ever uploaded Claude-Session: https://claude.ai/code/session_01GwMbERgLK4KAuLrz4b4Liw
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Identity redesign: claim-time adoption
Replaces the original join-reply adoption design, which was protocol-incompatible with the merged server (#6): a fresh client bootstrap-joined
sync:user:<provisional>, which the server rejects withtopic_user_mismatchbefore replying — so adoption could never bootstrap.Design (final)
user_idfromclaim_enrollment; the consuming site passes it through). The client persists it with the credentials.Clientconstruction — before the WebSocket spawns and before the handle is returned. No live sync exists at that moment, so the create-vs-adopt TOCTOU and swallowed-adoption races are impossible by construction, not by locking. Enrolling while running = claim → store creds → recreate the client.Client::newgainscanonical_user_id: Option<Uuid>. Decision table: not-adopted + id → adopt (re-stamp docs, flip flag, emitidentity_changed); adopted + same id → credential rotation, proceed; adopted + different id → error (account switch unsupported — reset local data); no id + adopted → normal restart; no id + not adopted → local-only.user_idis now a drift check: mismatch or garbage → sync error + disconnect. Never re-stamps from the socket path.identity_adoptedmust be 0), scopedUPDATE ... WHERE user_id = oldwith rows-affected check inside the transaction.user_idkey) are rejected and logged instead of stamped with the local owner.Dependencies / follow-ups
Nonefor the canonical id; plumbing (with bounded string buffers) lands in the enrollment PR (Device enrollment: per-user credentials over one-time email codes #38) stack.REPLICANT_TEST_USER_ID) lands next; the gated e2e test in this PR already consumes it.Full workspace suite: 155 passed, 0 failed.