Protection#

eazy_sdk.protection содержит прикладной API защит ответа: протоколы решателя и контекста, конструктор challenge_guard(), назначения решения, правила повтора и ошибки стадий. Контракты компилятора, реестра и хранения доступны только через eazy_sdk.protection.advanced. Там же находятся ChallengeParseError и AmbiguousChallengeError.

Устанавливаемые защиты#

Символ

Контракт

ChallengeSolver[TChallenge, TSolution]

Асинхронный протокол solve(challenge, context) -> solution

SolveContext

Операция, неизменяемый контекст ответа, номер попытки, deadline, fetch, identity и request_headers

ProtectedFetch

Асинхронный порт (url, *, method, headers, body) -> NormalizedResponse поверх транспортной сессии клиента

TransportIdentity

Неизменяемые сведения user_agent, proxy, impersonation с закрытым repr и методом fingerprint()

InstallableProtection

Структурный объект для Security.of(...) и with_protection()

Guard[TChallenge]

Защита одним классом: detect(), синхронный или асинхронный solve(), self.solution(...), параметры класса scope, cache, replay, headers, query, body

GuardSolution

Группа cookies, объявленных заголовков, query, body и expires_at, которую создаёт Guard.solution()

challenge_guard()

Понижает детектор, решатель, отображение решения, кеш и правило повтора

GuardCache

Значения "none", "call", "session" для хранения на совпадение, вызов или сессию до отказа

host(), operation()

Области защиты; по умолчанию используется RequestScope() для каждого запроса

class VendorGuard(Guard[VendorChallenge]):
    scope = host("api.example")

    def detect(self, response: ResponseContext[object]) -> VendorChallenge | None:
        return detect_challenge(response)

    async def solve(self, challenge: VendorChallenge, context: SolveContext) -> GuardSolution:
        return self.solution(cookies={"clearance": await vendor.solve(challenge)})


config = ClientConfig(security=Security.of(VendorGuard()))

Функциональная форма принимает challenge_guard(detect=..., solver=..., apply=solution_fields(...), name=..., scope=..., cache=..., replay=...). Имя по умолчанию берётся из функции детектора или класса-владельца, область охватывает каждый запрос, кеш равен "none". Решателем может быть объект с синхронным или асинхронным solve() либо функция (challenge, context) -> solution. Обе формы возвращают неизменяемое устанавливаемое описание и не читают ответ или сеть во время настройки. guards= и with_protection() понижают их одинаково.

client.invalidate_protection(*targets) удаляет сохранённые решения указанных защит или политик. Вызов без аргументов очищает всё состояние; результат равен числу удалённых записей.

Детектор получает ResponseContext[object] и даёт TChallenge | None. Значение None означает отсутствие совпадения. Повреждённую распознанную проверку обозначайте MalformedChallengeError. Любая другая ошибка детектора становится ChallengeDetectionError и сохраняет исходное исключение в __cause__.

Контекст решения#

SolveContext предоставляет решателю сведения о транспорте и ограниченный доступ к нему, но не раскрывает handler, хранилище cookies или клиент.

Поле

Контракт

operation

Идентификатор защищаемой операции

response

Неизменяемый контекст ответа с проверкой; None для предварительных и обязательных потоков

attempt

Номер исходной попытки; 0 для обязательного потока

deadline

Срок решения от начала вызова; None без timeout, remaining() не возвращает отрицательное значение

fetch

ProtectedFetch, привязанный к тому же handler, прокси и хранилищу cookies

identity

TransportIdentity: публичный User-Agent, объявленные прокси и профиль имитации

request_headers

Заголовки фактически отправленного запроса

fetch обращается напрямую к handler. Защиты, реакции, повторы, middleware и ограничитель частоты не запускаются, поэтому решатель не входит повторно в вызвавший его конвейер.

  • Перенаправления не выполняются. Возвращается исходный ответ 3xx.

  • Каждый запрос получает полный timeout защищаемого вызова. Для общего бюджета решатель читает context.remaining().

  • Запрос использует cookies сессии, а полученные Set-Cookie сохраняются в то же хранилище.

  • URL должен быть абсолютным. identity.user_agent и Host добавляются, если вызывающий их не задал.

Ошибка транспорта или решателя становится ChallengeSolveError с исходным исключением в __cause__.

Сохранённое решение связано с identity.fingerprint(). Перед применением runtime сравнивает этот отпечаток с текущей попыткой. Другой User-Agent, прокси или профиль имитации отменяет состояние; следующая проверка решается заново под новой транспортной идентичностью. Вручную созданный SolveContext имеет пустую идентичность, а его fetch вызывает ProtectionConfigurationError.

У попытки ровно один источник транспортной идентичности. user_agent берётся из публичного User-Agent после значений вызывающего, авторизации и зависимостей, но до применения решения. proxy и impersonation читает HandlerProfile. Заголовки защиты не меняют отпечаток, хотя context.request_headers показывает фактически отправленные заголовки.

Назначения решения#

solution_fields() связывает имена назначения с полями типизированного решения.

Параметр

Назначение

headers={"X-Token": "token"}

Фиксированный заголовок запроса

query={"challenge": "token"}

Фиксированное поле query

cookies={"clearance": "token"}

Фиксированный cookie запроса

body={"captcha.token": "token"}

Закрытый путь семантического тела

cookie_set=solution_cookie_set()

Динамический набор именованных cookies

Компилятор резервирует все назначения. Конфликт с вызывающим, авторизацией, зависимостью, подписью или другим закрытым writer обнаруживается до решателя и сети. Runtime сначала выбирает и проверяет всю группу решения, затем применяет одно атомарное изменение. PreparedRequest не изменяется.

Доказательства безопасного повтора#

Функция

Доказательство

safe_method(max_replays=1)

Повтор разрешён только для GET, HEAD и OPTIONS

idempotency_key(name, max_replays=1)

Подготовленный запрос содержит указанный заголовок

rejected_before_execution(max_replays=1)

Протокол доказывает отказ до выполнения

rejected_before_origin(max_replays=1)

Протокол доказывает, что запрос не дошёл до источника

ReplayPolicy.max_replays не может быть отрицательным. Нулевой бюджет запрещает повтор проверки. Новая попытка заново выполняет зависимости, авторизацию, проекцию, шифрование, кодирование, подготовку и подпись.

Управляемое состояние и сессия#

Повторно используемое решение живёт в одном ExecutionRuntime, которым владеет клиент и его handler-сессия. Режимы сессии (cache="session" и расширенные until-expiry или until-rejected) сводят параллельные решения одной проверки к одному вызову. cache="call" и cache="none" оставляют состояние внутри вызова. Ключ состояния включает фактический handler. Закрытие клиента очищает решения и записи согласования.

Прокси, User-Agent, семейство адресов и профиль имитации задаются при создании handler-сессии. Фабрики клиентов настраивают созданную ими сессию и объявляют параметры в HandlerProfile. Для переданной пользователем сессии proxy= лишь описывает уже заданное значение. Пользовательский handler сообщает те же сведения через profile. Eazy SDK не исследует сессию транспорта и не принимает поставщика идентичности на каждую попытку. При смене транспорта создайте новый handler и клиент; их состояние защиты начинается пустым.

Технология решателя не участвует в предварительной проверке. Браузер, JavaScript, WASM и внешний API реализуют один типизированный протокол. До запроса проверяются наличие решателя, назначения, повторяемость тела, доказательство безопасного повтора, конечные бюджеты и точность HandlerProfile.

Ошибки#

Стадия

Ошибка

Условие

Настройка

ProtectionConfigurationError

Повреждена защита или понижение, нет решателя либо конфликтуют назначения

Обнаружение

ChallengeDetectionError

Детектор вызвал неожиданное исключение

Обнаружение

ChallengeParseError из eazy_sdk.protection.advanced

Проверка распознана, но данные непригодны; доступны policy, attempt и причина

Обнаружение

AmbiguousChallengeError из eazy_sdk.protection.advanced

Две окончательные политики совпали с равным приоритетом

Решение

ChallengeSolveError

Решатель завершился ошибкой

Применение

ChallengeApplicationError

Некорректно поле или назначение решения

Повтор

ReplayDeniedError

Недостаточно повторяемости, доказательства или бюджета политики

Ошибки стадий содержат политику и номер попытки, сохраняют __cause__ и не включают тела ответов, значения решения, cookies, учётные данные, прокси или секреты решателя. Бюджет считается отдельно для каждой политики.

Runtime ожидает решатели в обоих клиентах. В синхронном Client асинхронный solve() выполняется в вызывающем потоке без цикла событий, пока не приостанавливается на собственном асинхронном вводе-выводе. Такая приостановка приводит к SynchronousSuspensionError: используйте AsyncClient либо синхронный решатель. Блокировки согласования не привязаны к циклу событий, поэтому общий для нескольких потоков синхронный клиент решает одну проверку сессии один раз.

Расширенный SPI понижения#

eazy_sdk.protection.advanced служит единственным путём импорта контрактов фреймворков и сгенерированных политик. Его __all__ закреплён тестом и содержит 49 имён.

  • Политики: ChallengePolicy, BeforeCallPolicy, challenge_policy(), before_call_policy().

  • Сигналы: ResponseSignal, SignalInterception, SignalMatch, SignalOutcome, AmbiguousSignal, MalformedSignal.

  • Решатели: SolverRequirement, SolverBinding, bind_solver(), SolverBindings, SolverLike.

  • Порты контекста: ProtectedFetch, TransportIdentity.

  • Операции: OperationReference для типизации acquire и verify.

  • Закрытые назначения: PrivateBindings, PrivateBinding, PrivateBindingTarget, PrivateCookieSetBinding, SolutionLocation, private_bindings(), private_header(), private_query(), private_cookie(), private_cookie_set(), private_body().

  • Хранение: ProtectionPersistence, ProtectionPersistenceMode, ProtectionStateKey, ProtectionStateScope, per_match(), per_attempt(), per_call(), until_expiry(), until_rejected(), client_identity(), session_identity(), GuardCache.

  • Повтор: ReplaySafety, BodyReplayPolicy.

  • Обязательные потоки: ProtectionFlow, protection_flow(), FromProtection.

  • Ошибки: ChallengeParseError, AmbiguousChallengeError.

  • Значение понижения: ProtectionBundle.

Записи кеша, реестры блокировок, функции компиляции, арбитраж сигналов и применение решения остаются закрытыми. Прикладной код не должен импортировать расширенный SPI.