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.
Metadata
Release files for s-authkit-client 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| s_authkit_client-0.1.1.tar.gz | 156.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| s_authkit_client-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 246.1 kB
Release files / s_authkit_client-0.1.1.tar.gz
| Download URL | s_authkit_client-0.1.1.tar.gz |
|---|---|
| Size | 156.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
89dec236f7b9470f27501945685dce7432ee0c557f3455359d51b51fcef79e7d
|
|
BLAKE2b-256 checksum How to use checksums |
5e720137963b4e89eaee23c3e048726f45a913f0d85399f5176166c83c731eb3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|
Release files / s_authkit_client-0.1.1-py3-none-any.whl
| Download URL | s_authkit_client-0.1.1-py3-none-any.whl |
|---|---|
| Size | 89.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7320a06391dd745f6d2d599ad3df97955c90ee9f78daab8b23d68d0653106427
|
|
BLAKE2b-256 checksum How to use checksums |
3df9dbe4b1242ef36cfa2107172c8425478741606bf8a30959045c738b722f1f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|