Перейти к содержимому

SECU-9: CleanSlice foundation (0.1.0) ​

Задача SECU-9, эпик SECU-1. Исходная Securify main: 74d188b. Предметные функции других эпиков сюда не входят.

Проверенный источник ​

Официальный CleanSlice Starter Kit, commit d86224a261ec366831167c76da74830f0fd4e0b8. Репозиторий указан в официальной организации CleanSlice. Источник получен напрямую через Git, не через сторонний генератор. Commit фиксирует использованный снимок; это не обещание отслеживать HEAD автоматически.

Сначала вызван CleanSlice MCP get_started: он вернул пустой шаблон. list_categories работает; поиск starter kit не дал результатов. Прочитаны docs/standards/nuxt.md и 02-standards/boundary-check.md через read_doc. Источник starter установлен независимо от неисправного вводного документа MCP.

Что перенесено и адаптировано ​

UpstreamSecurifyАдаптация
app/registerSlices.tsapp/, admin/, website/Тот же рекурсивный поиск Nuxt layers; пути относительно файла, сортировка, необязательный setup; пример user не нужен
app/nuxt.config.tsТри Nuxt приложенияextends: registerSlices(); сохранён Nuxt 4; devtools выключены; SSR оставлен для проверяемых HTML страниц
app/slices/common/nuxt.config.tsslices/common каждого frontendAlias #common; явный srcDir и импорт config для Nuxt 4 typecheck
api/tsconfig.jsonscanner/tsconfig.jsonCommonJS, ES2022, Nest decorators; aliases групп scanner (SECU-22)
api/nest-cli.jsonscanner/nest-cli.jsonКомпиляция Nest без Swagger plugin
api/scripts/cleanslice-check.cjsscanner/scripts/cleanslice-check.cjsПобайтовая копия; конфигурация групп отдельно
Тот же upstream boundary checkerСуществующий api/scripts/cleanslice-check.cjsУже побайтово совпадает; сохранён с существующей конфигурацией пяти групп

admin и website — отдельные экземпляры адаптированной Nuxt основы. Scanner — дополнительный Nest application context, а не готовый worker из starter. В его setup/runtime пока только минимальный Nest module; bootstrap выводит готовность каркаса и закрывает контекст. Никаких результатов сканирования он не создаёт.

Upstream содержит демонстрационные user/auth, Prisma/PostgreSQL, HTTP/OpenAPI, генерацию SDK, Pinia, Tailwind и библиотеку UI. Они не перенесены: предметные функции, БД и авторизация вне SECU-9; существующий backend остаётся MCP stdio. UI каркасов не требует state management или SDK. Декораторы и DI frontend пока не нужны; сохранены актуальные Nuxt project references существующего app.

Сохранена Node.js 22.x baseline Securify, хотя README upstream рекомендует 24+. Проверки выполняются с Node.js 22.23.1 и Bun 1.3.14. Каждый проект имеет собственный lockfile; версии из нового разрешения зависимостей фиксируются в нём. Root package.json остаётся только для Git hooks. VitePress и его landing не заменены.

Границы проверок ​

make check-all: существующие API unit tests, TypeScript и upstream boundary check; Nuxt typecheck трёх frontend; TypeScript и upstream boundary check scanner. Checker проверяет направление групп, циклы и зависимости слоёв backend; он не доказывает корректность DI или предметной логики. Nuxt typecheck не является проверкой архитектурных границ frontend. Singular-названия slices сохраняются при review; начальные frontend layers называются common, scanner — setup/runtime (группы scanner с SECU-22 — ниже).

make build-all собирает все шесть приложений. make smoke после сборки проверяет MCP handshake и список существующих инструментов, отдельный запуск scanner, HTTP-страницы трёх Nuxt приложений и основные маршруты docs. Процессы smoke завершаются автоматически, никакие внешние ресурсы не создаются.

Отклонения слоя данных (SECU-11) ​

Prisma/PostgreSQL добавлены в API позже по решению G01, с отличиями от starter:

  • Используется Prisma 8 (RC: [email protected], @prisma/[email protected]), а не Prisma 7 из starter. В api/package.json overrides выравнивают @prisma/orm-toolchain и @prisma/orm-framework с версией orm-postgres: без этого CLI падает на CONTRACT.PACK_CONTRIBUTION_INVALID.
  • Модели (// use prisma-8) лежат не в корне слайса, как требует starter, а в подслайсе, чей gateway их читает: user/user/user.prisma, user/session/session.prisma, team/team/teamMember.prisma, project/project/project.prisma. Модели, у которых кода ещё нет, лежат в папке будущего подслайса (team/apiKey/, team/invitation/, team/idempotency/). Правило starter нужно для относительных импортов prisma-import; здесь api/prisma.config.ts собирает файлы glob src/slices/**/*.prisma, импортов между ними нет, и место файла на контракт не влияет. prisma-import не нужен, отдельного datasource/generator-файла нет.
  • prisma contract emit (postinstall, build, typecheck, test) пишет contract.json и contract.d.ts в api/generated/prisma: вне src/ и вне Git, чтобы сгенерированный код не проверяли checker и eslint. Отдельного шага генерации клиента нет.
  • Runtime Prisma 8 — только ESM, а сборка Nest — CommonJS. tsconfig использует module/moduleResolution: nodenext: настоящий import() сохраняется, и Node 22 загружает ESM из CommonJS. В src из Prisma импортируются только типы; PrismaService загружает runtime и temporal-polyfill лениво, только при заданном DATABASE_URL. Поэтому MCP stdio и unit-тесты стартуют без базы, а test:db запускает jest с --experimental-vm-modules.
  • Время — Temporal.Instant (lib: esnext.temporal, полифил в runtime).
  • PrismaService не наследует клиент, а владеет им. Gateways читают db — ORM текущей транзакции внутри transaction() или корневого клиента. Вложенная транзакция присоединяется к внешней. Уровень изоляции задаётся SET TRANSACTION, предел времени — transaction_timeout (PostgreSQL 17). Конфликт сериализации или deadlock (SQLSTATE 40001/40P01 в sqlState) повторяется.
  • Миграции — TypeScript-планы prisma migration plan в api/prisma/migrations/. CHECK и частичный уникальный индекс заданы в PSL (@@check, @@index(where:)). То, что PSL не выражает (отложенный FK, constraint-триггеры), добавлено операциями rawSql с pre/postcheck. prisma db verify сверяет базу с контрактом.
  • Мапперы следуют паттерну CleanSlice: toData (строка → домен), toCreate (ICreate{Entity}Data → вход create), toUpdate (IUpdate{Entity}Data → вход update). Gateway оставляет себе только условия where. Prisma 8 не генерирует DB.User и Prisma.UserCreateInput/UpdateInput, поэтому псевдонимы I{Entity}Response/CreateRequest/UpdateRequest выводятся из коллекции помощниками PrismaResponse/PrismaCreateRequest/PrismaUpdateRequest (setup/prisma). Отсутствующее поле update — undefined, Prisma его пропускает.

Общий слой прав (SECU-12) ​

  • setup/access — матрица прав G01, AccessPolicyService (решение о доступе без ввода-вывода, внедряется через DI) и AccessService, который выполняет операцию только после проверки и записи в журнал audit_event. Каждая операция и каждый MCP-инструмент объявляют access; без него TypeScript не соберёт код, а assertGuarded остановит регистрацию. В starter такого слоя нет.
  • Файлов *.policy.ts нет: в CleanSlice бизнес-правила домена живут в сервисах. Политика пароля — в PasswordService.assertAcceptable, решение о доступе и матрица ролей G01 — в AccessPolicyService (accessPolicy.service.ts). Других нестандартных суффиксов (.matrix, .resolver, .token, .validation, .bootstrap) тоже нет: сбор принципала — PrincipalService, правила полей проекта — ProjectService, DI-токены — в *.types.ts. MCP stdio — presentation-слой McpController (setup/mcp/mcp.controller.ts), который передаёт вызовы доменному McpToolService (проверка доступа, request-ID, ошибка → isError).
  • setup/request — request-ID в AsyncLocalStorage за абстрактным IRequestContextGateway (глобальный RequestModule). Logger добавляет ID сам, toErrorResponse получает его от транспорта. Входящий ID принимается только по шаблону.
  • Журнал audit_event — только добавление: триггеры (rawSql) отклоняют UPDATE/DELETE/TRUNCATE.

Вход, команды и проекты (SECU-13) ​

  • user/user, user/auth, team/team, team/access, project/project — слайсы G01 §6. user/auth разбит на подслайсы по сущностям: user/session, user/emailToken, user/loginAttempt, user/password и user/secretToken. Правило, которым владеет подслайс, живёт в его сервисе (CleanSlice 03-patterns/service.md: движок — в слайсе, который владеет возможностью): SessionService — выдача, проверка (idle/absolute expiry, lastSeenAt) и отзыв сессий; EmailTokenService — ссылки подтверждения и лимит писем в час; LoginAttemptService — лимиты попыток по логину и IP; PasswordService — политика, хеш и проверка с фиктивным хешем, чтобы время ответа не выдавало несуществующий логин. AuthService только оркестрирует регистрацию, подтверждение и вход: какое письмо отправить и какую единую ошибку вернуть. Сессии team/team и team/access берут напрямую у SessionService. Gateway подслайсов наружу не экспортируются, только сервисы. Операции домена без транспорта: HTTP-контроллеров и OpenAPI из starter по-прежнему нет, stdio MCP их не вызывает.

  • setup/mail — SMTP через nodemailer за абстрактным IMailGateway; без APP_URL, SMTP_URL, MAIL_FROM регистрация отказывает.

  • Хеш пароля — @node-rs/argon2 (Argon2id, нативная сборка), а не bcrypt из демонстрационного user/auth starter.

  • В domain user/auth только типы, ошибки, абстрактный gateway писем и сервис. Случайные секреты (node:crypto) — за ISecretTokenGateway в user/secretToken, тексты писем — за IAuthMailGateway; реализации в data/. Нормализация email, логина и имени — методы UserService. Настройки IAuthSettings — абстрактный класс и сам себе DI-токен ({ provide: IAuthSettings, useFactory: loadAuthSettings }).

  • Ошибки слайса — по файлу на класс в domain/errors/{name}.error.ts с errors/index.ts, как в CleanSlice 03-patterns/error.md.

  • data/mock.gateway.ts — in-memory gateways для unit-тестов (@type:mock) в слайсе, которому принадлежит gateway; user/auth/data/mock.gateway.ts собирает из них AuthService и сервисы подслайсов. TEST_AUTH_SETTINGS лежит в setup/config/data/mock.gateway.ts. Исключены из сборки в tsconfig.build.json, runtime их не импортирует. Так же исключены тестовые данные *.fixtures.ts (например, setup/access/domain/access.fixtures.ts). Только эти тестовые файлы импортируют чужой data/mock.gateway напрямую, минуя index.

  • Остальные слайсы приведены к тем же правилам:

    • Repository самодостаточен. connector/data/repositories и scan/data/repositories описывают свои типы (remoteSite.types.ts, scanner.types.ts) и не импортируют domain. Gateway переводит их через ConnectorMapper / ScanMapper. Ошибка подключения из repository — RemoteConnectionError; gateway превращает её в доменную ConnectionFailedError. Вспомогательные функции repository (safeEntry, downloadLimits, walkFiles, fsProbe) лежат рядом с ними.
    • Mapper в каждом data-слое с gateway: AuditMapper (toData/toCreate/toUpdate), ConnectorMapper, ScanMapper, WorkspaceMapper.
    • MCP-инструменты — presentation. Шесть инструментов лежат в audit/tools/*.tool.ts: они проверяют вход и вызывают AuditService, в том числе testConnection. Zod-схема ответа getFindings — audit/dtos. McpModule.register({ imports, tools }) из setup/mcp собирает их под токеном MCP_TOOLS; domain setup/mcp от zod не зависит.
    • Domain без библиотек и инфраструктуры. Domain-сервисы зависят от абстракций: ILoggerGateway (setup/logger/domain; реализация LoggerGateway пишет JSON в stderr) и IRequestContextGateway (setup/request/domain; RequestContextGateway на AsyncLocalStorage). В setup/error классы ошибок и коды лежат в domain/, а toErrorResponse — в корне слайса (presentation).
    • Окружение читает только setup/config/data. Типы настроек — абстрактные классы в setup/config/domain (ILimits, IAuthSettings, IMailSettings, IWorkspaceSettings), они же DI-токены. load* из data/loadSettings.util.ts подключаются фабриками модулей. Глобальной константы LIMITS нет: ConnectorGateway и ScanGateway получают ILimits через DI. Repository коннектора получают лимиты в запросе (IRemoteLimits в remoteSite.types.ts) и не импортируют setup/config. DATABASE_URL по-прежнему читает PrismaService.
    • Mapper без исключений. EmailTokenMapper и LoginAttemptMapper переводят даты в Temporal.Instant и собирают запросы create/update; в gateway остаются только условия where. Типы подслайсов — в *.types.ts (emailToken.types.ts, loginAttempt.types.ts, IVerifiedLink в auth.types.ts).
    • Операции — по файлу на класс: project/project/domain/operations/ (createProject.operation.ts …, база projectResource.operation.ts, PROJECT_OPERATIONS в index.ts).
    • DTO инструментов. audit/dtos/ с index.ts: входы (auditIdInput.dto.ts, siteConnectionInput.dto.ts) и ответ (auditResult.dto.ts). Схемы MCP-инструментов не изменились.
    • Repository: {name}.repository.ts → {Name}Repository (FtpRepository, SftpRepository, OpenCartRepository, WordPressRepository, SecretsRepository, SuspiciousCodeRepository). Multi-provider токены SCANNER_REPOSITORIES и MCP_TOOLS объявлены в scan.types.ts и mcpTool.types.ts, как TOOL_REPOSITORIES в примере gateway CleanSlice.
    • Кросс-слайс импорт — через index (#connector/index, #scan/index, #workspace/index, #audit/index). Имена файлов — camelCase (mcpTool.service.ts, cmsDetector.util.ts); интерфейсы — с префиксом I (IFinding); gateway пароля — IPasswordGateway / PasswordGateway.
  • Сознательные отклонения:

    • Проверка входа в domain. ProjectService (project/project/domain) проверяет поля внутри операций, а не в DTO presentation. Операция (IGuardedOperation, SECU-12) получает сырой input от любого транспорта, и AccessService записывает отказ валидации в журнал как failed. Перенос в DTO возможен, когда появится HTTP-транспорт.
    • Детекторы CMS (scan/data/cms/*.util.ts) — функции data, а не repository: они только читают файлы и вызываются из ScanGateway.
    • Подслайсы team/apiKey, team/invitation, team/idempotency — только модели Prisma: gateway, domain и module появятся с задачами, которые их используют.

Команды setup, отдельные порты и запуск описаны в runbook.

Каркас ядра анализатора (SECU-22) ​

Scanner готовится к ядру анализатора (SECU-19) без логики анализа. Группы setup → archive → rule → analysis в scanner/cleanslice.config.cjs: setup/config — стартовые лимиты распаковки G04 (4 GiB, 256 MiB на файл, 200 000 записей, глубина 64, путь 1024 байта, сжатие 200:1 при записи > 1 MiB); analysis/job — строгая проверка входного job.json от trusted worker (schemaVersion: 1, UUID snapshotId/attemptId, archiveSha256 в нижнем регистре, необязательные limits поверх значений по умолчанию; лишние ключи — ошибка).

archive/extraction (SECU-23) — потоковая распаковка ZIP-снимка на yauzl 3 без вызова unzip: сверка SHA-256 архива, проверки имён, лимиты по записанным байтам, хеши файлов. Контракт входа и результата — contracts/operations.md. Тестовый генератор ZIP zip.fixture.ts исключён из сборки (tsconfig.build.json).

rule/static (SECU-24) — статические правила Securify, перенесённые из api/src/slices/scan без импорта из api/: инвентаризация, определение WordPress/OpenCart, подозрительный PHP, секреты, правила WP uploads и OpenCart. Работает только по списку файлов из результата распаковки; исполняемость — по Unix-режиму записи архива; .env выбирается по имени. Regex-правила читают файл до 5 MiB, больший или нечитаемый — в coverage.skipped. Свидетельство — строка и короткий очищенный фрагмент; у секрета только строка. api/ не менялся: stdio MCP работает на своих правилах.

analysis/report (SECU-25) — команда analyze: задание, распаковка, сверка с манифестом (analysis/manifest, временный формат до T014), правила и отчёт v1 в /output/report.json. Форма как у rule/static: в domain/ интерфейс gateway, типы, сервис и ошибка; в data/ gateway, mapper и репозитории reportFile (атомарная запись) и reportSchema (JSON Schema v1, ajv); контроллер команды зовёт только сервис. Контракт отчёта, итогов и кодов выхода — contracts/operations.md. Так же устроены analysis/job (репозитории jobFile и jobSchema) и archive/extraction (репозитории zipArchive и treeFile; проверки имён — доменный EntryPathService): ошибки — по одной в domain/errors/, коды ошибок, остановки и пропуска — enum.

node dist/main.js без аргументов печатает ready. С путём к job.json задача только проверяется: accepted в stdout или код выхода 2 с причиной в stderr. analyze пишет отчёт: выход 0, 3 (выполнение не удалось) или 1 (отчёта нет). Jest + ts-jest в scanner, как в API: bun run test, входит в make check-all; make smoke проверяет отказ неверного job.json и отчёт analyze с кодом 3.

Результаты проверки реализации ​

Проверено на Node.js 22.23.1 / Bun 1.3.14:

  • make setup дважды в чистой временной копии исходников без node_modules и корневого .env: успешно, порты после повторного запуска не изменились.
  • make doctor, make ports, make fixtures-config: успешно; Compose не изменялся.
  • node --test scripts/workspace.test.mjs: успешно; новые настройки, идемпотентность, резервации неактивного соседнего worktree, миграция старых настроек и отказ принимать настройки чужого checkout.
  • make check-all: успешно; API — 26 suites / 86 tests, typecheck и 5 групп / 83 модуля boundary check; три Nuxt typecheck; scanner typecheck и boundary check.
  • make build-all: успешно для api, app, admin, website, scanner, docs.
  • make smoke: успешно; scanner readiness/exit, MCP initialize/ping и 6 инструментов, HTML трёх Nuxt приложений, docs /, /documentation.html, /architecture/v0.1.0.html, /infrastructure.html, /cleanslice-foundation.html, /market/.
  • Chromium/Playwright: три Nuxt страницы, существующий landing и обе затронутые docs страницы при 1440×900 и 390×844; без ошибок JavaScript и горизонтального переполнения. Desktop/mobile screenshots просмотрены для app и runbook.
  • git diff --check: успешно. Исходники API, .do/app.yaml, существующий landing и referance/ не изменены. Merge и production deploy не выполнялись.

Securify · Документация проекта · Прототип → MVP