Metadata-Version: 2.5
Name: s-sessionkit
Version: 0.0.2
Summary: Владение сессией аккаунта: адрес, порт хранилища, аренда и жизненный цикл. Один дом вместо четырёх — реализации (файловое дерево, база) подключаются провайдерами, а движок задаётся строкой подключения.
Author: Dmitry
License: MIT
Keywords: accounts,cookies,lease,session,storage
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.11
Requires-Dist: s-corekit>=0.0.11
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Provides-Extra: live
Requires-Dist: s-accountpoolkit>=0.5.22; extra == 'live'
Requires-Dist: s-authkit-client>=0.0.4; extra == 'live'
Requires-Dist: s-librarykit>=0.7.25; extra == 'live'
Description-Content-Type: text/markdown

# sessionkit

> Владение сессией аккаунта: **дверь**, адрес, порт хранилища, аренда. Один дом
> вместо четырёх — где сессия лежит, решает кит, а не потребитель.

## Зачем

«Сессия аккаунта» физически живёт сразу в нескольких местах: файловым деревом
(`s-authkit-client`, envelope KEK/DEK), строкой в общей базе
(`s-accountpoolkit`, документ хранится запечатанным целиком) и профилем
браузера как кэшем узнавания устройства. Какое из них главное, до сих пор
решала переменная окружения, прочитанная посторонним китом.

У этого две цены. Первая: одна команда пишет туда, где другая ничего не
находит. Вторая, дороже: у сессии нет владельца — спросить «где она на самом
деле» не у кого, и спор «почему навык не видит вход» решается догадками.

Этот кит — владелец. Он знает обе живые реализации, находит общую базу сам,
выбирает носитель и **говорит, кого выбрал и почему**.

## Как спрашивают

```python
import sessionkit

address = sessionkit.SessionAddress(service="gemini", account="me@example.com")

state = sessionkit.load_state(address)          # None — входа ещё не было
sessionkit.save_state(address, state or {})     # записать тело сессии
sessionkit.mark(address, status="live")         # пометить, не трогая тело

choice = sessionkit.where()                     # КТО ответил и ПОЧЕМУ
print(choice.name, choice.reason)
for verdict in choice.verdicts:
    print(" ", verdict)                         # включая тех, кто отказался
```

Разбор полётов «почему выбрали не то» — `sessionkit.explain()`: опрашивает всех
кандидатов, а не останавливается на первом согласившемся.

Что умеет выбранное хранилище сверх обязательного минимума (аренда на минт,
набор аккаунтов, запись без слияния) — `sessionkit.capabilities()`. Способность
спрашивают, а не предполагают: предположение оборачивается тишиной на месте
защиты.

## Адрес

Шесть координат, ровно те же, которыми адресует хранилище
(`corekit.dto.SessionRef`):

| координата | вопрос | умолчание |
|---|---|---|
| `tenant` | чья сессия в общем хосте | `local` |
| `profile` | чей это набор входов (рабочий, личный) | `default` |
| `service` | к какому сервису | обязательна |
| `instance` | какой ИМЕННО вход сервиса (портал, национальный домен) | пусто |
| `account` | под каким аккаунтом | `default` |
| `stage` | боевая сессия или черновик записи трафика | `live` |

`instance` — не украшение: без него два разных портала Битрикса одного аккаунта
получают одно имя, а хранилище считает уникальность по шести координатам — и
запись уходит не туда, откуда читали.

## Где что лежит

`sessionkit.locate` отвечает на вопрос «где» один раз на всех:

- корень сессий — `SESSIONS_HOME`, иначе `~/.sessions`;
- общая база **находится сама** в `<корень>/sessions.db`; `SESSIONS_DB_URL` —
  переопределение (другой сервер, другой тенант), а не условие работы;
- ключ шифрования имеет **три** состояния: файл (`SESSIONS_DB_KEY_FILE` или
  `<корень>/.session-db.key`), явное «хранить открыто»
  (`SESSIONS_DB_PLAINTEXT=1`) и «не задан» — последнее НЕ равно «открыто».

Порядок опроса: сначала база, потом дерево. Если общая база заведена, то она и
есть сессия; читать в этот момент дерево значит завести второй носитель — обе
копии выглядят рабочими, а ротацию получает только та, через которую сходили.

## Аренда

`sessionkit.session_lease(address)` — окно записи на два слоя (лок внутри
процесса + файл между процессами) со сроком и владельцем. Нужна там, где вход
ВЫПУСКАЮТ: у сервисов вида Google одна сессия на аккаунт, и второй
одновременный вход инвалидирует первый.

## Что живёт МИМО двери

Честный список (подробности — в `CHANGELOG.md`): отпечаток выхода
`egress_fingerprint.json`, персона устройства (`corekit.persona`), секреты,
профиль браузера, а также координаты `instance`/`stage`, которые файловое дерево
пока игнорирует. Дверь отдаёт тело сессии; всё перечисленное адресуется своими
способами и ждёт своей очереди.

## Разработка

```sh
uv sync --extra dev --extra live
uv run --extra dev --extra live pytest -q
uv run --extra dev ruff check sessionkit tests
```

Набор `live` ставит НАСТОЯЩИЕ хранилища: утверждение «обе реализации доступны
через дверь» подделкой не доказывается — она подтверждает лишь то, что мы её так
и написали. Без набора живые тесты пропускаются с внятной причиной.

## Статус

- **Type**: `kit`
- **Status**: `active`
- **Priority**: P0
- **Slug**: `sessionkit` · **Prefix**: `ses`

```sh
atlas project get sessionkit
```

Физический layout:

- Storage: `_storage/sessionkit/`
- Junction: `Products\sessionkit` → `_storage/sessionkit`
