feat: mass-parallel implementation of 25 features#1
Conversation
Implement zero-downtime migration strategy with health checks, migration tracking, and automatic rollback support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add configurable rate limiting guard using ValKey for distributed rate tracking, with decorator-based configuration per endpoint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Enable custom URL aliases for notes with uniqueness validation, slug generation, and database migration for the alias table. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add soft-delete trash bin for notes with restore capability and a BullMQ processor for automatic purge after retention period. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Support duplicating notes with configurable options for metadata, attachments, and child notes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add ZIP export with preserved folder hierarchy and Obsidian vault import with frontmatter parsing and wikilink resolution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add vector-based semantic search using OpenAI embeddings stored in PostgreSQL with pgvector for similarity queries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement background image processing with WebP/AVIF conversion, responsive variants, and on-demand serving with cache headers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add templated email system with Handlebars for welcome, password reset, share invites, comment mentions, and notification digests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add comprehensive audit trail for workspace admin operations including member management, settings changes, and role updates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add granular permission scopes, key rotation, expiration policies, and usage tracking for programmatic API access. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement workspace webhook subscriptions with event filtering, secret signing, delivery tracking, and retry logic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement configurable horizontal rule node with inline style variants and toolbar button integration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement inline formatting marks for superscript and subscript with keyboard shortcuts and toolbar buttons. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add web app manifest, service worker with cache-first strategy, and offline fallback page for progressive web app support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add togglable focus mode with dimmed UI, centered editor, and Zustand store for focus state management. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add TipTap extension for drag-and-drop file handling with visual drop zone overlay and automatic attachment upload. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add TipTap node extension for inline Mermaid diagram rendering with syntax validation and error display. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add print-optimized view with dedicated CSS, header/footer control, and usePrint hook for triggering browser print dialog. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add theme store with light/dark/system modes and ThemeToggle component with smooth transitions and preference persistence. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add draggable, resizable floating window system with Zustand store for window state management and z-index layering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add breadcrumb component showing note path with clickable parent links and overflow truncation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add chronological timeline of note edits with TanStack Query integration and infinite scroll pagination. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add keybinding settings UI with conflict detection, reset to defaults, and enhanced keyboard shortcuts provider. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cover theme preference persistence, system preference detection, and mode switching logic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Register migration, rate-limiting, trash, alias, image optimization, email templates, semantic search, and webhook services in their respective NestJS modules. Update Prisma schema and barrel exports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update barrel exports, NoteEditorPage, Ribbon, and WorkspaceShell to wire in focus mode, print view, floating windows, breadcrumbs, timeline, theme toggle, and new editor extensions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add sharp, handlebars, pgvector, and other dependencies required by new backend services. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove unused Logger import, fix type export, and update test mock to avoid TypeScript strict-mode violations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix $transaction mock type by using (prisma.$transaction as any) to avoid Prisma's overloaded signature mismatch in test helper - Split ValidatedUserApiKey into export type to satisfy isolatedModules - Remove unused Logger import and declaration in ApiKeyOrJwtGuard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update task statuses reflecting completion of parallel agent batch runs (batch-001, batch-002, batch-003) across frontend and backend features. Phase: Post-Implementation Sync Workflow: mass-parallel-001/002/003
ZeiZel
left a comment
There was a problem hiding this comment.
Ревью PR #1: feat/mass-parallel-003 → main
Объём: 152 файлов, +22,894 / -341 строк, 32 коммита, ~700 тестов
Общее впечатление
Масштабный PR, добавляющий 25 фич: rate limiting, webhooks, trash/recycle bin, semantic search (pgvector), note aliases, image optimization, health checks, email templates, focus mode, floating windows, theme toggle, breadcrumb navigation, timeline view, drag-and-drop upload, keyboard shortcuts, mermaid preview, print view, PWA, editor extensions (horizontal rule, superscript, subscript) и другие.
Код в целом высокого качества: грамотная архитектура, TypeScript strict compliance, Zod для валидации, Prisma для доступа к БД, корректные миграции, хорошее покрытие тестами.
IMPORTANT (не блокируют мёрж, но стоит исправить в следующем PR)
1. NoteAliasService — прямой new PrismaClient() вместо DI
Файл: apps/server/src/modules/notes/note-alias.service.ts:31
private readonly prisma = new PrismaClient();Создаёт отдельный connection pool, обходит общие middleware/logging PrismaService. Паттерн уже есть в 5 других файлах проекта, но стоит мигрировать на инъекцию через конструктор.
2. Отсутствуют barrel exports для новых editor extensions
Файл: libs/editor-core/src/index.ts
Три новых расширения (horizontal-rule, superscript, subscript) и их 4 компонента (HorizontalRuleButton, HorizontalRuleView, SuperscriptButton, SubscriptButton) не экспортированы из barrel-файла. Потребители за пределами библиотеки не смогут их импортировать.
3. TrashListQueryDto — нет class-validator декораторов
Файл: apps/server/src/modules/notes/trash.controller.ts:22-33
DTO TrashListQueryDto использует только Swagger-декораторы, но не class-validator (@IsOptional, @IsInt, @Min, @Max). Параметры cursor и limit принимаются без серверной валидации типов (только ручной Number(query.limit)).
4. Service worker кэширует API-ответы
Файл: apps/web/public/sw.js:83
Все GET-запросы /api/* кэшируются в API_CACHE. Потенциально могут кэшироваться чувствительные данные. Рекомендуется добавить исключения для /api/auth/* и /api/*/webhooks/*/secret.
5. emptyTrash загружает все trashed notes в память
Файл: apps/server/src/modules/notes/trash.service.ts:205-230
Для workspace с тысячами trashed notes — потенциальная проблема памяти. Практически ограничено 30-дневным auto-purge, но для enterprise-сценариев стоит рассмотреть batch processing.
MINOR (для информации)
- Двойной ThemeToggle: компонент добавлен и в
Ribbon.tsx, и вWorkspaceShell.tsx— возможно, by design. FocusModeStore: правильно НЕ персиститisFocusModeв localStorage — хороший паттерн.- Semantic search fallback: корректный graceful degradation на FTS при отсутствии OPENAI_API_KEY.
- Rate limiter: Lua-скрипт для sliding window — идиоматичная реализация, fail-open при недоступности ValKey.
- Миграции (0013-0015): безопасные ADD COLUMN, CREATE INDEX, CREATE EXTENSION IF NOT EXISTS. Нет потери данных.
- Webhook RBAC: корректно — ADMIN/OWNER для write-операций, VIEWER+ для read.
- pgvector queries: используют Prisma tagged templates (
$queryRaw), параметризованы — SQL injection невозможен.
Безопасность ✅
- Нет SQL injection (все raw queries параметризованы через Prisma tagged templates)
- Нет XSS (Ant Design + React escaping)
- Нет hardcoded secrets (OpenAI key из env)
- Auth guards работают глобально через
APP_GUARD(ApiKeyOrJwtGuard) - Rate limiting с Lua atomic operations предотвращает race conditions
- Webhook secrets хранятся как HMAC hashes, raw secret показывается только при создании
- Image optimizer использует sharp library (не shell commands) — нет command injection
Архитектура ✅
- Frontend: FSD compliance, Zustand только для business logic, Ant Design
- Backend: Clean Architecture, модули NestJS правильно подключены
- Prisma schema расширен корректно, миграции безопасны
Вердикт: APPROVE ✅
Блокирующих проблем нет. 5 IMPORTANT issues стоит адресовать в follow-up PR.
Follow-up: дополнительные находки ревьюCRITICAL: XSS в MermaidPreview.tsxФайл: Mermaid инициализируется с Исправление: Изменить IMPORTANT: javascript: URL injection в PrintView.tsxФайл: Markdown-to-HTML конвертация вставляет Исправление: Добавить валидацию URL протокола перед вставкой в href/src. IMPORTANT: EditorDropZone.tsx — сломанный uploadФайл: Ручная установка Исправление: Убрать ручной Content-Type header, позволить axios установить его автоматически. IMPORTANT: KeybindingsSettings — нет Zod валидации импортаФайл: Импорт JSON клавиатурных сокращений использует Рекомендую создать follow-up issue для этих 4 находок, особенно XSS. |
🔍 Code Review — PR #1: mass-parallel implementation of 25 features
CRITICAL (5 проблем — требуют немедленного исправления)🔴 C1. XSS через Mermaid
|
| # | Проблема | Файл |
|---|---|---|
| I1 | NoteAliasController не валидирует input через Zod — DTO без декораторов | note-alias.controller.ts:~90 |
| I2 | Webhook delivery limit (?limit=) не валидируется — parseInt('abc') = NaN |
workspace-webhooks.controller.ts:~180 |
| I3 | TrashController limit → Number('abc') = NaN → Math.min(NaN,100) = NaN |
trash.controller.ts:~81 |
| I4 | Race condition в moveToTrash: два параллельных вызова проходят isTrashed check |
trash.service.ts:~85-100 |
| I5 | TOCTOU в restoreFromTrash: collision check отделён от DB update, файл может осиротеть |
trash.service.ts:~106-140 |
| I6 | Import: unbounded rename loop (note (1).md → note (2).md → ...) без лимита |
import.service.ts |
| I7 | NoteAliasController и TrashController — нет @UseGuards, полагаются на global guard (проверить!) |
Оба контроллера |
| I8 | MigrationService: execSync(command) с path из env var — использовать execFile с массивом аргументов |
migration.service.ts:~123 |
Frontend (9)
| # | Проблема | Файл |
|---|---|---|
| I9 | FSD violation: shared/ импортирует из features/ |
KeyboardShortcutsProvider.tsx:13-17 |
| I10 | FSD violation: прямой import из features/workspace/ui/ вместо barrel |
NoteEditorPage.tsx:22 |
| I11 | Дубликат ThemeToggle — в Ribbon и WorkspaceShell одновременно, две разные реализации | Ribbon.tsx:177, WorkspaceShell.tsx:352 |
| I12 | Raw HTML <button> вместо Ant Design Button в FloatingWindow |
FloatingWindow.tsx:60-77 |
| I13 | Raw HTML элементы в NoteBreadcrumb вместо Ant Design | NoteBreadcrumb.tsx:260-269 |
| I14 | Raw HTML элементы в TimelineView NoteCard | TimelineView.tsx:134-183 |
| I15 | Raw HTML в OfflineFallback вместо Ant Design | OfflineFallback.tsx:96-179 |
| I16 | Raw HTML кнопки в EditorDropZone upload panel | EditorDropZone.tsx:222-246 |
| I17 | usePrint.isPrinting читает из ref — не реактивен, UI не обновится |
use-print.ts:166-169 |
Editor-Core (4)
| # | Проблема | Файл |
|---|---|---|
| I18 | Новые расширения НЕ экспортируются из index.ts — код недостижим через @notesaner/editor-core |
editor-core/src/index.ts |
| I19 | Атрибут style в HR extension конфликтует с HTML style — переименовать в hrStyle |
horizontal-rule.ts:169 |
| I20 | parseHTML для subscript матчит ЛЮБОЙ элемент с [data-subscript] — слишком широко |
subscript.ts:147 |
| I21 | parseHTML для superscript матчит ЛЮБОЙ элемент с [data-superscript] |
superscript.ts:140 |
MODERATE (25 проблем — краткий список)
| # | Проблема | Файл |
|---|---|---|
| M1 | Webhook update использует raw SQL вместо Prisma | webhook.service.ts:~195 |
| M2 | ImageOptimizer хранит абсолютные пути в БД (непереносимо) | image-optimizer.processor.ts:~95 |
| M3 | Email templates: readFileSync блокирует event loop (хоть и в onModuleInit) |
email-templates.service.ts:~160 |
| M4 | Embedding cache TTL 7 дней без инвалидации при смене модели | embedding.service.ts:~14 |
| M5 | Неиспользуемый импорт ALIAS_REGEX |
note-alias.service.ts:~5 |
| M6 | TrashController DTOs без validation decorators | trash.controller.ts:~25-37 |
| M7 | Export controller — breaking API change (смена base path) | export.controller.ts:~19 |
| M8 | eslint-disable для non-null assertion вместо type narrowing | NoteBreadcrumb.tsx:186 |
| M9 | injectPrintStylesheet — dead code (создаёт link но не добавляет в head) |
use-print.ts:72-87 |
| M10 | navigator.platform deprecated — 3 файла |
use-print.ts:207, keyboard-shortcuts.ts:399,437 |
| M11 | Race condition при multi-file drop — все файлы вставляются в одну позицию | EditorDropZone.tsx:393-399 |
| M12 | URL строится конкатенацией без encodeURIComponent |
timeline.ts:83 |
| M13 | resolveHrStyle не вызывается в renderHTML addAttributes — нет defense-in-depth |
horizontal-rule.ts:173 |
| M14 | Тесты sub/superscript проверяют литералы сами на себя (zero value) | subscript.test.ts:~533, superscript.test.ts |
| M15-M25 | Мелкие: redundant contentEditable, as any casts (established pattern), inline styles в editor (OK), стартовая theme flash, deprecated API, отсутствие component tests |
— |
Приоритеты для исправления
Блокеры (fix ASAP):
- C1 — XSS через Mermaid →
securityLevel: 'strict'+ DOMPurify - C5 — Upload сломан → убрать явный
Content-Type - C2 — Неуправляемый PrismaClient → инжектировать PrismaService
- C3 — Rate limiter обходится → использовать
req.ip
Высокий приоритет:
- I18 — Barrel exports для editor-core (код недостижим!)
- I4/I5 — Race conditions в trash service → транзакции
- I7 — Проверить покрытие global auth guard
- C4 — Валидация vector-литерала
Средний приоритет:
- I9/I10 — FSD violations
- I1-I3, I6 — Валидация на границах (Zod)
- I11 — Консолидация ThemeToggle
- I12-I16 — Ant Design вместо raw HTML
Вердикт: Код содержит 5 критических проблем (XSS, auth bypass potential, сломанный upload, неуправляемый DB connection). Рекомендую создать фикс-PR для C1-C5 и I18 до деплоя в продакшен.
🤖 Generated with Claude Code
Follow-up #2: Находки безопасности бэкендаCRITICAL1. SSRF через URL вебхуковФайлы:
Fix: 2. Обход rate limit через X-Forwarded-ForФайл: Для неаутентифицированных запросов middleware вручную парсит Fix: Использовать 3. Path traversal в trash serviceФайл:
Fix: Проверка IMPORTANT4. Webhook signature — ручная реализация вместо crypto.timingSafeEqualФайл: XOR-цикл не гарантированно constant-time в V8. 5. API key rate limit — неатомарный INCR + EXPIREФайл: Crash между 6. fromPrismaScope — тихий fallback на READФайл: Новый scope в Prisma enum → маппится как READ. Потенциальная эскалация привилегий. 7. Webhooks разрешают HTTPФайл: HMAC-подписанные payload'ы передаются по plaintext HTTP. Утечка содержимого заметок. 8. Отсутствует ParseUUIDPipeФайлы: UUID-параметры принимаются как raw string без валидации → 500 вместо 400 на невалидном вводе. 9. listDeliveries — NaN propagationФайлы:
Рекомендация: Создать P0 issue для SSRF (#1) и path traversal (#3), P1 для rate limit bypass (#2). |
ZeiZel
left a comment
There was a problem hiding this comment.
Code Review — feat/mass-parallel-003 → main
153 файла, +22 894 / −341 строк, 25 фич
Критические (блокирующие)
(не обнаружено)
Важные
1. Raw SQL в webhook.service.ts для обычного CRUD (нарушение coding standards)
update() и findByIdWithStats() используют $queryRaw для стандартных SELECT/UPDATE. По стандартам проекта raw SQL разрешён только для FTS/pgvector. Следует переписать на Prisma ORM API.
2. parseInt(limit, 10) без валидации в WorkspaceWebhooksController.listDeliveries
При ?limit=abc вернёт NaN. Заменить на @Query('limit', new ParseIntPipe({ optional: true })) limit?: number.
3. Отсутствие FK на rotated_to_id (migration 0015)
Целостность только на уровне приложения. Самоссылочный FK корректен и рекомендован.
4. Email-шаблоны: тройные скобки без валидации протокола URL
{{{noteUrl}}} отключает HTML-escaping. При отсутствии проверки протокола — риск javascript: XSS. Добавить валидацию http(s):// в EmailTemplatesService.
Минорные
sendTest: 202 ACCEPTED, но@ApiOkResponse→ заменить на@ApiAcceptedResponsepublic/sw.jsсоздан без регистрации в приложении — PWA незавершенаdemoNotePathв NoteEditorPage — явный WIP, принятоcountWordsдублирован в тесте вместо импорта
Безопасность
✅ HMAC webhooks, ✅ атомарная ротация ключей, ✅ pgvector parameterized, ✅ SW same-origin only
Тестирование
Выдающееся покрытие — каждый новый сервис/процессор имеет unit-тесты.
Вердикт: APPROVE — критических проблем нет. Замечания 1-4 в следующий PR.
ZeiZel
left a comment
There was a problem hiding this comment.
Обзор PR: feat/mass-parallel-003
Ветка содержит значительный объём работы: health-check на основе миграций, ротация API-ключей, sliding-window rate limiting, webhook CRUD, семантический поиск через pgvector, Service Worker (PWA). Код хорошо документирован и покрыт тестами.
🔴 Проблемы безопасности
1. IP spoofing в rate-limit middleware (средняя серьёзность)
apps/server/src/common/middleware/rate-limit.middleware.ts
X-Forwarded-For берётся без проверки доверенных прокси — первый IP из заголовка используется как идентификатор:
const ip = forwarded.split(',')[0];
return `ip:${ip.trim()}`;Атакующий может выставить произвольный X-Forwarded-For: 1.2.3.4 и обойти IP-based rate limiting. Решение: доверять заголовку только при запросах с известного прокси-адреса, либо использовать Express trust proxy и опираться на req.ip.
2. Service Worker кеширует API-ответы без учёта смены сессии
apps/web/public/sw.js
Стратегия networkFirstWithCache для /api/* не учитывает logout / token rotation. При инвалидации токена закешированные ответы могут вернуться из браузерного кеша. Нужно либо исключить аутентифицированные эндпоинты, либо добавить Vary: Authorization на уровне сервера.
3. Нет cross-field валидации: OPENAI_API_KEY обязателен при openai-провайдере
apps/server/src/config/validation.ts
OPENAI_API_KEY: z.string().optional() — при дефолтном EMBEDDING_PROVIDER=openai ключ необходим, но валидация молчит. Сервис упадёт в runtime при первом embedding-запросе. Стоит добавить .superRefine() в Zod-схему.
🟡 Качество кода
1. Приведение к any в SubscriptButton (libs/editor-core/src/components/SubscriptButton.tsx)
(editor.commands as any).toggleSubscript?.();Нарушает strict mode. Правильно: editor.chain().focus().toggleSubscript().run() (стандартный TipTap chain API).
2. Service Worker без TypeScript
apps/web/public/sw.js написан на plain JS при строгом TypeScript во всём проекте. Если ограничение намеренно — добавить комментарий. Иначе переработать в sw.ts с компиляцией.
3. Отсутствует клемпинг query param limit в WebhooksController
Документировано «1–200», но parseInt(limit, 10) без ограничений позволяет -1 и 9999. Нужен @Min(1) @Max(200) через class-validator.
4. execSync с npx в MigrationService
npx prisma migrate deploy в Docker медленнее и зависит от поиска пакета. Предпочтительнее ./node_modules/.bin/prisma или абсолютный путь.
5. Отсутствие FK на rotated_to_id (migration 0015)
Комментарий ссылается на «circular dependency», но самореференциальный FK в PostgreSQL не создаёт проблем. Ссылочная целостность на уровне приложения слабее. Рекомендую добавить FK.
🟡 Нарушение архитектурных директив (CLAUDE.md)
SubscriptButton.tsx, SuperscriptButton.tsx, HorizontalRuleView.tsx используют raw <button> с inline-стилями. Директива CLAUDE.md: «No raw HTML elements — use Box component». Компоненты editor-core переиспользуются в web-приложении — стандарт должен соблюдаться.
✅ Что сделано хорошо
- MigrationService + MigrationHealthIndicator — fail-fast на bootstrap, корректный readiness probe для Kubernetes, 18 unit-сценариев.
- Sliding-window rate limiting — атомарный Lua-скрипт, fail-open при недоступности ValKey, нормализация UUID в ключах маршрутов, раздельные лимиты для auth.
- API key rotation — транзакционная замена (create + revoke в
$transaction), audit trail черезrotatedToId, atomic increment для requestCount. - pgvector migrations — корректный IVFFlat с
lists=100, CASCADE delete, уникальность one-per-note. - WebhookService — delivery stats через одну агрегирующую SQL-запрос с FILTER, хорошая декомпозиция.
- Тестовое покрытие — rate-limit, migration, webhook, API key — все ключевые пути покрыты.
ZeiZel
left a comment
There was a problem hiding this comment.
Code review complete. See detailed findings below.
ZeiZel
left a comment
There was a problem hiding this comment.
🔍 Code Review — PR #1: feat/mass-parallel-003
Scope: 153 файла, ~22 900 строк, 25 фич
Вердикт:
🔴 CRITICAL — блокируют мёрж
1. Command Injection в import.controller.ts
apps/server/src/modules/notes/import.controller.ts:376
execSync(\`unzip -q -o "\${zipPath}" -d "\${targetDir}"\`);`zipPath` приходит из Multer — может содержать `"`, `$`, обратные кавычки. Двойные кавычки не защищают от всех метасимволов shell.
Фикс: заменить на `execFileSync('unzip', ['-q', '-o', zipPath, '-d', targetDir])` — без интерполяции в shell.
2. Path Traversal в import.service.ts
`apps/server/src/modules/notes/import.service.ts:191`
`resolvedPath` берётся из имён файлов внутри загруженного архива. Злонамеренный архив с путём `../../etc/passwd` или `../other-workspace/secret.md` позволяет читать/писать за пределами workspace. `path.join()` не предотвращает path traversal.
Фикс: после `join()` проверять `absolutePath.startsWith(workspaceDir + path.sep)`.
3. XSS через Mermaid — securityLevel: 'loose' + dangerouslySetInnerHTML
`apps/web/src/features/editor/ui/MermaidPreview.tsx:233,293`
`libs/editor-core/src/components/MermaidView.tsx:348,389`
Mermaid инициализируется с `securityLevel: 'loose'`, что отключает встроенную санитизацию HTML в SVG. Результат инжектится через `dangerouslySetInnerHTML`. Пользователь может выполнить произвольный JS.
Фикс: `securityLevel: 'strict'` или `'sandbox'`. Если нужны интерактивные элементы — пропускать SVG через DOMPurify.
4. XSS через oEmbed HTML без санитизации
`libs/editor-core/src/components/ExternalEmbedView.tsx:366`
HTML от oEmbed-провайдеров вставляется через `dangerouslySetInnerHTML` без санитизации. oEmbed-спецификация допускает произвольный HTML.
Фикс: пропускать через DOMPurify с белым списком тегов (`iframe`, `img`, `blockquote`).
5. Race Condition в rate limiting API-ключей
`apps/server/src/modules/auth/api-keys/api-key.service.ts:385-398`
`checkRateLimit` использует отдельные команды `INCR` и `EXPIRE`. Между ними возможен crash (вечная блокировка) или concurrent reset окна.
Фикс: атомарный Lua-скрипт для INCR+EXPIRE.
6. Direct PrismaClient вне DI-контейнера
`apps/server/src/modules/notes/note-alias.service.ts:31`
`new PrismaClient()` создаёт отдельный пул соединений вне NestJS lifecycle. Утечка соединений.
Фикс: инжектировать `PrismaService` через конструктор.
🟠 HIGH — серьёзные проблемы
| # | Файл | Проблема |
|---|---|---|
| 7 | `trash.controller.ts` | Нет guard авторизации — любой аутентифицированный пользователь может удалять заметки в чужих workspace |
| 8 | `audit.service.ts:157` | `exportCsv` загружает ВСЕ audit-записи в память — OOM для активных workspace |
| 9 | `storage-quota.service.ts:343` | Загрузка полного текста ВСЕХ версий в память для подсчёта размера — нужен SQL aggregate |
| 10 | `export.service.ts:199` | N+1 запросы в batch export — каждая заметка = отдельный запрос |
| 11 | `PrintView.tsx:220` | XSS через `javascript:` URLs в markdown-ссылках — экранирование до конвертации не защищает |
| 12 | `drag-handle.ts:322` | `moveBlock` заменяет ВЕСЬ документ — уничтожает Yjs CRDT-состояние и курсоры коллабораторов |
| 13 | `mermaid-block.ts` | `atom: true` + `content: 'text*'` — противоречие, ProseMirror не позволит редактировать |
| 14 | `FloatingWindow.tsx:388` | Resize handles — `pointer-events-none` на обёртке, handles нельзя схватить мышью |
| 15 | `typography-enhanced.ts:258` | En-dash replacement вставляет литеральные `$1` и `$2` в текст |
🟡 MEDIUM (19 issues)
| # | Файл | Проблема |
|---|---|---|
| 16 | `import.controller.ts:140` | 500MB upload без проверки workspace quota |
| 17 | `webhook.controller.ts` | Отсутствует `ParseUUIDPipe` на id-параметрах |
| 18 | `api-key.service.ts:453` | `fromPrismaScope` тихо маппит неизвестные scope → READ |
| 19 | `import.service.ts:641` | Самодельный YAML-парсер — хрупкий, использовать пакет `yaml` |
| 20 | `import.service.ts:1080` | Unbounded цикл переименования конфликтов |
| 21 | `search-replace.service.ts:414` | User regex без ReDoS-защиты |
| 22 | `migration.service.ts:178` | `execSync` со строковой интерполяцией env-переменной |
| 23 | `storage-quota.service.ts:200` | Non-atomic read-then-write при конкурентных удалениях |
| 24 | `css-sanitizer.ts:45` | CSS-санитайзер на blocklist — обходится через `u\rl()` |
| 25 | `FloatingWindowsLayer.tsx:54` | SSR/hydration mismatch — `typeof document` в render |
| 26 | `KeyboardShortcutsProvider.tsx` | FSD violation: shared импортирует из features |
| 27 | `KeybindingsSettings.tsx:59` | Cross-feature импорт нарушает FSD |
| 28 | `EditorDropZone.tsx:54` | Hardcoded API URL, raw axios вместо shared apiClient |
| 29 | `editor-core/index.ts` | Множество расширений не экспортируются из barrel |
| 30 | `codeBlock` extension | Имя конфликтует с встроенным TipTap CodeBlock |
| 31 | `vim-mode.ts:833` | Vim state в closure — не переживает undo/redo |
| 32 | `toggle-list.ts` | Экспорт `ToggleList`, name `'toggleBlock'` — несоответствие |
| 33 | Дупликация `tryRequire` | import.controller + export.service — извлечь в common/utils |
| 34 | Raw HTML elements | EditorDropZone, FloatingWindow, OfflineFallback — нужен Box/Ant Design |
🟢 LOW / NITPICK (15 items)
- `MermaidPreview.tsx` — `resolvedTheme` отсутствует в deps useEffect
- `FocusMode.tsx:317` — `countWords` без useMemo
- `TimelineView.tsx:103` — Intl formatters создаются на каждый вызов
- `NoteBreadcrumb.tsx:186` — non-null assertion + eslint-disable
- `floating-windows-store.ts:152` — stale `window.innerWidth` в Zustand action
- `sw.js` — API_CACHE растёт бесконечно
- `EditorDropZone.tsx:96` — `Date.now()` для ID → лучше `crypto.randomUUID()`
- `MermaidView.tsx:386` — `mermaid.initialize()` на каждый рендер
- `heading-fold.ts:156` — O(n×m) сложность computeFoldRange
- `CalloutBlockView.tsx:256` — state update в render body
- Тесты FocusMode/use-print дублируют production-функции
- `block-reference.ts:122` — modulo bias в generateBlockId
- `external-embed.ts` — `fetch()` вместо `axios`
- `HighlightMenu/Subscript/SuperscriptButton` — `as any` при наличии type augmentation
- `audit.service.ts:264` — GDPR SAR ограничен 500 записями
✅ Что сделано хорошо
- API-ключи: SHA-256, prefix-based lookup, scope hierarchy, rotation с audit trail
- Webhook HMAC: constant-time comparison, GitHub-style подпись
- Trash lifecycle: soft-delete → FS move → collision detection → cron purge
- Zod validation на system boundaries
- Cursor-based pagination везде
- Fail-open для некритичных операций
- BullMQ processors — чистый паттерн
- Lazy loading тяжёлых зависимостей (KaTeX, Mermaid, lowlight)
- Accessibility: role, aria-*, keyboard navigation выше среднего
- Zustand stores: devtools + persist + partialize
- TanStack Query: useInfiniteQuery с query key factory
- Privacy-first: ExternalEmbed загружает контент только после согласия
Итог
PR содержит 6 критических проблем безопасности (command injection, path traversal, XSS ×3, race condition) и 1 критическое нарушение архитектуры (PrismaClient вне DI). Рекомендуется исправить все CRITICAL и HIGH перед мёржем.
Summary
25 features implemented in parallel by specialized agents across backend, frontend, and editor layers:
Backend (13 features)
Frontend (9 features)
Editor Extensions (3 features)
Test Coverage
~700+ new unit tests across all features
Test plan
pnpm nx affected -t testto verify all new tests passpnpm nx affected -t lintto check for lint issues🤖 Generated with Claude Code