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;