@ecsplico/editor 3.6.0 → 4.1.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.
- package/CHANGELOG.md +73 -0
- package/dist/adapters/direct-ai.d.ts +4 -4
- package/dist/adapters/direct-ai.js +2 -2
- package/dist/adapters/direct-ai.js.map +1 -1
- package/dist/adapters/document-state.d.ts +1 -1
- package/dist/adapters/document-state.js.map +1 -1
- package/dist/adapters/forge.d.ts +2 -2
- package/dist/adapters/forge.js.map +1 -1
- package/dist/adapters/nitro.d.ts +11 -3
- package/dist/adapters/nitro.js +3 -1
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +6 -6
- package/dist/adapters/shared.js.map +1 -1
- package/dist/code/CodeSurface.vue.d.ts +3 -3
- package/dist/config/index.d.ts +14 -14
- package/dist/config/index.js +8 -8
- package/dist/config/index.js.map +1 -1
- package/dist/core/backend-types.d.ts +3 -3
- package/dist/core/backend-types.js.map +1 -1
- package/dist/core/backend.d.ts +4 -4
- package/dist/core/backend.js.map +1 -1
- package/dist/core/block-convert.d.ts +1 -1
- package/dist/core/block-convert.js +5 -5
- package/dist/core/block-convert.js.map +1 -1
- package/dist/core/block-transfer.d.ts +8 -4
- package/dist/core/block-transfer.js +23 -14
- package/dist/core/block-transfer.js.map +1 -1
- package/dist/core/capabilities.d.ts +113 -0
- package/dist/core/capabilities.js +155 -0
- package/dist/core/capabilities.js.map +1 -0
- package/dist/core/command-types.d.ts +1 -1
- package/dist/core/context.js.map +1 -1
- package/dist/core/convert.d.ts +5 -5
- package/dist/core/convert.js +5 -5
- package/dist/core/convert.js.map +1 -1
- package/dist/core/core-feature.js +1 -1
- package/dist/core/define-editor.d.ts +2 -2
- package/dist/core/define-editor.js +34 -6
- package/dist/core/define-editor.js.map +1 -1
- package/dist/core/derivation.d.ts +119 -0
- package/dist/core/derivation.js +187 -0
- package/dist/core/derivation.js.map +1 -0
- package/dist/core/derive.d.ts +61 -0
- package/dist/core/derive.js +69 -0
- package/dist/core/derive.js.map +1 -0
- package/dist/core/extension-points.d.ts +16 -0
- package/dist/core/feature-context.d.ts +5 -1
- package/dist/core/feature-context.js +10 -3
- package/dist/core/feature-context.js.map +1 -1
- package/dist/core/feature.d.ts +41 -19
- package/dist/core/feature.js.map +1 -1
- package/dist/core/fences.js +1 -1
- package/dist/core/fences.js.map +1 -1
- package/dist/core/index.d.ts +6 -2
- package/dist/core/index.js +5 -1
- package/dist/core/pipeline.d.ts +14 -5
- package/dist/core/pipeline.js +19 -7
- package/dist/core/pipeline.js.map +1 -1
- package/dist/core/properties.d.ts +22 -0
- package/dist/core/properties.js +17 -0
- package/dist/core/properties.js.map +1 -0
- package/dist/core/schema/block-ids.js +5 -5
- package/dist/core/schema/block-ids.js.map +1 -1
- package/dist/core/schema/index.d.ts +2 -1
- package/dist/core/schema/index.js +2 -1
- package/dist/core/schema/index.js.map +1 -1
- package/dist/core/schema/opaque-fence.d.ts +4 -4
- package/dist/core/schema/opaque-fence.js +7 -7
- package/dist/core/schema/opaque-fence.js.map +1 -1
- package/dist/core/schema/persisted-names.d.ts +58 -0
- package/dist/core/schema/persisted-names.js +96 -0
- package/dist/core/schema/persisted-names.js.map +1 -0
- package/dist/core/toolbar-items.js +1 -1
- package/dist/core/toolbar-items.js.map +1 -1
- package/dist/core/util.js +5 -1
- package/dist/core/util.js.map +1 -1
- package/dist/document/DocumentSurface.vue.d.ts +3 -3
- package/dist/features/ai/agent-ops.js +1 -1
- package/dist/features/ai/ai-review.js +1 -1
- package/dist/features/ai/assistant.d.ts +4 -4
- package/dist/features/ai/assistant.js +1 -1
- package/dist/features/ai/assistant.js.map +1 -1
- package/dist/features/ai/components/AiSelectionMenu.js +1 -1
- package/dist/features/ai/components/AiSelectionMenu.js.map +1 -1
- package/dist/features/ai/components/ModelQuickSwitch.js +7 -7
- package/dist/features/ai/components/ModelQuickSwitch.js.map +1 -1
- package/dist/features/ai/components/ModelQuickSwitch.vue.d.ts +4 -4
- package/dist/features/ai/components/SettingsAiTab.js +8 -8
- package/dist/features/ai/components/SettingsAiTab.js.map +1 -1
- package/dist/features/ai/components/SettingsModelProfiles.js +1 -1
- package/dist/features/ai/components/SettingsModelProfiles.js.map +1 -1
- package/dist/features/ai/document-chat-model.d.ts +2 -2
- package/dist/features/ai/document-chat-model.js +5 -5
- package/dist/features/ai/document-chat-model.js.map +1 -1
- package/dist/features/ai/document-chat.js +2 -2
- package/dist/features/ai/document-chat.js.map +1 -1
- package/dist/features/ai/feature.d.ts +2 -2
- package/dist/features/ai/feature.js +2 -2
- package/dist/features/ai/feature.js.map +1 -1
- package/dist/features/ai/ghost-autocomplete.d.ts +4 -4
- package/dist/features/ai/ghost-autocomplete.js +1 -1
- package/dist/features/ai/ghost-autocomplete.js.map +1 -1
- package/dist/features/ai/index.d.ts +3 -3
- package/dist/features/ai/index.js +2 -2
- package/dist/features/ai/model-profiles.d.ts +9 -9
- package/dist/features/ai/model-profiles.js +13 -13
- package/dist/features/ai/model-profiles.js.map +1 -1
- package/dist/features/ai/protocol/agent-ops.d.ts +2 -2
- package/dist/features/ai/protocol/agent-ops.js +1 -1
- package/dist/features/ai/protocol/agent-ops.js.map +1 -1
- package/dist/features/ai/protocol/block-lines.d.ts +7 -7
- package/dist/features/ai/protocol/block-lines.js +10 -10
- package/dist/features/ai/protocol/block-lines.js.map +1 -1
- package/dist/features/ai/protocol/chat.d.ts +5 -5
- package/dist/features/ai/protocol/chat.js +12 -12
- package/dist/features/ai/protocol/chat.js.map +1 -1
- package/dist/features/ai/protocol/generate.d.ts +1 -1
- package/dist/features/ai/protocol/generate.js +1 -1
- package/dist/features/ai/protocol/generate.js.map +1 -1
- package/dist/features/ai/protocol/index.d.ts +3 -3
- package/dist/features/ai/protocol/index.js +2 -2
- package/dist/features/ai/protocol/models.d.ts +3 -3
- package/dist/features/ai/protocol/models.js +3 -3
- package/dist/features/ai/protocol/models.js.map +1 -1
- package/dist/features/ai/protocol/schemas.d.ts +1 -1
- package/dist/features/ai/protocol/schemas.js +1 -1
- package/dist/features/ai/protocol/schemas.js.map +1 -1
- package/dist/features/ai/protocol/tasks.d.ts +5 -5
- package/dist/features/ai/protocol/tasks.js +12 -12
- package/dist/features/ai/protocol/tasks.js.map +1 -1
- package/dist/features/ai/react/AiSelectionMenu.js.map +1 -1
- package/dist/features/ai/react/ModelQuickSwitch.d.ts +4 -4
- package/dist/features/ai/react/ModelQuickSwitch.js +6 -6
- package/dist/features/ai/react/ModelQuickSwitch.js.map +1 -1
- package/dist/features/ai/react/SettingsAiTab.d.ts +1 -1
- package/dist/features/ai/react/SettingsAiTab.js +8 -8
- package/dist/features/ai/react/SettingsAiTab.js.map +1 -1
- package/dist/features/ai/react/SettingsModelProfiles.d.ts +1 -1
- package/dist/features/ai/react/SettingsModelProfiles.js +1 -1
- package/dist/features/ai/react/SettingsModelProfiles.js.map +1 -1
- package/dist/features/ai/react/use-ai-assistant.d.ts +1 -1
- package/dist/features/ai/react/use-ai-assistant.js +1 -1
- package/dist/features/ai/react/use-ai-assistant.js.map +1 -1
- package/dist/features/ai/react/use-model-profiles.d.ts +5 -5
- package/dist/features/ai/react/use-model-profiles.js +2 -2
- package/dist/features/ai/react/use-model-profiles.js.map +1 -1
- package/dist/features/ai/use-ai-assistant.d.ts +1 -1
- package/dist/features/ai/use-ai-assistant.js +1 -1
- package/dist/features/ai/use-ai-assistant.js.map +1 -1
- package/dist/features/ai/use-model-profiles.d.ts +4 -4
- package/dist/features/ai/use-model-profiles.js +2 -2
- package/dist/features/ai/use-model-profiles.js.map +1 -1
- package/dist/features/block-ui/block-actions.d.ts +8 -4
- package/dist/features/block-ui/block-actions.js +38 -22
- package/dist/features/block-ui/block-actions.js.map +1 -1
- package/dist/features/block-ui/block-target.d.ts +29 -0
- package/dist/features/block-ui/block-target.js +63 -0
- package/dist/features/block-ui/block-target.js.map +1 -0
- package/dist/features/block-ui/components/BlockHandles.js +3 -3
- package/dist/features/block-ui/components/BlockHandles.js.map +1 -1
- package/dist/features/block-ui/components/BlockUi.js +11 -23
- package/dist/features/block-ui/components/BlockUi.js.map +1 -1
- package/dist/features/block-ui/feature.d.ts +3 -2
- package/dist/features/block-ui/feature.js +3 -2
- package/dist/features/block-ui/feature.js.map +1 -1
- package/dist/features/block-ui/handle-geometry.js +12 -23
- package/dist/features/block-ui/handle-geometry.js.map +1 -1
- package/dist/features/block-ui/index.d.ts +2 -1
- package/dist/features/block-ui/index.js +2 -1
- package/dist/features/block-ui/react/BlockHandles.d.ts +1 -1
- package/dist/features/block-ui/react/BlockHandles.js +4 -4
- package/dist/features/block-ui/react/BlockHandles.js.map +1 -1
- package/dist/features/block-ui/react/BlockUi.js +13 -25
- package/dist/features/block-ui/react/BlockUi.js.map +1 -1
- package/dist/features/comments/model.js +1 -1
- package/dist/features/composites/accepted-children.d.ts +5 -4
- package/dist/features/composites/accepted-children.js +26 -26
- package/dist/features/composites/accepted-children.js.map +1 -1
- package/dist/features/composites/ai-brief.d.ts +4 -4
- package/dist/features/composites/ai-brief.js +14 -12
- package/dist/features/composites/ai-brief.js.map +1 -1
- package/dist/features/composites/anchors.d.ts +1 -1
- package/dist/features/composites/anchors.js.map +1 -1
- package/dist/features/composites/baskets.d.ts +1 -1
- package/dist/features/composites/baskets.js.map +1 -1
- package/dist/features/composites/bibliography-command.d.ts +1 -1
- package/dist/features/composites/bibliography-form.js +1 -1
- package/dist/features/composites/bibliography-form.js.map +1 -1
- package/dist/features/composites/bibliography.js.map +1 -1
- package/dist/features/composites/block-types-json.d.ts +38 -0
- package/dist/features/composites/block-types-json.js +122 -0
- package/dist/features/composites/block-types-json.js.map +1 -0
- package/dist/features/composites/block-types.d.ts +119 -0
- package/dist/features/composites/block-types.js +343 -0
- package/dist/features/composites/block-types.js.map +1 -0
- package/dist/features/composites/capabilities.js +109 -0
- package/dist/features/composites/capabilities.js.map +1 -0
- package/dist/features/composites/components/BibliographyEditor.js +5 -5
- package/dist/features/composites/components/BibliographyEditor.js.map +1 -1
- package/dist/features/composites/components/CompositeBasketMenu.js +2 -2
- package/dist/features/composites/components/CompositeBasketMenu.js.map +1 -1
- package/dist/features/composites/components/CompositeChrome.js +18 -8
- package/dist/features/composites/components/CompositeChrome.js.map +1 -1
- package/dist/features/composites/components/CompositeChrome.vue.d.ts +1 -1
- package/dist/features/composites/components/CompositeDetails.js +11 -10
- package/dist/features/composites/components/CompositeDetails.js.map +1 -1
- package/dist/features/composites/components/CompositeDetails.vue.d.ts +1 -1
- package/dist/features/composites/components/CompositeFieldEditor.js +7 -7
- package/dist/features/composites/components/CompositeFieldEditor.js.map +1 -1
- package/dist/features/composites/components/CompositeFieldEditor.vue.d.ts +5 -5
- package/dist/features/composites/components/CompositeInspector.js +136 -113
- package/dist/features/composites/components/CompositeInspector.js.map +1 -1
- package/dist/features/composites/components/CompositeInspector.vue.d.ts +3 -3
- package/dist/features/composites/components/CompositeStatusMenu.js +2 -2
- package/dist/features/composites/components/CompositeStatusMenu.js.map +1 -1
- package/dist/features/composites/components/CompositeSummaryEditor.js +1 -1
- package/dist/features/composites/components/CompositeSummaryEditor.js.map +1 -1
- package/dist/features/composites/components/CompositeSummaryEditor.vue.d.ts +1 -1
- package/dist/features/composites/components/CompositeTable.js +2 -2
- package/dist/features/composites/components/CompositeTable.js.map +1 -1
- package/dist/features/composites/components/CompositeTagsEditor.js +1 -1
- package/dist/features/composites/components/CompositeTagsEditor.js.map +1 -1
- package/dist/features/composites/components/CompositeTagsEditor.vue.d.ts +1 -1
- package/dist/features/composites/components/CompositeView.js +19 -17
- package/dist/features/composites/components/CompositeView.js.map +1 -1
- package/dist/features/composites/components/DerivationBadge.js +15 -6
- package/dist/features/composites/components/DerivationBadge.js.map +1 -1
- package/dist/features/composites/components/FocusChip.js +1 -1
- package/dist/features/composites/components/FocusChip.js.map +1 -1
- package/dist/features/composites/components/FocusChip.vue.d.ts +1 -1
- package/dist/features/composites/components/ReferenceFieldSelect.js +3 -3
- package/dist/features/composites/components/ReferenceFieldSelect.js.map +1 -1
- package/dist/features/composites/components/ReferenceView.js +1 -1
- package/dist/features/composites/components/ReferenceView.js.map +1 -1
- package/dist/features/composites/components/SettingsEditingTab.js +18 -18
- package/dist/features/composites/components/SettingsEditingTab.js.map +1 -1
- package/dist/features/composites/components/SettingsEditingTab.vue.d.ts +3 -3
- package/dist/features/composites/convert.d.ts +4 -4
- package/dist/features/composites/convert.js +27 -28
- package/dist/features/composites/convert.js.map +1 -1
- package/dist/features/composites/derived-chrome.js +34 -28
- package/dist/features/composites/derived-chrome.js.map +1 -1
- package/dist/features/composites/editing-settings-keys.js +6 -6
- package/dist/features/composites/editing-settings-keys.js.map +1 -1
- package/dist/features/composites/feature.d.ts +10 -10
- package/dist/features/composites/feature.js +58 -46
- package/dist/features/composites/feature.js.map +1 -1
- package/dist/features/composites/fences.d.ts +3 -3
- package/dist/features/composites/fences.js +18 -13
- package/dist/features/composites/fences.js.map +1 -1
- package/dist/features/composites/focus.d.ts +10 -10
- package/dist/features/composites/focus.js +11 -11
- package/dist/features/composites/focus.js.map +1 -1
- package/dist/features/composites/heading-composites.d.ts +5 -5
- package/dist/features/composites/heading-composites.js +13 -16
- package/dist/features/composites/heading-composites.js.map +1 -1
- package/dist/features/composites/index.d.ts +20 -18
- package/dist/features/composites/index.js +9 -7
- package/dist/features/composites/inspector-model.d.ts +45 -22
- package/dist/features/composites/inspector-model.js +53 -14
- package/dist/features/composites/inspector-model.js.map +1 -1
- package/dist/features/composites/legacy.d.ts +5 -4
- package/dist/features/composites/legacy.js +16 -23
- package/dist/features/composites/legacy.js.map +1 -1
- package/dist/features/composites/markdown-export.js +3 -3
- package/dist/features/composites/markdown-export.js.map +1 -1
- package/dist/features/composites/merge.d.ts +8 -8
- package/dist/features/composites/merge.js +23 -22
- package/dist/features/composites/merge.js.map +1 -1
- package/dist/features/composites/meta-display.js +5 -5
- package/dist/features/composites/meta-display.js.map +1 -1
- package/dist/features/composites/meta-keys.d.ts +5 -5
- package/dist/features/composites/meta-keys.js +8 -8
- package/dist/features/composites/meta-keys.js.map +1 -1
- package/dist/features/composites/node-views.d.ts +3 -3
- package/dist/features/composites/node-views.js +2 -2
- package/dist/features/composites/node-views.js.map +1 -1
- package/dist/features/composites/nodes.d.ts +13 -13
- package/dist/features/composites/nodes.js +100 -77
- package/dist/features/composites/nodes.js.map +1 -1
- package/dist/features/composites/outline.d.ts +15 -12
- package/dist/features/composites/outline.js +24 -21
- package/dist/features/composites/outline.js.map +1 -1
- package/dist/features/composites/properties.d.ts +51 -0
- package/dist/features/composites/properties.js +89 -0
- package/dist/features/composites/properties.js.map +1 -0
- package/dist/features/composites/react/BibliographyEditor.js +5 -5
- package/dist/features/composites/react/BibliographyEditor.js.map +1 -1
- package/dist/features/composites/react/CompositeBasketMenu.js +2 -2
- package/dist/features/composites/react/CompositeBasketMenu.js.map +1 -1
- package/dist/features/composites/react/CompositeChrome.d.ts +1 -1
- package/dist/features/composites/react/CompositeChrome.js +15 -6
- package/dist/features/composites/react/CompositeChrome.js.map +1 -1
- package/dist/features/composites/react/CompositeDetails.d.ts +1 -1
- package/dist/features/composites/react/CompositeDetails.js +11 -10
- package/dist/features/composites/react/CompositeDetails.js.map +1 -1
- package/dist/features/composites/react/CompositeFieldEditor.d.ts +5 -5
- package/dist/features/composites/react/CompositeFieldEditor.js +6 -6
- package/dist/features/composites/react/CompositeFieldEditor.js.map +1 -1
- package/dist/features/composites/react/CompositeInspector.d.ts +3 -3
- package/dist/features/composites/react/CompositeInspector.js +142 -128
- package/dist/features/composites/react/CompositeInspector.js.map +1 -1
- package/dist/features/composites/react/CompositeStatusMenu.js +2 -2
- package/dist/features/composites/react/CompositeStatusMenu.js.map +1 -1
- package/dist/features/composites/react/CompositeSummaryEditor.d.ts +1 -1
- package/dist/features/composites/react/CompositeSummaryEditor.js +1 -1
- package/dist/features/composites/react/CompositeSummaryEditor.js.map +1 -1
- package/dist/features/composites/react/CompositeTable.d.ts +1 -1
- package/dist/features/composites/react/CompositeTable.js +3 -3
- package/dist/features/composites/react/CompositeTable.js.map +1 -1
- package/dist/features/composites/react/CompositeTagsEditor.d.ts +1 -1
- package/dist/features/composites/react/CompositeTagsEditor.js +1 -1
- package/dist/features/composites/react/CompositeTagsEditor.js.map +1 -1
- package/dist/features/composites/react/CompositeView.js +17 -15
- package/dist/features/composites/react/CompositeView.js.map +1 -1
- package/dist/features/composites/react/DerivationBadge.js +13 -4
- package/dist/features/composites/react/DerivationBadge.js.map +1 -1
- package/dist/features/composites/react/FocusChip.d.ts +1 -1
- package/dist/features/composites/react/FocusChip.js +1 -1
- package/dist/features/composites/react/FocusChip.js.map +1 -1
- package/dist/features/composites/react/ReferenceFieldSelect.js +3 -3
- package/dist/features/composites/react/ReferenceFieldSelect.js.map +1 -1
- package/dist/features/composites/react/ReferenceView.d.ts +1 -1
- package/dist/features/composites/react/ReferenceView.js +1 -1
- package/dist/features/composites/react/ReferenceView.js.map +1 -1
- package/dist/features/composites/react/SettingsEditingTab.d.ts +4 -4
- package/dist/features/composites/react/SettingsEditingTab.js +20 -20
- package/dist/features/composites/react/SettingsEditingTab.js.map +1 -1
- package/dist/features/composites/react.js +1 -1
- package/dist/features/composites/react.js.map +1 -1
- package/dist/features/composites/reference-ai.js +1 -1
- package/dist/features/composites/reference-ai.js.map +1 -1
- package/dist/features/composites/reference-label.d.ts +3 -3
- package/dist/features/composites/reference-label.js +6 -6
- package/dist/features/composites/reference-label.js.map +1 -1
- package/dist/features/composites/reference-suggestion.d.ts +1 -1
- package/dist/features/composites/reference-suggestion.js +3 -3
- package/dist/features/composites/reference-suggestion.js.map +1 -1
- package/dist/features/composites/reference-text.d.ts +2 -2
- package/dist/features/composites/reference-text.js +4 -4
- package/dist/features/composites/reference-text.js.map +1 -1
- package/dist/features/composites/reference.d.ts +3 -3
- package/dist/features/composites/reference.js +1 -1
- package/dist/features/composites/reference.js.map +1 -1
- package/dist/features/composites/reveal-source.d.ts +3 -3
- package/dist/features/composites/reveal-source.js +6 -6
- package/dist/features/composites/reveal-source.js.map +1 -1
- package/dist/features/composites/rows-model.d.ts +2 -2
- package/dist/features/composites/rows-model.js +4 -3
- package/dist/features/composites/rows-model.js.map +1 -1
- package/dist/features/composites/rows.d.ts +16 -28
- package/dist/features/composites/rows.js +20 -54
- package/dist/features/composites/rows.js.map +1 -1
- package/dist/features/composites/schema.d.ts +12 -11
- package/dist/features/composites/schema.js +10 -9
- package/dist/features/composites/schema.js.map +1 -1
- package/dist/features/composites/source.d.ts +20 -0
- package/dist/features/composites/source.js +40 -0
- package/dist/features/composites/source.js.map +1 -0
- package/dist/features/composites/split.d.ts +11 -11
- package/dist/features/composites/split.js +20 -20
- package/dist/features/composites/split.js.map +1 -1
- package/dist/features/composites/structure-mode.js +3 -3
- package/dist/features/composites/structure-mode.js.map +1 -1
- package/dist/features/composites/table-model.js +2 -2
- package/dist/features/composites/table-model.js.map +1 -1
- package/dist/features/composites/use-composite-inspector.js +3 -1
- package/dist/features/composites/use-composite-inspector.js.map +1 -1
- package/dist/features/composites/use-composite-rows.d.ts +2 -2
- package/dist/features/composites/use-composite-rows.js.map +1 -1
- package/dist/features/composites/vue.js +1 -1
- package/dist/features/composites/vue.js.map +1 -1
- package/dist/features/image-studio/components/ImageStudioDialog.js +150 -87
- package/dist/features/image-studio/components/ImageStudioDialog.js.map +1 -1
- package/dist/features/image-studio/components/ImageStudioDialog.vue.d.ts +5 -1
- package/dist/features/image-studio/components/ImageStudioLibraryPanel.js +186 -0
- package/dist/features/image-studio/components/ImageStudioLibraryPanel.js.map +1 -0
- package/dist/features/image-studio/components/ImageStudioLibraryTile.js +134 -0
- package/dist/features/image-studio/components/ImageStudioLibraryTile.js.map +1 -0
- package/dist/features/image-studio/components/ImageStudioView.js.map +1 -1
- package/dist/features/image-studio/feature.d.ts +1 -1
- package/dist/features/image-studio/feature.js +1 -1
- package/dist/features/image-studio/feature.js.map +1 -1
- package/dist/features/image-studio/index.d.ts +3 -2
- package/dist/features/image-studio/index.js +2 -2
- package/dist/features/image-studio/library.d.ts +35 -0
- package/dist/features/image-studio/node-core.d.ts +3 -3
- package/dist/features/image-studio/node-core.js +1 -1
- package/dist/features/image-studio/node-core.js.map +1 -1
- package/dist/features/image-studio/node-store.d.ts +1 -1
- package/dist/features/image-studio/node-store.js +1 -1
- package/dist/features/image-studio/node-store.js.map +1 -1
- package/dist/features/image-studio/react/ImageStudioDialog.d.ts +1 -1
- package/dist/features/image-studio/react/ImageStudioDialog.js +46 -5
- package/dist/features/image-studio/react/ImageStudioDialog.js.map +1 -1
- package/dist/features/image-studio/react/ImageStudioLibraryPanel.js +141 -0
- package/dist/features/image-studio/react/ImageStudioLibraryPanel.js.map +1 -0
- package/dist/features/image-studio/react/ImageStudioLibraryTile.js +105 -0
- package/dist/features/image-studio/react/ImageStudioLibraryTile.js.map +1 -0
- package/dist/features/image-studio/react/ImageStudioView.js +1 -1
- package/dist/features/image-studio/react/ImageStudioView.js.map +1 -1
- package/dist/features/image-studio/react/store.d.ts +8 -3
- package/dist/features/image-studio/react/store.js.map +1 -1
- package/dist/features/image-studio/react.d.ts +4 -3
- package/dist/features/image-studio/react.js +3 -3
- package/dist/features/image-studio/react.js.map +1 -1
- package/dist/features/image-studio/schema.d.ts +1 -1
- package/dist/features/image-studio/schema.js +1 -1
- package/dist/features/image-studio/schema.js.map +1 -1
- package/dist/features/image-studio/state.d.ts +8 -3
- package/dist/features/image-studio/state.js +11 -3
- package/dist/features/image-studio/state.js.map +1 -1
- package/dist/features/image-studio/store.d.ts +8 -3
- package/dist/features/image-studio/vue.d.ts +4 -3
- package/dist/features/image-studio/vue.js +3 -3
- package/dist/features/image-studio/vue.js.map +1 -1
- package/dist/features/mentions/tag.js +2 -1
- package/dist/features/mentions/tag.js.map +1 -1
- package/dist/features/themes/components/SettingsAppearanceTab.js +1 -1
- package/dist/features/themes/components/SettingsAppearanceTab.js.map +1 -1
- package/dist/features/themes/feature.d.ts +1 -1
- package/dist/features/themes/feature.js.map +1 -1
- package/dist/features/themes/react/SettingsAppearanceTab.d.ts +1 -1
- package/dist/features/themes/react/SettingsAppearanceTab.js +1 -1
- package/dist/features/themes/react/SettingsAppearanceTab.js.map +1 -1
- package/dist/features/themes/theme-runtime.js +1 -1
- package/dist/features/themes/theme-runtime.js.map +1 -1
- package/dist/features/transcript/TranscriptSegmentView.js +1 -1
- package/dist/features/transcript/TranscriptSegmentView.js.map +1 -1
- package/dist/features/transcript/react/TranscriptSegmentView.js +1 -1
- package/dist/features/transcript/react/TranscriptSegmentView.js.map +1 -1
- package/dist/features/transcript/segment-node.js.map +1 -1
- package/dist/features/transfer/feature.js +1 -1
- package/dist/features/transfer/feature.js.map +1 -1
- package/dist/features/transfer/transforms.d.ts +4 -4
- package/dist/features/transfer/transforms.js +12 -12
- package/dist/features/transfer/transforms.js.map +1 -1
- package/dist/i18n/de/ai.js +4 -4
- package/dist/i18n/de/ai.js.map +1 -1
- package/dist/i18n/de/authors.js +1 -1
- package/dist/i18n/de/authors.js.map +1 -1
- package/dist/i18n/de/comments.js +1 -1
- package/dist/i18n/de/comments.js.map +1 -1
- package/dist/i18n/de/composites.js +62 -59
- package/dist/i18n/de/composites.js.map +1 -1
- package/dist/i18n/de/image-studio.js +11 -2
- package/dist/i18n/de/image-studio.js.map +1 -1
- package/dist/i18n/de/integrations.js +20 -20
- package/dist/i18n/de/integrations.js.map +1 -1
- package/dist/i18n/de/markdown.js +1 -1
- package/dist/i18n/de/markdown.js.map +1 -1
- package/dist/i18n/de/palette.js +1 -1
- package/dist/i18n/de/palette.js.map +1 -1
- package/dist/i18n/de/themes.js +3 -3
- package/dist/i18n/de/themes.js.map +1 -1
- package/dist/i18n/de/transfer.js +2 -2
- package/dist/i18n/de/transfer.js.map +1 -1
- package/dist/i18n/de/tree.js +6 -6
- package/dist/i18n/de/tree.js.map +1 -1
- package/dist/i18n/de.d.ts +110 -98
- package/dist/i18n/en/ai.js +2 -2
- package/dist/i18n/en/ai.js.map +1 -1
- package/dist/i18n/en/composites.js +64 -61
- package/dist/i18n/en/composites.js.map +1 -1
- package/dist/i18n/en/image-studio.js +9 -0
- package/dist/i18n/en/image-studio.js.map +1 -1
- package/dist/i18n/en/integrations.js +9 -9
- package/dist/i18n/en/integrations.js.map +1 -1
- package/dist/i18n/en/markdown.js +1 -1
- package/dist/i18n/en/markdown.js.map +1 -1
- package/dist/i18n/en/palette.js +1 -1
- package/dist/i18n/en/palette.js.map +1 -1
- package/dist/i18n/en/themes.js +4 -4
- package/dist/i18n/en/themes.js.map +1 -1
- package/dist/i18n/en/tree.js +5 -5
- package/dist/i18n/en/tree.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/integrations/composite-ai/bibliography-fetch.d.ts +6 -6
- package/dist/integrations/composite-ai/bibliography-fetch.js +19 -19
- package/dist/integrations/composite-ai/bibliography-fetch.js.map +1 -1
- package/dist/integrations/composite-ai/composite-fill.d.ts +4 -4
- package/dist/integrations/composite-ai/composite-fill.js +5 -5
- package/dist/integrations/composite-ai/composite-fill.js.map +1 -1
- package/dist/integrations/composite-ai/composite-summary.d.ts +2 -2
- package/dist/integrations/composite-ai/composite-summary.js +5 -5
- package/dist/integrations/composite-ai/composite-summary.js.map +1 -1
- package/dist/integrations/composite-ai/derive.d.ts +17 -17
- package/dist/integrations/composite-ai/derive.js +27 -26
- package/dist/integrations/composite-ai/derive.js.map +1 -1
- package/dist/integrations/composite-ai/index.d.ts +14 -15
- package/dist/integrations/composite-ai/index.js +98 -77
- package/dist/integrations/composite-ai/index.js.map +1 -1
- package/dist/integrations/composite-ai/outline-sanitize.d.ts +1 -1
- package/dist/integrations/composite-ai/outline-sanitize.js +1 -1
- package/dist/integrations/composite-ai/outline-sanitize.js.map +1 -1
- package/dist/integrations/composite-ai/rendition.d.ts +9 -9
- package/dist/integrations/composite-ai/rendition.js +27 -27
- package/dist/integrations/composite-ai/rendition.js.map +1 -1
- package/dist/integrations/composite-ai/split.d.ts +8 -8
- package/dist/integrations/composite-ai/split.js +12 -12
- package/dist/integrations/composite-ai/split.js.map +1 -1
- package/dist/integrations/composite-ai/transfer-transforms.d.ts +2 -2
- package/dist/integrations/composite-ai/transfer-transforms.js +11 -11
- package/dist/integrations/composite-ai/transfer-transforms.js.map +1 -1
- package/dist/integrations/composite-media/index.d.ts +4 -3
- package/dist/integrations/composite-media/index.js +20 -14
- package/dist/integrations/composite-media/index.js.map +1 -1
- package/dist/markdown/MarkdownSurface.vue.d.ts +7 -7
- package/dist/markdown/document.js +1 -1
- package/dist/markdown/document.js.map +1 -1
- package/dist/nuxt/module.d.ts +48 -24
- package/dist/nuxt/module.js.map +1 -1
- package/dist/nuxt/runtime/server/plugins/editor-server.js +3 -2
- package/dist/nuxt/runtime/server/plugins/editor-server.js.map +1 -1
- package/dist/presets/compose.d.ts +6 -6
- package/dist/presets/compose.js +7 -4
- package/dist/presets/compose.js.map +1 -1
- package/dist/react/AdvancedEditor.d.ts +2 -2
- package/dist/react/AdvancedEditor.js +1 -1
- package/dist/react/AdvancedEditor.js.map +1 -1
- package/dist/react/compose.d.ts +1 -1
- package/dist/react/compose.js +1 -1
- package/dist/react/compose.js.map +1 -1
- package/dist/react/composed-surface.d.ts +5 -5
- package/dist/react/composed-surface.js +2 -2
- package/dist/react/composed-surface.js.map +1 -1
- package/dist/server/ai.d.ts +7 -7
- package/dist/server/ai.js +1 -1
- package/dist/server/ai.js.map +1 -1
- package/dist/server/auth.d.ts +1 -1
- package/dist/server/collab-state.d.ts +1 -1
- package/dist/server/collab-state.js.map +1 -1
- package/dist/server/collab.d.ts +4 -4
- package/dist/server/collab.js +22 -17
- package/dist/server/collab.js.map +1 -1
- package/dist/server/handlers/ai.d.ts +9 -6
- package/dist/server/handlers/ai.js +13 -17
- package/dist/server/handlers/ai.js.map +1 -1
- package/dist/server/handlers/context.d.ts +4 -0
- package/dist/server/handlers/context.js.map +1 -1
- package/dist/server/handlers/editor.d.ts +33 -32
- package/dist/server/handlers/editor.js +13 -8
- package/dist/server/handlers/editor.js.map +1 -1
- package/dist/server/handlers/images.d.ts +1 -1
- package/dist/server/handlers/images.js +1 -1
- package/dist/server/handlers/images.js.map +1 -1
- package/dist/server/host.d.ts +4 -0
- package/dist/server/host.js +10 -2
- package/dist/server/host.js.map +1 -1
- package/dist/server/index.d.ts +3 -3
- package/dist/server/ports.d.ts +8 -8
- package/dist/server/ports.js.map +1 -1
- package/dist/settings/index.d.ts +20 -19
- package/dist/settings/index.js +25 -24
- package/dist/settings/index.js.map +1 -1
- package/dist/settings/migrate.d.ts +18 -0
- package/dist/settings/migrate.js +46 -0
- package/dist/settings/migrate.js.map +1 -0
- package/dist/settings/values.d.ts +12 -12
- package/dist/settings/values.js +11 -11
- package/dist/settings/values.js.map +1 -1
- package/dist/styles/composites.css +10 -10
- package/dist/tree/DocumentTree.js +12 -5
- package/dist/tree/DocumentTree.js.map +1 -1
- package/dist/tree/drop-rules.js +5 -5
- package/dist/tree/drop-rules.js.map +1 -1
- package/dist/tree/react/DocumentTree.js +14 -5
- package/dist/tree/react/DocumentTree.js.map +1 -1
- package/dist/tree/tree-host.js +5 -5
- package/dist/tree/tree-host.js.map +1 -1
- package/dist/tree/tree-items.js +34 -28
- package/dist/tree/tree-items.js.map +1 -1
- package/dist/tree/tree-model.js +16 -16
- package/dist/tree/tree-model.js.map +1 -1
- package/dist/tree/tree-rows.js +2 -2
- package/dist/tree/tree-rows.js.map +1 -1
- package/dist/tree/tree-view-model.d.ts +2 -2
- package/dist/tree/tree-view-model.js.map +1 -1
- package/dist/vue/AdvancedEditor.js +2 -2
- package/dist/vue/AdvancedEditor.js.map +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +10 -10
- package/dist/vue/SimpleEditor.vue.d.ts +4 -4
- package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
- package/dist/vue/compose.d.ts +1 -1
- package/dist/vue/compose.js +1 -1
- package/dist/vue/compose.js.map +1 -1
- package/package.json +1 -1
- package/dist/features/composites/derivation.d.ts +0 -62
- package/dist/features/composites/derivation.js +0 -102
- package/dist/features/composites/derivation.js.map +0 -1
- package/dist/features/composites/kinds.d.ts +0 -89
- package/dist/features/composites/kinds.js +0 -285
- package/dist/features/composites/kinds.js.map +0 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { migratePersistedNames } from "../../core/schema/persisted-names.js";
|
|
2
|
+
import { isOutdatedEditorSettings, migrateEditorSettings } from "../../settings/migrate.js";
|
|
1
3
|
import { THEME_ID, editorSettingsSchema, parseEditorSettings } from "../../settings/index.js";
|
|
2
4
|
import { HttpError } from "../errors.js";
|
|
3
5
|
import { ASSET_DOCUMENT_ID, ASSET_NAME, assetUrl } from "../../features/media/assets.js";
|
|
@@ -7,6 +9,7 @@ import { COLLAB_FIELD } from "../collab.js";
|
|
|
7
9
|
import { authenticated, empty, json, port, readBody, text } from "./context.js";
|
|
8
10
|
import { serverCapabilities } from "../ports.js";
|
|
9
11
|
import { MAX_THEME_BYTES, themeManifestSchema } from "../../features/themes/schema.js";
|
|
12
|
+
import { z } from "zod";
|
|
10
13
|
import { createHash } from "node:crypto";
|
|
11
14
|
import { yDocToProsemirrorJSON } from "@tiptap/y-tiptap";
|
|
12
15
|
//#region src/server/handlers/editor.ts
|
|
@@ -19,21 +22,23 @@ async function capabilitiesGet(ctx, request) {
|
|
|
19
22
|
await authenticated(ctx, request);
|
|
20
23
|
return json(serverCapabilities(ctx.ports, ctx.served));
|
|
21
24
|
}
|
|
22
|
-
/** Body schema of `settingsPut`: the complete editor settings. */
|
|
23
|
-
const settingsPutBodySchema = editorSettingsSchema;
|
|
25
|
+
/** Body schema of `settingsPut`: the complete editor settings; a body of an older version (a tab from before the rename) is migrated first. */
|
|
26
|
+
const settingsPutBodySchema = z.preprocess(migrateEditorSettings, editorSettingsSchema);
|
|
24
27
|
const MAX_USERS = 20;
|
|
25
28
|
const MAX_TAGS = 20;
|
|
26
29
|
/**
|
|
27
|
-
* The authenticated user's editor settings, normalized with `parseEditorSettings`.
|
|
28
|
-
*
|
|
29
|
-
* ones).
|
|
30
|
+
* The authenticated user's editor settings, normalized with `parseEditorSettings`. A stored record older than
|
|
31
|
+
* `EDITOR_SETTINGS_VERSION` is written back migrated, once. `204` without a `settings` port (the client stays on
|
|
32
|
+
* localStorage); `200` with JSON `null` when the user has none stored yet (the client uploads its local ones).
|
|
30
33
|
*/
|
|
31
34
|
async function settingsGet(ctx, request) {
|
|
32
35
|
const user = await authenticated(ctx, request);
|
|
33
36
|
if (!ctx.ports.settings) return empty();
|
|
34
37
|
const stored = await ctx.ports.settings.loadSettings(user);
|
|
35
38
|
if (stored === null || stored === void 0) return json(null);
|
|
36
|
-
|
|
39
|
+
const settings = parseEditorSettings(stored);
|
|
40
|
+
if (isOutdatedEditorSettings(stored)) await ctx.ports.settings.storeSettings(user, settings);
|
|
41
|
+
return json(settings);
|
|
37
42
|
}
|
|
38
43
|
/** Stores the authenticated user's editor settings (complete, validated: 400 otherwise); `501` without a `settings` port. */
|
|
39
44
|
async function settingsPut(ctx, request) {
|
|
@@ -46,13 +51,13 @@ async function settingsPut(ctx, request) {
|
|
|
46
51
|
/**
|
|
47
52
|
* All `#` tag names used in any document, deduplicated and sorted. Documents currently open in the collaboration server
|
|
48
53
|
* are read from their live Y.Doc (a tag added in document A shows up in document B before the debounced store); all
|
|
49
|
-
* others from the stored content of `ports.documents
|
|
54
|
+
* others from the stored content of `ports.documents`, with its old persisted names migrated first (a stored document may predate the rename of the tags row).
|
|
50
55
|
*/
|
|
51
56
|
async function listWorkspaceTags({ ports, collab }) {
|
|
52
57
|
const names = /* @__PURE__ */ new Set();
|
|
53
58
|
const open = collab?.hocuspocus.documents;
|
|
54
59
|
if (open) for (const doc of open.values()) collectTagNames(yDocToProsemirrorJSON(doc, COLLAB_FIELD), names);
|
|
55
|
-
for (const { id, content } of await ports.documents.listContents()) if (!open?.has(id)) collectTagNames(content, names);
|
|
60
|
+
for (const { id, content } of await ports.documents.listContents()) if (!open?.has(id)) collectTagNames(migratePersistedNames(content, { earliest: true }), names);
|
|
56
61
|
return [...names].sort((a, b) => a.localeCompare(b, "de"));
|
|
57
62
|
}
|
|
58
63
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editor.js","names":[],"sources":["../../../src/server/handlers/editor.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { yDocToProsemirrorJSON } from '@tiptap/y-tiptap'\nimport { ASSET_DOCUMENT_ID, ASSET_NAME, assetUrl, MAX_MEDIA_BYTES, MEDIA_TYPES, mediaKindOf, mediaMime } from '../../features/media/schema'\nimport { collectTagNames, normalizeTagName } from '../../features/mentions/schema'\nimport { MAX_THEME_BYTES, THEME_ID, themeManifestSchema, type ThemeManifest, type ThemeRecord } from '../../features/themes/schema'\nimport { editorSettingsSchema, parseEditorSettings } from '../../settings'\nimport { COLLAB_FIELD } from '../collab'\nimport { HttpError } from '../errors'\nimport { serverCapabilities } from '../ports'\nimport { authenticated, empty, type HandlerContext, type HandlerRequest, type HandlerResponse, json, port, readBody, text } from './context'\n\n/**\n * The capabilities this server serves (`BackendCapability[]`, see `serverCapabilities`): what the client's probe\n * (`createHttpBackend({ probe: true })`) prunes its backend to. Requires an authenticated user through the `identity` port, like the\n * routes whose capabilities it lists.\n */\nexport async function capabilitiesGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n return json(serverCapabilities(ctx.ports, ctx.served))\n}\n\n/** Body schema of `settingsPut`: the complete editor settings. */\nexport const settingsPutBodySchema = editorSettingsSchema\n\nconst MAX_USERS = 20\nconst MAX_TAGS = 20\n\n/**\n * The authenticated user's editor settings, normalized with `parseEditorSettings`. `204` without a `settings` port (the\n * client stays on localStorage); `200` with JSON `null` when the user has none stored yet (the client uploads its local\n * ones).\n */\nexport async function settingsGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await authenticated(ctx, request)\n if (!ctx.ports.settings) return empty()\n const stored = await ctx.ports.settings.loadSettings(user)\n if (stored === null || stored === undefined) return json(null)\n return json(parseEditorSettings(stored))\n}\n\n/** Stores the authenticated user's editor settings (complete, validated: 400 otherwise); `501` without a `settings` port. */\nexport async function settingsPut(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await authenticated(ctx, request)\n const store = port(ctx, 'settings')\n const settings = await readBody(request, settingsPutBodySchema)\n await store.storeSettings(user, settings)\n return empty()\n}\n\n/**\n * All `#` tag names used in any document, deduplicated and sorted. Documents currently open in the collaboration server\n * are read from their live Y.Doc (a tag added in document A shows up in document B before the debounced store); all\n * others from the stored content of `ports.documents`.\n */\nexport async function listWorkspaceTags({ ports, collab }: HandlerContext): Promise<string[]> {\n const names = new Set<string>()\n const open = collab?.hocuspocus.documents\n if (open) for (const doc of open.values()) collectTagNames(yDocToProsemirrorJSON(doc, COLLAB_FIELD), names)\n for (const { id, content } of await ports.documents.listContents()) {\n if (!open?.has(id)) collectTagNames(content, names)\n }\n return [...names].sort((a, b) => a.localeCompare(b, 'de'))\n}\n\n/**\n * Tag names for the `#` popup: tags used in any document, filtered by the normalised `?q=` (prefix matches first, then\n * substrings). Requires an authenticated user through the `identity` port.\n */\nexport async function tagsGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n const q = normalizeTagName(String(request.query?.q ?? ''))\n const matches = (await listWorkspaceTags(ctx)).filter(tag => tag.includes(q))\n const prefixMatches = matches.filter(tag => tag.startsWith(q))\n return json([...prefixMatches, ...matches.filter(tag => !tag.startsWith(q))].slice(0, MAX_TAGS))\n}\n\n/**\n * Users for the `@` mention popup, filtered case-insensitively by `?q=` (prefix matches first, then substrings).\n * Requires an authenticated user through the `identity` port.\n */\nexport async function usersGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n const q = String(request.query?.q ?? '').trim().toLowerCase()\n // Re-pick the fields so a directory returning richer user records cannot leak them.\n const matches = (await port(ctx, 'directory').listUsers())\n .map(({ id, username, color }) => ({ id, username, color }))\n .filter(u => u.username.toLowerCase().includes(q))\n .sort((a, b) => a.username.localeCompare(b.username, 'de'))\n const prefixMatches = matches.filter(u => u.username.toLowerCase().startsWith(q))\n return json([...prefixMatches, ...matches.filter(u => !prefixMatches.includes(u))].slice(0, MAX_USERS))\n}\n\n/**\n * Stores a file (multipart field `file`) in the package of document `params.id` (or `scratch` for editors not bound to\n * a stored document); answers `{ url, name, sha256 }`. Requires an authenticated user through the `identity` port.\n */\nexport async function assetPost(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n const id = request.params?.id ?? ''\n if (!ASSET_DOCUMENT_ID.test(id)) throw new HttpError(400, 'Ungültige Dokument-ID')\n // A body that is no multipart form at all counts as „no file“.\n const form = await request.readForm?.().catch(() => null)\n const file = form?.get('file')\n if (!(file instanceof File)) throw new HttpError(400, 'Keine Datei übermittelt')\n const mime = mediaMime(file.type) || 'application/octet-stream'\n const maxBytes = MAX_MEDIA_BYTES[mediaKindOf(mime) ?? 'file']\n if (file.size > maxBytes) throw new HttpError(413, `Datei ist größer als ${maxBytes / 1024 / 1024} MB`)\n const nameExt = file.name.includes('.') ? file.name.split('.').pop()?.toLowerCase() : undefined\n const ext = MEDIA_TYPES[mime]?.ext ?? (nameExt && /^[a-z0-9]{1,8}$/.test(nameExt) ? nameExt : 'bin')\n const name = `${globalThis.crypto.randomUUID()}.${ext}`\n const data = Buffer.from(await file.arrayBuffer())\n const sha256 = createHash('sha256').update(data).digest('hex')\n await port(ctx, 'assets').storeAsset(id, {\n name,\n filename: file.name.slice(0, 255) || name,\n mimeType: mime,\n size: file.size,\n createdAt: new Date().toISOString(),\n sha256,\n data,\n })\n return json({ url: assetUrl(id, name), name, sha256 })\n}\n\n// Answers single byte ranges (`Range: bytes=a-b`, `bytes=a-`, `bytes=-n`): Safari only plays and seeks audio/video\n// served with range support. Other Range forms (several ranges) get the whole file, which RFC 9110 allows.\nconst BYTE_RANGE = /^bytes=(\\d*)-(\\d*)$/\n\n/**\n * The stored file `params.name` of document `params.id`, with `Range` support (media elements send the session cookie\n * like any same-origin request). Requires an authenticated user through the `identity` port.\n */\nexport async function assetGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n const id = request.params?.id ?? ''\n const name = request.params?.name ?? ''\n if (!ASSET_DOCUMENT_ID.test(id) || !ASSET_NAME.test(name)) throw new HttpError(404, 'Datei nicht gefunden')\n const asset = await port(ctx, 'assets').loadAsset(id, name)\n if (!asset) throw new HttpError(404, 'Datei nicht gefunden')\n const { record, data } = asset\n const total = data.length\n const disposition = record.mimeType === 'application/octet-stream' ? 'attachment' : 'inline'\n const headers: Record<string, string> = {\n 'accept-ranges': 'bytes',\n 'cache-control': 'private, max-age=31536000, immutable',\n 'content-type': record.mimeType,\n 'content-disposition': `${disposition}; filename*=UTF-8''${encodeURIComponent(record.filename)}`,\n }\n const range = BYTE_RANGE.exec(request.headers.get('range') ?? '')\n if (!range) return { status: 200, headers: { ...headers, 'content-length': String(total) }, body: data }\n const [, first, last] = range\n const start = first ? Number(first) : Math.max(0, total - Number(last))\n const end = first && last ? Math.min(Number(last), total - 1) : total - 1\n if (start >= total || start > end) return empty(416, { 'content-range': `bytes */${total}` })\n return {\n status: 206,\n headers: { ...headers, 'content-range': `bytes ${start}-${end}/${total}`, 'content-length': String(end - start + 1) },\n body: data.subarray(start, end + 1),\n }\n}\n\n/** The theme library (builtins and uploads) as `ThemeRecord[]`. `501` without a `themes` port. */\nexport async function themesGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n return json(await port(ctx, 'themes').listThemes())\n}\n\n/** `theme.css` of theme `params.id` as `text/css`. `400` for a malformed id, `404` for an unknown one, `501` without a `themes` port. */\nexport async function themeGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n const themes = port(ctx, 'themes')\n const id = request.params?.id ?? ''\n if (!THEME_ID.test(id)) throw new HttpError(400, 'Ungültige Theme-ID')\n const theme = await themes.loadTheme(id)\n if (!theme) throw new HttpError(404, 'Theme nicht gefunden')\n const etag = `\"${theme.record.version ?? '0'}-${new TextEncoder().encode(theme.css).length}\"`\n const headers = { 'cache-control': 'private, max-age=0, must-revalidate', 'etag': etag }\n if (request.headers.get('if-none-match') === etag) return empty(304, headers)\n return text(theme.css, 'text/css; charset=utf-8', headers)\n}\n\n/** Deletes the uploaded theme `params.id` (`204`). `400` for a malformed id, `404` for an unknown one, `403` for a builtin, `501` without a `themes` port. */\nexport async function themeDelete(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n const themes = port(ctx, 'themes')\n const id = request.params?.id ?? ''\n if (!THEME_ID.test(id)) throw new HttpError(400, 'Ungültige Theme-ID')\n // `removeTheme` answers `false` for builtins and unknown ids alike, so the record decides between 403 and 404.\n const record = (await themes.listThemes()).find(t => t.id === id)\n if (!record) throw new HttpError(404, 'Theme nicht gefunden')\n if (record.builtin) throw new HttpError(403, 'Mitgelieferte Themes können nicht gelöscht werden')\n if (!await themes.removeTheme(id)) throw new HttpError(404, 'Theme nicht gefunden')\n return empty()\n}\n\nconst MAX_MANIFEST_BYTES = 64 * 1024\nconst MAX_ID_LENGTH = 64\n// Markers of an HTML/XML/SVG document in the head of a „stylesheet“.\nconst MARKUP_HEAD = /<script|<\\?xml/i\n\n/** `[^a-z0-9]+` → `-`, trimmed, at most `max` characters; `theme` when nothing is left. */\nfunction slugOf(name: string, max: number): string {\n const slug = name.normalize('NFKD').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+/, '').slice(0, max).replace(/-+$/, '')\n return slug || 'theme'\n}\n\n/**\n * Uploads an Obsidian theme into the shared library: multipart `css` (File, required), `manifest` (File) and `name`\n * (string) optional. Answers 201 with the new `ThemeRecord`; the id is the slug of the name, made unique with `-2`,\n * `-3` … against the library.\n */\nexport async function themePost(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n const themes = port(ctx, 'themes')\n // A body that is no multipart form counts as „no file“.\n const form = await request.readForm?.().catch(() => null)\n const cssFile = form?.get('css')\n if (!(cssFile instanceof File) || cssFile.size === 0) throw new HttpError(400, 'Keine CSS-Datei übermittelt')\n if (cssFile.size > MAX_THEME_BYTES) throw new HttpError(413, `Theme ist größer als ${MAX_THEME_BYTES / 1024 / 1024} MB`)\n const bytes = new Uint8Array(await cssFile.arrayBuffer())\n if (bytes.includes(0) || MARKUP_HEAD.test(new TextDecoder('latin1').decode(bytes.subarray(0, 512)))) {\n throw new HttpError(400, 'Theme ist keine CSS-Textdatei')\n }\n let css: string\n try {\n css = new TextDecoder('utf-8', { fatal: true }).decode(bytes)\n }\n catch {\n throw new HttpError(400, 'Theme ist keine gültige UTF-8-Textdatei')\n }\n\n let manifest: ThemeManifest | undefined\n const manifestFile = form?.get('manifest')\n if (manifestFile instanceof File && manifestFile.size > 0) {\n try {\n if (manifestFile.size > MAX_MANIFEST_BYTES) throw new Error('manifest too large')\n manifest = themeManifestSchema.parse(JSON.parse(await manifestFile.text()))\n }\n catch {\n throw new HttpError(400, 'Ungültiges manifest.json')\n }\n }\n\n const nameField = form?.get('name')\n const cssBaseName = cssFile.name.replace(/\\.css$/i, '')\n const name = manifest?.name\n ?? [typeof nameField === 'string' ? nameField : '', cssBaseName].map(n => n.trim().slice(0, 120)).find(Boolean)\n ?? 'Theme'\n\n const taken = new Set((await themes.listThemes()).map(t => t.id))\n const base = slugOf(name, MAX_ID_LENGTH)\n let id = base\n for (let n = 2; taken.has(id); n++) {\n const suffix = `-${n}`\n id = `${slugOf(name, MAX_ID_LENGTH - suffix.length)}${suffix}`\n }\n\n const record: Omit<ThemeRecord, 'builtin'> = { ...manifest, name, id }\n await themes.storeTheme(record, css)\n return json({ ...record, builtin: false } satisfies ThemeRecord, 201)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"editor.js","names":[],"sources":["../../../src/server/handlers/editor.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { yDocToProsemirrorJSON } from '@tiptap/y-tiptap'\nimport { z } from 'zod'\nimport { migratePersistedNames } from '../../core/schema'\nimport { ASSET_DOCUMENT_ID, ASSET_NAME, assetUrl, MAX_MEDIA_BYTES, MEDIA_TYPES, mediaKindOf, mediaMime } from '../../features/media/schema'\nimport { collectTagNames, normalizeTagName } from '../../features/mentions/schema'\nimport { MAX_THEME_BYTES, THEME_ID, themeManifestSchema, type ThemeManifest, type ThemeRecord } from '../../features/themes/schema'\nimport { editorSettingsSchema, isOutdatedEditorSettings, migrateEditorSettings, parseEditorSettings } from '../../settings'\nimport { COLLAB_FIELD } from '../collab'\nimport { HttpError } from '../errors'\nimport { serverCapabilities } from '../ports'\nimport { authenticated, empty, type HandlerContext, type HandlerRequest, type HandlerResponse, json, port, readBody, text } from './context'\n\n/**\n * The capabilities this server serves (`BackendCapability[]`, see `serverCapabilities`): what the client's probe\n * (`createHttpBackend({ probe: true })`) prunes its backend to. Requires an authenticated user through the `identity` port, like the\n * routes whose capabilities it lists.\n */\nexport async function capabilitiesGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n return json(serverCapabilities(ctx.ports, ctx.served))\n}\n\n/** Body schema of `settingsPut`: the complete editor settings; a body of an older version (a tab from before the rename) is migrated first. */\nexport const settingsPutBodySchema = z.preprocess(migrateEditorSettings, editorSettingsSchema)\n\nconst MAX_USERS = 20\nconst MAX_TAGS = 20\n\n/**\n * The authenticated user's editor settings, normalized with `parseEditorSettings`. A stored record older than\n * `EDITOR_SETTINGS_VERSION` is written back migrated, once. `204` without a `settings` port (the client stays on\n * localStorage); `200` with JSON `null` when the user has none stored yet (the client uploads its local ones).\n */\nexport async function settingsGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await authenticated(ctx, request)\n if (!ctx.ports.settings) return empty()\n const stored = await ctx.ports.settings.loadSettings(user)\n if (stored === null || stored === undefined) return json(null)\n const settings = parseEditorSettings(stored)\n if (isOutdatedEditorSettings(stored)) await ctx.ports.settings.storeSettings(user, settings)\n return json(settings)\n}\n\n/** Stores the authenticated user's editor settings (complete, validated: 400 otherwise); `501` without a `settings` port. */\nexport async function settingsPut(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await authenticated(ctx, request)\n const store = port(ctx, 'settings')\n const settings = await readBody(request, settingsPutBodySchema)\n await store.storeSettings(user, settings)\n return empty()\n}\n\n/**\n * All `#` tag names used in any document, deduplicated and sorted. Documents currently open in the collaboration server\n * are read from their live Y.Doc (a tag added in document A shows up in document B before the debounced store); all\n * others from the stored content of `ports.documents`, with its old persisted names migrated first (a stored document may predate the rename of the tags row).\n */\nexport async function listWorkspaceTags({ ports, collab }: HandlerContext): Promise<string[]> {\n const names = new Set<string>()\n const open = collab?.hocuspocus.documents\n if (open) for (const doc of open.values()) collectTagNames(yDocToProsemirrorJSON(doc, COLLAB_FIELD), names)\n for (const { id, content } of await ports.documents.listContents()) {\n if (!open?.has(id)) collectTagNames(migratePersistedNames(content, { earliest: true }), names)\n }\n return [...names].sort((a, b) => a.localeCompare(b, 'de'))\n}\n\n/**\n * Tag names for the `#` popup: tags used in any document, filtered by the normalised `?q=` (prefix matches first, then\n * substrings). Requires an authenticated user through the `identity` port.\n */\nexport async function tagsGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n const q = normalizeTagName(String(request.query?.q ?? ''))\n const matches = (await listWorkspaceTags(ctx)).filter(tag => tag.includes(q))\n const prefixMatches = matches.filter(tag => tag.startsWith(q))\n return json([...prefixMatches, ...matches.filter(tag => !tag.startsWith(q))].slice(0, MAX_TAGS))\n}\n\n/**\n * Users for the `@` mention popup, filtered case-insensitively by `?q=` (prefix matches first, then substrings).\n * Requires an authenticated user through the `identity` port.\n */\nexport async function usersGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n const q = String(request.query?.q ?? '').trim().toLowerCase()\n // Re-pick the fields so a directory returning richer user records cannot leak them.\n const matches = (await port(ctx, 'directory').listUsers())\n .map(({ id, username, color }) => ({ id, username, color }))\n .filter(u => u.username.toLowerCase().includes(q))\n .sort((a, b) => a.username.localeCompare(b.username, 'de'))\n const prefixMatches = matches.filter(u => u.username.toLowerCase().startsWith(q))\n return json([...prefixMatches, ...matches.filter(u => !prefixMatches.includes(u))].slice(0, MAX_USERS))\n}\n\n/**\n * Stores a file (multipart field `file`) in the package of document `params.id` (or `scratch` for editors not bound to\n * a stored document); answers `{ url, name, sha256 }`. Requires an authenticated user through the `identity` port.\n */\nexport async function assetPost(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n const id = request.params?.id ?? ''\n if (!ASSET_DOCUMENT_ID.test(id)) throw new HttpError(400, 'Ungültige Dokument-ID')\n // A body that is no multipart form at all counts as „no file“.\n const form = await request.readForm?.().catch(() => null)\n const file = form?.get('file')\n if (!(file instanceof File)) throw new HttpError(400, 'Keine Datei übermittelt')\n const mime = mediaMime(file.type) || 'application/octet-stream'\n const maxBytes = MAX_MEDIA_BYTES[mediaKindOf(mime) ?? 'file']\n if (file.size > maxBytes) throw new HttpError(413, `Datei ist größer als ${maxBytes / 1024 / 1024} MB`)\n const nameExt = file.name.includes('.') ? file.name.split('.').pop()?.toLowerCase() : undefined\n const ext = MEDIA_TYPES[mime]?.ext ?? (nameExt && /^[a-z0-9]{1,8}$/.test(nameExt) ? nameExt : 'bin')\n const name = `${globalThis.crypto.randomUUID()}.${ext}`\n const data = Buffer.from(await file.arrayBuffer())\n const sha256 = createHash('sha256').update(data).digest('hex')\n await port(ctx, 'assets').storeAsset(id, {\n name,\n filename: file.name.slice(0, 255) || name,\n mimeType: mime,\n size: file.size,\n createdAt: new Date().toISOString(),\n sha256,\n data,\n })\n return json({ url: assetUrl(id, name), name, sha256 })\n}\n\n// Answers single byte ranges (`Range: bytes=a-b`, `bytes=a-`, `bytes=-n`): Safari only plays and seeks audio/video\n// served with range support. Other Range forms (several ranges) get the whole file, which RFC 9110 allows.\nconst BYTE_RANGE = /^bytes=(\\d*)-(\\d*)$/\n\n/**\n * The stored file `params.name` of document `params.id`, with `Range` support (media elements send the session cookie\n * like any same-origin request). Requires an authenticated user through the `identity` port.\n */\nexport async function assetGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n const id = request.params?.id ?? ''\n const name = request.params?.name ?? ''\n if (!ASSET_DOCUMENT_ID.test(id) || !ASSET_NAME.test(name)) throw new HttpError(404, 'Datei nicht gefunden')\n const asset = await port(ctx, 'assets').loadAsset(id, name)\n if (!asset) throw new HttpError(404, 'Datei nicht gefunden')\n const { record, data } = asset\n const total = data.length\n const disposition = record.mimeType === 'application/octet-stream' ? 'attachment' : 'inline'\n const headers: Record<string, string> = {\n 'accept-ranges': 'bytes',\n 'cache-control': 'private, max-age=31536000, immutable',\n 'content-type': record.mimeType,\n 'content-disposition': `${disposition}; filename*=UTF-8''${encodeURIComponent(record.filename)}`,\n }\n const range = BYTE_RANGE.exec(request.headers.get('range') ?? '')\n if (!range) return { status: 200, headers: { ...headers, 'content-length': String(total) }, body: data }\n const [, first, last] = range\n const start = first ? Number(first) : Math.max(0, total - Number(last))\n const end = first && last ? Math.min(Number(last), total - 1) : total - 1\n if (start >= total || start > end) return empty(416, { 'content-range': `bytes */${total}` })\n return {\n status: 206,\n headers: { ...headers, 'content-range': `bytes ${start}-${end}/${total}`, 'content-length': String(end - start + 1) },\n body: data.subarray(start, end + 1),\n }\n}\n\n/** The theme library (builtins and uploads) as `ThemeRecord[]`. `501` without a `themes` port. */\nexport async function themesGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n return json(await port(ctx, 'themes').listThemes())\n}\n\n/** `theme.css` of theme `params.id` as `text/css`. `400` for a malformed id, `404` for an unknown one, `501` without a `themes` port. */\nexport async function themeGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n const themes = port(ctx, 'themes')\n const id = request.params?.id ?? ''\n if (!THEME_ID.test(id)) throw new HttpError(400, 'Ungültige Theme-ID')\n const theme = await themes.loadTheme(id)\n if (!theme) throw new HttpError(404, 'Theme nicht gefunden')\n const etag = `\"${theme.record.version ?? '0'}-${new TextEncoder().encode(theme.css).length}\"`\n const headers = { 'cache-control': 'private, max-age=0, must-revalidate', 'etag': etag }\n if (request.headers.get('if-none-match') === etag) return empty(304, headers)\n return text(theme.css, 'text/css; charset=utf-8', headers)\n}\n\n/** Deletes the uploaded theme `params.id` (`204`). `400` for a malformed id, `404` for an unknown one, `403` for a builtin, `501` without a `themes` port. */\nexport async function themeDelete(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n const themes = port(ctx, 'themes')\n const id = request.params?.id ?? ''\n if (!THEME_ID.test(id)) throw new HttpError(400, 'Ungültige Theme-ID')\n // `removeTheme` answers `false` for builtins and unknown ids alike, so the record decides between 403 and 404.\n const record = (await themes.listThemes()).find(t => t.id === id)\n if (!record) throw new HttpError(404, 'Theme nicht gefunden')\n if (record.builtin) throw new HttpError(403, 'Mitgelieferte Themes können nicht gelöscht werden')\n if (!await themes.removeTheme(id)) throw new HttpError(404, 'Theme nicht gefunden')\n return empty()\n}\n\nconst MAX_MANIFEST_BYTES = 64 * 1024\nconst MAX_ID_LENGTH = 64\n// Markers of an HTML/XML/SVG document in the head of a „stylesheet“.\nconst MARKUP_HEAD = /<script|<\\?xml/i\n\n/** `[^a-z0-9]+` → `-`, trimmed, at most `max` characters; `theme` when nothing is left. */\nfunction slugOf(name: string, max: number): string {\n const slug = name.normalize('NFKD').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+/, '').slice(0, max).replace(/-+$/, '')\n return slug || 'theme'\n}\n\n/**\n * Uploads an Obsidian theme into the shared library: multipart `css` (File, required), `manifest` (File) and `name`\n * (string) optional. Answers 201 with the new `ThemeRecord`; the id is the slug of the name, made unique with `-2`,\n * `-3` … against the library.\n */\nexport async function themePost(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n await authenticated(ctx, request)\n const themes = port(ctx, 'themes')\n // A body that is no multipart form counts as „no file“.\n const form = await request.readForm?.().catch(() => null)\n const cssFile = form?.get('css')\n if (!(cssFile instanceof File) || cssFile.size === 0) throw new HttpError(400, 'Keine CSS-Datei übermittelt')\n if (cssFile.size > MAX_THEME_BYTES) throw new HttpError(413, `Theme ist größer als ${MAX_THEME_BYTES / 1024 / 1024} MB`)\n const bytes = new Uint8Array(await cssFile.arrayBuffer())\n if (bytes.includes(0) || MARKUP_HEAD.test(new TextDecoder('latin1').decode(bytes.subarray(0, 512)))) {\n throw new HttpError(400, 'Theme ist keine CSS-Textdatei')\n }\n let css: string\n try {\n css = new TextDecoder('utf-8', { fatal: true }).decode(bytes)\n }\n catch {\n throw new HttpError(400, 'Theme ist keine gültige UTF-8-Textdatei')\n }\n\n let manifest: ThemeManifest | undefined\n const manifestFile = form?.get('manifest')\n if (manifestFile instanceof File && manifestFile.size > 0) {\n try {\n if (manifestFile.size > MAX_MANIFEST_BYTES) throw new Error('manifest too large')\n manifest = themeManifestSchema.parse(JSON.parse(await manifestFile.text()))\n }\n catch {\n throw new HttpError(400, 'Ungültiges manifest.json')\n }\n }\n\n const nameField = form?.get('name')\n const cssBaseName = cssFile.name.replace(/\\.css$/i, '')\n const name = manifest?.name\n ?? [typeof nameField === 'string' ? nameField : '', cssBaseName].map(n => n.trim().slice(0, 120)).find(Boolean)\n ?? 'Theme'\n\n const taken = new Set((await themes.listThemes()).map(t => t.id))\n const base = slugOf(name, MAX_ID_LENGTH)\n let id = base\n for (let n = 2; taken.has(id); n++) {\n const suffix = `-${n}`\n id = `${slugOf(name, MAX_ID_LENGTH - suffix.length)}${suffix}`\n }\n\n const record: Omit<ThemeRecord, 'builtin'> = { ...manifest, name, id }\n await themes.storeTheme(record, css)\n return json({ ...record, builtin: false } satisfies ThemeRecord, 201)\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HandlerContext, HandlerRequest, HandlerResponse } from "./context.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
//#region src/server/handlers/images.d.ts
|
|
4
|
-
/** The
|
|
4
|
+
/** The prompt blocks the host manages (`ImageStudioPromptBlock[]`). `501` without an `images` port. */
|
|
5
5
|
export declare function imagePromptBlocksGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse>;
|
|
6
6
|
/** Body schema of `imageGeneratePost`. */
|
|
7
7
|
export declare const imageGenerateBodySchema: z.ZodObject<{
|
|
@@ -4,7 +4,7 @@ import { MAX_MEDIA_BYTES, mediaKindOf } from "../../features/media/schema.js";
|
|
|
4
4
|
import { authenticated, json, port, readBody } from "./context.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
//#region src/server/handlers/images.ts
|
|
7
|
-
/** The
|
|
7
|
+
/** The prompt blocks the host manages (`ImageStudioPromptBlock[]`). `501` without an `images` port. */
|
|
8
8
|
async function imagePromptBlocksGet(ctx, request) {
|
|
9
9
|
const user = await authenticated(ctx, request);
|
|
10
10
|
return json(await port(ctx, "images").promptBlocks(user));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"images.js","names":[],"sources":["../../../src/server/handlers/images.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { ImageStudioGenerateBody } from '../../core/backend-types'\nimport { ASSET_DOCUMENT_ID, MAX_MEDIA_BYTES, mediaKindOf } from '../../features/media/schema'\nimport { HttpError } from '../errors'\nimport { authenticated, type HandlerContext, type HandlerRequest, type HandlerResponse, json, port, readBody } from './context'\n\n// The Bild-Studio's three routes over the `images` port: the host's
|
|
1
|
+
{"version":3,"file":"images.js","names":[],"sources":["../../../src/server/handlers/images.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { ImageStudioGenerateBody } from '../../core/backend-types'\nimport { ASSET_DOCUMENT_ID, MAX_MEDIA_BYTES, mediaKindOf } from '../../features/media/schema'\nimport { HttpError } from '../errors'\nimport { authenticated, type HandlerContext, type HandlerRequest, type HandlerResponse, json, port, readBody } from './context'\n\n// The Bild-Studio's three routes over the `images` port: the host's prompt blocks, one generation, one reference upload.\n// The host's provider owns the prompt, the model and the storage; these handlers only authenticate, validate and pass through.\n\n/** The prompt blocks the host manages (`ImageStudioPromptBlock[]`). `501` without an `images` port. */\nexport async function imagePromptBlocksGet(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await authenticated(ctx, request)\n return json(await port(ctx, 'images').promptBlocks(user))\n}\n\n/** Body schema of `imageGeneratePost`. */\nexport const imageGenerateBodySchema = z.object({\n description: z.string().max(10_000),\n instruction: z.string().max(4_000),\n promptBlockIds: z.array(z.string().min(1).max(200)).max(50),\n referenceUrls: z.array(z.string().min(1).max(2_048)).max(20),\n}) satisfies z.ZodType<ImageStudioGenerateBody>\n\n/** Generates one image for the body (`ImageStudioGenerateBody`); answers the stored `ImageStudioImage`. `400` for a malformed body, `501` without an `images` port. */\nexport async function imageGeneratePost(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await authenticated(ctx, request)\n const images = port(ctx, 'images')\n const body = await readBody(request, imageGenerateBodySchema)\n return json(await images.generate(body, user))\n}\n\n/**\n * Stores a reference image: multipart fields `documentId` (the document it belongs to, or `scratch`) and `file` (an image);\n * answers the stored `ImageStudioImage`. `400` for a malformed `documentId` or a missing file, `413` for a file over the\n * image limit, `415` for a file that is no image, `501` without an `images` port.\n */\nexport async function imageUploadPost(ctx: HandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await authenticated(ctx, request)\n const images = port(ctx, 'images')\n // A body that is no multipart form at all counts as „no file“.\n const form = await request.readForm?.().catch(() => null)\n const documentId = form?.get('documentId')\n if (typeof documentId !== 'string' || !ASSET_DOCUMENT_ID.test(documentId)) throw new HttpError(400, 'Ungültige Dokument-ID')\n const file = form?.get('file')\n if (!(file instanceof File)) throw new HttpError(400, 'Keine Datei übermittelt')\n if (mediaKindOf(file.type) !== 'image') throw new HttpError(415, 'Nur Bilddateien werden akzeptiert')\n if (file.size > MAX_MEDIA_BYTES.image) throw new HttpError(413, `Datei ist größer als ${MAX_MEDIA_BYTES.image / 1024 / 1024} MB`)\n return json(await images.upload(documentId, file, user))\n}\n"],"mappings":";;;;;;;AAUA,eAAsB,qBAAqB,KAAqB,SAAmD;CACjH,MAAM,OAAO,MAAM,cAAc,KAAK,OAAO;CAC7C,OAAO,KAAK,MAAM,KAAK,KAAK,QAAQ,CAAC,CAAC,aAAa,IAAI,CAAC;AAC1D;;AAGA,MAAa,0BAA0B,EAAE,OAAO;CAC9C,aAAa,EAAE,OAAO,CAAC,CAAC,IAAI,GAAM;CAClC,aAAa,EAAE,OAAO,CAAC,CAAC,IAAI,GAAK;CACjC,gBAAgB,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE;CAC1D,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAK,CAAC,CAAC,CAAC,IAAI,EAAE;AAC7D,CAAC;;AAGD,eAAsB,kBAAkB,KAAqB,SAAmD;CAC9G,MAAM,OAAO,MAAM,cAAc,KAAK,OAAO;CAC7C,MAAM,SAAS,KAAK,KAAK,QAAQ;CACjC,MAAM,OAAO,MAAM,SAAS,SAAS,uBAAuB;CAC5D,OAAO,KAAK,MAAM,OAAO,SAAS,MAAM,IAAI,CAAC;AAC/C;;;;;;AAOA,eAAsB,gBAAgB,KAAqB,SAAmD;CAC5G,MAAM,OAAO,MAAM,cAAc,KAAK,OAAO;CAC7C,MAAM,SAAS,KAAK,KAAK,QAAQ;CAEjC,MAAM,OAAO,MAAM,QAAQ,WAAW,CAAC,CAAC,YAAY,IAAI;CACxD,MAAM,aAAa,MAAM,IAAI,YAAY;CACzC,IAAI,OAAO,eAAe,YAAY,CAAC,kBAAkB,KAAK,UAAU,GAAG,MAAM,IAAI,UAAU,KAAK,uBAAuB;CAC3H,MAAM,OAAO,MAAM,IAAI,MAAM;CAC7B,IAAI,EAAE,gBAAgB,OAAO,MAAM,IAAI,UAAU,KAAK,yBAAyB;CAC/E,IAAI,YAAY,KAAK,IAAI,MAAM,SAAS,MAAM,IAAI,UAAU,KAAK,mCAAmC;CACpG,IAAI,KAAK,OAAO,gBAAgB,OAAO,MAAM,IAAI,UAAU,KAAK,wBAAwB,gBAAgB,QAAQ,OAAO,KAAK,IAAI;CAChI,OAAO,KAAK,MAAM,OAAO,OAAO,YAAY,MAAM,IAAI,CAAC;AACzD"}
|
package/dist/server/host.d.ts
CHANGED
|
@@ -3,15 +3,19 @@ import "../features/ai/protocol/index.js";
|
|
|
3
3
|
import { AiModels } from "./ai.js";
|
|
4
4
|
import { EditorPorts } from "./ports.js";
|
|
5
5
|
import { CollabServer } from "./collab.js";
|
|
6
|
+
import { BlockType } from "../features/composites/block-types.js";
|
|
7
|
+
import "../features/composites/schema.js";
|
|
6
8
|
import { AnyExtension, JSONContent } from "@tiptap/core";
|
|
7
9
|
import { LanguageModel } from "ai";
|
|
8
10
|
//#region src/server/host.d.ts
|
|
9
11
|
/**
|
|
10
12
|
* Registers the host's ports for installers that read them later (the Nuxt module's server plugin installs on the first
|
|
11
13
|
* request, after every Nitro plugin has run). `options.schema`: the collaboration schema, see `CollabServerOptions.schema`.
|
|
14
|
+
* `options.blockTypes`: the host's Block types, which the AI chat describes to the model instead of the client's `blockTypes` (`EditorServerOptions.blockTypes`).
|
|
12
15
|
*/
|
|
13
16
|
export declare function setEditorPorts(next: EditorPorts, options?: {
|
|
14
17
|
schema?: () => AnyExtension[];
|
|
18
|
+
blockTypes?: readonly BlockType[];
|
|
15
19
|
}): void;
|
|
16
20
|
export declare function useEditorPorts(): EditorPorts;
|
|
17
21
|
/** The process-wide collaboration server of the installed server. Throws when none is installed or `collab: false`. */
|
package/dist/server/host.js
CHANGED
|
@@ -3,7 +3,8 @@ import { enforceRateLimit } from "./handlers/context.js";
|
|
|
3
3
|
//#region src/server/host.ts
|
|
4
4
|
let ports;
|
|
5
5
|
let schema;
|
|
6
|
-
|
|
6
|
+
let blockTypes;
|
|
7
|
+
/** Handle of the live installation; `adoptEditorServer` sets it, its release clears it. */
|
|
7
8
|
let installation;
|
|
8
9
|
let collab;
|
|
9
10
|
let models;
|
|
@@ -14,10 +15,12 @@ function installed() {
|
|
|
14
15
|
/**
|
|
15
16
|
* Registers the host's ports for installers that read them later (the Nuxt module's server plugin installs on the first
|
|
16
17
|
* request, after every Nitro plugin has run). `options.schema`: the collaboration schema, see `CollabServerOptions.schema`.
|
|
18
|
+
* `options.blockTypes`: the host's Block types, which the AI chat describes to the model instead of the client's `blockTypes` (`EditorServerOptions.blockTypes`).
|
|
17
19
|
*/
|
|
18
20
|
function setEditorPorts(next, options = {}) {
|
|
19
21
|
ports = next;
|
|
20
22
|
schema = options.schema;
|
|
23
|
+
blockTypes = options.blockTypes;
|
|
21
24
|
}
|
|
22
25
|
function useEditorPorts() {
|
|
23
26
|
if (!ports) throw new Error("@ecsplico/editor: setEditorPorts() was not called");
|
|
@@ -27,6 +30,10 @@ function useEditorPorts() {
|
|
|
27
30
|
function useEditorSchema() {
|
|
28
31
|
return schema;
|
|
29
32
|
}
|
|
33
|
+
/** The Block types registered with `setEditorPorts`, for installers that build the AI handlers themselves (`EditorServerOptions.blockTypes`). */
|
|
34
|
+
function useEditorBlockTypes() {
|
|
35
|
+
return blockTypes;
|
|
36
|
+
}
|
|
30
37
|
/**
|
|
31
38
|
* Makes the server `installEditorServer` built the process-wide one, so host code (`useCollabServer`, `useAiModels`,
|
|
32
39
|
* `liveContent`, `enforceAiRateLimit`, …) reaches the same instances the routes use. Returns the function that drops them
|
|
@@ -37,6 +44,7 @@ function adoptEditorServer(server) {
|
|
|
37
44
|
installation = own;
|
|
38
45
|
ports = server.ports;
|
|
39
46
|
schema = server.schema;
|
|
47
|
+
blockTypes = server.blockTypes;
|
|
40
48
|
collab = server.collab;
|
|
41
49
|
models = server.models;
|
|
42
50
|
limiter = server.limiter;
|
|
@@ -117,6 +125,6 @@ async function enforceAiRateLimit(request) {
|
|
|
117
125
|
}
|
|
118
126
|
}
|
|
119
127
|
//#endregion
|
|
120
|
-
export { adoptEditorServer, closeEditorServer, collabContentChanged, collabStats, enforceAiRateLimit, forgetCollabDocument, listAiModels, liveContent, resolveModel, setEditorPorts, useAiModels, useCollabServer, useEditorPorts, useEditorSchema, useRateLimiter };
|
|
128
|
+
export { adoptEditorServer, closeEditorServer, collabContentChanged, collabStats, enforceAiRateLimit, forgetCollabDocument, listAiModels, liveContent, resolveModel, setEditorPorts, useAiModels, useCollabServer, useEditorBlockTypes, useEditorPorts, useEditorSchema, useRateLimiter };
|
|
121
129
|
|
|
122
130
|
//# sourceMappingURL=host.js.map
|
package/dist/server/host.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.js","names":[],"sources":["../../src/server/host.ts"],"sourcesContent":["import type { AnyExtension, JSONContent } from '@tiptap/core'\nimport type { LanguageModel } from 'ai'\nimport type { ModelProfile, ModelRef } from '../features/ai/protocol'\nimport type { AiModels } from './ai'\nimport type { CollabServer } from './collab'\nimport { HttpError } from './errors'\nimport { enforceRateLimit } from './handlers/context'\nimport type { EditorPorts } from './ports'\nimport type { RateLimiter } from './rate-limit'\n\n// The process-wide editor server of hosts that wire the editor once at startup (the Nuxt module, a script with several\n// modules): the ports, collaboration server, AI models and rate limiter `installEditorServer` built, reachable from host\n// code (`useCollabServer`, `resolveModel`, `liveContent`, …). Their configuration is the installer's options (see\n// `defineEditorConfig` for the defaults); instances for tests or several tenants come from `createCollabServer` /\n// `createAiModels` / `createRateLimiter`, which take their configuration explicitly.\n\nlet ports: EditorPorts | undefined\nlet schema: (() => AnyExtension[]) | undefined\n/**
|
|
1
|
+
{"version":3,"file":"host.js","names":[],"sources":["../../src/server/host.ts"],"sourcesContent":["import type { AnyExtension, JSONContent } from '@tiptap/core'\nimport type { LanguageModel } from 'ai'\nimport type { BlockType } from '../features/composites/schema'\nimport type { ModelProfile, ModelRef } from '../features/ai/protocol'\nimport type { AiModels } from './ai'\nimport type { CollabServer } from './collab'\nimport { HttpError } from './errors'\nimport { enforceRateLimit } from './handlers/context'\nimport type { EditorPorts } from './ports'\nimport type { RateLimiter } from './rate-limit'\n\n// The process-wide editor server of hosts that wire the editor once at startup (the Nuxt module, a script with several\n// modules): the ports, collaboration server, AI models and rate limiter `installEditorServer` built, reachable from host\n// code (`useCollabServer`, `resolveModel`, `liveContent`, …). Their configuration is the installer's options (see\n// `defineEditorConfig` for the defaults); instances for tests or several tenants come from `createCollabServer` /\n// `createAiModels` / `createRateLimiter`, which take their configuration explicitly.\n\nlet ports: EditorPorts | undefined\nlet schema: (() => AnyExtension[]) | undefined\nlet blockTypes: readonly BlockType[] | undefined\n/** Handle of the live installation; `adoptEditorServer` sets it, its release clears it. */\nlet installation: object | undefined\nlet collab: CollabServer | undefined\nlet models: AiModels | undefined\nlet limiter: RateLimiter | undefined\n\nfunction installed() {\n if (!installation) throw new Error('@ecsplico/editor: server not installed')\n}\n\n/**\n * Registers the host's ports for installers that read them later (the Nuxt module's server plugin installs on the first\n * request, after every Nitro plugin has run). `options.schema`: the collaboration schema, see `CollabServerOptions.schema`.\n * `options.blockTypes`: the host's Block types, which the AI chat describes to the model instead of the client's `blockTypes` (`EditorServerOptions.blockTypes`).\n */\nexport function setEditorPorts(next: EditorPorts, options: { schema?: () => AnyExtension[], blockTypes?: readonly BlockType[] } = {}) {\n ports = next\n schema = options.schema\n blockTypes = options.blockTypes\n}\n\nexport function useEditorPorts(): EditorPorts {\n if (!ports) throw new Error('@ecsplico/editor: setEditorPorts() was not called')\n return ports\n}\n\n/** The collaboration schema registered with `setEditorPorts`, for installers that build the collaboration server themselves. */\nexport function useEditorSchema(): (() => AnyExtension[]) | undefined {\n return schema\n}\n\n/** The Block types registered with `setEditorPorts`, for installers that build the AI handlers themselves (`EditorServerOptions.blockTypes`). */\nexport function useEditorBlockTypes(): readonly BlockType[] | undefined {\n return blockTypes\n}\n\n/**\n * Makes the server `installEditorServer` built the process-wide one, so host code (`useCollabServer`, `useAiModels`,\n * `liveContent`, `enforceAiRateLimit`, …) reaches the same instances the routes use. Returns the function that drops them\n * again (only when no later installation replaced them); the installer calls it when its host closes.\n */\nexport function adoptEditorServer(server: { ports: EditorPorts, schema?: () => AnyExtension[], blockTypes?: readonly BlockType[], collab?: CollabServer, models?: AiModels, limiter?: RateLimiter }): () => void {\n const own = {}\n installation = own\n ports = server.ports\n schema = server.schema\n blockTypes = server.blockTypes\n collab = server.collab\n models = server.models\n limiter = server.limiter\n return () => {\n if (installation !== own) return\n installation = collab = models = limiter = undefined\n }\n}\n\n/** The process-wide collaboration server of the installed server. Throws when none is installed or `collab: false`. */\nexport function useCollabServer(): CollabServer {\n installed()\n if (!collab) throw new Error('@ecsplico/editor: collaboration is off (installEditorServer with collab: false)')\n return collab\n}\n\n/** The process-wide AI models of the installed server: its configured presets, with the `models` port's `languageModel`/`listModels` hooks. Throws when none is installed or it was installed without `ai`. */\nexport function useAiModels(): AiModels {\n installed()\n if (!models) throw new Error('@ecsplico/editor: AI is off (installEditorServer without ai)')\n return models\n}\n\n/** The process-wide AI rate limiter of the installed server (present with `ai`). */\nexport function useRateLimiter(): RateLimiter {\n installed()\n if (!limiter) throw new Error('@ecsplico/editor: AI is off (installEditorServer without ai)')\n return limiter\n}\n\n/** Closes the process-wide collaboration server (pending stores are written first) and forgets it. */\nexport async function closeEditorServer() {\n const open = collab\n collab = undefined\n await open?.close()\n}\n\n/** Open collaboration connections and loaded documents of this process (zeros when collaboration is off). */\nexport function collabStats(): { connections: number, documents: number } {\n installed()\n return collab?.stats() ?? { connections: 0, documents: 0 }\n}\n\n/** Tell connected clients the document is gone, disconnect them and drop the Yjs state. Call before removing the JSON. */\nexport function forgetCollabDocument(id: string): Promise<void> {\n return useCollabServer().forgetDocument(id)\n}\n\n/** Current content of a document that is open in the collaboration server (unsaved edits included), `null` when nobody has it open. */\nexport function liveContent(id: string): JSONContent | null {\n installed()\n return collab?.liveContent(id) ?? null\n}\n\n/** The host rewrote a document's content (an agent wrote the file): merge it into the open document now, see `CollabServer.contentChanged`. */\nexport function collabContentChanged(id: string): Promise<void> {\n installed()\n return collab?.contentChanged(id) ?? Promise.resolve()\n}\n\n/** AI SDK language model for `ref`, see `AiModels.resolveModel`. */\nexport function resolveModel(ref: ModelRef, user: object | null): Promise<LanguageModel> {\n return useAiModels().resolveModel(ref, user)\n}\n\n/** The models listed for `user`, see `AiModels.listModels`. */\nexport function listAiModels(user: object | null): Promise<ModelProfile[]> {\n return useAiModels().listModels(user)\n}\n\n/**\n * Per-user limit for host-defined AI routes: the same limiter as the editor's own AI endpoints. Call first in the route.\n * Throws 401 (`HttpError`) without a user, 429 when exceeded; returns the user the identity port authenticated, for `resolveModel`.\n * With a Node-backed request (`event.node.res`, as h3 events have) the 429's `Retry-After` header is set on the response too.\n */\nexport async function enforceAiRateLimit(request: { headers: Headers, node?: { res: { setHeader(name: string, value: string): unknown } } }): Promise<object> {\n try {\n return await enforceRateLimit({ ports: useEditorPorts(), ai: useAiModels(), limiter: useRateLimiter() }, { headers: request.headers })\n }\n catch (error) {\n if (error instanceof HttpError) for (const [name, value] of Object.entries(error.headers)) request.node?.res.setHeader(name, value)\n throw error\n }\n}\n"],"mappings":";;;AAiBA,IAAI;AACJ,IAAI;AACJ,IAAI;;AAEJ,IAAI;AACJ,IAAI;AACJ,IAAI;AACJ,IAAI;AAEJ,SAAS,YAAY;CACnB,IAAI,CAAC,cAAc,MAAM,IAAI,MAAM,wCAAwC;AAC7E;;;;;;AAOA,SAAgB,eAAe,MAAmB,UAAgF,CAAC,GAAG;CACpI,QAAQ;CACR,SAAS,QAAQ;CACjB,aAAa,QAAQ;AACvB;AAEA,SAAgB,iBAA8B;CAC5C,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,mDAAmD;CAC/E,OAAO;AACT;;AAGA,SAAgB,kBAAsD;CACpE,OAAO;AACT;;AAGA,SAAgB,sBAAwD;CACtE,OAAO;AACT;;;;;;AAOA,SAAgB,kBAAkB,QAA+K;CAC/M,MAAM,MAAM,CAAC;CACb,eAAe;CACf,QAAQ,OAAO;CACf,SAAS,OAAO;CAChB,aAAa,OAAO;CACpB,SAAS,OAAO;CAChB,SAAS,OAAO;CAChB,UAAU,OAAO;CACjB,aAAa;EACX,IAAI,iBAAiB,KAAK;EAC1B,eAAe,SAAS,SAAS,UAAU,KAAA;CAC7C;AACF;;AAGA,SAAgB,kBAAgC;CAC9C,UAAU;CACV,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,iFAAiF;CAC9G,OAAO;AACT;;AAGA,SAAgB,cAAwB;CACtC,UAAU;CACV,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,8DAA8D;CAC3F,OAAO;AACT;;AAGA,SAAgB,iBAA8B;CAC5C,UAAU;CACV,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,8DAA8D;CAC5F,OAAO;AACT;;AAGA,eAAsB,oBAAoB;CACxC,MAAM,OAAO;CACb,SAAS,KAAA;CACT,MAAM,MAAM,MAAM;AACpB;;AAGA,SAAgB,cAA0D;CACxE,UAAU;CACV,OAAO,QAAQ,MAAM,KAAK;EAAE,aAAa;EAAG,WAAW;CAAE;AAC3D;;AAGA,SAAgB,qBAAqB,IAA2B;CAC9D,OAAO,gBAAgB,CAAC,CAAC,eAAe,EAAE;AAC5C;;AAGA,SAAgB,YAAY,IAAgC;CAC1D,UAAU;CACV,OAAO,QAAQ,YAAY,EAAE,KAAK;AACpC;;AAGA,SAAgB,qBAAqB,IAA2B;CAC9D,UAAU;CACV,OAAO,QAAQ,eAAe,EAAE,KAAK,QAAQ,QAAQ;AACvD;;AAGA,SAAgB,aAAa,KAAe,MAA6C;CACvF,OAAO,YAAY,CAAC,CAAC,aAAa,KAAK,IAAI;AAC7C;;AAGA,SAAgB,aAAa,MAA8C;CACzE,OAAO,YAAY,CAAC,CAAC,WAAW,IAAI;AACtC;;;;;;AAOA,eAAsB,mBAAmB,SAAqH;CAC5J,IAAI;EACF,OAAO,MAAM,iBAAiB;GAAE,OAAO,eAAe;GAAG,IAAI,YAAY;GAAG,SAAS,eAAe;EAAE,GAAG,EAAE,SAAS,QAAQ,QAAQ,CAAC;CACvI,SACO,OAAO;EACZ,IAAI,iBAAiB,WAAW,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,MAAM,OAAO,GAAG,QAAQ,MAAM,IAAI,UAAU,MAAM,KAAK;EAClI,MAAM;CACR;AACF"}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ModelPresetConfig, TranscriptionConfig } from "../config/index.js";
|
|
2
2
|
import { AuthenticateRequest } from "./auth.js";
|
|
3
3
|
import { ChatMessage } from "../features/ai/protocol/generate.js";
|
|
4
4
|
import { HttpError } from "./errors.js";
|
|
5
5
|
import { AiModels, AiModelsConfig, ChatTurnPart, createAiModels, streamChat, streamChatTurn, upstreamError } from "./ai.js";
|
|
6
|
-
import { AssetStore, CollabStateStore, Directory, DocumentAdmin,
|
|
6
|
+
import { AccessControl, AssetStore, CollabStateStore, Directory, DocumentAdmin, DocumentContentStore, EditorPorts, ImageProvider, ModelProvider, PORT_CAPABILITY, SettingsStore, ThemeStore, serverCapabilities } from "./ports.js";
|
|
7
7
|
import { COLLAB_FIELD, CollabConnection, CollabServer, CollabServerOptions, WebSocketLike, createCollabServer } from "./collab.js";
|
|
8
8
|
import { decodeCollabState, encodeCollabState, isStateStale } from "./collab-state.js";
|
|
9
9
|
import { RateLimiter, RateLimiterOptions, createRateLimiter, userKey } from "./rate-limit.js";
|
|
10
10
|
import { closeEditorServer, collabContentChanged, collabStats, enforceAiRateLimit, forgetCollabDocument, listAiModels, liveContent, resolveModel, setEditorPorts, useAiModels, useCollabServer, useEditorPorts } from "./host.js";
|
|
11
|
-
export { type AiModels, type AiModelsConfig, type AssetStore, type AuthenticateRequest, COLLAB_FIELD, type ChatMessage, type ChatTurnPart, type CollabConnection, type CollabServer, type CollabServerOptions, type CollabStateStore, type Directory, type DocumentAdmin, type
|
|
11
|
+
export { type AccessControl, type AiModels, type AiModelsConfig, type AssetStore, type AuthenticateRequest, COLLAB_FIELD, type ChatMessage, type ChatTurnPart, type CollabConnection, type CollabServer, type CollabServerOptions, type CollabStateStore, type Directory, type DocumentAdmin, type DocumentContentStore, type EditorPorts, HttpError, type ImageProvider, type ModelPresetConfig, type ModelProvider, PORT_CAPABILITY, type RateLimiter, type RateLimiterOptions, type SettingsStore, type ThemeStore, type TranscriptionConfig, type WebSocketLike, closeEditorServer, collabContentChanged, collabStats, createAiModels, createCollabServer, createRateLimiter, decodeCollabState, encodeCollabState, enforceAiRateLimit, forgetCollabDocument, isStateStale, listAiModels, liveContent, resolveModel, serverCapabilities, setEditorPorts, streamChat, streamChatTurn, upstreamError, useAiModels, useCollabServer, useEditorPorts, userKey };
|
package/dist/server/ports.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { JSONContent } from "@tiptap/core";
|
|
|
12
12
|
import { LanguageModel } from "ai";
|
|
13
13
|
//#region src/server/ports.d.ts
|
|
14
14
|
/** Who a request or websocket connection belongs to, and which documents that user may open. */
|
|
15
|
-
export interface
|
|
15
|
+
export interface AccessControl {
|
|
16
16
|
/**
|
|
17
17
|
* The user behind a request, `null` = not authenticated (HTTP routes answer 401). Collaboration: called in Hocuspocus
|
|
18
18
|
* `onAuthenticate` with the upgrade request's `headers` and the client's `token` (`undefined` when it sent none, so cookie
|
|
@@ -30,8 +30,8 @@ export interface Identity {
|
|
|
30
30
|
readOnly: boolean;
|
|
31
31
|
} | null>;
|
|
32
32
|
}
|
|
33
|
-
/** The stored TipTap JSON of the workspace's documents. */
|
|
34
|
-
export interface
|
|
33
|
+
/** The stored TipTap JSON of the workspace's documents (content only; the structure around it is the tree store's). */
|
|
34
|
+
export interface DocumentContentStore {
|
|
35
35
|
/** TipTap JSON to seed a document's Y.Doc from; `null` = the document does not exist. */
|
|
36
36
|
loadContent(id: string): Promise<JSONContent | null>;
|
|
37
37
|
/** Mirror of the current Yjs state, called debounced after edits and on shutdown. Must tolerate ids deleted meanwhile. */
|
|
@@ -51,7 +51,7 @@ export interface DocumentStore {
|
|
|
51
51
|
content: JSONContent;
|
|
52
52
|
}[]>;
|
|
53
53
|
}
|
|
54
|
-
/** Document management a host needs next to the editor's own storage methods: `
|
|
54
|
+
/** Document management a host needs next to the editor's own storage methods: `DocumentContentStore.storeContent` only updates. */
|
|
55
55
|
export interface DocumentAdmin {
|
|
56
56
|
/** Creates the document or replaces its content; the content version changes. */
|
|
57
57
|
putDocument(id: string, content: JSONContent): Promise<void>;
|
|
@@ -122,9 +122,9 @@ export interface ModelProvider {
|
|
|
122
122
|
*/
|
|
123
123
|
listModels?(user: object | null): Promise<ModelProfile[] | null>;
|
|
124
124
|
}
|
|
125
|
-
/** The host's image generation for the Bild-Studio: its managed
|
|
125
|
+
/** The host's image generation for the Bild-Studio: its managed prompt blocks, the generator and the storage of uploaded references. Without this port the image routes answer 501. */
|
|
126
126
|
export interface ImageProvider {
|
|
127
|
-
/** The
|
|
127
|
+
/** The prompt blocks the host manages, in the order it lists them (`GET /api/editor/images/prompt-blocks`). `user` is what `authenticate` returned. */
|
|
128
128
|
promptBlocks(user: object): Promise<ImageStudioPromptBlock[]>;
|
|
129
129
|
/** Generates one image and stores it (`POST /api/editor/images/generate`); resolves with the stored image (`active: false`). The host builds the prompt from `body`. */
|
|
130
130
|
generate(body: ImageStudioGenerateBody, user: object): Promise<ImageStudioImage>;
|
|
@@ -133,8 +133,8 @@ export interface ImageProvider {
|
|
|
133
133
|
}
|
|
134
134
|
/** Everything the editor's server reads from the host, one port per capability. Only `identity` and `documents` are required. */
|
|
135
135
|
export interface EditorPorts {
|
|
136
|
-
identity:
|
|
137
|
-
documents:
|
|
136
|
+
identity: AccessControl;
|
|
137
|
+
documents: DocumentContentStore & Partial<DocumentAdmin>;
|
|
138
138
|
/** Required by the collaboration server. */
|
|
139
139
|
collabState?: CollabStateStore;
|
|
140
140
|
directory?: Directory;
|
package/dist/server/ports.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ports.js","names":[],"sources":["../../src/server/ports.ts"],"sourcesContent":["import type { JSONContent } from '@tiptap/core'\nimport type { LanguageModel } from 'ai'\nimport type { CommentStore } from '../adapters/comments/types'\nimport type { BackendCapability } from '../core/backend-capability'\nimport type { ImageStudioGenerateBody, ImageStudioImage, ImageStudioPromptBlock } from '../core/backend-types'\nimport type { ModelProfile, ModelRef } from '../features/ai/protocol'\nimport type { AssetRecord } from '../features/media/schema'\nimport type { MentionUser } from '../features/mentions/schema'\nimport type { ThemeRecord } from '../features/themes/schema'\nimport type { EditorSettings } from '../settings'\nimport type { AuthenticateRequest } from './auth'\n\n// The server side of the editor's backend, split by capability: a host (or an adapter) implements the ports it serves\n// and leaves the others out; routes and features that need an absent port answer 501.\n\n/** Who a request or websocket connection belongs to, and which documents that user may open. */\nexport interface Identity {\n /**\n * The user behind a request, `null` = not authenticated (HTTP routes answer 401). Collaboration: called in Hocuspocus\n * `onAuthenticate` with the upgrade request's `headers` and the client's `token` (`undefined` when it sent none, so cookie\n * hosts keep working and bearer-only hosts read `token`); `null` closes the connection with 4401. With the Nuxt module\n * option `collab.authenticateUpgrade` it runs once at the upgrade with the `headers` only and `null` refuses the upgrade\n * with HTTP 401. The value becomes the connection context's `user`.\n */\n authenticate(request: AuthenticateRequest): Promise<object | null>\n /**\n * Per-document access for the collaboration websocket, called in Hocuspocus `onAuthenticate` (right after `authenticate`) with the user.\n * `null` rejects the connection; `{ readOnly: true }` lets it sync but applies none of its edits. Omit it to let every\n * authenticated user edit every document.\n */\n authorize?(user: object, id: string): Promise<{ readOnly: boolean } | null>\n}\n\n/** The stored TipTap JSON of the workspace's documents. */\nexport interface DocumentStore {\n /** TipTap JSON to seed a document's Y.Doc from; `null` = the document does not exist. */\n loadContent(id: string): Promise<JSONContent | null>\n /** Mirror of the current Yjs state, called debounced after edits and on shutdown. Must tolerate ids deleted meanwhile. */\n storeContent(id: string, content: JSONContent): Promise<void>\n /**\n * Opaque version of the stored content of a document (mtime, hash, revision), `null` = unknown, no NUL. The collab server\n * keeps it next to the Yjs state. When it differs from the live version (the content changed behind the editor's back) the\n * state is discarded on load, removed through `removeState` and the document is re-seeded from `loadContent`. A document\n * that is open when its version moves is not written back: the next store merges the new `loadContent` into the open\n * Y.Doc. The version is read right after every `storeContent`, so it must reflect what the editor itself just wrote.\n * Omit it to never discard state.\n */\n contentVersion?(id: string): Promise<string | null>\n /** Stored TipTap JSON of every document in the workspace, e.g. to collect the `#` tags in use (`GET /api/editor/tags`). */\n listContents(): Promise<{ id: string, content: JSONContent }[]>\n}\n\n/** Document management a host needs next to the editor's own storage methods: `DocumentStore.storeContent` only updates. */\nexport interface DocumentAdmin {\n /** Creates the document or replaces its content; the content version changes. */\n putDocument(id: string, content: JSONContent): Promise<void>\n /** Removes the content, the Yjs state and the assets of a document; unknown ids are fine. Call `forgetCollabDocument(id)` first when it may be open. */\n deleteDocument(id: string): Promise<void>\n /** Ids of all stored documents. */\n listDocumentIds(): Promise<string[]>\n}\n\n/** Where the collaboration server keeps the Yjs state of open documents. */\nexport interface CollabStateStore {\n /** The bytes `storeState` stored (opaque to the host: the Yjs update behind a content-version prefix; state stored by earlier releases is a raw update and still loads); `null` = none yet (the Y.Doc is then seeded from `loadContent`). */\n loadState(id: string): Promise<Uint8Array | null>\n /** Persists the state bytes unchanged; called on every store and when block IDs were backfilled. Must tolerate ids deleted meanwhile. */\n storeState(id: string, state: Uint8Array): Promise<void>\n /** Drops the Yjs state of a deleted document (the next load re-seeds from `loadContent`). */\n removeState(id: string): Promise<void>\n}\n\n/** The users that can be `@`-mentioned. */\nexport interface Directory {\n /** All users that can be `@`-mentioned (served by `GET /api/editor/users`); must not include credentials. */\n listUsers(): Promise<MentionUser[]>\n}\n\n/** Uploaded files. */\nexport interface AssetStore {\n /** Stores an uploaded file (`GET /api/editor/assets/:id/:name` serves it). `id` is a document id or `SCRATCH_DOCUMENT_ID`; it may name no existing document. */\n storeAsset(id: string, asset: AssetRecord & { data: Buffer }): Promise<void>\n /** A stored file with its record; `null` = unknown id or name. */\n loadAsset(id: string, name: string): Promise<{ record: AssetRecord, data: Buffer } | null>\n}\n\n/** Per-user editor settings. Without this port the client keeps settings in localStorage only. */\nexport interface SettingsStore {\n /** Per-user editor settings as stored (`null` = none yet), validated by `GET /api/editor/settings`. */\n loadSettings(user: object): Promise<unknown | null>\n /** Stores validated settings for the user from `authenticate`. */\n storeSettings(user: object, settings: EditorSettings): Promise<void>\n}\n\n/** The theme library shared by all users. Without this port the themes feature is uninstalled: its routes answer 501. */\nexport interface ThemeStore {\n /** All themes (`GET /api/editor/themes`). */\n listThemes(): Promise<ThemeRecord[]>\n /** A theme's record and `theme.css` (`GET /api/editor/themes/:id`); `null` = unknown id. */\n loadTheme(id: string): Promise<{ record: ThemeRecord, css: string } | null>\n /** Stores an uploaded theme (`POST /api/editor/themes`). */\n storeTheme(record: Omit<ThemeRecord, 'builtin'>, css: string): Promise<void>\n /** Deletes an uploaded theme (`DELETE /api/editor/themes/:id`); `false` = not found or builtin. */\n removeTheme(id: string): Promise<boolean>\n}\n\n/** The host's own language models for the AI endpoints; both members fall through to the runtime-config presets. */\nexport interface ModelProvider {\n /**\n * The language model for `ref`, so the AI endpoints use the host's own providers; consulted before the runtime-config\n * presets for every model reference. `user` is what `authenticate` returned. `null` falls through to the presets\n * (and answers 400 for a preset that is not configured either).\n */\n languageModel?(ref: ModelRef, user: object | null): Promise<LanguageModel | null>\n /**\n * The models `GET /api/ai/models` lists for `user`, instead of the runtime-config presets (list them with\n * `source: 'preset'` and the `id` that `languageModel` receives as `{ preset: id }`). `null` (or omitting the method)\n * lists the presets.\n */\n listModels?(user: object | null): Promise<ModelProfile[] | null>\n}\n\n/** The host's image generation for the Bild-Studio: its managed Prompt-Bausteine, the generator and the storage of uploaded references. Without this port the image routes answer 501. */\nexport interface ImageProvider {\n /** The Prompt-Bausteine the host manages, in the order it lists them (`GET /api/editor/images/prompt-blocks`). `user` is what `authenticate` returned. */\n promptBlocks(user: object): Promise<ImageStudioPromptBlock[]>\n /** Generates one image and stores it (`POST /api/editor/images/generate`); resolves with the stored image (`active: false`). The host builds the prompt from `body`. */\n generate(body: ImageStudioGenerateBody, user: object): Promise<ImageStudioImage>\n /** Stores an uploaded reference image of document `documentId` (`POST /api/editor/images/upload`); resolves with the stored image (`active: true`). */\n upload(documentId: string, file: File, user: object): Promise<ImageStudioImage>\n}\n\n/** Everything the editor's server reads from the host, one port per capability. Only `identity` and `documents` are required. */\nexport interface EditorPorts {\n identity: Identity\n documents: DocumentStore & Partial<DocumentAdmin>\n /** Required by the collaboration server. */\n collabState?: CollabStateStore\n directory?: Directory\n assets?: AssetStore\n settings?: SettingsStore\n themes?: ThemeStore\n models?: ModelProvider\n images?: ImageProvider\n /** Where the comment threads live (`adapters/comments/{json,drizzle}`). */\n comments?: CommentStore\n}\n\n/** The capability each optional port provides; `identity` is no capability, `tags` derive from `documents` (see `serverCapabilities`). */\nexport const PORT_CAPABILITY: Record<Exclude<keyof EditorPorts, 'identity'>, BackendCapability> = {\n documents: 'documents',\n collabState: 'collaboration',\n directory: 'users',\n assets: 'assets',\n settings: 'settings',\n themes: 'themes',\n models: 'ai',\n comments: 'comments',\n images: 'images',\n}\n\n/**\n * The capabilities `ports` serves: those of every present port, plus `tags` (derived from `documents.listContents`).\n * Two capabilities depend on more than a port: `ai` (the AI routes need configured models) and `collaboration` (the\n * websocket needs the collab server). `served` states what the installed server really mounts and overrides the port-based\n * answer for them; omitted keys follow the ports.\n */\nexport function serverCapabilities(ports: EditorPorts, served: { ai?: boolean, collaboration?: boolean } = {}): BackendCapability[] {\n const capabilities = new Set<BackendCapability>(\n (Object.keys(PORT_CAPABILITY) as (keyof typeof PORT_CAPABILITY)[])\n .filter(port => ports[port] !== undefined)\n .map(port => PORT_CAPABILITY[port]),\n )\n for (const capability of ['ai', 'collaboration'] as const) {\n if (served[capability] === true) capabilities.add(capability)\n else if (served[capability] === false) capabilities.delete(capability)\n }\n if (ports.documents) capabilities.add('tags')\n return [...capabilities]\n}\n"],"mappings":";;AAqJA,MAAa,kBAAqF;CAChG,WAAW;CACX,aAAa;CACb,WAAW;CACX,QAAQ;CACR,UAAU;CACV,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,QAAQ;AACV;;;;;;;AAQA,SAAgB,mBAAmB,OAAoB,SAAoD,CAAC,GAAwB;CAClI,MAAM,eAAe,IAAI,IACtB,OAAO,KAAK,eAAe,CAAC,CAC1B,QAAO,SAAQ,MAAM,UAAU,KAAA,CAAS,CAAC,CACzC,KAAI,SAAQ,gBAAgB,KAAK,CACtC;CACA,KAAK,MAAM,cAAc,CAAC,MAAM,eAAe,GAC7C,IAAI,OAAO,gBAAgB,MAAM,aAAa,IAAI,UAAU;MACvD,IAAI,OAAO,gBAAgB,OAAO,aAAa,OAAO,UAAU;CAEvE,IAAI,MAAM,WAAW,aAAa,IAAI,MAAM;CAC5C,OAAO,CAAC,GAAG,YAAY;AACzB"}
|
|
1
|
+
{"version":3,"file":"ports.js","names":[],"sources":["../../src/server/ports.ts"],"sourcesContent":["import type { JSONContent } from '@tiptap/core'\nimport type { LanguageModel } from 'ai'\nimport type { CommentStore } from '../adapters/comments/types'\nimport type { BackendCapability } from '../core/backend-capability'\nimport type { ImageStudioGenerateBody, ImageStudioImage, ImageStudioPromptBlock } from '../core/backend-types'\nimport type { ModelProfile, ModelRef } from '../features/ai/protocol'\nimport type { AssetRecord } from '../features/media/schema'\nimport type { MentionUser } from '../features/mentions/schema'\nimport type { ThemeRecord } from '../features/themes/schema'\nimport type { EditorSettings } from '../settings'\nimport type { AuthenticateRequest } from './auth'\n\n// The server side of the editor's backend, split by capability: a host (or an adapter) implements the ports it serves\n// and leaves the others out; routes and features that need an absent port answer 501.\n\n/** Who a request or websocket connection belongs to, and which documents that user may open. */\nexport interface AccessControl {\n /**\n * The user behind a request, `null` = not authenticated (HTTP routes answer 401). Collaboration: called in Hocuspocus\n * `onAuthenticate` with the upgrade request's `headers` and the client's `token` (`undefined` when it sent none, so cookie\n * hosts keep working and bearer-only hosts read `token`); `null` closes the connection with 4401. With the Nuxt module\n * option `collab.authenticateUpgrade` it runs once at the upgrade with the `headers` only and `null` refuses the upgrade\n * with HTTP 401. The value becomes the connection context's `user`.\n */\n authenticate(request: AuthenticateRequest): Promise<object | null>\n /**\n * Per-document access for the collaboration websocket, called in Hocuspocus `onAuthenticate` (right after `authenticate`) with the user.\n * `null` rejects the connection; `{ readOnly: true }` lets it sync but applies none of its edits. Omit it to let every\n * authenticated user edit every document.\n */\n authorize?(user: object, id: string): Promise<{ readOnly: boolean } | null>\n}\n\n/** The stored TipTap JSON of the workspace's documents (content only; the structure around it is the tree store's). */\nexport interface DocumentContentStore {\n /** TipTap JSON to seed a document's Y.Doc from; `null` = the document does not exist. */\n loadContent(id: string): Promise<JSONContent | null>\n /** Mirror of the current Yjs state, called debounced after edits and on shutdown. Must tolerate ids deleted meanwhile. */\n storeContent(id: string, content: JSONContent): Promise<void>\n /**\n * Opaque version of the stored content of a document (mtime, hash, revision), `null` = unknown, no NUL. The collab server\n * keeps it next to the Yjs state. When it differs from the live version (the content changed behind the editor's back) the\n * state is discarded on load, removed through `removeState` and the document is re-seeded from `loadContent`. A document\n * that is open when its version moves is not written back: the next store merges the new `loadContent` into the open\n * Y.Doc. The version is read right after every `storeContent`, so it must reflect what the editor itself just wrote.\n * Omit it to never discard state.\n */\n contentVersion?(id: string): Promise<string | null>\n /** Stored TipTap JSON of every document in the workspace, e.g. to collect the `#` tags in use (`GET /api/editor/tags`). */\n listContents(): Promise<{ id: string, content: JSONContent }[]>\n}\n\n/** Document management a host needs next to the editor's own storage methods: `DocumentContentStore.storeContent` only updates. */\nexport interface DocumentAdmin {\n /** Creates the document or replaces its content; the content version changes. */\n putDocument(id: string, content: JSONContent): Promise<void>\n /** Removes the content, the Yjs state and the assets of a document; unknown ids are fine. Call `forgetCollabDocument(id)` first when it may be open. */\n deleteDocument(id: string): Promise<void>\n /** Ids of all stored documents. */\n listDocumentIds(): Promise<string[]>\n}\n\n/** Where the collaboration server keeps the Yjs state of open documents. */\nexport interface CollabStateStore {\n /** The bytes `storeState` stored (opaque to the host: the Yjs update behind a content-version prefix; state stored by earlier releases is a raw update and still loads); `null` = none yet (the Y.Doc is then seeded from `loadContent`). */\n loadState(id: string): Promise<Uint8Array | null>\n /** Persists the state bytes unchanged; called on every store and when block IDs were backfilled. Must tolerate ids deleted meanwhile. */\n storeState(id: string, state: Uint8Array): Promise<void>\n /** Drops the Yjs state of a deleted document (the next load re-seeds from `loadContent`). */\n removeState(id: string): Promise<void>\n}\n\n/** The users that can be `@`-mentioned. */\nexport interface Directory {\n /** All users that can be `@`-mentioned (served by `GET /api/editor/users`); must not include credentials. */\n listUsers(): Promise<MentionUser[]>\n}\n\n/** Uploaded files. */\nexport interface AssetStore {\n /** Stores an uploaded file (`GET /api/editor/assets/:id/:name` serves it). `id` is a document id or `SCRATCH_DOCUMENT_ID`; it may name no existing document. */\n storeAsset(id: string, asset: AssetRecord & { data: Buffer }): Promise<void>\n /** A stored file with its record; `null` = unknown id or name. */\n loadAsset(id: string, name: string): Promise<{ record: AssetRecord, data: Buffer } | null>\n}\n\n/** Per-user editor settings. Without this port the client keeps settings in localStorage only. */\nexport interface SettingsStore {\n /** Per-user editor settings as stored (`null` = none yet), validated by `GET /api/editor/settings`. */\n loadSettings(user: object): Promise<unknown | null>\n /** Stores validated settings for the user from `authenticate`. */\n storeSettings(user: object, settings: EditorSettings): Promise<void>\n}\n\n/** The theme library shared by all users. Without this port the themes feature is uninstalled: its routes answer 501. */\nexport interface ThemeStore {\n /** All themes (`GET /api/editor/themes`). */\n listThemes(): Promise<ThemeRecord[]>\n /** A theme's record and `theme.css` (`GET /api/editor/themes/:id`); `null` = unknown id. */\n loadTheme(id: string): Promise<{ record: ThemeRecord, css: string } | null>\n /** Stores an uploaded theme (`POST /api/editor/themes`). */\n storeTheme(record: Omit<ThemeRecord, 'builtin'>, css: string): Promise<void>\n /** Deletes an uploaded theme (`DELETE /api/editor/themes/:id`); `false` = not found or builtin. */\n removeTheme(id: string): Promise<boolean>\n}\n\n/** The host's own language models for the AI endpoints; both members fall through to the runtime-config presets. */\nexport interface ModelProvider {\n /**\n * The language model for `ref`, so the AI endpoints use the host's own providers; consulted before the runtime-config\n * presets for every model reference. `user` is what `authenticate` returned. `null` falls through to the presets\n * (and answers 400 for a preset that is not configured either).\n */\n languageModel?(ref: ModelRef, user: object | null): Promise<LanguageModel | null>\n /**\n * The models `GET /api/ai/models` lists for `user`, instead of the runtime-config presets (list them with\n * `source: 'preset'` and the `id` that `languageModel` receives as `{ preset: id }`). `null` (or omitting the method)\n * lists the presets.\n */\n listModels?(user: object | null): Promise<ModelProfile[] | null>\n}\n\n/** The host's image generation for the Bild-Studio: its managed prompt blocks, the generator and the storage of uploaded references. Without this port the image routes answer 501. */\nexport interface ImageProvider {\n /** The prompt blocks the host manages, in the order it lists them (`GET /api/editor/images/prompt-blocks`). `user` is what `authenticate` returned. */\n promptBlocks(user: object): Promise<ImageStudioPromptBlock[]>\n /** Generates one image and stores it (`POST /api/editor/images/generate`); resolves with the stored image (`active: false`). The host builds the prompt from `body`. */\n generate(body: ImageStudioGenerateBody, user: object): Promise<ImageStudioImage>\n /** Stores an uploaded reference image of document `documentId` (`POST /api/editor/images/upload`); resolves with the stored image (`active: true`). */\n upload(documentId: string, file: File, user: object): Promise<ImageStudioImage>\n}\n\n/** Everything the editor's server reads from the host, one port per capability. Only `identity` and `documents` are required. */\nexport interface EditorPorts {\n identity: AccessControl\n documents: DocumentContentStore & Partial<DocumentAdmin>\n /** Required by the collaboration server. */\n collabState?: CollabStateStore\n directory?: Directory\n assets?: AssetStore\n settings?: SettingsStore\n themes?: ThemeStore\n models?: ModelProvider\n images?: ImageProvider\n /** Where the comment threads live (`adapters/comments/{json,drizzle}`). */\n comments?: CommentStore\n}\n\n/** The capability each optional port provides; `identity` is no capability, `tags` derive from `documents` (see `serverCapabilities`). */\nexport const PORT_CAPABILITY: Record<Exclude<keyof EditorPorts, 'identity'>, BackendCapability> = {\n documents: 'documents',\n collabState: 'collaboration',\n directory: 'users',\n assets: 'assets',\n settings: 'settings',\n themes: 'themes',\n models: 'ai',\n comments: 'comments',\n images: 'images',\n}\n\n/**\n * The capabilities `ports` serves: those of every present port, plus `tags` (derived from `documents.listContents`).\n * Two capabilities depend on more than a port: `ai` (the AI routes need configured models) and `collaboration` (the\n * websocket needs the collab server). `served` states what the installed server really mounts and overrides the port-based\n * answer for them; omitted keys follow the ports.\n */\nexport function serverCapabilities(ports: EditorPorts, served: { ai?: boolean, collaboration?: boolean } = {}): BackendCapability[] {\n const capabilities = new Set<BackendCapability>(\n (Object.keys(PORT_CAPABILITY) as (keyof typeof PORT_CAPABILITY)[])\n .filter(port => ports[port] !== undefined)\n .map(port => PORT_CAPABILITY[port]),\n )\n for (const capability of ['ai', 'collaboration'] as const) {\n if (served[capability] === true) capabilities.add(capability)\n else if (served[capability] === false) capabilities.delete(capability)\n }\n if (ports.documents) capabilities.add('tags')\n return [...capabilities]\n}\n"],"mappings":";;AAqJA,MAAa,kBAAqF;CAChG,WAAW;CACX,aAAa;CACb,WAAW;CACX,QAAQ;CACR,UAAU;CACV,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,QAAQ;AACV;;;;;;;AAQA,SAAgB,mBAAmB,OAAoB,SAAoD,CAAC,GAAwB;CAClI,MAAM,eAAe,IAAI,IACtB,OAAO,KAAK,eAAe,CAAC,CAC1B,QAAO,SAAQ,MAAM,UAAU,KAAA,CAAS,CAAC,CACzC,KAAI,SAAQ,gBAAgB,KAAK,CACtC;CACA,KAAK,MAAM,cAAc,CAAC,MAAM,eAAe,GAC7C,IAAI,OAAO,gBAAgB,MAAM,aAAa,IAAI,UAAU;MACvD,IAAI,OAAO,gBAAgB,OAAO,aAAa,OAAO,UAAU;CAEvE,IAAI,MAAM,WAAW,aAAa,IAAI,MAAM;CAC5C,OAAO,CAAC,GAAG,YAAY;AACzB"}
|
package/dist/settings/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { AI_LANGUAGES, AUTHOR_COLOR_STYLES, AiLanguage, AuthorColorStyle, COMMENTS_LAYOUTS, CommentsLayout, DRAG_HANDLE_VISIBILITIES, DragHandleVisibility, HEADING_CONVERSION_SCOPES, HeadingConversionScope,
|
|
1
|
+
import { AI_LANGUAGES, AUTHOR_COLOR_STYLES, AiLanguage, AuthorColorStyle, BLOCK_TYPE_ICON_VISIBILITIES, BlockTypeIconVisibility, COMMENTS_LAYOUTS, CommentsLayout, DRAG_HANDLE_VISIBILITIES, DragHandleVisibility, HEADING_CONVERSION_SCOPES, HeadingConversionScope, MODEL_ROLES, ModelRole, STRUCTURE_MODES, StructureMode, TREE_ROOTS, TreeRoot } from "./values.js";
|
|
2
|
+
import { EDITOR_SETTINGS_VERSION, isOutdatedEditorSettings, migrateEditorSettings } from "./migrate.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
//#region src/settings/index.d.ts
|
|
4
|
-
export declare const EDITOR_SETTINGS_VERSION = 1;
|
|
5
5
|
/** Valid theme id: lowercase slug, used as directory name and in `/api/editor/themes/:id`. Defined here so core never imports a feature. */
|
|
6
6
|
export declare const THEME_ID: RegExp;
|
|
7
7
|
/** A user's editor preferences, shared by all editors on the page; persisted per user through the `settings` port. */
|
|
@@ -20,23 +20,23 @@ export interface EditorSettings {
|
|
|
20
20
|
ai: {
|
|
21
21
|
/** Ghost-text suggestions while typing (`GhostAutocomplete`). */
|
|
22
22
|
autocomplete: boolean;
|
|
23
|
-
/** Model profile id per
|
|
24
|
-
|
|
23
|
+
/** Model profile id per model role; a missing or unknown id falls back to the server's default preset for the role. */
|
|
24
|
+
roles: Partial<Record<ModelRole, string>>;
|
|
25
25
|
/** Preselected target language of „Übersetzen“. */
|
|
26
26
|
translateLanguage: AiLanguage;
|
|
27
27
|
};
|
|
28
28
|
blocks: {
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/** Block type id the Typed block shortcuts create (toolbar button, heading conversion). */
|
|
30
|
+
defaultBlockType: string;
|
|
31
31
|
/** Preferred document structure: toolbar block types and the structure of AI-generated blocks. */
|
|
32
32
|
structure: StructureMode;
|
|
33
|
-
/** Root of the document drawer while a
|
|
33
|
+
/** Root of the document drawer while a Document with Typed blocks is loaded: nested in the folders, or the file itself. */
|
|
34
34
|
treeRoot: TreeRoot;
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
/** Standard drag handle of blocks without
|
|
35
|
+
/** Block type icon of Typed blocks (their drag handle): always visible, or only on hover, focus or while the details are open. */
|
|
36
|
+
blockTypeIcon: BlockTypeIconVisibility;
|
|
37
|
+
/** Standard drag handle of blocks without Block type icon: on the hovered block, or permanently beside every block. */
|
|
38
38
|
dragHandle: DragHandleVisibility;
|
|
39
|
-
/** Target size in characters of the parts a length split cuts a
|
|
39
|
+
/** Target size in characters of the parts a length split cuts a Typed block into (200–20000). */
|
|
40
40
|
splitTargetChars: number;
|
|
41
41
|
};
|
|
42
42
|
appearance: {
|
|
@@ -52,7 +52,7 @@ export declare function clampSplitTargetChars(typed: string, current: number): n
|
|
|
52
52
|
export declare const EDITOR_SETTINGS_DEFAULTS: EditorSettings;
|
|
53
53
|
/** Complete, valid settings (the body of `PUT /api/editor/settings`). */
|
|
54
54
|
export declare const editorSettingsSchema: z.ZodObject<{
|
|
55
|
-
version: z.ZodLiteral<
|
|
55
|
+
version: z.ZodLiteral<2>;
|
|
56
56
|
editing: z.ZodObject<{
|
|
57
57
|
headingConversionScope: z.ZodEnum<{
|
|
58
58
|
container: "container";
|
|
@@ -71,7 +71,7 @@ export declare const editorSettingsSchema: z.ZodObject<{
|
|
|
71
71
|
}, z.core.$strip>;
|
|
72
72
|
ai: z.ZodObject<{
|
|
73
73
|
autocomplete: z.ZodBoolean;
|
|
74
|
-
|
|
74
|
+
roles: z.ZodRecord<z.ZodEnum<{
|
|
75
75
|
complete: "complete";
|
|
76
76
|
rewrite: "rewrite";
|
|
77
77
|
generate: "generate";
|
|
@@ -85,16 +85,16 @@ export declare const editorSettingsSchema: z.ZodObject<{
|
|
|
85
85
|
}>;
|
|
86
86
|
}, z.core.$strip>;
|
|
87
87
|
blocks: z.ZodObject<{
|
|
88
|
-
|
|
88
|
+
defaultBlockType: z.ZodString;
|
|
89
89
|
structure: z.ZodEnum<{
|
|
90
90
|
flat: "flat";
|
|
91
|
-
|
|
91
|
+
structured: "structured";
|
|
92
92
|
}>;
|
|
93
93
|
treeRoot: z.ZodEnum<{
|
|
94
94
|
file: "file";
|
|
95
95
|
folders: "folders";
|
|
96
96
|
}>;
|
|
97
|
-
|
|
97
|
+
blockTypeIcon: z.ZodEnum<{
|
|
98
98
|
always: "always";
|
|
99
99
|
hover: "hover";
|
|
100
100
|
}>;
|
|
@@ -109,10 +109,11 @@ export declare const editorSettingsSchema: z.ZodObject<{
|
|
|
109
109
|
}, z.core.$strip>;
|
|
110
110
|
}, z.core.$strip>;
|
|
111
111
|
/**
|
|
112
|
-
* Deep-merges unknown input over the defaults; unknown keys dropped, invalid values replaced.
|
|
113
|
-
* `
|
|
112
|
+
* Deep-merges unknown input over the defaults; unknown keys dropped, invalid values replaced. Reads older stored settings
|
|
113
|
+
* through `migrateEditorSettings` (`./migrate`), including the legacy flat `{ headingConversionScope }` per-browser shape.
|
|
114
|
+
* Always returns fresh objects of the current version.
|
|
114
115
|
*/
|
|
115
116
|
export declare function parseEditorSettings(input: unknown): EditorSettings;
|
|
116
117
|
//#endregion
|
|
117
|
-
export { AI_LANGUAGES, AUTHOR_COLOR_STYLES, AiLanguage, AuthorColorStyle, COMMENTS_LAYOUTS, CommentsLayout, DRAG_HANDLE_VISIBILITIES, DragHandleVisibility, HEADING_CONVERSION_SCOPES, HeadingConversionScope,
|
|
118
|
+
export { AI_LANGUAGES, AUTHOR_COLOR_STYLES, AiLanguage, AuthorColorStyle, BLOCK_TYPE_ICON_VISIBILITIES, BlockTypeIconVisibility, COMMENTS_LAYOUTS, CommentsLayout, DRAG_HANDLE_VISIBILITIES, DragHandleVisibility, EDITOR_SETTINGS_VERSION, HEADING_CONVERSION_SCOPES, HeadingConversionScope, MODEL_ROLES, ModelRole, STRUCTURE_MODES, StructureMode, TREE_ROOTS, TreeRoot, isOutdatedEditorSettings, migrateEditorSettings };
|
|
118
119
|
//# sourceMappingURL=index.d.ts.map
|