@ecsplico/editor 3.6.0 → 4.0.0

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 (578) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/adapters/direct-ai.d.ts +4 -4
  3. package/dist/adapters/direct-ai.js +2 -2
  4. package/dist/adapters/direct-ai.js.map +1 -1
  5. package/dist/adapters/document-state.d.ts +1 -1
  6. package/dist/adapters/document-state.js.map +1 -1
  7. package/dist/adapters/forge.d.ts +2 -2
  8. package/dist/adapters/forge.js.map +1 -1
  9. package/dist/adapters/nitro.d.ts +11 -3
  10. package/dist/adapters/nitro.js +3 -1
  11. package/dist/adapters/nitro.js.map +1 -1
  12. package/dist/adapters/shared.d.ts +6 -6
  13. package/dist/adapters/shared.js.map +1 -1
  14. package/dist/config/index.d.ts +14 -14
  15. package/dist/config/index.js +8 -8
  16. package/dist/config/index.js.map +1 -1
  17. package/dist/core/backend-types.d.ts +3 -3
  18. package/dist/core/backend-types.js.map +1 -1
  19. package/dist/core/backend.d.ts +4 -4
  20. package/dist/core/backend.js.map +1 -1
  21. package/dist/core/block-convert.d.ts +1 -1
  22. package/dist/core/block-convert.js +5 -5
  23. package/dist/core/block-convert.js.map +1 -1
  24. package/dist/core/block-transfer.d.ts +8 -4
  25. package/dist/core/block-transfer.js +23 -14
  26. package/dist/core/block-transfer.js.map +1 -1
  27. package/dist/core/capabilities.d.ts +113 -0
  28. package/dist/core/capabilities.js +155 -0
  29. package/dist/core/capabilities.js.map +1 -0
  30. package/dist/core/command-types.d.ts +1 -1
  31. package/dist/core/context.js.map +1 -1
  32. package/dist/core/convert.d.ts +5 -5
  33. package/dist/core/convert.js +5 -5
  34. package/dist/core/convert.js.map +1 -1
  35. package/dist/core/core-feature.js +1 -1
  36. package/dist/core/define-editor.d.ts +2 -2
  37. package/dist/core/define-editor.js +34 -6
  38. package/dist/core/define-editor.js.map +1 -1
  39. package/dist/core/derivation.d.ts +119 -0
  40. package/dist/core/derivation.js +187 -0
  41. package/dist/core/derivation.js.map +1 -0
  42. package/dist/core/derive.d.ts +61 -0
  43. package/dist/core/derive.js +69 -0
  44. package/dist/core/derive.js.map +1 -0
  45. package/dist/core/extension-points.d.ts +16 -0
  46. package/dist/core/feature-context.d.ts +5 -1
  47. package/dist/core/feature-context.js +10 -3
  48. package/dist/core/feature-context.js.map +1 -1
  49. package/dist/core/feature.d.ts +41 -19
  50. package/dist/core/feature.js.map +1 -1
  51. package/dist/core/fences.js +1 -1
  52. package/dist/core/fences.js.map +1 -1
  53. package/dist/core/index.d.ts +6 -2
  54. package/dist/core/index.js +5 -1
  55. package/dist/core/pipeline.d.ts +14 -5
  56. package/dist/core/pipeline.js +19 -7
  57. package/dist/core/pipeline.js.map +1 -1
  58. package/dist/core/properties.d.ts +22 -0
  59. package/dist/core/properties.js +17 -0
  60. package/dist/core/properties.js.map +1 -0
  61. package/dist/core/schema/block-ids.js +5 -5
  62. package/dist/core/schema/block-ids.js.map +1 -1
  63. package/dist/core/schema/index.d.ts +2 -1
  64. package/dist/core/schema/index.js +2 -1
  65. package/dist/core/schema/index.js.map +1 -1
  66. package/dist/core/schema/opaque-fence.d.ts +4 -4
  67. package/dist/core/schema/opaque-fence.js +7 -7
  68. package/dist/core/schema/opaque-fence.js.map +1 -1
  69. package/dist/core/schema/persisted-names.d.ts +58 -0
  70. package/dist/core/schema/persisted-names.js +96 -0
  71. package/dist/core/schema/persisted-names.js.map +1 -0
  72. package/dist/core/toolbar-items.js +1 -1
  73. package/dist/core/toolbar-items.js.map +1 -1
  74. package/dist/core/util.js +5 -1
  75. package/dist/core/util.js.map +1 -1
  76. package/dist/features/ai/agent-ops.js +1 -1
  77. package/dist/features/ai/ai-review.js +1 -1
  78. package/dist/features/ai/assistant.d.ts +4 -4
  79. package/dist/features/ai/assistant.js +1 -1
  80. package/dist/features/ai/assistant.js.map +1 -1
  81. package/dist/features/ai/components/AiSelectionMenu.js +1 -1
  82. package/dist/features/ai/components/AiSelectionMenu.js.map +1 -1
  83. package/dist/features/ai/components/ModelQuickSwitch.js +7 -7
  84. package/dist/features/ai/components/ModelQuickSwitch.js.map +1 -1
  85. package/dist/features/ai/components/ModelQuickSwitch.vue.d.ts +4 -4
  86. package/dist/features/ai/components/SettingsAiTab.js +8 -8
  87. package/dist/features/ai/components/SettingsAiTab.js.map +1 -1
  88. package/dist/features/ai/components/SettingsModelProfiles.js +1 -1
  89. package/dist/features/ai/components/SettingsModelProfiles.js.map +1 -1
  90. package/dist/features/ai/document-chat-model.d.ts +2 -2
  91. package/dist/features/ai/document-chat-model.js +5 -5
  92. package/dist/features/ai/document-chat-model.js.map +1 -1
  93. package/dist/features/ai/document-chat.js +2 -2
  94. package/dist/features/ai/document-chat.js.map +1 -1
  95. package/dist/features/ai/feature.d.ts +2 -2
  96. package/dist/features/ai/feature.js +2 -2
  97. package/dist/features/ai/feature.js.map +1 -1
  98. package/dist/features/ai/ghost-autocomplete.d.ts +4 -4
  99. package/dist/features/ai/ghost-autocomplete.js +1 -1
  100. package/dist/features/ai/ghost-autocomplete.js.map +1 -1
  101. package/dist/features/ai/index.d.ts +3 -3
  102. package/dist/features/ai/index.js +2 -2
  103. package/dist/features/ai/model-profiles.d.ts +9 -9
  104. package/dist/features/ai/model-profiles.js +13 -13
  105. package/dist/features/ai/model-profiles.js.map +1 -1
  106. package/dist/features/ai/protocol/agent-ops.d.ts +2 -2
  107. package/dist/features/ai/protocol/agent-ops.js +1 -1
  108. package/dist/features/ai/protocol/agent-ops.js.map +1 -1
  109. package/dist/features/ai/protocol/block-lines.d.ts +7 -7
  110. package/dist/features/ai/protocol/block-lines.js +10 -10
  111. package/dist/features/ai/protocol/block-lines.js.map +1 -1
  112. package/dist/features/ai/protocol/chat.d.ts +5 -5
  113. package/dist/features/ai/protocol/chat.js +12 -12
  114. package/dist/features/ai/protocol/chat.js.map +1 -1
  115. package/dist/features/ai/protocol/generate.d.ts +1 -1
  116. package/dist/features/ai/protocol/generate.js +1 -1
  117. package/dist/features/ai/protocol/generate.js.map +1 -1
  118. package/dist/features/ai/protocol/index.d.ts +3 -3
  119. package/dist/features/ai/protocol/index.js +2 -2
  120. package/dist/features/ai/protocol/models.d.ts +3 -3
  121. package/dist/features/ai/protocol/models.js +3 -3
  122. package/dist/features/ai/protocol/models.js.map +1 -1
  123. package/dist/features/ai/protocol/schemas.d.ts +1 -1
  124. package/dist/features/ai/protocol/schemas.js +1 -1
  125. package/dist/features/ai/protocol/schemas.js.map +1 -1
  126. package/dist/features/ai/protocol/tasks.d.ts +5 -5
  127. package/dist/features/ai/protocol/tasks.js +12 -12
  128. package/dist/features/ai/protocol/tasks.js.map +1 -1
  129. package/dist/features/ai/react/AiSelectionMenu.js.map +1 -1
  130. package/dist/features/ai/react/ModelQuickSwitch.d.ts +4 -4
  131. package/dist/features/ai/react/ModelQuickSwitch.js +6 -6
  132. package/dist/features/ai/react/ModelQuickSwitch.js.map +1 -1
  133. package/dist/features/ai/react/SettingsAiTab.d.ts +1 -1
  134. package/dist/features/ai/react/SettingsAiTab.js +8 -8
  135. package/dist/features/ai/react/SettingsAiTab.js.map +1 -1
  136. package/dist/features/ai/react/SettingsModelProfiles.d.ts +1 -1
  137. package/dist/features/ai/react/SettingsModelProfiles.js +1 -1
  138. package/dist/features/ai/react/SettingsModelProfiles.js.map +1 -1
  139. package/dist/features/ai/react/use-ai-assistant.d.ts +1 -1
  140. package/dist/features/ai/react/use-ai-assistant.js +1 -1
  141. package/dist/features/ai/react/use-ai-assistant.js.map +1 -1
  142. package/dist/features/ai/react/use-model-profiles.d.ts +5 -5
  143. package/dist/features/ai/react/use-model-profiles.js +2 -2
  144. package/dist/features/ai/react/use-model-profiles.js.map +1 -1
  145. package/dist/features/ai/use-ai-assistant.d.ts +1 -1
  146. package/dist/features/ai/use-ai-assistant.js +1 -1
  147. package/dist/features/ai/use-ai-assistant.js.map +1 -1
  148. package/dist/features/ai/use-model-profiles.d.ts +4 -4
  149. package/dist/features/ai/use-model-profiles.js +2 -2
  150. package/dist/features/ai/use-model-profiles.js.map +1 -1
  151. package/dist/features/block-ui/block-actions.d.ts +8 -4
  152. package/dist/features/block-ui/block-actions.js +38 -22
  153. package/dist/features/block-ui/block-actions.js.map +1 -1
  154. package/dist/features/block-ui/block-target.d.ts +29 -0
  155. package/dist/features/block-ui/block-target.js +63 -0
  156. package/dist/features/block-ui/block-target.js.map +1 -0
  157. package/dist/features/block-ui/components/BlockHandles.js +3 -3
  158. package/dist/features/block-ui/components/BlockHandles.js.map +1 -1
  159. package/dist/features/block-ui/components/BlockUi.js +11 -23
  160. package/dist/features/block-ui/components/BlockUi.js.map +1 -1
  161. package/dist/features/block-ui/feature.d.ts +3 -2
  162. package/dist/features/block-ui/feature.js +3 -2
  163. package/dist/features/block-ui/feature.js.map +1 -1
  164. package/dist/features/block-ui/handle-geometry.js +12 -23
  165. package/dist/features/block-ui/handle-geometry.js.map +1 -1
  166. package/dist/features/block-ui/index.d.ts +2 -1
  167. package/dist/features/block-ui/index.js +2 -1
  168. package/dist/features/block-ui/react/BlockHandles.d.ts +1 -1
  169. package/dist/features/block-ui/react/BlockHandles.js +4 -4
  170. package/dist/features/block-ui/react/BlockHandles.js.map +1 -1
  171. package/dist/features/block-ui/react/BlockUi.js +13 -25
  172. package/dist/features/block-ui/react/BlockUi.js.map +1 -1
  173. package/dist/features/comments/model.js +1 -1
  174. package/dist/features/composites/accepted-children.d.ts +5 -4
  175. package/dist/features/composites/accepted-children.js +26 -26
  176. package/dist/features/composites/accepted-children.js.map +1 -1
  177. package/dist/features/composites/ai-brief.d.ts +4 -4
  178. package/dist/features/composites/ai-brief.js +14 -12
  179. package/dist/features/composites/ai-brief.js.map +1 -1
  180. package/dist/features/composites/anchors.d.ts +1 -1
  181. package/dist/features/composites/anchors.js.map +1 -1
  182. package/dist/features/composites/baskets.d.ts +1 -1
  183. package/dist/features/composites/baskets.js.map +1 -1
  184. package/dist/features/composites/bibliography-command.d.ts +1 -1
  185. package/dist/features/composites/bibliography-form.js +1 -1
  186. package/dist/features/composites/bibliography-form.js.map +1 -1
  187. package/dist/features/composites/bibliography.js.map +1 -1
  188. package/dist/features/composites/block-types-json.d.ts +38 -0
  189. package/dist/features/composites/block-types-json.js +122 -0
  190. package/dist/features/composites/block-types-json.js.map +1 -0
  191. package/dist/features/composites/block-types.d.ts +119 -0
  192. package/dist/features/composites/block-types.js +343 -0
  193. package/dist/features/composites/block-types.js.map +1 -0
  194. package/dist/features/composites/capabilities.js +109 -0
  195. package/dist/features/composites/capabilities.js.map +1 -0
  196. package/dist/features/composites/components/BibliographyEditor.js +5 -5
  197. package/dist/features/composites/components/BibliographyEditor.js.map +1 -1
  198. package/dist/features/composites/components/CompositeBasketMenu.js +2 -2
  199. package/dist/features/composites/components/CompositeBasketMenu.js.map +1 -1
  200. package/dist/features/composites/components/CompositeChrome.js +18 -8
  201. package/dist/features/composites/components/CompositeChrome.js.map +1 -1
  202. package/dist/features/composites/components/CompositeChrome.vue.d.ts +1 -1
  203. package/dist/features/composites/components/CompositeDetails.js +11 -10
  204. package/dist/features/composites/components/CompositeDetails.js.map +1 -1
  205. package/dist/features/composites/components/CompositeDetails.vue.d.ts +1 -1
  206. package/dist/features/composites/components/CompositeFieldEditor.js +7 -7
  207. package/dist/features/composites/components/CompositeFieldEditor.js.map +1 -1
  208. package/dist/features/composites/components/CompositeFieldEditor.vue.d.ts +5 -5
  209. package/dist/features/composites/components/CompositeInspector.js +136 -113
  210. package/dist/features/composites/components/CompositeInspector.js.map +1 -1
  211. package/dist/features/composites/components/CompositeInspector.vue.d.ts +3 -3
  212. package/dist/features/composites/components/CompositeStatusMenu.js +2 -2
  213. package/dist/features/composites/components/CompositeStatusMenu.js.map +1 -1
  214. package/dist/features/composites/components/CompositeSummaryEditor.js +1 -1
  215. package/dist/features/composites/components/CompositeSummaryEditor.js.map +1 -1
  216. package/dist/features/composites/components/CompositeSummaryEditor.vue.d.ts +1 -1
  217. package/dist/features/composites/components/CompositeTable.js +2 -2
  218. package/dist/features/composites/components/CompositeTable.js.map +1 -1
  219. package/dist/features/composites/components/CompositeTagsEditor.js +1 -1
  220. package/dist/features/composites/components/CompositeTagsEditor.js.map +1 -1
  221. package/dist/features/composites/components/CompositeTagsEditor.vue.d.ts +1 -1
  222. package/dist/features/composites/components/CompositeView.js +19 -17
  223. package/dist/features/composites/components/CompositeView.js.map +1 -1
  224. package/dist/features/composites/components/DerivationBadge.js +15 -6
  225. package/dist/features/composites/components/DerivationBadge.js.map +1 -1
  226. package/dist/features/composites/components/FocusChip.js +1 -1
  227. package/dist/features/composites/components/FocusChip.js.map +1 -1
  228. package/dist/features/composites/components/FocusChip.vue.d.ts +1 -1
  229. package/dist/features/composites/components/ReferenceFieldSelect.js +3 -3
  230. package/dist/features/composites/components/ReferenceFieldSelect.js.map +1 -1
  231. package/dist/features/composites/components/ReferenceView.js +1 -1
  232. package/dist/features/composites/components/ReferenceView.js.map +1 -1
  233. package/dist/features/composites/components/SettingsEditingTab.js +18 -18
  234. package/dist/features/composites/components/SettingsEditingTab.js.map +1 -1
  235. package/dist/features/composites/components/SettingsEditingTab.vue.d.ts +3 -3
  236. package/dist/features/composites/convert.d.ts +4 -4
  237. package/dist/features/composites/convert.js +27 -28
  238. package/dist/features/composites/convert.js.map +1 -1
  239. package/dist/features/composites/derived-chrome.js +34 -28
  240. package/dist/features/composites/derived-chrome.js.map +1 -1
  241. package/dist/features/composites/editing-settings-keys.js +6 -6
  242. package/dist/features/composites/editing-settings-keys.js.map +1 -1
  243. package/dist/features/composites/feature.d.ts +10 -10
  244. package/dist/features/composites/feature.js +58 -46
  245. package/dist/features/composites/feature.js.map +1 -1
  246. package/dist/features/composites/fences.d.ts +3 -3
  247. package/dist/features/composites/fences.js +18 -13
  248. package/dist/features/composites/fences.js.map +1 -1
  249. package/dist/features/composites/focus.d.ts +10 -10
  250. package/dist/features/composites/focus.js +11 -11
  251. package/dist/features/composites/focus.js.map +1 -1
  252. package/dist/features/composites/heading-composites.d.ts +5 -5
  253. package/dist/features/composites/heading-composites.js +13 -16
  254. package/dist/features/composites/heading-composites.js.map +1 -1
  255. package/dist/features/composites/index.d.ts +20 -18
  256. package/dist/features/composites/index.js +9 -7
  257. package/dist/features/composites/inspector-model.d.ts +45 -22
  258. package/dist/features/composites/inspector-model.js +53 -14
  259. package/dist/features/composites/inspector-model.js.map +1 -1
  260. package/dist/features/composites/legacy.d.ts +5 -4
  261. package/dist/features/composites/legacy.js +16 -23
  262. package/dist/features/composites/legacy.js.map +1 -1
  263. package/dist/features/composites/markdown-export.js +3 -3
  264. package/dist/features/composites/markdown-export.js.map +1 -1
  265. package/dist/features/composites/merge.d.ts +8 -8
  266. package/dist/features/composites/merge.js +23 -22
  267. package/dist/features/composites/merge.js.map +1 -1
  268. package/dist/features/composites/meta-display.js +5 -5
  269. package/dist/features/composites/meta-display.js.map +1 -1
  270. package/dist/features/composites/meta-keys.d.ts +5 -5
  271. package/dist/features/composites/meta-keys.js +8 -8
  272. package/dist/features/composites/meta-keys.js.map +1 -1
  273. package/dist/features/composites/node-views.d.ts +3 -3
  274. package/dist/features/composites/node-views.js +2 -2
  275. package/dist/features/composites/node-views.js.map +1 -1
  276. package/dist/features/composites/nodes.d.ts +13 -13
  277. package/dist/features/composites/nodes.js +100 -77
  278. package/dist/features/composites/nodes.js.map +1 -1
  279. package/dist/features/composites/outline.d.ts +15 -12
  280. package/dist/features/composites/outline.js +24 -21
  281. package/dist/features/composites/outline.js.map +1 -1
  282. package/dist/features/composites/properties.d.ts +51 -0
  283. package/dist/features/composites/properties.js +89 -0
  284. package/dist/features/composites/properties.js.map +1 -0
  285. package/dist/features/composites/react/BibliographyEditor.js +5 -5
  286. package/dist/features/composites/react/BibliographyEditor.js.map +1 -1
  287. package/dist/features/composites/react/CompositeBasketMenu.js +2 -2
  288. package/dist/features/composites/react/CompositeBasketMenu.js.map +1 -1
  289. package/dist/features/composites/react/CompositeChrome.d.ts +1 -1
  290. package/dist/features/composites/react/CompositeChrome.js +15 -6
  291. package/dist/features/composites/react/CompositeChrome.js.map +1 -1
  292. package/dist/features/composites/react/CompositeDetails.d.ts +1 -1
  293. package/dist/features/composites/react/CompositeDetails.js +11 -10
  294. package/dist/features/composites/react/CompositeDetails.js.map +1 -1
  295. package/dist/features/composites/react/CompositeFieldEditor.d.ts +5 -5
  296. package/dist/features/composites/react/CompositeFieldEditor.js +6 -6
  297. package/dist/features/composites/react/CompositeFieldEditor.js.map +1 -1
  298. package/dist/features/composites/react/CompositeInspector.d.ts +3 -3
  299. package/dist/features/composites/react/CompositeInspector.js +142 -128
  300. package/dist/features/composites/react/CompositeInspector.js.map +1 -1
  301. package/dist/features/composites/react/CompositeStatusMenu.js +2 -2
  302. package/dist/features/composites/react/CompositeStatusMenu.js.map +1 -1
  303. package/dist/features/composites/react/CompositeSummaryEditor.d.ts +1 -1
  304. package/dist/features/composites/react/CompositeSummaryEditor.js +1 -1
  305. package/dist/features/composites/react/CompositeSummaryEditor.js.map +1 -1
  306. package/dist/features/composites/react/CompositeTable.d.ts +1 -1
  307. package/dist/features/composites/react/CompositeTable.js +3 -3
  308. package/dist/features/composites/react/CompositeTable.js.map +1 -1
  309. package/dist/features/composites/react/CompositeTagsEditor.d.ts +1 -1
  310. package/dist/features/composites/react/CompositeTagsEditor.js +1 -1
  311. package/dist/features/composites/react/CompositeTagsEditor.js.map +1 -1
  312. package/dist/features/composites/react/CompositeView.js +17 -15
  313. package/dist/features/composites/react/CompositeView.js.map +1 -1
  314. package/dist/features/composites/react/DerivationBadge.js +13 -4
  315. package/dist/features/composites/react/DerivationBadge.js.map +1 -1
  316. package/dist/features/composites/react/FocusChip.d.ts +1 -1
  317. package/dist/features/composites/react/FocusChip.js +1 -1
  318. package/dist/features/composites/react/FocusChip.js.map +1 -1
  319. package/dist/features/composites/react/ReferenceFieldSelect.js +3 -3
  320. package/dist/features/composites/react/ReferenceFieldSelect.js.map +1 -1
  321. package/dist/features/composites/react/ReferenceView.d.ts +1 -1
  322. package/dist/features/composites/react/ReferenceView.js +1 -1
  323. package/dist/features/composites/react/ReferenceView.js.map +1 -1
  324. package/dist/features/composites/react/SettingsEditingTab.d.ts +4 -4
  325. package/dist/features/composites/react/SettingsEditingTab.js +20 -20
  326. package/dist/features/composites/react/SettingsEditingTab.js.map +1 -1
  327. package/dist/features/composites/react.js +1 -1
  328. package/dist/features/composites/react.js.map +1 -1
  329. package/dist/features/composites/reference-ai.js +1 -1
  330. package/dist/features/composites/reference-ai.js.map +1 -1
  331. package/dist/features/composites/reference-label.d.ts +3 -3
  332. package/dist/features/composites/reference-label.js +6 -6
  333. package/dist/features/composites/reference-label.js.map +1 -1
  334. package/dist/features/composites/reference-suggestion.d.ts +1 -1
  335. package/dist/features/composites/reference-suggestion.js +3 -3
  336. package/dist/features/composites/reference-suggestion.js.map +1 -1
  337. package/dist/features/composites/reference-text.d.ts +2 -2
  338. package/dist/features/composites/reference-text.js +4 -4
  339. package/dist/features/composites/reference-text.js.map +1 -1
  340. package/dist/features/composites/reference.d.ts +3 -3
  341. package/dist/features/composites/reference.js +1 -1
  342. package/dist/features/composites/reference.js.map +1 -1
  343. package/dist/features/composites/reveal-source.d.ts +3 -3
  344. package/dist/features/composites/reveal-source.js +6 -6
  345. package/dist/features/composites/reveal-source.js.map +1 -1
  346. package/dist/features/composites/rows-model.d.ts +2 -2
  347. package/dist/features/composites/rows-model.js +4 -3
  348. package/dist/features/composites/rows-model.js.map +1 -1
  349. package/dist/features/composites/rows.d.ts +16 -28
  350. package/dist/features/composites/rows.js +20 -54
  351. package/dist/features/composites/rows.js.map +1 -1
  352. package/dist/features/composites/schema.d.ts +12 -11
  353. package/dist/features/composites/schema.js +10 -9
  354. package/dist/features/composites/schema.js.map +1 -1
  355. package/dist/features/composites/source.d.ts +20 -0
  356. package/dist/features/composites/source.js +40 -0
  357. package/dist/features/composites/source.js.map +1 -0
  358. package/dist/features/composites/split.d.ts +11 -11
  359. package/dist/features/composites/split.js +20 -20
  360. package/dist/features/composites/split.js.map +1 -1
  361. package/dist/features/composites/structure-mode.js +3 -3
  362. package/dist/features/composites/structure-mode.js.map +1 -1
  363. package/dist/features/composites/table-model.js +2 -2
  364. package/dist/features/composites/table-model.js.map +1 -1
  365. package/dist/features/composites/use-composite-inspector.js +3 -1
  366. package/dist/features/composites/use-composite-inspector.js.map +1 -1
  367. package/dist/features/composites/use-composite-rows.d.ts +2 -2
  368. package/dist/features/composites/use-composite-rows.js.map +1 -1
  369. package/dist/features/composites/vue.js +1 -1
  370. package/dist/features/composites/vue.js.map +1 -1
  371. package/dist/features/image-studio/components/ImageStudioDialog.js +1 -1
  372. package/dist/features/image-studio/components/ImageStudioDialog.js.map +1 -1
  373. package/dist/features/image-studio/components/ImageStudioDialog.vue.d.ts +1 -1
  374. package/dist/features/image-studio/components/ImageStudioView.js.map +1 -1
  375. package/dist/features/image-studio/feature.d.ts +1 -1
  376. package/dist/features/image-studio/feature.js +1 -1
  377. package/dist/features/image-studio/feature.js.map +1 -1
  378. package/dist/features/image-studio/node-core.d.ts +3 -3
  379. package/dist/features/image-studio/node-core.js +1 -1
  380. package/dist/features/image-studio/node-core.js.map +1 -1
  381. package/dist/features/image-studio/node-store.d.ts +1 -1
  382. package/dist/features/image-studio/node-store.js +1 -1
  383. package/dist/features/image-studio/node-store.js.map +1 -1
  384. package/dist/features/image-studio/react/ImageStudioDialog.d.ts +1 -1
  385. package/dist/features/image-studio/react/ImageStudioDialog.js +1 -1
  386. package/dist/features/image-studio/react/ImageStudioDialog.js.map +1 -1
  387. package/dist/features/image-studio/react/ImageStudioView.js +1 -1
  388. package/dist/features/image-studio/react/ImageStudioView.js.map +1 -1
  389. package/dist/features/image-studio/react/store.d.ts +3 -3
  390. package/dist/features/image-studio/react/store.js.map +1 -1
  391. package/dist/features/image-studio/react.d.ts +1 -1
  392. package/dist/features/image-studio/react.js +1 -1
  393. package/dist/features/image-studio/react.js.map +1 -1
  394. package/dist/features/image-studio/schema.d.ts +1 -1
  395. package/dist/features/image-studio/schema.js +1 -1
  396. package/dist/features/image-studio/schema.js.map +1 -1
  397. package/dist/features/image-studio/state.d.ts +3 -3
  398. package/dist/features/image-studio/state.js +2 -2
  399. package/dist/features/image-studio/state.js.map +1 -1
  400. package/dist/features/image-studio/store.d.ts +3 -3
  401. package/dist/features/image-studio/vue.d.ts +1 -1
  402. package/dist/features/image-studio/vue.js +1 -1
  403. package/dist/features/image-studio/vue.js.map +1 -1
  404. package/dist/features/media/index.js +1 -1
  405. package/dist/features/media/react/AudioRecorderModal.js +1 -1
  406. package/dist/features/media/react.js +1 -1
  407. package/dist/features/mentions/tag.js +2 -1
  408. package/dist/features/mentions/tag.js.map +1 -1
  409. package/dist/features/themes/components/SettingsAppearanceTab.js +1 -1
  410. package/dist/features/themes/components/SettingsAppearanceTab.js.map +1 -1
  411. package/dist/features/themes/feature.d.ts +1 -1
  412. package/dist/features/themes/feature.js.map +1 -1
  413. package/dist/features/themes/react/SettingsAppearanceTab.d.ts +1 -1
  414. package/dist/features/themes/react/SettingsAppearanceTab.js +1 -1
  415. package/dist/features/themes/react/SettingsAppearanceTab.js.map +1 -1
  416. package/dist/features/themes/theme-runtime.js +1 -1
  417. package/dist/features/themes/theme-runtime.js.map +1 -1
  418. package/dist/features/transcript/TranscriptSegmentView.js +1 -1
  419. package/dist/features/transcript/TranscriptSegmentView.js.map +1 -1
  420. package/dist/features/transcript/react/TranscriptSegmentView.js +1 -1
  421. package/dist/features/transcript/react/TranscriptSegmentView.js.map +1 -1
  422. package/dist/features/transcript/segment-node.js.map +1 -1
  423. package/dist/features/transfer/feature.js +1 -1
  424. package/dist/features/transfer/feature.js.map +1 -1
  425. package/dist/features/transfer/transforms.d.ts +4 -4
  426. package/dist/features/transfer/transforms.js +12 -12
  427. package/dist/features/transfer/transforms.js.map +1 -1
  428. package/dist/i18n/de/ai.js +4 -4
  429. package/dist/i18n/de/ai.js.map +1 -1
  430. package/dist/i18n/de/authors.js +1 -1
  431. package/dist/i18n/de/authors.js.map +1 -1
  432. package/dist/i18n/de/comments.js +1 -1
  433. package/dist/i18n/de/comments.js.map +1 -1
  434. package/dist/i18n/de/composites.js +62 -59
  435. package/dist/i18n/de/composites.js.map +1 -1
  436. package/dist/i18n/de/image-studio.js +2 -2
  437. package/dist/i18n/de/image-studio.js.map +1 -1
  438. package/dist/i18n/de/integrations.js +20 -20
  439. package/dist/i18n/de/integrations.js.map +1 -1
  440. package/dist/i18n/de/markdown.js +1 -1
  441. package/dist/i18n/de/markdown.js.map +1 -1
  442. package/dist/i18n/de/palette.js +1 -1
  443. package/dist/i18n/de/palette.js.map +1 -1
  444. package/dist/i18n/de/themes.js +3 -3
  445. package/dist/i18n/de/themes.js.map +1 -1
  446. package/dist/i18n/de/transfer.js +2 -2
  447. package/dist/i18n/de/transfer.js.map +1 -1
  448. package/dist/i18n/de/tree.js +6 -6
  449. package/dist/i18n/de/tree.js.map +1 -1
  450. package/dist/i18n/de.d.ts +101 -98
  451. package/dist/i18n/en/ai.js +2 -2
  452. package/dist/i18n/en/ai.js.map +1 -1
  453. package/dist/i18n/en/composites.js +64 -61
  454. package/dist/i18n/en/composites.js.map +1 -1
  455. package/dist/i18n/en/integrations.js +9 -9
  456. package/dist/i18n/en/integrations.js.map +1 -1
  457. package/dist/i18n/en/markdown.js +1 -1
  458. package/dist/i18n/en/markdown.js.map +1 -1
  459. package/dist/i18n/en/palette.js +1 -1
  460. package/dist/i18n/en/palette.js.map +1 -1
  461. package/dist/i18n/en/themes.js +4 -4
  462. package/dist/i18n/en/themes.js.map +1 -1
  463. package/dist/i18n/en/tree.js +5 -5
  464. package/dist/i18n/en/tree.js.map +1 -1
  465. package/dist/index.d.ts +4 -1
  466. package/dist/integrations/composite-ai/bibliography-fetch.d.ts +6 -6
  467. package/dist/integrations/composite-ai/bibliography-fetch.js +19 -19
  468. package/dist/integrations/composite-ai/bibliography-fetch.js.map +1 -1
  469. package/dist/integrations/composite-ai/composite-fill.d.ts +4 -4
  470. package/dist/integrations/composite-ai/composite-fill.js +5 -5
  471. package/dist/integrations/composite-ai/composite-fill.js.map +1 -1
  472. package/dist/integrations/composite-ai/composite-summary.d.ts +2 -2
  473. package/dist/integrations/composite-ai/composite-summary.js +5 -5
  474. package/dist/integrations/composite-ai/composite-summary.js.map +1 -1
  475. package/dist/integrations/composite-ai/derive.d.ts +17 -17
  476. package/dist/integrations/composite-ai/derive.js +27 -26
  477. package/dist/integrations/composite-ai/derive.js.map +1 -1
  478. package/dist/integrations/composite-ai/index.d.ts +14 -15
  479. package/dist/integrations/composite-ai/index.js +98 -77
  480. package/dist/integrations/composite-ai/index.js.map +1 -1
  481. package/dist/integrations/composite-ai/outline-sanitize.d.ts +1 -1
  482. package/dist/integrations/composite-ai/outline-sanitize.js +1 -1
  483. package/dist/integrations/composite-ai/outline-sanitize.js.map +1 -1
  484. package/dist/integrations/composite-ai/rendition.d.ts +9 -9
  485. package/dist/integrations/composite-ai/rendition.js +27 -27
  486. package/dist/integrations/composite-ai/rendition.js.map +1 -1
  487. package/dist/integrations/composite-ai/split.d.ts +8 -8
  488. package/dist/integrations/composite-ai/split.js +12 -12
  489. package/dist/integrations/composite-ai/split.js.map +1 -1
  490. package/dist/integrations/composite-ai/transfer-transforms.d.ts +2 -2
  491. package/dist/integrations/composite-ai/transfer-transforms.js +12 -12
  492. package/dist/integrations/composite-ai/transfer-transforms.js.map +1 -1
  493. package/dist/integrations/composite-media/index.d.ts +4 -3
  494. package/dist/integrations/composite-media/index.js +20 -14
  495. package/dist/integrations/composite-media/index.js.map +1 -1
  496. package/dist/markdown/document.js +1 -1
  497. package/dist/markdown/document.js.map +1 -1
  498. package/dist/nuxt/module.d.ts +30 -24
  499. package/dist/nuxt/module.js.map +1 -1
  500. package/dist/nuxt/runtime/server/plugins/editor-server.js +3 -2
  501. package/dist/nuxt/runtime/server/plugins/editor-server.js.map +1 -1
  502. package/dist/presets/compose.d.ts +6 -6
  503. package/dist/presets/compose.js +8 -5
  504. package/dist/presets/compose.js.map +1 -1
  505. package/dist/react/AdvancedEditor.d.ts +2 -2
  506. package/dist/react/AdvancedEditor.js +1 -1
  507. package/dist/react/AdvancedEditor.js.map +1 -1
  508. package/dist/react/compose.d.ts +1 -1
  509. package/dist/react/compose.js +2 -2
  510. package/dist/react/compose.js.map +1 -1
  511. package/dist/react/composed-surface.d.ts +5 -5
  512. package/dist/react/composed-surface.js +2 -2
  513. package/dist/react/composed-surface.js.map +1 -1
  514. package/dist/server/ai.d.ts +7 -7
  515. package/dist/server/ai.js +1 -1
  516. package/dist/server/ai.js.map +1 -1
  517. package/dist/server/auth.d.ts +1 -1
  518. package/dist/server/collab-state.d.ts +1 -1
  519. package/dist/server/collab-state.js.map +1 -1
  520. package/dist/server/collab.d.ts +4 -4
  521. package/dist/server/collab.js +22 -17
  522. package/dist/server/collab.js.map +1 -1
  523. package/dist/server/handlers/ai.d.ts +7 -4
  524. package/dist/server/handlers/ai.js +13 -17
  525. package/dist/server/handlers/ai.js.map +1 -1
  526. package/dist/server/handlers/context.d.ts +4 -0
  527. package/dist/server/handlers/context.js.map +1 -1
  528. package/dist/server/handlers/editor.d.ts +33 -32
  529. package/dist/server/handlers/editor.js +13 -8
  530. package/dist/server/handlers/editor.js.map +1 -1
  531. package/dist/server/handlers/images.d.ts +1 -1
  532. package/dist/server/handlers/images.js +1 -1
  533. package/dist/server/handlers/images.js.map +1 -1
  534. package/dist/server/host.d.ts +4 -0
  535. package/dist/server/host.js +10 -2
  536. package/dist/server/host.js.map +1 -1
  537. package/dist/server/index.d.ts +3 -3
  538. package/dist/server/ports.d.ts +8 -8
  539. package/dist/server/ports.js.map +1 -1
  540. package/dist/settings/index.d.ts +20 -19
  541. package/dist/settings/index.js +25 -24
  542. package/dist/settings/index.js.map +1 -1
  543. package/dist/settings/migrate.d.ts +18 -0
  544. package/dist/settings/migrate.js +46 -0
  545. package/dist/settings/migrate.js.map +1 -0
  546. package/dist/settings/values.d.ts +12 -12
  547. package/dist/settings/values.js +11 -11
  548. package/dist/settings/values.js.map +1 -1
  549. package/dist/styles/composites.css +10 -10
  550. package/dist/tree/DocumentTree.js +12 -5
  551. package/dist/tree/DocumentTree.js.map +1 -1
  552. package/dist/tree/drop-rules.js +5 -5
  553. package/dist/tree/drop-rules.js.map +1 -1
  554. package/dist/tree/react/DocumentTree.js +14 -5
  555. package/dist/tree/react/DocumentTree.js.map +1 -1
  556. package/dist/tree/tree-host.js +6 -6
  557. package/dist/tree/tree-host.js.map +1 -1
  558. package/dist/tree/tree-items.js +34 -28
  559. package/dist/tree/tree-items.js.map +1 -1
  560. package/dist/tree/tree-model.js +16 -16
  561. package/dist/tree/tree-model.js.map +1 -1
  562. package/dist/tree/tree-rows.js +2 -2
  563. package/dist/tree/tree-rows.js.map +1 -1
  564. package/dist/tree/tree-view-model.d.ts +2 -2
  565. package/dist/tree/tree-view-model.js.map +1 -1
  566. package/dist/vue/AdvancedEditor.js +2 -2
  567. package/dist/vue/AdvancedEditor.js.map +1 -1
  568. package/dist/vue/AdvancedEditor.vue.d.ts +6 -6
  569. package/dist/vue/compose.d.ts +1 -1
  570. package/dist/vue/compose.js +2 -2
  571. package/dist/vue/compose.js.map +1 -1
  572. package/package.json +1 -1
  573. package/dist/features/composites/derivation.d.ts +0 -62
  574. package/dist/features/composites/derivation.js +0 -102
  575. package/dist/features/composites/derivation.js.map +0 -1
  576. package/dist/features/composites/kinds.d.ts +0 -89
  577. package/dist/features/composites/kinds.js +0 -285
  578. package/dist/features/composites/kinds.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.0 - 2026-10-21
4
+
5
+ ### Added
6
+
7
+ - Block vocabulary: `BlockType`, `BlockTypeAi`, `defaultBlockTypes`, `blockTypeOf`, `acceptsChild` (`/composites`, `/composites/schema`; code in `features/composites/block-types.ts`), `PropertyDefinition` and `derivedChildrenOf` (`/core`), `ModelRole` / `MODEL_ROLES` (stored as `ai.roles`, see Changed, "Persisted names (settings)"), `ModelPresetConfig` (`/config`, `/server`), `AccessControl` and `DocumentContentStore` of the server ports (`/server`). The former names are listed under Removed.
8
+ - `blockTypes` option: `compositeFeature({ blockTypes })`, `compositeSchemaExtensions({ blockTypes })`, `composeEditor({ blockTypes })`, the `blockTypes` prop of `AdvancedEditor` and `ComposedSurface`.
9
+ - `contentTemplate` on a Block type's `ai` brief and as the AI wire field `block.contentTemplate` (client and server of the AI endpoints ship in the same package).
10
+ - Block capabilities (`@ecsplico/editor/core`): `EditorFeature.blockCapabilities` declares `BlockCapability` descriptors (`id`, `appliesTo(node, { blockTypeId })`, optional `storage` description); `defineEditor` merges them and rejects a duplicate id, also against the built-in `container` (nodes declaring `editorBlockContainer`) and `title` (first child is chrome). `capabilitiesAt(editor, pos)` / `capabilitiesOf(editor, node)` answer which capabilities a block has, memoised per node type and Block type id (`attrs.blockType`), so predicates read only the node type and `blockTypeId`. A capability describes and never installs schema; abilities stay the permission mechanism. `BlockAction.requires` lists capability ids the block must have; `blockActionsFor` hides the action otherwise and `blockActionsOf` throws when an action requires an id no Feature declares. `EditorPreset.capabilities` is the merged `CapabilityIndex`.
11
+ - Block types as data: `BlockType` is `id`, presentation (`label`, `icon`, `titlePlaceholder`, `descriptionPlaceholder`, `chrome`) and `capabilities: { id, config? }[]` (`BlockTypeData` / `BlockTypeCapability` in `@ecsplico/editor/core`). The former switches move into capability entries: `fields` → `properties` (`{ definitions }`), `source` → `source` (`{ media }`), `accepts` → `container` (`{ accepts }`), `ai` → `ai-brief`, plus the new `basket` and `bibliography` entries. Accessors: `propertiesOf`, `sourceMediaOf`, `aiBriefOf`, `acceptsOf`, `blockTypeConfigOf` (`/composites`, `/composites/schema`).
12
+ - The composites feature declares the `properties`, `source`, `bibliography`, `basket`, `ai-brief` and `references` capabilities through `blockCapabilities`; core only has `container` and `title`. `BlockCapability` gains `configSchema` (zod, validates the config a Block type gives) and `read(node, ctx)` (the contents or value in a block: the source child, the basket id, the bibliography record); `appliesTo` stays type-level and memoised per node type and Block type id. `CapabilityContext` gains `blockTypeOf(node)` and `configOf(node, capability)`. New exports (`@ecsplico/editor/core`): `capabilityConfigOf`, `capabilityDataOf`, `blockTypeIdOf`, `listsCapability`, `createCapabilityRegistry`, `validateBlockTypes`, `EditorFeature.blockTypes`, `BlockAction.blockTypes` (type-specific actions only). An editor checks every Block type when it is created: an unknown capability id or an invalid config throws.
13
+ - Derive methods (`@ecsplico/editor/core`): `EditorFeature.deriveMethods` contributes `DeriveMethod` entries (`id`, `label`, `icon`, `ability?`, `order?`, `placement?`, `requires` capability ids on the original, `available(ctx)` for backend flags and per-block conditions, `run` or `menu`). `defineEditor` merges them into `preset.deriveMethods` (`DeriveRegistry`) and rejects a duplicate id, a method with neither or both of `run` and `menu`, and a `requires` entry no feature declares; `blockActionsOf` turns every method into a block action (`deriveActionsOf`), `deriveMethodsAt(editor, ctx, pos)` lists the methods that can run on a block. New `derived` capability (declared by the composites feature; a block has it when `meta.derivation` is present, so `BlockCapability.perNode` lets a capability decide per node instead of per node type) and the memoised reverse index `derivationIndexOf(doc)` / `derivedOf(doc, pos)` / `derivationOriginOf(doc, pos, documentId?)`, computed from block IDs per document node; the original stores no back-pointer.
14
+ - Block types from a JSON file (`@ecsplico/editor/composites`, `/composites/schema`): `parseBlockTypes(input, { registry? })` reads a `BlockType[]` (the file's text or its parsed value) and checks every entry against the capability registry (unknown capability, capability listed twice, invalid `config` per the capability's `configSchema`, duplicate id); it throws one `BlockTypeJsonError` that names the Block type, the capability and the path for every problem (`issues` carries them as data). `mergeBlockTypes(base, extra)` layers the file over `defaultBlockTypes(t)`; the result goes to the existing `blockTypes` option. `examples/vue-two-pane` defines the Block type „Idee“ only in `shared/block-types.json`.
15
+ - Block types on the server: `installEditorServer({ blockTypes })` (`EditorServerOptions.blockTypes`), `AiHandlerContext.blockTypes` and `setEditorPorts(ports, { blockTypes })` (Nuxt) give the server the host's Block types. `POST /api/ai/chat` then describes them to the model and ignores the `blockTypes` of the request; without them it behaves as before. `aiBlockTypesOf(blockTypes)` (`/composites`, `/composites/schema`) builds the chat's Block type list for the editor's `aiContext` and the server alike.
16
+ - One Property definition for every Block (`PropertyDefinition`, `PROPERTY_TYPES` in `@ecsplico/editor/core`; `BlockType`'s `properties` config, the inspector, `AiContextBlockType.fields` and the server's chat schema use it). The `properties` capability applies to any node with a `meta` attribute whose Block type lists it. New helpers `carriesProperties(node)`, `blockPropertiesAt(editor, $pos, blockTypes)`, `propertyHostOf(doc, target)`, `blockPropertiesOf`, `setBlockProperty(editor, target, key, value)` and `metaWithProperty` (`/composites`); `CompositeInspectorState` gains `properties` (the Block properties at the caret) and the Vue `useCompositeInspector` a `properties` ref.
17
+ - The Block inspector shows a Block properties section for any block that has the `properties` capability (heading „Blockeigenschaften“ / "Block properties"), driven by the definitions of its Block type; `CompositeInspector` and `CompositeFieldEditor` write through `setBlockProperty`. Storage, Markdown fences, copy/paste and the Yjs state are unchanged.
18
+ - The Block inspector shows the Derivation of every Derived block, whatever its Block type, with the source-missing state („Quelle fehlt“ / "Source missing"); a Block type that also has a `derivation` Property definition shows it once (`CompositeInspectorState.derivation`, `InspectorDerivation`).
19
+ - The `container` config gains `root: true`: the document tree takes the Block type that sets it as the type of its document rows (`isRootBlockType`, `/composites`); the built-in Document type sets it, and no code looks the type up by its id any more.
20
+ - `capabilityIssues(registry, type)` (`/core`) is the one validator of a Block type's capabilities: `validateBlockTypes` throws on its issues and `parseBlockTypes` collects them. The wording of the unknown, duplicate and invalid-config messages changed.
21
+ - Documentation: the guides [Block capabilities and actions](https://skaile-ai.github.io/skaile-editor/guides/block-capabilities), [Block types in code and JSON](https://skaile-ai.github.io/skaile-editor/guides/block-types), [Derive methods and the Derivation index](https://skaile-ai.github.io/skaile-editor/guides/derive-methods) and [Block properties](https://skaile-ai.github.io/skaile-editor/guides/block-properties), and the reference pages Block capability contract, Block type contract and Derive method contract. Their code samples come from `docs/snippets`, which `bun run check:docs-snippets` keeps in sync.
22
+ - `BlockAction.target?: 'block' | 'typedBlock'` (`/core`, default `'block'`): on the toolbar surface an action with `target: 'typedBlock'` is checked against and runs with the Typed block around the selected block (the block itself when it is one), so its split, merge, export, bibliography and focus actions stay on the toolbar while the caret is in a paragraph inside the Typed block; with no Typed block around, the action is absent. `blockActionsFor` evaluates `ability`, `requires`, `blockTypes` and `isVisible` against that Typed block and passes its `node` and `pos` in the context; the hover bar ignores the field, and the bar still docks at the selected block. `typedBlockAround(editor, target)` (`/block-ui`) finds the Typed block. The composites feature's eight Typed-block actions declare `target: 'typedBlock'`.
23
+
24
+ ### Fixed
25
+
26
+ - The workspace tag list (`GET /api/editor/tags`) again offers the tags of stored documents that still use the old persisted names; it reads them through the old-name migration.
27
+
28
+ ### Changed
29
+
30
+ - Inspector, details popover, table, tree, outline and the AI brief read capabilities instead of Block type fields. The Basket menu and stripe need the `basket` capability, the BibTeX export action the `bibliography` capability, the Block properties the `properties` capability; a host Block type that relied on the former defaults must list them (migration guide, "Block types as data").
31
+ - Block actions decide by `requires` (BibTeX export, source replacement, AI fill, readable rendition, literature lookup) instead of node-name and Block-type-name checks.
32
+ - The Focus block action (`composite-focus`) is gated by `requires: ['container']` instead of a node-name check; it still appears only on Typed blocks.
33
+ - The built-in Folder and Document Block types declare `accepts: 'all'` themselves (in their `container` config); the id-keyed default is gone. A Block type without `accepts`, and a type id a list does not define, accepts no nested Typed blocks (`acceptsChild`); a host list that defines its own `folder` or `document` type must set `accepts` on it.
34
+ - Derivation lives in core: `Derivation`, `derivationOf`, `derivedChildrenOf`, `derivationState` and `DERIVATION_KEY` moved from `features/composites/derivation.ts` to `core/derivation.ts` (import them from `@ecsplico/editor/core`; the Source-media helpers `sourceOf`, `bodyMarkdown` and `SourceBlock` stay in `features/composites/source.ts`). **`derivationState` signature changed**: it takes `(doc, derivedPos, bodyHash, documentId?)` and finds the original itself (was `(composite, derived, bodyHash, kinds, documentId?)`), and its `source` field is `'found' | 'missing' | 'foreign'` (was `foreign: boolean`). A Derived block whose original was deleted or is gone from the document stays and shows „Quelle fehlt“ / „Source missing“ in the chrome row, the details card, the Block inspector and the Block outline (`OutlineNode.sourceMissing`, `TreeItem.sourceMissing`).
35
+ - The block actions `composite-fill`, `composite-summarize`, `composite-derive`, `bibliography-fetch`, `rendition-create` and `rendition-regenerate` are generated from derive methods; ids, labels, order and abilities are unchanged. `rendition-regenerate` now requires the `derived` capability instead of `container` and `title`.
36
+ - The Document Block type is plain configuration; no code tests the type id `document` any more. New capabilities of the composites feature: `export` (the block action „Exportieren“, `document-export`, is `requires: ['export']` instead of `blockTypes: ['document']`; the built-in Document type lists it, a host type may too) and `import` (config `{ media?: SourceMedia[] }`: the imported-file wrapping of `composite-media` and "Datei importieren" use the type whose `import` takes the file's media, by default `recording` for audio/video, `figure` for images and `document` as the catch-all; the tree's "Datei importieren" entry is offered below Typed blocks whose type accepts such a type). New helpers `importTypeOf`, `acceptsImport` (`/composites`, `/composites/schema`). A host Block type list that wrapped files by the ids `document`/`recording`/`figure` must list `import` on them. The server's legacy migration of the flat `meta.transcript` keys on that property (`hasFlatTranscript`), not on the Block type; stored documents open as before.
37
+ - Block UI addresses the outer block: with the caret or pointer inside a list item or a blockquote paragraph, the hover bar, the toolbar block actions (`selectedBlockOf`) and the drag handle act on the whole list or quote, because list items are not Blocks of their own. The rule is the outermost block below the nearest block container (`doc` or a Container block such as a Typed block); new framework-free resolvers `outerBlocksAt($pos)`, `outerBlockAt($pos)` and the drag-handle rule `outerBlockRule` (`@ecsplico/editor/block-ui`). Nested paragraphs keep their block IDs. `handleLeftOf` no longer takes a node; `gutterShiftOf` is removed.
38
+ - UI labels in the agreed vocabulary. English: „Building block“ → „Typed block“, „Kind“ → „Block type“ (inspector, table, filter, icon setting, default type), the rendition type → „Readable rendition“, „Darstellung“ → „Theme“ with „Default appearance“, the inspector's document section → „Document facts“, the structure setting → „Structure mode“ / „Flat mode“, „Baustein“ in reference chips → „block“. German: Baustein → Block (palette group „Typisierte Blöcke“), Bausteinart / Art → Blocktyp; Korb and Lesefassung stay. Message keys are unchanged.
39
+ - The library, the reference app, the examples and the docs use only the new names. The tree command `createBausteinFolder` is now `createFolderBlock` (`TreeCommand`, `@ecsplico/editor/tree`); a host executor that switches on the command type must use the new string. German demo and error strings that said Baustein now say Block or Dokument (`Keine gültige Dokumentdatei`).
40
+ - **Persisted names (documents).** The node `composite` is stored as `typedBlock`, its chrome nodes `compositeTitle`/`compositeDescription`/`compositeTags` as `typedBlockTitle`/`typedBlockDescription`/`typedBlockTags`, and the attribute `kind` of a Typed block and of `opaqueFence` as `blockType` (block IDs, `meta` and the `::: <Block type id>` Markdown fences are unchanged). HTML: `section[data-type="typedBlock"][data-block-type]`; the old `data-type="composite"` / `data-kind` markup and the legacy `section` markup still parse. `BlockTransfer.kind` is `BlockTransfer.blockType` (an old drag payload is read). The editor's node storage is `editor.storage.typedBlock` (was `composite`); the `ConvertTarget` of a Typed block is `{ type: 'typedBlock', blockType }` (id `typedBlock:<id>`); `data-kind-tone` is `data-block-type-tone`. Everything stored before loads through one migration (`migratePersistedNames` in `core/schema/persisted-names.ts`, run by `migrateLegacyContent`, `defineEditor` ahead of any contributed `migrateContent`, the server's `contentNode` and, for Yjs state, by `onLoadDocument` before any client syncs); old names are read, never written; the Yjs migration is idempotent and keeps block IDs and content ([migration guide](https://skaile-ai.github.io/skaile-editor/guides/migration), "Persisted names").
41
+ - **Persisted names (settings).** Settings version 2: `ai.slots` is `ai.roles`, `blocks.defaultKind` is `blocks.defaultBlockType`, `blocks.kindIcon` is `blocks.blockTypeIcon`, and the `blocks.structure` value `'composite'` is `'structured'` (`STRUCTURE_MODES = ['flat', 'structured']`). Saved settings (server store, `localStorage`) migrate on load through `migrateEditorSettings` and are rewritten once. `KIND_ICON_VISIBILITIES` / `KindIconVisibility` are `BLOCK_TYPE_ICON_VISIBILITIES` / `BlockTypeIconVisibility` (`/settings`). Host config: `ai.slots` is `ai.roles` (`EditorConfig`, `publicConfig`, `serverConfig`, `DirectAiOptions`); this needs a code change, there is no migration. `GET /api/ai/models` answers `{ presets, roles }` (was `slots`); client and server ship in the same package.
42
+ - The rename of old persisted names always runs: `defineEditor` applies it ahead of any contributed `migrateContent`, so a host hook receives current names and can no longer drop it.
43
+ - Further vocabulary renames (migration guide, "Block vocabulary"): `aiKindsOf` → `aiBlockTypesOf`, `derivableKindsOf` → `derivableBlockTypesOf`, `AiContextKind` → `AiContextBlockType`, `AiContext.kinds` / `ChatBlockModel.kinds` / the AI chat request field `kinds` → `blockTypes`, `ConvertTarget.kind` → `blockType`, `BlockProperties.fields` → `definitions`, the settings tab prop `kinds` → `blockTypes`, CSS `--sk-kind-tone` / `--sk-kind-tone-mix` → `--sk-block-type-tone` / `--sk-block-type-tone-mix`. One helper each for a Block type's capabilities: `listsCapability(type, id)` and `blockTypeConfigOf(type, id)` in `/core` (replacing `listsBlockTypeCapability` and the `(ctx, node, id)` form); `configOf` and `capabilityConfigOf` return `undefined` when absent (was `null`). `BlockTypeSpec` is gone; use `BlockType`.
44
+ - The Derivation helpers and `PropertyDefinition` are exported only from `@ecsplico/editor/core` (no longer re-exported from `/composites` and `/composites/schema`).
45
+
46
+ ### Removed
47
+
48
+ **Breaking.** The deprecated aliases and the remaining old names are gone; migrate to the new names ([migration guide](https://skaile-ai.github.io/skaile-editor/guides/migration), "Block vocabulary"). Persisted names are covered under Changed ("Persisted names").
49
+
50
+ | Removed | Use | Entry |
51
+ |---|---|---|
52
+ | `CompositeKind`, `CompositeKindAi`, `CompositeMetaField` | `BlockType`, `BlockTypeAi`, `PropertyDefinition` | `/composites`, `/composites/schema`; `PropertyDefinition` from `/core` |
53
+ | `CompositeKind.nestedAfterContent` (3.6.0) | `nestedAfterContent` in the Block type's `container` config (`{ id: 'container', config: { accepts, nestedAfterContent: true } }`; `nestedAfterContentOf(type)`) | `/composites`, `/composites/schema` |
54
+ | `defaultCompositeKinds`, `fallbackCompositeKinds`, `compositeKind`, `kindAccepts` | `defaultBlockTypes`, `fallbackBlockTypes`, `blockTypeOf`, `acceptsChild` | `/composites`, `/composites/schema` |
55
+ | `compositeKindsOf(editor)` | `blockTypesOf(editor)` | `/composites`, `/composites/schema` |
56
+ | `setCompositeKind(kind, pos?)` (editor command) | `changeBlockType(blockTypeId, pos?)` (not `setBlockType`: ProseMirror uses that name for node-type changes) | editor commands |
57
+ | `CompositeKindCount`, `CompositeKindOption`, `ChatCompositeKind` | `BlockTypeCount`, `BlockTypeOption`, `ChatBlockType` | `/composites`, `/ai` |
58
+ | `renditionsOf` | `derivedChildrenOf` | `/core` |
59
+ | `compositeMetaWith`, `setCompositeMetaField` | `metaWithProperty`, `setBlockProperty` | `/composites` |
60
+ | the `kinds` and `compositeKinds` options (`compositeFeature`, `compositeSchemaExtensions`, `composeEditor`, `AdvancedEditor`, `ComposedSurface`) | `blockTypes` | |
61
+ | `skeleton` in the `ai-brief` config and `BlockTypeAi`; the AI wire field `block.skeleton` (`AI_LIMITS.block.skeleton`, the server schema) | `contentTemplate` | `/composites`, `/ai`, `/server` |
62
+ | `ModelSlot`, `MODEL_SLOTS`, `MODEL_SLOT_KEYS` | `ModelRole`, `MODEL_ROLES`, `MODEL_ROLE_KEYS` | `/settings` |
63
+ | `PresetConfig` | `ModelPresetConfig` | `/config`, `/server` |
64
+ | `Identity`, `DocumentStore` (server ports) | `AccessControl`, `DocumentContentStore` | `/server` |
65
+
66
+ The Document tree's own `DocumentStore` (`/tree`) keeps its name.
67
+
3
68
  ## 3.6.0 - 2026-10-21
4
69
 
5
70
  ### Added
@@ -1,7 +1,7 @@
1
- import { ModelSlot } from "../settings/values.js";
1
+ import { ModelRole } from "../settings/values.js";
2
2
  import { EditorBackend } from "../core/backend.js";
3
3
  //#region src/adapters/direct-ai.d.ts
4
- /** A model the browser calls directly: `endpoint` is the OpenAI-compatible base URL (`<endpoint>/chat/completions` is called), like `PresetConfig.endpoint`. */
4
+ /** A model the browser calls directly: `endpoint` is the OpenAI-compatible base URL (`<endpoint>/chat/completions` is called), like `ModelPresetConfig.endpoint`. */
5
5
  export interface DirectAiPreset {
6
6
  label: string;
7
7
  /** Base URL, e.g. `https://openrouter.ai/api/v1`. */
@@ -13,8 +13,8 @@ export interface DirectAiPreset {
13
13
  export interface DirectAiOptions {
14
14
  /** The models the editor offers, keyed by preset id. At least one. */
15
15
  presets: Record<string, DirectAiPreset>;
16
- /** Default preset per model slot, answered by `models()`; the first preset for unset slots. */
17
- slots?: Partial<Record<ModelSlot, string>>;
16
+ /** Default preset per Model role, answered by `models()`; the first preset for unset roles. */
17
+ roles?: Partial<Record<ModelRole, string>>;
18
18
  /** Usernames offered to tasks with `needsUsers` (`@` mentions); none by default. */
19
19
  users?: () => Promise<string[]> | string[];
20
20
  /** The `fetch` the model calls go through (default: the platform's). */
@@ -1,6 +1,6 @@
1
1
  import { BackendError } from "../core/backend.js";
2
2
  import { completeBodySchema, runCompletion } from "../features/ai/protocol/complete.js";
3
- import { MODEL_SLOTS } from "../settings/values.js";
3
+ import { MODEL_ROLES } from "../settings/values.js";
4
4
  import { AI_TASKS } from "../features/ai/protocol/tasks.js";
5
5
  import { generateBodySchema, generateProblem, runGenerate } from "../features/ai/protocol/generate.js";
6
6
  import { TranscriptCleanupUnreadableError, runTranscriptCleanup, transcriptCleanupRequestSchema } from "../integrations/transcript-ai/protocol.js";
@@ -120,7 +120,7 @@ function createDirectAi(options) {
120
120
  model,
121
121
  source: "preset"
122
122
  })),
123
- slots: Object.fromEntries(MODEL_SLOTS.map((slot) => [slot, options.slots?.[slot] ?? ids[0]]))
123
+ roles: Object.fromEntries(MODEL_ROLES.map((role) => [role, options.roles?.[role] ?? ids[0]]))
124
124
  }),
125
125
  complete: async (body, signal) => {
126
126
  const { before, model } = parseBody(completeBodySchema, body);
@@ -1 +1 @@
1
- {"version":3,"file":"direct-ai.js","names":[],"sources":["../../src/adapters/direct-ai.ts"],"sourcesContent":["// i18n-ignore-file: error messages of the browser-direct AI transport (German whatever the editor's language)\nimport type { z } from 'zod'\nimport { BackendError, type EditorBackend } from '../core/backend'\nimport type { ModelProfile, ModelRef } from '../core/backend-types'\nimport { completeBodySchema, runCompletion } from '../features/ai/protocol/complete'\nimport { type ChatMessage, type ChatStream, generateBodySchema, generateProblem, runGenerate } from '../features/ai/protocol/generate'\nimport { AI_TASKS } from '../features/ai/protocol/tasks'\nimport { runTranscriptCleanup, TranscriptCleanupUnreadableError, transcriptCleanupRequestSchema } from '../integrations/transcript-ai/protocol'\nimport { MODEL_SLOTS, type ModelSlot } from '../settings/values'\n\n// The browser-direct adapter of `EditorBackend['ai']`: the model calls of `generate`, `complete`, `models` and\n// `transcriptCleanup` go from the page straight to an OpenAI-compatible chat-completions endpoint, with no `/api/ai`\n// route in between. It runs the same protocol modules the server handlers run (`runGenerate`, `runCompletion`,\n// `runTranscriptCleanup`), so prompts, limits and results are identical. What it cannot do without a server (the\n// document chat's tool calls, `extract`'s transcription and OCR) answers 501.\n\n/** A model the browser calls directly: `endpoint` is the OpenAI-compatible base URL (`<endpoint>/chat/completions` is called), like `PresetConfig.endpoint`. */\nexport interface DirectAiPreset {\n label: string\n /** Base URL, e.g. `https://openrouter.ai/api/v1`. */\n endpoint: string\n model: string\n /** Sent as `Authorization: Bearer …`. It is part of the page, so anyone who can open the page can read it. */\n apiKey?: string\n}\n\nexport interface DirectAiOptions {\n /** The models the editor offers, keyed by preset id. At least one. */\n presets: Record<string, DirectAiPreset>\n /** Default preset per model slot, answered by `models()`; the first preset for unset slots. */\n slots?: Partial<Record<ModelSlot, string>>\n /** Usernames offered to tasks with `needsUsers` (`@` mentions); none by default. */\n users?: () => Promise<string[]> | string[]\n /** The `fetch` the model calls go through (default: the platform's). */\n fetch?: typeof fetch\n}\n\n/** The most characters of an upstream error body that go into the error message. */\nconst MAX_ERROR_DETAIL = 300\n\n/** Parses `body` with `schema`; a mismatch is a 400 like the server's validation error. */\nfunction parseBody<S extends z.ZodType>(schema: S, body: unknown): z.infer<S> {\n const parsed = schema.safeParse(body)\n if (parsed.success) return parsed.data\n const [issue] = parsed.error.issues\n throw new BackendError(`Ungültige Anfrage${issue ? `: ${[...issue.path, issue.message].join(' ')}` : ''}`, 400, { cause: parsed.error })\n}\n\n/** The `data:` payloads of an SSE body, in order; stops at the end of the body. */\nasync function* sseData(body: ReadableStream<Uint8Array>): AsyncGenerator<string> {\n const reader = body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n try {\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n let newline = buffer.indexOf('\\n')\n while (newline !== -1) {\n const line = buffer.slice(0, newline).replace(/\\r$/, '')\n buffer = buffer.slice(newline + 1)\n if (line.startsWith('data:')) yield line.slice(5).trim()\n newline = buffer.indexOf('\\n')\n }\n }\n const rest = (buffer + decoder.decode()).trim()\n if (rest.startsWith('data:')) yield rest.slice(5).trim()\n }\n finally {\n await reader.cancel().catch(() => undefined)\n }\n}\n\n/** One chunk of a chat-completions stream: the text delta, or a provider error sent in the stream. */\ninterface CompletionChunk { choices?: { delta?: { content?: unknown } }[], error?: { message?: unknown } | string }\n\n/**\n * Streams the text deltas of a chat completion from `<endpoint>/chat/completions`. A refused request (non-2xx, or the\n * endpoint unreachable) throws a `BackendError` before anything is yielded, with the upstream status (502 when there\n * was none); an error inside the stream throws a plain `Error`.\n */\nasync function* streamChat(send: typeof fetch, { endpoint, model, apiKey }: { endpoint: string, model: string, apiKey?: string }, messages: ChatMessage[], signal: AbortSignal): AsyncGenerator<string> {\n let response: Response\n try {\n response = await send(`${endpoint.replace(/\\/+$/, '')}/chat/completions`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}) },\n body: JSON.stringify({ model, messages, stream: true }),\n signal,\n })\n }\n catch (error) {\n if (signal.aborted) throw error\n throw new BackendError(`KI-Endpunkt nicht erreichbar: ${error instanceof Error ? error.message : String(error)}`.slice(0, 500), 502, { cause: error })\n }\n if (!response.ok) {\n const detail = (await response.text().catch(() => '')).slice(0, MAX_ERROR_DETAIL)\n throw new BackendError(`KI-Endpunkt: ${[response.status, detail || response.statusText].filter(Boolean).join(' ')}`, response.status)\n }\n if (!response.body) throw new BackendError('KI-Endpunkt: leere Antwort', 502)\n\n for await (const data of sseData(response.body)) {\n if (data === '[DONE]') return\n let chunk: CompletionChunk | null\n try {\n chunk = JSON.parse(data) as CompletionChunk | null\n }\n catch {\n continue\n }\n if (typeof chunk !== 'object' || chunk === null) continue\n if (chunk.error) {\n const message = typeof chunk.error === 'string' ? chunk.error : chunk.error.message\n throw new Error(typeof message === 'string' && message ? message : 'KI-Endpunkt: Fehler im Stream')\n }\n const text = chunk.choices?.[0]?.delta?.content\n if (typeof text === 'string' && text) yield text\n }\n}\n\n/**\n * An `EditorBackend['ai']` that calls OpenAI-compatible chat-completion endpoints from the browser: `models`,\n * `complete`, `stream('generate')` and `transcriptCleanup`, with the protocol modules the server handlers use.\n * `stream('chat')` and `extract` need a server (tool calls, transcription, OCR) and fail with `BackendError` 501; a\n * host that needs them takes `ai` from `createHttpBackend()` instead.\n *\n * The keys in `presets` are part of the page: whoever can open it can read them, and the per-user rate limit of the\n * server routes does not apply. Use it for local models, tokens the user brings or scoped keys, and keep keys that\n * must stay secret behind the server routes. Combine it with the memory backend:\n * `{ ...createMemoryBackend(), ai: createDirectAi({ presets }) }`.\n */\nexport function createDirectAi(options: DirectAiOptions): NonNullable<EditorBackend['ai']> {\n const ids = Object.keys(options.presets)\n if (!ids.length) throw new Error('createDirectAi: presets is empty')\n const send = options.fetch ?? fetch\n\n const resolve = (ref: ModelRef) => {\n if (!('preset' in ref)) return ref\n const preset = Object.hasOwn(options.presets, ref.preset) ? options.presets[ref.preset] : undefined\n if (!preset) throw new BackendError('Unbekanntes Modell-Preset', 400)\n return preset\n }\n const chatOn = (ref: ModelRef): ChatStream => {\n const model = resolve(ref)\n return (messages, signal) => streamChat(send, model, messages, signal)\n }\n\n return {\n models: () => Promise.resolve({\n presets: Object.entries(options.presets).map(([id, { label, endpoint, model }]): ModelProfile => ({ id, label, endpoint, model, source: 'preset' })),\n slots: Object.fromEntries(MODEL_SLOTS.map(slot => [slot, options.slots?.[slot] ?? ids[0]!])) as Record<ModelSlot, string>,\n }),\n\n complete: async (body, signal) => {\n const { before, model } = parseBody(completeBodySchema, body)\n return { text: await runCompletion(before, chatOn(model), signal) }\n },\n\n async* stream(path, body, signal) {\n if (path === 'chat') throw new BackendError('Chat braucht einen Server (direct-ai)', 501)\n const { task: taskId, model, ...input } = parseBody(generateBodySchema, body)\n const stream = chatOn(model)\n const task = AI_TASKS[taskId]\n const problem = generateProblem(task, input)\n if (problem) throw new BackendError(problem, 400)\n const users = task.needsUsers ? await options.users?.() : undefined\n\n // `runGenerate` turns a failed model call into an `error` event; a refused request (before the first delta) is\n // rejected instead, like the server's non-2xx answer, so the caller can tell it from a failing stream.\n let refused: BackendError | undefined\n let started = false\n const guarded: ChatStream = async function* (messages, streamSignal) {\n try {\n for await (const text of stream(messages, streamSignal)) {\n started = true\n yield text\n }\n }\n catch (error) {\n if (!started && error instanceof BackendError) refused = error\n throw error\n }\n }\n for await (const event of runGenerate(task, input, users, guarded, signal)) {\n if (refused && !signal.aborted) throw refused\n yield event\n }\n },\n\n transcriptCleanup: async (body, signal) => {\n const { segments, model } = parseBody(transcriptCleanupRequestSchema, body)\n try {\n return await runTranscriptCleanup(segments, chatOn(model), signal)\n }\n catch (error) {\n throw error instanceof TranscriptCleanupUnreadableError ? new BackendError(error.message, 502, { cause: error }) : error\n }\n },\n\n extract: () => Promise.reject(new BackendError('Extraktion braucht einen Server (direct-ai)', 501)),\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"direct-ai.js","names":[],"sources":["../../src/adapters/direct-ai.ts"],"sourcesContent":["// i18n-ignore-file: error messages of the browser-direct AI transport (German whatever the editor's language)\nimport type { z } from 'zod'\nimport { BackendError, type EditorBackend } from '../core/backend'\nimport type { ModelProfile, ModelRef } from '../core/backend-types'\nimport { completeBodySchema, runCompletion } from '../features/ai/protocol/complete'\nimport { type ChatMessage, type ChatStream, generateBodySchema, generateProblem, runGenerate } from '../features/ai/protocol/generate'\nimport { AI_TASKS } from '../features/ai/protocol/tasks'\nimport { runTranscriptCleanup, TranscriptCleanupUnreadableError, transcriptCleanupRequestSchema } from '../integrations/transcript-ai/protocol'\nimport { MODEL_ROLES, type ModelRole } from '../settings/values'\n\n// The browser-direct adapter of `EditorBackend['ai']`: the model calls of `generate`, `complete`, `models` and\n// `transcriptCleanup` go from the page straight to an OpenAI-compatible chat-completions endpoint, with no `/api/ai`\n// route in between. It runs the same protocol modules the server handlers run (`runGenerate`, `runCompletion`,\n// `runTranscriptCleanup`), so prompts, limits and results are identical. What it cannot do without a server (the\n// document chat's tool calls, `extract`'s transcription and OCR) answers 501.\n\n/** A model the browser calls directly: `endpoint` is the OpenAI-compatible base URL (`<endpoint>/chat/completions` is called), like `ModelPresetConfig.endpoint`. */\nexport interface DirectAiPreset {\n label: string\n /** Base URL, e.g. `https://openrouter.ai/api/v1`. */\n endpoint: string\n model: string\n /** Sent as `Authorization: Bearer …`. It is part of the page, so anyone who can open the page can read it. */\n apiKey?: string\n}\n\nexport interface DirectAiOptions {\n /** The models the editor offers, keyed by preset id. At least one. */\n presets: Record<string, DirectAiPreset>\n /** Default preset per Model role, answered by `models()`; the first preset for unset roles. */\n roles?: Partial<Record<ModelRole, string>>\n /** Usernames offered to tasks with `needsUsers` (`@` mentions); none by default. */\n users?: () => Promise<string[]> | string[]\n /** The `fetch` the model calls go through (default: the platform's). */\n fetch?: typeof fetch\n}\n\n/** The most characters of an upstream error body that go into the error message. */\nconst MAX_ERROR_DETAIL = 300\n\n/** Parses `body` with `schema`; a mismatch is a 400 like the server's validation error. */\nfunction parseBody<S extends z.ZodType>(schema: S, body: unknown): z.infer<S> {\n const parsed = schema.safeParse(body)\n if (parsed.success) return parsed.data\n const [issue] = parsed.error.issues\n throw new BackendError(`Ungültige Anfrage${issue ? `: ${[...issue.path, issue.message].join(' ')}` : ''}`, 400, { cause: parsed.error })\n}\n\n/** The `data:` payloads of an SSE body, in order; stops at the end of the body. */\nasync function* sseData(body: ReadableStream<Uint8Array>): AsyncGenerator<string> {\n const reader = body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n try {\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n let newline = buffer.indexOf('\\n')\n while (newline !== -1) {\n const line = buffer.slice(0, newline).replace(/\\r$/, '')\n buffer = buffer.slice(newline + 1)\n if (line.startsWith('data:')) yield line.slice(5).trim()\n newline = buffer.indexOf('\\n')\n }\n }\n const rest = (buffer + decoder.decode()).trim()\n if (rest.startsWith('data:')) yield rest.slice(5).trim()\n }\n finally {\n await reader.cancel().catch(() => undefined)\n }\n}\n\n/** One chunk of a chat-completions stream: the text delta, or a provider error sent in the stream. */\ninterface CompletionChunk { choices?: { delta?: { content?: unknown } }[], error?: { message?: unknown } | string }\n\n/**\n * Streams the text deltas of a chat completion from `<endpoint>/chat/completions`. A refused request (non-2xx, or the\n * endpoint unreachable) throws a `BackendError` before anything is yielded, with the upstream status (502 when there\n * was none); an error inside the stream throws a plain `Error`.\n */\nasync function* streamChat(send: typeof fetch, { endpoint, model, apiKey }: { endpoint: string, model: string, apiKey?: string }, messages: ChatMessage[], signal: AbortSignal): AsyncGenerator<string> {\n let response: Response\n try {\n response = await send(`${endpoint.replace(/\\/+$/, '')}/chat/completions`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}) },\n body: JSON.stringify({ model, messages, stream: true }),\n signal,\n })\n }\n catch (error) {\n if (signal.aborted) throw error\n throw new BackendError(`KI-Endpunkt nicht erreichbar: ${error instanceof Error ? error.message : String(error)}`.slice(0, 500), 502, { cause: error })\n }\n if (!response.ok) {\n const detail = (await response.text().catch(() => '')).slice(0, MAX_ERROR_DETAIL)\n throw new BackendError(`KI-Endpunkt: ${[response.status, detail || response.statusText].filter(Boolean).join(' ')}`, response.status)\n }\n if (!response.body) throw new BackendError('KI-Endpunkt: leere Antwort', 502)\n\n for await (const data of sseData(response.body)) {\n if (data === '[DONE]') return\n let chunk: CompletionChunk | null\n try {\n chunk = JSON.parse(data) as CompletionChunk | null\n }\n catch {\n continue\n }\n if (typeof chunk !== 'object' || chunk === null) continue\n if (chunk.error) {\n const message = typeof chunk.error === 'string' ? chunk.error : chunk.error.message\n throw new Error(typeof message === 'string' && message ? message : 'KI-Endpunkt: Fehler im Stream')\n }\n const text = chunk.choices?.[0]?.delta?.content\n if (typeof text === 'string' && text) yield text\n }\n}\n\n/**\n * An `EditorBackend['ai']` that calls OpenAI-compatible chat-completion endpoints from the browser: `models`,\n * `complete`, `stream('generate')` and `transcriptCleanup`, with the protocol modules the server handlers use.\n * `stream('chat')` and `extract` need a server (tool calls, transcription, OCR) and fail with `BackendError` 501; a\n * host that needs them takes `ai` from `createHttpBackend()` instead.\n *\n * The keys in `presets` are part of the page: whoever can open it can read them, and the per-user rate limit of the\n * server routes does not apply. Use it for local models, tokens the user brings or scoped keys, and keep keys that\n * must stay secret behind the server routes. Combine it with the memory backend:\n * `{ ...createMemoryBackend(), ai: createDirectAi({ presets }) }`.\n */\nexport function createDirectAi(options: DirectAiOptions): NonNullable<EditorBackend['ai']> {\n const ids = Object.keys(options.presets)\n if (!ids.length) throw new Error('createDirectAi: presets is empty')\n const send = options.fetch ?? fetch\n\n const resolve = (ref: ModelRef) => {\n if (!('preset' in ref)) return ref\n const preset = Object.hasOwn(options.presets, ref.preset) ? options.presets[ref.preset] : undefined\n if (!preset) throw new BackendError('Unbekanntes Modell-Preset', 400)\n return preset\n }\n const chatOn = (ref: ModelRef): ChatStream => {\n const model = resolve(ref)\n return (messages, signal) => streamChat(send, model, messages, signal)\n }\n\n return {\n models: () => Promise.resolve({\n presets: Object.entries(options.presets).map(([id, { label, endpoint, model }]): ModelProfile => ({ id, label, endpoint, model, source: 'preset' })),\n roles: Object.fromEntries(MODEL_ROLES.map(role => [role, options.roles?.[role] ?? ids[0]!])) as Record<ModelRole, string>,\n }),\n\n complete: async (body, signal) => {\n const { before, model } = parseBody(completeBodySchema, body)\n return { text: await runCompletion(before, chatOn(model), signal) }\n },\n\n async* stream(path, body, signal) {\n if (path === 'chat') throw new BackendError('Chat braucht einen Server (direct-ai)', 501)\n const { task: taskId, model, ...input } = parseBody(generateBodySchema, body)\n const stream = chatOn(model)\n const task = AI_TASKS[taskId]\n const problem = generateProblem(task, input)\n if (problem) throw new BackendError(problem, 400)\n const users = task.needsUsers ? await options.users?.() : undefined\n\n // `runGenerate` turns a failed model call into an `error` event; a refused request (before the first delta) is\n // rejected instead, like the server's non-2xx answer, so the caller can tell it from a failing stream.\n let refused: BackendError | undefined\n let started = false\n const guarded: ChatStream = async function* (messages, streamSignal) {\n try {\n for await (const text of stream(messages, streamSignal)) {\n started = true\n yield text\n }\n }\n catch (error) {\n if (!started && error instanceof BackendError) refused = error\n throw error\n }\n }\n for await (const event of runGenerate(task, input, users, guarded, signal)) {\n if (refused && !signal.aborted) throw refused\n yield event\n }\n },\n\n transcriptCleanup: async (body, signal) => {\n const { segments, model } = parseBody(transcriptCleanupRequestSchema, body)\n try {\n return await runTranscriptCleanup(segments, chatOn(model), signal)\n }\n catch (error) {\n throw error instanceof TranscriptCleanupUnreadableError ? new BackendError(error.message, 502, { cause: error }) : error\n }\n },\n\n extract: () => Promise.reject(new BackendError('Extraktion braucht einen Server (direct-ai)', 501)),\n }\n}\n"],"mappings":"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"}
@@ -7,7 +7,7 @@ export interface EditorDocumentOptions {
7
7
  /** Where documents, the collaboration session and the comments live. Default: the backend of the editor context (`createEditor` / `EditorContextProvider`). Collaboration and comments need one that has them, e.g. `createHttpBackend()` (`@ecsplico/editor/adapters/http`). */
8
8
  backend?: EditorBackend;
9
9
  /**
10
- * Live collaboration: `true`, or `{ token }` for the token the collaboration server's `Identity.authenticate` receives.
10
+ * Live collaboration: `true`, or `{ token }` for the token the collaboration server's `AccessControl.authenticate` receives.
11
11
  * With it the Y.Doc is the document: the server loads and stores the content, so the hook neither loads nor saves it
12
12
  * (`modelValue` stays `undefined`, `dirty` stays `false`, `save()` resolves at once). Needs `backend.collaboration`.
13
13
  */
@@ -1 +1 @@
1
- {"version":3,"file":"document-state.js","names":[],"sources":["../../src/adapters/document-state.ts"],"sourcesContent":["import type { JSONContent } from '@tiptap/core'\nimport type { CollaborationSession } from '../core'\nimport type { CollaborationToken, EditorBackend } from '../core/backend'\nimport type { AuthorIdentity } from '../core/feature'\n\n// What the Vue and React `useEditorDocument` hooks share: their options, the empty document, the \"unsaved edits\"\n// comparison and the collaboration session built from the backend. Framework-free; nothing here talks HTTP, the hooks\n// reach documents, collaboration and comments only through `EditorBackend`.\n\nexport interface EditorDocumentOptions {\n /** Where documents, the collaboration session and the comments live. Default: the backend of the editor context (`createEditor` / `EditorContextProvider`). Collaboration and comments need one that has them, e.g. `createHttpBackend()` (`@ecsplico/editor/adapters/http`). */\n backend?: EditorBackend\n /**\n * Live collaboration: `true`, or `{ token }` for the token the collaboration server's `Identity.authenticate` receives.\n * With it the Y.Doc is the document: the server loads and stores the content, so the hook neither loads nor saves it\n * (`modelValue` stays `undefined`, `dirty` stays `false`, `save()` resolves at once). Needs `backend.collaboration`.\n */\n collaboration?: boolean | { token?: CollaborationToken }\n /** Who writes: labels remote carets and is the author of new comments; `null` or omitted = nobody (no comments). */\n user?: AuthorIdentity | null | (() => AuthorIdentity | null)\n /** Whether the user may comment now (a reader of a read-only document may be refused); default `true`. */\n canComment?: boolean | (() => boolean)\n}\n\nexport const EMPTY_DOCUMENT: JSONContent = { type: 'doc', content: [{ type: 'paragraph' }] }\n\n/**\n * The document as compared for \"unsaved changes\": without block ids. The editor gives every block of loaded content an\n * id the moment it mounts (stored files have none yet); that is not an edit the user made, so it must not mark a\n * freshly opened document dirty. Saving still writes the ids.\n */\nconst withoutBlockIds = (node: JSONContent): JSONContent => {\n const { attrs, content, ...rest } = node\n const kept = attrs && Object.fromEntries(Object.entries(attrs).filter(([name]) => name !== 'id'))\n return {\n ...rest,\n ...(kept && Object.keys(kept).length > 0 && { attrs: kept }),\n ...(content && { content: content.map(withoutBlockIds) }),\n }\n}\nexport const fingerprint = (content: JSONContent): string => JSON.stringify(withoutBlockIds(content))\n\n/** What a load leaves in the hook's state; `missing` = the document does not exist in the backend yet (the first save creates it even if nothing was typed). */\nexport interface LoadedDocument {\n modelValue: JSONContent\n /** `fingerprint` of the JSON the backend holds (the empty document while it does not exist). */\n saved: string\n missing: boolean\n}\n\n/** The state after `documents.load` answered: `null` (no such document) starts empty. */\nexport function loadedDocument(content: JSONContent | null): LoadedDocument {\n if (content === null) return { modelValue: structuredClone(EMPTY_DOCUMENT), saved: fingerprint(EMPTY_DOCUMENT), missing: true }\n return { modelValue: content, saved: fingerprint(content), missing: false }\n}\n\n/** Edits since the content was loaded or last saved; never in collaboration mode (the Y.Doc is the document) or before the first load. */\nexport function isDirty(collaborating: boolean, modelValue: JSONContent | undefined, saved: string | null): boolean {\n return !collaborating && modelValue !== undefined && saved !== null && fingerprint(modelValue) !== saved\n}\n\n/** The `fingerprint` a save of `content` would store, or `null` when there is nothing to save: no content yet, not ready, or unchanged and already stored. */\nexport function fingerprintToSave(content: JSONContent | undefined, ready: boolean, saved: string | null, missing: boolean): string | null {\n if (content === undefined || !ready) return null\n const next = fingerprint(content)\n return next === saved && !missing ? null : next\n}\n\nexport const errorOf = (cause: unknown) => cause instanceof Error ? cause : new Error(String(cause))\n\n/** The `capability` of `backend`; throws the developer error when it lacks it (called at setup, not on the first request). */\nexport function requireCapability<K extends 'documents' | 'collaboration'>(backend: EditorBackend, capability: K): NonNullable<EditorBackend[K]> {\n const port = backend[capability]\n if (!port) throw new Error(`@ecsplico/editor: useEditorDocument needs backend capability \"${capability}\", which \"${backend.name}\" lacks`)\n return port as NonNullable<EditorBackend[K]>\n}\n\n/** Opens the live session of document `id`; `close()` ends it (provider and Y.Doc). */\nexport function openSession(collaboration: NonNullable<EditorBackend['collaboration']>, id: string, option: EditorDocumentOptions['collaboration'], user: AuthorIdentity | null): { session: CollaborationSession, close: () => void } {\n const { ydoc, provider, dispose } = collaboration.connect(id, typeof option === 'object' ? option.token : undefined)\n return { session: { ydoc, provider, ...(user && { user }) }, close: dispose }\n}\n"],"mappings":";AAwBA,MAAa,iBAA8B;CAAE,MAAM;CAAO,SAAS,CAAC,EAAE,MAAM,YAAY,CAAC;AAAE;;;;;;AAO3F,MAAM,mBAAmB,SAAmC;CAC1D,MAAM,EAAE,OAAO,SAAS,GAAG,SAAS;CACpC,MAAM,OAAO,SAAS,OAAO,YAAY,OAAO,QAAQ,KAAK,CAAC,CAAC,QAAQ,CAAC,UAAU,SAAS,IAAI,CAAC;CAChG,OAAO;EACL,GAAG;EACH,GAAI,QAAQ,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,KAAK,EAAE,OAAO,KAAK;EAC1D,GAAI,WAAW,EAAE,SAAS,QAAQ,IAAI,eAAe,EAAE;CACzD;AACF;AACA,MAAa,eAAe,YAAiC,KAAK,UAAU,gBAAgB,OAAO,CAAC;;AAWpG,SAAgB,eAAe,SAA6C;CAC1E,IAAI,YAAY,MAAM,OAAO;EAAE,YAAY,gBAAgB,cAAc;EAAG,OAAO,YAAY,cAAc;EAAG,SAAS;CAAK;CAC9H,OAAO;EAAE,YAAY;EAAS,OAAO,YAAY,OAAO;EAAG,SAAS;CAAM;AAC5E;;AAGA,SAAgB,QAAQ,eAAwB,YAAqC,OAA+B;CAClH,OAAO,CAAC,iBAAiB,eAAe,KAAA,KAAa,UAAU,QAAQ,YAAY,UAAU,MAAM;AACrG;;AAGA,SAAgB,kBAAkB,SAAkC,OAAgB,OAAsB,SAAiC;CACzI,IAAI,YAAY,KAAA,KAAa,CAAC,OAAO,OAAO;CAC5C,MAAM,OAAO,YAAY,OAAO;CAChC,OAAO,SAAS,SAAS,CAAC,UAAU,OAAO;AAC7C;AAEA,MAAa,WAAW,UAAmB,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;;AAGnG,SAAgB,kBAA2D,SAAwB,YAA8C;CAC/I,MAAM,OAAO,QAAQ;CACrB,IAAI,CAAC,MAAM,MAAM,IAAI,MAAM,iEAAiE,WAAW,YAAY,QAAQ,KAAK,QAAQ;CACxI,OAAO;AACT;;AAGA,SAAgB,YAAY,eAA4D,IAAY,QAAgD,MAAmF;CACrO,MAAM,EAAE,MAAM,UAAU,YAAY,cAAc,QAAQ,IAAI,OAAO,WAAW,WAAW,OAAO,QAAQ,KAAA,CAAS;CACnH,OAAO;EAAE,SAAS;GAAE;GAAM;GAAU,GAAI,QAAQ,EAAE,KAAK;EAAG;EAAG,OAAO;CAAQ;AAC9E"}
1
+ {"version":3,"file":"document-state.js","names":[],"sources":["../../src/adapters/document-state.ts"],"sourcesContent":["import type { JSONContent } from '@tiptap/core'\nimport type { CollaborationSession } from '../core'\nimport type { CollaborationToken, EditorBackend } from '../core/backend'\nimport type { AuthorIdentity } from '../core/feature'\n\n// What the Vue and React `useEditorDocument` hooks share: their options, the empty document, the \"unsaved edits\"\n// comparison and the collaboration session built from the backend. Framework-free; nothing here talks HTTP, the hooks\n// reach documents, collaboration and comments only through `EditorBackend`.\n\nexport interface EditorDocumentOptions {\n /** Where documents, the collaboration session and the comments live. Default: the backend of the editor context (`createEditor` / `EditorContextProvider`). Collaboration and comments need one that has them, e.g. `createHttpBackend()` (`@ecsplico/editor/adapters/http`). */\n backend?: EditorBackend\n /**\n * Live collaboration: `true`, or `{ token }` for the token the collaboration server's `AccessControl.authenticate` receives.\n * With it the Y.Doc is the document: the server loads and stores the content, so the hook neither loads nor saves it\n * (`modelValue` stays `undefined`, `dirty` stays `false`, `save()` resolves at once). Needs `backend.collaboration`.\n */\n collaboration?: boolean | { token?: CollaborationToken }\n /** Who writes: labels remote carets and is the author of new comments; `null` or omitted = nobody (no comments). */\n user?: AuthorIdentity | null | (() => AuthorIdentity | null)\n /** Whether the user may comment now (a reader of a read-only document may be refused); default `true`. */\n canComment?: boolean | (() => boolean)\n}\n\nexport const EMPTY_DOCUMENT: JSONContent = { type: 'doc', content: [{ type: 'paragraph' }] }\n\n/**\n * The document as compared for \"unsaved changes\": without block ids. The editor gives every block of loaded content an\n * id the moment it mounts (stored files have none yet); that is not an edit the user made, so it must not mark a\n * freshly opened document dirty. Saving still writes the ids.\n */\nconst withoutBlockIds = (node: JSONContent): JSONContent => {\n const { attrs, content, ...rest } = node\n const kept = attrs && Object.fromEntries(Object.entries(attrs).filter(([name]) => name !== 'id'))\n return {\n ...rest,\n ...(kept && Object.keys(kept).length > 0 && { attrs: kept }),\n ...(content && { content: content.map(withoutBlockIds) }),\n }\n}\nexport const fingerprint = (content: JSONContent): string => JSON.stringify(withoutBlockIds(content))\n\n/** What a load leaves in the hook's state; `missing` = the document does not exist in the backend yet (the first save creates it even if nothing was typed). */\nexport interface LoadedDocument {\n modelValue: JSONContent\n /** `fingerprint` of the JSON the backend holds (the empty document while it does not exist). */\n saved: string\n missing: boolean\n}\n\n/** The state after `documents.load` answered: `null` (no such document) starts empty. */\nexport function loadedDocument(content: JSONContent | null): LoadedDocument {\n if (content === null) return { modelValue: structuredClone(EMPTY_DOCUMENT), saved: fingerprint(EMPTY_DOCUMENT), missing: true }\n return { modelValue: content, saved: fingerprint(content), missing: false }\n}\n\n/** Edits since the content was loaded or last saved; never in collaboration mode (the Y.Doc is the document) or before the first load. */\nexport function isDirty(collaborating: boolean, modelValue: JSONContent | undefined, saved: string | null): boolean {\n return !collaborating && modelValue !== undefined && saved !== null && fingerprint(modelValue) !== saved\n}\n\n/** The `fingerprint` a save of `content` would store, or `null` when there is nothing to save: no content yet, not ready, or unchanged and already stored. */\nexport function fingerprintToSave(content: JSONContent | undefined, ready: boolean, saved: string | null, missing: boolean): string | null {\n if (content === undefined || !ready) return null\n const next = fingerprint(content)\n return next === saved && !missing ? null : next\n}\n\nexport const errorOf = (cause: unknown) => cause instanceof Error ? cause : new Error(String(cause))\n\n/** The `capability` of `backend`; throws the developer error when it lacks it (called at setup, not on the first request). */\nexport function requireCapability<K extends 'documents' | 'collaboration'>(backend: EditorBackend, capability: K): NonNullable<EditorBackend[K]> {\n const port = backend[capability]\n if (!port) throw new Error(`@ecsplico/editor: useEditorDocument needs backend capability \"${capability}\", which \"${backend.name}\" lacks`)\n return port as NonNullable<EditorBackend[K]>\n}\n\n/** Opens the live session of document `id`; `close()` ends it (provider and Y.Doc). */\nexport function openSession(collaboration: NonNullable<EditorBackend['collaboration']>, id: string, option: EditorDocumentOptions['collaboration'], user: AuthorIdentity | null): { session: CollaborationSession, close: () => void } {\n const { ydoc, provider, dispose } = collaboration.connect(id, typeof option === 'object' ? option.token : undefined)\n return { session: { ydoc, provider, ...(user && { user }) }, close: dispose }\n}\n"],"mappings":";AAwBA,MAAa,iBAA8B;CAAE,MAAM;CAAO,SAAS,CAAC,EAAE,MAAM,YAAY,CAAC;AAAE;;;;;;AAO3F,MAAM,mBAAmB,SAAmC;CAC1D,MAAM,EAAE,OAAO,SAAS,GAAG,SAAS;CACpC,MAAM,OAAO,SAAS,OAAO,YAAY,OAAO,QAAQ,KAAK,CAAC,CAAC,QAAQ,CAAC,UAAU,SAAS,IAAI,CAAC;CAChG,OAAO;EACL,GAAG;EACH,GAAI,QAAQ,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,KAAK,EAAE,OAAO,KAAK;EAC1D,GAAI,WAAW,EAAE,SAAS,QAAQ,IAAI,eAAe,EAAE;CACzD;AACF;AACA,MAAa,eAAe,YAAiC,KAAK,UAAU,gBAAgB,OAAO,CAAC;;AAWpG,SAAgB,eAAe,SAA6C;CAC1E,IAAI,YAAY,MAAM,OAAO;EAAE,YAAY,gBAAgB,cAAc;EAAG,OAAO,YAAY,cAAc;EAAG,SAAS;CAAK;CAC9H,OAAO;EAAE,YAAY;EAAS,OAAO,YAAY,OAAO;EAAG,SAAS;CAAM;AAC5E;;AAGA,SAAgB,QAAQ,eAAwB,YAAqC,OAA+B;CAClH,OAAO,CAAC,iBAAiB,eAAe,KAAA,KAAa,UAAU,QAAQ,YAAY,UAAU,MAAM;AACrG;;AAGA,SAAgB,kBAAkB,SAAkC,OAAgB,OAAsB,SAAiC;CACzI,IAAI,YAAY,KAAA,KAAa,CAAC,OAAO,OAAO;CAC5C,MAAM,OAAO,YAAY,OAAO;CAChC,OAAO,SAAS,SAAS,CAAC,UAAU,OAAO;AAC7C;AAEA,MAAa,WAAW,UAAmB,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;;AAGnG,SAAgB,kBAA2D,SAAwB,YAA8C;CAC/I,MAAM,OAAO,QAAQ;CACrB,IAAI,CAAC,MAAM,MAAM,IAAI,MAAM,iEAAiE,WAAW,YAAY,QAAQ,KAAK,QAAQ;CACxI,OAAO;AACT;;AAGA,SAAgB,YAAY,eAA4D,IAAY,QAAgD,MAAmF;CACrO,MAAM,EAAE,MAAM,UAAU,YAAY,cAAc,QAAQ,IAAI,OAAO,WAAW,WAAW,OAAO,QAAQ,KAAA,CAAS;CACnH,OAAO;EAAE,SAAS;GAAE;GAAM;GAAU,GAAI,QAAQ,EAAE,KAAK;EAAG;EAAG,OAAO;CAAQ;AAC9E"}
@@ -1,4 +1,4 @@
1
- import { AssetStore, CollabStateStore, EditorPorts, Identity, SettingsStore, ThemeStore } from "../server/ports.js";
1
+ import { AccessControl, AssetStore, CollabStateStore, EditorPorts, SettingsStore, ThemeStore } from "../server/ports.js";
2
2
  import "../server/index.js";
3
3
  import { AdapterHooks } from "./shared.js";
4
4
  import { MarkdownDiagnostic, MarkdownPolicy } from "../markdown/policy.js";
@@ -39,7 +39,7 @@ export interface ForgeAdapterOptions extends Omit<AdapterHooks, 'authenticate'>
39
39
  */
40
40
  schema: AnyExtension[];
41
41
  /** The host's session check; there is no default for a host that owns its Markdown files. */
42
- authenticate: Identity['authenticate'];
42
+ authenticate: AccessControl['authenticate'];
43
43
  /** Markdown policy of the conversion (raw HTML, data URLs); the default drops both. */
44
44
  policy?: MarkdownPolicy;
45
45
  /** Called with the diagnostics of every load that produced some (dropped HTML or URLs, unknown fences kept verbatim). */
@@ -1 +1 @@
1
- {"version":3,"file":"forge.js","names":[],"sources":["../../src/adapters/forge.ts"],"sourcesContent":["import { type AnyExtension, Editor } from '@tiptap/core'\nimport { DocumentMarkdown } from '../core'\nimport { contentToMarkdown, hasFatalDiagnostics, joinFrontmatter, type MarkdownDiagnostic, type MarkdownPolicy, markdownToContent, splitFrontmatter } from '../markdown'\nimport type { CollabStateStore, AssetStore, EditorPorts, Identity, SettingsStore, ThemeStore } from '../server'\nimport { type AdapterHooks, directoryOf, identityOf } from './shared'\n\n/** A Markdown file of the host, as the host stores it: the whole text with its frontmatter, and an opaque version of that file. */\nexport interface ForgeFile {\n markdown: string\n /** Changes whenever the file's content changes (mtime and size, a hash, a revision); no NUL character. Becomes `contentVersion`. */\n version: string\n}\n\n/** Where the host keeps its Markdown documents. The adapter never touches a file system itself. */\nexport interface ForgeContent {\n /** The file behind a document id, `null` = it does not exist. */\n read(id: string): Promise<ForgeFile | null>\n /** Writes the file (frontmatter included). Only called for documents that exist. */\n write(id: string, markdown: string): Promise<void>\n /** Called after every `write` the adapter made (change notifications, indexes). */\n onWritten?(id: string): void | Promise<void>\n /** Ids of all documents, for `listContents` (the tag index). Omitted: `listContents` is empty. */\n list?(): Promise<string[]>\n}\n\n/** What the adapter delegates to a storage adapter that has no use for Markdown: the `collabState`, `assets`, `settings` and `themes` ports (the result of `createJsonAdapter` or `createDrizzleAdapter` fits). */\nexport interface ForgeState {\n collabState: CollabStateStore\n assets: AssetStore\n settings?: SettingsStore\n themes?: ThemeStore\n}\n\nexport interface ForgeAdapterOptions extends Omit<AdapterHooks, 'authenticate'> {\n content: ForgeContent\n state: ForgeState\n /**\n * The complete TipTap extension list of the Markdown conversion, e.g. `[...advancedSchemaExtensions(), MermaidBlock]`:\n * nodes and marks with their Markdown handlers. Host nodes also go to the collaboration schema through the server's\n * `schema` option (`() => [...advancedSchemaExtensions(), MermaidBlock]`).\n */\n schema: AnyExtension[]\n /** The host's session check; there is no default for a host that owns its Markdown files. */\n authenticate: Identity['authenticate']\n /** Markdown policy of the conversion (raw HTML, data URLs); the default drops both. */\n policy?: MarkdownPolicy\n /** Called with the diagnostics of every load that produced some (dropped HTML or URLs, unknown fences kept verbatim). */\n onDiagnostics?(id: string, diagnostics: MarkdownDiagnostic[]): void\n}\n\n/**\n * The editor's adapter over the host's Markdown files (concept documents, vault pages). The host keeps owning the\n * files, their locations, change notifications and access rules and passes them in as functions; this adapter does the\n * Markdown ↔ TipTap JSON conversion and the contract:\n *\n * - `loadContent` splits off the frontmatter and parses the body (`markdownToContent`); a body the conversion could not\n * keep intact (unclosed fence, malformed fence metadata) is refused with an error instead of loaded lossy.\n * - `storeContent` re-reads the file, keeps its current frontmatter verbatim, writes the serialized body and calls\n * `onWritten`. A file that is gone is not recreated, and an unchanged result is not rewritten.\n * - `contentVersion` is the file's own version, read after the write so it reflects what the editor stored.\n *\n * Yjs state, assets and settings are `state`'s. The converter is one headless editor per adapter, built on first use.\n */\nexport function createForgeAdapter(options: ForgeAdapterOptions): EditorPorts {\n const { content, state, schema, policy, onDiagnostics, authenticate, authorize } = options\n\n let converter: Editor | undefined\n // No `window` is needed as long as the editor starts from JSON content, not an HTML string.\n const editor = () => converter ??= new Editor({ content: { type: 'doc', content: [{ type: 'paragraph' }] }, extensions: [DocumentMarkdown, ...schema] })\n\n function parse(id: string, file: ForgeFile) {\n const { content: json, diagnostics } = markdownToContent(editor(), splitFrontmatter(file.markdown).body, policy ? { policy } : {})\n if (diagnostics.length) onDiagnostics?.(id, diagnostics)\n if (hasFatalDiagnostics(diagnostics)) {\n const problems = diagnostics.filter(diagnostic => diagnostic.severity === 'error').map(diagnostic => diagnostic.message)\n throw new Error(`@ecsplico/editor: document \"${id}\" cannot be converted without losing content: ${problems.join(' ')}`)\n }\n return json\n }\n\n return {\n identity: identityOf({ authenticate, authorize }),\n directory: directoryOf(options),\n\n documents: {\n async loadContent(id) {\n const file = await content.read(id)\n return file && parse(id, file)\n },\n async storeContent(id, json) {\n const current = await content.read(id)\n if (!current) return // deleted while a store was pending\n const instance = editor()\n instance.commands.setContent(json, { emitUpdate: false })\n const body = contentToMarkdown(instance)\n const markdown = joinFrontmatter(splitFrontmatter(current.markdown).frontmatter, body && `${body}\\n`)\n if (markdown === current.markdown) return\n await content.write(id, markdown)\n await content.onWritten?.(id)\n },\n async contentVersion(id) {\n return (await content.read(id))?.version ?? null\n },\n async listContents() {\n const entries = await Promise.all((await content.list?.() ?? []).map(async (id) => {\n const file = await content.read(id)\n return file && { id, content: parse(id, file) }\n }))\n return entries.filter((entry): entry is NonNullable<typeof entry> => entry !== null)\n },\n },\n\n collabState: state.collabState,\n assets: state.assets,\n // Present only when `state` has them: the routes and the settings client test for the ports.\n ...(state.settings && { settings: state.settings }),\n ...(state.themes && { themes: state.themes }),\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA+DA,SAAgB,mBAAmB,SAA2C;CAC5E,MAAM,EAAE,SAAS,OAAO,QAAQ,QAAQ,eAAe,cAAc,cAAc;CAEnF,IAAI;CAEJ,MAAM,eAAe,cAAc,IAAI,OAAO;EAAE,SAAS;GAAE,MAAM;GAAO,SAAS,CAAC,EAAE,MAAM,YAAY,CAAC;EAAE;EAAG,YAAY,CAAC,kBAAkB,GAAG,MAAM;CAAE,CAAC;CAEvJ,SAAS,MAAM,IAAY,MAAiB;EAC1C,MAAM,EAAE,SAAS,MAAM,gBAAgB,kBAAkB,OAAO,GAAG,iBAAiB,KAAK,QAAQ,CAAC,CAAC,MAAM,SAAS,EAAE,OAAO,IAAI,CAAC,CAAC;EACjI,IAAI,YAAY,QAAQ,gBAAgB,IAAI,WAAW;EACvD,IAAI,oBAAoB,WAAW,GAAG;GACpC,MAAM,WAAW,YAAY,QAAO,eAAc,WAAW,aAAa,OAAO,CAAC,CAAC,KAAI,eAAc,WAAW,OAAO;GACvH,MAAM,IAAI,MAAM,+BAA+B,GAAG,gDAAgD,SAAS,KAAK,GAAG,GAAG;EACxH;EACA,OAAO;CACT;CAEA,OAAO;EACL,UAAU,WAAW;GAAE;GAAc;EAAU,CAAC;EAChD,WAAW,YAAY,OAAO;EAE9B,WAAW;GACT,MAAM,YAAY,IAAI;IACpB,MAAM,OAAO,MAAM,QAAQ,KAAK,EAAE;IAClC,OAAO,QAAQ,MAAM,IAAI,IAAI;GAC/B;GACA,MAAM,aAAa,IAAI,MAAM;IAC3B,MAAM,UAAU,MAAM,QAAQ,KAAK,EAAE;IACrC,IAAI,CAAC,SAAS;IACd,MAAM,WAAW,OAAO;IACxB,SAAS,SAAS,WAAW,MAAM,EAAE,YAAY,MAAM,CAAC;IACxD,MAAM,OAAO,kBAAkB,QAAQ;IACvC,MAAM,WAAW,gBAAgB,iBAAiB,QAAQ,QAAQ,CAAC,CAAC,aAAa,QAAQ,GAAG,KAAK,GAAG;IACpG,IAAI,aAAa,QAAQ,UAAU;IACnC,MAAM,QAAQ,MAAM,IAAI,QAAQ;IAChC,MAAM,QAAQ,YAAY,EAAE;GAC9B;GACA,MAAM,eAAe,IAAI;IACvB,QAAQ,MAAM,QAAQ,KAAK,EAAE,EAAA,EAAI,WAAW;GAC9C;GACA,MAAM,eAAe;IAKnB,QAAO,MAJe,QAAQ,KAAK,MAAM,QAAQ,OAAO,KAAK,CAAC,EAAA,CAAG,IAAI,OAAO,OAAO;KACjF,MAAM,OAAO,MAAM,QAAQ,KAAK,EAAE;KAClC,OAAO,QAAQ;MAAE;MAAI,SAAS,MAAM,IAAI,IAAI;KAAE;IAChD,CAAC,CAAC,EAAA,CACa,QAAQ,UAA8C,UAAU,IAAI;GACrF;EACF;EAEA,aAAa,MAAM;EACnB,QAAQ,MAAM;EAEd,GAAI,MAAM,YAAY,EAAE,UAAU,MAAM,SAAS;EACjD,GAAI,MAAM,UAAU,EAAE,QAAQ,MAAM,OAAO;CAC7C;AACF"}
1
+ {"version":3,"file":"forge.js","names":[],"sources":["../../src/adapters/forge.ts"],"sourcesContent":["import { type AnyExtension, Editor } from '@tiptap/core'\nimport { DocumentMarkdown } from '../core'\nimport { contentToMarkdown, hasFatalDiagnostics, joinFrontmatter, type MarkdownDiagnostic, type MarkdownPolicy, markdownToContent, splitFrontmatter } from '../markdown'\nimport type { AccessControl, CollabStateStore, AssetStore, EditorPorts, SettingsStore, ThemeStore } from '../server'\nimport { type AdapterHooks, directoryOf, identityOf } from './shared'\n\n/** A Markdown file of the host, as the host stores it: the whole text with its frontmatter, and an opaque version of that file. */\nexport interface ForgeFile {\n markdown: string\n /** Changes whenever the file's content changes (mtime and size, a hash, a revision); no NUL character. Becomes `contentVersion`. */\n version: string\n}\n\n/** Where the host keeps its Markdown documents. The adapter never touches a file system itself. */\nexport interface ForgeContent {\n /** The file behind a document id, `null` = it does not exist. */\n read(id: string): Promise<ForgeFile | null>\n /** Writes the file (frontmatter included). Only called for documents that exist. */\n write(id: string, markdown: string): Promise<void>\n /** Called after every `write` the adapter made (change notifications, indexes). */\n onWritten?(id: string): void | Promise<void>\n /** Ids of all documents, for `listContents` (the tag index). Omitted: `listContents` is empty. */\n list?(): Promise<string[]>\n}\n\n/** What the adapter delegates to a storage adapter that has no use for Markdown: the `collabState`, `assets`, `settings` and `themes` ports (the result of `createJsonAdapter` or `createDrizzleAdapter` fits). */\nexport interface ForgeState {\n collabState: CollabStateStore\n assets: AssetStore\n settings?: SettingsStore\n themes?: ThemeStore\n}\n\nexport interface ForgeAdapterOptions extends Omit<AdapterHooks, 'authenticate'> {\n content: ForgeContent\n state: ForgeState\n /**\n * The complete TipTap extension list of the Markdown conversion, e.g. `[...advancedSchemaExtensions(), MermaidBlock]`:\n * nodes and marks with their Markdown handlers. Host nodes also go to the collaboration schema through the server's\n * `schema` option (`() => [...advancedSchemaExtensions(), MermaidBlock]`).\n */\n schema: AnyExtension[]\n /** The host's session check; there is no default for a host that owns its Markdown files. */\n authenticate: AccessControl['authenticate']\n /** Markdown policy of the conversion (raw HTML, data URLs); the default drops both. */\n policy?: MarkdownPolicy\n /** Called with the diagnostics of every load that produced some (dropped HTML or URLs, unknown fences kept verbatim). */\n onDiagnostics?(id: string, diagnostics: MarkdownDiagnostic[]): void\n}\n\n/**\n * The editor's adapter over the host's Markdown files (concept documents, vault pages). The host keeps owning the\n * files, their locations, change notifications and access rules and passes them in as functions; this adapter does the\n * Markdown ↔ TipTap JSON conversion and the contract:\n *\n * - `loadContent` splits off the frontmatter and parses the body (`markdownToContent`); a body the conversion could not\n * keep intact (unclosed fence, malformed fence metadata) is refused with an error instead of loaded lossy.\n * - `storeContent` re-reads the file, keeps its current frontmatter verbatim, writes the serialized body and calls\n * `onWritten`. A file that is gone is not recreated, and an unchanged result is not rewritten.\n * - `contentVersion` is the file's own version, read after the write so it reflects what the editor stored.\n *\n * Yjs state, assets and settings are `state`'s. The converter is one headless editor per adapter, built on first use.\n */\nexport function createForgeAdapter(options: ForgeAdapterOptions): EditorPorts {\n const { content, state, schema, policy, onDiagnostics, authenticate, authorize } = options\n\n let converter: Editor | undefined\n // No `window` is needed as long as the editor starts from JSON content, not an HTML string.\n const editor = () => converter ??= new Editor({ content: { type: 'doc', content: [{ type: 'paragraph' }] }, extensions: [DocumentMarkdown, ...schema] })\n\n function parse(id: string, file: ForgeFile) {\n const { content: json, diagnostics } = markdownToContent(editor(), splitFrontmatter(file.markdown).body, policy ? { policy } : {})\n if (diagnostics.length) onDiagnostics?.(id, diagnostics)\n if (hasFatalDiagnostics(diagnostics)) {\n const problems = diagnostics.filter(diagnostic => diagnostic.severity === 'error').map(diagnostic => diagnostic.message)\n throw new Error(`@ecsplico/editor: document \"${id}\" cannot be converted without losing content: ${problems.join(' ')}`)\n }\n return json\n }\n\n return {\n identity: identityOf({ authenticate, authorize }),\n directory: directoryOf(options),\n\n documents: {\n async loadContent(id) {\n const file = await content.read(id)\n return file && parse(id, file)\n },\n async storeContent(id, json) {\n const current = await content.read(id)\n if (!current) return // deleted while a store was pending\n const instance = editor()\n instance.commands.setContent(json, { emitUpdate: false })\n const body = contentToMarkdown(instance)\n const markdown = joinFrontmatter(splitFrontmatter(current.markdown).frontmatter, body && `${body}\\n`)\n if (markdown === current.markdown) return\n await content.write(id, markdown)\n await content.onWritten?.(id)\n },\n async contentVersion(id) {\n return (await content.read(id))?.version ?? null\n },\n async listContents() {\n const entries = await Promise.all((await content.list?.() ?? []).map(async (id) => {\n const file = await content.read(id)\n return file && { id, content: parse(id, file) }\n }))\n return entries.filter((entry): entry is NonNullable<typeof entry> => entry !== null)\n },\n },\n\n collabState: state.collabState,\n assets: state.assets,\n // Present only when `state` has them: the routes and the settings client test for the ports.\n ...(state.settings && { settings: state.settings }),\n ...(state.themes && { themes: state.themes }),\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA+DA,SAAgB,mBAAmB,SAA2C;CAC5E,MAAM,EAAE,SAAS,OAAO,QAAQ,QAAQ,eAAe,cAAc,cAAc;CAEnF,IAAI;CAEJ,MAAM,eAAe,cAAc,IAAI,OAAO;EAAE,SAAS;GAAE,MAAM;GAAO,SAAS,CAAC,EAAE,MAAM,YAAY,CAAC;EAAE;EAAG,YAAY,CAAC,kBAAkB,GAAG,MAAM;CAAE,CAAC;CAEvJ,SAAS,MAAM,IAAY,MAAiB;EAC1C,MAAM,EAAE,SAAS,MAAM,gBAAgB,kBAAkB,OAAO,GAAG,iBAAiB,KAAK,QAAQ,CAAC,CAAC,MAAM,SAAS,EAAE,OAAO,IAAI,CAAC,CAAC;EACjI,IAAI,YAAY,QAAQ,gBAAgB,IAAI,WAAW;EACvD,IAAI,oBAAoB,WAAW,GAAG;GACpC,MAAM,WAAW,YAAY,QAAO,eAAc,WAAW,aAAa,OAAO,CAAC,CAAC,KAAI,eAAc,WAAW,OAAO;GACvH,MAAM,IAAI,MAAM,+BAA+B,GAAG,gDAAgD,SAAS,KAAK,GAAG,GAAG;EACxH;EACA,OAAO;CACT;CAEA,OAAO;EACL,UAAU,WAAW;GAAE;GAAc;EAAU,CAAC;EAChD,WAAW,YAAY,OAAO;EAE9B,WAAW;GACT,MAAM,YAAY,IAAI;IACpB,MAAM,OAAO,MAAM,QAAQ,KAAK,EAAE;IAClC,OAAO,QAAQ,MAAM,IAAI,IAAI;GAC/B;GACA,MAAM,aAAa,IAAI,MAAM;IAC3B,MAAM,UAAU,MAAM,QAAQ,KAAK,EAAE;IACrC,IAAI,CAAC,SAAS;IACd,MAAM,WAAW,OAAO;IACxB,SAAS,SAAS,WAAW,MAAM,EAAE,YAAY,MAAM,CAAC;IACxD,MAAM,OAAO,kBAAkB,QAAQ;IACvC,MAAM,WAAW,gBAAgB,iBAAiB,QAAQ,QAAQ,CAAC,CAAC,aAAa,QAAQ,GAAG,KAAK,GAAG;IACpG,IAAI,aAAa,QAAQ,UAAU;IACnC,MAAM,QAAQ,MAAM,IAAI,QAAQ;IAChC,MAAM,QAAQ,YAAY,EAAE;GAC9B;GACA,MAAM,eAAe,IAAI;IACvB,QAAQ,MAAM,QAAQ,KAAK,EAAE,EAAA,EAAI,WAAW;GAC9C;GACA,MAAM,eAAe;IAKnB,QAAO,MAJe,QAAQ,KAAK,MAAM,QAAQ,OAAO,KAAK,CAAC,EAAA,CAAG,IAAI,OAAO,OAAO;KACjF,MAAM,OAAO,MAAM,QAAQ,KAAK,EAAE;KAClC,OAAO,QAAQ;MAAE;MAAI,SAAS,MAAM,IAAI,IAAI;KAAE;IAChD,CAAC,CAAC,EAAA,CACa,QAAQ,UAA8C,UAAU,IAAI;GACrF;EACF;EAEA,aAAa,MAAM;EACnB,QAAQ,MAAM;EAEd,GAAI,MAAM,YAAY,EAAE,UAAU,MAAM,SAAS;EACjD,GAAI,MAAM,UAAU,EAAE,QAAQ,MAAM,OAAO;CAC7C;AACF"}
@@ -1,6 +1,8 @@
1
1
  import { AiModels, AiModelsConfig } from "../server/ai.js";
2
2
  import { EditorPorts } from "../server/ports.js";
3
3
  import { CollabServer, CollabServerOptions } from "../server/collab.js";
4
+ import { BlockType } from "../features/composites/block-types.js";
5
+ import "../features/composites/schema.js";
4
6
  import { RateLimiterOptions } from "../server/rate-limit.js";
5
7
  import "../server/index.js";
6
8
  import { RouteDef } from "../server/routes.js";
@@ -15,12 +17,12 @@ export interface NitroHost {
15
17
  };
16
18
  }
17
19
  export interface EditorServerOptions {
18
- /** Identity, storage and everything else the backend serves, one port per capability: a route is mounted only when its port is present. A documents port that also manages documents (`json`, `drizzle`) lets `PUT /api/editor/documents/:id` create them; the `comments` port mounts the comments endpoint, `models` the host's language models. */
20
+ /** Access control, storage and everything else the backend serves, one port per capability: a route is mounted only when its port is present. A documents port that also manages documents (`json`, `drizzle`) lets `PUT /api/editor/documents/:id` create them; the `comments` port mounts the comments endpoint, `models` the host's language models. */
19
21
  ports: EditorPorts;
20
22
  /**
21
23
  * The collaboration websocket (`/collab/:id`). Schema, hooks and Hocuspocus configuration of `createCollabServer`;
22
24
  * `{}` takes everything from the ports (the `collabState` port is required). `false` mounts no websocket (documents then only load and save over HTTP).
23
- * `authenticateUpgrade`: authenticate the upgrade request itself (`Identity.authenticate` with its headers; cookies,
25
+ * `authenticateUpgrade`: authenticate the upgrade request itself (`AccessControl.authenticate` with its headers; cookies,
24
26
  * not the provider's `token`) and refuse it with HTTP 401, instead of closing an unauthenticated socket with 4401
25
27
  * after the upgrade; the user then travels with the connection and is not authenticated a second time.
26
28
  * Needs `experimental: { websocket: true }` in the Nitro config.
@@ -29,7 +31,7 @@ export interface EditorServerOptions {
29
31
  authenticateUpgrade?: boolean;
30
32
  }) | false;
31
33
  /**
32
- * Mounts the AI endpoints (`/api/ai/*`): presets, shared key, transcription, slots of `createAiModels`; `languageModel`
34
+ * Mounts the AI endpoints (`/api/ai/*`): presets, shared key, transcription, model roles of `createAiModels`; `languageModel`
33
35
  * and `listModels` default to those of the `models` port. Omitted: no AI routes.
34
36
  */
35
37
  ai?: Partial<AiModelsConfig>;
@@ -39,6 +41,12 @@ export interface EditorServerOptions {
39
41
  routes?: (routes: readonly RouteDef[]) => readonly RouteDef[];
40
42
  /** The collaboration schema (see `CollabServerOptions.schema`); default `advancedSchemaExtensions()`, host nodes: `() => [...advancedSchemaExtensions(), Callout]`. */
41
43
  schema?: () => AnyExtension[];
44
+ /**
45
+ * The host's Block types (the list its editors get through the `blockTypes` option, e.g. `mergeBlockTypes(defaultBlockTypes(t), parseBlockTypes(file))`),
46
+ * so the server resolves them without the client repeating them. The AI chat (`POST /api/ai/chat`) describes these to the model and ignores the
47
+ * `blockTypes` in the request. Omitted: the chat takes the `blockTypes` the client sends. The collaboration schema does not need them (Block types are storage, not schema).
48
+ */
49
+ blockTypes?: readonly BlockType[];
42
50
  }
43
51
  export interface EditorServer {
44
52
  ports: EditorPorts;
@@ -75,7 +75,8 @@ function installEditorServer(nitroApp, options) {
75
75
  const aiContext = models && limiter && {
76
76
  ...context,
77
77
  ai: models,
78
- limiter
78
+ limiter,
79
+ ...options.blockTypes && { blockTypes: options.blockTypes }
79
80
  };
80
81
  const mounted = serverRoutesOf(ports, { ai: aiContext !== void 0 });
81
82
  for (const route of options.routes ? options.routes(mounted) : mounted) {
@@ -86,6 +87,7 @@ function installEditorServer(nitroApp, options) {
86
87
  const release = adoptEditorServer({
87
88
  ports,
88
89
  schema,
90
+ blockTypes: options.blockTypes,
89
91
  collab,
90
92
  models,
91
93
  limiter
@@ -1 +1 @@
1
- {"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/nitro.ts"],"sourcesContent":["import type { AnyExtension } from '@tiptap/core'\nimport { type AiModels, type AiModelsConfig, type CollabConnection, type CollabServer, type CollabServerOptions, createAiModels, createCollabServer, createRateLimiter, type EditorPorts, type RateLimiter, type RateLimiterOptions } from '../server'\nimport { defineEditorConfig } from '../config'\nimport { routeOf } from '../server/h3'\nimport type { AiHandlerContext, HandlerContext } from '../server/handlers'\nimport { adoptEditorServer } from '../server/host'\nimport { type RouteDef, serverRoutesOf } from '../server/routes'\nimport { defineWebSocketHandler, type Router } from 'h3'\nimport { COLLAB_PATH } from '../core/api-routes'\n\n// The editor backend on a Nitro app: the routes the ports can serve (`serverRoutesOf`: settings, users, tags, assets,\n// themes, documents, comments, the optional AI endpoints) and the collaboration websocket, registered in one call from a\n// Nitro plugin. One installer serves both hosts: a plain Nitro app (nitropack 2) calls it from its own plugin, the\n// Nuxt module (`@ecsplico/editor/nuxt`) from its server plugin. Both end in the same `@ecsplico/editor/server/handlers`.\n\n/** The part of a Nitro app this entry uses; `nitroApp` of a Nitro plugin fits. */\nexport interface NitroHost {\n router: Pick<Router, 'add' | 'use'>\n hooks: { hook(name: 'close', listener: () => void | Promise<void>): unknown }\n}\n\n/** The route type of the table `EditorServerOptions.routes` filters. */\nexport type { RouteDef }\n\nexport interface EditorServerOptions {\n /** Identity, storage and everything else the backend serves, one port per capability: a route is mounted only when its port is present. A documents port that also manages documents (`json`, `drizzle`) lets `PUT /api/editor/documents/:id` create them; the `comments` port mounts the comments endpoint, `models` the host's language models. */\n ports: EditorPorts\n /**\n * The collaboration websocket (`/collab/:id`). Schema, hooks and Hocuspocus configuration of `createCollabServer`;\n * `{}` takes everything from the ports (the `collabState` port is required). `false` mounts no websocket (documents then only load and save over HTTP).\n * `authenticateUpgrade`: authenticate the upgrade request itself (`Identity.authenticate` with its headers; cookies,\n * not the provider's `token`) and refuse it with HTTP 401, instead of closing an unauthenticated socket with 4401\n * after the upgrade; the user then travels with the connection and is not authenticated a second time.\n * Needs `experimental: { websocket: true }` in the Nitro config.\n */\n collab: (Omit<CollabServerOptions, 'ports'> & { authenticateUpgrade?: boolean }) | false\n /**\n * Mounts the AI endpoints (`/api/ai/*`): presets, shared key, transcription, slots of `createAiModels`; `languageModel`\n * and `listModels` default to those of the `models` port. Omitted: no AI routes.\n */\n ai?: Partial<AiModelsConfig>\n /** Per-user limit of the AI endpoints; default: that of `defineEditorConfig` (30 requests per minute). */\n rateLimit?: RateLimiterOptions\n /** Filters or reorders the routes before they are mounted (a host that serves `/api/editor/assets/*` itself drops them); default: all. Receives what `serverRoutesOf(ports, { ai })` returns, so a route of an absent port, or an AI route without the `ai` option, is not in it. */\n routes?: (routes: readonly RouteDef[]) => readonly RouteDef[]\n /** The collaboration schema (see `CollabServerOptions.schema`); default `advancedSchemaExtensions()`, host nodes: `() => [...advancedSchemaExtensions(), Callout]`. */\n schema?: () => AnyExtension[]\n}\n\nexport interface EditorServer {\n ports: EditorPorts\n /** The collaboration server, `undefined` with `collab: false`. */\n collab: CollabServer | undefined\n /** The AI models, `undefined` without `ai`. */\n models: AiModels | undefined\n /** Writes pending collaboration stores and disconnects every client. Nitro's `close` hook calls it. */\n close(): Promise<void>\n}\n\ntype CollabPeer = { _hocuspocus?: CollabConnection }\n\n// Same crossws wiring as Hocuspocus' own Server; the document name comes from the provider's `name`. By default the user\n// is authenticated in Hocuspocus' `onAuthenticate` (cookie headers of the upgrade request or the provider's `token`), so\n// the upgrade always succeeds and an unauthenticated socket is closed with 4401 afterwards. With `authenticateUpgrade` the\n// upgrade request itself is authenticated (headers only) and refused with HTTP 401.\nconst collabRoute = (collab: CollabServer, ports: EditorPorts, authenticateUpgrade: boolean) => defineWebSocketHandler({\n async upgrade(request) {\n if (!authenticateUpgrade) return\n const user = await ports.identity.authenticate({ headers: request.headers })\n // No body: crossws' node adapter writes the response headers percent-encoded, which would mangle an automatic `content-type`.\n if (!user) return new Response(null, { status: 401, statusText: 'Unauthorized' })\n // crossws creates the context before it calls the hook (its type is optional only for other callers); `open` reads it as `peer.context`.\n request.context!.user = user\n },\n open(peer) {\n const user = peer.context.user as object | undefined\n ;(peer as unknown as CollabPeer)._hocuspocus = collab.handleConnection(peer.websocket as unknown as Parameters<CollabServer['handleConnection']>[0], peer.request as Request, user && { user })\n },\n message(peer, message) {\n ;(peer as unknown as CollabPeer)._hocuspocus?.handleMessage(message.uint8Array())\n },\n close(peer, details) {\n ;(peer as unknown as CollabPeer)._hocuspocus?.handleClose({ code: details.code ?? 1000, reason: details.reason ?? '' } as CloseEvent)\n },\n error(peer, error) {\n console.error('[collab]', peer.id, error)\n },\n})\n\n/**\n * Wires the editor backend into `nitroApp` (call it from a Nitro plugin):\n *\n * /api/editor/{capabilities,settings,users,tags,assets/…,themes/…} the editor's own endpoints, each with its port; `capabilities` lists what is served\n * /api/editor/documents/:id GET/PUT a document's TipTap JSON (what `useEditorDocument` uses)\n * /api/editor/comments/:id GET/POST comments, with `ports.comments`\n * /api/ai/{models,chat,complete,generate,extract,transcript-cleanup} with `ai`\n * /collab/:id the collaboration websocket, unless `collab: false`\n *\n * Every route authenticates through `ports.identity.authenticate`. The collaboration server is closed with Nitro's `close`\n * hook. The installed server is also the process-wide one of `@ecsplico/editor/server` (`useCollabServer`, `useAiModels`,\n * `liveContent`, …) until it closes. The paths are the ones the editor's client (`createHttpBackend`) calls.\n */\nexport function installEditorServer(nitroApp: NitroHost, options: EditorServerOptions): EditorServer {\n const { ports, schema } = options\n const { authenticateUpgrade = false, ...collabOptions }: Exclude<EditorServerOptions['collab'], false> = options.collab || {}\n const collab = options.collab === false ? undefined : createCollabServer({ ports, ...(schema && { schema }), ...collabOptions })\n const provider = ports.models\n const models = options.ai && createAiModels({\n presets: {},\n languageModel: provider?.languageModel && ((ref, user) => provider.languageModel!(ref, user)),\n listModels: provider?.listModels && (user => provider.listModels!(user)),\n ...options.ai,\n })\n const limiter: RateLimiter | undefined = models && createRateLimiter(options.rateLimit ?? defineEditorConfig().ai.rateLimit)\n\n const context: HandlerContext = { ports, ...(collab && { collab }), served: { ai: models !== undefined, collaboration: collab !== undefined } }\n const aiContext: AiHandlerContext | undefined = models && limiter && { ...context, ai: models, limiter }\n const mounted = serverRoutesOf(ports, { ai: aiContext !== undefined })\n for (const route of options.routes ? options.routes(mounted) : mounted) {\n // `serverRoutesOf` returns the AI routes only with `ai`, so `aiContext` is set for them.\n const handler = route.port === 'ai' ? routeOf(aiContext!, route.handler) : routeOf(context, route.handler)\n nitroApp.router.add(route.path, handler, route.method)\n }\n // No method: the websocket upgrade resolves its handler by path only.\n if (collab) nitroApp.router.use(`${COLLAB_PATH}/:id`, collabRoute(collab, ports, authenticateUpgrade))\n\n const release = adoptEditorServer({ ports, schema, collab, models, limiter })\n const server: EditorServer = {\n ports,\n collab,\n models,\n async close() {\n await collab?.close()\n release()\n },\n }\n nitroApp.hooks.hook('close', () => server.close())\n return server\n}\n"],"mappings":";;;;;;;;;;AAiEA,MAAM,eAAe,QAAsB,OAAoB,wBAAiC,uBAAuB;CACrH,MAAM,QAAQ,SAAS;EACrB,IAAI,CAAC,qBAAqB;EAC1B,MAAM,OAAO,MAAM,MAAM,SAAS,aAAa,EAAE,SAAS,QAAQ,QAAQ,CAAC;EAE3E,IAAI,CAAC,MAAM,OAAO,IAAI,SAAS,MAAM;GAAE,QAAQ;GAAK,YAAY;EAAe,CAAC;EAEhF,QAAQ,QAAS,OAAO;CAC1B;CACA,KAAK,MAAM;EACT,MAAM,OAAO,KAAK,QAAQ;EACzB,KAAgC,cAAc,OAAO,iBAAiB,KAAK,WAAyE,KAAK,SAAoB,QAAQ,EAAE,KAAK,CAAC;CAChM;CACA,QAAQ,MAAM,SAAS;EACpB,KAAgC,aAAa,cAAc,QAAQ,WAAW,CAAC;CAClF;CACA,MAAM,MAAM,SAAS;EAClB,KAAgC,aAAa,YAAY;GAAE,MAAM,QAAQ,QAAQ;GAAM,QAAQ,QAAQ,UAAU;EAAG,CAAe;CACtI;CACA,MAAM,MAAM,OAAO;EACjB,QAAQ,MAAM,YAAY,KAAK,IAAI,KAAK;CAC1C;AACF,CAAC;;;;;;;;;;;;;;AAeD,SAAgB,oBAAoB,UAAqB,SAA4C;CACnG,MAAM,EAAE,OAAO,WAAW;CAC1B,MAAM,EAAE,sBAAsB,OAAO,GAAG,kBAAiE,QAAQ,UAAU,CAAC;CAC5H,MAAM,SAAS,QAAQ,WAAW,QAAQ,KAAA,IAAY,mBAAmB;EAAE;EAAO,GAAI,UAAU,EAAE,OAAO;EAAI,GAAG;CAAc,CAAC;CAC/H,MAAM,WAAW,MAAM;CACvB,MAAM,SAAS,QAAQ,MAAM,eAAe;EAC1C,SAAS,CAAC;EACV,eAAe,UAAU,mBAAmB,KAAK,SAAS,SAAS,cAAe,KAAK,IAAI;EAC3F,YAAY,UAAU,gBAAe,SAAQ,SAAS,WAAY,IAAI;EACtE,GAAG,QAAQ;CACb,CAAC;CACD,MAAM,UAAmC,UAAU,kBAAkB,QAAQ,aAAa,mBAAmB,CAAC,CAAC,GAAG,SAAS;CAE3H,MAAM,UAA0B;EAAE;EAAO,GAAI,UAAU,EAAE,OAAO;EAAI,QAAQ;GAAE,IAAI,WAAW,KAAA;GAAW,eAAe,WAAW,KAAA;EAAU;CAAE;CAC9I,MAAM,YAA0C,UAAU,WAAW;EAAE,GAAG;EAAS,IAAI;EAAQ;CAAQ;CACvG,MAAM,UAAU,eAAe,OAAO,EAAE,IAAI,cAAc,KAAA,EAAU,CAAC;CACrE,KAAK,MAAM,SAAS,QAAQ,SAAS,QAAQ,OAAO,OAAO,IAAI,SAAS;EAEtE,MAAM,UAAU,MAAM,SAAS,OAAO,QAAQ,WAAY,MAAM,OAAO,IAAI,QAAQ,SAAS,MAAM,OAAO;EACzG,SAAS,OAAO,IAAI,MAAM,MAAM,SAAS,MAAM,MAAM;CACvD;CAEA,IAAI,QAAQ,SAAS,OAAO,IAAI,GAAG,YAAY,OAAO,YAAY,QAAQ,OAAO,mBAAmB,CAAC;CAErG,MAAM,UAAU,kBAAkB;EAAE;EAAO;EAAQ;EAAQ;EAAQ;CAAQ,CAAC;CAC5E,MAAM,SAAuB;EAC3B;EACA;EACA;EACA,MAAM,QAAQ;GACZ,MAAM,QAAQ,MAAM;GACpB,QAAQ;EACV;CACF;CACA,SAAS,MAAM,KAAK,eAAe,OAAO,MAAM,CAAC;CACjD,OAAO;AACT"}
1
+ {"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/nitro.ts"],"sourcesContent":["import type { AnyExtension } from '@tiptap/core'\nimport type { BlockType } from '../features/composites/schema'\nimport { type AiModels, type AiModelsConfig, type CollabConnection, type CollabServer, type CollabServerOptions, createAiModels, createCollabServer, createRateLimiter, type EditorPorts, type RateLimiter, type RateLimiterOptions } from '../server'\nimport { defineEditorConfig } from '../config'\nimport { routeOf } from '../server/h3'\nimport type { AiHandlerContext, HandlerContext } from '../server/handlers'\nimport { adoptEditorServer } from '../server/host'\nimport { type RouteDef, serverRoutesOf } from '../server/routes'\nimport { defineWebSocketHandler, type Router } from 'h3'\nimport { COLLAB_PATH } from '../core/api-routes'\n\n// The editor backend on a Nitro app: the routes the ports can serve (`serverRoutesOf`: settings, users, tags, assets,\n// themes, documents, comments, the optional AI endpoints) and the collaboration websocket, registered in one call from a\n// Nitro plugin. One installer serves both hosts: a plain Nitro app (nitropack 2) calls it from its own plugin, the\n// Nuxt module (`@ecsplico/editor/nuxt`) from its server plugin. Both end in the same `@ecsplico/editor/server/handlers`.\n\n/** The part of a Nitro app this entry uses; `nitroApp` of a Nitro plugin fits. */\nexport interface NitroHost {\n router: Pick<Router, 'add' | 'use'>\n hooks: { hook(name: 'close', listener: () => void | Promise<void>): unknown }\n}\n\n/** The route type of the table `EditorServerOptions.routes` filters. */\nexport type { RouteDef }\n\nexport interface EditorServerOptions {\n /** Access control, storage and everything else the backend serves, one port per capability: a route is mounted only when its port is present. A documents port that also manages documents (`json`, `drizzle`) lets `PUT /api/editor/documents/:id` create them; the `comments` port mounts the comments endpoint, `models` the host's language models. */\n ports: EditorPorts\n /**\n * The collaboration websocket (`/collab/:id`). Schema, hooks and Hocuspocus configuration of `createCollabServer`;\n * `{}` takes everything from the ports (the `collabState` port is required). `false` mounts no websocket (documents then only load and save over HTTP).\n * `authenticateUpgrade`: authenticate the upgrade request itself (`AccessControl.authenticate` with its headers; cookies,\n * not the provider's `token`) and refuse it with HTTP 401, instead of closing an unauthenticated socket with 4401\n * after the upgrade; the user then travels with the connection and is not authenticated a second time.\n * Needs `experimental: { websocket: true }` in the Nitro config.\n */\n collab: (Omit<CollabServerOptions, 'ports'> & { authenticateUpgrade?: boolean }) | false\n /**\n * Mounts the AI endpoints (`/api/ai/*`): presets, shared key, transcription, model roles of `createAiModels`; `languageModel`\n * and `listModels` default to those of the `models` port. Omitted: no AI routes.\n */\n ai?: Partial<AiModelsConfig>\n /** Per-user limit of the AI endpoints; default: that of `defineEditorConfig` (30 requests per minute). */\n rateLimit?: RateLimiterOptions\n /** Filters or reorders the routes before they are mounted (a host that serves `/api/editor/assets/*` itself drops them); default: all. Receives what `serverRoutesOf(ports, { ai })` returns, so a route of an absent port, or an AI route without the `ai` option, is not in it. */\n routes?: (routes: readonly RouteDef[]) => readonly RouteDef[]\n /** The collaboration schema (see `CollabServerOptions.schema`); default `advancedSchemaExtensions()`, host nodes: `() => [...advancedSchemaExtensions(), Callout]`. */\n schema?: () => AnyExtension[]\n /**\n * The host's Block types (the list its editors get through the `blockTypes` option, e.g. `mergeBlockTypes(defaultBlockTypes(t), parseBlockTypes(file))`),\n * so the server resolves them without the client repeating them. The AI chat (`POST /api/ai/chat`) describes these to the model and ignores the\n * `blockTypes` in the request. Omitted: the chat takes the `blockTypes` the client sends. The collaboration schema does not need them (Block types are storage, not schema).\n */\n blockTypes?: readonly BlockType[]\n}\n\nexport interface EditorServer {\n ports: EditorPorts\n /** The collaboration server, `undefined` with `collab: false`. */\n collab: CollabServer | undefined\n /** The AI models, `undefined` without `ai`. */\n models: AiModels | undefined\n /** Writes pending collaboration stores and disconnects every client. Nitro's `close` hook calls it. */\n close(): Promise<void>\n}\n\ntype CollabPeer = { _hocuspocus?: CollabConnection }\n\n// Same crossws wiring as Hocuspocus' own Server; the document name comes from the provider's `name`. By default the user\n// is authenticated in Hocuspocus' `onAuthenticate` (cookie headers of the upgrade request or the provider's `token`), so\n// the upgrade always succeeds and an unauthenticated socket is closed with 4401 afterwards. With `authenticateUpgrade` the\n// upgrade request itself is authenticated (headers only) and refused with HTTP 401.\nconst collabRoute = (collab: CollabServer, ports: EditorPorts, authenticateUpgrade: boolean) => defineWebSocketHandler({\n async upgrade(request) {\n if (!authenticateUpgrade) return\n const user = await ports.identity.authenticate({ headers: request.headers })\n // No body: crossws' node adapter writes the response headers percent-encoded, which would mangle an automatic `content-type`.\n if (!user) return new Response(null, { status: 401, statusText: 'Unauthorized' })\n // crossws creates the context before it calls the hook (its type is optional only for other callers); `open` reads it as `peer.context`.\n request.context!.user = user\n },\n open(peer) {\n const user = peer.context.user as object | undefined\n ;(peer as unknown as CollabPeer)._hocuspocus = collab.handleConnection(peer.websocket as unknown as Parameters<CollabServer['handleConnection']>[0], peer.request as Request, user && { user })\n },\n message(peer, message) {\n ;(peer as unknown as CollabPeer)._hocuspocus?.handleMessage(message.uint8Array())\n },\n close(peer, details) {\n ;(peer as unknown as CollabPeer)._hocuspocus?.handleClose({ code: details.code ?? 1000, reason: details.reason ?? '' } as CloseEvent)\n },\n error(peer, error) {\n console.error('[collab]', peer.id, error)\n },\n})\n\n/**\n * Wires the editor backend into `nitroApp` (call it from a Nitro plugin):\n *\n * /api/editor/{capabilities,settings,users,tags,assets/…,themes/…} the editor's own endpoints, each with its port; `capabilities` lists what is served\n * /api/editor/documents/:id GET/PUT a document's TipTap JSON (what `useEditorDocument` uses)\n * /api/editor/comments/:id GET/POST comments, with `ports.comments`\n * /api/ai/{models,chat,complete,generate,extract,transcript-cleanup} with `ai`\n * /collab/:id the collaboration websocket, unless `collab: false`\n *\n * Every route authenticates through `ports.identity.authenticate`. The collaboration server is closed with Nitro's `close`\n * hook. The installed server is also the process-wide one of `@ecsplico/editor/server` (`useCollabServer`, `useAiModels`,\n * `liveContent`, …) until it closes. The paths are the ones the editor's client (`createHttpBackend`) calls.\n */\nexport function installEditorServer(nitroApp: NitroHost, options: EditorServerOptions): EditorServer {\n const { ports, schema } = options\n const { authenticateUpgrade = false, ...collabOptions }: Exclude<EditorServerOptions['collab'], false> = options.collab || {}\n const collab = options.collab === false ? undefined : createCollabServer({ ports, ...(schema && { schema }), ...collabOptions })\n const provider = ports.models\n const models = options.ai && createAiModels({\n presets: {},\n languageModel: provider?.languageModel && ((ref, user) => provider.languageModel!(ref, user)),\n listModels: provider?.listModels && (user => provider.listModels!(user)),\n ...options.ai,\n })\n const limiter: RateLimiter | undefined = models && createRateLimiter(options.rateLimit ?? defineEditorConfig().ai.rateLimit)\n\n const context: HandlerContext = { ports, ...(collab && { collab }), served: { ai: models !== undefined, collaboration: collab !== undefined } }\n const aiContext: AiHandlerContext | undefined = models && limiter && { ...context, ai: models, limiter, ...(options.blockTypes && { blockTypes: options.blockTypes }) }\n const mounted = serverRoutesOf(ports, { ai: aiContext !== undefined })\n for (const route of options.routes ? options.routes(mounted) : mounted) {\n // `serverRoutesOf` returns the AI routes only with `ai`, so `aiContext` is set for them.\n const handler = route.port === 'ai' ? routeOf(aiContext!, route.handler) : routeOf(context, route.handler)\n nitroApp.router.add(route.path, handler, route.method)\n }\n // No method: the websocket upgrade resolves its handler by path only.\n if (collab) nitroApp.router.use(`${COLLAB_PATH}/:id`, collabRoute(collab, ports, authenticateUpgrade))\n\n const release = adoptEditorServer({ ports, schema, blockTypes: options.blockTypes, collab, models, limiter })\n const server: EditorServer = {\n ports,\n collab,\n models,\n async close() {\n await collab?.close()\n release()\n },\n }\n nitroApp.hooks.hook('close', () => server.close())\n return server\n}\n"],"mappings":";;;;;;;;;;AAwEA,MAAM,eAAe,QAAsB,OAAoB,wBAAiC,uBAAuB;CACrH,MAAM,QAAQ,SAAS;EACrB,IAAI,CAAC,qBAAqB;EAC1B,MAAM,OAAO,MAAM,MAAM,SAAS,aAAa,EAAE,SAAS,QAAQ,QAAQ,CAAC;EAE3E,IAAI,CAAC,MAAM,OAAO,IAAI,SAAS,MAAM;GAAE,QAAQ;GAAK,YAAY;EAAe,CAAC;EAEhF,QAAQ,QAAS,OAAO;CAC1B;CACA,KAAK,MAAM;EACT,MAAM,OAAO,KAAK,QAAQ;EACzB,KAAgC,cAAc,OAAO,iBAAiB,KAAK,WAAyE,KAAK,SAAoB,QAAQ,EAAE,KAAK,CAAC;CAChM;CACA,QAAQ,MAAM,SAAS;EACpB,KAAgC,aAAa,cAAc,QAAQ,WAAW,CAAC;CAClF;CACA,MAAM,MAAM,SAAS;EAClB,KAAgC,aAAa,YAAY;GAAE,MAAM,QAAQ,QAAQ;GAAM,QAAQ,QAAQ,UAAU;EAAG,CAAe;CACtI;CACA,MAAM,MAAM,OAAO;EACjB,QAAQ,MAAM,YAAY,KAAK,IAAI,KAAK;CAC1C;AACF,CAAC;;;;;;;;;;;;;;AAeD,SAAgB,oBAAoB,UAAqB,SAA4C;CACnG,MAAM,EAAE,OAAO,WAAW;CAC1B,MAAM,EAAE,sBAAsB,OAAO,GAAG,kBAAiE,QAAQ,UAAU,CAAC;CAC5H,MAAM,SAAS,QAAQ,WAAW,QAAQ,KAAA,IAAY,mBAAmB;EAAE;EAAO,GAAI,UAAU,EAAE,OAAO;EAAI,GAAG;CAAc,CAAC;CAC/H,MAAM,WAAW,MAAM;CACvB,MAAM,SAAS,QAAQ,MAAM,eAAe;EAC1C,SAAS,CAAC;EACV,eAAe,UAAU,mBAAmB,KAAK,SAAS,SAAS,cAAe,KAAK,IAAI;EAC3F,YAAY,UAAU,gBAAe,SAAQ,SAAS,WAAY,IAAI;EACtE,GAAG,QAAQ;CACb,CAAC;CACD,MAAM,UAAmC,UAAU,kBAAkB,QAAQ,aAAa,mBAAmB,CAAC,CAAC,GAAG,SAAS;CAE3H,MAAM,UAA0B;EAAE;EAAO,GAAI,UAAU,EAAE,OAAO;EAAI,QAAQ;GAAE,IAAI,WAAW,KAAA;GAAW,eAAe,WAAW,KAAA;EAAU;CAAE;CAC9I,MAAM,YAA0C,UAAU,WAAW;EAAE,GAAG;EAAS,IAAI;EAAQ;EAAS,GAAI,QAAQ,cAAc,EAAE,YAAY,QAAQ,WAAW;CAAG;CACtK,MAAM,UAAU,eAAe,OAAO,EAAE,IAAI,cAAc,KAAA,EAAU,CAAC;CACrE,KAAK,MAAM,SAAS,QAAQ,SAAS,QAAQ,OAAO,OAAO,IAAI,SAAS;EAEtE,MAAM,UAAU,MAAM,SAAS,OAAO,QAAQ,WAAY,MAAM,OAAO,IAAI,QAAQ,SAAS,MAAM,OAAO;EACzG,SAAS,OAAO,IAAI,MAAM,MAAM,SAAS,MAAM,MAAM;CACvD;CAEA,IAAI,QAAQ,SAAS,OAAO,IAAI,GAAG,YAAY,OAAO,YAAY,QAAQ,OAAO,mBAAmB,CAAC;CAErG,MAAM,UAAU,kBAAkB;EAAE;EAAO;EAAQ,YAAY,QAAQ;EAAY;EAAQ;EAAQ;CAAQ,CAAC;CAC5G,MAAM,SAAuB;EAC3B;EACA;EACA;EACA,MAAM,QAAQ;GACZ,MAAM,QAAQ,MAAM;GACpB,QAAQ;EACV;CACF;CACA,SAAS,MAAM,KAAK,eAAe,OAAO,MAAM,CAAC;CACjD,OAAO;AACT"}
@@ -1,6 +1,6 @@
1
1
  import { MentionUser } from "../core/backend-types.js";
2
2
  import "../features/mentions/schema.js";
3
- import { AssetStore, CollabStateStore, Directory, DocumentAdmin, DocumentStore, EditorPorts, Identity, SettingsStore } from "../server/ports.js";
3
+ import { AccessControl, AssetStore, CollabStateStore, Directory, DocumentAdmin, DocumentContentStore, EditorPorts, SettingsStore } from "../server/ports.js";
4
4
  import "../server/index.js";
5
5
  //#region src/adapters/shared.d.ts
6
6
  /**
@@ -9,19 +9,19 @@ import "../server/index.js";
9
9
  */
10
10
  export interface AdapterHooks {
11
11
  /**
12
- * The user behind a request, `null` = not authenticated (see `Identity.authenticate`). Default: an anonymous
12
+ * The user behind a request, `null` = not authenticated (see `AccessControl.authenticate`). Default: an anonymous
13
13
  * development user outside `NODE_ENV=production`, `null` (everyone refused) in production, so a deployment never
14
14
  * serves documents to the world by omission.
15
15
  */
16
- authenticate?: Identity['authenticate'];
17
- /** Per-document access for the collaboration websocket (see `Identity.authorize`). Omitted: every authenticated user edits every document. */
18
- authorize?: Identity['authorize'];
16
+ authenticate?: AccessControl['authenticate'];
17
+ /** Per-document access for the collaboration websocket (see `AccessControl.authorize`). Omitted: every authenticated user edits every document. */
18
+ authorize?: AccessControl['authorize'];
19
19
  /** Users that can be `@`-mentioned (`GET /api/editor/users`); no credentials. Default: none. */
20
20
  users?: () => MentionUser[] | Promise<MentionUser[]>;
21
21
  }
22
22
  /** The ports of an adapter that stores documents itself (`json`, `drizzle`): the contract including versioning, settings and document management. */
23
23
  export interface StoredPorts extends EditorPorts {
24
- documents: DocumentStore & DocumentAdmin & Required<Pick<DocumentStore, 'contentVersion'>>;
24
+ documents: DocumentContentStore & DocumentAdmin & Required<Pick<DocumentContentStore, 'contentVersion'>>;
25
25
  collabState: CollabStateStore;
26
26
  directory: Directory;
27
27
  assets: AssetStore;
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","names":[],"sources":["../../src/adapters/shared.ts"],"sourcesContent":["import type { MentionUser } from '../features/mentions/schema'\nimport type { AssetStore, CollabStateStore, Directory, DocumentAdmin, DocumentStore, EditorPorts, Identity, SettingsStore } from '../server'\n\n/**\n * The identity half of every adapter in this package: who the user is, which documents they may open and who can be\n * `@`-mentioned. Storage is the adapter's own business.\n */\nexport interface AdapterHooks {\n /**\n * The user behind a request, `null` = not authenticated (see `Identity.authenticate`). Default: an anonymous\n * development user outside `NODE_ENV=production`, `null` (everyone refused) in production, so a deployment never\n * serves documents to the world by omission.\n */\n authenticate?: Identity['authenticate']\n /** Per-document access for the collaboration websocket (see `Identity.authorize`). Omitted: every authenticated user edits every document. */\n authorize?: Identity['authorize']\n /** Users that can be `@`-mentioned (`GET /api/editor/users`); no credentials. Default: none. */\n users?: () => MentionUser[] | Promise<MentionUser[]>\n}\n\n/** The ports of an adapter that stores documents itself (`json`, `drizzle`): the contract including versioning, settings and document management. */\nexport interface StoredPorts extends EditorPorts {\n documents: DocumentStore & DocumentAdmin & Required<Pick<DocumentStore, 'contentVersion'>>\n collabState: CollabStateStore\n directory: Directory\n assets: AssetStore\n settings: SettingsStore\n}\n\nconst DEV_USER = { id: 'dev', username: 'dev', color: '#6366f1' } as const\n\nconst authenticateDefault: Identity['authenticate'] = async () => process.env.NODE_ENV === 'production' ? null : { ...DEV_USER }\n\n/** The `identity` port of `hooks`; `authorize` is only present when given (the collab server and the document routes test for it). */\nexport function identityOf(hooks: Pick<AdapterHooks, 'authenticate' | 'authorize'>): Identity {\n const { authenticate = authenticateDefault, authorize } = hooks\n return {\n authenticate: request => authenticate(request),\n ...(authorize && { authorize: (user: object, id: string) => authorize(user, id) }),\n }\n}\n\n/** The `directory` port of the `users` hook. */\nexport function directoryOf({ users }: Pick<AdapterHooks, 'users'>): Directory {\n return { listUsers: async () => (await users?.()) ?? [] }\n}\n\n/** The key per-user settings are stored under: the user's `id`, else its `username`. */\nexport function settingsKey(user: object): string {\n const { id, username } = user as { id?: unknown, username?: unknown }\n for (const candidate of [id, username]) {\n if (typeof candidate === 'string' && candidate !== '') return candidate\n if (typeof candidate === 'number') return String(candidate)\n }\n throw new TypeError('@ecsplico/editor: the authenticated user has neither an `id` nor a `username`, so its settings have no key')\n}\n\n/** An independent copy of the bytes (a driver's `Buffer` may view a larger pool), as the plain `Uint8Array` the port contract names. */\nexport const bytesOf = (data: Uint8Array): Uint8Array => new Uint8Array(data)\n"],"mappings":";AA6BA,MAAM,WAAW;CAAE,IAAI;CAAO,UAAU;CAAO,OAAO;AAAU;AAEhE,MAAM,sBAAgD,YAAY,QAAQ,IAAI,aAAa,eAAe,OAAO,EAAE,GAAG,SAAS;;AAG/H,SAAgB,WAAW,OAAmE;CAC5F,MAAM,EAAE,eAAe,qBAAqB,cAAc;CAC1D,OAAO;EACL,eAAc,YAAW,aAAa,OAAO;EAC7C,GAAI,aAAa,EAAE,YAAY,MAAc,OAAe,UAAU,MAAM,EAAE,EAAE;CAClF;AACF;;AAGA,SAAgB,YAAY,EAAE,SAAiD;CAC7E,OAAO,EAAE,WAAW,YAAa,MAAM,QAAQ,KAAM,CAAC,EAAE;AAC1D;;AAGA,SAAgB,YAAY,MAAsB;CAChD,MAAM,EAAE,IAAI,aAAa;CACzB,KAAK,MAAM,aAAa,CAAC,IAAI,QAAQ,GAAG;EACtC,IAAI,OAAO,cAAc,YAAY,cAAc,IAAI,OAAO;EAC9D,IAAI,OAAO,cAAc,UAAU,OAAO,OAAO,SAAS;CAC5D;CACA,MAAM,IAAI,UAAU,4GAA4G;AAClI;;AAGA,MAAa,WAAW,SAAiC,IAAI,WAAW,IAAI"}
1
+ {"version":3,"file":"shared.js","names":[],"sources":["../../src/adapters/shared.ts"],"sourcesContent":["import type { MentionUser } from '../features/mentions/schema'\nimport type { AccessControl, AssetStore, CollabStateStore, Directory, DocumentAdmin, DocumentContentStore, EditorPorts, SettingsStore } from '../server'\n\n/**\n * The identity half of every adapter in this package: who the user is, which documents they may open and who can be\n * `@`-mentioned. Storage is the adapter's own business.\n */\nexport interface AdapterHooks {\n /**\n * The user behind a request, `null` = not authenticated (see `AccessControl.authenticate`). Default: an anonymous\n * development user outside `NODE_ENV=production`, `null` (everyone refused) in production, so a deployment never\n * serves documents to the world by omission.\n */\n authenticate?: AccessControl['authenticate']\n /** Per-document access for the collaboration websocket (see `AccessControl.authorize`). Omitted: every authenticated user edits every document. */\n authorize?: AccessControl['authorize']\n /** Users that can be `@`-mentioned (`GET /api/editor/users`); no credentials. Default: none. */\n users?: () => MentionUser[] | Promise<MentionUser[]>\n}\n\n/** The ports of an adapter that stores documents itself (`json`, `drizzle`): the contract including versioning, settings and document management. */\nexport interface StoredPorts extends EditorPorts {\n documents: DocumentContentStore & DocumentAdmin & Required<Pick<DocumentContentStore, 'contentVersion'>>\n collabState: CollabStateStore\n directory: Directory\n assets: AssetStore\n settings: SettingsStore\n}\n\nconst DEV_USER = { id: 'dev', username: 'dev', color: '#6366f1' } as const\n\nconst authenticateDefault: AccessControl['authenticate'] = async () => process.env.NODE_ENV === 'production' ? null : { ...DEV_USER }\n\n/** The `identity` port of `hooks`; `authorize` is only present when given (the collab server and the document routes test for it). */\nexport function identityOf(hooks: Pick<AdapterHooks, 'authenticate' | 'authorize'>): AccessControl {\n const { authenticate = authenticateDefault, authorize } = hooks\n return {\n authenticate: request => authenticate(request),\n ...(authorize && { authorize: (user: object, id: string) => authorize(user, id) }),\n }\n}\n\n/** The `directory` port of the `users` hook. */\nexport function directoryOf({ users }: Pick<AdapterHooks, 'users'>): Directory {\n return { listUsers: async () => (await users?.()) ?? [] }\n}\n\n/** The key per-user settings are stored under: the user's `id`, else its `username`. */\nexport function settingsKey(user: object): string {\n const { id, username } = user as { id?: unknown, username?: unknown }\n for (const candidate of [id, username]) {\n if (typeof candidate === 'string' && candidate !== '') return candidate\n if (typeof candidate === 'number') return String(candidate)\n }\n throw new TypeError('@ecsplico/editor: the authenticated user has neither an `id` nor a `username`, so its settings have no key')\n}\n\n/** An independent copy of the bytes (a driver's `Buffer` may view a larger pool), as the plain `Uint8Array` the port contract names. */\nexport const bytesOf = (data: Uint8Array): Uint8Array => new Uint8Array(data)\n"],"mappings":";AA6BA,MAAM,WAAW;CAAE,IAAI;CAAO,UAAU;CAAO,OAAO;AAAU;AAEhE,MAAM,sBAAqD,YAAY,QAAQ,IAAI,aAAa,eAAe,OAAO,EAAE,GAAG,SAAS;;AAGpI,SAAgB,WAAW,OAAwE;CACjG,MAAM,EAAE,eAAe,qBAAqB,cAAc;CAC1D,OAAO;EACL,eAAc,YAAW,aAAa,OAAO;EAC7C,GAAI,aAAa,EAAE,YAAY,MAAc,OAAe,UAAU,MAAM,EAAE,EAAE;CAClF;AACF;;AAGA,SAAgB,YAAY,EAAE,SAAiD;CAC7E,OAAO,EAAE,WAAW,YAAa,MAAM,QAAQ,KAAM,CAAC,EAAE;AAC1D;;AAGA,SAAgB,YAAY,MAAsB;CAChD,MAAM,EAAE,IAAI,aAAa;CACzB,KAAK,MAAM,aAAa,CAAC,IAAI,QAAQ,GAAG;EACtC,IAAI,OAAO,cAAc,YAAY,cAAc,IAAI,OAAO;EAC9D,IAAI,OAAO,cAAc,UAAU,OAAO,OAAO,SAAS;CAC5D;CACA,MAAM,IAAI,UAAU,4GAA4G;AAClI;;AAGA,MAAa,WAAW,SAAiC,IAAI,WAAW,IAAI"}
@@ -1,9 +1,9 @@
1
1
  import { Locale, Messages } from "../i18n/index.js";
2
- import { ModelSlot } from "../settings/values.js";
2
+ import { ModelRole } from "../settings/values.js";
3
3
  import "../settings/index.js";
4
4
  //#region src/config/index.d.ts
5
- /** Server-defined model preset (any OpenAI-compatible chat completions endpoint); its key falls back to the shared `apiKey`. */
6
- export interface PresetConfig {
5
+ /** Server-defined Model preset (any OpenAI-compatible chat completions endpoint); its key falls back to the shared `apiKey`. The `preset` field of a model reference names one by id. */
6
+ export interface ModelPresetConfig {
7
7
  label: string;
8
8
  endpoint: string;
9
9
  model: string;
@@ -34,18 +34,18 @@ export interface EditorConfig {
34
34
  * the preset id in upper snake case, e.g. `flashLite` → `FLASH_LITE`, or `NUXT_TIPTAP_EDITOR_AI_API_KEY`). Preset ids
35
35
  * must be camelCase so the env names map onto them. Presets named like a default (`sonnet`, `flashLite`) override its fields.
36
36
  */
37
- presets: Record<string, PresetConfig>;
37
+ presets: Record<string, ModelPresetConfig>;
38
38
  /** Shared key of presets without their own. Default `''` (so the env variable has a key to override). */
39
39
  apiKey: string;
40
- /** Default preset per slot; users may override in settings. The `complete` model must support assistant prefill. */
41
- slots: Record<ModelSlot, string>;
40
+ /** Default preset per model role; users may override in settings (`ai.roles`). The `complete` model must support assistant prefill. */
41
+ roles: Record<ModelRole, string>;
42
42
  /** Per-user limit shared by all AI endpoints: at most `max` requests per sliding `windowMs`. */
43
43
  rateLimit: {
44
44
  max: number;
45
45
  windowMs: number;
46
46
  };
47
47
  /**
48
- * Speech-to-text for audio/video Bausteine: an OpenAI-compatible `<endpoint>/audio/transcriptions` or Deepgram
48
+ * Speech-to-text for audio/video Typed blocks: an OpenAI-compatible `<endpoint>/audio/transcriptions` or Deepgram
49
49
  * (`provider: 'deepgram'`; OpenRouter presets provide neither). Absent = the action and the endpoint are off.
50
50
  * The key comes from env only (Nuxt: `NUXT_TIPTAP_EDITOR_AI_TRANSCRIPTION_API_KEY`).
51
51
  */
@@ -53,7 +53,7 @@ export interface EditorConfig {
53
53
  };
54
54
  collab: {
55
55
  /**
56
- * Authenticate the `/collab/:id` websocket upgrade itself (`Identity.authenticate` with the upgrade request's
56
+ * Authenticate the `/collab/:id` websocket upgrade itself (`AccessControl.authenticate` with the upgrade request's
57
57
  * headers) and refuse it with HTTP 401 for an unknown user. Default `false`: the upgrade always succeeds, the user
58
58
  * is authenticated afterwards from the headers or the provider's `token`, and an unauthenticated socket is closed
59
59
  * with code 4401. Enable only when every client authenticates by cookie/headers: a client that sends its identity
@@ -67,18 +67,18 @@ type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]>; }
67
67
  /** What a host writes: every key optional, `undefined` (an unset env variable) means the default. */
68
68
  export type EditorConfigInput = DeepPartial<EditorConfig>;
69
69
  /**
70
- * Applies every default once: locale `'de'`, the OpenRouter presets `sonnet` and `flashLite` with the slots that use
70
+ * Applies every default once: locale `'de'`, the OpenRouter presets `sonnet` and `flashLite` with the model roles that use
71
71
  * them, 30 AI requests per user and minute, no upgrade authentication. Every preset and the transcription get an empty
72
72
  * `apiKey` unless one is given, so an environment variable has a key to override.
73
73
  */
74
74
  export declare function defineEditorConfig(input?: EditorConfigInput): EditorConfig;
75
- /** What the browser may see: the language and the default model slots, never a key. */
75
+ /** What the browser may see: the language and the default model roles, never a key. */
76
76
  export interface EditorPublicConfig {
77
77
  locale: Locale;
78
78
  messages?: Partial<Messages>;
79
- /** Inert AI configuration: default preset per slot before the models list has answered, and whether transcription is on. */
79
+ /** Inert AI configuration: default preset per model role before the models list has answered, and whether transcription is on. */
80
80
  ai: {
81
- slots: Record<ModelSlot, string>;
81
+ roles: Record<ModelRole, string>;
82
82
  transcription: boolean;
83
83
  };
84
84
  }
@@ -86,9 +86,9 @@ export declare function publicConfig(config: EditorConfig): EditorPublicConfig;
86
86
  /** What the server needs, secrets included: the shape `installEditorServer` takes for `ai`, `rateLimit` and `collab`. */
87
87
  export interface EditorServerConfig {
88
88
  ai: {
89
- presets: Record<string, PresetConfig>;
89
+ presets: Record<string, ModelPresetConfig>;
90
90
  apiKey: string;
91
- slots: Record<ModelSlot, string>;
91
+ roles: Record<ModelRole, string>;
92
92
  transcription?: TranscriptionConfig;
93
93
  };
94
94
  rateLimit: {