Commit Graph

724 Commits

Author SHA1 Message Date
claude code agent 227
a11c87c4dc docs(page-templates): document that lookupTemplate is flat (no server recursion) (#54)
Assessment of the page-embed depth/cycle cap: the server /pages/template/lookup
returns FLAT single-level content and does NOT recurse into embedded pages — the
recursive expansion + the PAGE_EMBED_MAX_DEPTH cap are entirely client render
concerns, and a scripted client is already bounded by the per-user throttle
(30/min) + the ArrayMaxSize(50) per-call cap. So no server-side depth guard is
needed; documented at lookupTemplate so future readers don't add a redundant one
or assume server recursion exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:40:40 +03:00
claude code agent 227
6928817cee fix(ws): broadcast realtime page rename/icon change (#72)
handleMessage became a no-op and PageWsListener intentionally ignored
PAGE_UPDATED, so a rename/icon change (client operation:updateOne) was no longer
rebroadcast -> other clients saw stale title/icon in the sidebar+breadcrumbs
until a reload (create/duplicate/restore were covered; updateOne regressed).
Add a server-authoritative onPageUpdated handler: PageService.update detects a
real title/icon change (DTO carries the field AND value differs; no-op/content-
only saves excluded) and attaches a treeUpdate snapshot to PAGE_UPDATED; the
listener broadcasts a tree updateOne via the restriction-aware emitTreeEvent
(so a restricted page's title never leaks). Content-only saves attach nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:29:52 +03:00
claude code agent 227
c78177c28b test(page): exercise the real getSidebarPagesTree via an extracted pure helper (#75)
sidebar-pages-tree.spec tested a LOCAL COPY of the tree-shaping (so a regression
in the real getSidebarPagesTree was invisible) and justified it with a false
jest-config claim (the ^src mapping exists). Extract the pure shaping into
shapeSidebarPagesTree(); the service now calls it and the spec imports the REAL
helper. Behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:29:52 +03:00
claude code agent 227
b597841cf0 test(ai-roles): cover update() happy-path return shape (#88)
The concurrent-soft-delete guard was already covered; add the missing assertion
that update() returns toView(updated) from the post-update re-fetch (full
AgentRoleView shape, distinct second findById row), so a regression returning the
stale pre-update view or leaking columns is caught.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:28:58 +03:00
claude code agent 227
317fdb9424 test(public-share): cover getSharePage positive + soft-deleted branches (#85)
The anonymous share page-fetch tool's positive branch (sanitize via
updatePublicAttachments then jsonToMarkdown before returning to the model) was
untested, so a dropped/reordered sanitizer would ship a comment-mark/raw-
attachment leak with green tests. Add a positive-branch test pinning the
sanitizer call + that markdown derives from sanitized content, and a soft-deleted
test asserting a generic error with no content fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:28:58 +03:00
claude code agent 227
40f68e95fb fix(ws): shrink restriction-cache TTL to bound the leak window (#53)
invalidateSpaceRestrictionCache has no callers because no restriction-mutation
path exists yet (PagePermissionRepo mutators are uncalled; there is no
restrict/grant/revoke endpoint), so the 30s spaceHasRestrictions cache could
serve a stale 'no restrictions' verdict. Until a mutation endpoint exists to
wire the direct invalidation, lower the TTL (30s -> 3s) to bound the worst-case
window; the invalidation primitive is kept for that future endpoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:28:58 +03:00
claude code agent 227
342bb47b30 fix(ai-roles): validate chatModel + guard driver-enum drift (#52)
chatModel was a free string accepted with empty/garbage values, failing only at
runtime as a provider 503; tighten it (trim + non-empty + max 200). Driver was
already @IsIn(AI_DRIVERS). Collapse the client driver list to one AI_DRIVER_VALUES
source and add a contract test that reads the server AI_DRIVERS and fails on
client/server drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:28:58 +03:00
claude_code
e9ceb0f899 fix(html-embed): address code-review findings on the sandbox commit
Follow-up fixes to the htmlEmbed-sandbox / trackerHead change:

- share-seo: inject trackerHead via a function replacer so `$`-sequences
  ($&, $', $`, $$) in the admin snippet are inserted literally instead of
  being treated as String.replace substitution patterns; warn when the
  </head> marker is absent instead of silently skipping injection.
- mcp: register a passthrough `htmlEmbed` node in the schema mirror so an
  AI/MCP edit of a page containing an embed no longer throws
  "Unknown node type: htmlEmbed" in TiptapTransformer.toYdoc.
- editor-ext + client: treat a non-finite `data-height` as auto (null) so a
  crafted/corrupted height cannot disable auto-resize or yield a NaN iframe
  height; extract a shared clampHeight helper.
- client: rename render-raw-html.{ts,test.ts} -> html-embed-sandbox.{...} and
  shouldExecute -> shouldRender so the seam name matches the sandbox model.
- client: i18n the iframe title; surface the real error reason in
  tracker-settings (console.error + err.response.data.message).
- docs: note hasHtmlEmbedNode is now a test-only helper; add an Unreleased
  CHANGELOG entry; drop the dangling "arbitrary HTML embed" planning-doc ref.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:22:37 +03:00
claude code agent 227
c0d312d8f5 harden(transclusion): depth-cap the ProseMirror collectors (#55)
collectPageEmbedsFromPmJson (and the sibling collectors/remap) recursed with no
guard, so a pathological/cyclic non-JSON input could stack-overflow (RangeError).
Add a depth cap (1000, far above any real doc nesting) so such input is handled
gracefully. Normal documents are unaffected. Updates a stale test that asserted
the old throwing behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:17:37 +03:00
claude code agent 227
5215913533 fix(security): env-configurable trustProxy with a safe default (#61)
trustProxy was unconditionally true, so req.ip came from a client-forgeable
X-Forwarded-For and the per-IP throttles (share-AI, /mcp brute-force) were
spoofable. Make it env-configurable (TRUST_PROXY) with a safe default that
trusts XFF only from loopback/private proxies, documented in .env.example.
NOTE: this changes the default from trust-all; deployments whose proxy is on a
public IP must set TRUST_PROXY (caveat documented).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:17:37 +03:00
claude code agent 227
099d31f594 fix(ai): sandwich SAFETY_FRAMEWORK around the role persona (#68)
A custom AI-role's text preceded the only SAFETY_FRAMEWORK block and replaced
the persona, so a jailbreak in the role text sat before the safety rules.
buildSystemPrompt now emits SAFETY both before AND after the persona, with the
role/persona delimited as lower-trust (<role_persona note=...>); the default
persona is sandwiched too. Context (currently-viewing-page) preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:17:37 +03:00
claude code agent 227
212bcea4d7 fix(page): movePage cycle guard + no phantom PAGE_MOVED (#67, #64)
#67: movePage didn't check the destination wasn't the page itself or inside its
own subtree, so MCP/REST/agent/fast-drag could persist+broadcast a cycle. Reject
before the update (self-parent, or moved page among the destination parent's
ancestors via getPageBreadCrumbs).
#64: movePage emitted PAGE_MOVED from a stale pre-read even when the row didn't
change / was concurrently deleted (phantom move). Gate the emit on
updateResult.numUpdatedRows !== 0n. Both are movePage hardening in one method.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:08:34 +03:00
claude code agent 227
05a7a4001f fix(share-ai): cap per-request output + unify provider errors (#60, #95)
#60: streamText had no maxOutputTokens, so one anonymous request could run up
the provider bill. Add maxOutputTokens (env SHARE_AI_MAX_OUTPUT_TOKENS, default
512) via resolveShareAiMaxOutputTokens().
#95: the anonymous path hand-built error strings, diverging from the unified
describeProviderError format used on the authenticated path; both onError blocks
now call describeProviderError so a share reader sees 402/429/503 causes in the
same form (and the stack is still logged). Both changes are in this one file and
share hunks, hence one commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:08:34 +03:00
claude code agent 227
5344a9bdde fix(auth): handle null-password (SSO/LDAP-only) accounts without bcrypt throw (#70)
A user with password=NULL passed the missing/disabled guard and reached
comparePasswordHash(pw, null), which native bcrypt rejects -> 500 on
/api/auth/login and, on /mcp, a leaky 401 that the brute-force limiter ignored
(enumeration oracle + limiter evasion). Treat a null/empty password like a
missing user in verifyUserCredentials (dummy compare for timing parity + unified
CREDENTIALS_MISMATCH_MESSAGE) and reject early in changePassword before bcrypt.
Contract spec asserts the null-password guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:07:53 +03:00
claude code agent 227
2b4ec0bfcc fix(share-ai): reject non-text message parts to close size-cap bypass (#63)
MAX_SHARE_MESSAGE_CHARS only counted text parts, so a forged non-text part
(tool-result/file/data) bypassed the cap and bloated the model input
(token-DoS); convertToModelMessages would also expand a forged tool-result. The
anonymous path runs no tools, so a client non-text part is never legitimate —
reject any message with a non-text part (isTextUIPart) before the size check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:07:53 +03:00
claude code agent 227
e19849d980 fix(share-ai): fail-closed workspace limiter on Redis failure (#62)
The per-workspace anonymous share-AI cost cap failed OPEN on a Redis error
(return true => admit), so a Redis outage removed the cap entirely (unmetered
billable anonymous calls). The feature is optional, so unavailability is
harmless: fail CLOSED (return false => controller 429s) instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:07:53 +03:00
claude_code
81823fce1e feat(html-embed): sandbox the embed block; split trusted trackers into an admin field
Convert the htmlEmbed node from same-origin raw-HTML execution to a sandboxed
iframe (sandbox="allow-scripts allow-popups allow-forms", no allow-same-origin,
srcdoc) with postMessage auto-resize (validated by event.source) and an optional
manual height attr. The block now runs in an opaque origin and cannot reach the
viewer's cookies/session/API, so it is safe for any member.

Because the block is now harmless, remove the entire admin/role gating apparatus:
drop htmlEmbedAllowed/canAuthorHtmlEmbed/stripDisallowedHtmlEmbedNodes/
collectHtmlEmbedSources and every role-based strip on the write paths (collab
REST/MCP + socket, page create/duplicate, import x2, transclusion unsync), along
with the now-unused WorkspaceRepo/UserRepo injections and the PageService.create
callerRole param. Keep one strip: prepareContentForShare still removes htmlEmbed
on the anonymous public-share read path when the workspace master toggle is OFF.

The workspace settings.htmlEmbed toggle is now a plain feature switch (gates the
slash-menu and share rendering); when ON the block is available to all members.

Add settings.trackerHead: an admin-only raw HTML/JS analytics snippet injected
verbatim into the <head> of public share pages only (ShareSeoController), for
trackers that genuinely need same-origin. Admin-gated via the existing CASL
Manage/Settings ability; never injected into the authenticated app shell.

Closes security-review findings #1, #2, #4, #5, #10 (and #3 as a security issue).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 02:48:41 +03:00
claude_code
64818cf9df Merge branch 'feat/share-ai-cost-guards' into develop 2026-06-21 02:21:04 +03:00
claude_code
262a0707d9 feat(share-ai): cap per-request output tokens and fail closed on Redis loss
Harden the anonymous public-share AI assistant against token-cost abuse
before exposing it to the internet:

- Add an env-tunable per-request output ceiling (maxOutputTokens) to the
  public-share streamText call so one anonymous request cannot run up the
  provider bill even if the per-IP throttle is evaded. New
  resolveShareAiMaxOutputTokens() / SHARE_AI_MAX_OUTPUT_TOKENS_DEFAULT
  (env SHARE_AI_MAX_OUTPUT_TOKENS, default 512), mirroring
  resolveShareAiWorkspaceMax().
- Flip the per-workspace cost limiter to FAIL CLOSED on Redis failure
  (was fail-open): if Redis is unavailable we cannot prove the workspace is
  under its cap, so deny rather than admit an unmetered, billable call.
- Update the limiter spec (fail-open -> fail-closed) and add resolver tests;
  document both knobs in .env.example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 02:15:54 +03:00
claude_code
4bf6d9f36b Merge develop into fix/html-embed-hardening (#46)
Some checks failed
Test / test (pull_request) Has been cancelled
Resolve the html-embed.spec.ts conflict as a union: both #46 and #49 (already in
develop) added different test cases to the same file. Keep all of them —
stripHtmlEmbedNodes gets #46's root-node case plus develop's deeply-nested,
non-object and empty-content cases; #46's collectHtmlEmbedSources and
stripDisallowedHtmlEmbedNodes suites and develop's hasHtmlEmbedNode suite all
kept; imports unioned. No production code conflicted.

Full suite green: server 651, client (16 files), editor-ext 56, mcp 247.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:59:22 +03:00
claude_code
d7681b4fb6 Merge develop into fix/page-template-demo-issues (#45)
Some checks failed
Test / test (pull_request) Has been cancelled
Resolve conflicts from the parallel page-embed refactor that landed in develop
via #49:
- page-embed-view.tsx: keep develop's canonical decideEmbedState for the
  cycle/depth/availability guard; keep #45's #39 chrome cleanup (single source
  link, IconFileText fallback) and #40 refresh remount key. Drop #45's now-unused
  isPageEmbedCycle/isPageEmbedTooDeep wiring.
- page-embed-picker.tsx: use develop's excludeHost util; drop #45's duplicate
  filterPageEmbedOptions and its test.
- page-embed-ancestry-context.test.tsx: keep #45's superset suite.
- page-template-access.spec.ts: keep develop's constructor args; update the two
  deleteByReferenceAndSources assertions to the new 4-arg workspace-scoped
  signature introduced by #45 (#36 defense-in-depth).

Full suite green: server 624, client 219, editor-ext 56, mcp 247.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:51:09 +03:00
claude_code
3695dbdf7f Merge remote-tracking branch 'gitea/develop' into fix/ai-chat-current-page 2026-06-21 01:29:37 +03:00
claude_code
730486ad12 test(mcp): keep real mcp-auth.helpers in gate spec mock (forward-compat with #49)
Some checks failed
Test / test (pull_request) Has been cancelled
After develop merged, mcp.service.ts calls decideBasicGate from mcp-auth.helpers.
The gate spec mocked the whole module returning only FailedLoginLimiter, so the
merged code crashed with 'decideBasicGate is not a function' (7/7 failing).
Spread jest.requireActual('./mcp-auth.helpers') so the real helpers are kept and
the gate exercises real logic; keep only FailedLoginLimiter stubbed so its
constructor runs without a real sweep timer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:25:36 +03:00
claude_code
5f3a3d3ec0 Merge remote-tracking branch 'gitea/develop' into fix/mcp-security-followups 2026-06-21 01:21:57 +03:00
claude_code
90d3fab483 test: cover features since 053a9c0d + repair test tooling
Add ~330 tests across server (Jest), client (Vitest), editor-ext (Vitest)
and packages/mcp (node:test) for the gitmost features added since
053a9c0d: AI chat, AI agent roles, public-share assistant, MCP per-user
auth, HTML embed, page templates/embed, realtime tree, tree
expand/collapse, and the AI-settings UI.

Test-tooling fixes (prerequisite, were silently hiding coverage):
- Repair 3 page-template specs broken by the 11-arg TransclusionService
  constructor; they never compiled, so template access-control / content
  -leak / unsync-strip coverage was fictitious.
- Build @docmost/editor-ext before server tests via a `pretest` hook;
  the stale dist omitted the new HtmlEmbed/PageEmbed exports (TS2305).
- Let jest resolve the .tsx email templates: add `tsx` to
  moduleFileExtensions and widen the ts-jest transform to (t|j)sx?.

Behaviour-preserving "extract pure core" refactors that the tests drive:
- server: resolveShareAssistantRequest + uiMessageTextLength
  (public-share controller), decideBasicGate + mapAuthResultToResponse
  (mcp), buildErrorAssistantRecord (ai-chat), jsonbObject export (roles).
- client: render-raw-html + shouldExecute/canEdit, decide-embed-state,
  page-embed picker utils, tree-socket reducers, open/close branch maps,
  isEndpointConfigured/resolveKeyField; buildTreeWithChildren now treats
  a permission-trimmed orphan as a root instead of crashing.

Deferred (need a test DB or HTTP harness, documented in the specs):
repo-level Postgres integration tests and the public-share XFF E2E.
Pre-existing DI/lib0-ESM suite failures are untouched and out of scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:40:40 +03:00
claude code agent 227
1f457b060c fix(mcp): security review follow-ups (#24)
Post-merge hardening from the #13 security review:
- isInitializeRequestBody now delegates to the SDK isInitializeRequest (same
  predicate as packages/mcp/http.ts), so a bare {method:'initialize'} with no
  id/params no longer triggers the side-effecting login() (audit-spam /
  user_sessions growth) before http.ts 400s it.
- Bind the Bearer path to the instance workspace: verifyBearerAccess rejects a
  token whose payload.workspaceId != the instance workspace (resolved via
  workspaceRepo.findFirst, consistent with the Basic path); optional param so
  it's a no-op when unset.
- Close the user-enumeration timing oracle in verifyUserCredentials: the
  missing/disabled branch now runs a bcrypt compare against a module-level dummy
  hash whose cost (12) matches production saltRounds, so both paths take one
  equal-cost bcrypt compare; the exact CREDENTIALS_MISMATCH_MESSAGE is preserved.
- Document the trusted-proxy requirement for the spoofable per-IP brute-force
  limiter in .env.example (trustProxy is on; deploy behind a trusted proxy).
- Add real-execution coverage for enforceBasicLoginGate (SSO enforced / EE-MFA
  bundled vs not / user-MFA / workspace-enforced-MFA) instead of stubbing the gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:36:53 +03:00
claude code agent 227
424761753e fix(html-embed): shrink the collab broadcast window with an early onChange guard (#26)
A non-admin's transient htmlEmbed could execute in other open editors until the
debounced (10s) onStoreDocument strip. Add a ~300ms onChange-debounced early
strip (guardHtmlEmbed) that converges the shared ydoc for everyone far sooner.

Safety-critical details:
- Scheduled from onChange ONLY for non-admins AND only when the workspace toggle
  is ON (cached per-document in onLoadDocument), so the common toggle-OFF case
  does zero extra work.
- guardHtmlEmbed does ALL async work (toggle + persisted allow-list read) FIRST,
  then performs fromYdoc -> strip -> fragment.delete -> applyUpdate in a single
  SYNCHRONOUS, await-free block, so no inbound Yjs update can interleave and a
  concurrent edit can never be clobbered. Bails if document.isDestroyed.
- Reuses the #29 preserve logic (admin-vetted embeds survive; only the non-admin's
  new ones are stripped). Loop-safe (corrective update has null origin -> no
  reschedule; post-strip no embed -> cheap no-op). Per-document timer cleared on
  unload. onStoreDocument stays the authoritative backstop.

The irreducible residual is only the very first inbound broadcast before the
debounce fires — Hocuspocus exposes no synchronous beforeBroadcast filter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:20:02 +03:00
claude code agent 227
b7ea8c850e fix(html-embed): preserve admin's existing embed on a non-admin co-editor's store (#29)
The collab persist strip keyed to the storing connection's user, so when a
non-admin co-editor stored, it removed an admin's legitimately-authored embed
too (data loss). Now: toggle OFF still strips all (feature disabled); toggle ON
+ non-admin storer strips only NEWLY-introduced embeds and preserves those
already present in the persisted content (admin-vetted), via new helpers
collectHtmlEmbedSources + stripDisallowedHtmlEmbedNodes (identity = attrs.source,
already-vetted HTML). The ydoc reflect is now guarded by a deep-equal check so
an unrelated non-admin edit that touches no new embed doesn't churn the doc.
A non-admin still cannot add a new embed. Documents the allow-list TOCTOU
(best-effort snapshot read outside the lock; converges on next store).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:02:01 +03:00
claude code agent 227
8191c37daa test(html-embed): real-execution gate tests for create/duplicate/import (#27)
The create/duplicate/import gate tests asserted gate presence via brittle
expect(SRC).toMatch(/regex/) over the source text plus a reimplemented
applyGate() stand-in, so a refactor could break the real gate while they still
passed. Rewrite both specs to execute the REAL methods (PageService.create /
duplicatePage; ImportService.importPage; FileImportTaskService.processGenericImport)
with each caller role and assert on the PERSISTED content via hasHtmlEmbedNode:
member -> stripped, admin/owner+toggle ON -> preserved, toggle OFF -> stripped
for everyone, unknown/missing role -> fail-closed. No source-regex assertions
remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 22:49:18 +03:00
claude code agent 227
39f3eacf89 test(page-templates): cover lookupTemplate anti-leak + edge cases (#33)
- The security-relevant catch->not_found branch in lookupTemplate (returns
  not_found instead of raw content when comment-mark stripping throws) is now
  tested by forcing the strip to throw with a malformed text node, asserting no
  content/marks leak.
- not_found for a soft-deleted source resolved through the REAL
  filterViewerAccessiblePageIds (deletedAt-excluded), not the stubbed filter.
- Rename the misleading 'honours <=50 cap' test to reflect it only exercises
  dedup (the cap lives in the DTO, never engaged in the service unit).
- Cover the onlyTemplates search filter (restricts to is_template=true).
Also fix two pre-existing failing 'should be defined' specs (search service +
controller) that couldn't resolve the @InjectKysely token via createTestingModule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 22:37:35 +03:00
claude code agent 227
bc1ea792f5 test(page-templates): cover duplicatePage pageEmbed remap + reference sync (#32)
Extract the per-node pageEmbed remap decision into a shared pure helper
(remapPageEmbedSourceId) and use it BOTH in PageService.duplicatePage and the
JSON walker, so the test guards the real production path (not a mirror that
could drift). Behavior is identical: source in the copied set -> new copy id;
otherwise keep the original. Add jest coverage (16 tests): the remap helper
(in-set/out-of-set/null/nested), syncPageTemplateReferences toDelete (stale refs
removed with the right workspaceId), and insertTemplateReferencesForPages
multi-workspace grouping/filtering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 22:37:35 +03:00
claude code agent 227
4f46f91db4 test(page-templates): fix TransclusionService spec constructor arity
The transclusion specs predated two added constructor params, so they failed to
compile (TS2554: expected 11 args, got 10) and the suites couldn't run. Add the
missing mock args: workspaceRepo (param 11) in the lookup/access specs, and
pageTemplateReferencesRepo (param 4, which had shifted pageRepo into the wrong
slot) in the unsync-html-embed spec. All three suites now compile and pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 22:22:56 +03:00
claude_code
c5f44a6eee Merge branch 'develop' into feat/footnotes
Resolve conflicts at shared registration points by unioning both features
(footnotes + the already-merged html-embed / page-embed work):
- slash-menu/menu-items.ts, editor extensions.ts: keep both imports + configures
- collaboration.util.ts: register footnote nodes and pageEmbed
- editor-ext marked.utils.ts: register footnote + html-embed markdown extensions
- editor-ext package.json/tsconfig.json/vitest.config.ts: union of test config
  (jsdom env for footnote DOM tests + combined test/spec include glob)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 22:21:07 +03:00
claude code agent 227
a6ba19f0dc feat(ai-chat): add get_current_page tool for proxy-robust page context (#43, hardness #2)
The current page id was only injected as text in the system prompt, which a
proxy (CLIProxyAPI) can rewrite/truncate, so the agent could lose track of 'this
page'. Add a getCurrentPage tool the model can call to read the open page (id +
title) from the server-side request context (forUser now takes openedPage,
threaded from body.openPage — the same value used for the system prompt). The
inline system-prompt line is kept as belt-and-suspenders. Reads/writes still go
through the CASL-enforced page tools by id, so this is strictly not worse than
the existing prompt hint — just delivered over a channel the proxy can't mangle.

User-approved on the issue. Completes #43 together with the hardness-1 fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 22:19:40 +03:00
claude code agent 227
8ee4279d30 harden(html-embed): make stripHtmlEmbedNodes total with a root-type check (#30)
stripHtmlEmbedNodes only filtered children, so a (never-in-practice) bare
htmlEmbed root node would be returned as-is. Add a defensive root check that
returns an embed-free doc, making the helper total — it can never return a node
for which hasHtmlEmbedNode is true. Adds a unit test for the root case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:52:32 +03:00
claude code agent 227
6a052b88b4 fix(html-embed): strip embeds at serve time on authenticated read paths (#28)
Completes the workspace htmlEmbed kill-switch. The public-share path already
strips at serve time when the toggle is OFF, but the authenticated read paths
(/info and /history/info) returned page/history content with embeds intact, so
a disabled feature kept executing for in-workspace view-only viewers until the
page was next saved. Now both paths resolve the workspace toggle and run
stripHtmlEmbedNodes when it's OFF (fail-closed on a missing workspace), before
any markdown/html format conversion. Admin-authored content only — completeness,
not privilege escalation. Injects WorkspaceRepo into PageController.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:52:32 +03:00
claude code agent 227
79d096ed7a fix(page-templates): defense-in-depth workspace checks (#36)
Consistency hardening from #17 review (not currently exploitable):
- toggleTemplate now explicitly rejects a page outside the caller's workspace
  (page.workspaceId !== user.workspaceId -> NotFound, avoiding existence leak)
  instead of relying solely on the space-membership model.
- PageTemplateReferencesRepo.deleteByReferenceAndSources is now workspace-scoped
  (adds a workspaceId filter + param), matching the 'scope by workspaceId
  everywhere' invariant; the sole caller threads its workspaceId.
The PAGE_TEMPLATE_THROTTLER limit is intentionally left as-is (the issue's
throttle item was 'consider only'; no change without usage data).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:42:49 +03:00
claude code agent 227
a15cccf557 chore(page-templates): remove dead findReferencePageIdsBySource (#34)
The 'used in N pages' reverse-navigation method had zero callers in the merged
PR #17 — unreachable, untested code. Remove it. The reverse-navigation feature
can be (re)added with the method if/when it's actually built.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:42:49 +03:00
claude code agent 227
22887c474a chore(page-templates): tidy ts suppression in duplicatePage pageEmbed remap (#37)
Replace bare //@ts-ignore (no space, no reason) with // @ts-expect-error plus a
reason on the pageEmbed sourcePageId reassignment, matching the codebase style.
ProseMirror Attrs is read-only typed, so the reassignment genuinely errors —
@ts-expect-error is valid here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:42:30 +03:00
claude_code
19ae6a0efa Merge pull request 'feat(editor): page templates — live whole-page embed (MVP)' (#17) from feat/page-templates into develop
Some checks failed
Develop / build (push) Has been cancelled
2026-06-20 20:34:44 +03:00
claude_code
2b3fc926cc Merge remote-tracking branch 'gitea/develop' into feat/html-embed-admin
# Conflicts:
#	apps/server/src/core/workspace/services/workspace.service.ts
2026-06-20 20:18:44 +03:00
claude_code
e9e9f74ec6 Merge remote-tracking branch 'gitea/develop' into feat/page-templates
# Conflicts:
#	apps/server/src/integrations/throttle/throttle.module.ts
#	apps/server/src/integrations/throttle/throttler-names.ts
2026-06-20 20:18:42 +03:00
claude code agent 227
52efd37fd9 fix(page-templates): import ThrottleModule into collab app so it boots
PageTemplateController (added on this branch) guards its lookup/toggle routes
with UserThrottlerGuard, which depends on the throttler options provided by
ThrottleModule. CollaborationModule -> TransclusionModule registers that
controller, and the collab server bootstraps CollabAppModule, which did not
import ThrottleModule. The API server's AppModule does, so :3000 booted, but
the collab server (:3001) crashed at startup with
'Nest can't resolve dependencies of the UserThrottlerGuard ... THROTTLER:MODULE_OPTIONS'.
Without collab the editor can't sync, so live editing was broken on this branch.

Import ThrottleModule into CollabAppModule, mirroring AppModule, so the guard
resolves in the collab process too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:10:09 +03:00
claude_code
4fe42ead56 feat(public-share): selectable agent-role identity + fix floating-icon overlap
Anonymous public-share AI assistant:
- Add a workspace setting `publicShareAssistantRoleId` so an admin can pick which
  agent role (identity/persona) the anonymous assistant adopts. The role's
  instructions REPLACE the built-in persona while the immutable safety framework
  is still always appended; the role's optional model override takes precedence
  over the cheap publicShareChatModel. Resolved server-authoritatively
  (workspace-scoped, soft-delete aware; disabled/missing roles fall back to the
  built-in persona, so the tool scope remains the real security boundary).
- Plumb the field through the update DTO, ai-settings service, the workspace.repo
  ALLOWED whitelist, resolve()/getMasked(), stream-time role resolution and the
  prompt/model, plus the settings UI: a new "Assistant identity" Select listing
  enabled roles (and surfacing a saved-but-disabled role explicitly).

Public-share branding / floating icon:
- Fix the AI assistant FAB overlapping the "Powered by ..." button (both were
  Affixed bottom-right): stack the FAB above the bottom-right branding.
- Rename "Powered by Docmost" -> "Powered by Gitmost" and point the link at the
  gitmost repo.

Tests: extend public-share-chat.spec (role persona replacement still appends the
safety framework, resolveShareRole edge cases, model-override precedence).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 19:54:45 +03:00
claude code agent 227
41f3944e79 fix(html-embed): execute embeds on public shares; toggle is server-side kill switch
The html-embed feature toggle was enforced CLIENT-side in the NodeView (reads
settings.htmlEmbed from the logged-in workspace), so an anonymous public-share
viewer — who has no workspace context — always saw it as OFF and got a
placeholder instead of the executing embed. That broke the whole point (a
tracker must run for anonymous visitors).

Make it server-authoritative:
- share.service prepareContentForShare (the single path both share-content
  flows use) strips htmlEmbed from served content when the workspace toggle is
  OFF; both callers (updatePublicAttachments host page + lookupTransclusionForShare)
  resolve the toggle once and pass it. Fail-closed: missing workspace -> OFF ->
  stripped.
- NodeView executes whatever it was served in read-only/share mode
  (shouldExecute = !editor.isEditable || htmlEmbedEnabled); the disabled
  placeholder now only shows in the editable editor when OFF.

Net: anonymous share + toggle ON -> server serves the (admin-authored) embed ->
it executes for everyone; toggle OFF -> stripped server-side from every
share-content path (true kill switch); a non-admin embed can never be served
(save-path strip). No XSS regression in the editable editor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 19:49:57 +03:00
claude_code
46688074d8 Merge pull request 'feat(tree): server-authoritative realtime tree updates' (#15) from feat/realtime-tree-server into develop 2026-06-20 19:48:36 +03:00
vvzvlad
f650d2591b fix(tree): address realtime-tree-server review findings
- make addTreeNode receivers idempotent (invalidateOnCreatePage guard +
  buildTree dedup) so the author's self-echo no longer duplicates the node
- broadcast realtime tree updates for bulk copy/duplicate and import via a
  root refetch: PAGE_CREATED now carries spaceId and the WS listener falls
  back to refetchRootTreeNodeEvent when no per-node snapshot is present
- remove the now-dead client-relay inbound path (isTreeEvent/handleTreeEvent)
  that remained a stale-restriction-cache attack surface
- honest string|null cast for a root move's parent id
- add tests: buildTree dedup; onPageCreated per-node vs refetch branching

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 19:48:06 +03:00
claude_code
f72e44c9b7 Merge pull request 'feat(mcp): per-user auth for /mcp (HTTP Basic, server-validated)' (#13) from feat/mcp-per-user-auth into develop 2026-06-20 19:32:02 +03:00
claude code agent 227
8fcce6a674 feat(html-embed): per-workspace feature toggle, default OFF
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>
2026-06-20 19:28:39 +03:00
vvzvlad
0c46f60ddf Merge gitea/develop into feat/public-share-assistant
Resolve conflicts with the independently-merged ai-agent-roles feature:
- ai-chat.module.ts: keep BOTH AiAgentRolesModule and the public-share
  wiring (Share/Search modules, PublicShareChatController, services).
- ai.service.ts: take develop's getChatModel ChatModelOverride superset,
  which already covers the public-share model-id-only override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:40:58 +03:00