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.
Что перенесено и адаптировано
| Upstream | Securify | Адаптация |
|---|---|---|
app/registerSlices.ts | app/, admin/, website/ | Тот же рекурсивный поиск Nuxt layers; пути относительно файла, сортировка, необязательный setup; пример user не нужен |
app/nuxt.config.ts | Три Nuxt приложения | extends: registerSlices(); сохранён Nuxt 4; devtools выключены; SSR оставлен для проверяемых HTML страниц |
app/slices/common/nuxt.config.ts | slices/common каждого frontend | Alias #common; явный srcDir и импорт config для Nuxt 4 typecheck |
api/tsconfig.json | scanner/tsconfig.json | CommonJS, ES2022, Nest decorators; aliases групп scanner (SECU-22) |
api/nest-cli.json | scanner/nest-cli.json | Компиляция Nest без Swagger plugin |
api/scripts/cleanslice-check.cjs | scanner/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.jsonoverridesвыравнивают@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собирает файлы globsrc/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. Правило, которым владеет подслайс, живёт в его сервисе (CleanSlice03-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, как в CleanSlice03-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; domainsetup/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.
- Repository самодостаточен.
Сознательные отклонения:
- Проверка входа в 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 появятся с задачами, которые их используют.
- Проверка входа в domain.
Команды 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 не выполнялись.