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>
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>
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>
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>
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.
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.
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>
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>
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.
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.
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>
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.
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.
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.
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>
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.
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.
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.
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.
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).
We added a new feature that allows users to copy
a link to a specific block within the document.
We can copy the link to the block by clicking on
the "Copy link to block" button in the block's menu
in the document editor.
When the link is pasted in the browser, it will
automatically scroll to the block.
We want to have our own side menu, so we can add more
features to it and display the ui as we want.
The new side menu is called DocsSideMenu
and it will be used in the BlockNoteEditor.
Depend the size of the screen and how the elements
are displayed, we want to have different
shadows and transparency for the floating bar
and the left panel.
The new comment composer cllipped its own formatting toolbar.
That made styling text impossible so we let the toolbar extend past the box.
Signed-off-by: BOUKERFA Mohamed El Amine <boukerfa.ma@gmail.com>
Reset the comment filter to open whenever the side panel
closes, and close the active panel when navigating to
another document. This prevents panel state from
leaking across sessions and documents.
Signed-off-by: fch-aa <21101725+fch-aa@users.noreply.github.com>
Added:
- ♿️(frontend) restore skip to content link after header redesign
- 🌐(i18n) rename cn_CN to zh_CN, add eo_PL and zh_TW locales
- ✨(backend) conditional email notification in server to server api
- ✨(backend) profile api using django-silk
Changed:
- ♿️(frontend) use semantic `<dl>` structure in document info card
- ⚡️(frontend) replace onboarding assets with webm and webp
- 💄(frontend) use the same highlight color for cells and moves
- ⚡️(backend) optimize media_auth endpoint
- 🚸(frontend) print from document options menu
Fixed:
- 🐛(frontend) refresh pins after document deletion and restoration
- 🐛(frontend) redirect homepage to login when homepage feat
is disabled
- 🐛(backend) ignore CSPs for API docs in development
- 🐛(frontend) export images embedded with a relative url
- 🐛(y-provider) fix sentry init
- 🐛(backend) handle object storage metadata keys case-insensitively
- 🐛(keycloak) fix database env variables in the self-hosting example
- 🐛(helm) show the database error while jobs wait for it to be ready
The imported docs were not displayed anymore in the
list grid view directly after the import.
It was because the request was not matched with the
correct query key.
STEP was in uppercase in the assets names, on the
preprod the assets were not found because the
server is case sensitive.
We renamed the assets to lowercase to fix the issue,
as the previous assets.
A new language has been added to the Django system,
allowing for Polish translations and localization
support.
We need to initialize the Polish language files before
being able to download the translations from
Crowdin. This commit includes the initial setup for
the Polish language, including the necessary configuration
files and directory structure.
Lot of elements did not have a focus style, or the
focus style was not consistent. We now have a global focus
style that is applied to all elements that are
focusable. The style is still overridable for
specific elements, so buttons and other elements
still have their own focus style.
We will have probably to improve the focus style
in the future of some elements, but this is a
good start.
We updated the UI of the doc toolbox to fit
with the design system. The icons have been updated,
we moved it to the doc management folder in order
to use it to different places in the app.