Commit Graph
8 Commits
Author SHA1 Message Date
Kevin JahnsandAnthony LC 90a5bc1768 (collaboration) show a migrated history save by save
A document's imported past is attributed to `system`, and until now it went
through the same grouping as ordinary editing. That is wrong twice over.

It loses history. Before the migration the editor saved the whole document on
an interval of exactly 60s, each save becoming one version of the legacy file,
and the migration replays them at their original timestamps — so an imported
history arrives as a chain of entries spaced almost exactly the width of the
grouping window. Whether two of them survived as two came down to whether the
round trip that wrote them ran a few milliseconds fast or slow: measured over
simulated sessions, about a third of the chain collapsed. A document whose
saves happened closer together lost far more — four versions two seconds apart
became one.

And it is the wrong question to ask of them. Grouping summarises someone's
editing into the moments that are worth listing; there was no editing session
here, only a record of saves that already happened, and the only honest thing
to do with that is to show it one for one.

`system` is now excluded from grouping on both sides. The collaboration server
takes the exclusion as a parameter, which is what the yhub bump in this commit
is for. The panel applies it again and more strictly: the server declines to
merge an excluded entry into the one before it, having already refused on the
author comparison it makes, while the panel merges across authors and so must
refuse in both directions — an edit made moments after a document was migrated
must not absorb the imported entry beside it, or be absorbed by it.

Live editing is unchanged: several edits by one person seconds apart are still
one version.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-01 15:33:25 +02:00
Kevin JahnsandAnthony LC 2ee19f618f (collaboration) build the version history from the activity api
The version history has been dead since the migration. It listed S3 object
versions of the legacy `{pk}/file` key, and nothing writes that key any more,
so every document's list has been frozen at its migration date; restoring one
was a stub that closed the modal and did nothing, while still promising that
the document would be replaced.

It now reads the collaboration server, which is what keeps the history: the
list comes from `activity`, a selected version is previewed from `changeset`
as the document stood at that moment, and restoring one is a `rollback`.

A version is a minute of editing — changes less than a minute apart become
one, and none spans more than a minute. The collaboration server groups only
changes by the same author, so the browser merges what is left across authors:
a version is a moment in the document, not a moment in one person's editing.
Both are needed, and both use the same rule.

This grants `history.rollback` to editors, which is the first time a browser
may change the past rather than read it, and publishes the rollback route.
A reader is refused it twice over — the collaboration server treats it as a
dead grant without document write access, and the endpoint is withheld as well.
Mutations refuse where reads clamp, so a rollback reaching further back than
the history a user was granted is rejected rather than trimmed: nobody can
undo work that predates their own access, and a rollback with no bound at all
is refused outright. `prune`, which erases, stays granted to nobody. Restoring
is not destructive: it appends a change that undoes another, so what it
replaced stays in the history and can be restored again.

The backend's version endpoints are untouched and now have no caller. They are
marked deprecated with the condition for removing them, since until a document
has been replayed by `migrate_documents` they hold the only record of what it
looked like before it moved.

Also fixes the e2e helper that waited for the removed content endpoint, so it
never returned, and the three version tests that hung behind it.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-01 15:33:02 +02:00
Kevin JahnsandAnthony LC f9929dc9c4 (collaboration) show a user the history since they got access
The collaboration server's activity and changeset routes are opened to
the browser, so a document's editing history can be read from where it
actually lives now. What a user may see of it is bounded to the moment
they were given access to the document: joining a document that has been
written for a year does not hand them the year.

That rule is not new. It is the one the version endpoints have always
applied - "only those created after the user got access to the document"
- and the date is the same one: the earliest access the user holds on
the document or on any of its ancestors, so sharing a folder shares its
subtree from that moment. It was computed twice in the backend,
differently, and exposed nowhere. It is now a single annotation,
user_access_since, that the version endpoints and the collaboration
server both read, the latter through the document detail response it
already fetches to authorize a connection.

The bound is applied server-side and silently: a client asks for
whatever range it likes and receives only its own share, so there is no
bound for it to get wrong and none it can widen. It is a stored date
rather than a wall-clock-relative one, which is what keeps it stable
across a websocket re-check, and it is never zero - the one value that
would also unlock a full-history connection.

A reader who reaches a document through its link alone holds no access
and so has no date to bound a history with. They get none, which is why
the backend has always refused them their versions. rollback and prune
stay refused to everyone: restoring a version is a separate decision.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-01 15:32:01 +02:00
Kevin JahnsandAnthony LC 4f46f29af4 ⬆️(collaboration) upgrade yhub to 0.8.0 and adopt its permission model
yhub 0.8.0 retires the 'r' | 'rw' | null access vocabulary. The auth
plugin now answers a typed permission object stating, facet by facet,
what a subject may do with a document, and yhub enforces every facet
itself - on the websocket and on the REST routes alike. Three rules we
wanted but could not express under the old vocabulary become one-line
facets. Our whole access policy now lives in
src/yhub-server/permissions.js, apart from the server so that it can be
read and tested without standing up redis and postgres.

Read-only users no longer share their cursor #2544. A read-only
connection could still propagate awareness updates to everyone else in
the document, even though its document updates were already dropped.
Presence is now a permission of its own, separate from the right to
edit: a reader receives it and never publishes it. The collaboration
server enforces that rather than trusting the editor to stay quiet, so a
modified or stale client changes nothing. The frontend has to know it
too - the http fallback provider has no receive-only mode, so a reader's
provider is built with no awareness instance at all, or its first PATCH
would take a 403 and close it for good.

The browser is granted only the two routes it uses, the websocket and
ydoc for the http fallback. Everything else - history, rollback, prune,
and every backend-internal endpoint - is refused to it, as is any
endpoint a future release adds, because the grant names no wildcard.
create-ydoc in particular was reachable by any signed-in editor and is
now the backend's alone.

Anonymous visitors are given the userid "anonymous" rather than no
identity at all, which is what lets them keep editing public documents:
yhub refuses the upgrade of a caller that holds the write but cannot be
attributed. Their edits share one author.

Room is DocRef throughout, which is a rename of object keys and not only
of types: the worker event payload and the stream message lookup both
carry it, and both fail silently rather than loudly when missed.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-01 15:32:00 +02:00
Kevin JahnsandAnthony LC f7e339c907 (collaboration) fall back to http polling when the websocket is blocked
Some networks refuse a websocket upgrade - corporate proxies, captive
portals - and a browser is told nothing more than "the connection
closed", so those users could not edit at all. The editor now runs a
second transport next to the socket, polling the collaboration server's
REST api on the same room, with the same session cookie and the same
authorization, and only while the socket is down. Local changes go out
about a second after the last keystroke and remote ones arrive within
ten seconds, so editing works with visibly more latency rather than not
at all. The socket keeps being retried underneath, so a client that fell
back during an outage returns to it on its own, and nothing is lost in
either direction - both transports publish from the same document.

This makes /collaboration/ydoc/ a route browsers call, so
COLLABORATION_SERVER_ORIGIN is now handed to yhub as its cors
configuration and gates the http routes as well as the websocket.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-01 15:32:00 +02:00
Manuel RaynaudandAnthony LC 0aa3fbb653 (helm) deploy new infra using helm
The new infra we have must be configured in the helm chart. This commit
all the missing templates to deploy yhub, it also automate the creation
of the private keys needed by all services.
2026-09-01 15:30:00 +02:00
Manuel RaynaudandAnthony LC d3fe8de9d0 🔥(backend) remove CollaborationService and can-edit endpoint
The CollaborationService was doing nothing since we started the
migration to yhub, all the code using it is now removed. Also the
`can-edit` endpoint and all the safeguard mechanism relying on the
presence of other users connected to the websocket will not be used
anymore, it will be possible to replace all of this with yhub, so all
this code is also removed.
2026-09-01 15:15:36 +02:00
Manuel Raynaud 4ed0ac5ba9 📝(documentation) how to enable collaboration safe guard mechanism
We write a documentation explaining how to enable the safe guard
collaboration feature to prevent a user not connected to the websocket
to erase the data coming from users connected to the websocket.
2026-07-06 08:55:21 +02:00