Skip to content

Identity redesign Phase 2: server-authoritative surrogate user id (client) - #37

Merged
adamski merged 16 commits into
mainfrom
adam/identity-redesign-client
Jul 13, 2026
Merged

Identity redesign Phase 2: server-authoritative surrogate user id (client)#37
adamski merged 16 commits into
mainfrom
adam/identity-redesign-client

Conversation

@adamski

@adamski adamski commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

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 with topic_user_mismatch before replying — so adoption could never bootstrap.

Design (final)

  • Canonical id is delivered at enrollment claim (server PR Separate callback types #7 returns user_id from claim_enrollment; the consuming site passes it through). The client persists it with the credentials.
  • Adoption runs only during Client construction — 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::new gains canonical_user_id: Option<Uuid>. Decision table: not-adopted + id → adopt (re-stamp docs, flip flag, emit identity_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.
  • Join-reply user_id is now a drift check: mismatch or garbage → sync error + disconnect. Never re-stamps from the socket path.
  • Offline-first contract locked by tests: no credentials (or unadopted identity) → local DB fully usable, zero connection attempts (verified with a counting listener; the guard demonstrably fails if the gating regresses), no reconnect spam.
  • Adoption gates hardened: nil canonical id rejected, single-shot (identity_adopted must be 0), scoped UPDATE ... WHERE user_id = old with rows-affected check inside the transaction.
  • Document payloads without an ownership envelope (user_id key) are rejected and logged instead of stamped with the local owner.

Dependencies / follow-ups

Full workspace suite: 155 passed, 0 failed.

adamski added 8 commits July 6, 2026 10:05
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
adamski added 8 commits July 13, 2026 15:33
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
@adamski
adamski merged commit 5066b78 into main Jul 13, 2026
4 checks passed
@adamski
adamski deleted the adam/identity-redesign-client branch July 13, 2026 18:55
@adamski
adamski restored the adam/identity-redesign-client branch July 13, 2026 18:57
@adamski
adamski deleted the adam/identity-redesign-client branch July 13, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant