isExternalHttpUrl treated any http(s):// URL as external, so an absolute link
back to the app's own host (e.g. https://self/p/{uuid}, /settings/members)
emitted by the assistant stayed clickable on the anonymous share, leaking
internal UUIDs/structure and pointing at auth-gated routes. Classify a link as
external only when its host differs from window.location.host; unparseable URLs
are treated as internal (fail-closed). Tests cover own-origin absolute (flag
on -> inert), external host (kept with safe rel/target), dangerous schemes, and
no behavior change for the internal chat (flag off).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The public-share widget was a separate minimal impl: plain-text answer, static
'Thinking…', no markdown, no tool-cards. Now it renders through the internal
chat's debugged presentational layer (MessageList/MessageItem/TypingIndicator/
ToolCallCard), so a share gets the same incremental streaming, animated typing
indicator, markdown, and tool-call cards. The share keeps its anonymous
transport (useChat + DefaultChatTransport '/api/shares/ai/stream',
credentials:'omit').
The shared components were already prop-driven (UIMessage[] + isStreaming) with
no transport/auth coupling; made the new props additive optionals (emptyState,
showCitations, neutralizeInternalLinks) all defaulting to current behavior, so
the internal chat is unchanged.
Security (review-caught): rendering assistant markdown on the ANONYMOUS share
made internal links (/p/{id}, /settings/...) clickable, which the old plain-text
render didn't. renderChatMarkdown gains neutralizeInternalLinks (true only on
the share): a one-shot DOMPurify afterSanitizeAttributes hook (added/removed by
reference around a single sanitize) strips href from internal/relative/non-http(s)
links (rendered inert) and keeps external http(s) links with
rel=noopener noreferrer nofollow target=_blank. Tests cover both the link
neutralization and the absence of any global-hook leak into internal renders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add reversible write tools to the per-user agent toolset (page create/update/
move/soft-delete; comment reply + resolve), exposed under the user's JWT and
enforced by Docmost CASL; no permanent/force delete (D3).
- Non-spoofable agent provenance: sign actor/aiChatId into the access and collab
tokens (TokenService), propagate via jwt.strategy onto the request, and set
pages.last_updated_source/last_updated_ai_chat_id on REST create/update/move and
comments.created_source/resolved_source/ai_chat_id.
- packages/mcp: add an optional getCollabToken provider (content-edit provenance)
and guard against empty tokens; service-account /mcp path unchanged.
Frontend:
- Admin 'AI / Models' settings section: provider/model/embedding/base URL, a
write-only API key field, system prompt, and Test connection.
- AI chat panel (useChat + DefaultChatTransport): conversation list, streamed
messages, tool-call action log and page citations; header entry point gated on
settings.ai.chat.
Compile-verified (server nest build + client tsc/vite); not yet live-tested.
Known gaps: history 'AI agent' badge (C3), vector RAG (D), external MCP (E);
chat tool-card citation links pending a fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>