s-authkit-client — слой ДОСТУПА
Библиотека, отвечающая на один вопрос: сервис меня узнаёт? Всё, что нужно, чтобы представиться сервису и удержать это представление живым — хранилище сессий, хранилище пары токенов, OAuth-обновление по сроку и живая проба доступа.
Браузера внутри нет: ни импорта, ни зависимости, ни лениво (см. ниже).
pip install s-authkit-client # Python >= 3.11
pip install "s-authkit-client[probe]" # + сетевое колесо для живой пробы
Место в семействе китов
clientkit <- сборщик клиентов по декларации
/ \
authkit-client browserkit <- доступ (кто ты) / чеканка (как войти)
\ /
netkit <- сеть (чем ходить)
|
corekit <- основание (значения и чистые правила)
Обязательная зависимость одна — s-corekit (значения SessionRef,
ExecutionContext, канон диагностики доступа). Соседи сверху и сбоку — отдельные
пакеты, и ни один из них этому киту не нужен.
Что внутри
| модуль | что делает |
|---|---|
authkit_client.sessions |
SessionStore: каталог — источник правды, SQLite — производный индекс без секретов, storage_state под Fernet (DEK в .dek.enc, обёрнут KEK из keyring / env / парольной фразы) |
authkit_client.secret_store |
SecretStore: пара токенов (access/refresh) в keyring с прозрачным file-fallback (Windows CredWrite ломается на длинных JWT — WinError 1783) |
authkit_client.oauth |
OAuthSession: проактивное обновление по TTL (живой токен = ноль сетевых вызовов), single-flight, окончательный OAuthRevoked вместо вечного ретрая |
authkit_client.access_probe |
ЖИВАЯ проба: спросить сервер, а не посмотреть на файл с куками |
authkit_client.session_fingerprint |
инвариант «один egress + один профиль»: сессия, отчеканенная через один выход, не уезжает в другой |
authkit_client.ports |
протоколы SessionStorePort / SecretStorePort / FetchPort / MinterPort и реестр чеканщиков |
authkit_client.context_port |
ссылка на носителя ExecutionContext (мультитенантность), если его завёл вышележащий слой |
authkit_client.diagnosis |
реэкспорт AccessState / AccessVerdict / classify_access из corekit |
Импорт самого кита ничего тяжёлого не исполняет: sqlite3, cryptography,
keyring, platformdirs подтягиваются по месту использования (PEP 562) —
платит тот, кто действительно открыл хранилище или полез в keyring.
Хранилище сессий
import asyncio
from corekit import SessionRef
from authkit_client import SessionStore
store = SessionStore(root="~/.myapp") # состояние ляжет в <root>/sessions/
ref = SessionRef(profile="work", social="example", account_id="me@example.com")
async def main() -> None:
await store.save_state(ref, {"cookies": [], "origins": []}) # шифруется
state = await store.load_state(ref) # расшифровывается
print(store.path_for(ref), await store.list(profile="work"))
asyncio.run(main())
Каталоги 0700 / файлы 0600 выставляются кросс-платформенно (на Windows —
через ACL). storage_state шифруется Fernet-ом: случайный DEK лежит в
.dek.enc, обёрнутый KEK из OS keyring, переменной окружения или парольной
фразы (Argon2id). Открытым текстом состояние молча не пишется никогда: если KEK
недоступен, а среда неинтерактивна — отказ с громким предупреждением.
Пара токенов
from authkit_client import SecretStore
secrets = SecretStore("myapp") # keyring-namespace = "myapp"
secrets.save_tokens("me@example.com", access="…", refresh="…")
access, refresh = secrets.load_tokens("me@example.com")
Порядок чтения: env MYAPP_ACCESS_TOKEN / MYAPP_REFRESH_TOKEN → keyring →
файловый fallback. Fallback включается сам, когда keyring отказал (например,
Windows Credential Manager на blob'е длиннее ~2.5 КБ).
OAuth с проактивным обновлением
from authkit_client import OAuthSession, OAuthTokens
session = OAuthSession(
OAuthTokens(access_token="…", refresh_token="…", expires_at=1893456000.0),
refresher=refresh_call, # async (payload) -> Mapping: чем ходить — дело вызывающего
on_tokens=persist, # куда сохранять обновлённую пару
)
await session.maybe_refresh() # внутри срока — сравнение чисел, ноль запросов
Своего HTTP-клиента модуль не имеет: обмен кода и обмен refresh-токена делает
колбэк потребителя. Отказ invalid_grant считается окончательным — дальнейшие
вызовы падают OAuthRevoked мгновенно, вместо того чтобы долбить сервер.
Живая проба доступа
from authkit_client import probe_access, probe_for
@probe_for("example", endpoint_markers=("unknown method",))
async def _probe(ctx):
return await client.get("https://example.com/", follow_redirects=True)
verdict = await probe_access("example", ctx)
if verdict.needs_human:
... # сервер действительно отверг куки, а не «сети нет»
Вердикт выносится по подписи ответа, а не по наличию файла с куками: «сервер увёл на страницу входа» (нужен человек) и «до сервера не дошли» (чинить выход) — разные состояния, и путать их дорого. Сеть приносит сама проба: своего HTTP-клиента у кита нет, подойдёт httpx, curl_cffi или что угодно утиное.
Браузера здесь нет
Минт сессии — дело браузерного слоя, и он приходит сюда через порт:
from authkit_client.ports import register_minter, minted_state
class NodriverMinter: # живёт в браузерном ките
name = "nodriver"
async def mint(self, ref, ctx=None): ...
register_minter(NodriverMinter())
state = await minted_state("nodriver", ref) # слой доступа не знает про браузер
Причина простая: клиенту белого API с api-ключом браузер не нужен никогда, а
жадный импорт стоил бы ему playwright/nodriver на каждом старте. Инвариант
закреплён tests/test_no_browser_imports.py — тест смотрит на sys.modules
свежего интерпретатора, а не на строчки исходника.
Хранилища подменяемы
SessionStorePort / SecretStorePort — структурные протоколы: подходит любой
объект с нужными методами. Реализация «из памяти» проходит их без единой строки
SQL, и код потребителя при подмене не правится (tests/test_ports.py).
Совместимость
Кит выделен из s-librarykit, поэтому у тех, кто ставит их вместе, прежние пути
импорта — librarykit.sessions, librarykit.secret_store,
librarykit.access_probe, librarykit.oauth, librarykit.session_fingerprint —
продолжают работать: там это sys.modules-алиасы на те же объекты модулей, а
не копии, включая приватные имена, которые патчат тесты. Сам по себе
s-authkit-client самодостаточен и librarykit не требует.
Сохранены и прежние имена переменных окружения / keyring-namespace'ов: новые ключи пишутся в нейтральный namespace, а ранее созданные сессии по-прежнему расшифровываются через fallback.
Лицензия
MIT — см. LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file s_authkit_client-0.0.2.tar.gz.
File metadata
- Download URL: s_authkit_client-0.0.2.tar.gz
- Upload date:
- Size: 59.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
45046732125e814663dfa1d0ca7169c97372e08fb203f722bb55cf0b87c15f2b
|
|
| MD5 |
a314609b184e35c855737df5b271d24a
|
|
| BLAKE2b-256 |
0cdcf016067fcc1e5566d3bbb5d8f4fbe692f75b143681f9965094ec8207bcb4
|
File details
Details for the file s_authkit_client-0.0.2-py3-none-any.whl.
File metadata
- Download URL: s_authkit_client-0.0.2-py3-none-any.whl
- Upload date:
- Size: 59.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
95c53ab59d4ee7623e142a6a511ccf635dad47227f227b67a032cb8562a3215d
|
|
| MD5 |
e32f31100efadd61c69b4d4c07037dce
|
|
| BLAKE2b-256 |
154927dbbbc563ff2bfce751fdc02d18bdcbaf96efe926c933d9ec51228cf058
|