The admin-only raw HTML/JS embed is a deliberate stored-XSS surface, so gate the
whole feature behind a workspace toggle that is OFF by default; it only works
when a workspace admin explicitly enables it.
- settings.htmlEmbed (boolean, default false) + workspace-update field htmlEmbed,
persisted via WorkspaceRepo.updateSetting with an audit diff. Flipping it is
admin-only (same Manage Settings CASL as other workspace toggles).
- New gate htmlEmbedAllowed(featureEnabled, role) = featureEnabled && admin/owner.
All 7 server write paths (create, duplicate, collab onStoreDocument, REST/MCP/AI
updatePageContent, single + zip import, transclusion unsync) now read the
workspace's settings.htmlEmbed and strip unless (toggle ON AND admin). OFF
(default, or a failed/empty workspace lookup) strips htmlEmbed for EVERYONE
including admins -> existing embeds are cleaned up on next save, none persist.
- Client (defense-in-depth): the /html slash item is hidden unless toggle ON +
admin; the NodeView executes nothing and shows a 'disabled in this workspace'
placeholder when OFF; an admin Switch in Workspace Settings -> General with a
description of the behavior.
- docs/html-embed-admin.md documents the toggle + admin-only + fail-closed
coedit (a non-admin save strips an admin's embed) + execution semantics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds an htmlEmbed block node that renders and executes raw HTML/CSS/JS in the
wiki origin (e.g. an analytics tracker) — the owner-chosen variant C. Because
this is stored-XSS by design, only workspace admins/owners may get such a node
persisted; everyone executes it when reading.
- Node (editor-ext): htmlEmbed atom/isolating block; source stored base64 in
data-source for lossless HTML<->JSON round-trip. renderHTML emits only the
encoded marker (never inlines raw markup), so generateHTML/export/search are
not themselves injection vectors. Registered in BOTH client extensions and
server tiptapExtensions. Markdown round-trip via an <!--html-embed:b64-->
comment (turndown) + a marked rule.
- Client NodeView: injects source and re-creates <script> elements so they
actually run; edit modal; renders in read-only/share too. Slash item is
admin-gated (adminOnly filtered by the user's workspace role).
- SERVER ENFORCEMENT (the real control — UI gating alone is insufficient):
stripHtmlEmbedNodes() removes htmlEmbed from any document persisted by a
non-admin, applied at every write path that introduces content from an
untrusted author: collab onStoreDocument, REST/MCP/AI updatePageContent,
single-file import, zip/multi-file import, page duplication, and transclusion
unsync. Page restore introduces no new content. Public share/readonly viewers
render fetched (already-stripped) content and do NOT open a collab socket, so
the only residual is a transient broadcast window to concurrent authenticated
editors (documented).
Implements docs/arbitrary-html-embed-plan.md (variant C).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add push-to-talk voice dictation that transcribes recorded audio on the
server via the workspace's OpenAI-compatible AI provider (Whisper /
gpt-4o-transcribe / self-hosted whisper), then inserts the text.
Backend:
- New `stt_api_key_enc` column + migration; STT creds parity with chat/
embeddings (sttModel/sttBaseUrl/sttApiKey, write-only key, fallbacks to
chat baseUrl/key). Both provider whitelists updated (service + repo).
- AiService.getTranscriptionModel + AiTranscriptionService.
- Gated POST /ai-chat/transcribe (dictation flag → 403, JWT + workspace
scope + throttle, 25MB cap, MIME whitelist, never logs audio/key).
- New `settings.ai.dictation` workspace flag (DTO + service + audit).
Frontend:
- Wire up the Voice/STT settings card (model/base URL/key) and the
Voice-dictation toggle.
- New `features/dictation`: useDictation (MediaRecorder state machine),
MicButton, transcribe service; integrated into the chat composer and a
new editor-toolbar dictation group, both gated by ai.dictation.
* feat(editor): add alt text support for images
* feat: extend alt text support to videos and diagrams
---------
Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
* fix(editor): hide transclusion borders and reset spacing in read-only mode
* feat(share): add full width toggle for shared pages
* feat(share): support resizing sidebar on shared pages
* fix: auto redirect if there is only one SSO provider.
- fix tighten sso redirect
- fix share tree margin
* sync
* package overrides
* feat(tree): replace react-arborist with custom tree implementation
* feat(tree): keyboard arrow navigation between rows
* feat(emoji-picker): focus search input on open
* refactor(emoji): switch to @slidoapp/emoji-mart fork for accessibility
* feat(tree): Home/End and typeahead keyboard navigation
* feat(tree): roving tabindex and * to expand sibling subtrees
* feat(tree): Space activation and ARIA refinements
* fix(tree): move treeitem role to focusable row + aria-current
* feat(editor): show emoji name in suggestion list
Replace the fixed-column emoji grid with a vertical list that displays
each emoji alongside its :shortcode: name. This makes the picker more
discoverable—users can see and learn shortcodes without prior knowledge.
Changes:
- EmojiList: switch from SimpleGrid/ActionIcon to UnstyledButton list
rows showing emoji glyph + monospace 🆔 label
- Navigation simplified to ArrowUp/ArrowDown (list has no columns)
- Results capped at 8 items for a focused, scannable dropdown
- CSS module: rename menuBtn -> menuItem, tighten padding
* feat(editor): replace SearchIndex with name/id includes search
Port the exact search algorithm from the original extension:
- Build a flat index from @emoji-mart/data: { id, name (lowercase), native }
- Filter with name.includes(q) || id.includes(q) — predictable, no
keyword indirection
- Results capped at 5 (same as extension)
- Frequently-used emojis (sorted by usage) shown when query is empty
- Remove emoji-mart init() / SearchIndex / getEmojiDataFromNative
dependencies; index is built lazily and cached in memory
- Remove unused GRID_COLUMNS constant
* feat(editor): emoji picker with browse and search modes
When the query is empty the picker shows a category bar with 8 tabs
(people, nature, food…) and a scrollable emoji grid. Typing after ':'
switches to a compact list that shows the glyph and :shortcode: side by
side, making it easy to discover emoji names while you type.
- Category data is loaded lazily from @emoji-mart/data and cached, so
opening the picker more than once has no overhead
- Grid keyboard nav: arrow keys move by cell/row, Enter picks
- List keyboard nav: up/down through results, Enter picks
- Mouse hover syncs the keyboard selection index in both modes
- incrementEmojiUsage tracks picks so frequently used ones bubble up
in future sessions
* fix(editor): polish emoji picker copy and loading
* feat: add emoji to slash command
* Add keyboard support to emoji group navigation
---------
Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
* fix: resolve keystroke input being swallowed after link in Firefox
In Firefox, when the cursor is at the right boundary of a link mark,
contenteditable inserts new text inside the <a> element. ProseMirror
then rejects the DOM mutation because the link mark has inclusive: false,
causing keystrokes to be silently swallowed. Unlike Chrome, Firefox also
does not fire ProseMirror's handleTextInput callback in this state.
This adds a ProseMirror plugin that intercepts printable character
keydowns at link mark boundaries and programmatically inserts the text
without the link mark, bypassing Firefox's native contenteditable
behavior entirely.
Fixes#1773
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve keystroke input being swallowed before a link in Firefox
Extend the linkBoundaryInput plugin to also handle the left boundary
of links, where the cursor is just before a link (e.g. at the start
of a line). Firefox inserts text inside the <a> element in this case
too, causing ProseMirror to reject the mutation.
Fixes#1748
* feat: new image menu
* switch to resizable side handles
* use pixels
* refactor excalidraw and drawio menu
* support image resize undo
* video resize
* callout menu refresh
* refresh table menus
* fix color scheme
* fix: patch @tiptap/core ResizableNodeView to prevent resize sticking after mouseup
* feat: columns
* notes callout
* focus on first column
* capture tab key in column
* fix print
* hide columns menu when some nodes are focused
* fix print
* fix columns
* selective placeholder
* fix blockquote
* quote
* fix callout in columns
* feat(ee): AI menu
* - Add insert below and copy option
* prebuild @editor-ext
* sanitize output
* clear existing output
* switch to menu component
* refactor directory
* separator
* refactor directory
* support more languages
* pass markdown to model
* fix: close AI menu on page change
* enhance text input and preview styling
* fix: Use absolute positioning for the AI menu
* make preview scrollable
* activation controls
* enhance bubble menu
* sync
* set width
* fix line break
* switch terminologies
* cloud
* buffer
---------
Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>