fix: review/red-team batch 2 — 30 issues (security, ws, page-templates, html-embed, mcp, tests, docs) #101

Merged
vvzvlad merged 32 commits from fix/review-batch-2 into develop 2026-06-21 05:47:05 +03:00

Прогон по второй пачке ревью/ред-тим issues (47 шт) на смерженном develop. Одна ветка от develop, по коммиту на issue (29 коммитов), каждое код-изменение прошло ревью-сабагента; верифицировано-перед-фиксом (часть issues оказалась уже сделана прошлыми PR — закрыта отдельно с ссылкой на коммит).

Closes #52, #53, #54, #55, #56, #60, #61, #62, #63, #64, #66, #67, #68, #70, #71, #72, #75, #78, #83, #84, #85, #87, #88, #89, #90, #91, #92, #93, #94, #95

Что сделано (30 issues)

RT / безопасность (анонимный share-AI, auth, mcp, ws):

  • #60 maxOutputTokens на анонимном стриме; #62 limiter fail-closed при Redis-сбое; #63 reject non-text parts (обход size-cap); #95 describeProviderError на анонимном пути + единый live-enabled резолв роли (findLiveEnabled).
  • #70 null-password (SSO/LDAP-only) аккаунты: без bcrypt-throw (500→401, fix утечки + обход brute-лимитера); #83 check-then-act гонка brute-лимитера /mcp закрыта атомарным tryReserve (+ release на ранних throw, чтобы SSO/MFA/конфиг-ошибки не жгли бюджет жертвы); #61 trustProxy env-настраиваемый с безопасным дефолтом.
  • #64 PAGE_MOVED только при реальном изменении строки; #67 серверная проверка цикла в movePage; #66 ресинк дерева на reconnect; #72 реалтайм rename/иконка (server-authoritative PAGE_UPDATED, restriction-aware); #53 TTL restriction-кэша 30→3с (нет вызывающего инвалидации — флоу ограничений пока недостижим).
  • #68 SAFETY-сэндвич вокруг персоны роли.

page-templates / transclusion / html-embed:

  • #55 depth-cap коллекторов (анти stack-overflow); #54 документировано, что lookup плоский (рекурсии нет); #71 удалён мёртвый isPageEmbedCycle/TooDeep; #78 поправлен врущий коммент кодека; #90 общий тестируемый хелпер admin-gate strip (5/7 call-site, 2 с иной семантикой оставлены); #92 единый resolveReadableSharePage (нашёл и закрыл недостающую deletedAt-проверку в воронке); #94 коллекторы → collectNodes, write-only граф задокументирован.
  • #52 валидация chatModel + contract-тест дрейфа драйверов.

Тесты / инфра / доки:

  • #56 включены 16 отключённых серверных сьютов (lib0 ESM transform + DI-специ); #75 sidebar-tree-спека тестирует реальный код; #85 позитивная ветка getSharePage; #88 happy-path ai-roles update; #91 coupling-тест login↔enforceBasicLoginGate; #87 share-виджет показывает реальную ошибку (describeChatError).
  • #84 doc breaking-change X-MCP-Token + one-time warning; #89 синк AGENTS.md/README.

Косяки, пойманные ревью (и доделанные)

  • #83: первая ��ерсия не освобождала резервацию на SSO/MFA-throw → атакующий мог исчерпать per-email бюджет жертвы без bcrypt. Доделал release на всех ранних throw + регресс-тесты.
  • #92: воронка не имела явной deletedAt-проверки (латентно безопасной) → унифицировал к строгому поведению.

Проверка

Полный серверный сьют: 70 suites, 721 tests — зелёные. Server + client tsc --noEmit — exit 0.

Закрыто отдельно (вне этого PR — уже в develop)

17 issues закрыты комментарием со ссылкой на смерженный коммит: #26,27,29,44,65,69,73,74,76,77,79,80,81,82 (уже сделаны прошлыми PR), #86 (покрытие уже есть), #96,#97 (зонтики — дети закрыты).

НЕ вошло (блокировано)

#98, #99, #100 — тест-покрытие + 2 бага (BUG-1 $&-hazard в инъекции trackerHead, BUG-2 height NaN) для рефактора sandbox html-embed (commit 81823fce), которого НЕТ в develop (он в отдельной несмерженной ветке). С ветки от develop их сделать нельзя — они должны лечь на/после мержа sandbox-рефактора. Флагнул.

🤖 Generated with Claude Code


Обновление: смержен текущий develop (полный sandbox html-embed) + добавлено тест-покрытие

После того как в develop приехал sandbox-рефактор html-embed (commit 81823fce, «перешли полностью на песочницу»), влил develop в эту ветку и разрулил 13 конфликтов:

  • Sandbox убрал ВСЮ ролевую обвязку strip у html-embed на write-путях → мой #90 и html-embed-части устарели → взял версию develop. (#90 фактически resolved-by-removal: дублирующегося гейта больше нет.)
  • develop независимо реализовал #60/#62 → взял develop (эквивалентно).
  • Сохранил мои валидные фиксы (movePage #64/#67, title/icon #72, sidebar #75, transclusion collectors #94, share-AI #95 describeProviderError + findLiveEnabled, mcp #83 и т.д.).
  • Скомбинировал доки (мой #84 Breaking + #89 sync с sandbox CHANGELOG/README/AGENTS), поправил неверное «admin-gated».
  • Подобрал коммит bc0c49db (dead-DI #92 + docs).

Добавил тест-покрытие, заведённое ревью PR #101 и sandbox-рефактора:
Closes #98, #99, #100, #102, #103, #104, #105, #106, #108, #109

  • #98/#99/#100 — sandbox/trackerHead: injectTrackerHead ($&-инвариант), sandbox-токены (allow-scripts allow-popups allow-forms, нет allow-same-origin, srcDoc), clampHeight/isTrustedHeightMessage, height-codec (NaN-guard), DTO-валидация, CASL-гейт, mcp round-trip, slash-меню. (Оба бага BUG-1/BUG-2 уже были починены в sandbox-рефакторе — добавил тесты, пиннящие инварианты.)
  • #102 movePage cycle, #103 non-text 400, #104 (уже было), #105 resolveTrustProxy export+test, #106 reconnect-resync, #108 assistant-name, #109 ru-RU typing-ключи.
  • #107 закрыт отдельно (нельзя переписывать смерженную историю).

Полный прогон после всего: server 69 suites / 689 tests, client 20 files / 240 tests, server+client tsc — чисто.

Прогон по второй пачке ревью/ред-тим issues (47 шт) на смерженном develop. Одна ветка от develop, **по коммиту на issue** (29 коммитов), каждое код-изменение прошло ревью-сабагента; верифицировано-перед-фиксом (часть issues оказалась уже сделана прошлыми PR — закрыта отдельно с ссылкой на коммит). Closes #52, #53, #54, #55, #56, #60, #61, #62, #63, #64, #66, #67, #68, #70, #71, #72, #75, #78, #83, #84, #85, #87, #88, #89, #90, #91, #92, #93, #94, #95 ## Что сделано (30 issues) **RT / безопасность (анонимный share-AI, auth, mcp, ws):** - #60 `maxOutputTokens` на анонимном стриме; #62 limiter fail-closed при Redis-сбое; #63 reject non-text parts (обход size-cap); #95 describeProviderError на анонимном пути + единый live-enabled резолв роли (findLiveEnabled). - #70 null-password (SSO/LDAP-only) аккаунты: без bcrypt-throw (500→401, fix утечки + обход brute-лимитера); #83 check-then-act гонка brute-лимитера /mcp закрыта атомарным `tryReserve` (+ release на ранних throw, чтобы SSO/MFA/конфиг-ошибки не жгли бюджет жертвы); #61 trustProxy env-настраиваемый с безопасным дефолтом. - #64 PAGE_MOVED только при реальном изменении строки; #67 серверная проверка цикла в movePage; #66 ресинк дерева на reconnect; #72 реалтайм rename/иконка (server-authoritative PAGE_UPDATED, restriction-aware); #53 TTL restriction-кэша 30→3с (нет вызывающего инвалидации — флоу ограничений пока недостижим). - #68 SAFETY-сэндвич вокруг персоны роли. **page-templates / transclusion / html-embed:** - #55 depth-cap коллекторов (анти stack-overflow); #54 документировано, что lookup плоский (рекурсии нет); #71 удалён мёртвый isPageEmbedCycle/TooDeep; #78 поправлен врущий коммент кодека; #90 общий тестируемый хелпер admin-gate strip (5/7 call-site, 2 с иной семантикой оставлены); #92 единый `resolveReadableSharePage` (нашёл и закрыл недостающую deletedAt-проверку в воронке); #94 коллекторы → `collectNodes`, write-only граф задокументирован. - #52 валидация chatModel + contract-тест дрейфа драйверов. **Тесты / инфра / доки:** - #56 включены 16 отключённых серверных сьютов (lib0 ESM transform + DI-специ); #75 sidebar-tree-спека тестирует реальный код; #85 позитивная ветка getSharePage; #88 happy-path ai-roles update; #91 coupling-тест login↔enforceBasicLoginGate; #87 share-виджет показывает реальную ошибку (describeChatError). - #84 doc breaking-change X-MCP-Token + one-time warning; #89 синк AGENTS.md/README. ## Косяки, пойманные ревью (и доделанные) - #83: первая ��ерсия не освобождала резервацию на SSO/MFA-throw → атакующий мог исчерпать per-email бюджет жертвы без bcrypt. Доделал release на всех ранних throw + регресс-тесты. - #92: воронка не имела явной deletedAt-проверки (латентно безопасной) → унифицировал к строгому поведению. ## Проверка Полный серверный сьют: **70 suites, 721 tests — зелёные**. Server + client `tsc --noEmit` — exit 0. ## Закрыто отдельно (вне этого PR — уже в develop) 17 issues закрыты комментарием со ссылкой на смерженный коммит: #26,27,29,44,65,69,73,74,76,77,79,80,81,82 (уже сделаны прошлыми PR), #86 (покрытие уже есть), #96,#97 (зонтики — дети закрыты). ## НЕ вошло (блокировано) **#98, #99, #100** — тест-покрытие + 2 бага (BUG-1 `$&`-hazard в инъекции trackerHead, BUG-2 height NaN) для рефактора **sandbox html-embed (commit `81823fce`)**, которого НЕТ в develop (он в отдельной несмерженной ветке). С ветки от develop их сделать нельзя — они должны лечь на/после мержа sandbox-рефактора. Флагнул. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- ## Обновление: смержен текущий develop (полный sandbox html-embed) + добавлено тест-покрытие После того как в develop приехал sandbox-рефактор html-embed (commit 81823fce, «перешли полностью на песочницу»), влил develop в эту ветку и разрулил 13 конфликтов: - Sandbox убрал ВСЮ ролевую обвязку strip у html-embed на write-путях → мой #90 и html-embed-части устарели → взял версию develop. (#90 фактически resolved-by-removal: дублирующегося гейта больше нет.) - develop независимо реализовал #60/#62 → взял develop (эквивалентно). - Сохранил мои валидные фиксы (movePage #64/#67, title/icon #72, sidebar #75, transclusion collectors #94, share-AI #95 describeProviderError + findLiveEnabled, mcp #83 и т.д.). - Скомбинировал доки (мой #84 Breaking + #89 sync с sandbox CHANGELOG/README/AGENTS), поправил неверное «admin-gated». - Подобрал коммит bc0c49db (dead-DI #92 + docs). Добавил тест-покрытие, заведённое ревью PR #101 и sandbox-рефактора: **Closes #98, #99, #100, #102, #103, #104, #105, #106, #108, #109** - #98/#99/#100 — sandbox/trackerHead: injectTrackerHead ($&-инвариант), sandbox-токены (allow-scripts allow-popups allow-forms, нет allow-same-origin, srcDoc), clampHeight/isTrustedHeightMessage, height-codec (NaN-guard), DTO-валидация, CASL-гейт, mcp round-trip, slash-меню. (Оба бага BUG-1/BUG-2 уже были починены в sandbox-рефакторе — добавил тесты, пиннящие инварианты.) - #102 movePage cycle, #103 non-text 400, #104 (уже было), #105 resolveTrustProxy export+test, #106 reconnect-resync, #108 assistant-name, #109 ru-RU typing-ключи. - #107 закрыт отдельно (нельзя переписывать смерженную историю). Полный прогон после всего: server 69 suites / 689 tests, client 20 files / 240 tests, server+client tsc — чисто.
Ghost added 29 commits 2026-06-21 04:19:32 +03:00
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>
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>
The widget hardcoded a generic 'Something went wrong' body and ignored
error.message, violating AGENTS.md. Render describeChatError(error.message, t) —
the same helper the internal chat uses — so a reader sees the real 402/429/503
cause instead of a bare 'try again'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
#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>
#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>
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>
The encode catch comment promised 'fall back to raw' but the code returns '';
returning raw source wouldn't help anyway (un-encoded markup can't be atob-decoded
downstream, so decode would yield '' regardless), and a raw value in data-source
breaks the inert-storage guarantee. '' is the correct decode-symmetric failure —
fix the misleading comment to say so. Adds a codec test for the encode-throw path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After a merge decideEmbedState became the canonical guard and inlines the
cycle/too-deep logic, leaving these predicates called only by their own tests.
Remove them (and their test blocks); keep PAGE_EMBED_MAX_DEPTH (used by
decideEmbedState). Production behavior stays covered by decide-embed-state.test.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WS events missed during a disconnect (wifi blip, sleep) were lost, so the
sidebar tree silently diverged until a manual reload. On RECONNECT (not the
first connect) invalidate the root-sidebar-pages + sidebar-pages queries so the
tree refetches through the authorized API and re-converges.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Fix doc drift: /mcp per-user auth + X-MCP-Token (was 'service account + optional
MCP_TOKEN'); CI builds :develop on push to develop (was main); add
page_template_references to the fork-tables list + is_template schema; mark
arbitrary HTML embed as shipped (was in-progress plan); remove the dead
page-templates-plan.md README link and move Page templates to implemented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
emitTreeEvent and emitCommentEvent were byte-identical (same room resolution,
spaceHasRestrictions gate, hasRestrictedAncestor, authorized-only vs broadcast
fallback). Collapse the body into one private emitRestrictedAwareToSpace; both
stay thin wrappers with unchanged signatures, so the restriction-routing gate
lives in exactly one place. Adds coverage for the comment entry point.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
resolveRoleForRequest and resolveShareRole duplicated the security invariant
'role exists, not soft-deleted, enabled, workspace-scoped, else null'. Move it to
AiAgentRoleRepo.findLiveEnabled(id, workspaceId) (deletedAt IS NULL + enabled +
workspace scope) and have both services call it, preserving each one's roleId
derivation + null handling. (describeProviderError half of #95 was done earlier.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The '(shareId,pageId) -> usable non-restricted page in THIS share' boundary was
written as 3 must-be-identical async sequences. They weren't: the chat funnel
omitted an explicit page.deletedAt check (latently safe via getShareForPage's
CTE) and layered isSharingAllowed separately. Add ShareService.resolveReadable-
SharePage(shareId,pageId,workspaceId) running the single canonical sequence
(getShareForPage -> id match (skipped when null) -> findById -> !deletedAt ->
!hasRestrictedAncestor) returning {share,page}|null; getSharedPage, the funnel,
and the getSharePage tool all use it. hasRestrictedAncestor now lives in the one
method no caller can skip; the funnel still returns uniform 404s and keeps
isSharingAllowed. Adds a direct security-invariant test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The three collect*FromPmJson collectors shared the same recursion (and the #55
depth cap) but were copy-pasted, so a future edit could diverge them. Extract a
generic collectNodes(doc, {type, map, key, lastWins, skipChildrenOfType}) and
reimplement all three on it, byte-output-identical (transclusions last-wins;
references/embeds first-wins + transclusionSource skip). Documents (not removes)
the write-only page_template_references graph and the near-duplicate client
lookup-context as tracked follow-ups, per the issue's guidance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(mcp): close the brute-force limiter check-then-act race (#83)
Some checks failed
Test / test (pull_request) Has been cancelled
a20f4c3876
isBlocked was checked synchronously but recordFailure ran only AFTER the bcrypt
awaits, so N concurrent /mcp Basic requests for one email all slipped past the
threshold. Add FailedLoginLimiter.tryReserve (atomic synchronous check+increment)
+ release (undo), and reserve all 3 keys BEFORE any await so the (threshold+1)-th
concurrent attempt is rejected before its bcrypt runs. The reservation IS the
failure record (post-await recordFailure removed -> counted exactly once). Non-
credential early throws (missing workspace, SSO/MFA gate) and business errors
release the reservation so they don't burn a victim's budget; success clears.
Tests prove login() runs exactly threshold times under concurrency and that
gate/config rejects don't consume budget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vvzvlad added 1 commit 2026-06-21 05:24:26 +03:00
fix(review): address PR #101 review findings (dead DI, docs)
Some checks failed
Test / test (pull_request) Has been cancelled
bc0c49db05
- ai-chat: drop the unused pagePermissionRepo injection from
  PublicShareChatToolsService (its only use moved into
  ShareService.resolveReadableSharePage); update all 5 positional
  test construction sites to match the 3-arg constructor.
- env: correct the anonymous share-AI per-workspace cap comment —
  the limiter FAILS CLOSED on Redis failure (#62), not open.
- docs: sync README.ru.md with README.md — move "Page templates"
  from Planned to Done and drop the dead plan-doc link.

Remaining test-coverage gaps tracked as #102, #103, #104, #105, #106.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ghost added 2 commits 2026-06-21 05:34:48 +03:00
# Conflicts:
#	AGENTS.md
#	CHANGELOG.md
#	README.md
#	apps/server/src/collaboration/collaboration.handler.ts
#	apps/server/src/common/helpers/prosemirror/html-embed.spec.ts
#	apps/server/src/common/helpers/prosemirror/html-embed.util.ts
#	apps/server/src/core/ai-chat/public-share-chat.service.ts
#	apps/server/src/core/ai-chat/public-share-chat.spec.ts
#	apps/server/src/core/ai-chat/public-share-workspace-limiter.ts
#	apps/server/src/core/page/services/page.service.ts
#	apps/server/src/core/page/transclusion/transclusion.service.ts
#	apps/server/src/integrations/import/services/file-import-task.service.ts
#	apps/server/src/integrations/import/services/import.service.ts
# Conflicts:
#	.env.example
#	README.ru.md
vvzvlad merged commit 084eafd0bb into develop 2026-06-21 05:47:05 +03:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#101