diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..aaab495 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +# AGENTS.md + +## Назначение + +Krona Staff — автономно развёртываемый блок Krona Business Platform, а не отдельный конкурирующий продукт. Предметные функции добавляются только по подтверждённым требованиям владельца. + +## Неизменяемые инварианты + +- Core владеет Tenant, User, Role, Permission, SSO, platform settings, files, notifications и event bus. Staff владеет Employee и HR-доменом. `User != Employee`. +- Все tenant-данные имеют `tenant_id`; запросы идут через tenant-scoped transaction; новые таблицы включают и форсируют PostgreSQL RLS. Добавляйте тест утечки между двумя tenant. +- ID — ULID в `VARCHAR(26)`. Деньги — `BIGINT` minor units + ISO 4217 currency. Временные метки — UTC; локальная интерпретация — IANA timezone tenant. +- Межмодульные вызовы только через публичные порты/platform gateway или versioned events. Прямые импорты и запись в чужие таблицы запрещены. +- События имеют version, tenant, subject, actor, correlation/causation; публикация через transactional outbox; consumer через inbox и идемпотентность. +- Локальный диск не является постоянным хранилищем. Вложения — через `FileStoragePort`/platform Files/S3. +- Standalone adapters совместимы, мигрируемы и отключаемы; они не становятся вторым постоянным source of truth. + +## Перед изменением + +Прочитайте `docs/109-PLATFORM-BLOCK-CONTRACT.md` и связанные актуальные контракты в `ochenstarik/business-platform`. Если контракт изменился, сначала синхронизируйте manifest/ADR и тесты. Не копируйте архитектуру Omnichannel механически. + +## Проверка + +Запускайте `npm run check`. Для изменений БД дополнительно проверьте up/down на чистой PostgreSQL и tenant isolation. Не объявляйте Docker/restore/CI проверенными, если соответствующая среда реально не запускалась. + +Файлы UTF-8 без BOM, LF. Коммиты небольшие, объясняют причину. Ветка задачи: `codex/bootstrap-platform-foundation` или отдельная ASCII feature-ветка. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dc2bf5a --- /dev/null +++ b/LICENSE @@ -0,0 +1,5 @@ +Copyright (c) 2026 Krona. All rights reserved. + +This software and its documentation are proprietary. No permission is granted to use, copy, +modify, distribute, sublicense, or disclose them except under a separate written agreement +with the copyright holder. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a2fe992 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Krona Staff + +Архитектурная основа блока сотрудников для Krona Business Platform. Это не готовая HRM: предметные правила табеля, графиков, отпусков, расчёта зарплаты, кадровых документов и отчётности намеренно не реализованы до требований владельца. + +Сейчас репозиторий содержит совместимое техническое ядро: два runtime-режима, порты платформы, PostgreSQL/RLS, инфраструктуру outbox/inbox/idempotency/audit, проверяемые OpenAPI и module manifest, health endpoints, контейнеризацию и CI. + +## Быстрый старт + +```bash +cp .env.example .env +docker compose --profile tools run --rm migrate +docker compose up --build app postgres redis +``` + +Открыть: shell `http://localhost:8080/`, OpenAPI JSON `/openapi.json`, проверки `/live`, `/ready`, `/version`. + +Без Docker: Node.js 22+, PostgreSQL 17 и Redis 7; затем `npm ci`, `npm run db:migrate`, `npm run dev`. + +## Команды + +- `npm run check` — lint, typecheck, tests, миграции, контракты и build. +- `npm run db:migrate` / `npm run db:migrate:down` — применить/откатить миграции. +- `npm run check:contracts` — проверить manifest/settings/OpenAPI. + +Архитектурные решения: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), интеграция: [docs/PLATFORM-INTEGRATION.md](docs/PLATFORM-INTEGRATION.md), будущий объём: [docs/ROADMAP.md](docs/ROADMAP.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..0bc1866 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +Сообщайте об уязвимостях приватно владельцу Forgejo, не создавая публичный issue и не прикладывая персональные/зарплатные данные. + +## Baseline + +Core SSO/JWT и RBAC в platform mode; fail-closed adapters; tenant scope + forced PostgreSQL RLS; least privilege permissions; audit trail; TLS; security headers; rate limits на будущих write endpoints; idempotency; validation; parameterized SQL; encrypted backups; S3 outside webroot; secrets только env/secret store. + +PII и payroll относятся к особо чувствительным данным. До их появления должны быть утверждены classification, field-level permissions, masking, encryption/key rotation, audit access, retention/deletion и incident response. Логи не содержат токены, зарплатные значения или полные персональные payload. + +Dependencies проверяются `npm audit` в CI; container и SBOM/image scan должны быть подключены к доступному registry scanner перед production release. diff --git a/api/openapi.yaml b/api/openapi.yaml new file mode 100644 index 0000000..83bce1d --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,84 @@ +openapi: 3.1.0 +info: + title: Krona Staff API + version: 0.1.0 + description: Operational API for the Krona Staff foundation. HR domain endpoints are intentionally absent until owner requirements are approved. + license: + name: Proprietary + url: https://git.ochenstarik.ru/ochenstarik/krona-staff/src/branch/main/LICENSE +servers: + - url: /api/v1/staff +security: [] +paths: + /live: + get: + summary: Check process liveness + operationId: getLiveness + tags: [Operations] + responses: + '200': + description: Process is alive + content: + application/json: + schema: { $ref: '#/components/schemas/Health' } + '400': { $ref: '#/components/responses/BadRequest' } + /ready: + get: + summary: Check dependency readiness + operationId: getReadiness + tags: [Operations] + responses: + '200': + description: Required dependencies are ready + content: + application/json: + schema: { $ref: '#/components/schemas/Health' } + '503': { description: A required dependency is unavailable } + '400': { $ref: '#/components/responses/BadRequest' } + /version: + get: + summary: Read runtime build identity + operationId: getVersion + tags: [Operations] + responses: + '200': + description: Runtime build identity + content: + application/json: + schema: { $ref: '#/components/schemas/Version' } + '400': { $ref: '#/components/responses/BadRequest' } +components: + responses: + BadRequest: + description: Invalid request + content: + application/json: + schema: { $ref: '#/components/schemas/Problem' } + schemas: + Ulid: + type: string + pattern: '^[0-9A-HJKMNP-TV-Z]{26}$' + examples: [01J00000000000000000000000] + Health: + type: object + additionalProperties: false + required: [status] + properties: + status: { type: string } + Problem: + type: object + additionalProperties: false + required: [type, title, status, trace_id] + properties: + type: { type: string, format: uri-reference } + title: { type: string } + status: { type: integer, minimum: 400, maximum: 599 } + trace_id: { $ref: '#/components/schemas/Ulid' } + Version: + type: object + additionalProperties: false + required: [name, version, mode] + properties: + name: { const: krona-staff } + version: { type: string } + mode: { enum: [standalone, platform] } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..e0e69f2 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,22 @@ +# Архитектура + +Krona Staff — deployable modular service с hexagonal boundaries. HTTP и workers вызывают application layer; domain contexts зависят только от ports; adapters обеспечивают standalone либо platform integration; PostgreSQL, Redis/BullMQ и object storage находятся на внешнем краю. + +```text +HTTP / worker -> application -> bounded-context contracts + |-> Identity / Authorization / Settings / Files / Notifications / EventBus ports + |-> tenant-scoped PostgreSQL repositories + transactional outbox +ports <- standalone adapters | platform-gateway adapters +``` + +Текущий код — foundation: health/version, конфигурация, ULID/event contracts, tenant transaction и инфраструктурная миграция. Доменный API отсутствует намеренно. + +## Контексты + +Предусмотрены границы: Employee Directory, Employment, Organization Structure, Scheduling, Time & Attendance, Leave, Payroll, Documents, Audit/Reporting. Между ними допускаются команды/события и стабильные ID, но не общий «богатый» HR-aggregate. Пакеты будут созданы по мере утверждения требований, чтобы пустые интерфейсы не стали ложным контрактом. + +## Данные и надёжность + +Shared database/shared schema, `tenant_id` в каждой tenant-таблице, application scope и forced RLS. Запись доменного состояния и outbox должна быть одной транзакцией. Inbox `(tenant_id,event_id)` подавляет повторную обработку; HTTP idempotency хранит request hash и результат. Сервис stateless, поэтому scale-out позже не требует переноса локальных файлов. + +См. [ADR-001](adr/ADR-001-technology-and-runtime-modes.md). diff --git a/docs/BACKUP-RESTORE.md b/docs/BACKUP-RESTORE.md new file mode 100644 index 0000000..33bc8a0 --- /dev/null +++ b/docs/BACKUP-RESTORE.md @@ -0,0 +1,9 @@ +# Backup и restore + +Production baseline: ежедневный `pg_dump` custom format, непрерывный WAL/PITR когда RPO этого требует, versioned S3 object copies и отдельный экспорт manifest/checksums. Копии шифруются recipient-based ключом вне сервера; retention задаётся политикой владельца. Секреты и ключ расшифрования в архив не входят. + +Restore runbook: остановить записи, проверить checksum/decrypt, восстановить в новую БД, применить только необходимые миграции, сверить tenant counts/outbox/inbox/audit, проверить `/ready`, выполнить выборочную сверку вложений, переключить connection string, сохранить audit evidence. Никогда не восстанавливать поверх единственной production БД. + +Тест восстановления обязателен по расписанию в изолированной среде. До принятия RPO/RTO и retention документ является архитектурным шаблоном, а не обещанием показателей. + +Tenant export/import использует versioned archive, ULID, schema version, object references и checksum. Import идемпотентен и не может перезаписать другой tenant. diff --git a/docs/DATA-OWNERSHIP.md b/docs/DATA-OWNERSHIP.md new file mode 100644 index 0000000..eced85b --- /dev/null +++ b/docs/DATA-OWNERSHIP.md @@ -0,0 +1,9 @@ +# Владение данными + +Core — system of record для tenants, user accounts, roles, permissions, platform settings, file metadata/storage access, notifications и event bus. Staff хранит только ссылки-ULID на Core records и не применяет foreign key через границу автономного deployment. + +Staff станет system of record для Employee, Employment и утверждённых HR aggregates. Scheduling/attendance/leave/payroll ownership уточняется с владельцем до схемы. Внешние кадровые/бухгалтерские системы подключаются через adapters; authority и направление sync фиксируются отдельным ADR. + +Каждая tenant-таблица: `tenant_id VARCHAR(26) NOT NULL`, составные unique/index keys с tenant, application scoping и forced RLS. Cross-tenant операции запрещены; системный export выполняется отдельной привилегированной ролью с audit. + +PII/payroll fields получают classification, field-level permissions, шифрование и retention после определения состава. Секреты не являются settings и поступают из secret store/env. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..c2bb055 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,9 @@ +# Развёртывание + +Одна команда локально: скопировать `.env.example` в `.env`, затем `docker compose --profile tools run --rm migrate` и `docker compose up --build`. Compose поднимает приложение, PostgreSQL и Redis. Object storage не включён, пока нет файлового сценария; production использует Platform Files или S3-compatible adapter. + +Production: один stateless app container/process, отдельные PostgreSQL и Redis, TLS/reverse proxy, secret manager, external object storage, централизованные logs/metrics/traces. `/live` проверяет процесс, `/ready` — обязательные зависимости, `/version` — release и runtime mode. Container работает non-root, read-only filesystem. + +HA сейчас не требуется. Для будущего scale-out: общий PostgreSQL/Redis/S3, несколько API/worker nodes, leader election для scheduler, outbox relay и идемпотентные consumers. Локальные вложения запрещены. Linux и Windows deployment сохраняют паритет на уровне Node/container contract. + +Миграции запускаются отдельным one-shot job до rollout. Down migration существует, но удаление данных требует отдельного backup и явного решения оператора. diff --git a/docs/DOMAIN-BOUNDARIES.md b/docs/DOMAIN-BOUNDARIES.md new file mode 100644 index 0000000..780e494 --- /dev/null +++ b/docs/DOMAIN-BOUNDARIES.md @@ -0,0 +1,17 @@ +# Доменные границы + +| Контекст | Будущее владение Staff | Сейчас | +|---|---|---| +| Employee Directory | Карточка сотрудника и связь с Core User по nullable reference | Только manifest | +| Employment | Трудовые отношения и их жизненный цикл | Ожидает требований | +| Organization Structure | HR-представление подразделений/позиций | Ожидает решения о source of truth | +| Scheduling | Плановые смены/графики | Ожидает требований | +| Time & Attendance | Факты времени/явки и корректировки | Ожидает требований | +| Leave | Заявки и остатки отпусков/отсутствий | Ожидает требований | +| Payroll | Расчётные входы/результаты и защищённый доступ | Ожидает требований и юрисдикции | +| Documents | HR-документы и ссылки на Platform Files | Ожидает требований | +| Audit/Reporting | HR audit projections и разрешённые отчёты | Базовый audit transport | + +Core User — учётная запись для входа и прав. Employee — предметная карточка человека в трудовом контексте. Они могут быть связаны, но ни один не является другим: сотрудник может не иметь аккаунта, аккаунт может не быть сотрудником. + +Запрещено до требований: придумывать ставки, формулы зарплаты, нормы времени, статусы кадровых процессов, юридические реквизиты, виды отпусков или фиктивные CRUD endpoints. diff --git a/docs/PLATFORM-INTEGRATION.md b/docs/PLATFORM-INTEGRATION.md new file mode 100644 index 0000000..ab9eb8a --- /dev/null +++ b/docs/PLATFORM-INTEGRATION.md @@ -0,0 +1,11 @@ +# Интеграция с Business Platform + +Основание: актуальная ветка `main` `ochenstarik/business-platform`, прежде всего Platform Block Contract 109, Module System 34, Lifecycle 54, Configuration Engine 51, Field Types 52, Integration 15 и deployment 93/97. + +В `platform` режиме gateway/JWT adapter получает tenant, actor и permissions из Core SSO; Staff не создаёт tenant/user/role. Settings, Files, Notifications и Event Bus доступны через отдельные порты. Реализация JWT намеренно fail-closed до публикации точного Core gateway/JWKS runtime contract. + +В `standalone` режиме совместимый adapter выдаёт один явно настроенный tenant/actor для разработки. Он не содержит таблиц пользователей и ролей и должен отключаться сменой `STAFF_RUNTIME_MODE=platform`. Миграция в platform означает сопоставление standalone ULID с Core references, проверку владения и отключение adapter. + +Манифест `module.json` объявляет hard dependency только на `ru.krona.core`, permissions, entity ownership, settings schema, configurable entities и UI slots. События пока пусты: имена и payload нельзя утверждать до бизнес-требований. API versioned; будущие события также versioned и используют platform envelope. + +Omnichannel использован только как существующий пример. Здесь заранее исключены его риски: параллельная identity/tenant модель, неоднозначный tenant-to-project mapping, доменные настройки вне platform schema и локально-незаменимое файловое состояние. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..34d2132 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,16 @@ +# Roadmap + +## Готово: foundation + +Runtime modes and ports, ULID/event envelope, PostgreSQL RLS infrastructure, outbox/inbox/idempotency/audit, manifest/settings/OpenAPI validation, health/version, Docker/Compose, CI and documentation. + +## Следующий этап — только после требований владельца + +1. Уточнить jurisdiction, terminology, data classification and ownership. +2. Согласовать Employee Directory и Employment workflows, затем создать первую vertical slice. +3. Отдельно описать Organization Structure, Scheduling, Time & Attendance, Leave, Payroll, Documents, Audit/Reporting. +4. Получить фактический Core gateway/JWT, Module SDK, Settings/Files/Notifications/Event Bus runtime contracts и реализовать platform adapters/conformance tests. +5. Утвердить event catalog/OpenAPI changes до публикации. +6. Утвердить backup RPO/RTO/retention и провести documented restore drill. + +Никакой пункт не означает автоматически готовую бизнес-функцию или срок. diff --git a/docs/adr/ADR-001-technology-and-runtime-modes.md b/docs/adr/ADR-001-technology-and-runtime-modes.md new file mode 100644 index 0000000..506e69a --- /dev/null +++ b/docs/adr/ADR-001-technology-and-runtime-modes.md @@ -0,0 +1,20 @@ +# ADR-001: TypeScript service and runtime adapters + +- Status: Accepted for foundation +- Date: 2026-09-10 + +## Context + +Platform contracts are TypeScript, baseline API is REST/OpenAPI 3.1, PostgreSQL/Redis/BullMQ are normative, blocks need independent deployment without duplicating Core identity. Staff business rules are unknown. + +## Decision + +Use Node.js 22 + strict TypeScript, Fastify, PostgreSQL 17, Redis/BullMQ, Zod, Vitest and a single stateless container. Keep domain code behind ports. Select either standalone adapters or platform-gateway adapters through configuration. Use ULID `VARCHAR(26)`, UTC, `BIGINT` minor money, transactional outbox/inbox and S3 abstraction. + +Standalone provides only explicit development tenant/actor references; no durable user/role source. Platform mode fails closed until verified Core JWT/gateway contracts are implemented. + +## Consequences + +The module can deploy separately and share platform contract types with minimal translation. PostgreSQL RLS gives defense in depth. Runtime integration remains explicit work rather than a mocked claim. Fastify adds a different web runtime from Omnichannel, but platform compatibility is contractual, not framework inheritance. + +Rejected: copy Omnichannel/Python mechanically; embed inside platform monolith; invent a local identity service; implement HR endpoints before requirements. diff --git a/module.json b/module.json new file mode 100644 index 0000000..62a1770 --- /dev/null +++ b/module.json @@ -0,0 +1,30 @@ +{ + "$schema": "./schemas/module.schema.json", + "id": "ru.krona.staff", + "name": "Krona Staff", + "version": "0.1.0", + "min_platform_version": "3.2.0", + "dependencies": ["ru.krona.core"], + "optional_dependencies": [], + "permissions": [ + "staff:employee:read", + "staff:employee:write", + "staff:employment:read", + "staff:employment:write", + "staff:sensitive:read", + "staff:sensitive:write", + "staff:audit:read" + ], + "entities": ["Employee", "Employment", "OrganizationUnit"], + "events_emitted": [], + "events_consumed": [], + "settings_schema": "settings.schema.json", + "configurable_entities": ["Employee", "Employment", "OrganizationUnit"], + "promote_targets": [], + "api_prefix": "/api/v1/staff", + "routes": ["/staff"], + "ui_extensions": [ + { "slot": "navigation", "component": "StaffNavigation" }, + { "slot": "settings.module", "component": "PlatformGeneratedSettings" } + ] +} diff --git a/schemas/module.schema.json b/schemas/module.schema.json new file mode 100644 index 0000000..266f801 --- /dev/null +++ b/schemas/module.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://krona.example/schemas/module.schema.json", + "type": "object", + "additionalProperties": false, + "required": ["id", "name", "version", "min_platform_version", "dependencies", "permissions", "entities", "events_emitted", "events_consumed", "settings_schema", "configurable_entities", "ui_extensions"], + "properties": { + "$schema": { "type": "string" }, + "id": { "const": "ru.krona.staff" }, + "name": { "type": "string", "minLength": 1 }, + "version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" }, + "min_platform_version": { "type": "string" }, + "dependencies": { "type": "array", "contains": { "const": "ru.krona.core" }, "items": { "type": "string" }, "uniqueItems": true }, + "optional_dependencies": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }, + "permissions": { "type": "array", "items": { "type": "string", "pattern": "^staff:[a-z_]+:(read|write)$" }, "uniqueItems": true }, + "entities": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }, + "events_emitted": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }, + "events_consumed": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }, + "settings_schema": { "type": "string" }, + "configurable_entities": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }, + "promote_targets": { "type": "array", "items": { "type": "object" } }, + "api_prefix": { "const": "/api/v1/staff" }, + "routes": { "type": "array", "items": { "type": "string" } }, + "ui_extensions": { "type": "array", "items": { "type": "object", "required": ["slot", "component"], "properties": { "slot": { "type": "string" }, "component": { "type": "string" } }, "additionalProperties": false } } + } +} diff --git a/schemas/settings.schema.schema.json b/schemas/settings.schema.schema.json new file mode 100644 index 0000000..af0fd6a --- /dev/null +++ b/schemas/settings.schema.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": ["groups"], + "properties": { + "$schema": { "type": "string" }, + "groups": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["key", "label", "fields"], + "properties": { + "key": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, + "label": { "type": "string" }, + "fields": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["key", "type", "label"], + "properties": { + "key": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, + "type": { "enum": ["text", "number", "boolean", "date", "datetime", "money", "select", "multiselect", "relation", "email", "phone", "url", "json"] }, + "label": { "type": "string" }, + "required": { "type": "boolean" }, + "description": { "type": "string" } + } + } + } + } + } + } + } +} diff --git a/settings.schema.json b/settings.schema.json new file mode 100644 index 0000000..91b7e67 --- /dev/null +++ b/settings.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "./schemas/settings.schema.schema.json", + "groups": [ + { + "key": "localization", + "label": "Локализация", + "fields": [ + { + "key": "business_timezone", + "type": "text", + "label": "Часовой пояс организации", + "required": true, + "description": "IANA timezone; timestamps are persisted in UTC and interpreted using this tenant setting." + } + ] + } + ] +}