Skip to content

feat: version gating, client-id declarations, and PUD-only update suppression - #28

Draft
petergaultney wants to merge 1 commit into
No-Instructions:mainfrom
TrilliantHealth:thds/upstream-pr-behavioral
Draft

feat: version gating, client-id declarations, and PUD-only update suppression#28
petergaultney wants to merge 1 commit into
No-Instructions:mainfrom
TrilliantHealth:thds/upstream-pr-behavioral

Conversation

@petergaultney

Copy link
Copy Markdown

Summary

Adds three server-side capabilities for self-hosters managing a fleet of Relay plugin installations:

  • Plugin version gating (allowed_client_versions): doc websocket connections from user-claimed tokens must report a version in the configured allowlist, or get 403. Server tokens are exempt. Empty list (default) disables gating. Denial warns are throttled to one log line per user per 30 minutes.

  • Client-id declarations (cid query param + relayClientIds awareness field): plugins can declare which Yjs client ids they mint edits under (e.g. a merge-recovery working copy uses a separate local doc with its own client id). The server records these in PUD at websocket upgrade and tracks their plugin version via awareness payloads. This enables accurate version attribution even for edits that arrive through intermediary docs.

  • PUD-only update suppression: updates authored entirely by the server's own 53-bit client ids (PermanentUserData registration writes) now skip webhook dispatch. These are internal bookkeeping, not human-produced content changes, and firing webhooks for them creates unnecessary git-sync churn.

Details

Version gating

A new allowed_client_versions field on [server] in relay.toml. When non-empty, the v query param on websocket upgrade is checked against it. Clients too old to report a version are blocked. The gate is enforced on both the deprecated and full-path websocket upgrade handlers.

Client-id declarations

The cid query param accepts a comma-separated list of Yjs client ids. At websocket upgrade, each declared id gets:

  1. Its version recorded (from the connection's v param)
  2. PUD registration (binding the client id to the authenticated user in the doc's users map)

The relayClientIds awareness payload field is also consumed: when a client broadcasts additional ids it mints edits under, those are recorded with the same declared version.

Version tracking infrastructure

client_versions.rs implements a two-tier trust model:

  • Declared: the client's own statement via cid param or relayVersion awareness payload. Overwrites previous values.
  • Inferred: first-introduction binding from a solo awareness entry's connection version. Immutable once set, rendered with ~ prefix.

Updates never bind versions (the deliverer is not the minter). Non-first awareness entries are tracked as "seen" but never inferred from.

PUD suppression

is_server_only_update checks whether all client ids in an update are >= 2^32 (server-authored). These updates still get dispatched as event envelopes (so sync protocol events fire), but skip the webhook path.

Relationship to the other open PRs

Independent - this branches straight off main and contains only behavior changes. No log-level changes here; those are all in #26.

PR Contains Independent?
#26 log volume: demotions, connection lifecycle logs, identity on warns yes
#23 (this one) behavior: version gating, cid declarations, PUD suppression yes
#24 semantic logging, gated on a config flag that defaults to off no - needs #23
#25 attributed-content endpoint yes

#26 and this PR both touch the Received awareness update with more than one client line - #26 changes its level, this one adds a doc_id field. Whichever merges second takes a one-line textual conflict; there is no semantic disagreement.

Test plan

  • Verify websocket connections without v param are rejected when allowed_client_versions is non-empty
  • Verify server tokens bypass the version gate
  • Verify cid param registers PUD entries at upgrade
  • Verify PUD-only updates don't fire webhooks
  • Verify empty allowed_client_versions (default) disables gating
  • 260 tests pass (cargo test in crates/)

…pression

Three behavioral additions, all backward compatible:

1. allowed_client_versions (relay.toml [server] field): when non-empty,
   doc websocket connections must report one of the listed plugin versions
   via the `v` query param. Server tokens are exempt. Denial warns are
   throttled to one per user per 30 minutes.

2. cid query param + relayClientIds awareness field: a plugin can
   pre-declare the Yjs client ids it mints (e.g. a merge working copy),
   binding them to its reported version at connect time. Also registers
   declared ids in PUD at upgrade, so the authenticated user is bound
   before any update can mis-attribute them.

3. Server-only update suppression: updates authored entirely by the
   server's own 53-bit client ids (PUD registration writes) skip the
   normal event callback path, suppressing the webhook dispatch that
   would otherwise fire for internal bookkeeping.

Supporting infrastructure:
- client_versions.rs: per-client-id version tracking with declared
  (owner-minted) and inferred (first-introduction) trust levels
- edit_author.rs: client-id extraction from updates, is_server_only_update
- AwarenessEntryFacts struct on DocConnection for typed awareness parsing
- PUD registration made public so the cid upgrade path can call it
- doc_id on DocConnection, so its log lines can name the doc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant