From c0b4250e9626909316a5d2eccd2d2a6dc3d2a481 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 25 Mar 2026 18:03:43 -0700 Subject: [PATCH] fix(spaces): pin visibility and ownerDID as server-authoritative Automerge CRDT sync could overwrite space visibility when a client with a stale cached doc reconnects and merges. Now the server snapshots visibility and ownerDID before processing sync messages and reverts any client-side changes to these fields. These fields can only be changed through the authenticated API (PATCH /api/spaces/:slug), not through CRDT sync. Co-Authored-By: Claude Opus 4.6 --- server/community-store.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/server/community-store.ts b/server/community-store.ts index 0d07729..1bc0f63 100644 --- a/server/community-store.ts +++ b/server/community-store.ts @@ -618,6 +618,10 @@ export function receiveSyncMessage( const peerState = getPeerSyncState(slug, peerId); + // Snapshot server-authoritative fields before sync merge + const prevVisibility = doc.meta?.visibility; + const prevOwnerDID = doc.meta?.ownerDID; + // Apply incoming sync message const result = Automerge.receiveSyncMessage( doc, @@ -625,9 +629,23 @@ export function receiveSyncMessage( message ); - const newDoc = result[0]; + let newDoc = result[0]; const newSyncState = result[1]; + // Pin server-authoritative fields — clients must not overwrite these via sync. + // Visibility and ownership can only be changed through the authenticated API. + if (newDoc !== doc) { + const visChanged = newDoc.meta?.visibility !== prevVisibility; + const ownerChanged = newDoc.meta?.ownerDID !== prevOwnerDID; + if (visChanged || ownerChanged) { + console.warn(`[Store] Sync tried to change authoritative fields in ${slug} — reverting (vis: ${prevVisibility}→${newDoc.meta?.visibility}, owner: ${prevOwnerDID}→${newDoc.meta?.ownerDID})`); + newDoc = Automerge.change(newDoc, 'Pin server-authoritative fields', (d) => { + if (visChanged && prevVisibility) d.meta.visibility = prevVisibility; + if (ownerChanged && prevOwnerDID) d.meta.ownerDID = prevOwnerDID; + }); + } + } + communities.set(slug, newDoc); peerState.syncState = newSyncState;