@open-mercato/documents 0.6.8-develop.7016.1.4ed7b1e49d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -0
- package/build.mjs +23 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/documents/acl.js +15 -0
- package/dist/modules/documents/acl.js.map +7 -0
- package/dist/modules/documents/api/[id]/archive/route.js +116 -0
- package/dist/modules/documents/api/[id]/archive/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/attachments/[attachmentId]/route.js +155 -0
- package/dist/modules/documents/api/[id]/attachments/[attachmentId]/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/attachments/route.js +153 -0
- package/dist/modules/documents/api/[id]/attachments/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/collab-token/route.js +180 -0
- package/dist/modules/documents/api/[id]/collab-token/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/comments/access-check/route.js +86 -0
- package/dist/modules/documents/api/[id]/comments/access-check/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/comments/route.js +387 -0
- package/dist/modules/documents/api/[id]/comments/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/content/route.js +143 -0
- package/dist/modules/documents/api/[id]/content/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/duplicate/route.js +200 -0
- package/dist/modules/documents/api/[id]/duplicate/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/export/route.js +421 -0
- package/dist/modules/documents/api/[id]/export/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/favorite/route.js +96 -0
- package/dist/modules/documents/api/[id]/favorite/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/links/[linkId]/route.js +85 -0
- package/dist/modules/documents/api/[id]/links/[linkId]/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/links/route.js +257 -0
- package/dist/modules/documents/api/[id]/links/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/principals/route.js +299 -0
- package/dist/modules/documents/api/[id]/principals/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/route.js +246 -0
- package/dist/modules/documents/api/[id]/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/shares/route.js +335 -0
- package/dist/modules/documents/api/[id]/shares/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/unarchive/route.js +39 -0
- package/dist/modules/documents/api/[id]/unarchive/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/versions/[versionId]/restore/route.js +133 -0
- package/dist/modules/documents/api/[id]/versions/[versionId]/restore/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/versions/[versionId]/route.js +93 -0
- package/dist/modules/documents/api/[id]/versions/[versionId]/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/versions/route.js +193 -0
- package/dist/modules/documents/api/[id]/versions/route.js.map +7 -0
- package/dist/modules/documents/api/[id]/watch/route.js +97 -0
- package/dist/modules/documents/api/[id]/watch/route.js.map +7 -0
- package/dist/modules/documents/api/_commands.js +34 -0
- package/dist/modules/documents/api/_commands.js.map +7 -0
- package/dist/modules/documents/api/_shared.js +490 -0
- package/dist/modules/documents/api/_shared.js.map +7 -0
- package/dist/modules/documents/api/folders/route.js +256 -0
- package/dist/modules/documents/api/folders/route.js.map +7 -0
- package/dist/modules/documents/api/instantiate/route.js +108 -0
- package/dist/modules/documents/api/instantiate/route.js.map +7 -0
- package/dist/modules/documents/api/route.js +322 -0
- package/dist/modules/documents/api/route.js.map +7 -0
- package/dist/modules/documents/api/templates/[templateId]/preview/route.js +94 -0
- package/dist/modules/documents/api/templates/[templateId]/preview/route.js.map +7 -0
- package/dist/modules/documents/api/templates/[templateId]/route.js +82 -0
- package/dist/modules/documents/api/templates/[templateId]/route.js.map +7 -0
- package/dist/modules/documents/api/templates/route.js +344 -0
- package/dist/modules/documents/api/templates/route.js.map +7 -0
- package/dist/modules/documents/backend/documents/DocumentsPageClient.js +160 -0
- package/dist/modules/documents/backend/documents/DocumentsPageClient.js.map +7 -0
- package/dist/modules/documents/backend/documents/DocumentsTable.js +143 -0
- package/dist/modules/documents/backend/documents/DocumentsTable.js.map +7 -0
- package/dist/modules/documents/backend/documents/FolderDialog.js +55 -0
- package/dist/modules/documents/backend/documents/FolderDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/FolderTree.js +40 -0
- package/dist/modules/documents/backend/documents/FolderTree.js.map +7 -0
- package/dist/modules/documents/backend/documents/MoveDocumentDialog.js +64 -0
- package/dist/modules/documents/backend/documents/MoveDocumentDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/CommentAnchorNavigation.js +77 -0
- package/dist/modules/documents/backend/documents/[id]/CommentAnchorNavigation.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/CommentComposer.js +126 -0
- package/dist/modules/documents/backend/documents/[id]/CommentComposer.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/CommentThreadList.js +88 -0
- package/dist/modules/documents/backend/documents/[id]/CommentThreadList.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/CommentsRail.js +123 -0
- package/dist/modules/documents/backend/documents/[id]/CommentsRail.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentCanvas.js +91 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentCanvas.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorErrorBoundary.js +27 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorErrorBoundary.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorIsland.js +50 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorIsland.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorSurface.js +168 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentEditorSurface.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentNavigator.js +129 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentNavigator.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentPageClient.js +429 -0
- package/dist/modules/documents/backend/documents/[id]/DocumentPageClient.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/EditorColorTools.js +68 -0
- package/dist/modules/documents/backend/documents/[id]/EditorColorTools.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/EditorStatusPresence.js +69 -0
- package/dist/modules/documents/backend/documents/[id]/EditorStatusPresence.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/EditorToolbar.js +151 -0
- package/dist/modules/documents/backend/documents/[id]/EditorToolbar.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/ExportMenu.js +121 -0
- package/dist/modules/documents/backend/documents/[id]/ExportMenu.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/MentionPicker.js +114 -0
- package/dist/modules/documents/backend/documents/[id]/MentionPicker.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/OutlinePane.js +70 -0
- package/dist/modules/documents/backend/documents/[id]/OutlinePane.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/RecordFieldsDialog.js +157 -0
- package/dist/modules/documents/backend/documents/[id]/RecordFieldsDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/RelatedRecordsPanel.js +266 -0
- package/dist/modules/documents/backend/documents/[id]/RelatedRecordsPanel.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/VersionHistoryPanel.js +227 -0
- package/dist/modules/documents/backend/documents/[id]/VersionHistoryPanel.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/VersionPreviewDialog.js +157 -0
- package/dist/modules/documents/backend/documents/[id]/VersionPreviewDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/commentTypes.js +150 -0
- package/dist/modules/documents/backend/documents/[id]/commentTypes.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/componentCapabilities.js +13 -0
- package/dist/modules/documents/backend/documents/[id]/componentCapabilities.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/documentPagination.js +295 -0
- package/dist/modules/documents/backend/documents/[id]/documentPagination.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/editorPresenceStyles.js +81 -0
- package/dist/modules/documents/backend/documents/[id]/editorPresenceStyles.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/editorTypes.js +52 -0
- package/dist/modules/documents/backend/documents/[id]/editorTypes.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/page.js +10 -0
- package/dist/modules/documents/backend/documents/[id]/page.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/page.meta.js +16 -0
- package/dist/modules/documents/backend/documents/[id]/page.meta.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/relatedRecordModel.js +38 -0
- package/dist/modules/documents/backend/documents/[id]/relatedRecordModel.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/restoreVersion.js +33 -0
- package/dist/modules/documents/backend/documents/[id]/restoreVersion.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentCollaboration.js +392 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentCollaboration.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentComments.js +253 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentComments.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentEditor.js +149 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentEditor.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentTitle.js +82 -0
- package/dist/modules/documents/backend/documents/[id]/useDocumentTitle.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/useEditorInsertions.js +144 -0
- package/dist/modules/documents/backend/documents/[id]/useEditorInsertions.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/useFallbackContentPersistence.js +237 -0
- package/dist/modules/documents/backend/documents/[id]/useFallbackContentPersistence.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/useMentionPicker.js +216 -0
- package/dist/modules/documents/backend/documents/[id]/useMentionPicker.js.map +7 -0
- package/dist/modules/documents/backend/documents/[id]/versionHistoryModel.js +25 -0
- package/dist/modules/documents/backend/documents/[id]/versionHistoryModel.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/EntityPicker.js +156 -0
- package/dist/modules/documents/backend/documents/components/EntityPicker.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/EntityPickerResults.js +46 -0
- package/dist/modules/documents/backend/documents/components/EntityPickerResults.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/NewFromTemplateDialog.js +127 -0
- package/dist/modules/documents/backend/documents/components/NewFromTemplateDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/PrincipalPicker.js +84 -0
- package/dist/modules/documents/backend/documents/components/PrincipalPicker.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/ShareDialog.js +77 -0
- package/dist/modules/documents/backend/documents/components/ShareDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/ShareDialogAddForm.js +93 -0
- package/dist/modules/documents/backend/documents/components/ShareDialogAddForm.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/ShareDialogList.js +82 -0
- package/dist/modules/documents/backend/documents/components/ShareDialogList.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/TemplateBodyEditor.js +96 -0
- package/dist/modules/documents/backend/documents/components/TemplateBodyEditor.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/TemplateEditorDialog.js +169 -0
- package/dist/modules/documents/backend/documents/components/TemplateEditorDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/TemplatePreview.js +38 -0
- package/dist/modules/documents/backend/documents/components/TemplatePreview.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/TemplateSlotFields.js +69 -0
- package/dist/modules/documents/backend/documents/components/TemplateSlotFields.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/TemplateSlotsEditor.js +71 -0
- package/dist/modules/documents/backend/documents/components/TemplateSlotsEditor.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/principalPickerModel.js +63 -0
- package/dist/modules/documents/backend/documents/components/principalPickerModel.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/shareDialogModel.js +68 -0
- package/dist/modules/documents/backend/documents/components/shareDialogModel.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/templateUi.js +89 -0
- package/dist/modules/documents/backend/documents/components/templateUi.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/useAvailableEntityRegistry.js +30 -0
- package/dist/modules/documents/backend/documents/components/useAvailableEntityRegistry.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/useEntitySearch.js +191 -0
- package/dist/modules/documents/backend/documents/components/useEntitySearch.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/usePrincipalPicker.js +221 -0
- package/dist/modules/documents/backend/documents/components/usePrincipalPicker.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/useShareDialog.js +236 -0
- package/dist/modules/documents/backend/documents/components/useShareDialog.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/useTemplateDetail.js +49 -0
- package/dist/modules/documents/backend/documents/components/useTemplateDetail.js.map +7 -0
- package/dist/modules/documents/backend/documents/components/useTemplateInstantiation.js +234 -0
- package/dist/modules/documents/backend/documents/components/useTemplateInstantiation.js.map +7 -0
- package/dist/modules/documents/backend/documents/documentUi.js +106 -0
- package/dist/modules/documents/backend/documents/documentUi.js.map +7 -0
- package/dist/modules/documents/backend/documents/documentsListTypes.js +132 -0
- package/dist/modules/documents/backend/documents/documentsListTypes.js.map +7 -0
- package/dist/modules/documents/backend/documents/page.js +9 -0
- package/dist/modules/documents/backend/documents/page.js.map +7 -0
- package/dist/modules/documents/backend/documents/page.meta.js +26 -0
- package/dist/modules/documents/backend/documents/page.meta.js.map +7 -0
- package/dist/modules/documents/backend/documents/templates/TemplatesPageClient.js +91 -0
- package/dist/modules/documents/backend/documents/templates/TemplatesPageClient.js.map +7 -0
- package/dist/modules/documents/backend/documents/templates/TemplatesTable.js +58 -0
- package/dist/modules/documents/backend/documents/templates/TemplatesTable.js.map +7 -0
- package/dist/modules/documents/backend/documents/templates/page.js +9 -0
- package/dist/modules/documents/backend/documents/templates/page.js.map +7 -0
- package/dist/modules/documents/backend/documents/templates/page.meta.js +34 -0
- package/dist/modules/documents/backend/documents/templates/page.meta.js.map +7 -0
- package/dist/modules/documents/backend/documents/templates/useTemplatesPage.js +139 -0
- package/dist/modules/documents/backend/documents/templates/useTemplatesPage.js.map +7 -0
- package/dist/modules/documents/backend/documents/useDocumentsList.js +314 -0
- package/dist/modules/documents/backend/documents/useDocumentsList.js.map +7 -0
- package/dist/modules/documents/ce.js +8 -0
- package/dist/modules/documents/ce.js.map +7 -0
- package/dist/modules/documents/commands/aggregate.js +99 -0
- package/dist/modules/documents/commands/aggregate.js.map +7 -0
- package/dist/modules/documents/commands/attachments.js +252 -0
- package/dist/modules/documents/commands/attachments.js.map +7 -0
- package/dist/modules/documents/commands/comments.js +782 -0
- package/dist/modules/documents/commands/comments.js.map +7 -0
- package/dist/modules/documents/commands/content.js +238 -0
- package/dist/modules/documents/commands/content.js.map +7 -0
- package/dist/modules/documents/commands/document-crud.js +505 -0
- package/dist/modules/documents/commands/document-crud.js.map +7 -0
- package/dist/modules/documents/commands/documents.js +473 -0
- package/dist/modules/documents/commands/documents.js.map +7 -0
- package/dist/modules/documents/commands/duplicate.js +640 -0
- package/dist/modules/documents/commands/duplicate.js.map +7 -0
- package/dist/modules/documents/commands/favorites.js +150 -0
- package/dist/modules/documents/commands/favorites.js.map +7 -0
- package/dist/modules/documents/commands/folders.js +420 -0
- package/dist/modules/documents/commands/folders.js.map +7 -0
- package/dist/modules/documents/commands/index.js +64 -0
- package/dist/modules/documents/commands/index.js.map +7 -0
- package/dist/modules/documents/commands/interceptors.js +300 -0
- package/dist/modules/documents/commands/interceptors.js.map +7 -0
- package/dist/modules/documents/commands/lifecycle.js +249 -0
- package/dist/modules/documents/commands/lifecycle.js.map +7 -0
- package/dist/modules/documents/commands/links.js +410 -0
- package/dist/modules/documents/commands/links.js.map +7 -0
- package/dist/modules/documents/commands/mutation-helpers.js +42 -0
- package/dist/modules/documents/commands/mutation-helpers.js.map +7 -0
- package/dist/modules/documents/commands/projection-types.js +1 -0
- package/dist/modules/documents/commands/projection-types.js.map +7 -0
- package/dist/modules/documents/commands/shared.js +143 -0
- package/dist/modules/documents/commands/shared.js.map +7 -0
- package/dist/modules/documents/commands/shares.js +527 -0
- package/dist/modules/documents/commands/shares.js.map +7 -0
- package/dist/modules/documents/commands/side-effects.js +103 -0
- package/dist/modules/documents/commands/side-effects.js.map +7 -0
- package/dist/modules/documents/commands/templates.js +461 -0
- package/dist/modules/documents/commands/templates.js.map +7 -0
- package/dist/modules/documents/commands/versions.js +432 -0
- package/dist/modules/documents/commands/versions.js.map +7 -0
- package/dist/modules/documents/commands/watchers.js +164 -0
- package/dist/modules/documents/commands/watchers.js.map +7 -0
- package/dist/modules/documents/data/entities.js +653 -0
- package/dist/modules/documents/data/entities.js.map +7 -0
- package/dist/modules/documents/data/extensions.js +44 -0
- package/dist/modules/documents/data/extensions.js.map +7 -0
- package/dist/modules/documents/data/validators.js +219 -0
- package/dist/modules/documents/data/validators.js.map +7 -0
- package/dist/modules/documents/di.js +35 -0
- package/dist/modules/documents/di.js.map +7 -0
- package/dist/modules/documents/encryption.js +12 -0
- package/dist/modules/documents/encryption.js.map +7 -0
- package/dist/modules/documents/events.js +27 -0
- package/dist/modules/documents/events.js.map +7 -0
- package/dist/modules/documents/index.js +19 -0
- package/dist/modules/documents/index.js.map +7 -0
- package/dist/modules/documents/lib/attachmentServicePort.js +30 -0
- package/dist/modules/documents/lib/attachmentServicePort.js.map +7 -0
- package/dist/modules/documents/lib/capabilities.js +32 -0
- package/dist/modules/documents/lib/capabilities.js.map +7 -0
- package/dist/modules/documents/lib/collabCloseEvents.js +17 -0
- package/dist/modules/documents/lib/collabCloseEvents.js.map +7 -0
- package/dist/modules/documents/lib/collabEndpoint.js +22 -0
- package/dist/modules/documents/lib/collabEndpoint.js.map +7 -0
- package/dist/modules/documents/lib/collabMaterializer.js +107 -0
- package/dist/modules/documents/lib/collabMaterializer.js.map +7 -0
- package/dist/modules/documents/lib/collabToken.js +120 -0
- package/dist/modules/documents/lib/collabToken.js.map +7 -0
- package/dist/modules/documents/lib/collaborationAwareness.js +68 -0
- package/dist/modules/documents/lib/collaborationAwareness.js.map +7 -0
- package/dist/modules/documents/lib/commentAnchors.js +15 -0
- package/dist/modules/documents/lib/commentAnchors.js.map +7 -0
- package/dist/modules/documents/lib/constants.js +25 -0
- package/dist/modules/documents/lib/constants.js.map +7 -0
- package/dist/modules/documents/lib/contentService.js +248 -0
- package/dist/modules/documents/lib/contentService.js.map +7 -0
- package/dist/modules/documents/lib/displayLabels.js +23 -0
- package/dist/modules/documents/lib/displayLabels.js.map +7 -0
- package/dist/modules/documents/lib/docxRenderer.js +601 -0
- package/dist/modules/documents/lib/docxRenderer.js.map +7 -0
- package/dist/modules/documents/lib/duplicateContent.js +22 -0
- package/dist/modules/documents/lib/duplicateContent.js.map +7 -0
- package/dist/modules/documents/lib/editorConfig.js +287 -0
- package/dist/modules/documents/lib/editorConfig.js.map +7 -0
- package/dist/modules/documents/lib/entityLinks.js +148 -0
- package/dist/modules/documents/lib/entityLinks.js.map +7 -0
- package/dist/modules/documents/lib/entityRegistry.js +258 -0
- package/dist/modules/documents/lib/entityRegistry.js.map +7 -0
- package/dist/modules/documents/lib/entityRegistry.server.js +189 -0
- package/dist/modules/documents/lib/entityRegistry.server.js.map +7 -0
- package/dist/modules/documents/lib/entityRegistryAvailability.server.js +9 -0
- package/dist/modules/documents/lib/entityRegistryAvailability.server.js.map +7 -0
- package/dist/modules/documents/lib/entitySuggestion.js +50 -0
- package/dist/modules/documents/lib/entitySuggestion.js.map +7 -0
- package/dist/modules/documents/lib/favorites.js +22 -0
- package/dist/modules/documents/lib/favorites.js.map +7 -0
- package/dist/modules/documents/lib/folderHierarchySerialization.js +19 -0
- package/dist/modules/documents/lib/folderHierarchySerialization.js.map +7 -0
- package/dist/modules/documents/lib/historyLimits.js +100 -0
- package/dist/modules/documents/lib/historyLimits.js.map +7 -0
- package/dist/modules/documents/lib/historyPolicy.js +14 -0
- package/dist/modules/documents/lib/historyPolicy.js.map +7 -0
- package/dist/modules/documents/lib/organizationAccess.js +17 -0
- package/dist/modules/documents/lib/organizationAccess.js.map +7 -0
- package/dist/modules/documents/lib/pdfHtml.js +62 -0
- package/dist/modules/documents/lib/pdfHtml.js.map +7 -0
- package/dist/modules/documents/lib/pdfRenderer.js +284 -0
- package/dist/modules/documents/lib/pdfRenderer.js.map +7 -0
- package/dist/modules/documents/lib/permissions.js +222 -0
- package/dist/modules/documents/lib/permissions.js.map +7 -0
- package/dist/modules/documents/lib/platformServices.js +35 -0
- package/dist/modules/documents/lib/platformServices.js.map +7 -0
- package/dist/modules/documents/lib/recordFieldInsertion.js +58 -0
- package/dist/modules/documents/lib/recordFieldInsertion.js.map +7 -0
- package/dist/modules/documents/lib/requestBody.js +80 -0
- package/dist/modules/documents/lib/requestBody.js.map +7 -0
- package/dist/modules/documents/lib/resourceLimits.js +62 -0
- package/dist/modules/documents/lib/resourceLimits.js.map +7 -0
- package/dist/modules/documents/lib/shareCounts.js +25 -0
- package/dist/modules/documents/lib/shareCounts.js.map +7 -0
- package/dist/modules/documents/lib/templateFill.js +84 -0
- package/dist/modules/documents/lib/templateFill.js.map +7 -0
- package/dist/modules/documents/lib/templateInstantiation.js +147 -0
- package/dist/modules/documents/lib/templateInstantiation.js.map +7 -0
- package/dist/modules/documents/lib/templatePreviewDigest.js +82 -0
- package/dist/modules/documents/lib/templatePreviewDigest.js.map +7 -0
- package/dist/modules/documents/lib/templateSeeds.js +201 -0
- package/dist/modules/documents/lib/templateSeeds.js.map +7 -0
- package/dist/modules/documents/lib/userLabels.js +37 -0
- package/dist/modules/documents/lib/userLabels.js.map +7 -0
- package/dist/modules/documents/lib/versionContent.js +131 -0
- package/dist/modules/documents/lib/versionContent.js.map +7 -0
- package/dist/modules/documents/lib/versionLabels.js +12 -0
- package/dist/modules/documents/lib/versionLabels.js.map +7 -0
- package/dist/modules/documents/lib/versioning.js +14 -0
- package/dist/modules/documents/lib/versioning.js.map +7 -0
- package/dist/modules/documents/lib/visibility.js +422 -0
- package/dist/modules/documents/lib/visibility.js.map +7 -0
- package/dist/modules/documents/lib/watchers.js +106 -0
- package/dist/modules/documents/lib/watchers.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260708163836_documents.js +44 -0
- package/dist/modules/documents/migrations/Migration20260708163836_documents.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260709164720_documents.js +17 -0
- package/dist/modules/documents/migrations/Migration20260709164720_documents.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260710002318_documents.js +32 -0
- package/dist/modules/documents/migrations/Migration20260710002318_documents.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260710071003_documents.js +13 -0
- package/dist/modules/documents/migrations/Migration20260710071003_documents.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260712120000_documents.js +13 -0
- package/dist/modules/documents/migrations/Migration20260712120000_documents.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260712183000_documents.js +14 -0
- package/dist/modules/documents/migrations/Migration20260712183000_documents.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260713092156_documents.js +29 -0
- package/dist/modules/documents/migrations/Migration20260713092156_documents.js.map +7 -0
- package/dist/modules/documents/migrations/Migration20260717000000_documents.js +37 -0
- package/dist/modules/documents/migrations/Migration20260717000000_documents.js.map +7 -0
- package/dist/modules/documents/notifications.js +47 -0
- package/dist/modules/documents/notifications.js.map +7 -0
- package/dist/modules/documents/search.js +101 -0
- package/dist/modules/documents/search.js.map +7 -0
- package/dist/modules/documents/setup.js +19 -0
- package/dist/modules/documents/setup.js.map +7 -0
- package/dist/modules/documents/widgets/injection/related-documents/LinkDocumentDialog.js +214 -0
- package/dist/modules/documents/widgets/injection/related-documents/LinkDocumentDialog.js.map +7 -0
- package/dist/modules/documents/widgets/injection/related-documents/context.js +84 -0
- package/dist/modules/documents/widgets/injection/related-documents/context.js.map +7 -0
- package/dist/modules/documents/widgets/injection/related-documents/useRelatedDocuments.js +98 -0
- package/dist/modules/documents/widgets/injection/related-documents/useRelatedDocuments.js.map +7 -0
- package/dist/modules/documents/widgets/injection/related-documents/widget.client.js +83 -0
- package/dist/modules/documents/widgets/injection/related-documents/widget.client.js.map +7 -0
- package/dist/modules/documents/widgets/injection/related-documents/widget.js +19 -0
- package/dist/modules/documents/widgets/injection/related-documents/widget.js.map +7 -0
- package/dist/modules/documents/widgets/injection-table.js +21 -0
- package/dist/modules/documents/widgets/injection-table.js.map +7 -0
- package/dist/server/documents-collab-server.js +2260 -0
- package/dist/server/documents-collab-server.js.map +7 -0
- package/generated/entities/document/index.ts +12 -0
- package/generated/entities/document_attachment/index.ts +9 -0
- package/generated/entities/document_comment/index.ts +14 -0
- package/generated/entities/document_content/index.ts +11 -0
- package/generated/entities/document_entity_link/index.ts +19 -0
- package/generated/entities/document_favorite/index.ts +7 -0
- package/generated/entities/document_folder/index.ts +9 -0
- package/generated/entities/document_share/index.ts +11 -0
- package/generated/entities/document_template/index.ts +13 -0
- package/generated/entities/document_version/index.ts +9 -0
- package/generated/entities/document_watcher/index.ts +7 -0
- package/generated/entities.ids.generated.ts +21 -0
- package/generated/entity-fields-registry.ts +151 -0
- package/jest.config.cjs +34 -0
- package/package.json +149 -0
- package/server/documents-collab-server.ts +3416 -0
- package/src/index.ts +1 -0
- package/src/modules/documents/acl.ts +11 -0
- package/src/modules/documents/api/[id]/archive/route.ts +132 -0
- package/src/modules/documents/api/[id]/attachments/[attachmentId]/route.ts +166 -0
- package/src/modules/documents/api/[id]/attachments/route.ts +173 -0
- package/src/modules/documents/api/[id]/collab-token/route.ts +203 -0
- package/src/modules/documents/api/[id]/comments/access-check/route.ts +96 -0
- package/src/modules/documents/api/[id]/comments/route.ts +442 -0
- package/src/modules/documents/api/[id]/content/route.ts +156 -0
- package/src/modules/documents/api/[id]/duplicate/route.ts +231 -0
- package/src/modules/documents/api/[id]/export/route.ts +535 -0
- package/src/modules/documents/api/[id]/favorite/route.ts +110 -0
- package/src/modules/documents/api/[id]/links/[linkId]/route.ts +92 -0
- package/src/modules/documents/api/[id]/links/route.ts +301 -0
- package/src/modules/documents/api/[id]/principals/route.ts +375 -0
- package/src/modules/documents/api/[id]/route.ts +267 -0
- package/src/modules/documents/api/[id]/shares/route.ts +372 -0
- package/src/modules/documents/api/[id]/unarchive/route.ts +42 -0
- package/src/modules/documents/api/[id]/versions/[versionId]/restore/route.ts +152 -0
- package/src/modules/documents/api/[id]/versions/[versionId]/route.ts +100 -0
- package/src/modules/documents/api/[id]/versions/route.ts +207 -0
- package/src/modules/documents/api/[id]/watch/route.ts +111 -0
- package/src/modules/documents/api/_commands.ts +53 -0
- package/src/modules/documents/api/_shared.ts +676 -0
- package/src/modules/documents/api/folders/route.ts +287 -0
- package/src/modules/documents/api/instantiate/route.ts +114 -0
- package/src/modules/documents/api/route.ts +362 -0
- package/src/modules/documents/api/templates/[templateId]/preview/route.ts +97 -0
- package/src/modules/documents/api/templates/[templateId]/route.ts +92 -0
- package/src/modules/documents/api/templates/route.ts +370 -0
- package/src/modules/documents/backend/documents/DocumentsPageClient.tsx +150 -0
- package/src/modules/documents/backend/documents/DocumentsTable.tsx +168 -0
- package/src/modules/documents/backend/documents/FolderDialog.tsx +62 -0
- package/src/modules/documents/backend/documents/FolderTree.tsx +72 -0
- package/src/modules/documents/backend/documents/MoveDocumentDialog.tsx +70 -0
- package/src/modules/documents/backend/documents/[id]/CommentAnchorNavigation.ts +88 -0
- package/src/modules/documents/backend/documents/[id]/CommentComposer.tsx +136 -0
- package/src/modules/documents/backend/documents/[id]/CommentThreadList.tsx +131 -0
- package/src/modules/documents/backend/documents/[id]/CommentsRail.tsx +138 -0
- package/src/modules/documents/backend/documents/[id]/DocumentCanvas.tsx +111 -0
- package/src/modules/documents/backend/documents/[id]/DocumentEditorErrorBoundary.tsx +39 -0
- package/src/modules/documents/backend/documents/[id]/DocumentEditorIsland.tsx +50 -0
- package/src/modules/documents/backend/documents/[id]/DocumentEditorSurface.tsx +162 -0
- package/src/modules/documents/backend/documents/[id]/DocumentNavigator.tsx +129 -0
- package/src/modules/documents/backend/documents/[id]/DocumentPageClient.tsx +463 -0
- package/src/modules/documents/backend/documents/[id]/EditorColorTools.tsx +71 -0
- package/src/modules/documents/backend/documents/[id]/EditorStatusPresence.tsx +78 -0
- package/src/modules/documents/backend/documents/[id]/EditorToolbar.tsx +108 -0
- package/src/modules/documents/backend/documents/[id]/ExportMenu.tsx +144 -0
- package/src/modules/documents/backend/documents/[id]/MentionPicker.tsx +137 -0
- package/src/modules/documents/backend/documents/[id]/OutlinePane.tsx +98 -0
- package/src/modules/documents/backend/documents/[id]/RecordFieldsDialog.tsx +185 -0
- package/src/modules/documents/backend/documents/[id]/RelatedRecordsPanel.tsx +320 -0
- package/src/modules/documents/backend/documents/[id]/VersionHistoryPanel.tsx +244 -0
- package/src/modules/documents/backend/documents/[id]/VersionPreviewDialog.tsx +197 -0
- package/src/modules/documents/backend/documents/[id]/commentTypes.ts +192 -0
- package/src/modules/documents/backend/documents/[id]/componentCapabilities.ts +17 -0
- package/src/modules/documents/backend/documents/[id]/documentPagination.ts +352 -0
- package/src/modules/documents/backend/documents/[id]/editorPresenceStyles.ts +77 -0
- package/src/modules/documents/backend/documents/[id]/editorTypes.ts +84 -0
- package/src/modules/documents/backend/documents/[id]/page.meta.ts +12 -0
- package/src/modules/documents/backend/documents/[id]/page.tsx +6 -0
- package/src/modules/documents/backend/documents/[id]/relatedRecordModel.ts +49 -0
- package/src/modules/documents/backend/documents/[id]/restoreVersion.ts +49 -0
- package/src/modules/documents/backend/documents/[id]/useDocumentCollaboration.ts +458 -0
- package/src/modules/documents/backend/documents/[id]/useDocumentComments.ts +286 -0
- package/src/modules/documents/backend/documents/[id]/useDocumentEditor.ts +157 -0
- package/src/modules/documents/backend/documents/[id]/useDocumentTitle.ts +77 -0
- package/src/modules/documents/backend/documents/[id]/useEditorInsertions.ts +147 -0
- package/src/modules/documents/backend/documents/[id]/useFallbackContentPersistence.ts +277 -0
- package/src/modules/documents/backend/documents/[id]/useMentionPicker.ts +259 -0
- package/src/modules/documents/backend/documents/[id]/versionHistoryModel.ts +29 -0
- package/src/modules/documents/backend/documents/components/EntityPicker.tsx +171 -0
- package/src/modules/documents/backend/documents/components/EntityPickerResults.tsx +64 -0
- package/src/modules/documents/backend/documents/components/NewFromTemplateDialog.tsx +136 -0
- package/src/modules/documents/backend/documents/components/PrincipalPicker.tsx +90 -0
- package/src/modules/documents/backend/documents/components/ShareDialog.tsx +88 -0
- package/src/modules/documents/backend/documents/components/ShareDialogAddForm.tsx +113 -0
- package/src/modules/documents/backend/documents/components/ShareDialogList.tsx +117 -0
- package/src/modules/documents/backend/documents/components/TemplateBodyEditor.tsx +96 -0
- package/src/modules/documents/backend/documents/components/TemplateEditorDialog.tsx +171 -0
- package/src/modules/documents/backend/documents/components/TemplatePreview.tsx +43 -0
- package/src/modules/documents/backend/documents/components/TemplateSlotFields.tsx +72 -0
- package/src/modules/documents/backend/documents/components/TemplateSlotsEditor.tsx +72 -0
- package/src/modules/documents/backend/documents/components/principalPickerModel.ts +86 -0
- package/src/modules/documents/backend/documents/components/shareDialogModel.ts +94 -0
- package/src/modules/documents/backend/documents/components/templateUi.ts +128 -0
- package/src/modules/documents/backend/documents/components/useAvailableEntityRegistry.ts +43 -0
- package/src/modules/documents/backend/documents/components/useEntitySearch.ts +197 -0
- package/src/modules/documents/backend/documents/components/usePrincipalPicker.ts +165 -0
- package/src/modules/documents/backend/documents/components/useShareDialog.ts +267 -0
- package/src/modules/documents/backend/documents/components/useTemplateDetail.ts +54 -0
- package/src/modules/documents/backend/documents/components/useTemplateInstantiation.ts +252 -0
- package/src/modules/documents/backend/documents/documentUi.ts +134 -0
- package/src/modules/documents/backend/documents/documentsListTypes.ts +163 -0
- package/src/modules/documents/backend/documents/page.meta.ts +24 -0
- package/src/modules/documents/backend/documents/page.tsx +5 -0
- package/src/modules/documents/backend/documents/templates/TemplatesPageClient.tsx +93 -0
- package/src/modules/documents/backend/documents/templates/TemplatesTable.tsx +71 -0
- package/src/modules/documents/backend/documents/templates/page.meta.ts +32 -0
- package/src/modules/documents/backend/documents/templates/page.tsx +5 -0
- package/src/modules/documents/backend/documents/templates/useTemplatesPage.tsx +148 -0
- package/src/modules/documents/backend/documents/useDocumentsList.tsx +305 -0
- package/src/modules/documents/ce.ts +6 -0
- package/src/modules/documents/commands/aggregate.ts +128 -0
- package/src/modules/documents/commands/attachments.ts +309 -0
- package/src/modules/documents/commands/comments.ts +966 -0
- package/src/modules/documents/commands/content.ts +294 -0
- package/src/modules/documents/commands/document-crud.ts +609 -0
- package/src/modules/documents/commands/documents.ts +581 -0
- package/src/modules/documents/commands/duplicate.ts +772 -0
- package/src/modules/documents/commands/favorites.ts +171 -0
- package/src/modules/documents/commands/folders.ts +506 -0
- package/src/modules/documents/commands/index.ts +33 -0
- package/src/modules/documents/commands/interceptors.ts +402 -0
- package/src/modules/documents/commands/lifecycle.ts +333 -0
- package/src/modules/documents/commands/links.ts +486 -0
- package/src/modules/documents/commands/mutation-helpers.ts +48 -0
- package/src/modules/documents/commands/projection-types.ts +77 -0
- package/src/modules/documents/commands/shared.ts +219 -0
- package/src/modules/documents/commands/shares.ts +612 -0
- package/src/modules/documents/commands/side-effects.ts +130 -0
- package/src/modules/documents/commands/templates.ts +522 -0
- package/src/modules/documents/commands/versions.ts +537 -0
- package/src/modules/documents/commands/watchers.ts +181 -0
- package/src/modules/documents/data/entities.ts +631 -0
- package/src/modules/documents/data/extensions.ts +42 -0
- package/src/modules/documents/data/validators.ts +248 -0
- package/src/modules/documents/di.ts +33 -0
- package/src/modules/documents/encryption.ts +14 -0
- package/src/modules/documents/events.ts +23 -0
- package/src/modules/documents/i18n/.hardcoded-allowlist.json +9 -0
- package/src/modules/documents/i18n/de.json +522 -0
- package/src/modules/documents/i18n/en.json +522 -0
- package/src/modules/documents/i18n/es.json +522 -0
- package/src/modules/documents/i18n/ko.json +522 -0
- package/src/modules/documents/i18n/pl.json +522 -0
- package/src/modules/documents/index.ts +17 -0
- package/src/modules/documents/lib/attachmentServicePort.ts +94 -0
- package/src/modules/documents/lib/capabilities.ts +57 -0
- package/src/modules/documents/lib/collabCloseEvents.ts +26 -0
- package/src/modules/documents/lib/collabEndpoint.ts +43 -0
- package/src/modules/documents/lib/collabMaterializer.ts +140 -0
- package/src/modules/documents/lib/collabToken.ts +170 -0
- package/src/modules/documents/lib/collaborationAwareness.ts +102 -0
- package/src/modules/documents/lib/commentAnchors.ts +19 -0
- package/src/modules/documents/lib/constants.ts +39 -0
- package/src/modules/documents/lib/contentService.ts +340 -0
- package/src/modules/documents/lib/displayLabels.ts +31 -0
- package/src/modules/documents/lib/docxRenderer.ts +685 -0
- package/src/modules/documents/lib/duplicateContent.ts +32 -0
- package/src/modules/documents/lib/editorConfig.ts +371 -0
- package/src/modules/documents/lib/entityLinks.ts +250 -0
- package/src/modules/documents/lib/entityRegistry.server.ts +267 -0
- package/src/modules/documents/lib/entityRegistry.ts +345 -0
- package/src/modules/documents/lib/entityRegistryAvailability.server.ts +19 -0
- package/src/modules/documents/lib/entitySuggestion.ts +61 -0
- package/src/modules/documents/lib/favorites.ts +35 -0
- package/src/modules/documents/lib/folderHierarchySerialization.ts +34 -0
- package/src/modules/documents/lib/historyLimits.ts +154 -0
- package/src/modules/documents/lib/historyPolicy.ts +7 -0
- package/src/modules/documents/lib/organizationAccess.ts +44 -0
- package/src/modules/documents/lib/pdfHtml.ts +65 -0
- package/src/modules/documents/lib/pdfRenderer.ts +367 -0
- package/src/modules/documents/lib/permissions.ts +299 -0
- package/src/modules/documents/lib/platformServices.ts +70 -0
- package/src/modules/documents/lib/recordFieldInsertion.ts +72 -0
- package/src/modules/documents/lib/requestBody.ts +97 -0
- package/src/modules/documents/lib/resourceLimits.ts +94 -0
- package/src/modules/documents/lib/shareCounts.ts +44 -0
- package/src/modules/documents/lib/templateFill.ts +138 -0
- package/src/modules/documents/lib/templateInstantiation.ts +206 -0
- package/src/modules/documents/lib/templatePreviewDigest.ts +122 -0
- package/src/modules/documents/lib/templateSeeds.ts +248 -0
- package/src/modules/documents/lib/userLabels.ts +58 -0
- package/src/modules/documents/lib/versionContent.ts +163 -0
- package/src/modules/documents/lib/versionLabels.ts +9 -0
- package/src/modules/documents/lib/versioning.ts +29 -0
- package/src/modules/documents/lib/visibility.ts +610 -0
- package/src/modules/documents/lib/watchers.ts +129 -0
- package/src/modules/documents/migrations/.snapshot-open-mercato.json +2892 -0
- package/src/modules/documents/migrations/Migration20260708163836_documents.ts +50 -0
- package/src/modules/documents/migrations/Migration20260709164720_documents.ts +19 -0
- package/src/modules/documents/migrations/Migration20260710002318_documents.ts +35 -0
- package/src/modules/documents/migrations/Migration20260710071003_documents.ts +13 -0
- package/src/modules/documents/migrations/Migration20260712120000_documents.ts +13 -0
- package/src/modules/documents/migrations/Migration20260712183000_documents.ts +12 -0
- package/src/modules/documents/migrations/Migration20260713092156_documents.ts +43 -0
- package/src/modules/documents/migrations/Migration20260717000000_documents.ts +40 -0
- package/src/modules/documents/notifications.ts +45 -0
- package/src/modules/documents/search.ts +118 -0
- package/src/modules/documents/setup.ts +19 -0
- package/src/modules/documents/widgets/injection/related-documents/LinkDocumentDialog.tsx +224 -0
- package/src/modules/documents/widgets/injection/related-documents/context.ts +101 -0
- package/src/modules/documents/widgets/injection/related-documents/useRelatedDocuments.ts +112 -0
- package/src/modules/documents/widgets/injection/related-documents/widget.client.tsx +76 -0
- package/src/modules/documents/widgets/injection/related-documents/widget.ts +19 -0
- package/src/modules/documents/widgets/injection-table.ts +20 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,2260 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { LockMode } from "@mikro-orm/core";
|
|
5
|
+
import {
|
|
6
|
+
Connection as HocuspocusConnection,
|
|
7
|
+
IncomingMessage as HocuspocusIncomingMessage,
|
|
8
|
+
MessageReceiver as HocuspocusMessageReceiver,
|
|
9
|
+
MessageType,
|
|
10
|
+
OutgoingMessage as HocuspocusOutgoingMessage
|
|
11
|
+
} from "@hocuspocus/server";
|
|
12
|
+
import { Redis as HocuspocusRedis } from "@hocuspocus/extension-redis";
|
|
13
|
+
import * as Y from "yjs";
|
|
14
|
+
import { hasAllFeatures } from "@open-mercato/shared/lib/auth/featureMatch";
|
|
15
|
+
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
16
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
17
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
18
|
+
import { OPTIMISTIC_LOCK_CONFLICT_CODE } from "@open-mercato/shared/lib/crud/optimistic-lock-headers";
|
|
19
|
+
import {
|
|
20
|
+
COLLAB_TOKEN_CLOCK_SKEW_SECONDS,
|
|
21
|
+
COLLAB_TOKEN_TTL_SECONDS,
|
|
22
|
+
isCollabTokenV2Ready,
|
|
23
|
+
resolveLegacyCollabTokenVerifier,
|
|
24
|
+
verifyCollabTokenV2
|
|
25
|
+
} from "@open-mercato/documents/modules/documents/lib/collabToken";
|
|
26
|
+
import {
|
|
27
|
+
advanceDocumentCollaborationGeneration,
|
|
28
|
+
loadDocumentCollaborationGeneration,
|
|
29
|
+
loadDocumentContentForCollaboration,
|
|
30
|
+
normalizeDocumentCollaborationGeneration,
|
|
31
|
+
persistDocumentContent
|
|
32
|
+
} from "@open-mercato/documents/modules/documents/lib/contentService";
|
|
33
|
+
import {
|
|
34
|
+
htmlToYDoc,
|
|
35
|
+
yDocToContent
|
|
36
|
+
} from "@open-mercato/documents/modules/documents/lib/collabMaterializer";
|
|
37
|
+
import { Document, DocumentContent } from "@open-mercato/documents/modules/documents/data/entities";
|
|
38
|
+
import { deriveDocumentCapabilities } from "@open-mercato/documents/modules/documents/lib/capabilities";
|
|
39
|
+
import { resolveUserAccess } from "@open-mercato/documents/modules/documents/lib/permissions";
|
|
40
|
+
import { resolveUserLabels } from "@open-mercato/documents/modules/documents/lib/userLabels";
|
|
41
|
+
import { resolveOrganizationScopeService } from "@open-mercato/documents/modules/documents/lib/platformServices";
|
|
42
|
+
import {
|
|
43
|
+
hasResolvedDocumentsOrganizationAccess
|
|
44
|
+
} from "@open-mercato/documents/modules/documents/lib/organizationAccess";
|
|
45
|
+
import {
|
|
46
|
+
createCanonicalCollaborationAwarenessUser
|
|
47
|
+
} from "@open-mercato/documents/modules/documents/lib/collaborationAwareness";
|
|
48
|
+
import {
|
|
49
|
+
assertDocumentContentResourceLimits,
|
|
50
|
+
assertDocumentYjsStateByteLength,
|
|
51
|
+
DOCUMENTS_COLLAB_MAX_PAYLOAD_BYTES
|
|
52
|
+
} from "@open-mercato/documents/modules/documents/lib/resourceLimits";
|
|
53
|
+
import { COLLAB_CONTENT_RESET_CLOSE_EVENT } from "@open-mercato/documents/modules/documents/lib/collabCloseEvents";
|
|
54
|
+
const logger = createLogger("documents-collab");
|
|
55
|
+
const MAX_COLLAB_STORE_ATTEMPTS = 4;
|
|
56
|
+
const DOCUMENTS_COLLAB_MAX_AWARENESS_STATE_BYTES = 8 * 1024;
|
|
57
|
+
const DOCUMENTS_COLLAB_MAX_AWARENESS_CLIENT_IDS_PER_CONNECTION = 1;
|
|
58
|
+
const DOCUMENTS_COLLAB_MAX_AWARENESS_CLIENT_IDS_PER_ROOM = 256;
|
|
59
|
+
const MAX_YJS_CLIENT_ID = 4294967295;
|
|
60
|
+
const COLLAB_SERVER_TRANSPORT_OPTIONS = {
|
|
61
|
+
maxPayload: DOCUMENTS_COLLAB_MAX_PAYLOAD_BYTES
|
|
62
|
+
};
|
|
63
|
+
const DOCUMENTS_COLLAB_MAX_PENDING_MESSAGES = 128;
|
|
64
|
+
const DOCUMENTS_COLLAB_MAX_PENDING_BYTES = DOCUMENTS_COLLAB_MAX_PAYLOAD_BYTES * 2;
|
|
65
|
+
const DOCUMENTS_COLLAB_MAX_PENDING_DOCUMENTS = 32;
|
|
66
|
+
const DOCUMENTS_COLLAB_MAX_AWARENESS_FRAME_BYTES = 32 * 1024;
|
|
67
|
+
const DOCUMENTS_COLLAB_MAX_CONTROL_FRAME_BYTES = 64 * 1024;
|
|
68
|
+
const DOCUMENTS_COLLAB_MAX_READ_ONLY_SYNC_FRAME_BYTES = 256 * 1024;
|
|
69
|
+
const DOCUMENTS_COLLAB_AUTHORIZATION_TICKET_TIMEOUT_MS = 6e4;
|
|
70
|
+
const DOCUMENTS_COLLAB_ACTIVE_REAUTHORIZATION_INTERVAL_MS = 15e3;
|
|
71
|
+
const COLLAB_SERVER_WEBSOCKET_CONFIGURATION = {
|
|
72
|
+
websocketOptions: COLLAB_SERVER_TRANSPORT_OPTIONS
|
|
73
|
+
};
|
|
74
|
+
const COLLAB_SERVER_UNAUTHENTICATED_CONFIGURATION = {
|
|
75
|
+
maxUnauthenticatedQueueSize: DOCUMENTS_COLLAB_MAX_PENDING_BYTES,
|
|
76
|
+
maxUnauthenticatedQueueMessages: DOCUMENTS_COLLAB_MAX_PENDING_MESSAGES,
|
|
77
|
+
maxPendingDocuments: DOCUMENTS_COLLAB_MAX_PENDING_DOCUMENTS
|
|
78
|
+
};
|
|
79
|
+
const COLLAB_SERVER_RUNTIME_CONFIGURATION = {
|
|
80
|
+
...COLLAB_SERVER_WEBSOCKET_CONFIGURATION,
|
|
81
|
+
...COLLAB_SERVER_UNAUTHENTICATED_CONFIGURATION
|
|
82
|
+
};
|
|
83
|
+
const DEFAULT_DOCUMENTS_COLLAB_REDIS_PREFIX = "open-mercato:documents:collab:development";
|
|
84
|
+
const LEGACY_DOCUMENTS_COLLAB_REDIS_PREFIX = "open-mercato:documents:collab";
|
|
85
|
+
const DOCUMENTS_COLLAB_REDIS_PREFIX_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._:-]{0,158}[A-Za-z0-9])?$/;
|
|
86
|
+
function resolveDocumentsCollabRedisPrefix(environment) {
|
|
87
|
+
const configured = environment.DOCUMENTS_COLLAB_REDIS_PREFIX?.trim() ?? "";
|
|
88
|
+
if (!configured && environment.NODE_ENV === "production") {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"[internal] DOCUMENTS_COLLAB_REDIS_PREFIX is required in production when collaboration Redis is configured"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const prefix = configured || DEFAULT_DOCUMENTS_COLLAB_REDIS_PREFIX;
|
|
94
|
+
if (prefix === LEGACY_DOCUMENTS_COLLAB_REDIS_PREFIX || !DOCUMENTS_COLLAB_REDIS_PREFIX_PATTERN.test(prefix)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
"[internal] DOCUMENTS_COLLAB_REDIS_PREFIX must be a deployment-scoped Redis key prefix using letters, numbers, dot, underscore, colon, or dash"
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return prefix;
|
|
100
|
+
}
|
|
101
|
+
function resolveDocumentsCollabRedisConfiguration(environment = process.env) {
|
|
102
|
+
const configured = environment.DOCUMENTS_COLLAB_REDIS_URL?.trim() || environment.REDIS_URL?.trim() || "";
|
|
103
|
+
if (!configured) return null;
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = new URL(configured);
|
|
107
|
+
} catch {
|
|
108
|
+
throw new Error("[internal] Documents collaboration Redis URL is invalid");
|
|
109
|
+
}
|
|
110
|
+
if (parsed.protocol !== "redis:" && parsed.protocol !== "rediss:" || !parsed.hostname) {
|
|
111
|
+
throw new Error("[internal] Documents collaboration Redis URL must use redis:// or rediss://");
|
|
112
|
+
}
|
|
113
|
+
const port = parsed.port ? Number(parsed.port) : 6379;
|
|
114
|
+
const databasePath = parsed.pathname.replace(/^\//, "");
|
|
115
|
+
const db = databasePath ? Number(databasePath) : void 0;
|
|
116
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
117
|
+
throw new Error("[internal] Documents collaboration Redis port is invalid");
|
|
118
|
+
}
|
|
119
|
+
if (db !== void 0 && (!Number.isInteger(db) || db < 0)) {
|
|
120
|
+
throw new Error("[internal] Documents collaboration Redis database is invalid");
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
host: parsed.hostname,
|
|
124
|
+
port,
|
|
125
|
+
prefix: resolveDocumentsCollabRedisPrefix(environment),
|
|
126
|
+
options: {
|
|
127
|
+
...parsed.username ? { username: decodeURIComponent(parsed.username) } : {},
|
|
128
|
+
...parsed.password ? { password: decodeURIComponent(parsed.password) } : {},
|
|
129
|
+
...db !== void 0 ? { db } : {},
|
|
130
|
+
...parsed.protocol === "rediss:" ? { tls: {} } : {},
|
|
131
|
+
maxRetriesPerRequest: null
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const DOCUMENTS_COLLAB_REDIS_FANOUT_COMMAND_TIMEOUT_MS = 1e3;
|
|
136
|
+
const DOCUMENTS_COLLAB_REDIS_FANOUT_RETRY_MIN_MS = 250;
|
|
137
|
+
const DOCUMENTS_COLLAB_REDIS_FANOUT_RETRY_MAX_MS = 5e3;
|
|
138
|
+
const DOCUMENTS_COLLAB_REDIS_FANOUT_SHUTDOWN_TIMEOUT_MS = 1500;
|
|
139
|
+
const DOCUMENTS_COLLAB_REDIS_LOCK_RELEASE_TIMEOUT_MS = 1250;
|
|
140
|
+
const DOCUMENTS_COLLAB_REDIS_SUBSCRIBE_TIMEOUT_MS = 5e3;
|
|
141
|
+
const DOCUMENTS_COLLAB_REDIS_FANOUT_MAX_PENDING_ROOMS = 256;
|
|
142
|
+
const DOCUMENTS_COLLAB_REDIS_FANOUT_MAX_PENDING_BYTES = DOCUMENTS_COLLAB_MAX_PAYLOAD_BYTES * 8;
|
|
143
|
+
const DOCUMENTS_COLLAB_REDIS_FANOUT_MAGIC = Buffer.from("OMDF1", "ascii");
|
|
144
|
+
const DOCUMENTS_COLLAB_REDIS_FANOUT_GENERATION_BYTES = 8;
|
|
145
|
+
function isHocuspocusStoreLockContention(error) {
|
|
146
|
+
return error instanceof Error && error.name === "SkipFurtherHooksError";
|
|
147
|
+
}
|
|
148
|
+
function isDocumentsCollabSourceStore(payload) {
|
|
149
|
+
const origin = payload.lastTransactionOrigin;
|
|
150
|
+
return !(origin && typeof origin === "object" && "source" in origin && origin.source === "redis");
|
|
151
|
+
}
|
|
152
|
+
function enforceDocumentsCollabSourceStoreOwnership(extension) {
|
|
153
|
+
const onStoreDocument = extension.onStoreDocument.bind(extension);
|
|
154
|
+
extension.onStoreDocument = async (payload) => {
|
|
155
|
+
if (!isDocumentsCollabSourceStore(payload)) return;
|
|
156
|
+
try {
|
|
157
|
+
return await onStoreDocument(payload);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (isHocuspocusStoreLockContention(error)) {
|
|
160
|
+
void payload.instance.storeDocumentHooks(payload.document, payload);
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
return extension;
|
|
166
|
+
}
|
|
167
|
+
function assertDocumentsCollabRedisAggregateUpdate(document, update) {
|
|
168
|
+
const candidate = new Y.Doc();
|
|
169
|
+
try {
|
|
170
|
+
Y.applyUpdate(candidate, Y.encodeStateAsUpdate(document));
|
|
171
|
+
Y.applyUpdate(candidate, update);
|
|
172
|
+
return assertDocumentsCollabRedisAggregateState(candidate);
|
|
173
|
+
} finally {
|
|
174
|
+
candidate.destroy();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function assertDocumentsCollabRedisAggregateState(document) {
|
|
178
|
+
const materialized = yDocToContent(document);
|
|
179
|
+
const yjsState = Y.encodeStateAsUpdate(document);
|
|
180
|
+
assertDocumentContentResourceLimits({
|
|
181
|
+
yjsState,
|
|
182
|
+
contentHtml: materialized?.html,
|
|
183
|
+
contentText: materialized?.text
|
|
184
|
+
});
|
|
185
|
+
return yjsState.byteLength;
|
|
186
|
+
}
|
|
187
|
+
class DocumentsCollabRedisExtension extends HocuspocusRedis {
|
|
188
|
+
constructor(configuration, options = {}) {
|
|
189
|
+
super(configuration);
|
|
190
|
+
this.persistedStates = /* @__PURE__ */ new WeakMap();
|
|
191
|
+
this.pendingFanouts = /* @__PURE__ */ new Map();
|
|
192
|
+
this.activeFanouts = /* @__PURE__ */ new Set();
|
|
193
|
+
this.fanoutRetryAttempts = /* @__PURE__ */ new Map();
|
|
194
|
+
this.fanoutRetryTimers = /* @__PURE__ */ new Map();
|
|
195
|
+
this.pendingLocalAfterStoreDelays = /* @__PURE__ */ new Map();
|
|
196
|
+
this.invalidatedFanoutDocuments = /* @__PURE__ */ new WeakSet();
|
|
197
|
+
this.bufferedFanouts = /* @__PURE__ */ new WeakMap();
|
|
198
|
+
this.bufferedFanoutBytes = /* @__PURE__ */ new WeakMap();
|
|
199
|
+
this.nextFanoutRevision = 0;
|
|
200
|
+
this.pendingFanoutBytes = 0;
|
|
201
|
+
this.fanoutDestroyed = false;
|
|
202
|
+
/**
|
|
203
|
+
* Hocuspocus does not publish a loading document in instance.documents until
|
|
204
|
+
* after afterLoadDocument completes. Keep the hook payload reachable so a
|
|
205
|
+
* Redis update received while the scoped database snapshot is loading can be
|
|
206
|
+
* applied to that same Y.Doc instead of being silently dropped.
|
|
207
|
+
*/
|
|
208
|
+
this.knownDocuments = /* @__PURE__ */ new Map();
|
|
209
|
+
/**
|
|
210
|
+
* EventEmitter never observes the promise an async listener returns, so a
|
|
211
|
+
* truncated frame, a receiver failure, or a failed reply publish would become
|
|
212
|
+
* an unhandled rejection and terminate the sidecar. Keep the registered
|
|
213
|
+
* listener synchronous and terminate every rejection here instead: one
|
|
214
|
+
* malformed frame must cost at most that frame.
|
|
215
|
+
*/
|
|
216
|
+
this.receiveDocumentsRedisMessage = (channel, data) => {
|
|
217
|
+
void this.handleDocumentsRedisMessage(channel, data).catch((error) => {
|
|
218
|
+
logger.warn("rejected malformed Redis collaboration frame", {
|
|
219
|
+
error: error instanceof Error ? error.message : String(error)
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
this.handleDocumentsRedisMessage = async (_channel, data) => {
|
|
224
|
+
const identifierLength = data[0];
|
|
225
|
+
if (identifierLength === void 0 || data.byteLength <= identifierLength + 1) return;
|
|
226
|
+
const identifier = data.toString("utf8", 1, identifierLength + 1);
|
|
227
|
+
if (identifier === this.configuration.identifier) return;
|
|
228
|
+
const redisPayload = data.subarray(identifierLength + 1);
|
|
229
|
+
let collaborationGeneration;
|
|
230
|
+
let messageBuffer = redisPayload;
|
|
231
|
+
if (redisPayload.byteLength >= DOCUMENTS_COLLAB_REDIS_FANOUT_MAGIC.byteLength + DOCUMENTS_COLLAB_REDIS_FANOUT_GENERATION_BYTES && redisPayload.subarray(0, DOCUMENTS_COLLAB_REDIS_FANOUT_MAGIC.byteLength).equals(DOCUMENTS_COLLAB_REDIS_FANOUT_MAGIC)) {
|
|
232
|
+
const generationOffset = DOCUMENTS_COLLAB_REDIS_FANOUT_MAGIC.byteLength;
|
|
233
|
+
const encodedGeneration = redisPayload.readBigUInt64BE(generationOffset);
|
|
234
|
+
if (encodedGeneration > BigInt(Number.MAX_SAFE_INTEGER) || encodedGeneration < 1n) return;
|
|
235
|
+
collaborationGeneration = Number(encodedGeneration);
|
|
236
|
+
messageBuffer = redisPayload.subarray(
|
|
237
|
+
generationOffset + DOCUMENTS_COLLAB_REDIS_FANOUT_GENERATION_BYTES
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const header = new HocuspocusIncomingMessage(messageBuffer);
|
|
241
|
+
const documentName = header.readVarString();
|
|
242
|
+
const document = this.instance.documents.get(documentName) ?? this.knownDocuments.get(documentName);
|
|
243
|
+
if (!document) return;
|
|
244
|
+
const messageType = header.readVarUint();
|
|
245
|
+
if (messageType === MessageType.Sync || messageType === MessageType.SyncReply) {
|
|
246
|
+
const syncType = header.readVarUint();
|
|
247
|
+
if (messageType !== MessageType.Sync || syncType !== 2) return;
|
|
248
|
+
if (collaborationGeneration === void 0) {
|
|
249
|
+
logger.warn("rejected unversioned Redis collaboration update", { room: documentName });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const update = header.readVarUint8Array();
|
|
253
|
+
this.applyDurableFanout(
|
|
254
|
+
identifier,
|
|
255
|
+
documentName,
|
|
256
|
+
document,
|
|
257
|
+
collaborationGeneration,
|
|
258
|
+
update
|
|
259
|
+
);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const receiverMessage = new HocuspocusIncomingMessage(messageBuffer);
|
|
263
|
+
receiverMessage.readVarString();
|
|
264
|
+
receiverMessage.writeVarString(documentName);
|
|
265
|
+
const receiver = new HocuspocusMessageReceiver(
|
|
266
|
+
receiverMessage,
|
|
267
|
+
this.redisTransactionOrigin
|
|
268
|
+
);
|
|
269
|
+
await receiver.apply(document, void 0, (reply) => {
|
|
270
|
+
void this.publishRedisMessage(documentName, reply).catch((error) => {
|
|
271
|
+
logger.warn("failed to publish Redis collaboration reply", {
|
|
272
|
+
room: documentName,
|
|
273
|
+
error: error instanceof Error ? error.message : String(error)
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
this.onRejectedAggregate = options.onRejectedAggregate;
|
|
279
|
+
this.onAcceptedAggregate = options.onAcceptedAggregate;
|
|
280
|
+
this.resolveCollaborationGeneration = options.resolveCollaborationGeneration;
|
|
281
|
+
this.fanoutPublisher = this.pub.duplicate({
|
|
282
|
+
autoResendUnfulfilledCommands: false,
|
|
283
|
+
commandTimeout: DOCUMENTS_COLLAB_REDIS_FANOUT_COMMAND_TIMEOUT_MS,
|
|
284
|
+
enableOfflineQueue: false,
|
|
285
|
+
maxRetriesPerRequest: 1
|
|
286
|
+
});
|
|
287
|
+
for (const listener of this.sub.listeners("messageBuffer")) {
|
|
288
|
+
this.sub.off("messageBuffer", listener);
|
|
289
|
+
}
|
|
290
|
+
this.sub.on("messageBuffer", this.receiveDocumentsRedisMessage);
|
|
291
|
+
}
|
|
292
|
+
markPersisted(document, yjsState, collaborationGeneration) {
|
|
293
|
+
if (this.invalidatedFanoutDocuments.has(document)) return;
|
|
294
|
+
this.persistedStates.set(document, {
|
|
295
|
+
collaborationGeneration,
|
|
296
|
+
state: Uint8Array.from(yjsState)
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async onLoadDocument(data) {
|
|
300
|
+
const { documentName, document } = data;
|
|
301
|
+
this.knownDocuments.set(documentName, document);
|
|
302
|
+
const loadingDocument = data.instance.loadingDocuments.get(documentName);
|
|
303
|
+
if (loadingDocument) {
|
|
304
|
+
void loadingDocument.catch(() => {
|
|
305
|
+
if (this.knownDocuments.get(documentName) === document && !data.instance.documents.has(documentName)) {
|
|
306
|
+
this.knownDocuments.delete(documentName);
|
|
307
|
+
void this.sub.unsubscribe(this.redisKey(documentName)).catch(() => void 0);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
await this.subscribeDocument(documentName);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (this.knownDocuments.get(documentName) === document) {
|
|
315
|
+
this.knownDocuments.delete(documentName);
|
|
316
|
+
}
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async afterLoadDocument({
|
|
321
|
+
documentName,
|
|
322
|
+
document
|
|
323
|
+
}) {
|
|
324
|
+
let aggregateByteLength;
|
|
325
|
+
try {
|
|
326
|
+
this.applyBufferedFanouts(documentName, document);
|
|
327
|
+
aggregateByteLength = assertDocumentsCollabRedisAggregateState(document);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
this.onRejectedAggregate?.(documentName, document);
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
this.onAcceptedAggregate?.(document, aggregateByteLength);
|
|
333
|
+
const awarenessQuery = new HocuspocusOutgoingMessage(documentName).writeQueryAwareness().toUint8Array();
|
|
334
|
+
await this.publishRedisMessage(documentName, awarenessQuery);
|
|
335
|
+
}
|
|
336
|
+
async afterUnloadDocument(data) {
|
|
337
|
+
if (data.instance.documents.has(data.documentName) || data.instance.loadingDocuments.has(data.documentName)) return;
|
|
338
|
+
this.knownDocuments.delete(data.documentName);
|
|
339
|
+
await super.afterUnloadDocument(data);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The subscriber client keeps ioredis' offline queue enabled, so a room load
|
|
343
|
+
* started during a Redis outage would otherwise wait for recovery with no
|
|
344
|
+
* deadline while its socket stays open. Fail the load instead: onLoadDocument
|
|
345
|
+
* releases the early subscription and Hocuspocus rejects the connection, so
|
|
346
|
+
* the client can retry rather than hanging against a green health check.
|
|
347
|
+
*/
|
|
348
|
+
async subscribeDocument(documentName) {
|
|
349
|
+
await new Promise((resolve2, reject) => {
|
|
350
|
+
const deadline = setTimeout(() => {
|
|
351
|
+
reject(new Error("[internal] documents collab: Redis room subscription timed out"));
|
|
352
|
+
}, DOCUMENTS_COLLAB_REDIS_SUBSCRIBE_TIMEOUT_MS);
|
|
353
|
+
deadline.unref?.();
|
|
354
|
+
this.sub.subscribe(this.redisKey(documentName), (error) => {
|
|
355
|
+
clearTimeout(deadline);
|
|
356
|
+
if (error) reject(error);
|
|
357
|
+
else resolve2();
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
async onChange(_data) {
|
|
362
|
+
}
|
|
363
|
+
async afterStoreDocument(data) {
|
|
364
|
+
const persistedState = this.persistedStates.get(data.document);
|
|
365
|
+
this.persistedStates.delete(data.document);
|
|
366
|
+
try {
|
|
367
|
+
await this.releaseStoreLock(data);
|
|
368
|
+
} finally {
|
|
369
|
+
if (persistedState && isDocumentsCollabSourceStore(data) && !this.invalidatedFanoutDocuments.has(data.document)) {
|
|
370
|
+
this.queuePersistedFanout(
|
|
371
|
+
data.documentName,
|
|
372
|
+
data.document,
|
|
373
|
+
persistedState.state,
|
|
374
|
+
persistedState.collaborationGeneration
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async releaseStoreLock(data) {
|
|
380
|
+
const lockKey = `${this.redisKey(data.documentName)}:lock`;
|
|
381
|
+
const lockState = this.locks.get(lockKey);
|
|
382
|
+
if (lockState) {
|
|
383
|
+
let timeout = null;
|
|
384
|
+
try {
|
|
385
|
+
const releasing = lockState.lock.release();
|
|
386
|
+
lockState.release = releasing;
|
|
387
|
+
const deadline = new Promise((resolve2) => {
|
|
388
|
+
timeout = setTimeout(
|
|
389
|
+
() => resolve2(false),
|
|
390
|
+
DOCUMENTS_COLLAB_REDIS_LOCK_RELEASE_TIMEOUT_MS
|
|
391
|
+
);
|
|
392
|
+
timeout.unref?.();
|
|
393
|
+
});
|
|
394
|
+
const released = await Promise.race([
|
|
395
|
+
releasing.then(
|
|
396
|
+
() => true,
|
|
397
|
+
() => true
|
|
398
|
+
),
|
|
399
|
+
deadline
|
|
400
|
+
]);
|
|
401
|
+
if (!released) {
|
|
402
|
+
void releasing.catch(() => void 0);
|
|
403
|
+
logger.warn("Redis collaboration store lock release timed out; continuing after TTL", {
|
|
404
|
+
room: data.documentName
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
} catch {
|
|
408
|
+
} finally {
|
|
409
|
+
if (timeout) clearTimeout(timeout);
|
|
410
|
+
if (this.locks.get(lockKey) === lockState) this.locks.delete(lockKey);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
await this.delayLocalAfterStore(data);
|
|
414
|
+
}
|
|
415
|
+
async delayLocalAfterStore(data) {
|
|
416
|
+
const origin = data.lastTransactionOrigin;
|
|
417
|
+
if (!origin || typeof origin !== "object" || !("source" in origin) || origin.source !== "local") return;
|
|
418
|
+
const previous = this.pendingLocalAfterStoreDelays.get(data.documentName);
|
|
419
|
+
if (previous) {
|
|
420
|
+
clearTimeout(previous.timeout);
|
|
421
|
+
this.pendingLocalAfterStoreDelays.delete(data.documentName);
|
|
422
|
+
previous.resolve();
|
|
423
|
+
}
|
|
424
|
+
await new Promise((resolve2) => {
|
|
425
|
+
const finish = () => {
|
|
426
|
+
const current = this.pendingLocalAfterStoreDelays.get(data.documentName);
|
|
427
|
+
if (current?.resolve === finish) {
|
|
428
|
+
this.pendingLocalAfterStoreDelays.delete(data.documentName);
|
|
429
|
+
}
|
|
430
|
+
resolve2();
|
|
431
|
+
};
|
|
432
|
+
const timeout = setTimeout(finish, this.configuration.disconnectDelay);
|
|
433
|
+
this.pendingLocalAfterStoreDelays.set(data.documentName, { timeout, resolve: finish });
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
discardPendingFanout(documentName, document) {
|
|
437
|
+
if (document) this.invalidatedFanoutDocuments.add(document);
|
|
438
|
+
const pending = this.pendingFanouts.get(documentName);
|
|
439
|
+
if (document && pending && pending.document !== document) return;
|
|
440
|
+
this.forgetPendingFanout(documentName);
|
|
441
|
+
}
|
|
442
|
+
removePendingFanoutEntry(documentName) {
|
|
443
|
+
const pending = this.pendingFanouts.get(documentName);
|
|
444
|
+
if (!pending) return;
|
|
445
|
+
this.pendingFanoutBytes -= pending.message.byteLength;
|
|
446
|
+
this.pendingFanouts.delete(documentName);
|
|
447
|
+
}
|
|
448
|
+
forgetPendingFanout(documentName) {
|
|
449
|
+
this.removePendingFanoutEntry(documentName);
|
|
450
|
+
this.fanoutRetryAttempts.delete(documentName);
|
|
451
|
+
const retryTimer = this.fanoutRetryTimers.get(documentName);
|
|
452
|
+
if (retryTimer) clearTimeout(retryTimer);
|
|
453
|
+
this.fanoutRetryTimers.delete(documentName);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* A pending fanout carries the room's complete post-store state and pins its
|
|
457
|
+
* Y.Doc, so an unreachable Redis would otherwise let one entry per edited
|
|
458
|
+
* document accumulate for the whole outage — long after those rooms unload.
|
|
459
|
+
* Bound the backlog by dropping the least recently refreshed rooms: their
|
|
460
|
+
* state is already durable, every later fanout for a room supersedes the one
|
|
461
|
+
* dropped here, and a peer that never sees the frame reloads authoritative
|
|
462
|
+
* content on its next load anyway.
|
|
463
|
+
*/
|
|
464
|
+
evictOverflowingFanouts(retainedDocumentName) {
|
|
465
|
+
for (const documentName of this.pendingFanouts.keys()) {
|
|
466
|
+
if (this.pendingFanouts.size <= DOCUMENTS_COLLAB_REDIS_FANOUT_MAX_PENDING_ROOMS && this.pendingFanoutBytes <= DOCUMENTS_COLLAB_REDIS_FANOUT_MAX_PENDING_BYTES) return;
|
|
467
|
+
if (documentName === retainedDocumentName) continue;
|
|
468
|
+
this.forgetPendingFanout(documentName);
|
|
469
|
+
logger.warn("dropped a stale pending Redis collaboration fanout to bound the outage backlog", {
|
|
470
|
+
room: documentName,
|
|
471
|
+
pendingRooms: this.pendingFanouts.size,
|
|
472
|
+
pendingBytes: this.pendingFanoutBytes
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
queuePersistedFanout(documentName, document, persistedState, collaborationGeneration) {
|
|
477
|
+
if (this.fanoutDestroyed) return;
|
|
478
|
+
const message = new HocuspocusOutgoingMessage(documentName).createSyncMessage().writeUpdate(persistedState).toUint8Array();
|
|
479
|
+
this.removePendingFanoutEntry(documentName);
|
|
480
|
+
this.pendingFanouts.set(documentName, {
|
|
481
|
+
document,
|
|
482
|
+
collaborationGeneration,
|
|
483
|
+
revision: ++this.nextFanoutRevision,
|
|
484
|
+
message
|
|
485
|
+
});
|
|
486
|
+
this.pendingFanoutBytes += message.byteLength;
|
|
487
|
+
this.evictOverflowingFanouts(documentName);
|
|
488
|
+
this.startPersistedFanout(documentName);
|
|
489
|
+
}
|
|
490
|
+
startPersistedFanout(documentName) {
|
|
491
|
+
if (this.fanoutDestroyed || this.activeFanouts.has(documentName) || this.fanoutRetryTimers.has(documentName) || !this.pendingFanouts.has(documentName)) return;
|
|
492
|
+
this.activeFanouts.add(documentName);
|
|
493
|
+
void this.drainPersistedFanout(documentName).finally(() => {
|
|
494
|
+
this.activeFanouts.delete(documentName);
|
|
495
|
+
if (!this.fanoutDestroyed && this.pendingFanouts.has(documentName) && !this.fanoutRetryTimers.has(documentName)) {
|
|
496
|
+
this.startPersistedFanout(documentName);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
async drainPersistedFanout(documentName) {
|
|
501
|
+
const pending = this.pendingFanouts.get(documentName);
|
|
502
|
+
if (!pending || this.fanoutDestroyed) return;
|
|
503
|
+
try {
|
|
504
|
+
await this.publishRedisMessage(
|
|
505
|
+
documentName,
|
|
506
|
+
pending.message,
|
|
507
|
+
this.fanoutPublisher,
|
|
508
|
+
pending.collaborationGeneration
|
|
509
|
+
);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (this.fanoutDestroyed || !this.pendingFanouts.has(documentName)) return;
|
|
512
|
+
const attempt = (this.fanoutRetryAttempts.get(documentName) ?? 0) + 1;
|
|
513
|
+
this.fanoutRetryAttempts.set(documentName, attempt);
|
|
514
|
+
const delay = Math.min(
|
|
515
|
+
DOCUMENTS_COLLAB_REDIS_FANOUT_RETRY_MAX_MS,
|
|
516
|
+
DOCUMENTS_COLLAB_REDIS_FANOUT_RETRY_MIN_MS * 2 ** Math.min(attempt - 1, 8)
|
|
517
|
+
);
|
|
518
|
+
logger.warn("durable Redis collaboration fanout failed; retrying latest state", {
|
|
519
|
+
room: documentName,
|
|
520
|
+
attempt,
|
|
521
|
+
error: error instanceof Error ? error.message : String(error)
|
|
522
|
+
});
|
|
523
|
+
const retryTimer = setTimeout(() => {
|
|
524
|
+
this.fanoutRetryTimers.delete(documentName);
|
|
525
|
+
this.startPersistedFanout(documentName);
|
|
526
|
+
}, delay);
|
|
527
|
+
retryTimer.unref?.();
|
|
528
|
+
this.fanoutRetryTimers.set(documentName, retryTimer);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
this.fanoutRetryAttempts.delete(documentName);
|
|
532
|
+
if (this.pendingFanouts.get(documentName)?.revision === pending.revision) {
|
|
533
|
+
this.removePendingFanoutEntry(documentName);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
redisKey(documentName) {
|
|
537
|
+
return `${this.configuration.prefix}:${documentName}`;
|
|
538
|
+
}
|
|
539
|
+
encodeRedisMessage(message) {
|
|
540
|
+
return Buffer.concat([this.messagePrefix, Buffer.from(message)]);
|
|
541
|
+
}
|
|
542
|
+
encodeDurableFanoutMessage(message, collaborationGeneration) {
|
|
543
|
+
if (!Number.isSafeInteger(collaborationGeneration) || collaborationGeneration < 1) {
|
|
544
|
+
throw new Error("[internal] documents collab: invalid Redis fanout generation");
|
|
545
|
+
}
|
|
546
|
+
const generation = Buffer.allocUnsafe(DOCUMENTS_COLLAB_REDIS_FANOUT_GENERATION_BYTES);
|
|
547
|
+
generation.writeBigUInt64BE(BigInt(collaborationGeneration));
|
|
548
|
+
return Buffer.concat([
|
|
549
|
+
this.messagePrefix,
|
|
550
|
+
DOCUMENTS_COLLAB_REDIS_FANOUT_MAGIC,
|
|
551
|
+
generation,
|
|
552
|
+
Buffer.from(message)
|
|
553
|
+
]);
|
|
554
|
+
}
|
|
555
|
+
async publishRedisMessage(documentName, message, publisher = this.pub, collaborationGeneration) {
|
|
556
|
+
return await publisher.publish(
|
|
557
|
+
this.redisKey(documentName),
|
|
558
|
+
collaborationGeneration === void 0 ? this.encodeRedisMessage(message) : this.encodeDurableFanoutMessage(message, collaborationGeneration)
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
bufferDurableFanout(sender, documentName, document, collaborationGeneration, update) {
|
|
562
|
+
const buffered = this.bufferedFanouts.get(document) ?? /* @__PURE__ */ new Map();
|
|
563
|
+
const previous = buffered.get(sender);
|
|
564
|
+
const bufferedBytes = (this.bufferedFanoutBytes.get(document) ?? 0) - (previous?.update.byteLength ?? 0) + update.byteLength;
|
|
565
|
+
if (!previous && buffered.size >= DOCUMENTS_COLLAB_MAX_PENDING_MESSAGES || bufferedBytes > DOCUMENTS_COLLAB_MAX_PENDING_BYTES) {
|
|
566
|
+
this.bufferedFanouts.delete(document);
|
|
567
|
+
this.bufferedFanoutBytes.delete(document);
|
|
568
|
+
this.onRejectedAggregate?.(documentName, document);
|
|
569
|
+
logger.warn("rejected buffered Redis collaboration updates that exceeded load limits", {
|
|
570
|
+
room: documentName
|
|
571
|
+
});
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
buffered.set(sender, {
|
|
575
|
+
collaborationGeneration,
|
|
576
|
+
update: Uint8Array.from(update)
|
|
577
|
+
});
|
|
578
|
+
this.bufferedFanouts.set(document, buffered);
|
|
579
|
+
this.bufferedFanoutBytes.set(document, bufferedBytes);
|
|
580
|
+
}
|
|
581
|
+
applyDurableFanout(sender, documentName, document, collaborationGeneration, update) {
|
|
582
|
+
const loadedGeneration = this.resolveCollaborationGeneration?.(document);
|
|
583
|
+
if (loadedGeneration === void 0) {
|
|
584
|
+
this.bufferDurableFanout(
|
|
585
|
+
sender,
|
|
586
|
+
documentName,
|
|
587
|
+
document,
|
|
588
|
+
collaborationGeneration,
|
|
589
|
+
update
|
|
590
|
+
);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (loadedGeneration !== collaborationGeneration) {
|
|
594
|
+
logger.warn("rejected stale Redis collaboration update from another generation", {
|
|
595
|
+
room: documentName,
|
|
596
|
+
expectedGeneration: loadedGeneration,
|
|
597
|
+
receivedGeneration: collaborationGeneration
|
|
598
|
+
});
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
let aggregateByteLength;
|
|
602
|
+
try {
|
|
603
|
+
aggregateByteLength = assertDocumentsCollabRedisAggregateUpdate(document, update);
|
|
604
|
+
} catch (error) {
|
|
605
|
+
if (isCrudHttpError(error) && error.status === 413) {
|
|
606
|
+
this.onRejectedAggregate?.(documentName, document);
|
|
607
|
+
logger.warn("rejected Redis collaboration update that exceeded room limits", {
|
|
608
|
+
room: documentName
|
|
609
|
+
});
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
logger.warn("rejected malformed Redis collaboration update", {
|
|
613
|
+
room: documentName,
|
|
614
|
+
error: error instanceof Error ? error.message : String(error)
|
|
615
|
+
});
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
this.onAcceptedAggregate?.(document, aggregateByteLength);
|
|
619
|
+
Y.applyUpdate(document, update, this.redisTransactionOrigin);
|
|
620
|
+
}
|
|
621
|
+
applyBufferedFanouts(documentName, document) {
|
|
622
|
+
const buffered = this.bufferedFanouts.get(document);
|
|
623
|
+
this.bufferedFanouts.delete(document);
|
|
624
|
+
this.bufferedFanoutBytes.delete(document);
|
|
625
|
+
if (!buffered) return;
|
|
626
|
+
const loadedGeneration = this.resolveCollaborationGeneration?.(document);
|
|
627
|
+
if (loadedGeneration === void 0) {
|
|
628
|
+
this.onRejectedAggregate?.(documentName, document);
|
|
629
|
+
throw new Error("[internal] documents collab: loaded room has no collaboration generation");
|
|
630
|
+
}
|
|
631
|
+
for (const [sender, fanout] of buffered) {
|
|
632
|
+
this.applyDurableFanout(
|
|
633
|
+
sender,
|
|
634
|
+
documentName,
|
|
635
|
+
document,
|
|
636
|
+
fanout.collaborationGeneration,
|
|
637
|
+
fanout.update
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
async flushPendingFanoutsBeforeDestroy() {
|
|
642
|
+
const pending = [...this.pendingFanouts.entries()];
|
|
643
|
+
if (pending.length === 0) return;
|
|
644
|
+
const attempts = Promise.allSettled(pending.map(async ([documentName, fanout]) => {
|
|
645
|
+
await this.publishRedisMessage(
|
|
646
|
+
documentName,
|
|
647
|
+
fanout.message,
|
|
648
|
+
this.fanoutPublisher,
|
|
649
|
+
fanout.collaborationGeneration
|
|
650
|
+
);
|
|
651
|
+
}));
|
|
652
|
+
let timeout = null;
|
|
653
|
+
const deadline = new Promise((resolve2) => {
|
|
654
|
+
timeout = setTimeout(() => resolve2(null), DOCUMENTS_COLLAB_REDIS_FANOUT_SHUTDOWN_TIMEOUT_MS);
|
|
655
|
+
timeout.unref?.();
|
|
656
|
+
});
|
|
657
|
+
const results = await Promise.race([attempts, deadline]);
|
|
658
|
+
if (timeout) clearTimeout(timeout);
|
|
659
|
+
if (results === null) {
|
|
660
|
+
logger.warn("durable Redis collaboration fanout did not drain before shutdown", {
|
|
661
|
+
pending: pending.length
|
|
662
|
+
});
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const failed = results.filter((result) => result.status === "rejected").length;
|
|
666
|
+
if (failed > 0) {
|
|
667
|
+
logger.warn("durable Redis collaboration fanout failed during shutdown", {
|
|
668
|
+
pending: pending.length,
|
|
669
|
+
failed
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
async onDestroy() {
|
|
674
|
+
this.fanoutDestroyed = true;
|
|
675
|
+
for (const retryTimer of this.fanoutRetryTimers.values()) clearTimeout(retryTimer);
|
|
676
|
+
this.fanoutRetryTimers.clear();
|
|
677
|
+
const pendingLocalDelays = [...this.pendingLocalAfterStoreDelays.values()];
|
|
678
|
+
this.pendingLocalAfterStoreDelays.clear();
|
|
679
|
+
for (const pending of pendingLocalDelays) {
|
|
680
|
+
clearTimeout(pending.timeout);
|
|
681
|
+
pending.resolve();
|
|
682
|
+
}
|
|
683
|
+
try {
|
|
684
|
+
await this.flushPendingFanoutsBeforeDestroy();
|
|
685
|
+
} finally {
|
|
686
|
+
this.pendingFanouts.clear();
|
|
687
|
+
this.pendingFanoutBytes = 0;
|
|
688
|
+
this.fanoutRetryAttempts.clear();
|
|
689
|
+
this.fanoutPublisher.disconnect(false);
|
|
690
|
+
this.pub.disconnect(false);
|
|
691
|
+
this.sub.disconnect(false);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function resolveDocumentsCollabRedisExtensions(environment, createRedisExtension) {
|
|
696
|
+
const configuration = resolveDocumentsCollabRedisConfiguration(environment);
|
|
697
|
+
if (!configuration) {
|
|
698
|
+
logger.warn(
|
|
699
|
+
"DOCUMENTS_COLLAB_REDIS_URL and REDIS_URL are unset; running in single-node mode. Multi-instance deployments require Redis for cross-instance document sync."
|
|
700
|
+
);
|
|
701
|
+
return [];
|
|
702
|
+
}
|
|
703
|
+
return [createRedisExtension(configuration)];
|
|
704
|
+
}
|
|
705
|
+
function collabAuthorizationStateKey(documentName, scope) {
|
|
706
|
+
return scope ? `${documentName}\0${scope.tenantId}\0${scope.organizationId}` : documentName;
|
|
707
|
+
}
|
|
708
|
+
function liveCollabConnectionCount(document) {
|
|
709
|
+
const getConnectionsCount = document.getConnectionsCount;
|
|
710
|
+
if (typeof getConnectionsCount !== "function") return null;
|
|
711
|
+
try {
|
|
712
|
+
const count = getConnectionsCount.call(document);
|
|
713
|
+
return typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : null;
|
|
714
|
+
} catch {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function captureCollabPendingMessageReadiness(document) {
|
|
719
|
+
const getConnections = document.getConnections;
|
|
720
|
+
if (typeof getConnections !== "function") return null;
|
|
721
|
+
let connections;
|
|
722
|
+
try {
|
|
723
|
+
connections = getConnections.call(document);
|
|
724
|
+
} catch {
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
if (!Array.isArray(connections) || connections.length === 0) return null;
|
|
728
|
+
const pending = connections.map((candidate) => {
|
|
729
|
+
const connection = candidate;
|
|
730
|
+
if (typeof connection?.waitForPendingMessages !== "function") {
|
|
731
|
+
return Promise.reject(new Error(
|
|
732
|
+
"[internal] documents collab: connection queue readiness is unavailable"
|
|
733
|
+
));
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
const readiness = connection.waitForPendingMessages();
|
|
737
|
+
if (!readiness || typeof readiness !== "object" && typeof readiness !== "function" || typeof readiness.then !== "function") {
|
|
738
|
+
return Promise.reject(new Error(
|
|
739
|
+
"[internal] documents collab: connection queue readiness is invalid"
|
|
740
|
+
));
|
|
741
|
+
}
|
|
742
|
+
return Promise.resolve(readiness);
|
|
743
|
+
} catch (error) {
|
|
744
|
+
return Promise.reject(error);
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
return Promise.all(pending).then(() => void 0);
|
|
748
|
+
}
|
|
749
|
+
function createCollabFinalDrainRegistry(resolveLiveConnections = liveCollabConnectionCount) {
|
|
750
|
+
const markedDocuments = /* @__PURE__ */ new WeakMap();
|
|
751
|
+
const authorizationStates = /* @__PURE__ */ new Map();
|
|
752
|
+
const releaseAuthorization = (ticket) => {
|
|
753
|
+
if (ticket.released) return;
|
|
754
|
+
ticket.released = true;
|
|
755
|
+
if (ticket.expiry) clearTimeout(ticket.expiry);
|
|
756
|
+
const state = authorizationStates.get(ticket.stateKey);
|
|
757
|
+
if (state !== ticket.state) return;
|
|
758
|
+
state.active = Math.max(0, state.active - 1);
|
|
759
|
+
if (state.active === 0) authorizationStates.delete(ticket.stateKey);
|
|
760
|
+
};
|
|
761
|
+
return {
|
|
762
|
+
mark(document, readiness) {
|
|
763
|
+
const guardedReadiness = readiness.then(
|
|
764
|
+
() => true,
|
|
765
|
+
() => false
|
|
766
|
+
);
|
|
767
|
+
if ((resolveLiveConnections(document) ?? 0) <= 0) return false;
|
|
768
|
+
if (markedDocuments.has(document)) return true;
|
|
769
|
+
markedDocuments.set(document, {
|
|
770
|
+
readiness: guardedReadiness
|
|
771
|
+
});
|
|
772
|
+
return true;
|
|
773
|
+
},
|
|
774
|
+
isMarked(document) {
|
|
775
|
+
return markedDocuments.has(document);
|
|
776
|
+
},
|
|
777
|
+
async consume(document) {
|
|
778
|
+
const state = markedDocuments.get(document);
|
|
779
|
+
if (!state) return "unmarked";
|
|
780
|
+
if (state.consuming || state.granted) return "busy";
|
|
781
|
+
const consuming = (async () => {
|
|
782
|
+
const ready = await state.readiness;
|
|
783
|
+
if (markedDocuments.get(document) !== state) return "unmarked";
|
|
784
|
+
if (!ready) return "failed";
|
|
785
|
+
if (resolveLiveConnections(document) !== 0) return "connected";
|
|
786
|
+
state.granted = true;
|
|
787
|
+
return "consumed";
|
|
788
|
+
})();
|
|
789
|
+
state.consuming = consuming;
|
|
790
|
+
return consuming;
|
|
791
|
+
},
|
|
792
|
+
complete(document) {
|
|
793
|
+
const state = markedDocuments.get(document);
|
|
794
|
+
if (state?.granted) state.completed = true;
|
|
795
|
+
},
|
|
796
|
+
discard(document) {
|
|
797
|
+
markedDocuments.delete(document);
|
|
798
|
+
},
|
|
799
|
+
beginAuthorization(documentName, scope) {
|
|
800
|
+
const stateKey = collabAuthorizationStateKey(documentName, scope);
|
|
801
|
+
let state = authorizationStates.get(stateKey);
|
|
802
|
+
if (!state) {
|
|
803
|
+
state = { active: 0, epoch: 0 };
|
|
804
|
+
authorizationStates.set(stateKey, state);
|
|
805
|
+
}
|
|
806
|
+
state.active += 1;
|
|
807
|
+
const ticket = {
|
|
808
|
+
documentName,
|
|
809
|
+
epoch: state.epoch,
|
|
810
|
+
released: false,
|
|
811
|
+
stateKey,
|
|
812
|
+
state
|
|
813
|
+
};
|
|
814
|
+
ticket.expiry = setTimeout(() => {
|
|
815
|
+
releaseAuthorization(ticket);
|
|
816
|
+
}, DOCUMENTS_COLLAB_AUTHORIZATION_TICKET_TIMEOUT_MS);
|
|
817
|
+
ticket.expiry.unref?.();
|
|
818
|
+
return ticket;
|
|
819
|
+
},
|
|
820
|
+
bumpAuthorization(documentName, scope) {
|
|
821
|
+
const state = authorizationStates.get(collabAuthorizationStateKey(documentName, scope));
|
|
822
|
+
if (state) state.epoch += 1;
|
|
823
|
+
},
|
|
824
|
+
isAuthorizationCurrent(ticket) {
|
|
825
|
+
const internalTicket = ticket;
|
|
826
|
+
const state = authorizationStates.get(internalTicket.stateKey);
|
|
827
|
+
return !internalTicket.released && state === ticket.state && state.epoch === ticket.epoch;
|
|
828
|
+
},
|
|
829
|
+
endAuthorization(ticket) {
|
|
830
|
+
releaseAuthorization(ticket);
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function markCollabFinalDrainForReauth(document, registry) {
|
|
835
|
+
const readiness = captureCollabPendingMessageReadiness(document);
|
|
836
|
+
if (!readiness) return false;
|
|
837
|
+
return registry.mark(document, readiness);
|
|
838
|
+
}
|
|
839
|
+
const collabIngressStates = /* @__PURE__ */ new WeakMap();
|
|
840
|
+
const boundedCollabConnections = /* @__PURE__ */ new WeakSet();
|
|
841
|
+
const guardedCollabConnectionPrototypes = /* @__PURE__ */ new WeakSet();
|
|
842
|
+
function closeCollabIngress(connection, state) {
|
|
843
|
+
if (state.blocked) return;
|
|
844
|
+
state.blocked = true;
|
|
845
|
+
try {
|
|
846
|
+
connection.close();
|
|
847
|
+
} catch {
|
|
848
|
+
}
|
|
849
|
+
try {
|
|
850
|
+
connection.webSocket.close(1009, "Collaboration ingress limit exceeded");
|
|
851
|
+
} catch {
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
function handleBoundedCollabIngress(connection, data, handleMessage, limits) {
|
|
855
|
+
const socketKey = connection.webSocket;
|
|
856
|
+
let state = collabIngressStates.get(socketKey);
|
|
857
|
+
if (!state) {
|
|
858
|
+
state = { blocked: false, pendingBytes: 0, pendingMessages: 0 };
|
|
859
|
+
collabIngressStates.set(socketKey, state);
|
|
860
|
+
}
|
|
861
|
+
if (state.blocked) return;
|
|
862
|
+
const pendingBytes = state.pendingBytes + data.byteLength;
|
|
863
|
+
const pendingMessages = state.pendingMessages + 1;
|
|
864
|
+
if (pendingBytes > limits.maxPendingBytes || pendingMessages > limits.maxPendingMessages) {
|
|
865
|
+
closeCollabIngress(connection, state);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
state.pendingBytes = pendingBytes;
|
|
869
|
+
state.pendingMessages = pendingMessages;
|
|
870
|
+
let released = false;
|
|
871
|
+
const release = () => {
|
|
872
|
+
if (released) return;
|
|
873
|
+
released = true;
|
|
874
|
+
state.pendingBytes = Math.max(0, state.pendingBytes - data.byteLength);
|
|
875
|
+
state.pendingMessages = Math.max(0, state.pendingMessages - 1);
|
|
876
|
+
};
|
|
877
|
+
try {
|
|
878
|
+
handleMessage(data);
|
|
879
|
+
void connection.waitForPendingMessages().then(release, release);
|
|
880
|
+
} catch (error) {
|
|
881
|
+
release();
|
|
882
|
+
throw error;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
function installBoundedCollabIngress(connection, limits = {
|
|
886
|
+
maxPendingBytes: DOCUMENTS_COLLAB_MAX_PENDING_BYTES,
|
|
887
|
+
maxPendingMessages: DOCUMENTS_COLLAB_MAX_PENDING_MESSAGES
|
|
888
|
+
}) {
|
|
889
|
+
const connectionKey = connection;
|
|
890
|
+
if (boundedCollabConnections.has(connectionKey)) return;
|
|
891
|
+
boundedCollabConnections.add(connectionKey);
|
|
892
|
+
const handleMessage = connection.handleMessage.bind(connection);
|
|
893
|
+
connection.handleMessage = (data) => {
|
|
894
|
+
handleBoundedCollabIngress(connection, data, handleMessage, limits);
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
function installHocuspocusCollabIngressGuard(ConnectionClass = HocuspocusConnection) {
|
|
898
|
+
const prototype = ConnectionClass.prototype;
|
|
899
|
+
if (guardedCollabConnectionPrototypes.has(prototype)) return;
|
|
900
|
+
guardedCollabConnectionPrototypes.add(prototype);
|
|
901
|
+
const handleMessage = prototype.handleMessage;
|
|
902
|
+
prototype.handleMessage = function guardedHandleMessage(data) {
|
|
903
|
+
if (boundedCollabConnections.has(this)) {
|
|
904
|
+
handleMessage.call(this, data);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
handleBoundedCollabIngress(
|
|
908
|
+
this,
|
|
909
|
+
data,
|
|
910
|
+
(input) => handleMessage.call(this, input),
|
|
911
|
+
{
|
|
912
|
+
maxPendingBytes: DOCUMENTS_COLLAB_MAX_PENDING_BYTES,
|
|
913
|
+
maxPendingMessages: DOCUMENTS_COLLAB_MAX_PENDING_MESSAGES
|
|
914
|
+
}
|
|
915
|
+
);
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function assertCollabInboundFramePolicy(update, options) {
|
|
919
|
+
if (update.byteLength > DOCUMENTS_COLLAB_MAX_PAYLOAD_BYTES) {
|
|
920
|
+
throw new Error("[internal] documents collab: frame exceeds transport limit");
|
|
921
|
+
}
|
|
922
|
+
let messageType;
|
|
923
|
+
try {
|
|
924
|
+
const message = new HocuspocusIncomingMessage(update);
|
|
925
|
+
message.readVarString();
|
|
926
|
+
messageType = message.readVarUint();
|
|
927
|
+
} catch {
|
|
928
|
+
throw new Error("[internal] documents collab: malformed frame envelope");
|
|
929
|
+
}
|
|
930
|
+
const isSync = messageType === MessageType.Sync || messageType === MessageType.SyncReply;
|
|
931
|
+
if (messageType === MessageType.Awareness && update.byteLength > DOCUMENTS_COLLAB_MAX_AWARENESS_FRAME_BYTES) {
|
|
932
|
+
throw new Error("[internal] documents collab: awareness frame exceeds limit");
|
|
933
|
+
}
|
|
934
|
+
if (!isSync && update.byteLength > DOCUMENTS_COLLAB_MAX_CONTROL_FRAME_BYTES) {
|
|
935
|
+
throw new Error("[internal] documents collab: control frame exceeds limit");
|
|
936
|
+
}
|
|
937
|
+
if (options.readOnly && isSync && update.byteLength > DOCUMENTS_COLLAB_MAX_READ_ONLY_SYNC_FRAME_BYTES) {
|
|
938
|
+
throw new Error("[internal] documents collab: read-only sync frame exceeds limit");
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const COLLAB_AUTHORIZATION_LIFECYCLE = /* @__PURE__ */ Symbol("documents.collab.authorization-lifecycle");
|
|
942
|
+
async function loadFreshCollabAcl(rbacService, userId, scope) {
|
|
943
|
+
if (typeof rbacService?.invalidateUserCache !== "function" || typeof rbacService.loadAcl !== "function") {
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
await rbacService.invalidateUserCache(userId);
|
|
947
|
+
return rbacService.loadAcl(userId, scope);
|
|
948
|
+
}
|
|
949
|
+
function readHeader(headers, name) {
|
|
950
|
+
const value = headers?.[name] ?? headers?.[name.toLowerCase()];
|
|
951
|
+
if (Array.isArray(value)) return value[0];
|
|
952
|
+
return value;
|
|
953
|
+
}
|
|
954
|
+
function normalizeTrustedOrigin(value) {
|
|
955
|
+
try {
|
|
956
|
+
const url = new URL(value);
|
|
957
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.origin : null;
|
|
958
|
+
} catch {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function resolveCollabAllowedOrigins(env) {
|
|
963
|
+
const candidates = [
|
|
964
|
+
...(env.DOCUMENTS_COLLAB_ALLOWED_ORIGINS ?? "").split(","),
|
|
965
|
+
env.APP_URL ?? "",
|
|
966
|
+
env.NEXT_PUBLIC_APP_URL ?? ""
|
|
967
|
+
];
|
|
968
|
+
return Array.from(new Set(candidates.map((candidate) => candidate.trim()).filter(Boolean).map(normalizeTrustedOrigin).filter((origin) => Boolean(origin))));
|
|
969
|
+
}
|
|
970
|
+
function isCollabRequestOriginAllowed(input) {
|
|
971
|
+
if (!input.origin) return !input.requireOrigin;
|
|
972
|
+
const origin = normalizeTrustedOrigin(input.origin);
|
|
973
|
+
if (!origin) return false;
|
|
974
|
+
const allowedOrigins = input.allowedOrigins ?? [];
|
|
975
|
+
if (allowedOrigins.length === 0) return !input.requireOrigin;
|
|
976
|
+
return allowedOrigins.includes(origin);
|
|
977
|
+
}
|
|
978
|
+
function assertScopedContext(context) {
|
|
979
|
+
if (!context?.tenantId || !context.organizationId) {
|
|
980
|
+
throw new Error("[internal] documents collab: missing tenant scope");
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function normalizeContentVersion(value) {
|
|
984
|
+
if (value instanceof Date) {
|
|
985
|
+
return Number.isFinite(value.getTime()) ? value.toISOString() : null;
|
|
986
|
+
}
|
|
987
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
988
|
+
const parsed = new Date(value);
|
|
989
|
+
return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : null;
|
|
990
|
+
}
|
|
991
|
+
function isReadOnlyTier(tier) {
|
|
992
|
+
return tier === "viewer" || tier === "commenter";
|
|
993
|
+
}
|
|
994
|
+
function toContext(claims, readOnly, exp) {
|
|
995
|
+
return {
|
|
996
|
+
userId: claims.userId,
|
|
997
|
+
tenantId: claims.tenantId,
|
|
998
|
+
organizationId: claims.organizationId,
|
|
999
|
+
documentId: claims.documentId,
|
|
1000
|
+
tier: claims.tier,
|
|
1001
|
+
readOnly,
|
|
1002
|
+
exp
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
function resolveCollabClaims(deps, token) {
|
|
1006
|
+
const v2Claims = deps.verifyTokenV2?.(token) ?? null;
|
|
1007
|
+
if (v2Claims) return toContext(v2Claims, v2Claims.readOnly, v2Claims.exp);
|
|
1008
|
+
const legacyClaims = deps.verifyToken?.(token) ?? null;
|
|
1009
|
+
if (!legacyClaims) return null;
|
|
1010
|
+
const exp = readLegacyTokenExpiration(token);
|
|
1011
|
+
return exp === null ? null : toContext(legacyClaims, isReadOnlyTier(legacyClaims.tier), exp);
|
|
1012
|
+
}
|
|
1013
|
+
function bindCollabAwarenessStates(context, states, ownership) {
|
|
1014
|
+
if (!context?.awarenessUser) {
|
|
1015
|
+
throw new Error("[internal] documents collab: awareness identity is not authenticated");
|
|
1016
|
+
}
|
|
1017
|
+
const admittedThisUpdate = /* @__PURE__ */ new Set();
|
|
1018
|
+
for (const [clientId, state] of states) {
|
|
1019
|
+
if (!state || typeof state !== "object" || Array.isArray(state) || Object.keys(state).length === 0) {
|
|
1020
|
+
states.delete(clientId);
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
if (!Number.isSafeInteger(clientId) || clientId < 0 || clientId > MAX_YJS_CLIENT_ID || !isBoundedAwarenessState(state)) {
|
|
1024
|
+
states.delete(clientId);
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if (ownership?.occupiedClientIds.has(clientId) && !ownership.ownedClientIds.has(clientId)) {
|
|
1028
|
+
states.delete(clientId);
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const roomOwner = ownership?.roomClientOwners?.get(clientId);
|
|
1032
|
+
if (roomOwner !== void 0 && roomOwner !== context.awarenessUser.id) {
|
|
1033
|
+
states.delete(clientId);
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
const knownToConnection = Boolean(
|
|
1037
|
+
ownership?.ownedClientIds.has(clientId) || ownership?.claimedClientIds?.has(clientId)
|
|
1038
|
+
);
|
|
1039
|
+
const connectionClientCount = (/* @__PURE__ */ new Set([
|
|
1040
|
+
...ownership?.ownedClientIds ?? [],
|
|
1041
|
+
...ownership?.claimedClientIds ?? [],
|
|
1042
|
+
...admittedThisUpdate
|
|
1043
|
+
])).size;
|
|
1044
|
+
if (!knownToConnection && connectionClientCount >= DOCUMENTS_COLLAB_MAX_AWARENESS_CLIENT_IDS_PER_CONNECTION) {
|
|
1045
|
+
states.delete(clientId);
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
if (roomOwner === void 0 && (ownership?.roomClientOwners?.size ?? 0) >= DOCUMENTS_COLLAB_MAX_AWARENESS_CLIENT_IDS_PER_ROOM) {
|
|
1049
|
+
states.delete(clientId);
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
const cursor = sanitizeAwarenessCursor(state.cursor);
|
|
1053
|
+
const canonicalState = {
|
|
1054
|
+
user: { ...context.awarenessUser }
|
|
1055
|
+
};
|
|
1056
|
+
if (cursor) canonicalState.cursor = cursor;
|
|
1057
|
+
states.set(clientId, canonicalState);
|
|
1058
|
+
admittedThisUpdate.add(clientId);
|
|
1059
|
+
ownership?.claimedClientIds?.add(clientId);
|
|
1060
|
+
if (roomOwner === void 0) {
|
|
1061
|
+
ownership?.roomClientOwners?.set(clientId, context.awarenessUser.id);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
function isBoundedAwarenessState(state) {
|
|
1066
|
+
try {
|
|
1067
|
+
const serialized = JSON.stringify(state);
|
|
1068
|
+
return typeof serialized === "string" && Buffer.byteLength(serialized, "utf8") <= DOCUMENTS_COLLAB_MAX_AWARENESS_STATE_BYTES;
|
|
1069
|
+
} catch {
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function sanitizeAwarenessId(value) {
|
|
1074
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1075
|
+
const record = value;
|
|
1076
|
+
if (!Number.isSafeInteger(record.client) || record.client < 0 || record.client > MAX_YJS_CLIENT_ID || !Number.isSafeInteger(record.clock) || record.clock < 0) {
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
return { client: record.client, clock: record.clock };
|
|
1080
|
+
}
|
|
1081
|
+
function sanitizeAwarenessPosition(value) {
|
|
1082
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1083
|
+
const record = value;
|
|
1084
|
+
const assoc = record.assoc === void 0 ? 0 : record.assoc;
|
|
1085
|
+
if (!Number.isSafeInteger(assoc) || assoc < -1 || assoc > 1) {
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
const item = sanitizeAwarenessId(record.item);
|
|
1089
|
+
if (item) return { item, assoc };
|
|
1090
|
+
if (typeof record.tname === "string" && record.tname.length > 0 && record.tname.length <= 120) {
|
|
1091
|
+
return { tname: record.tname, assoc };
|
|
1092
|
+
}
|
|
1093
|
+
const type = sanitizeAwarenessId(record.type);
|
|
1094
|
+
return type ? { type, assoc } : null;
|
|
1095
|
+
}
|
|
1096
|
+
function sanitizeAwarenessCursor(value) {
|
|
1097
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1098
|
+
const record = value;
|
|
1099
|
+
const anchor = sanitizeAwarenessPosition(record.anchor);
|
|
1100
|
+
const head = sanitizeAwarenessPosition(record.head);
|
|
1101
|
+
return anchor && head ? { anchor, head } : null;
|
|
1102
|
+
}
|
|
1103
|
+
function readLegacyTokenExpiration(token) {
|
|
1104
|
+
const encodedPayload = token.split(".")[1];
|
|
1105
|
+
if (!encodedPayload) return null;
|
|
1106
|
+
try {
|
|
1107
|
+
const payload = JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8"));
|
|
1108
|
+
if (!payload || typeof payload !== "object") return null;
|
|
1109
|
+
const { exp, iat } = payload;
|
|
1110
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1111
|
+
if (typeof exp !== "number" || !Number.isInteger(exp) || typeof iat !== "number" || !Number.isInteger(iat) || exp <= now || exp <= iat || exp - iat > COLLAB_TOKEN_TTL_SECONDS || iat > now + COLLAB_TOKEN_CLOCK_SKEW_SECONDS || exp > now + COLLAB_TOKEN_TTL_SECONDS + COLLAB_TOKEN_CLOCK_SKEW_SECONDS) {
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
return exp;
|
|
1115
|
+
} catch {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function scheduleCollabConnectionExpiry(connection, exp) {
|
|
1120
|
+
let timer = null;
|
|
1121
|
+
const clearExpiry = () => {
|
|
1122
|
+
if (timer === null) return;
|
|
1123
|
+
clearTimeout(timer);
|
|
1124
|
+
timer = null;
|
|
1125
|
+
};
|
|
1126
|
+
timer = setTimeout(() => {
|
|
1127
|
+
timer = null;
|
|
1128
|
+
connection.close();
|
|
1129
|
+
}, Math.max(0, exp * 1e3 - Date.now()));
|
|
1130
|
+
connection.onClose(clearExpiry);
|
|
1131
|
+
return clearExpiry;
|
|
1132
|
+
}
|
|
1133
|
+
function scheduleCollabConnectionReauthorization(connection, authorize, intervalMs = DOCUMENTS_COLLAB_ACTIVE_REAUTHORIZATION_INTERVAL_MS) {
|
|
1134
|
+
let stopped = false;
|
|
1135
|
+
let timer = null;
|
|
1136
|
+
const stop = () => {
|
|
1137
|
+
stopped = true;
|
|
1138
|
+
if (timer === null) return;
|
|
1139
|
+
clearTimeout(timer);
|
|
1140
|
+
timer = null;
|
|
1141
|
+
};
|
|
1142
|
+
const schedule = () => {
|
|
1143
|
+
if (stopped) return;
|
|
1144
|
+
timer = setTimeout(() => {
|
|
1145
|
+
timer = null;
|
|
1146
|
+
void (async () => {
|
|
1147
|
+
let authorized = false;
|
|
1148
|
+
try {
|
|
1149
|
+
authorized = await authorize();
|
|
1150
|
+
} catch {
|
|
1151
|
+
authorized = false;
|
|
1152
|
+
}
|
|
1153
|
+
if (stopped) return;
|
|
1154
|
+
if (!authorized) {
|
|
1155
|
+
stop();
|
|
1156
|
+
connection.close();
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
schedule();
|
|
1160
|
+
})();
|
|
1161
|
+
}, Math.max(1, intervalMs));
|
|
1162
|
+
timer.unref?.();
|
|
1163
|
+
};
|
|
1164
|
+
connection.onClose(stop);
|
|
1165
|
+
schedule();
|
|
1166
|
+
return stop;
|
|
1167
|
+
}
|
|
1168
|
+
function handleCollabHealthRequest(request, response) {
|
|
1169
|
+
const pathname = new URL(request.url ?? "/", "http://documents-collab.local").pathname;
|
|
1170
|
+
if (pathname !== "/healthz") return false;
|
|
1171
|
+
response.setHeader("Cache-Control", "no-store");
|
|
1172
|
+
if (request.method !== "GET") {
|
|
1173
|
+
response.statusCode = 405;
|
|
1174
|
+
response.setHeader("Allow", "GET");
|
|
1175
|
+
response.end();
|
|
1176
|
+
return true;
|
|
1177
|
+
}
|
|
1178
|
+
const ready = isCollabTokenV2Ready();
|
|
1179
|
+
response.statusCode = ready ? 200 : 503;
|
|
1180
|
+
response.setHeader("Content-Type", "application/json");
|
|
1181
|
+
response.end(JSON.stringify({
|
|
1182
|
+
status: ready ? "ok" : "unavailable",
|
|
1183
|
+
service: "documents-collab",
|
|
1184
|
+
capabilityTokenVersion: 2
|
|
1185
|
+
}));
|
|
1186
|
+
return true;
|
|
1187
|
+
}
|
|
1188
|
+
async function handleCollabServerRequest(input) {
|
|
1189
|
+
if (handleCollabHealthRequest(input.request, input.response)) {
|
|
1190
|
+
return Promise.reject();
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
async function initializeDocumentYjsState(em, documentId, scope) {
|
|
1194
|
+
return em.transactional(async (transactionalEm) => {
|
|
1195
|
+
const document = await findOneWithDecryption(
|
|
1196
|
+
transactionalEm,
|
|
1197
|
+
Document,
|
|
1198
|
+
{
|
|
1199
|
+
id: documentId,
|
|
1200
|
+
tenantId: scope.tenantId,
|
|
1201
|
+
organizationId: scope.organizationId,
|
|
1202
|
+
deletedAt: null
|
|
1203
|
+
},
|
|
1204
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
1205
|
+
scope
|
|
1206
|
+
);
|
|
1207
|
+
if (!document) return null;
|
|
1208
|
+
let content = await findOneWithDecryption(
|
|
1209
|
+
transactionalEm,
|
|
1210
|
+
DocumentContent,
|
|
1211
|
+
{
|
|
1212
|
+
documentId,
|
|
1213
|
+
tenantId: scope.tenantId,
|
|
1214
|
+
organizationId: scope.organizationId
|
|
1215
|
+
},
|
|
1216
|
+
{ filters: false, lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
1217
|
+
scope
|
|
1218
|
+
);
|
|
1219
|
+
if (!content) {
|
|
1220
|
+
content = transactionalEm.create(DocumentContent, {
|
|
1221
|
+
id: randomUUID(),
|
|
1222
|
+
documentId,
|
|
1223
|
+
tenantId: scope.tenantId,
|
|
1224
|
+
organizationId: scope.organizationId,
|
|
1225
|
+
contentHtml: "",
|
|
1226
|
+
contentText: "",
|
|
1227
|
+
yjsState: null,
|
|
1228
|
+
collaborationGeneration: 1,
|
|
1229
|
+
deletedAt: null,
|
|
1230
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1231
|
+
});
|
|
1232
|
+
transactionalEm.persist(content);
|
|
1233
|
+
await transactionalEm.flush();
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
if (content.deletedAt) {
|
|
1237
|
+
const now = /* @__PURE__ */ new Date();
|
|
1238
|
+
content.yjsState = null;
|
|
1239
|
+
content.contentHtml = "";
|
|
1240
|
+
content.contentText = "";
|
|
1241
|
+
content.deletedAt = null;
|
|
1242
|
+
advanceDocumentCollaborationGeneration(content);
|
|
1243
|
+
content.updatedAt = now.getTime() > content.updatedAt.getTime() ? now : new Date(content.updatedAt.getTime() + 1);
|
|
1244
|
+
await transactionalEm.flush();
|
|
1245
|
+
}
|
|
1246
|
+
if (content.yjsState && content.yjsState.length > 0) {
|
|
1247
|
+
assertDocumentContentResourceLimits({
|
|
1248
|
+
yjsState: content.yjsState,
|
|
1249
|
+
contentHtml: content.contentHtml,
|
|
1250
|
+
contentText: content.contentText
|
|
1251
|
+
});
|
|
1252
|
+
return Buffer.from(content.yjsState);
|
|
1253
|
+
}
|
|
1254
|
+
if (!content.contentHtml) return null;
|
|
1255
|
+
assertDocumentContentResourceLimits({
|
|
1256
|
+
contentHtml: content.contentHtml,
|
|
1257
|
+
contentText: content.contentText
|
|
1258
|
+
});
|
|
1259
|
+
const materialized = htmlToYDoc(content.contentHtml);
|
|
1260
|
+
const yjsState = Buffer.from(Y.encodeStateAsUpdate(materialized));
|
|
1261
|
+
assertDocumentContentResourceLimits({ yjsState, contentHtml: content.contentHtml });
|
|
1262
|
+
await transactionalEm.nativeUpdate(
|
|
1263
|
+
DocumentContent,
|
|
1264
|
+
{
|
|
1265
|
+
id: content.id,
|
|
1266
|
+
documentId,
|
|
1267
|
+
tenantId: scope.tenantId,
|
|
1268
|
+
organizationId: scope.organizationId,
|
|
1269
|
+
deletedAt: null
|
|
1270
|
+
},
|
|
1271
|
+
{ yjsState }
|
|
1272
|
+
);
|
|
1273
|
+
return yjsState;
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
function isCollabAuthorizationCurrent(context, snapshot) {
|
|
1277
|
+
if (!hasResolvedDocumentsOrganizationAccess(
|
|
1278
|
+
snapshot,
|
|
1279
|
+
context.organizationId,
|
|
1280
|
+
snapshot.organizationScope
|
|
1281
|
+
)) {
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
const features = snapshot.isSuperAdmin ? ["*"] : snapshot.features;
|
|
1285
|
+
const managerOverride = hasAllFeatures(["documents.manage"], features);
|
|
1286
|
+
const relationshipTier = managerOverride ? "owner" : snapshot.relationshipTier;
|
|
1287
|
+
const capabilities = deriveDocumentCapabilities({
|
|
1288
|
+
relationshipTier,
|
|
1289
|
+
managerOverride,
|
|
1290
|
+
archived: snapshot.archived === true,
|
|
1291
|
+
userFeatures: features
|
|
1292
|
+
});
|
|
1293
|
+
return capabilities.canView && relationshipTier === context.tier && context.readOnly === !capabilities.canEdit;
|
|
1294
|
+
}
|
|
1295
|
+
async function authorizeCollabContext(container, context) {
|
|
1296
|
+
try {
|
|
1297
|
+
const scope = {
|
|
1298
|
+
tenantId: context.tenantId,
|
|
1299
|
+
organizationId: context.organizationId
|
|
1300
|
+
};
|
|
1301
|
+
const em = container.resolve("em");
|
|
1302
|
+
const rbacService = container.resolve("rbacService");
|
|
1303
|
+
let acl = await loadFreshCollabAcl(rbacService, context.userId, scope);
|
|
1304
|
+
if (!acl) return false;
|
|
1305
|
+
let organizationScope = null;
|
|
1306
|
+
if (!hasResolvedDocumentsOrganizationAccess(acl, context.organizationId)) {
|
|
1307
|
+
const organizationScopeService = resolveOrganizationScopeService(container);
|
|
1308
|
+
if (!organizationScopeService) return false;
|
|
1309
|
+
const freshAuthorization = await organizationScopeService.resolveFresh({
|
|
1310
|
+
auth: {
|
|
1311
|
+
sub: context.userId,
|
|
1312
|
+
userId: context.userId,
|
|
1313
|
+
tenantId: context.tenantId,
|
|
1314
|
+
orgId: context.organizationId,
|
|
1315
|
+
isSuperAdmin: false
|
|
1316
|
+
},
|
|
1317
|
+
selectedId: context.organizationId,
|
|
1318
|
+
tenantId: context.tenantId
|
|
1319
|
+
});
|
|
1320
|
+
organizationScope = freshAuthorization.scope;
|
|
1321
|
+
acl = freshAuthorization.acl;
|
|
1322
|
+
if (!hasResolvedDocumentsOrganizationAccess(
|
|
1323
|
+
acl,
|
|
1324
|
+
context.organizationId,
|
|
1325
|
+
organizationScope
|
|
1326
|
+
)) {
|
|
1327
|
+
return false;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
const features = acl.isSuperAdmin ? ["*"] : acl.features;
|
|
1331
|
+
const managerOverride = hasAllFeatures(["documents.manage"], features);
|
|
1332
|
+
const relationshipTier = managerOverride ? "owner" : await resolveUserAccess(
|
|
1333
|
+
em,
|
|
1334
|
+
context.documentId,
|
|
1335
|
+
scope,
|
|
1336
|
+
context.userId,
|
|
1337
|
+
container
|
|
1338
|
+
);
|
|
1339
|
+
const documentRow = await findOneWithDecryption(
|
|
1340
|
+
em,
|
|
1341
|
+
Document,
|
|
1342
|
+
{ id: context.documentId, tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
1343
|
+
{ fields: ["id", "archivedAt", "deletedAt"], filters: false },
|
|
1344
|
+
scope
|
|
1345
|
+
);
|
|
1346
|
+
if (!documentRow || documentRow.deletedAt) return false;
|
|
1347
|
+
return isCollabAuthorizationCurrent(context, {
|
|
1348
|
+
relationshipTier,
|
|
1349
|
+
isSuperAdmin: acl.isSuperAdmin,
|
|
1350
|
+
features: acl.features,
|
|
1351
|
+
organizations: acl.organizations,
|
|
1352
|
+
organizationScope,
|
|
1353
|
+
archived: documentRow?.archivedAt != null
|
|
1354
|
+
});
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
logger.warn("collab authorization check failed; failing closed", {
|
|
1357
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1358
|
+
});
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
function createCollabHooks(deps) {
|
|
1363
|
+
const materializationWarningRooms = /* @__PURE__ */ new Set();
|
|
1364
|
+
const loadedContentVersions = /* @__PURE__ */ new WeakMap();
|
|
1365
|
+
const loadedCollaborationGenerations = /* @__PURE__ */ new WeakMap();
|
|
1366
|
+
const roomResourceBudgets = /* @__PURE__ */ new WeakMap();
|
|
1367
|
+
const roomScopes = /* @__PURE__ */ new WeakMap();
|
|
1368
|
+
const awarenessConnectionClientIds = /* @__PURE__ */ new WeakMap();
|
|
1369
|
+
const awarenessRoomClientOwners = /* @__PURE__ */ new WeakMap();
|
|
1370
|
+
const roomWritableContexts = /* @__PURE__ */ new WeakMap();
|
|
1371
|
+
const rememberWritableContext = (document, context) => {
|
|
1372
|
+
if (!document || !context || context.readOnly) return;
|
|
1373
|
+
roomWritableContexts.set(document, context);
|
|
1374
|
+
};
|
|
1375
|
+
const isInvalidated = (documentName, document) => deps.isRoomInvalidated?.(documentName, document) ?? (document ? deps.isRoomClosing?.(documentName, document) : false) ?? false;
|
|
1376
|
+
const isFinalDrainPending = (documentName) => {
|
|
1377
|
+
const room = deps.resolveRoomDocument?.(documentName);
|
|
1378
|
+
return Boolean(room && deps.finalDrainRegistry?.isMarked(room));
|
|
1379
|
+
};
|
|
1380
|
+
const assertRoomAcceptsAuthentication = (documentName) => {
|
|
1381
|
+
if (isInvalidated(documentName)) {
|
|
1382
|
+
throw new Error("[internal] documents collab: room is reloading authoritative content");
|
|
1383
|
+
}
|
|
1384
|
+
if (isFinalDrainPending(documentName)) {
|
|
1385
|
+
throw new Error("[internal] documents collab: room is draining accepted edits");
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
const invalidateStoreRoom = (data) => {
|
|
1389
|
+
deps.finalDrainRegistry?.discard(data.document);
|
|
1390
|
+
deps.invalidateRoom?.(data.documentName, data.document);
|
|
1391
|
+
};
|
|
1392
|
+
const retireFailedStore = (data, ownsGrant = false) => {
|
|
1393
|
+
const invalidated = isInvalidated(data.documentName, data.document);
|
|
1394
|
+
if (!ownsGrant && !invalidated && !deps.finalDrainRegistry?.isMarked(data.document)) return false;
|
|
1395
|
+
if (invalidated) {
|
|
1396
|
+
deps.finalDrainRegistry?.discard(data.document);
|
|
1397
|
+
} else {
|
|
1398
|
+
invalidateStoreRoom(data);
|
|
1399
|
+
}
|
|
1400
|
+
logger.error(invalidated ? "invalidated store failed; retiring in-memory room" : "final drain failed; retiring in-memory room");
|
|
1401
|
+
return true;
|
|
1402
|
+
};
|
|
1403
|
+
const assertConnectionAuthorization = (context) => {
|
|
1404
|
+
if (!deps.finalDrainRegistry) return;
|
|
1405
|
+
const lifecycle = context[COLLAB_AUTHORIZATION_LIFECYCLE];
|
|
1406
|
+
if (lifecycle?.established) return;
|
|
1407
|
+
if (!lifecycle?.ticket || !deps.finalDrainRegistry.isAuthorizationCurrent(lifecycle.ticket)) {
|
|
1408
|
+
throw new Error("[internal] documents collab: access changed during connection setup");
|
|
1409
|
+
}
|
|
1410
|
+
assertRoomAcceptsAuthentication(context.documentId);
|
|
1411
|
+
};
|
|
1412
|
+
const establishConnectionAuthorization = (context) => {
|
|
1413
|
+
if (!deps.finalDrainRegistry) return;
|
|
1414
|
+
assertConnectionAuthorization(context);
|
|
1415
|
+
const lifecycle = context[COLLAB_AUTHORIZATION_LIFECYCLE];
|
|
1416
|
+
if (!lifecycle) return;
|
|
1417
|
+
if (lifecycle.ticket) deps.finalDrainRegistry.endAuthorization(lifecycle.ticket);
|
|
1418
|
+
lifecycle.ticket = void 0;
|
|
1419
|
+
lifecycle.established = true;
|
|
1420
|
+
};
|
|
1421
|
+
const releaseConnectionAuthorization = (context) => {
|
|
1422
|
+
if (!deps.finalDrainRegistry) return;
|
|
1423
|
+
const ticketedContext = context;
|
|
1424
|
+
const lifecycle = ticketedContext[COLLAB_AUTHORIZATION_LIFECYCLE];
|
|
1425
|
+
if (lifecycle?.ticket) deps.finalDrainRegistry.endAuthorization(lifecycle.ticket);
|
|
1426
|
+
delete ticketedContext[COLLAB_AUTHORIZATION_LIFECYCLE];
|
|
1427
|
+
};
|
|
1428
|
+
const authorizeStoreAttempt = async (data) => {
|
|
1429
|
+
let finalDrain = "unmarked";
|
|
1430
|
+
if (deps.finalDrainRegistry?.isMarked(data.document)) {
|
|
1431
|
+
finalDrain = await deps.finalDrainRegistry.consume(data.document);
|
|
1432
|
+
if (finalDrain === "busy") {
|
|
1433
|
+
return "denied";
|
|
1434
|
+
}
|
|
1435
|
+
if (finalDrain === "connected" || finalDrain === "failed") {
|
|
1436
|
+
invalidateStoreRoom(data);
|
|
1437
|
+
return "denied";
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
let authorized;
|
|
1441
|
+
try {
|
|
1442
|
+
authorized = await deps.authorizeContext(data.context);
|
|
1443
|
+
} catch {
|
|
1444
|
+
invalidateStoreRoom(data);
|
|
1445
|
+
logger.error("store authorization failed; retiring in-memory room");
|
|
1446
|
+
return "denied";
|
|
1447
|
+
}
|
|
1448
|
+
if (isInvalidated(data.documentName, data.document)) {
|
|
1449
|
+
deps.finalDrainRegistry?.discard(data.document);
|
|
1450
|
+
return "denied";
|
|
1451
|
+
}
|
|
1452
|
+
if (finalDrain === "unmarked") {
|
|
1453
|
+
finalDrain = await (deps.finalDrainRegistry?.consume(data.document) ?? Promise.resolve("unmarked"));
|
|
1454
|
+
}
|
|
1455
|
+
if (finalDrain === "busy") return "denied";
|
|
1456
|
+
if (finalDrain === "connected" || finalDrain === "failed") {
|
|
1457
|
+
invalidateStoreRoom(data);
|
|
1458
|
+
return "denied";
|
|
1459
|
+
}
|
|
1460
|
+
if (finalDrain === "consumed") return "final-drain";
|
|
1461
|
+
if (authorized) return "normal";
|
|
1462
|
+
invalidateStoreRoom(data);
|
|
1463
|
+
return "denied";
|
|
1464
|
+
};
|
|
1465
|
+
const reauthorizeActiveConnection = async (context) => {
|
|
1466
|
+
if (isInvalidated(context.documentId) || isFinalDrainPending(context.documentId)) {
|
|
1467
|
+
return false;
|
|
1468
|
+
}
|
|
1469
|
+
let authorized = false;
|
|
1470
|
+
try {
|
|
1471
|
+
authorized = await deps.authorizeContext(context);
|
|
1472
|
+
} catch {
|
|
1473
|
+
authorized = false;
|
|
1474
|
+
}
|
|
1475
|
+
if (isInvalidated(context.documentId) || isFinalDrainPending(context.documentId)) {
|
|
1476
|
+
return false;
|
|
1477
|
+
}
|
|
1478
|
+
if (authorized) {
|
|
1479
|
+
const resolveRoomDocument = deps.resolveRoomDocument;
|
|
1480
|
+
if (!resolveRoomDocument) return true;
|
|
1481
|
+
const room2 = resolveRoomDocument(context.documentId);
|
|
1482
|
+
if (!room2) return false;
|
|
1483
|
+
const loadedGeneration = loadedCollaborationGenerations.get(room2);
|
|
1484
|
+
let durableGeneration = null;
|
|
1485
|
+
try {
|
|
1486
|
+
const container = await deps.resolveContainer();
|
|
1487
|
+
const em = container.resolve("em");
|
|
1488
|
+
const scope = {
|
|
1489
|
+
tenantId: context.tenantId,
|
|
1490
|
+
organizationId: context.organizationId
|
|
1491
|
+
};
|
|
1492
|
+
if (deps.loadCollaborationGeneration) {
|
|
1493
|
+
durableGeneration = await deps.loadCollaborationGeneration(
|
|
1494
|
+
em,
|
|
1495
|
+
context.documentId,
|
|
1496
|
+
scope
|
|
1497
|
+
);
|
|
1498
|
+
} else {
|
|
1499
|
+
const content = await deps.loadContent(em, context.documentId, scope);
|
|
1500
|
+
durableGeneration = normalizeDocumentCollaborationGeneration(
|
|
1501
|
+
content?.collaborationGeneration
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
} catch {
|
|
1505
|
+
durableGeneration = null;
|
|
1506
|
+
}
|
|
1507
|
+
if (isInvalidated(context.documentId) || isFinalDrainPending(context.documentId)) {
|
|
1508
|
+
return false;
|
|
1509
|
+
}
|
|
1510
|
+
if (resolveRoomDocument(context.documentId) !== room2) return false;
|
|
1511
|
+
if (loadedGeneration !== void 0 && durableGeneration !== null && durableGeneration === loadedGeneration) {
|
|
1512
|
+
rememberWritableContext(room2, context);
|
|
1513
|
+
return true;
|
|
1514
|
+
}
|
|
1515
|
+
invalidateStoreRoom({
|
|
1516
|
+
documentName: context.documentId,
|
|
1517
|
+
document: room2
|
|
1518
|
+
});
|
|
1519
|
+
return false;
|
|
1520
|
+
}
|
|
1521
|
+
const room = deps.resolveRoomDocument?.(context.documentId);
|
|
1522
|
+
if (room) {
|
|
1523
|
+
invalidateStoreRoom({
|
|
1524
|
+
documentName: context.documentId,
|
|
1525
|
+
document: room
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
return false;
|
|
1529
|
+
};
|
|
1530
|
+
return {
|
|
1531
|
+
assertConnectionAuthorization,
|
|
1532
|
+
establishConnectionAuthorization,
|
|
1533
|
+
releaseConnectionAuthorization,
|
|
1534
|
+
reauthorizeActiveConnection,
|
|
1535
|
+
recordRedisAggregate(document, byteLength) {
|
|
1536
|
+
const previous = roomResourceBudgets.get(document);
|
|
1537
|
+
roomResourceBudgets.set(document, {
|
|
1538
|
+
bytes: byteLength,
|
|
1539
|
+
revision: (previous?.revision ?? 0) + 1
|
|
1540
|
+
});
|
|
1541
|
+
},
|
|
1542
|
+
resolveCollaborationGeneration(document) {
|
|
1543
|
+
return loadedCollaborationGenerations.get(document);
|
|
1544
|
+
},
|
|
1545
|
+
resolveRoomScope(document) {
|
|
1546
|
+
return roomScopes.get(document) ?? null;
|
|
1547
|
+
},
|
|
1548
|
+
async onAuthenticate(data) {
|
|
1549
|
+
if (!isCollabRequestOriginAllowed({
|
|
1550
|
+
origin: readHeader(data.requestHeaders, "origin"),
|
|
1551
|
+
allowedOrigins: deps.allowedOrigins,
|
|
1552
|
+
requireOrigin: deps.requireOrigin ?? process.env.NODE_ENV === "production"
|
|
1553
|
+
})) {
|
|
1554
|
+
throw new Error("[internal] documents collab: origin not allowed");
|
|
1555
|
+
}
|
|
1556
|
+
const context = resolveCollabClaims(deps, data.token ?? "");
|
|
1557
|
+
if (!context) {
|
|
1558
|
+
throw new Error("[internal] documents collab: invalid token");
|
|
1559
|
+
}
|
|
1560
|
+
if (context.documentId !== data.documentName) {
|
|
1561
|
+
throw new Error("[internal] documents collab: room mismatch");
|
|
1562
|
+
}
|
|
1563
|
+
const authorizationTicket = deps.finalDrainRegistry?.beginAuthorization(data.documentName, {
|
|
1564
|
+
tenantId: context.tenantId,
|
|
1565
|
+
organizationId: context.organizationId
|
|
1566
|
+
});
|
|
1567
|
+
let retainAuthorizationTicket = false;
|
|
1568
|
+
try {
|
|
1569
|
+
assertRoomAcceptsAuthentication(data.documentName);
|
|
1570
|
+
if (!await deps.authorizeContext(context)) {
|
|
1571
|
+
throw new Error("[internal] documents collab: stale or revoked token");
|
|
1572
|
+
}
|
|
1573
|
+
const awarenessName = await deps.resolveAwarenessName?.(context);
|
|
1574
|
+
if (!await deps.authorizeContext(context)) {
|
|
1575
|
+
throw new Error("[internal] documents collab: stale or revoked token");
|
|
1576
|
+
}
|
|
1577
|
+
if (authorizationTicket && !deps.finalDrainRegistry?.isAuthorizationCurrent(authorizationTicket)) {
|
|
1578
|
+
throw new Error("[internal] documents collab: access changed during authentication");
|
|
1579
|
+
}
|
|
1580
|
+
assertRoomAcceptsAuthentication(data.documentName);
|
|
1581
|
+
context.awarenessUser = createCanonicalCollaborationAwarenessUser(
|
|
1582
|
+
context.userId,
|
|
1583
|
+
awarenessName
|
|
1584
|
+
);
|
|
1585
|
+
data.connection.readOnly = context.readOnly;
|
|
1586
|
+
if (authorizationTicket) {
|
|
1587
|
+
context[COLLAB_AUTHORIZATION_LIFECYCLE] = {
|
|
1588
|
+
established: false,
|
|
1589
|
+
ticket: authorizationTicket
|
|
1590
|
+
};
|
|
1591
|
+
retainAuthorizationTicket = true;
|
|
1592
|
+
}
|
|
1593
|
+
return context;
|
|
1594
|
+
} finally {
|
|
1595
|
+
if (authorizationTicket && !retainAuthorizationTicket) {
|
|
1596
|
+
deps.finalDrainRegistry?.endAuthorization(authorizationTicket);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
},
|
|
1600
|
+
async beforeHandleAwareness(data) {
|
|
1601
|
+
let claimedClientIds;
|
|
1602
|
+
if (data.connection) {
|
|
1603
|
+
claimedClientIds = awarenessConnectionClientIds.get(data.connection);
|
|
1604
|
+
if (!claimedClientIds) {
|
|
1605
|
+
claimedClientIds = /* @__PURE__ */ new Set();
|
|
1606
|
+
awarenessConnectionClientIds.set(data.connection, claimedClientIds);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
let roomClientOwners;
|
|
1610
|
+
if (data.document) {
|
|
1611
|
+
roomClientOwners = awarenessRoomClientOwners.get(data.document);
|
|
1612
|
+
if (!roomClientOwners) {
|
|
1613
|
+
roomClientOwners = /* @__PURE__ */ new Map();
|
|
1614
|
+
awarenessRoomClientOwners.set(data.document, roomClientOwners);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
bindCollabAwarenessStates(
|
|
1618
|
+
data.context,
|
|
1619
|
+
data.states,
|
|
1620
|
+
data.ownedClientIds && data.occupiedClientIds ? {
|
|
1621
|
+
ownedClientIds: data.ownedClientIds,
|
|
1622
|
+
occupiedClientIds: data.occupiedClientIds,
|
|
1623
|
+
claimedClientIds,
|
|
1624
|
+
roomClientOwners
|
|
1625
|
+
} : void 0
|
|
1626
|
+
);
|
|
1627
|
+
},
|
|
1628
|
+
async onLoadDocument(data) {
|
|
1629
|
+
assertScopedContext(data.context);
|
|
1630
|
+
roomScopes.set(data.document, {
|
|
1631
|
+
tenantId: data.context.tenantId,
|
|
1632
|
+
organizationId: data.context.organizationId
|
|
1633
|
+
});
|
|
1634
|
+
rememberWritableContext(data.document, data.context);
|
|
1635
|
+
const container = await deps.resolveContainer();
|
|
1636
|
+
const em = container.resolve("em");
|
|
1637
|
+
const scope = {
|
|
1638
|
+
tenantId: data.context.tenantId,
|
|
1639
|
+
organizationId: data.context.organizationId
|
|
1640
|
+
};
|
|
1641
|
+
let content = await deps.loadContent(em, data.documentName, scope);
|
|
1642
|
+
if (!content) {
|
|
1643
|
+
await deps.initializeYjsState(em, data.documentName, scope);
|
|
1644
|
+
content = await deps.loadContent(em, data.documentName, scope);
|
|
1645
|
+
}
|
|
1646
|
+
const loadedVersion = normalizeContentVersion(content?.updatedAt);
|
|
1647
|
+
if (!loadedVersion) {
|
|
1648
|
+
throw new Error("[internal] documents collab: content row has no durable version");
|
|
1649
|
+
}
|
|
1650
|
+
if (content?.yjsState && content.yjsState.length > 0) {
|
|
1651
|
+
assertDocumentContentResourceLimits({
|
|
1652
|
+
yjsState: content.yjsState,
|
|
1653
|
+
contentHtml: content.contentHtml
|
|
1654
|
+
});
|
|
1655
|
+
Y.applyUpdate(data.document, new Uint8Array(content.yjsState));
|
|
1656
|
+
roomResourceBudgets.set(data.document, {
|
|
1657
|
+
bytes: content.yjsState.byteLength,
|
|
1658
|
+
revision: 0
|
|
1659
|
+
});
|
|
1660
|
+
} else if (content?.contentHtml) {
|
|
1661
|
+
assertDocumentContentResourceLimits({ contentHtml: content.contentHtml });
|
|
1662
|
+
const initializedState = await deps.initializeYjsState(
|
|
1663
|
+
em,
|
|
1664
|
+
data.documentName,
|
|
1665
|
+
scope
|
|
1666
|
+
);
|
|
1667
|
+
if (initializedState && initializedState.length > 0) {
|
|
1668
|
+
Y.applyUpdate(data.document, new Uint8Array(initializedState));
|
|
1669
|
+
roomResourceBudgets.set(data.document, {
|
|
1670
|
+
bytes: initializedState.byteLength,
|
|
1671
|
+
revision: 0
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
if (!roomResourceBudgets.has(data.document)) {
|
|
1676
|
+
roomResourceBudgets.set(data.document, { bytes: 0, revision: 0 });
|
|
1677
|
+
}
|
|
1678
|
+
const collaborationGeneration = normalizeDocumentCollaborationGeneration(
|
|
1679
|
+
content?.collaborationGeneration
|
|
1680
|
+
);
|
|
1681
|
+
if (collaborationGeneration === null) {
|
|
1682
|
+
throw new Error("[internal] documents collab: content row has no durable collaboration generation");
|
|
1683
|
+
}
|
|
1684
|
+
loadedContentVersions.set(data.document, loadedVersion);
|
|
1685
|
+
loadedCollaborationGenerations.set(data.document, collaborationGeneration);
|
|
1686
|
+
return data.document;
|
|
1687
|
+
},
|
|
1688
|
+
async beforeSync(data) {
|
|
1689
|
+
if (data.connection.readOnly || data.type !== 1 && data.type !== 2) return;
|
|
1690
|
+
let authorized = false;
|
|
1691
|
+
if (data.context && !isInvalidated(data.context.documentId, data.document)) {
|
|
1692
|
+
try {
|
|
1693
|
+
authorized = await deps.authorizeContext(data.context);
|
|
1694
|
+
} catch {
|
|
1695
|
+
authorized = false;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (!authorized || !data.context || isInvalidated(data.context.documentId, data.document) || isFinalDrainPending(data.context.documentId)) {
|
|
1699
|
+
data.connection.close?.();
|
|
1700
|
+
if (data.context) {
|
|
1701
|
+
invalidateStoreRoom({
|
|
1702
|
+
documentName: data.context.documentId,
|
|
1703
|
+
document: data.document
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
throw new Error("[internal] documents collab: write authorization is no longer current");
|
|
1707
|
+
}
|
|
1708
|
+
rememberWritableContext(data.document, data.context);
|
|
1709
|
+
const budget = roomResourceBudgets.get(data.document) ?? { bytes: 0, revision: 0 };
|
|
1710
|
+
const nextBytes = budget.bytes + data.payload.byteLength;
|
|
1711
|
+
assertDocumentYjsStateByteLength(nextBytes);
|
|
1712
|
+
budget.bytes = nextBytes;
|
|
1713
|
+
budget.revision += 1;
|
|
1714
|
+
roomResourceBudgets.set(data.document, budget);
|
|
1715
|
+
},
|
|
1716
|
+
async onStoreDocument(data) {
|
|
1717
|
+
try {
|
|
1718
|
+
assertScopedContext(data.context);
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
if (retireFailedStore(data)) return;
|
|
1721
|
+
throw error;
|
|
1722
|
+
}
|
|
1723
|
+
if (isInvalidated(data.documentName, data.document)) return;
|
|
1724
|
+
if (data.context.readOnly) {
|
|
1725
|
+
const writableContext = roomWritableContexts.get(data.document);
|
|
1726
|
+
if (!writableContext) {
|
|
1727
|
+
retireFailedStore(data);
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
data = { ...data, context: writableContext };
|
|
1731
|
+
} else {
|
|
1732
|
+
rememberWritableContext(data.document, data.context);
|
|
1733
|
+
}
|
|
1734
|
+
let expectedUpdatedAt = loadedContentVersions.get(data.document);
|
|
1735
|
+
let collaborationGeneration = loadedCollaborationGenerations.get(data.document);
|
|
1736
|
+
if (!expectedUpdatedAt || collaborationGeneration === void 0) {
|
|
1737
|
+
const error = new Error("[internal] documents collab: room store has no loaded content version");
|
|
1738
|
+
if (retireFailedStore(data)) return;
|
|
1739
|
+
throw error;
|
|
1740
|
+
}
|
|
1741
|
+
let em;
|
|
1742
|
+
let searchIndexer = null;
|
|
1743
|
+
try {
|
|
1744
|
+
const container = await deps.resolveContainer();
|
|
1745
|
+
em = container.resolve("em");
|
|
1746
|
+
try {
|
|
1747
|
+
searchIndexer = container.resolve("searchIndexer");
|
|
1748
|
+
} catch {
|
|
1749
|
+
searchIndexer = null;
|
|
1750
|
+
}
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
if (retireFailedStore(data)) return;
|
|
1753
|
+
throw error;
|
|
1754
|
+
}
|
|
1755
|
+
const scope = {
|
|
1756
|
+
tenantId: data.context.tenantId,
|
|
1757
|
+
organizationId: data.context.organizationId
|
|
1758
|
+
};
|
|
1759
|
+
let finalDrainAuthorized = false;
|
|
1760
|
+
try {
|
|
1761
|
+
for (let attempt = 0; attempt < MAX_COLLAB_STORE_ATTEMPTS; attempt += 1) {
|
|
1762
|
+
if (isInvalidated(data.documentName, data.document)) {
|
|
1763
|
+
if (finalDrainAuthorized) deps.finalDrainRegistry?.discard(data.document);
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
if (!finalDrainAuthorized) {
|
|
1767
|
+
const authorization = await authorizeStoreAttempt(data);
|
|
1768
|
+
if (authorization === "denied") return;
|
|
1769
|
+
finalDrainAuthorized = authorization === "final-drain";
|
|
1770
|
+
}
|
|
1771
|
+
if (isInvalidated(data.documentName, data.document)) {
|
|
1772
|
+
if (finalDrainAuthorized) deps.finalDrainRegistry?.discard(data.document);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
const materialized = yDocToContent(data.document);
|
|
1776
|
+
if (materialized) materializationWarningRooms.delete(data.documentName);
|
|
1777
|
+
if (!materialized && !materializationWarningRooms.has(data.documentName)) {
|
|
1778
|
+
materializationWarningRooms.add(data.documentName);
|
|
1779
|
+
logger.warn("materialization failed; preserving previous html/text", { room: data.documentName });
|
|
1780
|
+
}
|
|
1781
|
+
const yjsState = Buffer.from(Y.encodeStateAsUpdate(data.document));
|
|
1782
|
+
const resourceBudgetRevision = roomResourceBudgets.get(data.document)?.revision ?? 0;
|
|
1783
|
+
assertDocumentContentResourceLimits({
|
|
1784
|
+
yjsState,
|
|
1785
|
+
contentHtml: materialized?.html,
|
|
1786
|
+
contentText: materialized?.text
|
|
1787
|
+
});
|
|
1788
|
+
let persisted;
|
|
1789
|
+
try {
|
|
1790
|
+
persisted = await deps.persistContent(
|
|
1791
|
+
em,
|
|
1792
|
+
data.documentName,
|
|
1793
|
+
scope,
|
|
1794
|
+
materialized ? {
|
|
1795
|
+
yjsState,
|
|
1796
|
+
contentHtml: materialized.html,
|
|
1797
|
+
contentText: materialized.text
|
|
1798
|
+
} : { yjsState },
|
|
1799
|
+
{
|
|
1800
|
+
searchIndexer,
|
|
1801
|
+
expectedUpdatedAt,
|
|
1802
|
+
expectedCollaborationGeneration: collaborationGeneration,
|
|
1803
|
+
requireExpectedVersion: true
|
|
1804
|
+
}
|
|
1805
|
+
);
|
|
1806
|
+
} catch (error) {
|
|
1807
|
+
if (isInvalidated(data.documentName, data.document)) {
|
|
1808
|
+
if (finalDrainAuthorized) deps.finalDrainRegistry?.discard(data.document);
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
const isVersionConflict = isCrudHttpError(error) && error.status === 409 && error.body.code === OPTIMISTIC_LOCK_CONFLICT_CODE;
|
|
1812
|
+
if (!isVersionConflict) throw error;
|
|
1813
|
+
const authoritative = await deps.loadContent(em, data.documentName, scope);
|
|
1814
|
+
const authoritativeVersion = normalizeContentVersion(authoritative?.updatedAt);
|
|
1815
|
+
if (!authoritativeVersion) {
|
|
1816
|
+
invalidateStoreRoom(data);
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
const authoritativeGeneration = normalizeDocumentCollaborationGeneration(
|
|
1820
|
+
authoritative?.collaborationGeneration
|
|
1821
|
+
);
|
|
1822
|
+
if (authoritativeGeneration === null) {
|
|
1823
|
+
invalidateStoreRoom(data);
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
const authoritativeDocument = authoritative?.yjsState?.length ? new Y.Doc() : htmlToYDoc(authoritative?.contentHtml ?? "");
|
|
1827
|
+
if (authoritative?.yjsState?.length) {
|
|
1828
|
+
assertDocumentContentResourceLimits({
|
|
1829
|
+
yjsState: authoritative.yjsState,
|
|
1830
|
+
contentHtml: authoritative.contentHtml
|
|
1831
|
+
});
|
|
1832
|
+
Y.applyUpdate(authoritativeDocument, new Uint8Array(authoritative.yjsState));
|
|
1833
|
+
}
|
|
1834
|
+
if (authoritativeGeneration !== collaborationGeneration || isInvalidated(data.documentName, data.document)) {
|
|
1835
|
+
invalidateStoreRoom(data);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
Y.applyUpdate(
|
|
1839
|
+
data.document,
|
|
1840
|
+
Y.encodeStateAsUpdate(authoritativeDocument),
|
|
1841
|
+
{
|
|
1842
|
+
source: "local",
|
|
1843
|
+
skipStoreHooks: true,
|
|
1844
|
+
context: data.context
|
|
1845
|
+
}
|
|
1846
|
+
);
|
|
1847
|
+
expectedUpdatedAt = authoritativeVersion;
|
|
1848
|
+
collaborationGeneration = authoritativeGeneration;
|
|
1849
|
+
loadedContentVersions.set(data.document, authoritativeVersion);
|
|
1850
|
+
loadedCollaborationGenerations.set(data.document, authoritativeGeneration);
|
|
1851
|
+
if (attempt + 1 >= MAX_COLLAB_STORE_ATTEMPTS) throw error;
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
if (isInvalidated(data.documentName, data.document)) {
|
|
1855
|
+
if (finalDrainAuthorized) deps.finalDrainRegistry?.discard(data.document);
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
const persistedVersion = normalizeContentVersion(persisted.updatedAt);
|
|
1859
|
+
if (!persistedVersion) {
|
|
1860
|
+
throw new Error("[internal] documents collab: content store returned no durable version");
|
|
1861
|
+
}
|
|
1862
|
+
if (persisted.collaborationGeneration !== collaborationGeneration) {
|
|
1863
|
+
invalidateStoreRoom(data);
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
loadedContentVersions.set(data.document, persistedVersion);
|
|
1867
|
+
loadedCollaborationGenerations.set(data.document, collaborationGeneration);
|
|
1868
|
+
const currentBudget = roomResourceBudgets.get(data.document);
|
|
1869
|
+
if (!currentBudget || currentBudget.revision === resourceBudgetRevision) {
|
|
1870
|
+
roomResourceBudgets.set(data.document, {
|
|
1871
|
+
bytes: yjsState.byteLength,
|
|
1872
|
+
revision: resourceBudgetRevision
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
deps.onPersisted?.(data.document, yjsState, collaborationGeneration);
|
|
1876
|
+
if (finalDrainAuthorized) deps.finalDrainRegistry?.complete(data.document);
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
if (isCrudHttpError(error) && error.status === 413) {
|
|
1881
|
+
invalidateStoreRoom(data);
|
|
1882
|
+
logger.warn("retiring collaboration room that exceeded content limits", {
|
|
1883
|
+
room: data.documentName
|
|
1884
|
+
});
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
if (retireFailedStore(data, finalDrainAuthorized)) return;
|
|
1888
|
+
throw error;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
function headersToRecord(headers) {
|
|
1894
|
+
return { origin: headers.get("origin") ?? void 0 };
|
|
1895
|
+
}
|
|
1896
|
+
function eventDocumentId(payload) {
|
|
1897
|
+
if (!payload || typeof payload !== "object") return null;
|
|
1898
|
+
const record = payload;
|
|
1899
|
+
const id = record.documentId ?? record.id;
|
|
1900
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
1901
|
+
}
|
|
1902
|
+
function closeCollabRoomConnectionsForContentReset(registry, documentName) {
|
|
1903
|
+
const room = registry.documents.get(documentName);
|
|
1904
|
+
if (!room) return 0;
|
|
1905
|
+
let closed = 0;
|
|
1906
|
+
room.connections.forEach((_clients, connection) => {
|
|
1907
|
+
connection.close(COLLAB_CONTENT_RESET_CLOSE_EVENT);
|
|
1908
|
+
closed += 1;
|
|
1909
|
+
});
|
|
1910
|
+
return closed;
|
|
1911
|
+
}
|
|
1912
|
+
function resolveCollabRoomEventAction(event, payload) {
|
|
1913
|
+
if (event === "documents.document.shared" || event === "documents.document.unshared" || event === "documents.document.archived" || event === "documents.document.unarchived") {
|
|
1914
|
+
return "reauth";
|
|
1915
|
+
}
|
|
1916
|
+
const record = payload && typeof payload === "object" ? payload : null;
|
|
1917
|
+
if (event === "documents.document.deleted" || event === "documents.version.restored" || event === "documents.document.updated" && record?.contentEpochReset === true) {
|
|
1918
|
+
return "invalidate";
|
|
1919
|
+
}
|
|
1920
|
+
return "ignore";
|
|
1921
|
+
}
|
|
1922
|
+
function nonEmptyString(value) {
|
|
1923
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
1924
|
+
}
|
|
1925
|
+
function resolveTrustedDocumentsCrossProcessEvent(envelope) {
|
|
1926
|
+
if (envelope.options?.emitterModuleId !== "documents") return null;
|
|
1927
|
+
const tenantId = nonEmptyString(envelope.options?.tenantId);
|
|
1928
|
+
const organizationId = nonEmptyString(envelope.options?.organizationId);
|
|
1929
|
+
if (!tenantId || !organizationId) return null;
|
|
1930
|
+
const action = resolveCollabRoomEventAction(envelope.event, envelope.payload);
|
|
1931
|
+
if (action === "ignore") return null;
|
|
1932
|
+
const documentId = eventDocumentId(envelope.payload);
|
|
1933
|
+
if (!documentId) return null;
|
|
1934
|
+
return {
|
|
1935
|
+
action,
|
|
1936
|
+
documentId,
|
|
1937
|
+
scope: { tenantId, organizationId }
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
function isTrustedDocumentsCollabRoomScope(event, roomScope) {
|
|
1941
|
+
return roomScope?.tenantId === event.scope.tenantId && roomScope.organizationId === event.scope.organizationId;
|
|
1942
|
+
}
|
|
1943
|
+
function isOwnDocumentsCrossProcessEvent(envelope, ownInstanceId) {
|
|
1944
|
+
return envelope.originInstanceId === ownInstanceId;
|
|
1945
|
+
}
|
|
1946
|
+
function isMainModule() {
|
|
1947
|
+
const entry = process.argv[1];
|
|
1948
|
+
return Boolean(entry && import.meta.url === pathToFileURL(resolve(entry)).href);
|
|
1949
|
+
}
|
|
1950
|
+
async function main() {
|
|
1951
|
+
const [
|
|
1952
|
+
{ Connection, Server },
|
|
1953
|
+
{ bootstrapFromAppRoot },
|
|
1954
|
+
{ createRequestContainer },
|
|
1955
|
+
{ registerCrossProcessEventListener, CROSS_PROCESS_EVENT_INSTANCE_ID }
|
|
1956
|
+
] = await Promise.all([
|
|
1957
|
+
import("@hocuspocus/server"),
|
|
1958
|
+
import("@open-mercato/shared/lib/bootstrap/dynamicLoader"),
|
|
1959
|
+
import("@open-mercato/shared/lib/di/container"),
|
|
1960
|
+
import("@open-mercato/events")
|
|
1961
|
+
]);
|
|
1962
|
+
installHocuspocusCollabIngressGuard(Connection);
|
|
1963
|
+
const appRoot = process.env.DOCUMENTS_COLLAB_APP_ROOT || void 0;
|
|
1964
|
+
await bootstrapFromAppRoot(appRoot);
|
|
1965
|
+
const port = Number(process.env.DOCUMENTS_COLLAB_PORT || 4101);
|
|
1966
|
+
const allowedOrigins = resolveCollabAllowedOrigins(process.env);
|
|
1967
|
+
const invalidatedRoomDocuments = /* @__PURE__ */ new WeakSet();
|
|
1968
|
+
const finalDrainRegistry = createCollabFinalDrainRegistry();
|
|
1969
|
+
let server = null;
|
|
1970
|
+
let redisStoreExtension = null;
|
|
1971
|
+
const closeRoomConnectionsForContentReset = (documentName) => {
|
|
1972
|
+
if (server) closeCollabRoomConnectionsForContentReset(server.hocuspocus, documentName);
|
|
1973
|
+
};
|
|
1974
|
+
const invalidateRoom = (documentName, document) => {
|
|
1975
|
+
redisStoreExtension?.discardPendingFanout(documentName, document);
|
|
1976
|
+
finalDrainRegistry.discard(document);
|
|
1977
|
+
invalidatedRoomDocuments.add(document);
|
|
1978
|
+
closeRoomConnectionsForContentReset(documentName);
|
|
1979
|
+
};
|
|
1980
|
+
const legacyTokenVerifier = resolveLegacyCollabTokenVerifier(process.env);
|
|
1981
|
+
const hooks = createCollabHooks({
|
|
1982
|
+
...legacyTokenVerifier ? { verifyToken: legacyTokenVerifier } : {},
|
|
1983
|
+
verifyTokenV2: verifyCollabTokenV2,
|
|
1984
|
+
authorizeContext: async (context) => {
|
|
1985
|
+
const container = await createRequestContainer();
|
|
1986
|
+
return authorizeCollabContext(container, context);
|
|
1987
|
+
},
|
|
1988
|
+
resolveAwarenessName: async (context) => {
|
|
1989
|
+
const container = await createRequestContainer();
|
|
1990
|
+
const labels = await resolveUserLabels(
|
|
1991
|
+
container,
|
|
1992
|
+
{
|
|
1993
|
+
tenantId: context.tenantId,
|
|
1994
|
+
organizationId: context.organizationId
|
|
1995
|
+
},
|
|
1996
|
+
[context.userId]
|
|
1997
|
+
);
|
|
1998
|
+
return labels.get(context.userId)?.label ?? null;
|
|
1999
|
+
},
|
|
2000
|
+
resolveContainer: () => createRequestContainer(),
|
|
2001
|
+
loadContent: async (em, id, scope) => {
|
|
2002
|
+
const content = await loadDocumentContentForCollaboration(
|
|
2003
|
+
em,
|
|
2004
|
+
id,
|
|
2005
|
+
scope
|
|
2006
|
+
);
|
|
2007
|
+
if (!content) return null;
|
|
2008
|
+
return {
|
|
2009
|
+
yjsState: content.yjsState ?? null,
|
|
2010
|
+
contentHtml: content.contentHtml ?? null,
|
|
2011
|
+
updatedAt: content.updatedAt,
|
|
2012
|
+
collaborationGeneration: content.collaborationGeneration
|
|
2013
|
+
};
|
|
2014
|
+
},
|
|
2015
|
+
loadCollaborationGeneration: (em, id, scope) => loadDocumentCollaborationGeneration(
|
|
2016
|
+
em,
|
|
2017
|
+
id,
|
|
2018
|
+
scope
|
|
2019
|
+
),
|
|
2020
|
+
initializeYjsState: (em, id, scope) => initializeDocumentYjsState(
|
|
2021
|
+
em,
|
|
2022
|
+
id,
|
|
2023
|
+
scope
|
|
2024
|
+
),
|
|
2025
|
+
persistContent: (em, id, scope, input, serviceDeps) => persistDocumentContent(
|
|
2026
|
+
em,
|
|
2027
|
+
id,
|
|
2028
|
+
scope,
|
|
2029
|
+
input,
|
|
2030
|
+
serviceDeps
|
|
2031
|
+
),
|
|
2032
|
+
allowedOrigins,
|
|
2033
|
+
requireOrigin: process.env.NODE_ENV === "production",
|
|
2034
|
+
onPersisted: (document, yjsState, collaborationGeneration) => {
|
|
2035
|
+
redisStoreExtension?.markPersisted(document, yjsState, collaborationGeneration);
|
|
2036
|
+
},
|
|
2037
|
+
finalDrainRegistry,
|
|
2038
|
+
resolveRoomDocument: (documentName) => server?.hocuspocus.documents.get(documentName),
|
|
2039
|
+
isRoomInvalidated: (documentName, document) => {
|
|
2040
|
+
const room = document ?? server?.hocuspocus.documents.get(documentName);
|
|
2041
|
+
return Boolean(room && invalidatedRoomDocuments.has(room));
|
|
2042
|
+
},
|
|
2043
|
+
invalidateRoom
|
|
2044
|
+
});
|
|
2045
|
+
const runningServer = new Server({
|
|
2046
|
+
port,
|
|
2047
|
+
...COLLAB_SERVER_RUNTIME_CONFIGURATION,
|
|
2048
|
+
extensions: resolveDocumentsCollabRedisExtensions(
|
|
2049
|
+
process.env,
|
|
2050
|
+
(configuration) => {
|
|
2051
|
+
redisStoreExtension = new DocumentsCollabRedisExtension(configuration, {
|
|
2052
|
+
resolveCollaborationGeneration: hooks.resolveCollaborationGeneration,
|
|
2053
|
+
onRejectedAggregate: invalidateRoom,
|
|
2054
|
+
onAcceptedAggregate: (document, byteLength) => {
|
|
2055
|
+
hooks.recordRedisAggregate(document, byteLength);
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
return enforceDocumentsCollabSourceStoreOwnership(redisStoreExtension);
|
|
2059
|
+
}
|
|
2060
|
+
),
|
|
2061
|
+
async onAuthenticate(data) {
|
|
2062
|
+
return await hooks.onAuthenticate({
|
|
2063
|
+
token: data.token,
|
|
2064
|
+
documentName: data.documentName,
|
|
2065
|
+
connection: data.connectionConfig,
|
|
2066
|
+
requestHeaders: headersToRecord(data.requestHeaders)
|
|
2067
|
+
});
|
|
2068
|
+
},
|
|
2069
|
+
async connected(data) {
|
|
2070
|
+
try {
|
|
2071
|
+
hooks.establishConnectionAuthorization(data.context);
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
data.connection.close();
|
|
2074
|
+
hooks.releaseConnectionAuthorization(data.context);
|
|
2075
|
+
throw error;
|
|
2076
|
+
}
|
|
2077
|
+
if (data.context.exp !== null) {
|
|
2078
|
+
scheduleCollabConnectionExpiry(data.connection, data.context.exp);
|
|
2079
|
+
}
|
|
2080
|
+
scheduleCollabConnectionReauthorization(
|
|
2081
|
+
data.connection,
|
|
2082
|
+
() => hooks.reauthorizeActiveConnection(data.context)
|
|
2083
|
+
);
|
|
2084
|
+
},
|
|
2085
|
+
async beforeHandleMessage(data) {
|
|
2086
|
+
try {
|
|
2087
|
+
hooks.assertConnectionAuthorization(data.context);
|
|
2088
|
+
} catch (error) {
|
|
2089
|
+
data.connection.close();
|
|
2090
|
+
hooks.releaseConnectionAuthorization(data.context);
|
|
2091
|
+
throw error;
|
|
2092
|
+
}
|
|
2093
|
+
assertCollabInboundFramePolicy(data.update, {
|
|
2094
|
+
readOnly: data.connection.readOnly
|
|
2095
|
+
});
|
|
2096
|
+
},
|
|
2097
|
+
async beforeHandleAwareness(data) {
|
|
2098
|
+
if (!data.connection) return;
|
|
2099
|
+
return hooks.beforeHandleAwareness({
|
|
2100
|
+
context: data.context,
|
|
2101
|
+
states: data.states,
|
|
2102
|
+
ownedClientIds: new Set(data.document.getClients(data.connection)),
|
|
2103
|
+
occupiedClientIds: new Set(data.awareness.getStates().keys()),
|
|
2104
|
+
connection: data.connection,
|
|
2105
|
+
document: data.document
|
|
2106
|
+
});
|
|
2107
|
+
},
|
|
2108
|
+
async beforeSync(data) {
|
|
2109
|
+
return hooks.beforeSync({
|
|
2110
|
+
type: data.type,
|
|
2111
|
+
payload: data.payload,
|
|
2112
|
+
document: data.document,
|
|
2113
|
+
connection: data.connection,
|
|
2114
|
+
context: data.context
|
|
2115
|
+
});
|
|
2116
|
+
},
|
|
2117
|
+
async onLoadDocument(data) {
|
|
2118
|
+
try {
|
|
2119
|
+
hooks.assertConnectionAuthorization(data.context);
|
|
2120
|
+
const document = await hooks.onLoadDocument({
|
|
2121
|
+
documentName: data.documentName,
|
|
2122
|
+
context: data.context,
|
|
2123
|
+
document: data.document
|
|
2124
|
+
});
|
|
2125
|
+
hooks.assertConnectionAuthorization(data.context);
|
|
2126
|
+
return document;
|
|
2127
|
+
} catch (error) {
|
|
2128
|
+
hooks.releaseConnectionAuthorization(data.context);
|
|
2129
|
+
data.document.destroy();
|
|
2130
|
+
throw error;
|
|
2131
|
+
}
|
|
2132
|
+
},
|
|
2133
|
+
async afterLoadDocument(data) {
|
|
2134
|
+
try {
|
|
2135
|
+
hooks.assertConnectionAuthorization(data.context);
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
hooks.releaseConnectionAuthorization(data.context);
|
|
2138
|
+
data.document.destroy();
|
|
2139
|
+
throw error;
|
|
2140
|
+
}
|
|
2141
|
+
},
|
|
2142
|
+
async onStoreDocument(data) {
|
|
2143
|
+
if (!isDocumentsCollabSourceStore(data)) return;
|
|
2144
|
+
return await hooks.onStoreDocument({
|
|
2145
|
+
documentName: data.documentName,
|
|
2146
|
+
context: data.lastContext,
|
|
2147
|
+
document: data.document
|
|
2148
|
+
});
|
|
2149
|
+
},
|
|
2150
|
+
async onRequest(data) {
|
|
2151
|
+
return handleCollabServerRequest(data);
|
|
2152
|
+
},
|
|
2153
|
+
async onDisconnect(data) {
|
|
2154
|
+
hooks.releaseConnectionAuthorization(data.context);
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
server = runningServer;
|
|
2158
|
+
await runningServer.listen();
|
|
2159
|
+
registerCrossProcessEventListener((envelope) => {
|
|
2160
|
+
if (isOwnDocumentsCrossProcessEvent(envelope, CROSS_PROCESS_EVENT_INSTANCE_ID)) return;
|
|
2161
|
+
const trustedEvent = resolveTrustedDocumentsCrossProcessEvent(envelope);
|
|
2162
|
+
if (!trustedEvent) return;
|
|
2163
|
+
const { action, documentId, scope } = trustedEvent;
|
|
2164
|
+
finalDrainRegistry.bumpAuthorization(documentId, scope);
|
|
2165
|
+
const roomDocument = runningServer.hocuspocus.documents.get(documentId);
|
|
2166
|
+
if (roomDocument && !isTrustedDocumentsCollabRoomScope(trustedEvent, hooks.resolveRoomScope(roomDocument))) {
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
if (roomDocument && action === "reauth") {
|
|
2170
|
+
markCollabFinalDrainForReauth(roomDocument, finalDrainRegistry);
|
|
2171
|
+
}
|
|
2172
|
+
if (action === "invalidate") {
|
|
2173
|
+
redisStoreExtension?.discardPendingFanout(documentId);
|
|
2174
|
+
if (roomDocument) {
|
|
2175
|
+
redisStoreExtension?.discardPendingFanout(documentId, roomDocument);
|
|
2176
|
+
finalDrainRegistry.discard(roomDocument);
|
|
2177
|
+
invalidatedRoomDocuments.add(roomDocument);
|
|
2178
|
+
}
|
|
2179
|
+
closeRoomConnectionsForContentReset(documentId);
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
runningServer.hocuspocus.closeConnections(documentId);
|
|
2183
|
+
});
|
|
2184
|
+
let shuttingDown = false;
|
|
2185
|
+
const shutdown = (signal) => {
|
|
2186
|
+
if (shuttingDown) return;
|
|
2187
|
+
shuttingDown = true;
|
|
2188
|
+
logger.info("shutting down; draining pending document stores", { signal });
|
|
2189
|
+
void runningServer.destroy().catch((error) => {
|
|
2190
|
+
logger.error("graceful shutdown failed", {
|
|
2191
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2192
|
+
});
|
|
2193
|
+
process.exitCode = 1;
|
|
2194
|
+
}).finally(() => {
|
|
2195
|
+
process.exit(process.exitCode ?? 0);
|
|
2196
|
+
});
|
|
2197
|
+
};
|
|
2198
|
+
process.once("SIGTERM", shutdown);
|
|
2199
|
+
process.once("SIGINT", shutdown);
|
|
2200
|
+
logger.info(`listening on :${port}`);
|
|
2201
|
+
}
|
|
2202
|
+
if (process.env.DOCUMENTS_COLLAB_START !== "off" && isMainModule()) {
|
|
2203
|
+
void main().catch((error) => {
|
|
2204
|
+
logger.error("failed to start", {
|
|
2205
|
+
error: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
2206
|
+
});
|
|
2207
|
+
process.exitCode = 1;
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
export {
|
|
2211
|
+
COLLAB_SERVER_RUNTIME_CONFIGURATION,
|
|
2212
|
+
COLLAB_SERVER_TRANSPORT_OPTIONS,
|
|
2213
|
+
COLLAB_SERVER_UNAUTHENTICATED_CONFIGURATION,
|
|
2214
|
+
COLLAB_SERVER_WEBSOCKET_CONFIGURATION,
|
|
2215
|
+
DOCUMENTS_COLLAB_ACTIVE_REAUTHORIZATION_INTERVAL_MS,
|
|
2216
|
+
DOCUMENTS_COLLAB_AUTHORIZATION_TICKET_TIMEOUT_MS,
|
|
2217
|
+
DOCUMENTS_COLLAB_MAX_AWARENESS_CLIENT_IDS_PER_CONNECTION,
|
|
2218
|
+
DOCUMENTS_COLLAB_MAX_AWARENESS_CLIENT_IDS_PER_ROOM,
|
|
2219
|
+
DOCUMENTS_COLLAB_MAX_AWARENESS_FRAME_BYTES,
|
|
2220
|
+
DOCUMENTS_COLLAB_MAX_AWARENESS_STATE_BYTES,
|
|
2221
|
+
DOCUMENTS_COLLAB_MAX_CONTROL_FRAME_BYTES,
|
|
2222
|
+
DOCUMENTS_COLLAB_MAX_PENDING_BYTES,
|
|
2223
|
+
DOCUMENTS_COLLAB_MAX_PENDING_DOCUMENTS,
|
|
2224
|
+
DOCUMENTS_COLLAB_MAX_PENDING_MESSAGES,
|
|
2225
|
+
DOCUMENTS_COLLAB_MAX_READ_ONLY_SYNC_FRAME_BYTES,
|
|
2226
|
+
DOCUMENTS_COLLAB_REDIS_FANOUT_MAX_PENDING_BYTES,
|
|
2227
|
+
DOCUMENTS_COLLAB_REDIS_FANOUT_MAX_PENDING_ROOMS,
|
|
2228
|
+
DOCUMENTS_COLLAB_REDIS_SUBSCRIBE_TIMEOUT_MS,
|
|
2229
|
+
DocumentsCollabRedisExtension,
|
|
2230
|
+
assertCollabInboundFramePolicy,
|
|
2231
|
+
assertDocumentsCollabRedisAggregateUpdate,
|
|
2232
|
+
authorizeCollabContext,
|
|
2233
|
+
bindCollabAwarenessStates,
|
|
2234
|
+
closeCollabRoomConnectionsForContentReset,
|
|
2235
|
+
createCollabFinalDrainRegistry,
|
|
2236
|
+
createCollabHooks,
|
|
2237
|
+
enforceDocumentsCollabSourceStoreOwnership,
|
|
2238
|
+
handleCollabHealthRequest,
|
|
2239
|
+
handleCollabServerRequest,
|
|
2240
|
+
htmlToYDoc,
|
|
2241
|
+
initializeDocumentYjsState,
|
|
2242
|
+
installBoundedCollabIngress,
|
|
2243
|
+
installHocuspocusCollabIngressGuard,
|
|
2244
|
+
isCollabAuthorizationCurrent,
|
|
2245
|
+
isCollabRequestOriginAllowed,
|
|
2246
|
+
isDocumentsCollabSourceStore,
|
|
2247
|
+
isOwnDocumentsCrossProcessEvent,
|
|
2248
|
+
isTrustedDocumentsCollabRoomScope,
|
|
2249
|
+
main,
|
|
2250
|
+
markCollabFinalDrainForReauth,
|
|
2251
|
+
resolveCollabAllowedOrigins,
|
|
2252
|
+
resolveCollabRoomEventAction,
|
|
2253
|
+
resolveDocumentsCollabRedisConfiguration,
|
|
2254
|
+
resolveDocumentsCollabRedisExtensions,
|
|
2255
|
+
resolveTrustedDocumentsCrossProcessEvent,
|
|
2256
|
+
scheduleCollabConnectionExpiry,
|
|
2257
|
+
scheduleCollabConnectionReauthorization,
|
|
2258
|
+
yDocToContent
|
|
2259
|
+
};
|
|
2260
|
+
//# sourceMappingURL=documents-collab-server.js.map
|