Files
twenty/packages
Thomas Trompette 62c1cf78af fix(server): scope personal favorite SSE events to their owner (#23712)
## Problem

Fixes #20483.

In a multi-user workspace, when a user creates/updates/deletes a
**personal favorite** (a `navigationMenuItem` with a non-null
`userWorkspaceId`), the metadata SSE event is broadcast workspace-wide.
Every other connected user receives it and the favorite pops into their
own sidebar in real time. Cross-user data-isolation leak.

## Root cause

The delivery filter in `WorkspaceEventBroadcaster` already supports
per-user scoping via `recipientUserWorkspaceIds`, but treats an
**undefined** list as workspace-wide (delivered to every stream).
`MetadataEventPublisher.publish` never set that field, so every favorite
event fell into the workspace-wide default. The `agentChatThread` path
already sets `recipientUserWorkspaceIds` explicitly and is scoped
correctly; favorites simply never opted in.

## Fix

In `MetadataEventPublisher`, resolve the owning `userWorkspaceId` for
`navigationMenuItem` events (from `properties.after` on create/update,
`properties.before` on delete) and set `recipientUserWorkspaceIds:
[userWorkspaceId]` when present. Workspace-level items (`userWorkspaceId
=== null`) leave it unset and keep broadcasting to everyone. This
mirrors the existing `agentChatThread` precedent and touches only the
producer, not the broadcaster or any consumer.

## Testing

Unit test (`metadata-event-publisher.spec.ts`) covers personal
create/update/delete (scoped to owner), workspace-level (unscoped), and
an unrelated metadata entity carrying a user id (unscoped).

Also verified end to end against a local multi-user workspace (Tim and
Jane, same workspace): each opened a live SSE stream (`/metadata`
`onEventSubscription`) and Tim created favorites.

| Case | Before fix | After fix |
|------|-----------|-----------|
| Personal favorite -> owner (Tim) | receives | receives |
| Personal favorite -> other user (Jane) | **receives (leak)** | not
received |
| Workspace-level favorite -> other user (Jane) | receives | receives |

- `nx typecheck twenty-server`: pass
- oxlint + oxfmt on changed files: clean

## Scope / follow-up

Favorites only. Two related items are intentionally out of scope and
worth tracking separately: scoping other user-owned metadata (`view` via
its visibility rules, `roleTarget`), and making the broadcaster's "no
recipient list = everyone" default explicit rather than fail-open.
2026-08-03 16:07:54 +00:00
..