================================================================================
  СЛОВАРЬ ОШИБОК API — ptdept_srv
  Дата генерации: 2026-02-23
================================================================================

Этот документ содержит полный список ошибок, которые может вернуть сервер.
Ошибки возвращаются в едином формате как для HTTP, так и для gRPC.

────────────────────────────────────────────────────────────────────────────────
  ФОРМАТ ОТВЕТА
────────────────────────────────────────────────────────────────────────────────

HTTP JSON ответ:
{
  "code":      "<ErrorCode>",         // унифицированный код ошибки (см. таблицу ниже)
  "error":     "<HTTP Status Text>",  // текстовое описание HTTP статуса
  "message":   "<подробное сообщение>",
  "path":      "<URL path>",
  "timestamp": "<ISO 8601>",
  "status":    <HTTP status code>
}

gRPC ответ:
  - gRPC Status Code  (маппинг из ErrorCode, см. таблицу ниже)
  - Status Message:    "<ErrorCode>: <подробное сообщение>"

────────────────────────────────────────────────────────────────────────────────
  СТРУКТУРА ПОЛЯ message
────────────────────────────────────────────────────────────────────────────────

Поле message содержит полный путь ошибки через слои приложения:

  HTTP:  "<namespace>.<method>: <message_text>[ - cause: <detail>]"
  gRPC:  "<ErrorCode>: <namespace>.<method>: <message_text>[ - cause: <detail>]"

  Где:
  - <namespace>.<method> — путь к методу, например "app.auth.RegisterUser"
  - <message_text> — текст ошибки (указан ниже в разделе 2 для каждой ошибки)
  - <detail> — (опционально) дополнительные детали оригинальной ошибки

  Примеры реальных message:
  HTTP:  "app.auth.RegisterUser: validation failed - cause: Key: 'emailBindingInput.Email'..."
  gRPC:  "VALIDATION_FAILED: app.auth.RegisterUser: validation failed - cause: ..."
  HTTP:  "app.auth.LogIn: invalid email or password"
  HTTP:  "app.ratelimit.login: rate limit exceeded, retry after 45 seconds"
  HTTP:  "app.perm.GetUserFromContext: user does not have permission"

  ВАЖНО для клиента:
  - Для идентификации ошибки используйте поле "code" (ErrorCode), НЕ message.
  - Поле message может содержать переменные данные (email, секунды, UUID).
  - Ниже для каждой ошибки указан стабильный текст message_text,
    по которому при необходимости можно делать contains-поиск.

================================================================================
  1. ТАБЛИЦА УНИФИЦИРОВАННЫХ КОДОВ ОШИБОК (ErrorCode)
================================================================================

┌─────────────────────────────┬───────────┬──────────────────────┬─────────────────────────────────────────┐
│ ErrorCode                   │ HTTP Code │ gRPC Code            │ Описание                                │
├─────────────────────────────┼───────────┼──────────────────────┼─────────────────────────────────────────┤
│ INTERNAL_ERROR              │ 500       │ Internal             │ Внутренняя ошибка сервера               │
│ INVALID_INPUT               │ 400       │ InvalidArgument      │ Некорректные входные данные             │
│ VALIDATION_FAILED           │ 400       │ InvalidArgument      │ Ошибка валидации полей                  │
├─────────────────────────────┼───────────┼──────────────────────┼─────────────────────────────────────────┤
│ UNAUTHORIZED                │ 401       │ Unauthenticated      │ Пользователь не авторизован             │
│ INVALID_CREDENTIALS         │ 401       │ Unauthenticated      │ Неверный email или пароль               │
│ TOKEN_INVALID               │ 401       │ Unauthenticated      │ Невалидный токен                        │
│ TOKEN_EXPIRED               │ 401       │ Unauthenticated      │ Срок действия токена истёк              │
│ SESSION_INVALID             │ 401       │ Unauthenticated      │ Сессия невалидна или отозвана           │
├─────────────────────────────┼───────────┼──────────────────────┼─────────────────────────────────────────┤
│ PERMISSION_DENIED           │ 403       │ PermissionDenied     │ Доступ запрещён                         │
│ NOT_ROOT_OR_ADMIN           │ 403       │ PermissionDenied     │ Требуется роль Root или Admin           │
│ NOT_ROOT                    │ 403       │ PermissionDenied     │ Требуется роль Root                     │
│ NOT_ADMIN                   │ 403       │ PermissionDenied     │ Требуется роль Admin                    │
│ USER_BLOCKED                │ 403       │ PermissionDenied     │ Пользователь заблокирован               │
│ EMAIL_NOT_VERIFIED          │ 403       │ PermissionDenied     │ Email не подтверждён                    │
├─────────────────────────────┼───────────┼──────────────────────┼─────────────────────────────────────────┤
│ NOT_FOUND                   │ 404       │ NotFound             │ Ресурс не найден                        │
│ USER_NOT_FOUND              │ 404       │ NotFound             │ Пользователь не найден                  │
│ USER_ALREADY_EXISTS         │ 409       │ AlreadyExists        │ Пользователь с таким email уже есть     │
├─────────────────────────────┼───────────┼──────────────────────┼─────────────────────────────────────────┤
│ PASSWORD_WEAK               │ 400       │ InvalidArgument      │ Пароль слишком слабый                   │
│ PASSWORD_MISMATCH           │ 400       │ InvalidArgument      │ Пароли не совпадают                     │
│ PASSWORD_SAME_AS_OLD        │ 400       │ InvalidArgument      │ Новый пароль совпадает со старым         │
├─────────────────────────────┼───────────┼──────────────────────┼─────────────────────────────────────────┤
│ EMAIL_INVALID               │ 400       │ InvalidArgument      │ Некорректный формат email               │
│ EMAIL_ALREADY_VERIFIED      │ 409       │ AlreadyExists        │ Email уже подтверждён                   │
│ VERIFICATION_CODE_INVALID   │ 400       │ InvalidArgument      │ Невалидный код верификации              │
│ VERIFICATION_CODE_EXPIRED   │ 400       │ InvalidArgument      │ Код верификации истёк                   │
├─────────────────────────────┼───────────┼──────────────────────┼─────────────────────────────────────────┤
│ RATE_LIMIT_EXCEEDED         │ 429       │ ResourceExhausted    │ Превышен лимит запросов                 │
└─────────────────────────────┴───────────┴──────────────────────┴─────────────────────────────────────────┘


================================================================================
  2. ОШИБКИ ПО МЕТОДАМ — СЛОЙ APP (бизнес-логика)
================================================================================

────────────────────────────────────────────────────────────────────────────────
2.1  Auth (аутентификация) — namespace: app.auth
────────────────────────────────────────────────────────────────────────────────

Метод: Auth.RegisterUser
  Путь HTTP: POST /v1/auth/register
  gRPC:      AuthService.Register
  Namespace: app.auth.RegisterUser
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов на регистрацию
      message: "rate limit exceeded, retry after N seconds"
              | "client IP is required"
              | "IP temporarily blocked, retry after N seconds"
    • VALIDATION_FAILED           — ошибка валидации email/name/password
      message: "validation failed"
      cause может содержать:
        - ошибку валидации поля ("Key: '...Field' Error:Field validation for '...' failed on the '...' tag")
        - "syntax validation failed: invalid email address format"
        - "domain validation failed: domain does not have valid MX records"
        - "SPF validation failed: domain does not have a valid SPF record"
        - "DMARC validation failed: domain does not have a valid DMARC record"
        - ошибку сложности пароля ("insecure password, ...")
    • USER_ALREADY_EXISTS         — пользователь с таким email уже существует
      message: "user already exists, email: <email>"
    • INTERNAL_ERROR              — ошибка проверки наличия пользователей в БД
      message: "failed to check if users exist"
    • INTERNAL_ERROR              — ошибка генерации хеша пароля
      message: "failed to generate password hash"
    • INTERNAL_ERROR              — ошибка создания пользователя в БД
      message: "failed to create user"
    • INTERNAL_ERROR              — ошибка генерации кода верификации
      message: "failed to generate verification code"
    • INTERNAL_ERROR              — ошибка сохранения кода верификации
      message: "failed to set verification code"
    • INTERNAL_ERROR              — ошибка отправки email верификации
      message: "user registered but failed to send verification email"

Метод: Auth.LogIn
  Путь HTTP: POST /v1/auth/login
  gRPC:      AuthService.Login
  Namespace: app.auth.LogIn
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов на логин
      message: (см. RateLimiter.Check)
    • VALIDATION_FAILED           — ошибка валидации email/password
      message: "invalid login input"
      cause может содержать: ошибку валидации email / пароля (см. RegisterUser)
    • USER_NOT_FOUND              — пользователь не найден (только debug/test режим)
      message: "user not found"
    • INVALID_CREDENTIALS         — неверный email или пароль
      message: "invalid email or password"          (production)
             | "invalid password"                    (debug/test)
    • USER_BLOCKED                — пользователь заблокирован (только debug/test режим)
      message: "user is blocked"                    (debug/test)
             | "invalid email or password"           (production, маскируется)
    • TOKEN_EXPIRED               — временный пароль истёк (только debug/test режим)
      message: "your temporary password has expired, please request a new password reset"  (debug/test)
             | "invalid email or password"           (production, маскируется)
    • EMAIL_NOT_VERIFIED          — email не подтверждён
      message: "email not verified"
    • INTERNAL_ERROR              — ошибка генерации токена (через GenerateToken)
      message: (см. GenerateTokenWithContext)

Метод: Auth.RefreshToken
  Путь HTTP: POST /v1/auth/refresh
  gRPC:      AuthService.RefreshToken
  Namespace: app.auth.RefreshToken
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • VALIDATION_FAILED           — refresh/access токен не предоставлены
      message: "refresh/access token validation failed"
    • TOKEN_INVALID               — не удалось декодировать access token
      message: "failed to decode access token"
    • TOKEN_INVALID               — невалидный refresh token
      message: "refresh token validation failed"
             | "invalid signing method"
    • TOKEN_INVALID               — refresh token claims неверного типа
      message: "token claims are not of type *tokenClaims"
    • TOKEN_INVALID               — токены принадлежат разным пользователям
      message: "access and refresh tokens belong to different users"
    • SESSION_INVALID             — сессия не найдена или уже использована
      message: "session not found or already used"
    • USER_NOT_FOUND              — пользователь не найден
      message: "user not found"
    • USER_BLOCKED                — пользователь заблокирован
      message: "user is blocked"
    • INTERNAL_ERROR              — ошибка отзыва старого keystore
      message: "failed to revoke old keystore"
    • INTERNAL_ERROR              — ошибка генерации нового токена
      message: (см. GenerateTokenWithContext)

Метод: Auth.LogOut
  Путь HTTP: POST /v1/auth/logout
  gRPC:      AuthService.Logout
  Namespace: app.auth.LogOut
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • TOKEN_INVALID               — невалидный метод подписи
      message: "invalid signing method"
    • TOKEN_INVALID               — невалидный токен
      message: "logout failed: invalid token"
    • TOKEN_INVALID               — пустые claims токена
      message: "token claims are empty"
    • TOKEN_INVALID               — ошибка парсинга claims
      message: "failed to parse token claims"
    • INTERNAL_ERROR              — ошибка отзыва сессии
      message: "failed to revoke session"

Метод: Auth.LogOutAll
  Путь HTTP: POST /v1/auth/logout-all
  gRPC:      AuthService.LogoutAll
  Namespace: app.auth.LogOutAll
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • TOKEN_INVALID               — ошибка парсинга токена (через ParseToken)
      message: (см. ParseToken)
    • SESSION_INVALID             — сессия невалидна (через ParseToken)
      message: (см. ParseToken)
    • INTERNAL_ERROR              — ошибка отзыва всех сессий пользователя
      message: "failed to revoke all user sessions"

Метод: Auth.VerifyEmail
  Путь HTTP: POST /v1/auth/verify
  gRPC:      AuthService.VerifyEmail
  Namespace: app.auth.VerifyEmail
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • VALIDATION_FAILED           — ошибка валидации email/code
      message: "email/code validation failed"
      cause может содержать: ошибку валидации email (см. RegisterUser) или кода
    • VERIFICATION_CODE_INVALID   — невалидный код верификации
      message: "invalid verification code"
    • VERIFICATION_CODE_EXPIRED   — код верификации истёк
      message: "verification code expired"
    • EMAIL_ALREADY_VERIFIED      — email уже подтверждён
      message: "email already verified"
    • INTERNAL_ERROR              — ошибка обновления статуса верификации
      message: "failed to verify email"

Метод: Auth.ResendVerificationEmail
  Путь HTTP: GET /v1/auth/email/verify-send
  gRPC:      AuthService.ResendVerificationEmail
  Namespace: app.auth.ResendVerificationEmail
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • VALIDATION_FAILED           — некорректный формат email
      message: "invalid email format"
      cause может содержать: ошибку валидации email (см. RegisterUser)
    • USER_NOT_FOUND              — пользователь не найден
      message: "user not found"
    • USER_BLOCKED                — пользователь заблокирован
      message: "user blocked"
    • EMAIL_ALREADY_VERIFIED      — email уже подтверждён
      message: "email already verified"
    • INTERNAL_ERROR              — ошибка генерации кода верификации
      message: "failed to generate verification code"
    • INTERNAL_ERROR              — ошибка сохранения кода верификации
      message: "failed to set verification code"
    • INTERNAL_ERROR              — ошибка отправки email
      message: "failed to send verification email"

Метод: Auth.RequestPasswordReset
  Путь HTTP: GET /v1/auth/email/reset-password
  gRPC:      AuthService.RequestPasswordReset
  Namespace: app.auth.RequestPasswordReset
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • VALIDATION_FAILED           — некорректный формат email/lang
      message: "invalid email and lang format"
      cause может содержать: ошибку валидации email (см. RegisterUser)
    • USER_BLOCKED                — пользователь заблокирован
      message: "user blocked"
    • INTERNAL_ERROR              — ошибка генерации временного пароля
      message: "failed generate temporary password"
    • INTERNAL_ERROR              — ошибка сохранения временного пароля
      message: "failed to set temporary password"
    • INTERNAL_ERROR              — ошибка отправки email с паролем
      message: "failed to send password reset email"
    ПРИМЕЧАНИЕ: Если email не найден — ошибка НЕ возвращается (безопасность).

Метод: Auth.GetActiveSessionsByUser
  Путь HTTP: GET /v1/user/sessions
  gRPC:      UserService.GetActiveSessions
  Namespace: app.auth.GetActiveSessions
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • TOKEN_INVALID               — ошибка парсинга токена
      message: "failed to parse token" + (см. ParseToken)
    • SESSION_INVALID             — сессия невалидна
      message: (см. ParseToken)
    • INTERNAL_ERROR              — ошибка получения сессий из БД
      message: "failed to get sessions"

Метод: Auth.GenerateToken / Auth.GenerateTokenWithContext
  (внутренний метод, вызывается из LogIn/RefreshToken)
  Namespace: app.auth.GenerateToken
  Возможные ошибки:
    • INTERNAL_ERROR              — ошибка генерации primary key
      message: "failed to generate primary key"
    • INTERNAL_ERROR              — ошибка генерации secondary key
      message: "failed to generate secondary key"
    • INTERNAL_ERROR              — ошибка подписи access token
      message: "failed to sign access token"
    • INTERNAL_ERROR              — ошибка подписи refresh token
      message: "failed to sign refresh token"
    • INTERNAL_ERROR              — ошибка создания keystore в БД
      message: "failed to create keystore"

Метод: Auth.ParseToken
  (внутренний метод, вызывается из других методов)
  Namespace: app.auth.ParseToken
  Возможные ошибки:
    • TOKEN_INVALID               — невалидный метод подписи
      message: "invalid signing method"
    • TOKEN_INVALID               — ошибка парсинга токена
      message: "token parsing failed"
    • TOKEN_INVALID               — claims неверного типа
      message: "token claims are not of type *tokenClaims"
    • SESSION_INVALID             — сессия токена не найдена или отозвана
      message: "token session not found or revoked"
    • SESSION_INVALID             — сессия не активна или несовпадение пользователя
      message: "session is not active or user mismatch"

────────────────────────────────────────────────────────────────────────────────
2.2  Users (операции пользователя) — namespace: app.users
────────────────────────────────────────────────────────────────────────────────

Метод: Users.Get
  Путь HTTP: GET /v1/user/get
  gRPC:      UserService.GetCurrentUser
  Namespace: app.users.Get
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • TOKEN_INVALID               — ошибка парсинга токена (через Perm.User)
      message: "permission check failed" + (см. ParseToken)
    • PERMISSION_DENIED           — нет прав доступа (через Perm.User)
      message: "user does not have permission"
    • NOT_FOUND                   — пользователь не найден (через Perm.User)
      message: "user not found"

Метод: Users.UpdateName
  Путь HTTP: PUT /v1/user/name
  gRPC:      UserService.UpdateName
  Namespace: app.users.UpdateName
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • TOKEN_INVALID               — ошибка парсинга токена (через Perm)
      message: (см. ParseToken)
    • PERMISSION_DENIED           — нет прав доступа
      message: "user does not have permission"
    • VALIDATION_FAILED           — имя не прошло валидацию (min=2, max=32)
      message: "validation failed"
      cause: "Key: 'updateNameBindingInput.Name' Error:Field validation for 'Name' failed on the '...' tag"
    • INTERNAL_ERROR              — ошибка обновления имени в БД
      message: "failed to update name"

Метод: Users.ChangePassword
  Путь HTTP: PUT /v1/user/password
  gRPC:      UserService.ChangePassword
  Namespace: app.users.ChangePassword
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • TOKEN_INVALID               — ошибка парсинга токена (через Perm.User)
      message: (см. ParseToken)
    • PERMISSION_DENIED           — нет прав доступа
      message: "user does not have permission"
    • NOT_FOUND                   — пользователь не найден
      message: "user not found"
    • VALIDATION_FAILED           — old/new/confirm пароли не прошли валидацию
      message: "validation failed"
      cause может содержать:
        - "new password is same as old"     (новый пароль совпадает со старым)
        - "password mismatch"               (new ≠ confirm)
        - "insecure password, ..."          (слабый пароль)
    • INVALID_CREDENTIALS         — неверный старый пароль
      message: "invalid old password"
    • INTERNAL_ERROR              — ошибка хеширования нового пароля
      message: "failed to hash password"
    • INTERNAL_ERROR              — ошибка обновления пароля в БД
      message: "failed to update password"

Метод: Users.Blocked (самоблокировка)
  Путь HTTP: GET /v1/user/block
  gRPC:      UserService.BlockSelf
  Namespace: app.users.Blocked
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • TOKEN_INVALID               — ошибка парсинга токена (через Perm)
      message: (см. ParseToken)
    • PERMISSION_DENIED           — нет прав доступа
      message: "user does not have permission"
    • INTERNAL_ERROR              — ошибка блокировки в БД
      message: "failed to block user"

────────────────────────────────────────────────────────────────────────────────
2.3  Root (администрирование) — namespace: app.root
────────────────────────────────────────────────────────────────────────────────

Метод: Root.CreateUser
  Путь HTTP: POST /v1/root/users/create
  gRPC:      RootService.CreateUser
  Namespace: app.root.CreateUser
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • NOT_ROOT_OR_ADMIN           — нет прав Root или Admin (через Perm.CreateUserRoot)
      message: "user is not root or admin"
             | "not permission root or admin"
    • NOT_ROOT                    — нет прав Root (при создании root-пользователя)
      message: "not permission root"
    • VALIDATION_FAILED           — ошибка валидации email/name
      message: "validation failed"
      cause может содержать: ошибку валидации email (см. RegisterUser) или поля name
    • USER_ALREADY_EXISTS         — пользователь с таким email уже существует
      message: "user already exists, email: <email>"
    • INTERNAL_ERROR              — ошибка создания пользователя в БД
      message: "failed to create user"
    • INTERNAL_ERROR              — ошибка генерации временного пароля
      message: "failed generate temporary password"
    • INTERNAL_ERROR              — ошибка сохранения временного пароля
      message: "failed to set temporary password"
    • INTERNAL_ERROR              — ошибка отправки email с паролем
      message: "user created but failed to send password email"

Метод: Root.GetUser
  Путь HTTP: GET /v1/root/users/read/:id
  gRPC:      RootService.GetUser
  Namespace: app.root.GetUser
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • NOT_ROOT_OR_ADMIN           — нет прав Root или Admin
      message: "user is not root or admin"
    • NOT_FOUND                   — пользователь не найден
      message: "user not found"

Метод: Root.UpdateUser
  Путь HTTP: PUT /v1/root/users/update/:id
  gRPC:      RootService.UpdateUser
  Namespace: app.root.UpdateUser
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • NOT_ROOT_OR_ADMIN           — нет прав Root или Admin (через Perm.UpdateUserRoot)
      message: "user is not root or admin"
             | "not permission root or admin"
    • NOT_ROOT                    — нет прав Root (при обновлении root-пользователя)
      message: "not permission root"
    • NOT_FOUND                   — пользователь не найден
      message: "user not found"
    • VALIDATION_FAILED           — ошибка валидации email/name
      message: "validation failed"
    • INTERNAL_ERROR              — ошибка обновления имени в БД
      message: "failed to update name"
    • INTERNAL_ERROR              — ошибка обновления email в БД
      message: "failed to update email"
    • INTERNAL_ERROR              — ошибка обновления ACL в БД
      message: "failed to update acl"

Метод: Root.Blocked
  Путь HTTP: GET /v1/root/users/block/:id
  gRPC:      RootService.BlockUser
  Namespace: app.root.Blocked
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • NOT_ROOT_OR_ADMIN           — нет прав (через Perm.BlockOrDeleteRoot)
      message: "user is not root or admin"
    • NOT_ROOT                    — нет прав Root (при блокировке root-пользователя)
      message: "not permission root"
    • NOT_FOUND                   — пользователь не найден
      message: "user not found"
    • INTERNAL_ERROR              — ошибка блокировки в БД
      message: "failed to block user"

Метод: Root.BlockedArray
  Путь HTTP: PUT /v1/root/users/selected/blocked
  gRPC:      RootService.BlockUsersArray
  Namespace: app.root.BlockedArray
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • PERMISSION_DENIED           — нет прав ни на одного пользователя из списка
      message: "no users to block: permission denied for all requested IDs"
    • INTERNAL_ERROR              — ошибка блокировки в транзакции
      message: "failed to block user"

Метод: Root.UnBlocked
  Путь HTTP: GET /v1/root/users/unblock/:id
  gRPC:      RootService.UnblockUser
  Namespace: app.root.UnBlocked
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • NOT_ROOT_OR_ADMIN           — нет прав (через Perm.BlockOrDeleteRoot)
      message: "user is not root or admin"
    • NOT_ROOT                    — нет прав Root (при разблокировке root-пользователя)
      message: "not permission root"
    • NOT_FOUND                   — пользователь не найден
      message: "user not found"
    • INTERNAL_ERROR              — ошибка разблокировки в БД
      message: "failed to unblock user"

Метод: Root.UnBlockedArray
  Путь HTTP: PUT /v1/root/users/selected/unblocked
  gRPC:      RootService.UnblockUsersArray
  Namespace: app.root.UnBlockedArray
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • PERMISSION_DENIED           — нет прав ни на одного пользователя из списка
      message: "no users to unblock: permission denied for all requested IDs"
    • INTERNAL_ERROR              — ошибка разблокировки в транзакции
      message: "failed to unblock user"

Метод: Root.Delete
  Путь HTTP: DELETE /v1/root/users/delete/:id
  gRPC:      RootService.DeleteUser
  Namespace: app.root.Delete
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • NOT_ROOT_OR_ADMIN           — нет прав (через Perm.BlockOrDeleteRoot)
      message: "user is not root or admin"
    • NOT_ROOT                    — нет прав Root (при удалении root-пользователя)
      message: "not permission root"
    • NOT_FOUND                   — пользователь не найден
      message: "user not found"
    • INTERNAL_ERROR              — ошибка удаления в БД
      message: "failed to delete user"

Метод: Root.DeleteArray
  Путь HTTP: DELETE /v1/root/users/selected/deleted
  gRPC:      RootService.DeleteUsersArray
  Namespace: app.root.DeleteArray
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • PERMISSION_DENIED           — нет прав ни на одного пользователя из списка
      message: "no users to delete: permission denied for all requested IDs"
    • INTERNAL_ERROR              — ошибка удаления в транзакции
      message: "failed to delete user"

Метод: Root.UsersList
  Путь HTTP: GET /v1/root/users/list
  gRPC:      RootService.ListUsers
  Namespace: app.root.UserList
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • NOT_ROOT_OR_ADMIN           — нет прав Root или Admin
      message: "user is not root or admin"
    • INTERNAL_ERROR              — ошибка получения списка из БД
      message: "failed to list users"

Метод: Root.RootChangePassword
  Путь HTTP: PUT /v1/root/users/password/:id
  gRPC:      RootService.ChangePasswordAdmin
  Namespace: app.root.RootChangePassword
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)
    • NOT_ROOT_OR_ADMIN           — нет прав Root или Admin
      message: "user is not root or admin"
    • PASSWORD_WEAK               — пароль не прошёл валидацию
      message: "password validation failed"
      cause: "insecure password, try including more special characters, using longer passwords, mixing up letters, capitalization, and numbers"
    • INTERNAL_ERROR              — ошибка хеширования пароля
      message: "failed to hash password"
    • INTERNAL_ERROR              — ошибка обновления пароля в БД
      message: "failed to update password"

────────────────────────────────────────────────────────────────────────────────
2.4  Perm (проверка прав доступа) — namespace: app.perm
────────────────────────────────────────────────────────────────────────────────

Метод: Perm.GetUserFromContext
  Namespace: app.perm.GetUserFromContext
  Возможные ошибки:
    • TOKEN_INVALID               — ошибка парсинга токена
      message: "failed to parse token" + (см. ParseToken)
    • SESSION_INVALID             — сессия невалидна
      message: (см. ParseToken)
    • PERMISSION_DENIED           — у пользователя нет прав (ACL = 0)
      message: "user does not have permission"

Метод: Perm.User
  Namespace: app.perm.User
  Возможные ошибки:
    • (все ошибки GetUserFromContext)
    • NOT_FOUND                   — пользователь не найден в БД
      message: "user not found"

Метод: Perm.RootOrAdmin
  Namespace: app.perm.RootOrAdmin
  Возможные ошибки:
    • (все ошибки GetUserFromContext)
    • NOT_ROOT_OR_ADMIN           — пользователь не Root и не Admin
      message: "user is not root or admin"

Метод: Perm.Root
  Namespace: app.perm.Root
  Возможные ошибки:
    • (все ошибки GetUserFromContext)
    • NOT_ROOT                    — пользователь не Root
      message: "user is not root"

Метод: Perm.Admin
  Namespace: app.perm.Admin
  Возможные ошибки:
    • (все ошибки GetUserFromContext)
    • NOT_ADMIN                   — пользователь не Admin
      message: "user is not admin"

Метод: Perm.CreateUserRoot
  Namespace: app.perm.CreateUserRoot
  Возможные ошибки:
    • NOT_ROOT_OR_ADMIN           — нет прав Root или Admin
      message: "user is not root or admin"
             | "not permission root or admin"
    • NOT_ROOT                    — попытка создать root-пользователя без роли Root
      message: "not permission root"

Метод: Perm.UpdateUserRoot
  Namespace: app.perm.UpdateUserRoot
  Возможные ошибки:
    • NOT_ROOT_OR_ADMIN           — нет прав Root или Admin
      message: "user is not root or admin"
             | "not permission root or admin"
    • NOT_FOUND                   — целевой пользователь не найден
      message: "user not found"
    • NOT_ROOT                    — попытка обновить root-пользователя без роли Root
      message: "not permission root"

Метод: Perm.BlockOrDeleteRoot
  Namespace: app.perm.BlockOrDeleteRoot
  Возможные ошибки:
    • NOT_ROOT_OR_ADMIN           — нет прав Root или Admin
      message: "user is not root or admin"
             | "not permission root or admin"
    • NOT_FOUND                   — целевой пользователь не найден
      message: "user not found"
    • NOT_ROOT                    — попытка заблокировать/удалить root-пользователя без роли Root
      message: "not permission root"

────────────────────────────────────────────────────────────────────────────────
2.5  Check (проверка состояния) — namespace: app.check
────────────────────────────────────────────────────────────────────────────────

Метод: Check.Health
  Путь HTTP: GET /v1/health
  gRPC:      CheckService.Health
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)

Метод: Check.Readiness
  Путь HTTP: GET /v1/health/ready
  gRPC:      CheckService.Readiness
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)

Метод: Check.Liveness
  Путь HTTP: GET /v1/health/live
  gRPC:      CheckService.Liveness
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)

────────────────────────────────────────────────────────────────────────────────
2.6  Info (справочная информация) — namespace: app.info
────────────────────────────────────────────────────────────────────────────────

Метод: Info.Acl
  Путь HTTP: GET /v1/info/acl
  gRPC:      InfoService.GetAclInfo
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)

────────────────────────────────────────────────────────────────────────────────
2.8  Orgs (организации) — namespace: app.orgs
────────────────────────────────────────────────────────────────────────────────

Метод: Orgs.List
  Namespace: app.orgs.List
  Возможные ошибки:
    • INTERNAL_ERROR              — ошибка получения списка организаций
      message: "failed to list orgs"
      cause: postgres.orgs.get failed.list_paginated.scan
      Пример полного сообщения:
        "app.orgs.List: failed to list orgs, cause: postgres.orgs.get
         failed.list_paginated.scan: can't scan into dest[4] (col: date_ogrn):
         cannot scan date (OID 1082) in binary format into *string"
      Причина: несоответствие типа колонки date_ogrn (date) в PostgreSQL
               и типа поля (*string) в Go-модели. Требуется исправление
               на стороне сервера (использовать time.Time или pgtype.Date).
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов
      message: (см. RateLimiter.Check)

────────────────────────────────────────────────────────────────────────────────
2.9  RateLimiter (ограничение частоты запросов) — namespace: app.ratelimit
────────────────────────────────────────────────────────────────────────────────

RateLimiter.Check вызывается в начале каждого метода.
  Namespace: app.ratelimit.<operation>
  Возможные ошибки:
    • RATE_LIMIT_EXCEEDED         — превышен лимит запросов для операции
      message: "rate limit exceeded, retry after N seconds"
    • RATE_LIMIT_EXCEEDED         — IP клиента отсутствует (запрос блокируется)
      message: "client IP is required"
    • RATE_LIMIT_EXCEEDED         — IP временно заблокирован (многократные нарушения)
      message: "IP temporarily blocked, retry after N seconds"


================================================================================
  3. ОШИБКИ ТРАНСПОРТНОГО УРОВНЯ HTTP
================================================================================

Помимо ошибок app-слоя, HTTP хендлеры могут возвращать:

INVALID_INPUT (HTTP 400) — при ошибке парсинга JSON тела запроса:
  • ShouldBindJSON() ошибка — некорректные/отсутствующие обязательные поля
  • ShouldBindQuery() ошибка — некорректные параметры запроса
  • uuid.Parse() ошибка — невалидный UUID в path-параметре :id

Эти ошибки генерируются до вызова app-слоя и имеют формат:
  ErrorCode: INVALID_INPUT
  HTTP Status: 400 Bad Request

Полный список хендлерных ошибок INVALID_INPUT по эндпоинтам:

  POST  /v1/auth/login            — ошибка парсинга LoginInput (email, password)
  POST  /v1/auth/register         — ошибка парсинга RegisterUser (email, name, password)
  POST  /v1/auth/verify           — ошибка парсинга VerifyEmailInput (email, code)
  POST  /v1/auth/refresh          — ошибка парсинга RefreshTokenInput (refresh_token, access_token)
  PUT   /v1/user/password         — ошибка парсинга ChangePasswordInput (old, new, confirm)
  POST  /v1/root/users/create     — ошибка парсинга CreateUserRootInput (email, name)
  GET   /v1/root/users/list       — ошибка парсинга параметров пагинации
  GET   /v1/root/users/read/:id   — невалидный UUID
  PUT   /v1/root/users/update/:id — невалидный UUID / ошибка парсинга RootUpdateUserInput
  GET   /v1/root/users/block/:id  — невалидный UUID
  GET   /v1/root/users/unblock/:id — невалидный UUID
  DELETE /v1/root/users/delete/:id — невалидный UUID
  PUT   /v1/root/users/password/:id — невалидный UUID / ошибка парсинга RootChangePasswordInput
  PUT   /v1/root/users/selected/blocked   — ошибка парсинга UUIDArrayInput
  PUT   /v1/root/users/selected/unblocked — ошибка парсинга UUIDArrayInput
  DELETE /v1/root/users/selected/deleted  — ошибка парсинга UUIDArrayInput


================================================================================
  4. ОШИБКИ ТРАНСПОРТНОГО УРОВНЯ gRPC
================================================================================

Помимо ошибок app-слоя, gRPC хендлеры могут возвращать:

INVALID_INPUT (gRPC InvalidArgument) — при ошибке парсинга параметров:
  • uuid.Parse() ошибка — невалидный UUID в полях запроса
  • parseUUIDs() ошибка — невалидные UUID в массиве

Эти ошибки генерируются до вызова app-слоя и имеют формат:
  gRPC Status Code: InvalidArgument (3)
  Status Message: "INVALID_INPUT: <описание>"

Полный список gRPC-хендлерных ошибок INVALID_INPUT по методам:

  RootService.GetUser           — невалидный UUID (поле id)
  RootService.UpdateUser        — невалидный UUID (поле id)
  RootService.BlockUser         — невалидный UUID (поле id)
  RootService.UnblockUser       — невалидный UUID (поле id)
  RootService.DeleteUser        — невалидный UUID (поле id)
  RootService.ChangePasswordAdmin — невалидный UUID (поле id)
  RootService.BlockUsersArray   — невалидный UUID в массиве ids
  RootService.UnblockUsersArray — невалидный UUID в массиве ids
  RootService.DeleteUsersArray  — невалидный UUID в массиве ids


================================================================================
  5. СВОДНАЯ ТАБЛИЦА: МЕТОД → ВОЗМОЖНЫЕ КОДЫ ОШИБОК
================================================================================

┌─────────────────────────────────────┬─────────────────────────────────────────────────────────┐
│ Метод                               │ Возможные ErrorCode                                     │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Auth.RegisterUser                   │ RATE_LIMIT_EXCEEDED, VALIDATION_FAILED,                 │
│                                     │ USER_ALREADY_EXISTS, INTERNAL_ERROR                     │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Auth.LogIn                          │ RATE_LIMIT_EXCEEDED, VALIDATION_FAILED,                 │
│                                     │ USER_NOT_FOUND, INVALID_CREDENTIALS,                    │
│                                     │ USER_BLOCKED, TOKEN_EXPIRED,                            │
│                                     │ EMAIL_NOT_VERIFIED, INTERNAL_ERROR                      │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Auth.RefreshToken                   │ RATE_LIMIT_EXCEEDED, VALIDATION_FAILED,                 │
│                                     │ TOKEN_INVALID, SESSION_INVALID,                         │
│                                     │ USER_NOT_FOUND, USER_BLOCKED, INTERNAL_ERROR            │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Auth.LogOut                         │ RATE_LIMIT_EXCEEDED, TOKEN_INVALID,                     │
│                                     │ INTERNAL_ERROR                                          │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Auth.LogOutAll                      │ RATE_LIMIT_EXCEEDED, TOKEN_INVALID,                     │
│                                     │ SESSION_INVALID, INTERNAL_ERROR                         │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Auth.VerifyEmail                    │ RATE_LIMIT_EXCEEDED, VALIDATION_FAILED,                 │
│                                     │ VERIFICATION_CODE_INVALID,                              │
│                                     │ VERIFICATION_CODE_EXPIRED,                              │
│                                     │ EMAIL_ALREADY_VERIFIED, INTERNAL_ERROR                  │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Auth.ResendVerificationEmail        │ RATE_LIMIT_EXCEEDED, VALIDATION_FAILED,                 │
│                                     │ USER_NOT_FOUND, USER_BLOCKED,                           │
│                                     │ EMAIL_ALREADY_VERIFIED, INTERNAL_ERROR                  │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Auth.RequestPasswordReset           │ RATE_LIMIT_EXCEEDED, VALIDATION_FAILED,                 │
│                                     │ USER_BLOCKED, INTERNAL_ERROR                            │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Auth.GetActiveSessionsByUser        │ RATE_LIMIT_EXCEEDED, TOKEN_INVALID,                     │
│                                     │ SESSION_INVALID, INTERNAL_ERROR                         │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Users.Get                           │ RATE_LIMIT_EXCEEDED, TOKEN_INVALID,                     │
│                                     │ PERMISSION_DENIED, NOT_FOUND                            │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Users.UpdateName                    │ RATE_LIMIT_EXCEEDED, TOKEN_INVALID,                     │
│                                     │ PERMISSION_DENIED, VALIDATION_FAILED,                   │
│                                     │ INTERNAL_ERROR                                          │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Users.ChangePassword                │ RATE_LIMIT_EXCEEDED, TOKEN_INVALID,                     │
│                                     │ PERMISSION_DENIED, NOT_FOUND,                           │
│                                     │ VALIDATION_FAILED, INVALID_CREDENTIALS,                 │
│                                     │ INTERNAL_ERROR                                          │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Users.Blocked                       │ RATE_LIMIT_EXCEEDED, TOKEN_INVALID,                     │
│                                     │ PERMISSION_DENIED, INTERNAL_ERROR                       │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Root.CreateUser                     │ RATE_LIMIT_EXCEEDED, NOT_ROOT_OR_ADMIN,                 │
│                                     │ NOT_ROOT, VALIDATION_FAILED,                            │
│                                     │ USER_ALREADY_EXISTS, INTERNAL_ERROR                     │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Root.GetUser                        │ RATE_LIMIT_EXCEEDED, NOT_ROOT_OR_ADMIN,                 │
│                                     │ NOT_FOUND                                               │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Root.UpdateUser                     │ RATE_LIMIT_EXCEEDED, NOT_ROOT_OR_ADMIN,                 │
│                                     │ NOT_ROOT, NOT_FOUND, VALIDATION_FAILED,                 │
│                                     │ INTERNAL_ERROR                                          │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Root.Blocked                        │ RATE_LIMIT_EXCEEDED, NOT_ROOT_OR_ADMIN,                 │
│                                     │ NOT_ROOT, NOT_FOUND, INTERNAL_ERROR                     │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Root.BlockedArray                   │ RATE_LIMIT_EXCEEDED, PERMISSION_DENIED,                 │
│                                     │ INTERNAL_ERROR                                          │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Root.UnBlocked                      │ RATE_LIMIT_EXCEEDED, NOT_ROOT_OR_ADMIN,                 │
│                                     │ NOT_ROOT, NOT_FOUND, INTERNAL_ERROR                     │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Root.UnBlockedArray                 │ RATE_LIMIT_EXCEEDED, PERMISSION_DENIED,                 │
│                                     │ INTERNAL_ERROR                                          │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Root.Delete                         │ RATE_LIMIT_EXCEEDED, NOT_ROOT_OR_ADMIN,                 │
│                                     │ NOT_ROOT, NOT_FOUND, INTERNAL_ERROR                     │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Root.DeleteArray                    │ RATE_LIMIT_EXCEEDED, PERMISSION_DENIED,                 │
│                                     │ INTERNAL_ERROR                                          │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Root.UsersList                      │ RATE_LIMIT_EXCEEDED, NOT_ROOT_OR_ADMIN,                 │
│                                     │ INTERNAL_ERROR                                          │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Root.RootChangePassword             │ RATE_LIMIT_EXCEEDED, NOT_ROOT_OR_ADMIN,                 │
│                                     │ PASSWORD_WEAK, INTERNAL_ERROR                           │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Check.Health                        │ RATE_LIMIT_EXCEEDED                                     │
│ Check.Readiness                     │ RATE_LIMIT_EXCEEDED                                     │
│ Check.Liveness                      │ RATE_LIMIT_EXCEEDED                                     │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Info.Acl                            │ RATE_LIMIT_EXCEEDED                                     │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Orgs.List                           │ RATE_LIMIT_EXCEEDED, INTERNAL_ERROR                     │
├─────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ HTTP транспорт (все хендлеры)       │ INVALID_INPUT (парсинг JSON/query/UUID)                 │
│ gRPC транспорт (Root хендлеры)      │ INVALID_INPUT (парсинг UUID)                            │
└─────────────────────────────────────┴─────────────────────────────────────────────────────────┘


================================================================================
  6. ОШИБКИ СЛОЯ STORAGE (PostgreSQL) — передаются в APP
================================================================================

Ошибки storage-слоя НЕ возвращаются клиенту напрямую. Они оборачиваются
в app-слое в *AppError с соответствующим ErrorCode (обычно INTERNAL_ERROR
или NOT_FOUND). Ниже перечислены все ошибки, которые storage может породить
и которые app-слой обрабатывает.

Namespace ошибок storage: postgres.*

────────────────────────────────────────────────────────────────────────────────
6.1  Storage Init — namespace: postgres.init
────────────────────────────────────────────────────────────────────────────────

  • connect failed           — не удалось подключиться к PostgreSQL
  • db ping failed           — не удалось выполнить ping к БД
  • run migrations failed    — ошибка выполнения миграций
  • config db url not in config file — URL БД не указан в конфигурации
  • password for db is nil   — пароль БД не указан

  Эти ошибки возникают при старте приложения и НЕ доходят до клиента.

────────────────────────────────────────────────────────────────────────────────
6.2  Users — namespace: postgres.users
────────────────────────────────────────────────────────────────────────────────

  Константа: WithIdNotFound = "with id not found"

  Метод storage              │ Ошибка errorx                          │ Как оборачивается в app
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.List                 │ users.get failed.list.query            │ (не используется в app напрямую)
                             │ users.get failed.list.scan             │
                             │ users.get failed.list.rows error       │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.ListPaginated        │ users.get failed.list_paginated.count  │ → INTERNAL_ERROR (Root.UsersList)
                             │ users.get failed.list_paginated.query  │
                             │ users.get failed.list_paginated.scan   │
                             │ users.get failed.list_paginated.rows   │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.GetEmail             │ users.get failed.email.scan            │ → USER_ALREADY_EXISTS (RegisterUser, CreateUser)
                             │ pgx.ErrNoRows = пользователь не найден │   если err==nil, значит пользователь уже есть
                             │                                        │ → INVALID_CREDENTIALS / USER_NOT_FOUND (LogIn)
                             │                                        │ → USER_NOT_FOUND (ResendVerificationEmail)
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.Get                  │ users.get failed.id.scan               │ → NOT_FOUND (Perm.User, Perm.UpdateUserRoot,
                             │ pgx.ErrNoRows = пользователь не найден │   Perm.BlockOrDeleteRoot, Root.GetUser)
                             │                                        │ → USER_NOT_FOUND (Auth.RefreshToken)
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.Create               │ users.create failed                    │ → INTERNAL_ERROR (RegisterUser, CreateUser)
                             │ (unique violation = duplicate email)   │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.UpdateAll            │ users.update failed                    │ → INTERNAL_ERROR
                             │ users.update failed (WithIdNotFound)   │   (запись не найдена по id)
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.UpdateName           │ users.update user failed               │ → INTERNAL_ERROR (Users.UpdateName,
                             │ users.update user failed               │   Root.UpdateUser)
                             │   (WithIdNotFound)                     │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.UpdateEmail          │ users.update email failed              │ → INTERNAL_ERROR (Root.UpdateUser)
                             │ users.update email failed              │
                             │   (WithIdNotFound)                     │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.UpdateAcl            │ users.update acl failed                │ → INTERNAL_ERROR (Root.UpdateUser)
                             │ users.update acl failed                │
                             │   (WithIdNotFound)                     │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.UpdatePassword       │ users.update failed.password           │ → INTERNAL_ERROR (Users.ChangePassword,
                             │ users.update failed.password           │   Root.RootChangePassword)
                             │   (WithIdNotFound)                     │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.Delete               │ users.delete failed                    │ → INTERNAL_ERROR (Root.Delete)
                             │ users.delete failed (WithIdNotFound)   │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.DeleteTx             │ users.delete failed                    │ → INTERNAL_ERROR (Root.DeleteArray)
                             │ users.delete failed (WithIdNotFound)   │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.Blocked              │ users.update failed.blocked.exec       │ → INTERNAL_ERROR (Root.Blocked,
                             │ users.update failed.blocked            │   Root.UnBlocked, Users.Blocked)
                             │   (WithIdNotFound)                     │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.BlockedTx            │ users.update failed.blocked.exec       │ → INTERNAL_ERROR (Root.BlockedArray,
                             │ users.update failed.blocked            │   Root.UnBlockedArray)
                             │   (WithIdNotFound)                     │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.Vemail               │ users.update failed.verytify email.exec│ → INTERNAL_ERROR (Auth.VerifyEmail)
                             │ users.update failed.verytify email     │
                             │   (WithIdNotFound)                     │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.SetVerificationCode  │ users.update failed.set verification   │ → INTERNAL_ERROR (RegisterUser,
                             │   code.exec                            │   ResendVerificationEmail)
                             │ users.update failed.set verification   │
                             │   code (WithIdNotFound)                │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.GetByEmailAndCode    │ users.get failed.verification          │ → VERIFICATION_CODE_INVALID
                             │   code.scan                            │   (Auth.VerifyEmail)
                             │ pgx.ErrNoRows = код не найден          │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.SetTemporaryPassword │ users.update failed.set temporary      │ → INTERNAL_ERROR (RequestPasswordReset,
                             │   password.exec                        │   Root.CreateUser)
                             │ users.update failed.set temporary      │
                             │   password (WithIdNotFound)            │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.ClearTemporaryPasswd │ users.update failed.clear temporary    │ (вызывается из cleanup, не из app)
                             │   password.exec                        │
                             │ users.update failed.clear temporary    │
                             │   password (WithIdNotFound)            │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Users.HasUsers             │ (стандартная pgx ошибка)               │ → INTERNAL_ERROR (RegisterUser)

────────────────────────────────────────────────────────────────────────────────
6.3  Keystore — namespace: postgres.keystore
────────────────────────────────────────────────────────────────────────────────

  Метод storage              │ Ошибка errorx                          │ Как оборачивается в app
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  CreateKeystore             │ create keystore: failed to create      │ → INTERNAL_ERROR
                             │                                        │   (Auth.GenerateTokenWithContext)
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  FindKeystoreByPrimaryKey   │ find keystore by primary key:          │ → SESSION_INVALID
                             │   keystore not found                   │   (Auth.ParseToken)
                             │ pgx.ErrNoRows = не найден              │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  FindKeystoreForRefresh     │ find keystore for refresh:             │ → SESSION_INVALID
                             │   keystore not found for refresh       │   (Auth.RefreshToken)
                             │ pgx.ErrNoRows = не найден              │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  RevokeKeystore             │ revoke keystore: failed to revoke      │ → INTERNAL_ERROR
                             │ revoke keystore: keystore not found    │   (Auth.RefreshToken)
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  RevokeKeystoreByPrimaryKey │ revoke keystore: failed to revoke      │ → INTERNAL_ERROR
                             │   by primary key                       │   (Auth.LogOut)
                             │ revoke keystore: keystore not found    │
                             │   or already revoked                   │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  RevokeAllUserKeystores     │ revoke all user keystores: failed      │ → INTERNAL_ERROR
                             │                                        │   (Auth.LogOutAll)
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  GetActiveKeystoresByUser   │ get active keystores by user: failed   │ → INTERNAL_ERROR
                             │   to get active keystores              │   (Auth.GetActiveSessionsByUser)
                             │ get active keystores by user: failed   │
                             │   to scan keystore                     │
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  CleanupExpiredKeystores    │ cleanup expired keystores: failed      │ (вызывается из cleanup job,
                             │                                        │  не доходит до клиента)

────────────────────────────────────────────────────────────────────────────────
6.4  Orgs — namespace: postgres.orgs
────────────────────────────────────────────────────────────────────────────────

  Метод storage              │ Ошибка errorx                          │ Как оборачивается в app
  ───────────────────────────┼────────────────────────────────────────┼─────────────────────────────────
  Orgs.ListPaginated         │ orgs.get failed.list_paginated.count   │ → INTERNAL_ERROR (Orgs.List)
                             │ orgs.get failed.list_paginated.query   │
                             │ orgs.get failed.list_paginated.scan    │
                             │ orgs.get failed.list_paginated.rows    │

  Известная проблема: колонка date_ogrn имеет тип date в PostgreSQL,
  но Go-модель использует *string для этого поля — pgx не может сканировать
  date (OID 1082) в *string в бинарном формате.

────────────────────────────────────────────────────────────────────────────────
6.5  WithTransaction
────────────────────────────────────────────────────────────────────────────────

  • begin tx: <pgx error>    — ошибка начала транзакции → fmt.Errorf
  • <fn error>               — ошибка внутри транзакции → Rollback + возврат ошибки
  • commit error             — ошибка коммита транзакции

  Используется в: Root.BlockedArray, Root.UnBlockedArray, Root.DeleteArray
  В app оборачивается как INTERNAL_ERROR или PERMISSION_DENIED (в зависимости
  от ошибки внутри fn).


================================================================================
  ПРИМЕЧАНИЯ
================================================================================

1. Все ошибки app-слоя возвращаются как *AppError с полем Code (ErrorCode).
   Транспортный слой автоматически преобразует ErrorCode в HTTP Status / gRPC Code.

2. В режиме production (release mode) при логине:
   - Ошибки USER_NOT_FOUND, USER_BLOCKED, TOKEN_EXPIRED заменяются на
     INVALID_CREDENTIALS для защиты от перебора.
   - В debug/test режиме возвращаются точные коды для отладки.

3. При вызове RequestPasswordReset с несуществующим email — ошибка НЕ
   возвращается клиенту (мера безопасности — не раскрывается, существует ли email).

4. Все middleware-ошибки (Recovery) возвращают INTERNAL_ERROR.

5. Код RATE_LIMIT_EXCEEDED содержит в message:
   - "rate limit exceeded, retry after N seconds" — превышен лимит
   - "client IP is required" — IP не определён
   - "IP temporarily blocked, retry after N seconds" — IP заблокирован
