Commit Graph
2373 Commits
Author SHA1 Message Date
Manuel Raynaud cb73dba070 🔥(yhub) remove custom endpoint get-ydoc
We don't need anymore the get-ydoc endpoint to fetch a document content
since yhub 0.5.0 can manage json encoding. We can safely remove it.
2026-09-02 09:41:48 +02:00
Manuel Raynaud ae5ca7c340 ♻️(backend) take adavantage of yhub 0.5.0 json encoding returns
The version 0.5.0 can manage response format by using accept and
content-type headers. In python we can't use for now the lib0 decoder so
we have to use the json format. When the lib0 decoder will be available
in pycrdt we will use it. So we can now use directly the /ydoc api to
fetch a document content instead the custom api made for this.
2026-09-02 09:41:48 +02:00
Manuel Raynaud f1ca8222d9 ♻️(backend) duplicate the onboarding sandbox using YHub service
Duplicate the onboarding sandbox document through the
collaboration server: its content is read from there and copied under the
identity of the user the sandbox is created for. A collaboration server that
cannot be reached skips the sandbox, as a missing template already did, and
never fails the signup
2026-09-02 09:41:47 +02:00
Manuel Raynaud 4bf7ce7bae ♻️(backend) index the content of a document from updated_content endpoint
the search indexer reads it with `YHubService`, and the indexation of an
edited document is triggered by the `content-updated` call the collaboration
server makes — nothing else sees the content change anymore. It is queued as
a celery task, throttled like the other updates, so no indexation ever runs
in the process serving the request. A document whose content cannot be read
is left out of the batch rather than indexed empty, which would have erased
it from the search backend
2026-09-02 09:41:47 +02:00
Manuel Raynaud c288872132 (collaboration) notify the backend when the worker persists new content
notify the backend when the worker persists new content for
a document, so the lists ordered by `updated_at` follow the edits made on the
collaboration server. The backend serves it on
`POST /api/v1.0/documents/{id}/content-updated/`, authenticated with a short
lived RS256 JWT the collaboration server signs (`aud: "docs-backend"`) and
the backend verifies against the JWKS the collaboration server publishes on
`/collaboration/jwks/v1` — the mirror of the admin token the backend signs to
call it, so no long lived secret is shared and either side can roll its key
on its own
2026-09-02 09:41:47 +02:00
Manuel Raynaud d4f93c3d86 (backend) serve documents/{id}/formatted-content/ from yhub
The formatted-content endpoint was using the `document.content` to fetch
the ydoc from s3, we want to move from this usage to using yhub to
retrieve the content, so yhub is becoming our source of thruth.
2026-09-02 09:41:46 +02:00
Manuel Raynaud 2573d899f9 💥(backend) remove the documents/{id}/content/ endpoint
, both its PATCH and its GET: the content of a document is saved and
served by the collaboration server. The `content_patch` and
`content_retrieve` abilities go with it.
2026-09-02 09:41:46 +02:00
Manuel Raynaud e1014d5d41 (backend) duplicate a document through the collaboration server
its stateis fetched from yhub and seeded into the copy instead
of being copied from the content stored by Django
2026-09-02 09:41:44 +02:00
Manuel Raynaud 62a77b99de (collaboration) add a get-ydoc endpoint on yhub
`GET /collaboration/get-ydoc/v1/docs/{id}` answers the current Yjs state of a
document as a raw binary update, the read counterpart of create-ydoc, and
204 when the document has no content yet
2026-09-02 09:36:42 +02:00
Manuel Raynaud e1907d0934 (backend) call YHubService to seed initial document content
When a new Docs is created and a file is sent, as before we convert it
first and we need to use the raw content to seed it by calling the
create-ydoc api in the YHub service.
2026-09-02 09:36:42 +02:00
Manuel Raynaud 89d61985a5 ️(backend) reintroduce the reset connection mechanism
When an access change or is deleted or a link configuration changes, we
call the yhub server to reset connections and remove them if needed. The
YHubService is used for this.
2026-09-02 09:36:41 +02:00
Manuel Raynaud 05190af71f (backend) implement reset-connections and create-ydoc in YHubService
The reset-connections and create-ydoc are the first action we want to
implement in the YHubService. They will be used in next commits.
2026-09-02 09:36:41 +02:00
Manuel Raynaud 48d022c4c0 ♻️(backend) audience is an enum to be used by the JWTService
To ease the use of the audience with the JWTService, we choose to create
an enum holding all the possible values and then use them in the Yhub
and Y-converter services.
2026-09-02 09:36:41 +02:00
Manuel Raynaud 8580b4b346 (backend) add a service to call the yhub REST API
The backend application will have to call the yhub REST API for some
operations. We want to use a dedicated service to do that. This first
commit introduces the shape of this service, it only does the
configuration for now, calling actions will be implemented later.
2026-09-02 09:36:41 +02:00
Anthony LCandManuel Raynaud 860e296bb0 🛂(y-provider) verify jwt token instead of the shared api key
The /api/convert route no longer accepts the Y_PROVIDER_API_KEY shared
secret. It now verifies the admin JWT signed by Django against the
JWKS published on its /api/v1.0/jwks endpoint.
2026-09-02 09:36:40 +02:00
Manuel Raynaud f5f3be945a 🔧(collaboration) adapt docker stack for development purpose
The yhub image was build only for a production usage. In development we
want to have a hot reload when a file is modified. For this the
Dockerfile is modified, the nodemon package install in dev environment
and used to watch modification against the source files.
2026-09-02 09:36:40 +02:00
Manuel Raynaud 433b28cd7f ♻️(yhub) maintain database schema using npm run init-db
The yhub database schema have new update and will probably be modified
in the future. We don't want to maintain this sql schema in the Docs
repo, we want to reuse what is directly made in the yhub project. For
this we reuse the existing bin/init-db.js script
2026-09-02 09:36:40 +02:00
Manuel Raynaud c607f28478 ⬆️(yhub) upgrade yhub to version 0.6.0 2026-09-02 09:36:39 +02:00
Kevin JahnsandManuel Raynaud c9690e4eca (collaboration) test the legacy migrations against a real yhub
Cover both paths off the legacy Django store end to end: the lazy seed on
first access, and the migrate endpoint replaying every S3 version. The tests
need no database — the admin JWT short-circuits document authorization, so a
fixture is an S3 object on a random uuid — and read the timeline through
yhub 0.5.0's `Accept: application/json`, which spares python a lib0 decoder.
CI grows a valkey service and starts a collaboration server alongside the
backend test job; the tests skip themselves when nothing answers on the new
COLLABORATION_API_URL setting, so `make test` without the dev stack still
passes.

Writing them turned up three things worth fixing in the server.

Backend reads now seed too. getAccessType short-circuited on the admin token
before reaching the legacy store, so a server-side read of an unmigrated
document answered with an empty one, and a create-ydoc against it would have
written a second lineage beside the content the first user access was about
to seed in.

Seeding no longer decides access; the backend's answer alone does. A legacy
object that cannot be migrated — it does not decode, or it exceeds the size
we load — opens as a new document instead of denying, since no retry can fix
it and refusing would leave the document unopenable by anyone. The cause is
logged once per attempt with the bucket, key and stack, and every later access
logs that it admitted a caller without migrating.

That made the failure classifier dangerous, so it is inverted. It was an
allowlist of retryable errors — eight socket errnos — which left every way S3
can refuse (AccessDenied on a rotated key, NoSuchBucket, a region redirect)
counting as "this object is unusable". Denying, that was survivable; opening
empty, one misscoped credential would fork every document touched during the
window. Now only a failure raised while interpreting bytes we already hold is
permanent, marked at the throw site, and everything else answers a retryable
503. Guessing wrong that way costs a retry; the other way costs the document.

The admin seed is also fenced to the org and to main, like the user path
above it. The legacy store is branchless — {docid}/file is main — and the
bookkeeping is per document, so seeding ?branch=draft would have written
main's content into an orphan room and left the real one permanently empty.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-02 09:36:39 +02:00
Kevin JahnsandManuel Raynaud 16f8aaa759 (collaboration) replay legacy s3 version history into yhub
Add POST /collaboration/migrate/v1/docs/{id}, which replays every S3 version
of a document's legacy `{id}/file` object into one gc:false Yjs document and
stores it as a single row at clock 0, crediting each version with its own S3
timestamp. Nothing existing is deleted and nothing goes on the stream, so the
next compaction merges that row like any other. The clock-0 insert is ON
CONFLICT DO NOTHING and migrated ids are kept in a valkey set, so the endpoint
is idempotent without a lock. The activity api then reports the same timeline
as the backend's /documents/{id}/versions/, instead of the single
migration-time change the lazy soft migration leaves behind.

That lazy seed now writes no insertAt/deleteAt. Persisted contentmaps are
merged rather than de-duplicated, so a seed timestamp would survive next to
the real per-version one on the same ids and the activity api would report
whichever the unordered row scan put last. A seed is not an editing event and
has no honest time to report.

Upgrade yhub to 0.5.0, where error codes encode retry semantics (4xx
permanent, 5xx and 429 retryable) and auth plugins may throw apiError(503). A
temporarily unreachable Django backend, JWKS endpoint or legacy S3 store is
now reported as 503 rather than denied like a permission failure, so clients
retry instead of giving up.

The legacy-store code moves out of server.js into migration.js, with the
shared *_FILE secret helper in env.js.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-02 09:36:39 +02:00
Kevin JahnsandManuel Raynaud 175705ce80 🔒️(collaboration) reject admin jwts not issued for the yhub audience
yhub verified Django's RS256 admin JWT without checking "aud", so the
y-converter token Django hands to the converter process was replayable
here — and admin: true short-circuits getAccessType to "rw" on every
document, plus the backend-internal reset-connections purpose and the
X-User-Id attribution override. Require aud: "yhub", as y-provider
already does for its own audience. Nothing in the backend calls yhub's
admin endpoints yet, so no caller is affected.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-02 09:36:38 +02:00
Kevin JahnsandManuel Raynaud 6a5eb379c5 📝(changelog) note that get-connections is dropped, not deferred
The hocuspocus-to-yhub migration entry claimed both the kick and
get-connections APIs were deferred pending a yhub equivalent. The kick
flow now has its server-side replacement (the reset-connections
endpoint, backend wiring pending), and get-connections lost its only
consumer when the can-edit mechanism was removed — it is dropped, not
awaiting reimplementation.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-02 09:36:38 +02:00
Kevin JahnsandManuel Raynaud 9544915f91 (collaboration) soft-migrate legacy S3 documents into yhub
With SOFT_MIGRATION=true, the first access to a document yhub does not
know yet fetches the legacy snapshot from Django's S3 media bucket
({id}/file, UTF-8 base64 of a raw Yjs update), seeds the room through
the compute pool - attributed to "system" with a migration=s3 custom
attribution - and only then admits the connection, so the initial sync
always includes the seed. Now that the frontend no longer bootstraps
rooms client-side (content GET/PATCH removal), this is the only path
that brings legacy content into yhub; keep the flag on until a batch
backfill has migrated the full corpus.

A missing S3 object is the brand-new-document case and yields an empty
room; every real failure fails closed (opaque 401, y-websocket retries
with backoff). Existence is probed postgres-first (bare SELECT, then
the valkey stream, then the SELECT again to close the compaction
race). Guard rails: a per-docid verdict cache (poison objects cannot
sustain an S3 retry storm, transient errors expire in 15s, per-replica
seed backpressure denies once without caching), in-flight dedup, a
token-owned cross-replica valkey lock released by compare-and-delete,
a 10s S3 fetch timeout that also destroys a late-arriving response
stream, and the same 10MiB decoded cap as create-ydoc. Concurrent
seeds stay correct regardless: the frozen snapshots share one Yjs
lineage, so duplicates merge as CRDT no-ops.

Also reject non-lowercase docids (Django serializes UUIDs lowercase; a
case variant would open a parallel room and miss its S3 object) and
refuse to boot when AWS_S3_ENDPOINT_URL carries a path the minio
client cannot address. On AWS the read-only credentials must include
s3:ListBucket so a missing object surfaces as NoSuchKey rather than
AccessDenied - see the README for the full guarantees and ops notes.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-02 09:36:38 +02:00
Kevin JahnsandManuel Raynaud 4fb93e1e96 🙈(dev) ignore playwright-mcp browser artifacts
The Playwright MCP browser session writes snapshots and console logs
into .playwright-mcp/ at the repository root while driving the app
during development — keep them out of version control.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-02 09:36:37 +02:00
Anthony LCandManuel Raynaud 6360168a1c 🔥(frontend) remove content GET PATCH
We remove the code related to the content GET and
PATCH endpoints, as they are no longer used in the
codebase. The yhub server will handle the content
management directly, providing the content and
managing the updates. This change simplifies the code
and reduces the complexity of the frontend application.

We will need to reimplement the saving mechanism
in the service worker when we are offline.
Let's wait that the service is fully developed
on the yhub side before we implement this feature.
2026-09-02 09:36:37 +02:00
Anthony LCandManuel Raynaud 98ae5f1d94 🔥(project) remove occurences of COLLABORATION_SERVER_SECRET
COLLABORATION_SERVER_SECRET is no longer used in the codebase,
so we can remove it from the codebase.
2026-09-02 09:36:37 +02:00
Anthony LCandManuel Raynaud ee4bd54621 🔥(frontend) remove "can-edit" mechanism
We will not block anymore the users not connected
to the collaboration server from editing the document,
we will have an HTTP fallback instead, so we can
remove the "can-edit" mechanism and the related code.
2026-09-02 09:36:36 +02:00
Kevin JahnsandManuel Raynaud 18be947448 🔒️(collaboration) harden the create-ydoc endpoint
Address the findings of an adversarial review of the new endpoint:

- Only the backend admin token may attribute content to another user via
  the X-User-Id header. The endpoint uses the default access purpose, so
  any editor with update ability can call it — honoring the header for
  them would let an editor forge the attribution history of the first
  revision (the websocket path likewise stamps the server-side
  identity). Regular callers now always author as themselves; verified:
  an editor session posting X-User-Id gets its own userid stamped.

- Reject non-main ?branch= requests (400). Cookie users are main-only
  via getAccessType, but the admin token bypasses it and could seed an
  orphan (org, docid, branch) room no user-facing path reads — while
  dodging the branch-scoped 409 existence check.

- Correct the concurrent-create comment: two racing creates merge as
  independently generated updates (fresh clientIDs), so the seeded
  content appears twice — user-visible duplication, not merely a
  doubly-attributed revision. Still accepted (Django creates each doc
  once and a duplicated seed is user-fixable), but the tradeoff is now
  stated accurately.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-02 09:36:36 +02:00
Kevin JahnsandManuel Raynaud 3f5f1486fb (collaboration) add create-ydoc endpoint on yhub
Python cannot call yhub's built-in PATCH ydoc api because its body must
be lib0-any encoded - a lib0-specific binary framing with no
implementation outside javascript. The new endpoint
POST /collaboration/create-ydoc/v1/{org}/{docid} accepts the raw binary
Yjs update (pycrdt get_update() / Y.encodeStateAsUpdate output) as
application/octet-stream, so Django can seed a document's initial state
with a plain requests.post(url, data=raw_bytes) - needed by the
server-side creation flows (file import, create-for-owner, duplication,
template instantiation) whose yhub rooms currently stay empty until the
first browser connects.

Strict create semantics: 409 when the room already has content
(checked via getDoc, covering persisted state and uncompacted stream
messages; yhub has no atomic create, concurrent creates merge via CRDT
and never corrupt). The initial content is attributed to the optional
X-User-Id header, else to the caller's identity. Access uses the
default purpose, i.e. standard document write access like the built-in
ydoc routes: the admin JWT, or a user session with update ability.
Malformed updates map to 400 (the compute worker rejects them and the
pool replaces the thread), empty updates to 400, bodies over 10MiB to
413.

Gotcha worth noting: req.bytes() resolves to a Node Buffer, but yhub's
compute-task schema validates with lib0's exact-constructor Uint8Array
check, so the body is re-viewed as a plain Uint8Array before it is
handed to the compute pool.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-02 09:36:36 +02:00
Anthony LCandManuel Raynaud 2228bb7463 🛂(backend) add audience to jwt
Add audience to the jwt, scoping the token to it
prevents an admin JWT issued for another backend
service from being replayed against y-provider.
2026-09-02 09:36:35 +02:00
Anthony LCandManuel Raynaud 54e499b233 🔥(helm) remove occurences of Y_PROVIDER_API_KEY
Y_PROVIDER_API_KEY is no longer used in the codebase,
so we can remove it from the helm chart and the documentation.
We adapt the documentation to use the new JWT conversion
mechanism instead.
2026-09-02 09:36:35 +02:00
Anthony LCandManuel Raynaud 9e1261bdac 🛂(y-provider) verify jwt token instead of the shared api key
The /api/convert route no longer accepts the Y_PROVIDER_API_KEY shared
secret. It now verifies the admin JWT signed by Django against the
JWKS published on its /api/v1.0/jwks endpoint.
2026-09-02 09:36:35 +02:00
Anthony LCandManuel Raynaud 5998fb30fb 🛂(django) use jwt token for converter services
The Y_PROVIDER_API_KEY shared secret is replaced by a
signed admin JWT when Django calls the y-provider
conversion endpoint.
2026-09-02 09:36:34 +02:00
Kevin JahnsandManuel Raynaud 894a907e59 (collaboration) add admin reset-connections endpoint on yhub 0.4.0
Add POST /collaboration/reset-connections/v1/{org}/{docid} (optional
X-User-Id header) to yhub-server. It distributes yhub recheckAuth: every
server re-runs the access check per matching connection and closes only
those whose access actually changed (close code 4401), so unaffected
clients see no reconnect churn. The endpoint authenticates with the
RS256 admin JWT issued by JWTService, verified against the backend JWKS
(new jose dependency); the admin token acts as the "system" user and is
the only principal granted the reset-connections access purpose. The
backend does not trigger it on permission changes yet - that wiring
comes separately, now that CollaborationService is gone.

yhub is upgraded to 0.4.0 and serves every route under the
/collaboration/ prefix (server.apiPrefix): the websocket moves to
/collaboration/ws/v1/docs, and the built-in document apis are meant to
be publicly exposed alongside it, with reset-connections as the one
backend-internal exception.

Also harden websocket auth: fail closed when the backend errors (only a
genuine 401/403 falls back to the anonymous identity, so a signed-in
editor can never hide from a targeted recheck under an anon userid) and
tolerate small clock skew when verifying the cached admin token.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-02 09:36:34 +02:00
Manuel Raynaud 03795436f4 🔥(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-02 09:36:34 +02:00
Manuel Raynaud e909c4905e 🔥(ci) remove checking print statement in lint-git
Since we use ruff, it is not needed anymore to check the presence of
print statement, the rule T201 is already doing it in a more performant
way.
2026-09-02 09:36:33 +02:00
Manuel Raynaud e7afd2a386 🔧(dev) generate the JWT signing key when bootstrapping the dev stack
Thw private key needed to generate a jwt token will be mandatory. In
order to ease the development we want to automate its generation
2026-09-02 09:36:33 +02:00
Manuel Raynaud c282dc6096 (backend) add a method to create a dedicated admin token
For now the only token we will need is ont with the admin claim set to
True. To not repeat the creation of this token again and again, we
created a dedicated method to issue this token in the JWTService class.
2026-09-02 09:36:33 +02:00
Manuel Raynaud 5adfcecf4e (backend) publish the JWT public key on a JWKS endpoint
The yhub service will need our public key in order to validate the jwt
token we will used. We choose to expose a jwks endpoint as it is a
standard wat to do this.
2026-09-02 09:36:32 +02:00
Manuel Raynaud 4fe86ef19f (backend) add a service generating cached RS256 JWT tokens
We want to generate jwt token using the RS256 algotrithm. This token
will be used for internal call with the yhub service.
2026-09-02 09:36:32 +02:00
Kevin JahnsandManuel Raynaud 8b91ce5d57 📄(collaboration) add license notice for yhub-server directory
Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-02 09:36:31 +02:00
Kevin JahnsandManuel Raynaud ecbff84fd4 ♻️(collaboration) switch collaboration server from hocuspocus to yhub
Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-02 09:36:31 +02:00
Amine BOUKERFAandGitHub 681f9a8c40 🐛(backend) fix duplicating a document that has no conten
Document.content reads from object storage and returns None when nothing
was ever written there. That None, raised "content should be a string.",
so the duplicate endpoint answered a 500. Default to an empty string instead.
    
Signed-off-by: BOUKERFA Mohamed El Amine <boukerfa.ma@gmail.com>
2026-09-02 06:46:37 +00:00
renovate[bot]andGitHub 4633cc7690 ⬆️(dependencies) update djangorestframework to v3.17.2 [SECURITY] 2026-09-02 02:42:52 +00:00
Anthony LC 3646d7dc42 🚚(frontend) add doc move to doc options
In order to harmonize the doc options, we are adding
the "Move to my docs" and "Move into a doc" options
to the doc options menu.
This allows users to move documents directly from
the options menu, providing a more streamlined
experience.
2026-08-28 12:56:49 +02:00
Anthony LC 85177dc3ca (frontend) add find and replace feature to the editor
We have added a new feature to the editor that allows
users to find and replace text within their documents.
This feature enhances the editing experience by
providing a convenient way to search for specific
words or phrases and replace them with new content.
2026-08-28 12:20:06 +02:00
Anthony LC 1c52633f96 ️(frontend) responsive throttling instead of debouncing
We changed the responsive store to use throttling
instead of debouncing for the window resize event.
This change ensures that the store updates more
consistently during rapid window resizing,
providing a smoother user experience.
2026-08-28 11:09:13 +02:00
Anthony LC 672f84d8bc (frontend) add word count to doc header toolbox
We can now see the word count in the doc header toolbox.
2026-08-28 11:09:12 +02:00
Anthony LC e0fcd2a01e ⬆️(frontend) replace ui-kit by ui-components
A major release was made on the ui-kit side,
ui-kit is replaced by ui-components, which is a new
package that contains the same components as ui-kit
but with some improvements.
To build the tokens, a new dev dependency is necessary,
which is @gouvfr-lasuite/ui-tokens.
These replacements bring better separation of concerns
and a better architecture for the future.
We need to adapt our codebase to use the new package
and the new dev dependency.
2026-08-28 11:09:11 +02:00
Anthony LC 944ea131c4 📄(frontend) allowed partially export when MIT
When MIT, we were not able to export anything.
But actually, the export HTML and Print are allowed
for MIT, so we should allow them.
We now allow partially export when MIT, but the
AGPL export is still not allowed (pdf / odt / docx).
2026-08-26 13:17:27 +02:00