Skip to content

[WIP] switch to yhub as a collab backend - #2556

Open
dmonad wants to merge 23 commits into
mainfrom
yhub
Open

[WIP] switch to yhub as a collab backend#2556
dmonad wants to merge 23 commits into
mainfrom
yhub

Conversation

@dmonad

@dmonad dmonad commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Purpose

Switch to yhub as the collaborative backend.

Note that a large chunk of this code was AI generated. I open this PR to work more closely together with others.

Remaining issues

(incomplete list)

  • force reconnect on authorization change
  • testing
  • make yhub the source of truth
  • http-provider
  • ...

General requirements

  • I have read and followed the contributing guidelines
  • I have read and agreed to the Code of Conduct
  • I have added corresponding tests for new features or bug fixes (if applicable)
  • Before submitting a PR for a new feature I made sure to contact the product manager

CI requirements

  • I made sure that all existing tests are passing
  • I have signed off my commits with git commit --signoff (DCO compliance)
  • I have signed my commits with my SSH or GPG key (git commit -S)
  • My commit messages follow the required format: <gitmoji>(type) title description
  • I have added a changelog entry under ## [Unreleased] section (if noticeable change)

AI requirements

  • I used AI assistance to produce part or all of this contribution
  • I have read, reviewed, understood and can explain the code I am submitting
  • I can jump in a call or a chat to explain my work to a maintainer

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
@gitguardian

gitguardian Bot commented Aug 3, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
33900402 Triggered Generic Password 3bc372c compose.yml View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Revoke and rotate the secret.

  2. If possible, rewrite git history with git commit --amend and git push --force.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Size Change: -3.67 kB (-0.08%)

Total Size: 4.36 MB

📦 View Changed
Filename Size Change
apps/impress/out/_next/static/9b55b32b/_buildManifest.js 0 B -677 B (removed) 🏆
apps/impress/out/_next/static/62495a80/_buildManifest.js 672 B +672 B (new file) 🆕
apps/impress/out/_next/static/chunks/879.js 648 kB -971 B (-0.15%)
apps/impress/out/_next/static/chunks/1222.js 70.9 kB +70.9 kB (new file) 🆕
apps/impress/out/_next/static/chunks/6511.js 0 B -73.6 kB (removed) 🏆

compressed-size-action

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The collaboration service moves from Hocuspocus to YHub backed by Valkey and PostgreSQL. The former Y provider becomes converter-only. Backend collaboration APIs and websocket-specific document fields are removed. Frontend collaboration uses y-websocket, updated provider APIs, and new WebSocket routes. The backend adds RS256 JWT generation and a JWKS endpoint.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: antolc

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title identifies the main change (switching to yhub as collaborative backend) but is marked [WIP], which indicates incomplete work unsuitable for production merge. Remove the [WIP] prefix or clarify readiness status. Use a final title such as 'Switch to yhub as collaborative backend' when ready for merge.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description is directly related to the changeset and clearly states the purpose of switching to yhub with documented remaining issues and AI assistance disclosure.
Docstring Coverage ✅ Passed Docstring coverage is 95.56% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch yhub
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yhub

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@env.d/production.dist/common`:
- Around line 9-10: Complete the production collaboration route: uncomment and
set COLLABORATION_WS_URL in env.d/production.dist/common at lines 9-10 to the
YHub /ws/docs endpoint; update src/helm/impress/values.yaml lines 53-79 and
templates/ingress_collaboration_ws.yaml so the collaboration ingress targets
YHub, or document the external override explicitly; update
documentation/system-requirements.md line 92 to describe the external port-3002
route and relevant YHub dependencies or sizing.

In `@src/backend/core/management/commands/clean_document.py`:
- Around line 3-10: Update clean_document to remove YHub PostgreSQL rows and
Redis room keys for every document in all_documents, including descendants,
before reporting success. Ensure failures or unavailable atomic cross-store
cleanup cause the command to fail or explicitly mark cleanup incomplete rather
than printing “Done.”

In `@src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts`:
- Around line 42-45: Restore authorization enforcement for active collaboration
sessions by implementing connection invalidation or per-update authorization
checks in the YHub collaboration flow; update
src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts:42-45 to
remove the temporary TODO once enforcement works, and re-enable or replace the
skipped close/reconnect test at
src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts:114-131
with coverage for the new behavior.

In
`@src/frontend/apps/impress/src/features/docs/doc-management/stores/useProviderStore.tsx`:
- Around line 110-123: Update destroyProvider and the useCollaboration cleanup
so the BlockNote collaboration extension is unbound from provider.doc before
provider.doc.destroy() runs. Ensure document destruction occurs from the
useCollaboration unmount cleanup after unbinding, while preserving provider
awareness and connection cleanup behavior.

In `@src/yhub-server/Dockerfile`:
- Around line 1-13: Update the yhub Dockerfile to create and select a dedicated
non-root user before CMD, ensuring the application runs without root privileges.
Add a HEALTHCHECK before CMD that probes the exposed port or service endpoint
and reports failure when the yhub process is unresponsive.

In `@src/yhub-server/server.js`:
- Around line 12-20: Validate REDIS and POSTGRES immediately after their
configuration constants are initialized, and fail startup with a clear error
identifying whichever required variable is missing before calling createYHub.
Preserve the existing defaults and startup flow when both values are present.
- Around line 42-64: Update readAuthInfo so only an explicit authentication
failure response, such as HTTP 401 from backendFetch, returns the cookie-derived
anon identity. Preserve propagation or distinct handling for network errors, 5xx
responses, and other non-authentication failures so they cannot silently become
anonymous users.
- Line 21: Update the Y_PROVIDER_API_KEY initialization to require an explicitly
configured secret rather than passing the hardcoded fallback to secret. Ensure
startup fails loudly when neither Y_PROVIDER_API_KEY nor Y_PROVIDER_API_KEY_FILE
is set, while preserving the X-Y-Provider-Key authentication flow.
- Around line 26-38: Update backendFetch to enforce a finite request timeout by
creating an AbortController, scheduling its abort, and passing its signal to
fetch; clear the timeout after fetch completes while preserving the existing
response validation and JSON return behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8dfb28b1-7431-4a43-9667-9866e60dc88e

📥 Commits

Reviewing files that changed from the base of the PR and between edf26d6 and 3bc372c.

⛔ Files ignored due to path filters (2)
  • src/frontend/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
  • src/yhub-server/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (58)
  • .gitignore
  • CHANGELOG.md
  • Makefile
  • compose-e2e.yml
  • compose.yml
  • docker/files/yhub/initdb/01-yhub.sql
  • documentation/system-requirements.md
  • env.d/development/common
  • env.d/development/common.e2e
  • env.d/production.dist/common
  • src/backend/core/management/commands/clean_document.py
  • src/backend/core/services/collaboration_services.py
  • src/backend/core/tests/documents/test_api_documents_can_edit.py
  • src/backend/core/tests/documents/test_api_documents_content_update.py
  • src/backend/core/tests/documents/test_api_documents_update.py
  • src/backend/core/tests/external_api/test_external_api_documents_accesses.py
  • src/backend/core/tests/external_api/test_external_api_documents_link_configuration.py
  • src/backend/core/tests/test_services_collaboration_services.py
  • src/backend/core/tests/test_tasks_access.py
  • src/backend/impress/settings.py
  • src/frontend/apps/e2e/.env
  • src/frontend/apps/e2e/.env.example
  • src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts
  • src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts
  • src/frontend/apps/impress/package.json
  • src/frontend/apps/impress/src/core/config/hooks/useCollaborationUrl.tsx
  • src/frontend/apps/impress/src/features/docs/doc-comments/hooks/useComments.ts
  • src/frontend/apps/impress/src/features/docs/doc-editor/__tests__/DocEditor.spec.tsx
  • src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx
  • src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx
  • src/frontend/apps/impress/src/features/docs/doc-editor/hook/useCollaboration.tsx
  • src/frontend/apps/impress/src/features/docs/doc-editor/hook/useSaveDoc.tsx
  • src/frontend/apps/impress/src/features/docs/doc-management/api/useDuplicateDoc.tsx
  • src/frontend/apps/impress/src/features/docs/doc-management/api/useUpdateDoc.tsx
  • src/frontend/apps/impress/src/features/docs/doc-management/stores/useProviderStore.tsx
  • src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalConfirmationVersion.tsx
  • src/frontend/apps/impress/src/features/right-panel/components/RightPanel.tsx
  • src/frontend/apps/impress/src/stores/useBroadcastStore.tsx
  • src/frontend/package.json
  • src/frontend/servers/y-provider/__tests__/collaborationBackend.test.ts
  • src/frontend/servers/y-provider/__tests__/collaborationResetConnections.test.ts
  • src/frontend/servers/y-provider/__tests__/getDocumentConnectionInfoHandler.test.ts
  • src/frontend/servers/y-provider/__tests__/hocuspocusWS.test.ts
  • src/frontend/servers/y-provider/package.json
  • src/frontend/servers/y-provider/src/api/collaborationBackend.ts
  • src/frontend/servers/y-provider/src/env.ts
  • src/frontend/servers/y-provider/src/handlers/collaborationResetConnectionsHandler.ts
  • src/frontend/servers/y-provider/src/handlers/collaborationWSHandler.ts
  • src/frontend/servers/y-provider/src/handlers/getDocumentConnectionInfoHandler.ts
  • src/frontend/servers/y-provider/src/handlers/index.ts
  • src/frontend/servers/y-provider/src/middlewares.ts
  • src/frontend/servers/y-provider/src/routes.ts
  • src/frontend/servers/y-provider/src/servers/appServer.ts
  • src/frontend/servers/y-provider/src/servers/hocuspocusServer.ts
  • src/frontend/servers/y-provider/src/servers/index.ts
  • src/yhub-server/Dockerfile
  • src/yhub-server/package.json
  • src/yhub-server/server.js
💤 Files with no reviewable changes (17)
  • env.d/development/common.e2e
  • src/frontend/servers/y-provider/src/servers/index.ts
  • src/frontend/servers/y-provider/src/handlers/collaborationWSHandler.ts
  • src/frontend/servers/y-provider/src/handlers/index.ts
  • src/frontend/servers/y-provider/src/routes.ts
  • src/backend/core/tests/external_api/test_external_api_documents_link_configuration.py
  • src/frontend/servers/y-provider/src/handlers/collaborationResetConnectionsHandler.ts
  • src/frontend/servers/y-provider/tests/collaborationBackend.test.ts
  • src/frontend/servers/y-provider/tests/collaborationResetConnections.test.ts
  • src/frontend/servers/y-provider/src/middlewares.ts
  • src/frontend/servers/y-provider/tests/hocuspocusWS.test.ts
  • src/frontend/servers/y-provider/src/env.ts
  • src/frontend/servers/y-provider/src/api/collaborationBackend.ts
  • src/frontend/servers/y-provider/src/servers/hocuspocusServer.ts
  • src/backend/core/tests/test_tasks_access.py
  • src/frontend/servers/y-provider/src/handlers/getDocumentConnectionInfoHandler.ts
  • src/frontend/servers/y-provider/tests/getDocumentConnectionInfoHandler.test.ts

Comment on lines +9 to 10
# TODO(yhub): route is /ws/docs once prod ingress is migrated
#COLLABORATION_WS_URL=wss://${DOCS_HOST}/collaboration/ws/ No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate production deployment configuration and confirm that YHub is routed
# to /ws/docs and exposed to the frontend configuration.
rg -n -C 3 \
  'COLLABORATION_WS_URL|YPROVIDER_HOST|yhub|/ws/docs|/collaboration/ws' \
  .

Repository: suitenumerique/docs

Length of output: 49692


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Helm collaboration ingress values"
sed -n '50,75p' src/helm/impress/values.yaml

echo
echo "## Helm charts mentioning yProvider / yhub / collaboration"
rg -n "yProvider|y-provider|yhub|yhub-service|ingressCollaborationWS|COLLABORATION_WS_URL|yHub|Y_HUB|YHUB" src/helm docs documentation CHANGELOG.md env.d src/backend src/frontend -g '*.yaml' -g '*.yml' -g '*.md' -g '*.py' -g '*.tsx' -g 'Dockerfile*' | head -200

echo
echo "## Frontend fallback logic context"
sed -n '1,35p' src/frontend/apps/impress/src/core/config/hooks/useCollaborationUrl.tsx

echo
echo "## Backend API config exposure"
sed -n '3060,3105p' src/backend/core/api/viewsets.py

Repository: suitenumerique/docs

Length of output: 28957


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Helm ingressCollaborationWS templates"
fd -a 'ingress.*collaboration.*|.*collaboration.*ingress.*' src/helm/impress/templates -x sh -c 'echo "--- $1"; sed -n "1,130p" "$1"' sh {} \;

echo
echo "## Helm chart yhub service/service references"
rg -n "yhub|yHub|3002|ws/docs|COLLABORATION_WS_URL|yProvider" src/helm src/helm/impress/templates src/helm/impress/templates/* -g '*.yaml' -g '*.yml' -g '*.tpl' -g '*.go'

echo
echo "## Deterministic config-fallback behavior probe"
node - <<'JS'
function frontendCollaborationUrl(config, windowHost) {
  let backend = null; // backend config not modeled here; frontend gets COLLABORATION_WS_URL
  if (backend && backend.COLLABORATION_WS_URL) return backend.COLLABORATION_WS_URL;
  if (windowHost) return `wss://${windowHost}/ws/docs`;
  return '';
}
const cases = [
  [{COLLABORATION_WS_URL: undefined}, 'prod.example.com'],
  [{COLLABORATION_WS_URL: ''}, 'prod.example.com'],
  [{COLLABORATION_WS_URL: 'wss://prod.example.com/ws/docs'}, 'prod.example.com'],
];
for (const [config, host] of cases) {
  console.log(JSON.stringify({config, host, result: frontendCollaborationUrl(config, host)}));
}
JS

Repository: suitenumerique/docs

Length of output: 49302


Complete the production YHub route before release.

Default Helm values disable ingressCollaborationWS, but this chart still creates a /collaboration/ws/ Ingress that forwards to y-provider instead of YHub. The production env template also leaves COLLABORATION_WS_URL commented, so frontend clients can fall back to wss://${host}/ws/docs. If production is not using this chart or an equivalent custom route, production collaboration will fail unless COLLABORATION_WS_URL is provided externally.

  • env.d/production.dist/common#L9-L10: add the production COLLABORATION_WS_URL=wss://${DOCS_HOST}/ws/docs after the ingress route is ready.
  • src/helm/impress/values.yaml#L53-L79 / src/helm/impress/templates/ingress_collaboration_ws.yaml: update the collaboration WS ingress/backend to YHub, or explicitly document when/where the route is overridden.
  • documentation/system-requirements.md#L92-L92: update the row for port 3002 to mention the external route and any YHub sizing/dependency guidance if it belongs here.
📍 Affects 2 files
  • env.d/production.dist/common#L9-L10 (this comment)
  • documentation/system-requirements.md#L92-L92
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@env.d/production.dist/common` around lines 9 - 10, Complete the production
collaboration route: uncomment and set COLLABORATION_WS_URL in
env.d/production.dist/common at lines 9-10 to the YHub /ws/docs endpoint; update
src/helm/impress/values.yaml lines 53-79 and
templates/ingress_collaboration_ws.yaml so the collaboration ingress targets
YHub, or document the external override explicitly; update
documentation/system-requirements.md line 92 to describe the external port-3002
route and relevant YHub dependencies or sizing.

Comment on lines +3 to +10
# TODO(yhub): this sandbox reset no longer erases the document content. It purges
# the S3 versions, but yhub durably retains the Yjs document in its own Postgres
# and re-serves it on the next websocket connect (CRDT merge with the empty
# seed resurrects the purged content). yhub has no delete API; until it grows
# one, the interim remediation is to run, against yhub's stores:
# DELETE FROM yhub_ydoc_v1 WHERE org='docs' AND docid='<document_id>';
# and drop the `yhub:room:docs:<document_id>:*` redis keys.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make clean_document remove YHub state for every cleaned document.

The command deletes S3 content but leaves the root YHub document available for CRDT resurrection. It also deletes descendant Django records without deleting their YHub rows.

Before this command reports success, delete the YHub PostgreSQL rows and Redis room keys for all_documents. If atomic cross-store cleanup is not possible, fail the command or mark the cleanup incomplete instead of reporting Done.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/core/management/commands/clean_document.py` around lines 3 - 10,
Update clean_document to remove YHub PostgreSQL rows and Redis room keys for
every document in all_documents, including descendants, before reporting
success. Ensure failures or unavailable atomic cross-store cleanup cause the
command to fail or explicitly mark cleanup incomplete rather than printing
“Done.”

Comment on lines +42 to +45
// TODO(yhub): re-add the close/reconnect check (the backend closed the
// connection when the doc visibility changed) once yhub exposes a kick
// API - `reset_connections` is currently a no-op so the server never
// closes the connection.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Restore authorization enforcement for active collaboration sessions.

YHub currently cannot invalidate an established connection after document authorization changes. The migration also skips the test that would detect this failure. Resolve both before release.

  • src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts#L42-L45: implement connection invalidation or per-update authorization enforcement when access changes.
  • src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts#L114-L131: re-enable this test, or replace it with an integration test for the new enforcement mechanism.
📍 Affects 1 file
  • src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts#L42-L45 (this comment)
  • src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts#L114-L131
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts` around
lines 42 - 45, Restore authorization enforcement for active collaboration
sessions by implementing connection invalidation or per-update authorization
checks in the YHub collaboration flow; update
src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts:42-45 to
remove the temporary TODO once enforcement works, and re-enable or replace the
skipped close/reconnect test at
src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts:114-131
with coverage for the new behavior.

Comment on lines 110 to +123
destroyProvider: () => {
clearTimeout(reconnectTimeout);
clearTimeout(lostConnectionTimeout);
const provider = get().provider;
if (provider) {
/**
* destroy() emits 'connection-close' synchronously before removing
* listeners, which re-arms lostConnectionTimeout: it must be cleared
* after, or a stale "connection lost" banner flashes on the next doc.
*/
provider.destroy();
// y-websocket never destroys the awareness: its interval would leak
provider.awareness.destroy();
provider.doc.destroy();
}
clearTimeout(lostConnectionTimeout);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how useCollaboration and DocCoreEditor/BlockNoteEditor are composed in DocEditor.tsx
fd -a DocEditor.tsx src/frontend/apps/impress/src/features/docs/doc-editor
rg -n -B3 -A20 'useCollaboration\(' src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx

Repository: suitenumerique/docs

Length of output: 1104


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== DocEditor outline and relevant contents =="
ast-grep outline src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx || true
wc -l src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx
sed -n '1,260p' src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx

echo
echo "== Collaboration hook =="
fd -a 'useCollaboration.tsx|useCollaboration.ts' src/frontend/apps/impress/src/features/docs/doc-management src/frontend/apps/impress/src/features/docs/doc-editor 2>/dev/null || true
rg -n -B5 -A80 'export function useCollaboration|export const useCollaboration|const useCollaboration|function useCollaboration|destroyProvider|useProviderStore' src/frontend/apps/impress/src/features/docs/doc-management src/frontend/apps/impress/src/features/docs/doc-editor 2>/dev/null || true

echo
echo "== DocCoreEditor and BlockNoteEditor composition details =="
fd -a 'BlockNoteEditor.tsx|DocCoreEditor.tsx|Editor.*\.tsx' src/frontend/apps/impress/src/features/docs 2>/dev/null || true
rg -n -B5 -A40 'DocCoreEditor|BlockNoteEditor|useSaveDoc\(|provider\.doc\.getXmlFragment|useProviderStore|getDocProvider|provider\.doc' src/frontend/apps/impress/src/features/docs/doc-editor src/frontend/apps/impress/src/features/docs/doc-management/store src/frontend/apps/impress/src/features/docs/doc-management/stores src/frontend/apps/impress/src/features/docs 2>/dev/null || true

Repository: suitenumerique/docs

Length of output: 50376


Destroy provider.doc only after unbinding the editor.

DocEditor renders DocCoreEditor after calling useCollaboration(doc.id), and DocEditor has no explicit cleanup before DocCoreEditor unmounts. React unmounts children before parent effects run, so the current useCollaboration cleanup does not guarantee this. Unbind the BlockNote collaboration extension from provider.doc first, then destroy it when useCollaboration unmounts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/apps/impress/src/features/docs/doc-management/stores/useProviderStore.tsx`
around lines 110 - 123, Update destroyProvider and the useCollaboration cleanup
so the BlockNote collaboration extension is unbound from provider.doc before
provider.doc.destroy() runs. Ensure document destruction occurs from the
useCollaboration unmount cleanup after unbinding, while preserving provider
awareness and connection cleanup behavior.

Comment on lines +1 to +13
# trixie for glibc >= 2.38 — uws prebuilt binaries reject bookworm's 2.36
FROM node:22-trixie AS yhub

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --omit=dev

COPY server.js ./

EXPOSE 3002

CMD ["node", "server.js"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add a non-root user and a HEALTHCHECK.

The image runs as root because no USER directive is set. Static analysis flags this (CKV_DOCKER_3). Add a dedicated non-root user before the CMD instruction to reduce the container's privilege footprint. Static analysis also flags a missing HEALTHCHECK (CKV_DOCKER_2). Add one so the orchestrator can detect an unresponsive yhub process.

🔒️ Proposed fix
 COPY server.js ./
 
+RUN groupadd --gid 1000 yhub && useradd --uid 1000 --gid yhub --shell /bin/false yhub
+USER yhub
+
 EXPOSE 3002
 
+HEALTHCHECK --interval=30s --timeout=3s CMD node -e "fetch('http://localhost:3002/').then(()=>process.exit(0)).catch(()=>process.exit(1))"
+
 CMD ["node", "server.js"]
🧰 Tools
🪛 Checkov (3.3.8)

[low] 1-13: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)


[low] 1-13: Ensure that a user for the container has been created

(CKV_DOCKER_3)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/yhub-server/Dockerfile` around lines 1 - 13, Update the yhub Dockerfile
to create and select a dedicated non-root user before CMD, ensuring the
application runs without root privileges. Add a HEALTHCHECK before CMD that
probes the exposed port or service endpoint and reports failure when the yhub
process is unresponsive.

Source: Linters/SAST tools

Comment thread src/yhub-server/server.js
Comment on lines +12 to +20
const PORT = Number(process.env.PORT || 3002);
const REDIS = process.env.REDIS;
const POSTGRES = process.env.POSTGRES;
const REDIS_PREFIX = process.env.REDIS_PREFIX || 'yhub';
const COLLABORATION_BACKEND_BASE_URL =
process.env.COLLABORATION_BACKEND_BASE_URL || 'http://app-dev:8000';
const allowedOrigins = (
process.env.COLLABORATION_SERVER_ORIGIN || 'http://localhost:3000'
).split(',');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Validate required configuration at startup.

REDIS and POSTGRES have no default and no explicit check before being passed into createYHub (lines 84-98). If either is unset, the failure surfaces deep inside @y/hub rather than as a clear startup error naming the missing variable.

Add an explicit check that fails fast with a clear message when REDIS or POSTGRES is missing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/yhub-server/server.js` around lines 12 - 20, Validate REDIS and POSTGRES
immediately after their configuration constants are initialized, and fail
startup with a clear error identifying whichever required variable is missing
before calling createYHub. Preserve the existing defaults and startup flow when
both values are present.

Comment thread src/yhub-server/server.js
Comment thread src/yhub-server/server.js
Comment thread src/yhub-server/server.js
dmonad and others added 6 commits August 4, 2026 16:54
Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
We want to generate jwt token using the RS256 algotrithm. This token
will be used for internal call with the yhub service.
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.
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.
Thw private key needed to generate a jwt token will be mandatory. In
order to ease the development we want to automate its generation
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bin/generate-jwt-private-key.sh`:
- Around line 11-22: Set a restrictive umask before the mkdir and openssl
commands in the key-generation flow, ensuring the directory and private key are
created inaccessible to other users; retain the existing chmod 600 safeguard and
idempotent KEY_PATH check.

In `@src/backend/core/services/jwt_services.py`:
- Around line 84-86: Update the lifetime method to validate that
settings.JWT_TOKEN_LIFETIME is strictly greater than zero before returning it,
and reject zero or negative values so generate_token cannot create tokens with
an invalid exp relative to iat.

In `@src/backend/core/tests/test_api_jwks.py`:
- Around line 106-117: Update the JWKS rotation implementation exercised by
test_api_jwks_follows_the_key_rotation to retain and publish the retired public
key alongside the current key for at least JWT_TOKEN_LIFETIME. Extend the test
to create and preserve a token before changing JWT_PRIVATE_KEY, then verify that
token with the post-rotation JWKS while retaining the existing new-token
validation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 29e129f4-04fd-4bfa-a317-f0f3c99106e1

📥 Commits

Reviewing files that changed from the base of the PR and between c6a0695 and 1b49a4f.

⛔ Files ignored due to path filters (1)
  • src/backend/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • .github/workflows/impress.yml
  • CHANGELOG.md
  • Makefile
  • UPGRADE.md
  • bin/_config.sh
  • bin/generate-jwt-private-key.sh
  • compose.yml
  • documentation/env.md
  • documentation/resource_server.md
  • env.d/development/common
  • src/backend/core/api/viewsets.py
  • src/backend/core/services/jwt_services.py
  • src/backend/core/tests/test_api_jwks.py
  • src/backend/core/tests/test_services_jwt_services.py
  • src/backend/core/urls.py
  • src/backend/impress/settings.py
  • src/backend/pyproject.toml
💤 Files with no reviewable changes (1)
  • .github/workflows/impress.yml

Comment on lines +11 to +22
set -eo pipefail

REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
KEY_PATH="${REPO_DIR}/data/jwt/private.pem"

if [ -f "${KEY_PATH}" ]; then
exit 0
fi

mkdir -p "$(dirname "${KEY_PATH}")"
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${KEY_PATH}" 2>/dev/null
chmod 600 "${KEY_PATH}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Sensitive Data Exposure (CWE-732): Incorrect Permission Assignment for Critical Resource

Reachability: Internal

Create the private key with restrictive permissions.

If a shared development host uses a permissive umask and another user can traverse data/jwt, line 21 creates the private key before line 22 restricts it. A local attacker can read the key during that interval and mint JWTs accepted by yhub. Set umask 077 before creating the directory and key.

Proposed fix
 set -eo pipefail
+umask 077
 
 REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
#!/usr/bin/env bash
set -euo pipefail

tmp_dir="$(mktemp -d)"
trap 'rm -rf "${tmp_dir}"' EXIT

(
  umask 0022
  openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
    -out "${tmp_dir}/private.pem" 2>/dev/null
  stat -c 'created mode: %a' "${tmp_dir}/private.pem"
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/generate-jwt-private-key.sh` around lines 11 - 22, Set a restrictive
umask before the mkdir and openssl commands in the key-generation flow, ensuring
the directory and private key are created inaccessible to other users; retain
the existing chmod 600 safeguard and idempotent KEY_PATH check.

Comment on lines +84 to +86
def lifetime(self):
"""Return the token lifetime, in seconds."""
return settings.JWT_TOKEN_LIFETIME

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject non-positive JWT lifetimes.

Line 86 returns zero or negative values without validation. generate_token then creates a token whose exp is at or before iat. yhub and external validators reject that token. Validate lifetime > 0 before use.

Proposed fix
     def lifetime(self):
         """Return the token lifetime, in seconds."""
-        return settings.JWT_TOKEN_LIFETIME
+        lifetime = settings.JWT_TOKEN_LIFETIME
+        if lifetime <= 0:
+            raise ConfigurationError("JWT_TOKEN_LIFETIME must be greater than zero.")
+        return lifetime
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/core/services/jwt_services.py` around lines 84 - 86, Update the
lifetime method to validate that settings.JWT_TOKEN_LIFETIME is strictly greater
than zero before returning it, and reject zero or negative values so
generate_token cannot create tokens with an invalid exp relative to iat.

Comment on lines +106 to +117
def test_api_jwks_follows_the_key_rotation(jwt_settings):
"""After a rotation, the JWKS validates the tokens signed with the new key."""
first_jwks = APIClient().get("/api/v1.0/jwks").json()

jwt_settings.JWT_PRIVATE_KEY = OTHER_PRIVATE_KEY
token = JWTService().get_token({"sub": "user-id"})
second_jwks = APIClient().get("/api/v1.0/jwks").json()

assert first_jwks != second_jwks

key = jwt.PyJWKSet.from_dict(second_jwks).keys[0]
assert jwt.decode(token, key, algorithms=["RS256"])["sub"] == "user-id"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Keep the retired public key available during key rotation.

The JWKS contains one key and replaces it immediately after rotation. Tokens signed before rotation remain valid for JWT_TOKEN_LIFETIME, but a consumer that refreshes this JWKS can no longer verify them.

Publish the current and retired public keys during an overlap period of at least the maximum token lifetime. Add a test that verifies a pre-rotation token with the post-rotation JWKS.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/core/tests/test_api_jwks.py` around lines 106 - 117, Update the
JWKS rotation implementation exercised by test_api_jwks_follows_the_key_rotation
to retain and publish the retired public key alongside the current key for at
least JWT_TOKEN_LIFETIME. Extend the test to create and preserve a token before
changing JWT_PRIVATE_KEY, then verify that token with the post-rotation JWKS
while retaining the existing new-token validation.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@documentation/collaboration.md`:
- Around line 3-8: Update the collaboration paragraph to refer to the “yhub
service” instead of the “y-provider service,” while keeping the existing
COLLABORATION_BACKEND_BASE_URL and COLLABORATION_SERVER_ORIGIN configuration
details unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a0db8c39-75ab-47a1-9c93-f4897fd1486b

📥 Commits

Reviewing files that changed from the base of the PR and between 1b49a4f and 9030c3f.

📒 Files selected for processing (26)
  • CHANGELOG.md
  • documentation/collaboration.md
  • documentation/env.md
  • documentation/examples/helm/impress.values.yaml
  • env.d/development/common
  • env.d/production.dist/yprovider
  • src/backend/core/api/serializers.py
  • src/backend/core/api/viewsets.py
  • src/backend/core/models.py
  • src/backend/core/services/collaboration_services.py
  • src/backend/core/tasks/access.py
  • src/backend/core/tests/documents/test_api_document_accesses.py
  • src/backend/core/tests/documents/test_api_documents_can_edit.py
  • src/backend/core/tests/documents/test_api_documents_content_update.py
  • src/backend/core/tests/documents/test_api_documents_link_configuration.py
  • src/backend/core/tests/documents/test_api_documents_retrieve.py
  • src/backend/core/tests/documents/test_api_documents_trashbin.py
  • src/backend/core/tests/documents/test_api_documents_update.py
  • src/backend/core/tests/external_api/test_external_api_documents_link_configuration.py
  • src/backend/core/tests/test_api_config.py
  • src/backend/core/tests/test_models_documents.py
  • src/backend/core/tests/test_services_collaboration_services.py
  • src/backend/core/tests/test_tasks_access.py
  • src/backend/impress/settings.py
  • src/helm/env.d/dev/values.impress.yaml.gotmpl
  • src/helm/env.d/feature/values.impress.yaml.gotmpl
💤 Files with no reviewable changes (18)
  • src/helm/env.d/feature/values.impress.yaml.gotmpl
  • env.d/production.dist/yprovider
  • src/backend/core/tests/documents/test_api_documents_retrieve.py
  • src/backend/core/models.py
  • documentation/examples/helm/impress.values.yaml
  • src/backend/core/tests/documents/test_api_documents_trashbin.py
  • env.d/development/common
  • src/backend/core/tasks/access.py
  • documentation/env.md
  • src/backend/core/tests/test_tasks_access.py
  • src/backend/core/api/serializers.py
  • src/backend/core/tests/documents/test_api_documents_can_edit.py
  • src/backend/impress/settings.py
  • src/helm/env.d/dev/values.impress.yaml.gotmpl
  • src/backend/core/tests/test_models_documents.py
  • src/backend/core/services/collaboration_services.py
  • src/backend/core/tests/test_api_config.py
  • src/backend/core/tests/test_services_collaboration_services.py

Comment on lines +3 to 8
By default with Docs, collaboration is enabled. To allow the collaboration between users, a connection to a websocket server is made (the y-provider service), you only have to configure the Django backend URL and the allowed origin in your y-provider service:

```yaml
COLLABORATION_BACKEND_BASE_URL: https://{yourdocsdomain.tld}
COLLABORATION_SERVER_ORIGIN: https://{yourdocsdomain.tld}
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Collect the collaboration/yhub configuration variable names declared in the repository.
set -euo pipefail

fd -t f 'server.js|README.md' src/yhub-server --exec rg -n 'process\.env\.[A-Z_]+|COLLABORATION_[A-Z_]+' {}

fd -t f -g 'compose*.yml' . --exec rg -n 'yhub|COLLABORATION_[A-Z_]+' {}

rg -n 'COLLABORATION_[A-Z_]+' documentation env.d

Repository: suitenumerique/docs

Length of output: 1499


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant docs =="
sed -n '1,40p' documentation/collaboration.md
echo
echo "== changelog references =="
rg -n -C 3 'y-provider|yhub|COLLABORATION_SERVER_ORIGIN|COLLABORATION_BACKEND_BASE_URL|/collaboration/ws/' . -g '!node_modules' -g '!dist' -g '!build' | head -n 200

echo
echo "== documentation exact context =="
sed -n '1,12p' documentation/collaboration.md

Repository: suitenumerique/docs

Length of output: 12979


Update the collaboration docs to yhub.

Replace “y-provider service” with “yhub service” in this paragraph, because yhub-server/server.js reads these variables: COLLABORATION_BACKEND_BASE_URL and COLLABORATION_SERVER_ORIGIN (secret-file variants are supported for env var names such as Y_PROVIDER_API_KEY, but COLLABORATION_* can be set directly).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@documentation/collaboration.md` around lines 3 - 8, Update the collaboration
paragraph to refer to the “yhub service” instead of the “y-provider service,”
while keeping the existing COLLABORATION_BACKEND_BASE_URL and
COLLABORATION_SERVER_ORIGIN configuration details unchanged.

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.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CHANGELOG.md (1)

43-46: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Block rollout until Helm routes collaboration to YHub.

The converter-only lasuite/impress-y-provider image removes the collaboration WebSocket. Existing Helm values therefore lose collaboration unless the chart routes /collaboration/ to YHub. Update the chart and values before rollout, or add a deployment guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 43 - 46, Block rollout of the converter-only
y-provider image until the Helm chart routes /collaboration/ to YHub, updating
the chart and values accordingly; alternatively, add a deployment guard that
prevents configurations still routing collaboration to the y-provider image.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/apps/impress/src/core/config/hooks/useCollaborationUrl.tsx`:
- Around line 12-16: Verify the runtime configuration wiring for
COLLABORATION_WS_URL in production and ensure it is always exposed to the
client; if it is not, add the required production ingress route for the fallback
used by the collaboration URL hook before release.

In `@src/yhub-server/README.md`:
- Around line 14-18: Use the exact collaboration route parameter name `{docid}`
consistently in the documentation. Update CHANGELOG.md lines 11-14 and 17-21 to
replace `{id}` with `{docid}` for the reset-connections and websocket routes;
src/yhub-server/README.md lines 14-18 requires no direct change and documents
the correct contract.

---

Outside diff comments:
In `@CHANGELOG.md`:
- Around line 43-46: Block rollout of the converter-only y-provider image until
the Helm chart routes /collaboration/ to YHub, updating the chart and values
accordingly; alternatively, add a deployment guard that prevents configurations
still routing collaboration to the y-provider image.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 00971b27-32ee-4038-811a-17ac39a1504e

📥 Commits

Reviewing files that changed from the base of the PR and between 9030c3f and 0414067.

⛔ Files ignored due to path filters (1)
  • src/yhub-server/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • CHANGELOG.md
  • env.d/development/common
  • src/frontend/apps/impress/src/core/config/hooks/useCollaborationUrl.tsx
  • src/yhub-server/README.md
  • src/yhub-server/package.json
  • src/yhub-server/server.js

Comment on lines 12 to +16
conf?.COLLABORATION_WS_URL ||
(typeof window !== 'undefined'
? `wss://${window.location.host}/collaboration/ws/`
: '');

return `${base}?room=${room}`;
? // TODO(yhub): no prod ingress route yet
`wss://${window.location.host}/collaboration/ws/v1/docs`
: '')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial

Verify the production fallback path.

The fallback uses /collaboration/ws/v1/docs, but the code states that production ingress does not provide this route. If COLLABORATION_WS_URL is absent from runtime configuration, collaboration cannot connect.

Confirm that production always exposes COLLABORATION_WS_URL to the client. Otherwise, add the ingress route before release.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/apps/impress/src/core/config/hooks/useCollaborationUrl.tsx`
around lines 12 - 16, Verify the runtime configuration wiring for
COLLABORATION_WS_URL in production and ensure it is always exposed to the
client; if it is not, add the required production ingress route for the fallback
used by the collaboration URL hook before release.

Comment thread src/yhub-server/README.md
Comment on lines +14 to +18
- serves every route under the `/collaboration/` prefix
(`server.apiPrefix`), including the websocket sync route
`/collaboration/ws/v1/{org}/{docid}`,
- exposes `POST /collaboration/reset-connections/v1/{org}/{docid}` (optional
`X-User-Id` header), for the Django backend to re-check the authorization

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 4 'reset-connections|ws/v1|apiPrefix' \
  src/yhub-server/server.js src/yhub-server/README.md CHANGELOG.md src/backend

Repository: suitenumerique/docs

Length of output: 7898


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg '(^|/)server\.js$|(^|/)README\.md$|^CHANGELOG\.md$|src/backend' | sed -n '1,120p'

echo
echo "== reset-connections and ws route references =="
rg -n -C 3 'reset-connections|ws/v1|ws/v1\/docs|/collaboration/ws/v1|apiPrefix' \
  CHANGELOG.md src/yhub-server src/backend compose.yml docker-compose*.yml -g '!node_modules' -g '!dist' || true

echo
echo "== exact server.js relevant section =="
sed -n '145,165p' src/yhub-server/server.js

Repository: suitenumerique/docs

Length of output: 14380


Use one exact collaboration route contract.

src/yhub-server/server.js registers POST /collaboration/reset-connections/v1/{org}/{docid} and GET /collaboration/ws/v1/{org}/{docid}. Update CHANGELOG.md#L11-L14 and CHANGELOG.md#L17-L21 to match that path shape instead of using the {id} path parameter in changelog docs.

📍 Affects 2 files
  • src/yhub-server/README.md#L14-L18 (this comment)
  • CHANGELOG.md#L11-L14
  • CHANGELOG.md#L17-L21
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/yhub-server/README.md` around lines 14 - 18, Use the exact collaboration
route parameter name `{docid}` consistently in the documentation. Update
CHANGELOG.md lines 11-14 and 17-21 to replace `{id}` with `{docid}` for the
reset-connections and websocket routes; src/yhub-server/README.md lines 14-18
requires no direct change and documents the correct contract.

AntoLC and others added 14 commits August 5, 2026 12:23
The Y_PROVIDER_API_KEY shared secret is replaced by a
signed admin JWT when Django calls the y-provider
conversion endpoint.
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.
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.
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.
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.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
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.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
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.
COLLABORATION_SERVER_SECRET is no longer used in the codebase,
so we can remove it from the codebase.
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.
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>
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 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>
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>
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>
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.

3 participants