The problem I am facing
I need to modify an incoming awareness update on the server before it is broadcast to peers. The motivating case is stamping a server-validated user identity (from onAuthenticate / connection.context.user) onto state.user so cursor labels cannot be spoofed by clients.
The closest existing hook is onAwarenessUpdate, but it runs after Document.handleAwarenessUpdate has already broadcast the original payload. Document.handleAwarenessUpdate is attached in the Document constructor and ends up first in the listener chain; onAwarenessUpdate is wired later, in loadDocument. Mutations the hook makes therefore only reach peers in a second frame moments later. The CRDT converges on the corrected value, but the original payload still leaves the server and is visible to DevTools and any custom y-awareness listener on the client.
The solution I would like
Either of the following would work:
- A new
beforeAwarenessUpdate hook fired before Document.handleAwarenessUpdate, where the hook can mutate or reject the awareness state and the broadcast picks up its changes. This mirrors beforeHandleMessage but at the awareness layer.
- Reorder the listener chain so
onAwarenessUpdate runs before the broadcast, and document that mutations made from inside it are included in the outgoing frame.
The contract in either case: whatever a hook writes into awareness.getStates().get(clientId) is what peers receive.
Alternatives I have considered
beforeHandleMessage with byte-level rewriting. Works, but requires decoding the y-protocols wire format and tracking future protocol changes.
- Living with the double-broadcast and relying on CRDT convergence. Browsers usually only paint the corrected state, but the original payload is still on the wire and observable.
- Client-side identity stamping. Moves the trust boundary back to the client, which defeats the goal.
Additional context
Repro (assumes #1094 is merged, otherwise payload.connection is undefined):
new Server({
async onAuthenticate({ token }) {
return { user: { id: 'authoritative', name: 'Authoritative' } }
},
async onAwarenessUpdate({ awareness, connection, added, updated }) {
const expected = connection?.context?.user
if (!expected) return
for (const clientId of [...added, ...updated]) {
const state = awareness.getStates().get(clientId)
if (!state) continue
state.user = expected
awareness.emit('update', [{ added: [], updated: [clientId], removed: [] }, 'server'])
}
},
})
Connect a client, call awareness.setLocalStateField('user', { name: 'Spoofed' }). In a peer's DevTools Network → WS, two consecutive frames appear: spoofed, then corrected.
I am happy to put up a PR for whichever shape fits the project. Asking first because the answer likely touches the public hook contract.
Related: #1094.
The problem I am facing
I need to modify an incoming awareness update on the server before it is broadcast to peers. The motivating case is stamping a server-validated user identity (from
onAuthenticate/connection.context.user) ontostate.userso cursor labels cannot be spoofed by clients.The closest existing hook is
onAwarenessUpdate, but it runs afterDocument.handleAwarenessUpdatehas already broadcast the original payload.Document.handleAwarenessUpdateis attached in theDocumentconstructor and ends up first in the listener chain;onAwarenessUpdateis wired later, inloadDocument. Mutations the hook makes therefore only reach peers in a second frame moments later. The CRDT converges on the corrected value, but the original payload still leaves the server and is visible to DevTools and any custom y-awareness listener on the client.The solution I would like
Either of the following would work:
beforeAwarenessUpdatehook fired beforeDocument.handleAwarenessUpdate, where the hook can mutate or reject the awareness state and the broadcast picks up its changes. This mirrorsbeforeHandleMessagebut at the awareness layer.onAwarenessUpdateruns before the broadcast, and document that mutations made from inside it are included in the outgoing frame.The contract in either case: whatever a hook writes into
awareness.getStates().get(clientId)is what peers receive.Alternatives I have considered
beforeHandleMessagewith byte-level rewriting. Works, but requires decoding the y-protocols wire format and tracking future protocol changes.Additional context
Repro (assumes #1094 is merged, otherwise
payload.connectionisundefined):Connect a client, call
awareness.setLocalStateField('user', { name: 'Spoofed' }). In a peer's DevToolsNetwork → WS, two consecutive frames appear: spoofed, then corrected.I am happy to put up a PR for whichever shape fits the project. Asking first because the answer likely touches the public hook contract.
Related: #1094.