@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.
Files changed (602) hide show
  1. package/README.md +88 -0
  2. package/build.mjs +23 -0
  3. package/dist/index.js +5 -0
  4. package/dist/index.js.map +7 -0
  5. package/dist/modules/documents/acl.js +15 -0
  6. package/dist/modules/documents/acl.js.map +7 -0
  7. package/dist/modules/documents/api/[id]/archive/route.js +116 -0
  8. package/dist/modules/documents/api/[id]/archive/route.js.map +7 -0
  9. package/dist/modules/documents/api/[id]/attachments/[attachmentId]/route.js +155 -0
  10. package/dist/modules/documents/api/[id]/attachments/[attachmentId]/route.js.map +7 -0
  11. package/dist/modules/documents/api/[id]/attachments/route.js +153 -0
  12. package/dist/modules/documents/api/[id]/attachments/route.js.map +7 -0
  13. package/dist/modules/documents/api/[id]/collab-token/route.js +180 -0
  14. package/dist/modules/documents/api/[id]/collab-token/route.js.map +7 -0
  15. package/dist/modules/documents/api/[id]/comments/access-check/route.js +86 -0
  16. package/dist/modules/documents/api/[id]/comments/access-check/route.js.map +7 -0
  17. package/dist/modules/documents/api/[id]/comments/route.js +387 -0
  18. package/dist/modules/documents/api/[id]/comments/route.js.map +7 -0
  19. package/dist/modules/documents/api/[id]/content/route.js +143 -0
  20. package/dist/modules/documents/api/[id]/content/route.js.map +7 -0
  21. package/dist/modules/documents/api/[id]/duplicate/route.js +200 -0
  22. package/dist/modules/documents/api/[id]/duplicate/route.js.map +7 -0
  23. package/dist/modules/documents/api/[id]/export/route.js +421 -0
  24. package/dist/modules/documents/api/[id]/export/route.js.map +7 -0
  25. package/dist/modules/documents/api/[id]/favorite/route.js +96 -0
  26. package/dist/modules/documents/api/[id]/favorite/route.js.map +7 -0
  27. package/dist/modules/documents/api/[id]/links/[linkId]/route.js +85 -0
  28. package/dist/modules/documents/api/[id]/links/[linkId]/route.js.map +7 -0
  29. package/dist/modules/documents/api/[id]/links/route.js +257 -0
  30. package/dist/modules/documents/api/[id]/links/route.js.map +7 -0
  31. package/dist/modules/documents/api/[id]/principals/route.js +299 -0
  32. package/dist/modules/documents/api/[id]/principals/route.js.map +7 -0
  33. package/dist/modules/documents/api/[id]/route.js +246 -0
  34. package/dist/modules/documents/api/[id]/route.js.map +7 -0
  35. package/dist/modules/documents/api/[id]/shares/route.js +335 -0
  36. package/dist/modules/documents/api/[id]/shares/route.js.map +7 -0
  37. package/dist/modules/documents/api/[id]/unarchive/route.js +39 -0
  38. package/dist/modules/documents/api/[id]/unarchive/route.js.map +7 -0
  39. package/dist/modules/documents/api/[id]/versions/[versionId]/restore/route.js +133 -0
  40. package/dist/modules/documents/api/[id]/versions/[versionId]/restore/route.js.map +7 -0
  41. package/dist/modules/documents/api/[id]/versions/[versionId]/route.js +93 -0
  42. package/dist/modules/documents/api/[id]/versions/[versionId]/route.js.map +7 -0
  43. package/dist/modules/documents/api/[id]/versions/route.js +193 -0
  44. package/dist/modules/documents/api/[id]/versions/route.js.map +7 -0
  45. package/dist/modules/documents/api/[id]/watch/route.js +97 -0
  46. package/dist/modules/documents/api/[id]/watch/route.js.map +7 -0
  47. package/dist/modules/documents/api/_commands.js +34 -0
  48. package/dist/modules/documents/api/_commands.js.map +7 -0
  49. package/dist/modules/documents/api/_shared.js +490 -0
  50. package/dist/modules/documents/api/_shared.js.map +7 -0
  51. package/dist/modules/documents/api/folders/route.js +256 -0
  52. package/dist/modules/documents/api/folders/route.js.map +7 -0
  53. package/dist/modules/documents/api/instantiate/route.js +108 -0
  54. package/dist/modules/documents/api/instantiate/route.js.map +7 -0
  55. package/dist/modules/documents/api/route.js +322 -0
  56. package/dist/modules/documents/api/route.js.map +7 -0
  57. package/dist/modules/documents/api/templates/[templateId]/preview/route.js +94 -0
  58. package/dist/modules/documents/api/templates/[templateId]/preview/route.js.map +7 -0
  59. package/dist/modules/documents/api/templates/[templateId]/route.js +82 -0
  60. package/dist/modules/documents/api/templates/[templateId]/route.js.map +7 -0
  61. package/dist/modules/documents/api/templates/route.js +344 -0
  62. package/dist/modules/documents/api/templates/route.js.map +7 -0
  63. package/dist/modules/documents/backend/documents/DocumentsPageClient.js +160 -0
  64. package/dist/modules/documents/backend/documents/DocumentsPageClient.js.map +7 -0
  65. package/dist/modules/documents/backend/documents/DocumentsTable.js +143 -0
  66. package/dist/modules/documents/backend/documents/DocumentsTable.js.map +7 -0
  67. package/dist/modules/documents/backend/documents/FolderDialog.js +55 -0
  68. package/dist/modules/documents/backend/documents/FolderDialog.js.map +7 -0
  69. package/dist/modules/documents/backend/documents/FolderTree.js +40 -0
  70. package/dist/modules/documents/backend/documents/FolderTree.js.map +7 -0
  71. package/dist/modules/documents/backend/documents/MoveDocumentDialog.js +64 -0
  72. package/dist/modules/documents/backend/documents/MoveDocumentDialog.js.map +7 -0
  73. package/dist/modules/documents/backend/documents/[id]/CommentAnchorNavigation.js +77 -0
  74. package/dist/modules/documents/backend/documents/[id]/CommentAnchorNavigation.js.map +7 -0
  75. package/dist/modules/documents/backend/documents/[id]/CommentComposer.js +126 -0
  76. package/dist/modules/documents/backend/documents/[id]/CommentComposer.js.map +7 -0
  77. package/dist/modules/documents/backend/documents/[id]/CommentThreadList.js +88 -0
  78. package/dist/modules/documents/backend/documents/[id]/CommentThreadList.js.map +7 -0
  79. package/dist/modules/documents/backend/documents/[id]/CommentsRail.js +123 -0
  80. package/dist/modules/documents/backend/documents/[id]/CommentsRail.js.map +7 -0
  81. package/dist/modules/documents/backend/documents/[id]/DocumentCanvas.js +91 -0
  82. package/dist/modules/documents/backend/documents/[id]/DocumentCanvas.js.map +7 -0
  83. package/dist/modules/documents/backend/documents/[id]/DocumentEditorErrorBoundary.js +27 -0
  84. package/dist/modules/documents/backend/documents/[id]/DocumentEditorErrorBoundary.js.map +7 -0
  85. package/dist/modules/documents/backend/documents/[id]/DocumentEditorIsland.js +50 -0
  86. package/dist/modules/documents/backend/documents/[id]/DocumentEditorIsland.js.map +7 -0
  87. package/dist/modules/documents/backend/documents/[id]/DocumentEditorSurface.js +168 -0
  88. package/dist/modules/documents/backend/documents/[id]/DocumentEditorSurface.js.map +7 -0
  89. package/dist/modules/documents/backend/documents/[id]/DocumentNavigator.js +129 -0
  90. package/dist/modules/documents/backend/documents/[id]/DocumentNavigator.js.map +7 -0
  91. package/dist/modules/documents/backend/documents/[id]/DocumentPageClient.js +429 -0
  92. package/dist/modules/documents/backend/documents/[id]/DocumentPageClient.js.map +7 -0
  93. package/dist/modules/documents/backend/documents/[id]/EditorColorTools.js +68 -0
  94. package/dist/modules/documents/backend/documents/[id]/EditorColorTools.js.map +7 -0
  95. package/dist/modules/documents/backend/documents/[id]/EditorStatusPresence.js +69 -0
  96. package/dist/modules/documents/backend/documents/[id]/EditorStatusPresence.js.map +7 -0
  97. package/dist/modules/documents/backend/documents/[id]/EditorToolbar.js +151 -0
  98. package/dist/modules/documents/backend/documents/[id]/EditorToolbar.js.map +7 -0
  99. package/dist/modules/documents/backend/documents/[id]/ExportMenu.js +121 -0
  100. package/dist/modules/documents/backend/documents/[id]/ExportMenu.js.map +7 -0
  101. package/dist/modules/documents/backend/documents/[id]/MentionPicker.js +114 -0
  102. package/dist/modules/documents/backend/documents/[id]/MentionPicker.js.map +7 -0
  103. package/dist/modules/documents/backend/documents/[id]/OutlinePane.js +70 -0
  104. package/dist/modules/documents/backend/documents/[id]/OutlinePane.js.map +7 -0
  105. package/dist/modules/documents/backend/documents/[id]/RecordFieldsDialog.js +157 -0
  106. package/dist/modules/documents/backend/documents/[id]/RecordFieldsDialog.js.map +7 -0
  107. package/dist/modules/documents/backend/documents/[id]/RelatedRecordsPanel.js +266 -0
  108. package/dist/modules/documents/backend/documents/[id]/RelatedRecordsPanel.js.map +7 -0
  109. package/dist/modules/documents/backend/documents/[id]/VersionHistoryPanel.js +227 -0
  110. package/dist/modules/documents/backend/documents/[id]/VersionHistoryPanel.js.map +7 -0
  111. package/dist/modules/documents/backend/documents/[id]/VersionPreviewDialog.js +157 -0
  112. package/dist/modules/documents/backend/documents/[id]/VersionPreviewDialog.js.map +7 -0
  113. package/dist/modules/documents/backend/documents/[id]/commentTypes.js +150 -0
  114. package/dist/modules/documents/backend/documents/[id]/commentTypes.js.map +7 -0
  115. package/dist/modules/documents/backend/documents/[id]/componentCapabilities.js +13 -0
  116. package/dist/modules/documents/backend/documents/[id]/componentCapabilities.js.map +7 -0
  117. package/dist/modules/documents/backend/documents/[id]/documentPagination.js +295 -0
  118. package/dist/modules/documents/backend/documents/[id]/documentPagination.js.map +7 -0
  119. package/dist/modules/documents/backend/documents/[id]/editorPresenceStyles.js +81 -0
  120. package/dist/modules/documents/backend/documents/[id]/editorPresenceStyles.js.map +7 -0
  121. package/dist/modules/documents/backend/documents/[id]/editorTypes.js +52 -0
  122. package/dist/modules/documents/backend/documents/[id]/editorTypes.js.map +7 -0
  123. package/dist/modules/documents/backend/documents/[id]/page.js +10 -0
  124. package/dist/modules/documents/backend/documents/[id]/page.js.map +7 -0
  125. package/dist/modules/documents/backend/documents/[id]/page.meta.js +16 -0
  126. package/dist/modules/documents/backend/documents/[id]/page.meta.js.map +7 -0
  127. package/dist/modules/documents/backend/documents/[id]/relatedRecordModel.js +38 -0
  128. package/dist/modules/documents/backend/documents/[id]/relatedRecordModel.js.map +7 -0
  129. package/dist/modules/documents/backend/documents/[id]/restoreVersion.js +33 -0
  130. package/dist/modules/documents/backend/documents/[id]/restoreVersion.js.map +7 -0
  131. package/dist/modules/documents/backend/documents/[id]/useDocumentCollaboration.js +392 -0
  132. package/dist/modules/documents/backend/documents/[id]/useDocumentCollaboration.js.map +7 -0
  133. package/dist/modules/documents/backend/documents/[id]/useDocumentComments.js +253 -0
  134. package/dist/modules/documents/backend/documents/[id]/useDocumentComments.js.map +7 -0
  135. package/dist/modules/documents/backend/documents/[id]/useDocumentEditor.js +149 -0
  136. package/dist/modules/documents/backend/documents/[id]/useDocumentEditor.js.map +7 -0
  137. package/dist/modules/documents/backend/documents/[id]/useDocumentTitle.js +82 -0
  138. package/dist/modules/documents/backend/documents/[id]/useDocumentTitle.js.map +7 -0
  139. package/dist/modules/documents/backend/documents/[id]/useEditorInsertions.js +144 -0
  140. package/dist/modules/documents/backend/documents/[id]/useEditorInsertions.js.map +7 -0
  141. package/dist/modules/documents/backend/documents/[id]/useFallbackContentPersistence.js +237 -0
  142. package/dist/modules/documents/backend/documents/[id]/useFallbackContentPersistence.js.map +7 -0
  143. package/dist/modules/documents/backend/documents/[id]/useMentionPicker.js +216 -0
  144. package/dist/modules/documents/backend/documents/[id]/useMentionPicker.js.map +7 -0
  145. package/dist/modules/documents/backend/documents/[id]/versionHistoryModel.js +25 -0
  146. package/dist/modules/documents/backend/documents/[id]/versionHistoryModel.js.map +7 -0
  147. package/dist/modules/documents/backend/documents/components/EntityPicker.js +156 -0
  148. package/dist/modules/documents/backend/documents/components/EntityPicker.js.map +7 -0
  149. package/dist/modules/documents/backend/documents/components/EntityPickerResults.js +46 -0
  150. package/dist/modules/documents/backend/documents/components/EntityPickerResults.js.map +7 -0
  151. package/dist/modules/documents/backend/documents/components/NewFromTemplateDialog.js +127 -0
  152. package/dist/modules/documents/backend/documents/components/NewFromTemplateDialog.js.map +7 -0
  153. package/dist/modules/documents/backend/documents/components/PrincipalPicker.js +84 -0
  154. package/dist/modules/documents/backend/documents/components/PrincipalPicker.js.map +7 -0
  155. package/dist/modules/documents/backend/documents/components/ShareDialog.js +77 -0
  156. package/dist/modules/documents/backend/documents/components/ShareDialog.js.map +7 -0
  157. package/dist/modules/documents/backend/documents/components/ShareDialogAddForm.js +93 -0
  158. package/dist/modules/documents/backend/documents/components/ShareDialogAddForm.js.map +7 -0
  159. package/dist/modules/documents/backend/documents/components/ShareDialogList.js +82 -0
  160. package/dist/modules/documents/backend/documents/components/ShareDialogList.js.map +7 -0
  161. package/dist/modules/documents/backend/documents/components/TemplateBodyEditor.js +96 -0
  162. package/dist/modules/documents/backend/documents/components/TemplateBodyEditor.js.map +7 -0
  163. package/dist/modules/documents/backend/documents/components/TemplateEditorDialog.js +169 -0
  164. package/dist/modules/documents/backend/documents/components/TemplateEditorDialog.js.map +7 -0
  165. package/dist/modules/documents/backend/documents/components/TemplatePreview.js +38 -0
  166. package/dist/modules/documents/backend/documents/components/TemplatePreview.js.map +7 -0
  167. package/dist/modules/documents/backend/documents/components/TemplateSlotFields.js +69 -0
  168. package/dist/modules/documents/backend/documents/components/TemplateSlotFields.js.map +7 -0
  169. package/dist/modules/documents/backend/documents/components/TemplateSlotsEditor.js +71 -0
  170. package/dist/modules/documents/backend/documents/components/TemplateSlotsEditor.js.map +7 -0
  171. package/dist/modules/documents/backend/documents/components/principalPickerModel.js +63 -0
  172. package/dist/modules/documents/backend/documents/components/principalPickerModel.js.map +7 -0
  173. package/dist/modules/documents/backend/documents/components/shareDialogModel.js +68 -0
  174. package/dist/modules/documents/backend/documents/components/shareDialogModel.js.map +7 -0
  175. package/dist/modules/documents/backend/documents/components/templateUi.js +89 -0
  176. package/dist/modules/documents/backend/documents/components/templateUi.js.map +7 -0
  177. package/dist/modules/documents/backend/documents/components/useAvailableEntityRegistry.js +30 -0
  178. package/dist/modules/documents/backend/documents/components/useAvailableEntityRegistry.js.map +7 -0
  179. package/dist/modules/documents/backend/documents/components/useEntitySearch.js +191 -0
  180. package/dist/modules/documents/backend/documents/components/useEntitySearch.js.map +7 -0
  181. package/dist/modules/documents/backend/documents/components/usePrincipalPicker.js +221 -0
  182. package/dist/modules/documents/backend/documents/components/usePrincipalPicker.js.map +7 -0
  183. package/dist/modules/documents/backend/documents/components/useShareDialog.js +236 -0
  184. package/dist/modules/documents/backend/documents/components/useShareDialog.js.map +7 -0
  185. package/dist/modules/documents/backend/documents/components/useTemplateDetail.js +49 -0
  186. package/dist/modules/documents/backend/documents/components/useTemplateDetail.js.map +7 -0
  187. package/dist/modules/documents/backend/documents/components/useTemplateInstantiation.js +234 -0
  188. package/dist/modules/documents/backend/documents/components/useTemplateInstantiation.js.map +7 -0
  189. package/dist/modules/documents/backend/documents/documentUi.js +106 -0
  190. package/dist/modules/documents/backend/documents/documentUi.js.map +7 -0
  191. package/dist/modules/documents/backend/documents/documentsListTypes.js +132 -0
  192. package/dist/modules/documents/backend/documents/documentsListTypes.js.map +7 -0
  193. package/dist/modules/documents/backend/documents/page.js +9 -0
  194. package/dist/modules/documents/backend/documents/page.js.map +7 -0
  195. package/dist/modules/documents/backend/documents/page.meta.js +26 -0
  196. package/dist/modules/documents/backend/documents/page.meta.js.map +7 -0
  197. package/dist/modules/documents/backend/documents/templates/TemplatesPageClient.js +91 -0
  198. package/dist/modules/documents/backend/documents/templates/TemplatesPageClient.js.map +7 -0
  199. package/dist/modules/documents/backend/documents/templates/TemplatesTable.js +58 -0
  200. package/dist/modules/documents/backend/documents/templates/TemplatesTable.js.map +7 -0
  201. package/dist/modules/documents/backend/documents/templates/page.js +9 -0
  202. package/dist/modules/documents/backend/documents/templates/page.js.map +7 -0
  203. package/dist/modules/documents/backend/documents/templates/page.meta.js +34 -0
  204. package/dist/modules/documents/backend/documents/templates/page.meta.js.map +7 -0
  205. package/dist/modules/documents/backend/documents/templates/useTemplatesPage.js +139 -0
  206. package/dist/modules/documents/backend/documents/templates/useTemplatesPage.js.map +7 -0
  207. package/dist/modules/documents/backend/documents/useDocumentsList.js +314 -0
  208. package/dist/modules/documents/backend/documents/useDocumentsList.js.map +7 -0
  209. package/dist/modules/documents/ce.js +8 -0
  210. package/dist/modules/documents/ce.js.map +7 -0
  211. package/dist/modules/documents/commands/aggregate.js +99 -0
  212. package/dist/modules/documents/commands/aggregate.js.map +7 -0
  213. package/dist/modules/documents/commands/attachments.js +252 -0
  214. package/dist/modules/documents/commands/attachments.js.map +7 -0
  215. package/dist/modules/documents/commands/comments.js +782 -0
  216. package/dist/modules/documents/commands/comments.js.map +7 -0
  217. package/dist/modules/documents/commands/content.js +238 -0
  218. package/dist/modules/documents/commands/content.js.map +7 -0
  219. package/dist/modules/documents/commands/document-crud.js +505 -0
  220. package/dist/modules/documents/commands/document-crud.js.map +7 -0
  221. package/dist/modules/documents/commands/documents.js +473 -0
  222. package/dist/modules/documents/commands/documents.js.map +7 -0
  223. package/dist/modules/documents/commands/duplicate.js +640 -0
  224. package/dist/modules/documents/commands/duplicate.js.map +7 -0
  225. package/dist/modules/documents/commands/favorites.js +150 -0
  226. package/dist/modules/documents/commands/favorites.js.map +7 -0
  227. package/dist/modules/documents/commands/folders.js +420 -0
  228. package/dist/modules/documents/commands/folders.js.map +7 -0
  229. package/dist/modules/documents/commands/index.js +64 -0
  230. package/dist/modules/documents/commands/index.js.map +7 -0
  231. package/dist/modules/documents/commands/interceptors.js +300 -0
  232. package/dist/modules/documents/commands/interceptors.js.map +7 -0
  233. package/dist/modules/documents/commands/lifecycle.js +249 -0
  234. package/dist/modules/documents/commands/lifecycle.js.map +7 -0
  235. package/dist/modules/documents/commands/links.js +410 -0
  236. package/dist/modules/documents/commands/links.js.map +7 -0
  237. package/dist/modules/documents/commands/mutation-helpers.js +42 -0
  238. package/dist/modules/documents/commands/mutation-helpers.js.map +7 -0
  239. package/dist/modules/documents/commands/projection-types.js +1 -0
  240. package/dist/modules/documents/commands/projection-types.js.map +7 -0
  241. package/dist/modules/documents/commands/shared.js +143 -0
  242. package/dist/modules/documents/commands/shared.js.map +7 -0
  243. package/dist/modules/documents/commands/shares.js +527 -0
  244. package/dist/modules/documents/commands/shares.js.map +7 -0
  245. package/dist/modules/documents/commands/side-effects.js +103 -0
  246. package/dist/modules/documents/commands/side-effects.js.map +7 -0
  247. package/dist/modules/documents/commands/templates.js +461 -0
  248. package/dist/modules/documents/commands/templates.js.map +7 -0
  249. package/dist/modules/documents/commands/versions.js +432 -0
  250. package/dist/modules/documents/commands/versions.js.map +7 -0
  251. package/dist/modules/documents/commands/watchers.js +164 -0
  252. package/dist/modules/documents/commands/watchers.js.map +7 -0
  253. package/dist/modules/documents/data/entities.js +653 -0
  254. package/dist/modules/documents/data/entities.js.map +7 -0
  255. package/dist/modules/documents/data/extensions.js +44 -0
  256. package/dist/modules/documents/data/extensions.js.map +7 -0
  257. package/dist/modules/documents/data/validators.js +219 -0
  258. package/dist/modules/documents/data/validators.js.map +7 -0
  259. package/dist/modules/documents/di.js +35 -0
  260. package/dist/modules/documents/di.js.map +7 -0
  261. package/dist/modules/documents/encryption.js +12 -0
  262. package/dist/modules/documents/encryption.js.map +7 -0
  263. package/dist/modules/documents/events.js +27 -0
  264. package/dist/modules/documents/events.js.map +7 -0
  265. package/dist/modules/documents/index.js +19 -0
  266. package/dist/modules/documents/index.js.map +7 -0
  267. package/dist/modules/documents/lib/attachmentServicePort.js +30 -0
  268. package/dist/modules/documents/lib/attachmentServicePort.js.map +7 -0
  269. package/dist/modules/documents/lib/capabilities.js +32 -0
  270. package/dist/modules/documents/lib/capabilities.js.map +7 -0
  271. package/dist/modules/documents/lib/collabCloseEvents.js +17 -0
  272. package/dist/modules/documents/lib/collabCloseEvents.js.map +7 -0
  273. package/dist/modules/documents/lib/collabEndpoint.js +22 -0
  274. package/dist/modules/documents/lib/collabEndpoint.js.map +7 -0
  275. package/dist/modules/documents/lib/collabMaterializer.js +107 -0
  276. package/dist/modules/documents/lib/collabMaterializer.js.map +7 -0
  277. package/dist/modules/documents/lib/collabToken.js +120 -0
  278. package/dist/modules/documents/lib/collabToken.js.map +7 -0
  279. package/dist/modules/documents/lib/collaborationAwareness.js +68 -0
  280. package/dist/modules/documents/lib/collaborationAwareness.js.map +7 -0
  281. package/dist/modules/documents/lib/commentAnchors.js +15 -0
  282. package/dist/modules/documents/lib/commentAnchors.js.map +7 -0
  283. package/dist/modules/documents/lib/constants.js +25 -0
  284. package/dist/modules/documents/lib/constants.js.map +7 -0
  285. package/dist/modules/documents/lib/contentService.js +248 -0
  286. package/dist/modules/documents/lib/contentService.js.map +7 -0
  287. package/dist/modules/documents/lib/displayLabels.js +23 -0
  288. package/dist/modules/documents/lib/displayLabels.js.map +7 -0
  289. package/dist/modules/documents/lib/docxRenderer.js +601 -0
  290. package/dist/modules/documents/lib/docxRenderer.js.map +7 -0
  291. package/dist/modules/documents/lib/duplicateContent.js +22 -0
  292. package/dist/modules/documents/lib/duplicateContent.js.map +7 -0
  293. package/dist/modules/documents/lib/editorConfig.js +287 -0
  294. package/dist/modules/documents/lib/editorConfig.js.map +7 -0
  295. package/dist/modules/documents/lib/entityLinks.js +148 -0
  296. package/dist/modules/documents/lib/entityLinks.js.map +7 -0
  297. package/dist/modules/documents/lib/entityRegistry.js +258 -0
  298. package/dist/modules/documents/lib/entityRegistry.js.map +7 -0
  299. package/dist/modules/documents/lib/entityRegistry.server.js +189 -0
  300. package/dist/modules/documents/lib/entityRegistry.server.js.map +7 -0
  301. package/dist/modules/documents/lib/entityRegistryAvailability.server.js +9 -0
  302. package/dist/modules/documents/lib/entityRegistryAvailability.server.js.map +7 -0
  303. package/dist/modules/documents/lib/entitySuggestion.js +50 -0
  304. package/dist/modules/documents/lib/entitySuggestion.js.map +7 -0
  305. package/dist/modules/documents/lib/favorites.js +22 -0
  306. package/dist/modules/documents/lib/favorites.js.map +7 -0
  307. package/dist/modules/documents/lib/folderHierarchySerialization.js +19 -0
  308. package/dist/modules/documents/lib/folderHierarchySerialization.js.map +7 -0
  309. package/dist/modules/documents/lib/historyLimits.js +100 -0
  310. package/dist/modules/documents/lib/historyLimits.js.map +7 -0
  311. package/dist/modules/documents/lib/historyPolicy.js +14 -0
  312. package/dist/modules/documents/lib/historyPolicy.js.map +7 -0
  313. package/dist/modules/documents/lib/organizationAccess.js +17 -0
  314. package/dist/modules/documents/lib/organizationAccess.js.map +7 -0
  315. package/dist/modules/documents/lib/pdfHtml.js +62 -0
  316. package/dist/modules/documents/lib/pdfHtml.js.map +7 -0
  317. package/dist/modules/documents/lib/pdfRenderer.js +284 -0
  318. package/dist/modules/documents/lib/pdfRenderer.js.map +7 -0
  319. package/dist/modules/documents/lib/permissions.js +222 -0
  320. package/dist/modules/documents/lib/permissions.js.map +7 -0
  321. package/dist/modules/documents/lib/platformServices.js +35 -0
  322. package/dist/modules/documents/lib/platformServices.js.map +7 -0
  323. package/dist/modules/documents/lib/recordFieldInsertion.js +58 -0
  324. package/dist/modules/documents/lib/recordFieldInsertion.js.map +7 -0
  325. package/dist/modules/documents/lib/requestBody.js +80 -0
  326. package/dist/modules/documents/lib/requestBody.js.map +7 -0
  327. package/dist/modules/documents/lib/resourceLimits.js +62 -0
  328. package/dist/modules/documents/lib/resourceLimits.js.map +7 -0
  329. package/dist/modules/documents/lib/shareCounts.js +25 -0
  330. package/dist/modules/documents/lib/shareCounts.js.map +7 -0
  331. package/dist/modules/documents/lib/templateFill.js +84 -0
  332. package/dist/modules/documents/lib/templateFill.js.map +7 -0
  333. package/dist/modules/documents/lib/templateInstantiation.js +147 -0
  334. package/dist/modules/documents/lib/templateInstantiation.js.map +7 -0
  335. package/dist/modules/documents/lib/templatePreviewDigest.js +82 -0
  336. package/dist/modules/documents/lib/templatePreviewDigest.js.map +7 -0
  337. package/dist/modules/documents/lib/templateSeeds.js +201 -0
  338. package/dist/modules/documents/lib/templateSeeds.js.map +7 -0
  339. package/dist/modules/documents/lib/userLabels.js +37 -0
  340. package/dist/modules/documents/lib/userLabels.js.map +7 -0
  341. package/dist/modules/documents/lib/versionContent.js +131 -0
  342. package/dist/modules/documents/lib/versionContent.js.map +7 -0
  343. package/dist/modules/documents/lib/versionLabels.js +12 -0
  344. package/dist/modules/documents/lib/versionLabels.js.map +7 -0
  345. package/dist/modules/documents/lib/versioning.js +14 -0
  346. package/dist/modules/documents/lib/versioning.js.map +7 -0
  347. package/dist/modules/documents/lib/visibility.js +422 -0
  348. package/dist/modules/documents/lib/visibility.js.map +7 -0
  349. package/dist/modules/documents/lib/watchers.js +106 -0
  350. package/dist/modules/documents/lib/watchers.js.map +7 -0
  351. package/dist/modules/documents/migrations/Migration20260708163836_documents.js +44 -0
  352. package/dist/modules/documents/migrations/Migration20260708163836_documents.js.map +7 -0
  353. package/dist/modules/documents/migrations/Migration20260709164720_documents.js +17 -0
  354. package/dist/modules/documents/migrations/Migration20260709164720_documents.js.map +7 -0
  355. package/dist/modules/documents/migrations/Migration20260710002318_documents.js +32 -0
  356. package/dist/modules/documents/migrations/Migration20260710002318_documents.js.map +7 -0
  357. package/dist/modules/documents/migrations/Migration20260710071003_documents.js +13 -0
  358. package/dist/modules/documents/migrations/Migration20260710071003_documents.js.map +7 -0
  359. package/dist/modules/documents/migrations/Migration20260712120000_documents.js +13 -0
  360. package/dist/modules/documents/migrations/Migration20260712120000_documents.js.map +7 -0
  361. package/dist/modules/documents/migrations/Migration20260712183000_documents.js +14 -0
  362. package/dist/modules/documents/migrations/Migration20260712183000_documents.js.map +7 -0
  363. package/dist/modules/documents/migrations/Migration20260713092156_documents.js +29 -0
  364. package/dist/modules/documents/migrations/Migration20260713092156_documents.js.map +7 -0
  365. package/dist/modules/documents/migrations/Migration20260717000000_documents.js +37 -0
  366. package/dist/modules/documents/migrations/Migration20260717000000_documents.js.map +7 -0
  367. package/dist/modules/documents/notifications.js +47 -0
  368. package/dist/modules/documents/notifications.js.map +7 -0
  369. package/dist/modules/documents/search.js +101 -0
  370. package/dist/modules/documents/search.js.map +7 -0
  371. package/dist/modules/documents/setup.js +19 -0
  372. package/dist/modules/documents/setup.js.map +7 -0
  373. package/dist/modules/documents/widgets/injection/related-documents/LinkDocumentDialog.js +214 -0
  374. package/dist/modules/documents/widgets/injection/related-documents/LinkDocumentDialog.js.map +7 -0
  375. package/dist/modules/documents/widgets/injection/related-documents/context.js +84 -0
  376. package/dist/modules/documents/widgets/injection/related-documents/context.js.map +7 -0
  377. package/dist/modules/documents/widgets/injection/related-documents/useRelatedDocuments.js +98 -0
  378. package/dist/modules/documents/widgets/injection/related-documents/useRelatedDocuments.js.map +7 -0
  379. package/dist/modules/documents/widgets/injection/related-documents/widget.client.js +83 -0
  380. package/dist/modules/documents/widgets/injection/related-documents/widget.client.js.map +7 -0
  381. package/dist/modules/documents/widgets/injection/related-documents/widget.js +19 -0
  382. package/dist/modules/documents/widgets/injection/related-documents/widget.js.map +7 -0
  383. package/dist/modules/documents/widgets/injection-table.js +21 -0
  384. package/dist/modules/documents/widgets/injection-table.js.map +7 -0
  385. package/dist/server/documents-collab-server.js +2260 -0
  386. package/dist/server/documents-collab-server.js.map +7 -0
  387. package/generated/entities/document/index.ts +12 -0
  388. package/generated/entities/document_attachment/index.ts +9 -0
  389. package/generated/entities/document_comment/index.ts +14 -0
  390. package/generated/entities/document_content/index.ts +11 -0
  391. package/generated/entities/document_entity_link/index.ts +19 -0
  392. package/generated/entities/document_favorite/index.ts +7 -0
  393. package/generated/entities/document_folder/index.ts +9 -0
  394. package/generated/entities/document_share/index.ts +11 -0
  395. package/generated/entities/document_template/index.ts +13 -0
  396. package/generated/entities/document_version/index.ts +9 -0
  397. package/generated/entities/document_watcher/index.ts +7 -0
  398. package/generated/entities.ids.generated.ts +21 -0
  399. package/generated/entity-fields-registry.ts +151 -0
  400. package/jest.config.cjs +34 -0
  401. package/package.json +149 -0
  402. package/server/documents-collab-server.ts +3416 -0
  403. package/src/index.ts +1 -0
  404. package/src/modules/documents/acl.ts +11 -0
  405. package/src/modules/documents/api/[id]/archive/route.ts +132 -0
  406. package/src/modules/documents/api/[id]/attachments/[attachmentId]/route.ts +166 -0
  407. package/src/modules/documents/api/[id]/attachments/route.ts +173 -0
  408. package/src/modules/documents/api/[id]/collab-token/route.ts +203 -0
  409. package/src/modules/documents/api/[id]/comments/access-check/route.ts +96 -0
  410. package/src/modules/documents/api/[id]/comments/route.ts +442 -0
  411. package/src/modules/documents/api/[id]/content/route.ts +156 -0
  412. package/src/modules/documents/api/[id]/duplicate/route.ts +231 -0
  413. package/src/modules/documents/api/[id]/export/route.ts +535 -0
  414. package/src/modules/documents/api/[id]/favorite/route.ts +110 -0
  415. package/src/modules/documents/api/[id]/links/[linkId]/route.ts +92 -0
  416. package/src/modules/documents/api/[id]/links/route.ts +301 -0
  417. package/src/modules/documents/api/[id]/principals/route.ts +375 -0
  418. package/src/modules/documents/api/[id]/route.ts +267 -0
  419. package/src/modules/documents/api/[id]/shares/route.ts +372 -0
  420. package/src/modules/documents/api/[id]/unarchive/route.ts +42 -0
  421. package/src/modules/documents/api/[id]/versions/[versionId]/restore/route.ts +152 -0
  422. package/src/modules/documents/api/[id]/versions/[versionId]/route.ts +100 -0
  423. package/src/modules/documents/api/[id]/versions/route.ts +207 -0
  424. package/src/modules/documents/api/[id]/watch/route.ts +111 -0
  425. package/src/modules/documents/api/_commands.ts +53 -0
  426. package/src/modules/documents/api/_shared.ts +676 -0
  427. package/src/modules/documents/api/folders/route.ts +287 -0
  428. package/src/modules/documents/api/instantiate/route.ts +114 -0
  429. package/src/modules/documents/api/route.ts +362 -0
  430. package/src/modules/documents/api/templates/[templateId]/preview/route.ts +97 -0
  431. package/src/modules/documents/api/templates/[templateId]/route.ts +92 -0
  432. package/src/modules/documents/api/templates/route.ts +370 -0
  433. package/src/modules/documents/backend/documents/DocumentsPageClient.tsx +150 -0
  434. package/src/modules/documents/backend/documents/DocumentsTable.tsx +168 -0
  435. package/src/modules/documents/backend/documents/FolderDialog.tsx +62 -0
  436. package/src/modules/documents/backend/documents/FolderTree.tsx +72 -0
  437. package/src/modules/documents/backend/documents/MoveDocumentDialog.tsx +70 -0
  438. package/src/modules/documents/backend/documents/[id]/CommentAnchorNavigation.ts +88 -0
  439. package/src/modules/documents/backend/documents/[id]/CommentComposer.tsx +136 -0
  440. package/src/modules/documents/backend/documents/[id]/CommentThreadList.tsx +131 -0
  441. package/src/modules/documents/backend/documents/[id]/CommentsRail.tsx +138 -0
  442. package/src/modules/documents/backend/documents/[id]/DocumentCanvas.tsx +111 -0
  443. package/src/modules/documents/backend/documents/[id]/DocumentEditorErrorBoundary.tsx +39 -0
  444. package/src/modules/documents/backend/documents/[id]/DocumentEditorIsland.tsx +50 -0
  445. package/src/modules/documents/backend/documents/[id]/DocumentEditorSurface.tsx +162 -0
  446. package/src/modules/documents/backend/documents/[id]/DocumentNavigator.tsx +129 -0
  447. package/src/modules/documents/backend/documents/[id]/DocumentPageClient.tsx +463 -0
  448. package/src/modules/documents/backend/documents/[id]/EditorColorTools.tsx +71 -0
  449. package/src/modules/documents/backend/documents/[id]/EditorStatusPresence.tsx +78 -0
  450. package/src/modules/documents/backend/documents/[id]/EditorToolbar.tsx +108 -0
  451. package/src/modules/documents/backend/documents/[id]/ExportMenu.tsx +144 -0
  452. package/src/modules/documents/backend/documents/[id]/MentionPicker.tsx +137 -0
  453. package/src/modules/documents/backend/documents/[id]/OutlinePane.tsx +98 -0
  454. package/src/modules/documents/backend/documents/[id]/RecordFieldsDialog.tsx +185 -0
  455. package/src/modules/documents/backend/documents/[id]/RelatedRecordsPanel.tsx +320 -0
  456. package/src/modules/documents/backend/documents/[id]/VersionHistoryPanel.tsx +244 -0
  457. package/src/modules/documents/backend/documents/[id]/VersionPreviewDialog.tsx +197 -0
  458. package/src/modules/documents/backend/documents/[id]/commentTypes.ts +192 -0
  459. package/src/modules/documents/backend/documents/[id]/componentCapabilities.ts +17 -0
  460. package/src/modules/documents/backend/documents/[id]/documentPagination.ts +352 -0
  461. package/src/modules/documents/backend/documents/[id]/editorPresenceStyles.ts +77 -0
  462. package/src/modules/documents/backend/documents/[id]/editorTypes.ts +84 -0
  463. package/src/modules/documents/backend/documents/[id]/page.meta.ts +12 -0
  464. package/src/modules/documents/backend/documents/[id]/page.tsx +6 -0
  465. package/src/modules/documents/backend/documents/[id]/relatedRecordModel.ts +49 -0
  466. package/src/modules/documents/backend/documents/[id]/restoreVersion.ts +49 -0
  467. package/src/modules/documents/backend/documents/[id]/useDocumentCollaboration.ts +458 -0
  468. package/src/modules/documents/backend/documents/[id]/useDocumentComments.ts +286 -0
  469. package/src/modules/documents/backend/documents/[id]/useDocumentEditor.ts +157 -0
  470. package/src/modules/documents/backend/documents/[id]/useDocumentTitle.ts +77 -0
  471. package/src/modules/documents/backend/documents/[id]/useEditorInsertions.ts +147 -0
  472. package/src/modules/documents/backend/documents/[id]/useFallbackContentPersistence.ts +277 -0
  473. package/src/modules/documents/backend/documents/[id]/useMentionPicker.ts +259 -0
  474. package/src/modules/documents/backend/documents/[id]/versionHistoryModel.ts +29 -0
  475. package/src/modules/documents/backend/documents/components/EntityPicker.tsx +171 -0
  476. package/src/modules/documents/backend/documents/components/EntityPickerResults.tsx +64 -0
  477. package/src/modules/documents/backend/documents/components/NewFromTemplateDialog.tsx +136 -0
  478. package/src/modules/documents/backend/documents/components/PrincipalPicker.tsx +90 -0
  479. package/src/modules/documents/backend/documents/components/ShareDialog.tsx +88 -0
  480. package/src/modules/documents/backend/documents/components/ShareDialogAddForm.tsx +113 -0
  481. package/src/modules/documents/backend/documents/components/ShareDialogList.tsx +117 -0
  482. package/src/modules/documents/backend/documents/components/TemplateBodyEditor.tsx +96 -0
  483. package/src/modules/documents/backend/documents/components/TemplateEditorDialog.tsx +171 -0
  484. package/src/modules/documents/backend/documents/components/TemplatePreview.tsx +43 -0
  485. package/src/modules/documents/backend/documents/components/TemplateSlotFields.tsx +72 -0
  486. package/src/modules/documents/backend/documents/components/TemplateSlotsEditor.tsx +72 -0
  487. package/src/modules/documents/backend/documents/components/principalPickerModel.ts +86 -0
  488. package/src/modules/documents/backend/documents/components/shareDialogModel.ts +94 -0
  489. package/src/modules/documents/backend/documents/components/templateUi.ts +128 -0
  490. package/src/modules/documents/backend/documents/components/useAvailableEntityRegistry.ts +43 -0
  491. package/src/modules/documents/backend/documents/components/useEntitySearch.ts +197 -0
  492. package/src/modules/documents/backend/documents/components/usePrincipalPicker.ts +165 -0
  493. package/src/modules/documents/backend/documents/components/useShareDialog.ts +267 -0
  494. package/src/modules/documents/backend/documents/components/useTemplateDetail.ts +54 -0
  495. package/src/modules/documents/backend/documents/components/useTemplateInstantiation.ts +252 -0
  496. package/src/modules/documents/backend/documents/documentUi.ts +134 -0
  497. package/src/modules/documents/backend/documents/documentsListTypes.ts +163 -0
  498. package/src/modules/documents/backend/documents/page.meta.ts +24 -0
  499. package/src/modules/documents/backend/documents/page.tsx +5 -0
  500. package/src/modules/documents/backend/documents/templates/TemplatesPageClient.tsx +93 -0
  501. package/src/modules/documents/backend/documents/templates/TemplatesTable.tsx +71 -0
  502. package/src/modules/documents/backend/documents/templates/page.meta.ts +32 -0
  503. package/src/modules/documents/backend/documents/templates/page.tsx +5 -0
  504. package/src/modules/documents/backend/documents/templates/useTemplatesPage.tsx +148 -0
  505. package/src/modules/documents/backend/documents/useDocumentsList.tsx +305 -0
  506. package/src/modules/documents/ce.ts +6 -0
  507. package/src/modules/documents/commands/aggregate.ts +128 -0
  508. package/src/modules/documents/commands/attachments.ts +309 -0
  509. package/src/modules/documents/commands/comments.ts +966 -0
  510. package/src/modules/documents/commands/content.ts +294 -0
  511. package/src/modules/documents/commands/document-crud.ts +609 -0
  512. package/src/modules/documents/commands/documents.ts +581 -0
  513. package/src/modules/documents/commands/duplicate.ts +772 -0
  514. package/src/modules/documents/commands/favorites.ts +171 -0
  515. package/src/modules/documents/commands/folders.ts +506 -0
  516. package/src/modules/documents/commands/index.ts +33 -0
  517. package/src/modules/documents/commands/interceptors.ts +402 -0
  518. package/src/modules/documents/commands/lifecycle.ts +333 -0
  519. package/src/modules/documents/commands/links.ts +486 -0
  520. package/src/modules/documents/commands/mutation-helpers.ts +48 -0
  521. package/src/modules/documents/commands/projection-types.ts +77 -0
  522. package/src/modules/documents/commands/shared.ts +219 -0
  523. package/src/modules/documents/commands/shares.ts +612 -0
  524. package/src/modules/documents/commands/side-effects.ts +130 -0
  525. package/src/modules/documents/commands/templates.ts +522 -0
  526. package/src/modules/documents/commands/versions.ts +537 -0
  527. package/src/modules/documents/commands/watchers.ts +181 -0
  528. package/src/modules/documents/data/entities.ts +631 -0
  529. package/src/modules/documents/data/extensions.ts +42 -0
  530. package/src/modules/documents/data/validators.ts +248 -0
  531. package/src/modules/documents/di.ts +33 -0
  532. package/src/modules/documents/encryption.ts +14 -0
  533. package/src/modules/documents/events.ts +23 -0
  534. package/src/modules/documents/i18n/.hardcoded-allowlist.json +9 -0
  535. package/src/modules/documents/i18n/de.json +522 -0
  536. package/src/modules/documents/i18n/en.json +522 -0
  537. package/src/modules/documents/i18n/es.json +522 -0
  538. package/src/modules/documents/i18n/ko.json +522 -0
  539. package/src/modules/documents/i18n/pl.json +522 -0
  540. package/src/modules/documents/index.ts +17 -0
  541. package/src/modules/documents/lib/attachmentServicePort.ts +94 -0
  542. package/src/modules/documents/lib/capabilities.ts +57 -0
  543. package/src/modules/documents/lib/collabCloseEvents.ts +26 -0
  544. package/src/modules/documents/lib/collabEndpoint.ts +43 -0
  545. package/src/modules/documents/lib/collabMaterializer.ts +140 -0
  546. package/src/modules/documents/lib/collabToken.ts +170 -0
  547. package/src/modules/documents/lib/collaborationAwareness.ts +102 -0
  548. package/src/modules/documents/lib/commentAnchors.ts +19 -0
  549. package/src/modules/documents/lib/constants.ts +39 -0
  550. package/src/modules/documents/lib/contentService.ts +340 -0
  551. package/src/modules/documents/lib/displayLabels.ts +31 -0
  552. package/src/modules/documents/lib/docxRenderer.ts +685 -0
  553. package/src/modules/documents/lib/duplicateContent.ts +32 -0
  554. package/src/modules/documents/lib/editorConfig.ts +371 -0
  555. package/src/modules/documents/lib/entityLinks.ts +250 -0
  556. package/src/modules/documents/lib/entityRegistry.server.ts +267 -0
  557. package/src/modules/documents/lib/entityRegistry.ts +345 -0
  558. package/src/modules/documents/lib/entityRegistryAvailability.server.ts +19 -0
  559. package/src/modules/documents/lib/entitySuggestion.ts +61 -0
  560. package/src/modules/documents/lib/favorites.ts +35 -0
  561. package/src/modules/documents/lib/folderHierarchySerialization.ts +34 -0
  562. package/src/modules/documents/lib/historyLimits.ts +154 -0
  563. package/src/modules/documents/lib/historyPolicy.ts +7 -0
  564. package/src/modules/documents/lib/organizationAccess.ts +44 -0
  565. package/src/modules/documents/lib/pdfHtml.ts +65 -0
  566. package/src/modules/documents/lib/pdfRenderer.ts +367 -0
  567. package/src/modules/documents/lib/permissions.ts +299 -0
  568. package/src/modules/documents/lib/platformServices.ts +70 -0
  569. package/src/modules/documents/lib/recordFieldInsertion.ts +72 -0
  570. package/src/modules/documents/lib/requestBody.ts +97 -0
  571. package/src/modules/documents/lib/resourceLimits.ts +94 -0
  572. package/src/modules/documents/lib/shareCounts.ts +44 -0
  573. package/src/modules/documents/lib/templateFill.ts +138 -0
  574. package/src/modules/documents/lib/templateInstantiation.ts +206 -0
  575. package/src/modules/documents/lib/templatePreviewDigest.ts +122 -0
  576. package/src/modules/documents/lib/templateSeeds.ts +248 -0
  577. package/src/modules/documents/lib/userLabels.ts +58 -0
  578. package/src/modules/documents/lib/versionContent.ts +163 -0
  579. package/src/modules/documents/lib/versionLabels.ts +9 -0
  580. package/src/modules/documents/lib/versioning.ts +29 -0
  581. package/src/modules/documents/lib/visibility.ts +610 -0
  582. package/src/modules/documents/lib/watchers.ts +129 -0
  583. package/src/modules/documents/migrations/.snapshot-open-mercato.json +2892 -0
  584. package/src/modules/documents/migrations/Migration20260708163836_documents.ts +50 -0
  585. package/src/modules/documents/migrations/Migration20260709164720_documents.ts +19 -0
  586. package/src/modules/documents/migrations/Migration20260710002318_documents.ts +35 -0
  587. package/src/modules/documents/migrations/Migration20260710071003_documents.ts +13 -0
  588. package/src/modules/documents/migrations/Migration20260712120000_documents.ts +13 -0
  589. package/src/modules/documents/migrations/Migration20260712183000_documents.ts +12 -0
  590. package/src/modules/documents/migrations/Migration20260713092156_documents.ts +43 -0
  591. package/src/modules/documents/migrations/Migration20260717000000_documents.ts +40 -0
  592. package/src/modules/documents/notifications.ts +45 -0
  593. package/src/modules/documents/search.ts +118 -0
  594. package/src/modules/documents/setup.ts +19 -0
  595. package/src/modules/documents/widgets/injection/related-documents/LinkDocumentDialog.tsx +224 -0
  596. package/src/modules/documents/widgets/injection/related-documents/context.ts +101 -0
  597. package/src/modules/documents/widgets/injection/related-documents/useRelatedDocuments.ts +112 -0
  598. package/src/modules/documents/widgets/injection/related-documents/widget.client.tsx +76 -0
  599. package/src/modules/documents/widgets/injection/related-documents/widget.ts +19 -0
  600. package/src/modules/documents/widgets/injection-table.ts +20 -0
  601. package/tsconfig.json +9 -0
  602. 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