Commit Graph

1208 Commits

Author SHA1 Message Date
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
b53b0c651e docs(footnotes): delete footnotes design plan
Some checks failed
Develop / build (push) Has been cancelled
The detailed footnotes implementation plan has been removed from the repository now that the design is finalized and tracked elsewhere.
2026-06-20 21:03:50 +03:00
claude_code
be17391e18 docs: remove admin-only HTML embed documentation
Some checks failed
Develop / build (push) Has been cancelled
2026-06-20 21:03:31 +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
7a03321d43 Merge pull request 'feat(editor): admin-only raw HTML/CSS/JS embed (variant C)' (#16) from feat/html-embed-admin into develop
Some checks failed
Develop / build (push) Has been cancelled
2026-06-20 20:19:06 +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
d80a419963 ci(develop): build the :develop image on push to develop, not main
Some checks failed
Develop / build (push) Has been cancelled
The "Develop" workflow builds the :develop image but was triggered on
push to main (the stable/default branch, released via v* tags). Switch
the trigger to the develop branch so pushes to develop build the image.
2026-06-20 20:05:44 +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
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
claude_code
c718b2a6de Merge pull request 'feat(ai): anonymous AI assistant on public shares' (#14) from feat/public-share-assistant into develop 2026-06-20 18:41:17 +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
vvzvlad
90e9b0a3f4 docs(public-share): document trusted-proxy XFF requirement + cost cap
The anonymous public-share AI assistant's per-IP rate limit is only
effective behind a trusted reverse proxy that overwrites X-Forwarded-For
with the real client IP (the app runs with trustProxy). Document this
deployment requirement and the per-workspace cost backstop env var
(SHARE_AI_WORKSPACE_MAX_PER_HOUR, default 300) in .env.example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:34:16 +03:00
claude_code
4c1d1aa2ee Merge pull request 'feat(ai-chat): agent roles (admin persona + optional model)' (#11) from feat/ai-agent-roles into develop 2026-06-20 18:31:10 +03:00
vvzvlad
4b31128e24 fix(ai-roles): harden model override, role-name uniqueness, id validation, list least-privilege
Follow-up fixes on the agent-roles feature:

- ai.service: a cross-driver override to the ollama driver (when the
  workspace driver is not ollama) now fails with an explicit 503 instead
  of silently reusing the workspace base URL, which belongs to a different
  provider. Same-driver ollama and openai/gemini overrides are unchanged.
- migration: add a partial unique index on (workspace_id, name) WHERE
  deleted_at IS NULL so role names are unique per workspace without
  soft-deleted rows blocking re-creation; map Postgres 23505 to a 409
  ConflictException on create/update.
- dto: validate the role id as @IsUUID instead of @IsString.
- roles list: do not expose instructions/modelConfig to non-admin members.
  The list endpoint now returns a picker view (id/name/emoji/description/
  enabled) to members and the full view only to admins (same gate as the
  CRUD endpoints). Client IAiRole fields made optional accordingly.

Adds tests for the cross-driver-ollama throw, the 23505->409 mapping, and
the non-admin picker-view security invariant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:30:33 +03:00
claude_code
127d26c057 Merge pull request 'test(ai-chat): crypto/SSRF/assistant-parts coverage + a11y + refactors' (#10) from feat/ai-chat-review-followups into develop 2026-06-20 18:10:33 +03:00
vvzvlad
45cf4140eb Merge branch 'develop' into feat/ai-chat-review-followups
Integrate the already-merged step-limit work from develop. Only conflict was
ai-chat.service.spec.ts: both sides appended a describe block and edited the
import line. Resolved as a union — keep compactToolOutput + the assistantParts/
serializeSteps/rowToUiMessage suites (this branch) AND the prepareAgentStep
suite (develop), importing all symbols from ai-chat.service.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:09:17 +03:00
claude code agent 227
ec128d54b4 test(ssrf): add IP-level bypass-vector cases (ported from GLM branch)
Adds explicit isIpAllowed cases for the CGNAT, ULA (fd00::/8) and IPv4-mapped
IPv6 loopback (::ffff:127.0.0.1) sample addresses from the parallel
safety-coverage branch. The mapped-loopback case is genuinely new (the existing
table only covered the mapped *private* variant); CGNAT and ULA ranges were
already covered with other samples and are kept here as explicit regression
guards for these specific addresses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:00:43 +03:00
claude code agent 227
cedea4072b refactor(ai-chat)!: unify provider error formatting via describeProviderError
Behaviour change (split out of the test commit per review, and now covered).

Both the stream onError log line and the error text streamed to the client were
formatted by separate inline blocks that only emitted "<status>: <message>".
Route both through the shared describeProviderError() so formatting stays in one
place.

BEHAVIOUR CHANGE: describeProviderError additionally appends a single-line,
300-char-truncated snippet of the provider responseBody/text. So the log line
AND the user-facing stream error now include that snippet (e.g. the HTML error
page from a misconfigured endpoint), which previously neither did. This is
intentional — it makes a misconfigured external endpoint diagnosable — and is
safe: the API key travels in the Authorization header and is never echoed in
the response body (see the util's docstring). A `fallback` param is added so
each call site keeps its own default ('AI stream error' for the stream).

Adds ai-error.util.spec.ts covering the formatter, including the appended /
truncated body snippet, so this behaviour is no longer untested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:59:55 +03:00
claude code agent 227
1e650262a4 fix(ai-chat): record chats that fail on their first turn
Behaviour change (split out of the test commit per review).

In AI SDK v6 the useChat `onFinish` callback does NOT fire when the stream
errors. A brand-new chat whose very first turn fails would therefore never run
the post-turn path: the chat list was not invalidated and the client never
adopted the server-created chat id — so the failed chat only appeared in
history after a manual refresh (the server already creates the row and stores
the error message). Running the same `onTurnFinished()` handler on `onError`
makes the failed chat show up immediately. The error itself is still surfaced
to the user via the existing `error` state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:58:57 +03:00
claude code agent 227
f1980cf425 test(ai-chat): safety-critical coverage + a11y + pure refactors
Unit tests for the safety-critical paths: crypto secret-box (round-trip,
tamper detection, wrong key), the SSRF guard (blocked ranges + DNS-rebinding),
the ai-chat tools service, the page-embedding repo, and the
assistant-parts/serialization helpers. Those server helpers (assistantParts,
rowToUiMessage, serializeSteps) are exported ONLY for the tests — no runtime
change.

Also: keyboard a11y on the chat history header and conversation rows
(role/tabIndex/Enter+Space), and DRY refactors that move shared logic into one
place (isToolPart -> tool-parts util; buildInitialValues in the MCP form).

The behaviour-changing edits that previously rode along in this commit are
split out into the following two commits, per review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:58:44 +03:00
claude_code
965cbb32e5 Merge pull request 'feat(ai-chat): step cap 8→20 + forced final text answer' (#9) from feat/ai-chat-step-limit into develop 2026-06-20 17:47:37 +03:00
vvzvlad
0b969c8675 test(ai-chat): pin step-limit boundary + note AI SDK v7 system->instructions
Port two refinements from the GLM variant onto the Claude base:
- prepareAgentStep: add a comment note that AI SDK v7 renames the per-step
  `system` field to `instructions` (v6 ^6.0.134 still uses `system`), so it
  gets updated correctly on the next SDK bump.
- ai-chat.service.spec: add an explicit off-by-one boundary test for
  prepareAgentStep, expressed via MAX_AGENT_STEPS instead of a hardcoded 18/19
  so it tracks the constant if the cap changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:47:16 +03:00
claude_code
b20ffd1b91 Merge pull request 'feat(tree): Expand all / Collapse all for the space page tree' (#23) from feat/tree-expand-collapse-all-agent227 into develop 2026-06-20 17:40:29 +03:00
vvzvlad
949a251553 fix(tree): close the space menu after Expand all
Expand all kept the menu open (closeMenuOnClick={false}) while Collapse all
closed it. Make both close on click for consistent behavior, and drop the
now-pointless in-menu isExpanding loading state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:39:34 +03:00
vvzvlad
234ae759f5 refactor(tree): borrow cleanups from the sibling expand-all impl
- extract collectAllIds / collectBranchIds into tree/utils and use them in
  space-tree.tsx instead of inline closures
- drop the duplicate SidebarPageTreeDto, reuse the existing SidebarPageDto
  for the /pages/tree endpoint
- type the getSpaceTree client call as api.post<{ items: IPage[] }>

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:39:34 +03:00
claude_code
151bd7a0e0 Merge pull request 'feat(ai-settings): rebind endpoint status dot to configured x enabled' (#19) from feat/ai-endpoint-status-dot-config-enabled into develop 2026-06-20 17:22:22 +03:00
vvzvlad
689f435630 docs: remove implemented ai-endpoint-status-dot backlog plan
The configured x enabled status dot is implemented and merged via this
branch, so the backlog plan is no longer needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:22:03 +03:00
claude_code
1982ef0f23 Merge pull request 'feat(ai-settings): put Clear inside the API key field, drop the eye' (#20) from feat/api-key-clear-in-place-of-eye into develop 2026-06-20 17:18:54 +03:00
vvzvlad
4bfb143288 docs: remove implemented api-key-field-clear backlog plan
The in-field Clear for the API key fields is implemented and merged via
this branch, so the backlog plan is no longer needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:18:24 +03:00
claude_code
f8bb4b37ce Merge pull request 'feat(comments): denser comments panel' (#5) from feat/comments-panel-density into develop 2026-06-20 17:10:30 +03:00
claude code agent 180
d11cf0112f Merge branch 'feat/compact-page-tree-toggle-180' into develop
Gate page-tree row density behind the COMPACT_PAGE_TREE flag
(standard 32px default, compact 26px opt-in). Authored by the local
Claude agent on machine 180.
2026-06-20 16:59:43 +03:00
claude code agent 180
36ae4bd3d3 feat(page-tree): gate compact tree density behind COMPACT_PAGE_TREE flag
Make the denser page-tree layout opt-in instead of hardcoded, so row
density can be toggled per deployment via the COMPACT_PAGE_TREE runtime
config flag.

- doc-tree: extract ROW_HEIGHT_STANDARD (32) / ROW_HEIGHT_COMPACT (26);
  default the virtualizer row stride to STANDARD density.
- client: isCompactPageTreeEnabled() in lib/config (reads
  COMPACT_PAGE_TREE, default true); used by space-tree and shared-tree
  to choose the row height.
- server: EnvironmentService.isCompactPageTreeEnabled() and expose
  COMPACT_PAGE_TREE through the window runtime config (static.module).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:54:09 +03:00
claude code agent 227
be2530a0b9 chore(tree): document the restriction-cache primitive; drop dead notify code
Release-cycle audit flagged WsService.invalidateSpaceRestrictionCache and
WsTreeService.notifyPageRestricted/notifyPermissionGranted as never-wired dead
code. Investigation: this community fork has NO page-permission grant/revoke/
restrict mutation site (the page-access repo mutators have zero callers — that
flow is EE / not yet built), so there is nothing to wire them into.
- Keep invalidateSpaceRestrictionCache (it's the one-line correctness primitive
  the future permission-mutation path must call to avoid the 30s stale-cache
  window) but document exactly that + add a test that it deletes only the
  space-scoped cache key.
- Remove the untested, security-adjacent dead methods notifyPageRestricted /
  notifyPermissionGranted and their now-orphaned helpers emitToUsers /
  emitToSpaceExceptUsers (no remaining references; build confirms). A future
  permission-change realtime feature can reintroduce them wired + tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:50:53 +03:00
claude code agent 227
71fc58dbed harden(page-templates): throttle lookup/toggle; workspace-scope ref writes
Release-cycle review: POST /pages/template/lookup had only JwtAuthGuard and the
embed depth cap was client-only, so a scripted client could drive heavy
full-content fan-out (access control holds per-id, but a cost/DoS gap). And
page_template_references rows were written for any sourcePageId with no
workspace check at sync time (no leak today since lookup re-checks access, but
the graph could accumulate cross-space rows).

- Apply the standard per-user throttler (PAGE_TEMPLATE_THROTTLER, 30/min) to
  /pages/template/lookup and /pages/toggle-template (mirrors ai-chat); auth +
  the toggle's validateCanEdit CASL are unchanged.
- syncPageTemplateReferences / insertTemplateReferencesForPages now restrict
  inserts to in-workspace source ids (filterInWorkspaceSourceIds, workspace +
  not-deleted scoped, trx-aware) and still delete stale out-of-workspace rows
  (self-heal). SECURITY comment: the ref table is NOT access-filtered; every
  consumer must permission-filter at read time (as lookupTemplate does).
- Tests: lookup access exercises the REAL filterViewerAccessiblePageIds
  (no_access / cross-workspace excluded / accessible+comment-stripped / <=50);
  toggle controller CASL (cannot-edit -> Forbidden, flag not flipped); ref-sync
  excludes cross-workspace and keeps in-workspace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:16:15 +03:00
claude code agent 227
9aff427ad8 harden(public-share): sliding cluster-wide token cap; testable access seam
Release-cycle review: the per-workspace cost cap was fixed-window + per-instance
(allowed ~2x at a window boundary and K*cap behind K instances) on an anonymous
endpoint that spends the owner's provider budget. Rewrite it as a sliding-window,
CLUSTER-WIDE Redis limiter: one atomic Lua EVAL does ZREMRANGEBYSCORE (age out)
-> ZCARD -> ZADD with PEXPIRE, so concurrent instances share one budget and the
true rate over any trailing window is <= cap. Fails OPEN on a Redis error (logged)
— it's a cost backstop, not access control (the funnel gates + per-IP throttle
still apply), so a Redis blip must not take the assistant offline. Per-IP @Throttle
kept; commented that it needs an XFF-rewriting trusted proxy to be meaningful.

Extract deriveShareAccess (resolvedShareId===requestedShareId + isSharingAllowed +
!restricted, equality-only, never widening) and filterShareTranscript into pure
helpers, and add tests: limiter sliding-window + boundary-burst + fail-open;
access derivation; and red-team boundary locks (cross-share/cross-workspace swap
rejected, forged shareId can't widen tool scope, transcript injection filtered).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:04:26 +03:00
claude code agent 227
caac5c7f36 test(html-embed): exercise the REAL admin-gate write paths + import round-trip
Release-cycle test audit: the strip boundary was tested only via a stand-in
helper re-implemented in the spec, so a deleted/misplaced guard kept CI green
(the missing create() guard was proof). Replace it with tests against real code:
- persistence.extension.onStoreDocument: real ydoc from a rich doc (columns/
  table/mention/htmlEmbed) -> non-admin strip removes only htmlEmbed, every other
  node preserved (data-loss guard); admin keeps; empty fragment no-throw.
- collaboration.handler.updatePageContent: real path, user?.role gate, decoded
  ydoc embed-free for non-admin, kept for admin.
- transclusion unsync: member stripped, admin preserved.
- editor-ext gains a vitest setup (was zero tests) + a markdown round-trip:
  the <!--html-embed:BASE64--> marker -> htmlEmbed node with decoded source, and
  hasHtmlEmbedNode matches it — pinning the marked/turndown shape the import
  strip relies on. tsconfig now excludes specs from the shipped dist.
- Fail-closed identity: source-pinned contracts that the gate keys on
  fileTask.creatorId (zip) / request userId (single) / callerRole (create) /
  authUser.role (duplicate), and missing-user -> strip (services can't load under
  jest's ESM graph; helpers replay the exact predicate).
Adds the verified-safe ^src/ jest moduleNameMapper (identical fail set).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 14:52:29 +03:00
claude code agent 227
3672093f56 test(mcp): cover X-MCP-Token/clientIp/bearer-type/creds-failure (pure seams)
Release-cycle test audit: the /mcp auth's constant-time token guard, IP keying,
ACCESS-type pinning, and brute-force message coupling were untested. Extract
behavior-preserving pure helpers so they're testable and cover them:
- sharedTokenMatches: length-mismatch early-returns before timingSafeEqual
  (which throws on unequal lengths); equal-length uses timingSafeEqual; array
  header -> first element; non-string -> false.
- clientIp: req.ip > socket > first XFF hop > 'unknown' (limiter keying).
- bindAccessJwtVerifier: verifyJwt pinned to JwtType.ACCESS (rejects REFRESH).
- CREDENTIALS_MISMATCH_MESSAGE single source of truth shared by
  verifyUserCredentials and isCredentialsFailure, so a reworded auth error can't
  silently disable the /mcp brute-force counter.
- verifyUserCredentials no-side-effect contract asserted via a TS-AST spec
  (AuthService can't load under jest): its body has no createSessionAndToken/
  audit/updateLastLogin while login() has all three.
Extractions are behavior-preserving (reviewed); class delegates to the helpers,
dead code + unused imports removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 14:32:29 +03:00
claude code agent 227
20a1780977 test(ai-roles): cover role-resolution, CASL gate, model override; hide disabled badge
Release-cycle test audit found the role feature's security-critical paths
untested. Adds real unit tests (against the actual functions):
- resolveRoleForRequest invariants: role comes from chat.roleId not body.roleId
  (no per-turn swap), lookup scoped to workspace.id, disabled/soft-deleted role
  -> null, new-chat uses body.roleId, stale chatId falls back.
- CASL admin gate: non-admin create/update/delete -> Forbidden and service not
  called; admin delegates with workspace.id; list() is member-reachable.
- roleModelOverride: unknown driver dropped (never reaches getChatModel's
  throwing default), valid override passes through, blanks ignored.
- getChatModel override success path (cross-driver fetch + decrypt; chatModel-
  only reuse), and service update/remove cross-workspace 'not found' guards +
  modelConfig tri-state.
Tiny fix: findByCreator badge left-join now also requires enabled=true, so a
disabled role (downgraded to universal by resolveRoleForRequest) no longer shows
a misleading chat-list badge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 14:20:08 +03:00
claude code agent 227
cac7abc395 fix(ai-roles): guard update() re-fetch against concurrent soft-delete
Release-cycle review: update() re-read the role via findById (filters
deleted_at IS NULL) and passed it straight to toView(updated as AiAgentRole).
A concurrent soft-delete between the UPDATE and the re-fetch makes findById
return undefined, and toView(undefined) dereferences row.id -> opaque 500. Add
the same 'Role not found' guard remove() already uses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 14:03:03 +03:00
glm5.2 agent 180
4430784094 docs: remove implemented comments-panel-density backlog plan 2026-06-20 14:03:02 +03:00
glm5.2 agent 180
680995247a feat(comment): tighten the comments panel density
The Comments panel was sparse: 12px inner/outer paddings per thread, a
16px gap between avatar and body, body text at the global 16px ProseMirror
size. On a narrow aside column this ate vertical space - few comments per
screen, lots of air.

Tighten strictly inside features/comment (the shared aside frame is left
untouched, so TOC/Details tabs keep their padding):

- Thread Paper: p='sm'->p='xs', mb='sm'->mb='xs' (12->10px).
- Reply-editor Divider: my={4}->my={2}.
- CommentListItem outer Box: pb='xs'->pb={6}; the header Group
  (avatar + body) gains gap='xs' (16->10px).
- Font hierarchy: author name sm->xs (14->12px, fw=500 kept), selection
  quote sm->xs; comment body via a scoped CSS override on
  .commentEditor .ProseMirror: font-size sm (14px) + line-height 1.4,
  margin-top 10->4. The page editor is unaffected (the override is
  scoped to the comment editor module).
- Selection quote padding 8->6, margin-top 4->2.
- Dropped the unused .wrapper rule (no references).
2026-06-20 14:02:40 +03:00
claude code agent 227
5d5f61fc6e fix(tree): place remote moves by position; remove stale node on move-into-restricted
Release-cycle review found two move-path issues:
- Remote moves were placed at index:0 (broadcastPageMoved hardcodes index:0),
  so every observer rendered the moved node at the TOP of its new siblings
  until refetch. Client moveTreeNode now places by fractional position
  (treeModel.placeByPosition, mirroring addTreeNode/insertByPosition) and
  applies the payload's pageData (title->name, icon, hasChildren) so receivers
  keep the node correct.
- Moving a page under a restricted ancestor left a stale named node (title/
  slugId/icon) in the trees of users who lost visibility. broadcastPageMoved
  now derives one FRESH hasRestrictedAncestor decision and drives both paths
  from it: when restricted, the move goes to authorized users only
  (emitToAuthorizedUsers, not the space-cache-gated emitTreeEvent) and a
  compensating deleteTreeNode goes to the unauthorized complement (same fresh
  getUserIdsWithPageAccess set) — disjoint, no stale-cache window. Non-restricted
  moves are unchanged (one moveTreeNode to the room).

Follow-up (noted): invalidateSpaceRestrictionCache is still unwired at
permission-mutation sites; the open-space fast path can lag up to the 30s TTL,
but the move/delete consistency above no longer depends on it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 14:01:37 +03:00