Commit Graph

655 Commits

Author SHA1 Message Date
claude code agent 227
7c57a386b2 test(mcp): coupling guard between enforceBasicLoginGate and login (#91)
McpService.enforceBasicLoginGate re-implements AuthController.login's pre-token
SSO/MFA gate; silent drift would re-open the bypass. Add an AST contract test
(comments stripped) asserting BOTH method bodies contain validateSsoEnforcement,
the EE-MFA require, and checkMfaRequirements — so dropping the gate from either
side fails CI. Test-only (no core/auth refactor).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:49:52 +03:00
claude code agent 227
a2ded7ecfb refactor(html-embed): extract the admin-gate strip into one tested helper (#90)
The 4-step html-embed gate (feature-enabled AND role-allowed -> stripHtmlEmbedNodes)
was replicated across call-sites, pinned only by brittle source-regex tests. Add
stripHtmlEmbedIfNotAllowed(json, {featureEnabled, role, onStrip}) and migrate the
5 plain strip-all sites (collab handler, page create+duplicate, both import paths,
transclusion) to it, each keeping its own feature/role resolve + log via onStrip.
Left the 2 sites with different semantics: persistence.extension (#29 preserve-
admin) and share.service (feature-only kill-switch, no role gate). Real unit tests
replace the regex pins; behavior identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:49:52 +03:00
claude code agent 227
c486750b2a test-infra: re-enable 16 disabled server suites (jest DI + lib0 ESM) (#56)
16 suites were disabled via testPathIgnorePatterns due to two root causes: lib0
ESM not transformed (the @hocuspocus/server -> lib0/decoding.js chain) and stock
'should be defined' specs built via Test.createTestingModule without providers.
Add lib0 to transformIgnorePatterns; convert the 14 DI placeholders to direct
new X(...) instantiation with stub deps (keeping a real construct smoke test);
re-enable the suites. Also updates the public-share limiter test to assert the
fail-closed behavior from #62 (surfaced now that the suite runs). Full server
suite: 67 passed, 689 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:40:40 +03:00
claude code agent 227
d45ca00bcc docs(mcp): document the MCP_TOKEN header breaking change + one-time warning (#84)
The shared MCP_TOKEN guard moved from 'Authorization: Bearer <MCP_TOKEN>' to the
X-MCP-Token header (Authorization is now per-user Basic/Bearer), silently breaking
existing /mcp clients. Document it as a Breaking Change in CHANGELOG (reconfigure
to X-MCP-Token). Add a once-per-process migration warning: when MCP_TOKEN is set,
no x-mcp-token is present, and Authorization carries the old 'Bearer <MCP_TOKEN>',
log a hint to migrate — without changing the auth decision (still rejected) or
logging the token value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 03:40:40 +03:00
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 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
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
d96f94a80a ci: run the test suites on push/PR + quarantine broken stock scaffolds
Some checks failed
Test / test (pull_request) Has been cancelled
Add .github/workflows/test.yml (pnpm + Node 22): on pull_request and push
to develop it installs, builds @docmost/editor-ext and runs `pnpm -r test`
across all packages (server Jest, client Vitest, editor-ext Vitest,
packages/mcp node:test). So tests now run automatically in CI, not just
on demand.

To make the run green, quarantine the 16 pre-existing stock NestJS
`should be defined` scaffold specs via jest `testPathIgnorePatterns` —
they never compiled (missing DI providers / lib0 ESM) and assert nothing
useful. Tracked for a proper fix/removal in issue #56. Verified each
pattern drops only its scaffold (46 of 62 suites still collected) and the
full `pnpm -r test` is green: server 587, client 185, editor-ext 56,
mcp 247.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 00:44:21 +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
6128920264 Merge pull request 'feat(public-share): selectable agent-role identity + fix floating-icon overlap' (#25) from feat/share-assistant-identity-and-branding into develop 2026-06-20 19:59:48 +03:00
claude_code
cf29a0fc11 0.93.0 2026-06-20 19:57:37 +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