Skip to content

Add a hook to modify awareness updates before broadcast #1095

@dschmidt

Description

@dschmidt

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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions