interface ErrorDefinition { status: number; message: string; retryable: boolean; } export const ERROR_DEFINITIONS = Object.freeze({ CONTROL_UNREACHABLE: { status: 503, message: 'Harbor сейчас недоступен.', retryable: true }, REQUEST_INVALID: { status: 400, message: 'Запрос содержит некорректные данные.', retryable: false }, ENDPOINT_NOT_FOUND: { status: 404, message: 'Запрошенный API-метод не найден.', retryable: false }, SUBSCRIPTION_INVALID: { status: 400, message: 'Ссылка подписки недействительна.', retryable: false }, SUBSCRIPTION_EXPIRED: { status: 400, message: 'Срок действия подписки истёк.', retryable: false }, SUBSCRIPTION_TRAFFIC_EXHAUSTED: { status: 400, message: 'Трафик по подписке закончился.', retryable: false }, SUBSCRIPTION_DISABLED: { status: 400, message: 'Подписка отключена провайдером.', retryable: false }, SUBSCRIPTION_REJECTED: { status: 400, message: 'Провайдер отклонил подписку.', retryable: false }, PROVIDER_UNAVAILABLE: { status: 502, message: 'Провайдер подписки временно недоступен.', retryable: true }, STATE_CONFLICT: { status: 409, message: 'Данные изменились во время операции.', retryable: true }, SERVER_NOT_FOUND: { status: 404, message: 'Выбранный сервер больше недоступен.', retryable: false }, DEVICE_NOT_FOUND: { status: 404, message: 'Устройство больше недоступно.', retryable: false }, DEVICE_IDENTITY_AMBIGUOUS: { status: 409, message: 'Gateway не может безопасно применить маршрут к этому устройству.', retryable: true }, DEVICE_POLICY_APPLY_FAILED: { status: 503, message: 'Не удалось применить маршрут устройства.', retryable: true }, DIAGNOSTICS_FAILED: { status: 503, message: 'Не удалось проверить маршруты. Попробуйте ещё раз.', retryable: true }, CONFIG_INVALID: { status: 422, message: 'Конфигурация VPN недействительна.', retryable: false }, PROCESS_START_FAILED: { status: 503, message: 'Не удалось запустить VPN-процесс.', retryable: true }, OPERATION_IN_PROGRESS: { status: 409, message: 'Другая операция ещё выполняется.', retryable: true }, UNKNOWN: { status: 500, message: 'Не удалось выполнить действие.', retryable: false }, } satisfies Record); export type HarborErrorCode = keyof typeof ERROR_DEFINITIONS; export function errorDefinition(code: unknown): ErrorDefinition { return typeof code === 'string' && Object.hasOwn(ERROR_DEFINITIONS, code) ? ERROR_DEFINITIONS[code as HarborErrorCode] : ERROR_DEFINITIONS.UNKNOWN; } export class HarborError extends Error { code: HarborErrorCode; status: number; retryable: boolean; details: unknown; constructor(code: string, { cause, details }: { cause?: unknown; details?: unknown } = {}) { const definition = errorDefinition(code); super(definition.message, { cause }); this.name = 'HarborError'; this.code = Object.hasOwn(ERROR_DEFINITIONS, code) ? code as HarborErrorCode : 'UNKNOWN'; this.status = definition.status; this.retryable = definition.retryable; this.details = details; } } export function normalizeHarborError(error: unknown) { return error instanceof HarborError ? error : new HarborError('UNKNOWN', { cause: error }); }