Klinok is a Russian-language, local-first veterinary application with operational email/password authentication and signed P2P authorization.
src/— Vue UI. It starts withGET /api/auth/session, caches private user keys in IndexedDB, and opens P2P databases only for an authenticated, attested device.auth-node/— Fastify/LevelDB authentication service for credentials, email verification, password recovery, HTTP sessions, automatic device enrollment, encrypted user-key escrow, and SMTP delivery.p2p-node/— untrusted OrbitDB storage/transport node. It verifies signed envelopes but receives no passwords or user private keys.packages/protocol/— shared event contracts, cryptography, authorization rules, and deterministic reducers used by browser and Node runtimes.
The current databases are klinok-control-v1 and klinok-medical-v3. There is no migration or runtime fallback from earlier demo data.
Roles, profiles, pets, grants, and medical records are encrypted signed events in these OrbitDB databases; they are not rows in the authentication LevelDB. The browser keeps an IndexedDB cache and durable outbox. A change is shown as fully saved only after p2p-node acknowledges that the event was written to its persistent /data volume; offline changes remain visible with a pending-sync status and are retried in dependency order.
The easiest way to start the complete application locally is Docker Compose:
./scripts/run-local.shThe script builds the images, provisions a local bootstrap Administrator, connects the
services with the generated trust keys, and waits for the application to become ready.
It prints the local credentials when it finishes. The application is available at
http://localhost:8080 and Mailpit at http://localhost:8025.
After the complete stack is running, rebuild and restart only the UI service with:
./scripts/rebuild-ui.shThe script preserves the running auth, P2P, and mail services and reuses their current trust configuration.
Чтобы запустить сервисы аутентификации, P2P и почты в Docker, а интерфейс — через Vite с горячей перезагрузкой, выполните:
./scripts/run-mixed-dev.shСкрипт временно останавливает контейнеры ui и auth, но сохраняет и переиспользует
те же локальные тома и данные, что и полный Docker-стек. Он выполняет provision при первом запуске,
получает публичные ключи доверия и идентификатор P2P-узла, формирует локальную
runtime-конфигурацию, публикует API приёма P2P-событий только на 127.0.0.1:8091
и запускает npm run dev. Vite перенаправляет /api/events в этот API. Интерфейс
доступен по каноническому адресу http://localhost:8080, совпадающему с адресом
полного Docker-стека, а Mailpit — по адресу http://localhost:8025. Vite и локальный
Nginx перенаправляют запросы с альтернативных имён хоста на канонический адрес до
загрузки приложения, чтобы идентификатор устройства оставался в одном browser origin.
Пока backend-контейнеры продолжают работать, последующие запуски интерфейса можно
выполнять обычной командой npm run dev: Vite автоматически использует созданную
конфигурацию из .klinok-local/.
После первого запуска сборку backend-образов можно пропустить:
KLINOK_SKIP_BUILD=true ./scripts/run-mixed-dev.shОстановить backend-контейнеры можно командой, которую скрипт выводит перед запуском Vite.
Docker with Compose v2 and curl are required. To rebuild less often after the first
run, use KLINOK_SKIP_BUILD=true ./scripts/run-local.sh. Stop the stack with:
COMPOSE_PROJECT_NAME=klinok_local docker compose downAn ordinary docker compose down preserves the authentication LevelDB and P2P
OrbitDB data in Docker volumes. To remove all local containers and database data and
start from scratch, run:
COMPOSE_PROJECT_NAME=klinok_local docker compose down -v --remove-orphans
rm -rf .klinok-local
./scripts/run-local.shThe -v option permanently removes the local authentication accounts, roles, pets,
medical records, and P2P history. Removing .klinok-local also removes the generated
mixed-development configuration and the local copy of the bootstrap recovery bundle.
Save that recovery bundle elsewhere first if it is needed for an existing deployment.
The browser stores its device identity, a cached copy of its private keys, application
data, and the durable outbox in IndexedDB and other site storage for
http://localhost:8080. Clear this storage as well after deleting the Docker volumes
so that an old browser identity is not reused with the new deployment:
- Close every open Klinok tab except one.
- Open
http://localhost:8080and the browser Developer Tools. - In Chrome or Edge, select Application → Storage → Clear site data. In Firefox,
select Storage → Indexed DB, delete the Klinok databases, and clear the site
cookies and local storage for
localhost. - Reload the application and sign in with the credentials printed by
./scripts/run-local.sh.
A new browser profile can be used instead. A private/incognito window is appropriate for short tests only because its device identity is deleted when the window closes. Browser storage does not need to be cleared for ordinary restarts or UI rebuilds. For an account that has completed automatic server-key migration, clearing browser storage is recoverable: sign in again and the new device is approved automatically. Legacy accounts without a server key copy still require an active old device or the bootstrap recovery bundle until they next sign in from an active browser.
If Compose crashes with SIGBUS or reports an input/output error under WSL, quit
Docker Desktop, run wsl --shutdown in Windows PowerShell, and restart Docker Desktop.
For local Node.js checks outside Docker:
npm ci
npm test
npm run buildProvision exactly once with secrets supplied through the environment or Docker secrets:
export KLINOK_BOOTSTRAP_EMAIL="${KLINOK_BOOTSTRAP_EMAIL:-maxirmx@sw.consulting}"
export KLINOK_BOOTSTRAP_PASSWORD="${KLINOK_BOOTSTRAP_PASSWORD:-Password&Spaniel&26}"
export KLINOK_RECOVERY_PASSPHRASE="${KLINOK_RECOVERY_PASSPHRASE:-Bene facta me clarum non fecerunt}"
npm run build:auth && npm run auth:provisionNewly provisioned bootstrap keys are encrypted by the authentication service and can
be restored after an ordinary sign-in, including after browser storage is cleared.
Store bootstrap-recovery.bundle.json and KLINOK_RECOVERY_PASSPHRASE separately
offline as an emergency and legacy-migration backup. The bootstrap account remains
protected from deletion.
The authentication service stores every account's exported private keys encrypted
with /data/user-key-escrow-key.json. It can decrypt these keys, so this prototype
does not provide strict end-to-end key custody. Back up the authentication data volume
as one unit: losing the escrow master-key file makes the encrypted account key copies
unrecoverable. KLINOK_BOOTSTRAP_ACCOUNT_ID and
KLINOK_BOOTSTRAP_SIGNING_PUBLIC_KEY must remain identical for the UI,
authentication service, and P2P node; the supplied Docker Compose files wire these
values automatically.
Сервис аутентификации применяет ограничения до выполнения операций с LevelDB, Argon2, SMTP и аттестационным ключом. Первый уровень учитывает адрес клиента и защищает каждый маршрут от перебора и истощения ресурсов. Второй уровень объединяет запросы по нормализованному адресу электронной почты, хешу токена или идентификатору аккаунта, поэтому смена адреса клиента не обходит ограничения для одной учётной записи. Адреса электронной почты и идентификаторы аккаунтов не сохраняются в ключах счётчиков: для них используется HMAC-SHA-256 со случайным ключом процесса.
Начальные значения ограничений:
| Операция | По адресу клиента | Дополнительное ограничение |
|---|---|---|
| Регистрация | 5 запросов в час | 1 письмо в сутки на адрес электронной почты |
| Вход | 30 запросов за 15 минут | 5 неудачных попыток за 15 минут на аккаунт |
| Запрос восстановления пароля | 10 запросов в час | 3 письма в час на аккаунт |
| Подтверждение почты и сброс пароля | 20 запросов за 15 минут | 5 попыток за 15 минут на хеш токена |
| Чтение сессии | 300 запросов в минуту | — |
| Изменение профиля и обычный выход | 60 запросов в минуту | 60 изменений в минуту на аккаунт |
| Выход на всех устройствах, удаление аккаунта и операции с устройствами | 10 запросов в минуту | 10 чувствительных изменений в минуту на аккаунт |
При превышении публичного ограничения сервис отвечает кодом 429, возвращает
error.code со значением RATE_LIMITED и заголовок Retry-After. Ограничения на
повторную отправку писем не раскрывают существование аккаунта: сервис сохраняет
одинаковый ответ 202 {"accepted":true}, но не выполняет повторную отправку.
Прежняя сохраняемая в аккаунте блокировка lockedUntil больше не используется как
основной механизм защиты; неудачные входы учитываются во временном счётчике.
Сервис по умолчанию не доверяет X-Forwarded-For. Параметр
KLINOK_AUTH_TRUST_PROXY должен содержать точное число доверенных промежуточных
узлов либо доверенный адрес или диапазон сети. Значение true нельзя использовать
при доступности порта сервиса из недоверенной сети. В локальном Docker Compose
установлено значение 1, поскольку порт контейнера auth не опубликован и запросы
приходят только через nginx контейнера ui.
Пороговые значения настраиваются переменными окружения:
KLINOK_RATE_LIMIT_REGISTRATION_IP_PER_HOUR;KLINOK_RATE_LIMIT_REGISTRATION_EMAIL_PER_DAY;KLINOK_RATE_LIMIT_LOGIN_IP_PER_15_MINUTES;KLINOK_RATE_LIMIT_LOGIN_FAILURES_PER_ACCOUNT_15_MINUTES;KLINOK_RATE_LIMIT_RECOVERY_IP_PER_HOUR;KLINOK_RATE_LIMIT_RECOVERY_ACCOUNT_PER_HOUR;KLINOK_RATE_LIMIT_TOKEN_IP_PER_15_MINUTES;KLINOK_RATE_LIMIT_TOKEN_PER_15_MINUTES;KLINOK_RATE_LIMIT_SESSION_IP_PER_MINUTE;KLINOK_RATE_LIMIT_MUTATION_IP_PER_MINUTE;KLINOK_RATE_LIMIT_SENSITIVE_MUTATION_IP_PER_MINUTE;KLINOK_RATE_LIMIT_MUTATION_ACCOUNT_PER_MINUTE;KLINOK_RATE_LIMIT_SENSITIVE_MUTATION_ACCOUNT_PER_MINUTE.
Все значения должны быть положительными целыми числами. Счётчики находятся в памяти
процесса. Такая конфигурация рассчитана на один экземпляр auth-node; перед запуском
нескольких экземпляров необходимо подключить общее отказоустойчивое хранилище
счётчиков и единый секрет для формирования приватных ключей ограничений.
- Roles are
administrator,doctor, andowner; users explicitly select one active role for every route and signed write. - Profiles and medical records are encrypted with AES-GCM. Keys are wrapped with RSA-OAEP-256 and event envelopes are signed with ECDSA P-256.
- Administrators manage accounts and roles but receive no medical key envelopes.
- Revoking pet access rotates the key for future records; historical data already decrypted by a recipient cannot be clawed back.
- Legal document text, versions, retention schedules, SMTP, credentials, and persistent volumes are deployment-owned configuration.