s-authkit-server 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. s_authkit_server-0.1.1/.coverage +0 -0
  2. s_authkit_server-0.1.1/.gitignore +47 -0
  3. s_authkit_server-0.1.1/AGENTS.md +54 -0
  4. s_authkit_server-0.1.1/CHANGELOG.md +49 -0
  5. s_authkit_server-0.1.1/LICENSE +21 -0
  6. s_authkit_server-0.1.1/PKG-INFO +238 -0
  7. s_authkit_server-0.1.1/README.md +221 -0
  8. s_authkit_server-0.1.1/authkit_server/__init__.py +68 -0
  9. s_authkit_server-0.1.1/authkit_server/exceptions.py +34 -0
  10. s_authkit_server-0.1.1/authkit_server/password/__init__.py +10 -0
  11. s_authkit_server-0.1.1/authkit_server/password/hasher.py +85 -0
  12. s_authkit_server-0.1.1/authkit_server/refresh/__init__.py +9 -0
  13. s_authkit_server-0.1.1/authkit_server/refresh/models.py +249 -0
  14. s_authkit_server-0.1.1/authkit_server/token/__init__.py +16 -0
  15. s_authkit_server-0.1.1/authkit_server/token/jose_issuer.py +317 -0
  16. s_authkit_server-0.1.1/authkit_server/token/keys.py +46 -0
  17. s_authkit_server-0.1.1/authkit_server/token/models.py +99 -0
  18. s_authkit_server-0.1.1/pyproject.toml +56 -0
  19. s_authkit_server-0.1.1/tests/__init__.py +1 -0
  20. s_authkit_server-0.1.1/tests/conftest.py +170 -0
  21. s_authkit_server-0.1.1/tests/integration/__init__.py +1 -0
  22. s_authkit_server-0.1.1/tests/integration/test_jose_issuer_with_mock_store.py +122 -0
  23. s_authkit_server-0.1.1/tests/unit/__init__.py +1 -0
  24. s_authkit_server-0.1.1/tests/unit/password/__init__.py +1 -0
  25. s_authkit_server-0.1.1/tests/unit/password/test_argon2_hasher.py +82 -0
  26. s_authkit_server-0.1.1/tests/unit/refresh/__init__.py +1 -0
  27. s_authkit_server-0.1.1/tests/unit/refresh/test_refresh_token_record.py +172 -0
  28. s_authkit_server-0.1.1/tests/unit/token/__init__.py +1 -0
  29. s_authkit_server-0.1.1/tests/unit/token/test_jose_issuer.py +355 -0
  30. s_authkit_server-0.1.1/tests/unit/token/test_keys.py +68 -0
  31. s_authkit_server-0.1.1/tests/unit/token/test_models.py +78 -0
  32. s_authkit_server-0.1.1/uv.lock +556 -0
Binary file
@@ -0,0 +1,47 @@
1
+ # === atlas universal gitignore ===
2
+
3
+ # OS / IDE
4
+ .DS_Store
5
+ Thumbs.db
6
+ .vscode/
7
+ .idea/
8
+ *.swp
9
+ *.swo
10
+
11
+ # Sensitive
12
+ .env
13
+ .env.local
14
+ *.key
15
+ *.pem
16
+ secrets/
17
+ private/
18
+
19
+ # Python
20
+ __pycache__/
21
+ *.py[cod]
22
+ .venv/
23
+ venv/
24
+ .pytest_cache/
25
+ .ruff_cache/
26
+ *.egg-info/
27
+
28
+ # Node / JS
29
+ node_modules/
30
+ .next/
31
+ dist/
32
+ build/
33
+
34
+ # Temporary / large
35
+ *.log
36
+ *.tmp
37
+ nul
38
+ NUL
39
+ *.zip
40
+ *.rar
41
+ *.7z
42
+
43
+ # Media (selectively unignore via !path/*.ext if needed for fixtures)
44
+ *.mp4
45
+ *.mov
46
+ *.avi
47
+ *.mkv
@@ -0,0 +1,54 @@
1
+ # AGENTS.md — authkit-server
2
+
3
+ > Контекст для AI-ассистентов (Claude Code, ChatGPT, Cursor и т.п.), работающих
4
+ > над этим проектом.
5
+
6
+ ## Что это
7
+
8
+ Серверное ядро авторизации: НАШ бэкенд авторизует СВОИХ пользователей (JWT RS256,
9
+ persistent refresh с ротацией, Argon2-хеширование). Дистрибутив — `s-authkit-server`,
10
+ импорт — `authkit_server`.
11
+
12
+ Не путать с `s-authkit-client` (импорт `authkit_client`, репо `../authkit-client`) — там
13
+ обратная сторона: доступ НАШЕГО кода к ЧУЖИМ сервисам (сессии, секреты, OAuth-обновление,
14
+ живая проба). Суффикс имени и есть указатель стороны.
15
+
16
+ > Слаг проекта в Atlas и имя git-репозитория остались `authkit` — переименован ПАКЕТ, а не
17
+ > репозиторий; команды ниже намеренно используют старый слаг.
18
+
19
+ ## Atlas
20
+
21
+ Проект зарегистрирован в Atlas-БД (Atlas). Карточка:
22
+
23
+ ```sh
24
+ atlas projects get authkit
25
+ ```
26
+
27
+ Любые изменения метаданных (приоритет, статус, теги) — через atlas CLI:
28
+
29
+ - `atlas projects update authkit --priority P0` — поменять приоритет
30
+ - `atlas add-tags authkit -t domain:<slug>` — добавить тег
31
+ - `atlas projects move authkit --to-type <type>` — конвертировать тип
32
+
33
+ ## Тип / Статус (на момент создания)
34
+
35
+ - type=`kit`, status=`experiment`, priority=`P1`
36
+
37
+ ## Правила работы
38
+
39
+ - Все исходные тексты, документы, код проекта — в этом репо.
40
+ - Чувствительные данные (`.env`, токены, ключи) — игнорируются `.gitignore`.
41
+ - AI-ассистенту разрешено: читать, генерировать, редактировать в этом репо.
42
+
43
+ ## Канонические команды
44
+
45
+ - `atlas projects get authkit` — карточка проекта
46
+ - `atlas task list --project authkit` — задачи проекта (когда W7
47
+ волна будет реализована)
48
+
49
+ <!-- atlas:usage:start -->
50
+ ## Управление проектом — через Atlas
51
+
52
+ Этот проект ведётся в Atlas (личная PM-система портфеля). Для задач/проектов/эпиков/бэкапов
53
+ используй CLI `atlas` и вызывай навык `atlas` — вся логика и роутинг внутри навыка.
54
+ <!-- atlas:usage:end -->
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ Все значительные изменения этого проекта будут документироваться в этом файле.
4
+
5
+ Формат основан на [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ и этот проект соответствует [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.1] - 2026-08-18
9
+
10
+ ### Changed
11
+
12
+ - **Переименование кита: `s-authkit` → `s-authkit-server`, импорт `authkit` → `authkit_server`** (#1983).
13
+ Имя теперь называет сторону: этот кит авторизует НАШИХ пользователей на НАШЕМ сервере,
14
+ а зеркальный `s-authkit-client` (импорт `authkit_client`) даёт НАШЕМУ коду доступ к ЧУЖИМ
15
+ сервисам. Раньше пара читалась как «`authkit` и SDK к нему»; путаница была человеческая —
16
+ в коде пространства имён не пересекались.
17
+ - Публичный API не изменён: те же `JoseTokenIssuer`, `Argon2PasswordHasher`,
18
+ `ensure_jwt_keypair`, `RefreshTokenRecord`, `IRefreshTokenStore`, исключения. Правится
19
+ ровно строка импорта и строка зависимости.
20
+
21
+ ### Notes
22
+
23
+ - **PyPI.** Переименовать дистрибутив «на месте» нельзя: `s-authkit-server` — НОВЫЙ
24
+ дистрибутив. `s-authkit==0.1.0` остаётся опубликованным как есть (не yank, не delete —
25
+ уже собранные сборки на него живые), но новых выпусков под старым именем не будет: имя
26
+ заморожено на 0.1.0.
27
+ - Версия 0.1.1 продолжает историю кита, а не начинается с нуля — так видно, что это то же
28
+ ядро под новым именем.
29
+ - Потребители переведены тем же заходом: `skillery-backend` и `skills-hub/backend`
30
+ (`infrastructure/auth/{__init__,argon2_hasher,keys}.py` + `pyproject.toml`).
31
+
32
+ ## [0.1.0] - 2026-07-02
33
+
34
+ ### Added
35
+
36
+ - **JWT RS256 issuer** (`JoseTokenIssuer`): выпуск и верификация JWT access-токенов с асимметричным ключом
37
+ - **Persistent refresh-tokens** (`RefreshTokenRecord`, `IRefreshTokenStore`): хранение и ротация refresh-токенов с хешированием SHA256
38
+ - **Argon2 password hashing** (`Argon2PasswordHasher`): хеширование паролей через passlib с Argon2id
39
+ - **RSA keypair generation** (`ensure_jwt_keypair`): утилита для создания RSA 2048 ключей в PEM-формате
40
+ - **Comprehensive test suite**: 38 unit и integration тестов с coverage ≥80%
41
+ - **Protocol-based architecture**: `IRefreshTokenStore` как Protocol для контрактного дизайна
42
+
43
+ ### Notes
44
+
45
+ - Первый релиз модуля `s-authkit` как отделённого ядра авторизации для Skillery проектов
46
+ - Нет завязок на конкретное приложение: только чистая инфраструктура
47
+ - OAuth, PermissionResolver, ExchangeCode отложены на v0.2+
48
+ - Все методы асинхронные (async/await)
49
+ - Python ≥3.11, зависимости: python-jose[cryptography], passlib[argon2], cryptography
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dmitry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.5
2
+ Name: s-authkit-server
3
+ Version: 0.1.1
4
+ Summary: Серверная авторизация своих пользователей: JWT RS256 + refresh-tokens + Argon2 hashing. Пара к s-authkit-client (доступ к чужим сервисам). 0 завязок на конкретное приложение.
5
+ Author: Dmitry
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: cryptography>=41.0.0
10
+ Requires-Dist: passlib[argon2]>=1.7.4
11
+ Requires-Dist: python-jose[cryptography]>=3.3.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
14
+ Requires-Dist: pytest-cov>=6.0; extra == 'dev'
15
+ Requires-Dist: pytest>=8; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # s-authkit-server
19
+
20
+ СЕРВЕРНОЕ ядро авторизации для Skillery проектов: JWT RS256, refresh-token management,
21
+ Argon2 hashing. Импорт — `authkit_server`.
22
+
23
+ **Версия:** 0.1.1
24
+ **Лицензия:** MIT
25
+ **Зависимости:** `python-jose[cryptography]`, `passlib[argon2]`, `cryptography`
26
+
27
+ ## Какая это сторона
28
+
29
+ | Кит | Импорт | Чью авторизацию решает |
30
+ | --- | --- | --- |
31
+ | `s-authkit-server` (этот) | `authkit_server` | НАШ сервис авторизует СВОИХ пользователей: свои токены, свои пароли, свои refresh |
32
+ | `s-authkit-client` | `authkit_client` | НАШ код получает доступ к ЧУЖИМ сервисам: сессии, секреты, OAuth-обновление, живая проба |
33
+
34
+ Суффикс имени и есть указатель стороны. До 0.1.1 этот кит назывался `s-authkit`
35
+ (импорт `authkit`) — из-за пары «`authkit` / `authkit-client`» серверный кит читался
36
+ как SDK к клиентскому. См. раздел «Переименование» ниже.
37
+
38
+ ## Что входит
39
+
40
+ ### JWT access-токены (RS256)
41
+ - `JoseTokenIssuer` — выпуск и верификация JWT
42
+ - `TokenPair` / `TokenClaims` — value objects для типизации
43
+ - `ensure_jwt_keypair` — генерация RSA 2048 ключей
44
+
45
+ ### Persistent refresh-tokens
46
+ - `IRefreshTokenStore` — protocol для БД-реализации
47
+ - `RefreshTokenRecord` — агрегат с хешированием и валидацией
48
+ - Rotation с audit-trail (`rotated_from`)
49
+
50
+ ### Password hashing
51
+ - `Argon2PasswordHasher` — Argon2id через passlib
52
+
53
+ ## Примеры
54
+
55
+ ### 1. Инициализация
56
+
57
+ ```python
58
+ from pathlib import Path
59
+ from authkit_server import (
60
+ JoseTokenIssuer,
61
+ Argon2PasswordHasher,
62
+ ensure_jwt_keypair,
63
+ )
64
+
65
+ # Создаём ключи (если нет)
66
+ keys_dir = Path.home() / ".myapp" / "keys"
67
+ ensure_jwt_keypair(
68
+ private_path=keys_dir / "jwt_private.pem",
69
+ public_path=keys_dir / "jwt_public.pem",
70
+ )
71
+
72
+ # Прочитаем ключи
73
+ private_key = (keys_dir / "jwt_private.pem").read_text()
74
+ public_key = (keys_dir / "jwt_public.pem").read_text()
75
+
76
+ # Создаём компоненты
77
+ hasher = Argon2PasswordHasher()
78
+ issuer = JoseTokenIssuer(
79
+ private_key_pem=private_key,
80
+ public_key_pem=public_key,
81
+ issuer="myproject.com",
82
+ access_ttl_min=15,
83
+ refresh_ttl_days=30,
84
+ refresh_store=your_store_impl, # реализуете вы
85
+ )
86
+ ```
87
+
88
+ ### 2. Login (выпуск пары токенов)
89
+
90
+ ```python
91
+ async def login_with_password(username: str, password: str):
92
+ # Получаем юзера из БД
93
+ user = await db.get_user_by_username(username)
94
+ if not user:
95
+ raise ValueError("User not found")
96
+
97
+ # Проверяем пароль
98
+ if not hasher.verify(password, user.password_hash):
99
+ raise ValueError("Invalid password")
100
+
101
+ # Выпускаем пару
102
+ pair = await issuer.issue_pair(
103
+ user_id=str(user.id),
104
+ company_id=str(user.active_company_id) if user.active_company_id else None,
105
+ permissions={"skill.read", "skill.install"},
106
+ )
107
+ return pair
108
+ ```
109
+
110
+ ### 3. Middleware (верификация токена)
111
+
112
+ ```python
113
+ from authkit_server import TokenError
114
+
115
+ async def auth_middleware(request, call_next):
116
+ auth_header = request.headers.get("Authorization", "")
117
+ if not auth_header.startswith("Bearer "):
118
+ return Response("Unauthorized", status_code=401)
119
+
120
+ token = auth_header[7:]
121
+ try:
122
+ claims = issuer.verify_access(token)
123
+ except TokenError as e:
124
+ return Response(f"Invalid token: {e}", status_code=401)
125
+
126
+ request.state.claims = claims
127
+ return await call_next(request)
128
+ ```
129
+
130
+ ### 4. Refresh (rotation)
131
+
132
+ ```python
133
+ async def refresh_session(refresh_token: str):
134
+ try:
135
+ new_pair = await issuer.rotate_refresh(refresh_token)
136
+ except RefreshTokenError as e:
137
+ raise Unauthorized(f"Refresh failed: {e}")
138
+ return new_pair
139
+ ```
140
+
141
+ ### 5. Реализация IRefreshTokenStore
142
+
143
+ ```python
144
+ from authkit_server import RefreshTokenRecord, IRefreshTokenStore
145
+
146
+ class PostgresRefreshTokenStore:
147
+ def __init__(self, db_engine):
148
+ self.engine = db_engine
149
+
150
+ async def save(self, record: RefreshTokenRecord) -> None:
151
+ # INSERT/UPDATE в БД
152
+ async with self.engine.begin() as conn:
153
+ await conn.execute(
154
+ "INSERT INTO refresh_tokens (user_id, token_hash, ...) VALUES (...)"
155
+ )
156
+
157
+ async def get_by_hash(self, token_hash: str) -> RefreshTokenRecord | None:
158
+ # SELECT * FROM refresh_tokens WHERE token_hash = ?
159
+ ...
160
+
161
+ async def revoke_all_for_user(self, user_id: str) -> int:
162
+ # UPDATE refresh_tokens SET revoked_at = NOW() WHERE user_id = ?
163
+ ...
164
+
165
+ # Реализуете остальные методы Protocol'а
166
+ ```
167
+
168
+ ## Архитектура
169
+
170
+ ```
171
+ authkit_server/
172
+ ├── __init__.py # Public API
173
+ ├── exceptions.py # AuthKitError, TokenError, ...
174
+ ├── token/
175
+ │ ├── models.py # TokenPair, TokenClaims
176
+ │ ├── jose_issuer.py # JoseTokenIssuer
177
+ │ └── keys.py # ensure_jwt_keypair
178
+ ├── refresh/
179
+ │ └── models.py # RefreshTokenRecord, IRefreshTokenStore
180
+ └── password/
181
+ └── hasher.py # Argon2PasswordHasher
182
+ ```
183
+
184
+ ## Тестирование
185
+
186
+ ```bash
187
+ pytest tests/ # Все тесты
188
+ pytest tests/ -v --cov # С coverage report
189
+ pytest tests/ -k "test_verify" # Конкретный тест
190
+ ```
191
+
192
+ Coverage gate: ≥ 80%.
193
+
194
+ ## Что НЕ входит (v0.1)
195
+
196
+ - **OAuth** (YandexOAuth, ExchangeCode) — в v0.2+
197
+ - **PermissionResolver** — остаются на стороне приложения
198
+ - **Redis cache invalidation** — в v0.2+ как optional
199
+ - **Signing PK/SK rotation** — будущая фича
200
+ - **Sync обёртки** — только async API в v0.1
201
+
202
+ ## Интеграция в существующее приложение
203
+
204
+ Типовой путь перевода существующего приложения на модуль:
205
+ 1. Импортирует `from authkit_server import ...` вместо локального кода
206
+ 2. Обёрнет `RefreshTokenRecord` в ORM-адаптер для своей БД
207
+ 3. Оставит PermissionResolver/OAuth/ExchangeCode локально
208
+
209
+ ## Переименование (0.1.0 → 0.1.1)
210
+
211
+ Кит переименован: дистрибутив `s-authkit` → `s-authkit-server`, импорт `authkit` →
212
+ `authkit_server`. Публичный API (имена классов и функций) не менялся — правится только
213
+ строка импорта и строка зависимости.
214
+
215
+ Со старым дистрибутивом на PyPI:
216
+
217
+ - `s-authkit==0.1.0` остаётся опубликованным как есть — его не отзываем (yank) и не
218
+ удаляем: он рабочий, а отзыв сломал бы любую уже собранную сборку;
219
+ - **новых выпусков под именем `s-authkit` больше не будет** — имя заморожено на 0.1.0;
220
+ - переименование на PyPI «на месте» невозможно: `s-authkit-server` — это НОВЫЙ
221
+ дистрибутив, первый его выпуск — 0.1.1 (нумерация продолжает историю кита, а не
222
+ начинается заново, чтобы версия читалась как «то же ядро, новое имя»).
223
+
224
+ Миграция потребителя — две строки:
225
+
226
+ ```diff
227
+ -"s-authkit>=0.1.0",
228
+ +"s-authkit-server>=0.1.1",
229
+ ```
230
+
231
+ ```diff
232
+ -from authkit import Argon2PasswordHasher, ensure_jwt_keypair
233
+ +from authkit_server import Argon2PasswordHasher, ensure_jwt_keypair
234
+ ```
235
+
236
+ ## Лицензия
237
+
238
+ MIT
@@ -0,0 +1,221 @@
1
+ # s-authkit-server
2
+
3
+ СЕРВЕРНОЕ ядро авторизации для Skillery проектов: JWT RS256, refresh-token management,
4
+ Argon2 hashing. Импорт — `authkit_server`.
5
+
6
+ **Версия:** 0.1.1
7
+ **Лицензия:** MIT
8
+ **Зависимости:** `python-jose[cryptography]`, `passlib[argon2]`, `cryptography`
9
+
10
+ ## Какая это сторона
11
+
12
+ | Кит | Импорт | Чью авторизацию решает |
13
+ | --- | --- | --- |
14
+ | `s-authkit-server` (этот) | `authkit_server` | НАШ сервис авторизует СВОИХ пользователей: свои токены, свои пароли, свои refresh |
15
+ | `s-authkit-client` | `authkit_client` | НАШ код получает доступ к ЧУЖИМ сервисам: сессии, секреты, OAuth-обновление, живая проба |
16
+
17
+ Суффикс имени и есть указатель стороны. До 0.1.1 этот кит назывался `s-authkit`
18
+ (импорт `authkit`) — из-за пары «`authkit` / `authkit-client`» серверный кит читался
19
+ как SDK к клиентскому. См. раздел «Переименование» ниже.
20
+
21
+ ## Что входит
22
+
23
+ ### JWT access-токены (RS256)
24
+ - `JoseTokenIssuer` — выпуск и верификация JWT
25
+ - `TokenPair` / `TokenClaims` — value objects для типизации
26
+ - `ensure_jwt_keypair` — генерация RSA 2048 ключей
27
+
28
+ ### Persistent refresh-tokens
29
+ - `IRefreshTokenStore` — protocol для БД-реализации
30
+ - `RefreshTokenRecord` — агрегат с хешированием и валидацией
31
+ - Rotation с audit-trail (`rotated_from`)
32
+
33
+ ### Password hashing
34
+ - `Argon2PasswordHasher` — Argon2id через passlib
35
+
36
+ ## Примеры
37
+
38
+ ### 1. Инициализация
39
+
40
+ ```python
41
+ from pathlib import Path
42
+ from authkit_server import (
43
+ JoseTokenIssuer,
44
+ Argon2PasswordHasher,
45
+ ensure_jwt_keypair,
46
+ )
47
+
48
+ # Создаём ключи (если нет)
49
+ keys_dir = Path.home() / ".myapp" / "keys"
50
+ ensure_jwt_keypair(
51
+ private_path=keys_dir / "jwt_private.pem",
52
+ public_path=keys_dir / "jwt_public.pem",
53
+ )
54
+
55
+ # Прочитаем ключи
56
+ private_key = (keys_dir / "jwt_private.pem").read_text()
57
+ public_key = (keys_dir / "jwt_public.pem").read_text()
58
+
59
+ # Создаём компоненты
60
+ hasher = Argon2PasswordHasher()
61
+ issuer = JoseTokenIssuer(
62
+ private_key_pem=private_key,
63
+ public_key_pem=public_key,
64
+ issuer="myproject.com",
65
+ access_ttl_min=15,
66
+ refresh_ttl_days=30,
67
+ refresh_store=your_store_impl, # реализуете вы
68
+ )
69
+ ```
70
+
71
+ ### 2. Login (выпуск пары токенов)
72
+
73
+ ```python
74
+ async def login_with_password(username: str, password: str):
75
+ # Получаем юзера из БД
76
+ user = await db.get_user_by_username(username)
77
+ if not user:
78
+ raise ValueError("User not found")
79
+
80
+ # Проверяем пароль
81
+ if not hasher.verify(password, user.password_hash):
82
+ raise ValueError("Invalid password")
83
+
84
+ # Выпускаем пару
85
+ pair = await issuer.issue_pair(
86
+ user_id=str(user.id),
87
+ company_id=str(user.active_company_id) if user.active_company_id else None,
88
+ permissions={"skill.read", "skill.install"},
89
+ )
90
+ return pair
91
+ ```
92
+
93
+ ### 3. Middleware (верификация токена)
94
+
95
+ ```python
96
+ from authkit_server import TokenError
97
+
98
+ async def auth_middleware(request, call_next):
99
+ auth_header = request.headers.get("Authorization", "")
100
+ if not auth_header.startswith("Bearer "):
101
+ return Response("Unauthorized", status_code=401)
102
+
103
+ token = auth_header[7:]
104
+ try:
105
+ claims = issuer.verify_access(token)
106
+ except TokenError as e:
107
+ return Response(f"Invalid token: {e}", status_code=401)
108
+
109
+ request.state.claims = claims
110
+ return await call_next(request)
111
+ ```
112
+
113
+ ### 4. Refresh (rotation)
114
+
115
+ ```python
116
+ async def refresh_session(refresh_token: str):
117
+ try:
118
+ new_pair = await issuer.rotate_refresh(refresh_token)
119
+ except RefreshTokenError as e:
120
+ raise Unauthorized(f"Refresh failed: {e}")
121
+ return new_pair
122
+ ```
123
+
124
+ ### 5. Реализация IRefreshTokenStore
125
+
126
+ ```python
127
+ from authkit_server import RefreshTokenRecord, IRefreshTokenStore
128
+
129
+ class PostgresRefreshTokenStore:
130
+ def __init__(self, db_engine):
131
+ self.engine = db_engine
132
+
133
+ async def save(self, record: RefreshTokenRecord) -> None:
134
+ # INSERT/UPDATE в БД
135
+ async with self.engine.begin() as conn:
136
+ await conn.execute(
137
+ "INSERT INTO refresh_tokens (user_id, token_hash, ...) VALUES (...)"
138
+ )
139
+
140
+ async def get_by_hash(self, token_hash: str) -> RefreshTokenRecord | None:
141
+ # SELECT * FROM refresh_tokens WHERE token_hash = ?
142
+ ...
143
+
144
+ async def revoke_all_for_user(self, user_id: str) -> int:
145
+ # UPDATE refresh_tokens SET revoked_at = NOW() WHERE user_id = ?
146
+ ...
147
+
148
+ # Реализуете остальные методы Protocol'а
149
+ ```
150
+
151
+ ## Архитектура
152
+
153
+ ```
154
+ authkit_server/
155
+ ├── __init__.py # Public API
156
+ ├── exceptions.py # AuthKitError, TokenError, ...
157
+ ├── token/
158
+ │ ├── models.py # TokenPair, TokenClaims
159
+ │ ├── jose_issuer.py # JoseTokenIssuer
160
+ │ └── keys.py # ensure_jwt_keypair
161
+ ├── refresh/
162
+ │ └── models.py # RefreshTokenRecord, IRefreshTokenStore
163
+ └── password/
164
+ └── hasher.py # Argon2PasswordHasher
165
+ ```
166
+
167
+ ## Тестирование
168
+
169
+ ```bash
170
+ pytest tests/ # Все тесты
171
+ pytest tests/ -v --cov # С coverage report
172
+ pytest tests/ -k "test_verify" # Конкретный тест
173
+ ```
174
+
175
+ Coverage gate: ≥ 80%.
176
+
177
+ ## Что НЕ входит (v0.1)
178
+
179
+ - **OAuth** (YandexOAuth, ExchangeCode) — в v0.2+
180
+ - **PermissionResolver** — остаются на стороне приложения
181
+ - **Redis cache invalidation** — в v0.2+ как optional
182
+ - **Signing PK/SK rotation** — будущая фича
183
+ - **Sync обёртки** — только async API в v0.1
184
+
185
+ ## Интеграция в существующее приложение
186
+
187
+ Типовой путь перевода существующего приложения на модуль:
188
+ 1. Импортирует `from authkit_server import ...` вместо локального кода
189
+ 2. Обёрнет `RefreshTokenRecord` в ORM-адаптер для своей БД
190
+ 3. Оставит PermissionResolver/OAuth/ExchangeCode локально
191
+
192
+ ## Переименование (0.1.0 → 0.1.1)
193
+
194
+ Кит переименован: дистрибутив `s-authkit` → `s-authkit-server`, импорт `authkit` →
195
+ `authkit_server`. Публичный API (имена классов и функций) не менялся — правится только
196
+ строка импорта и строка зависимости.
197
+
198
+ Со старым дистрибутивом на PyPI:
199
+
200
+ - `s-authkit==0.1.0` остаётся опубликованным как есть — его не отзываем (yank) и не
201
+ удаляем: он рабочий, а отзыв сломал бы любую уже собранную сборку;
202
+ - **новых выпусков под именем `s-authkit` больше не будет** — имя заморожено на 0.1.0;
203
+ - переименование на PyPI «на месте» невозможно: `s-authkit-server` — это НОВЫЙ
204
+ дистрибутив, первый его выпуск — 0.1.1 (нумерация продолжает историю кита, а не
205
+ начинается заново, чтобы версия читалась как «то же ядро, новое имя»).
206
+
207
+ Миграция потребителя — две строки:
208
+
209
+ ```diff
210
+ -"s-authkit>=0.1.0",
211
+ +"s-authkit-server>=0.1.1",
212
+ ```
213
+
214
+ ```diff
215
+ -from authkit import Argon2PasswordHasher, ensure_jwt_keypair
216
+ +from authkit_server import Argon2PasswordHasher, ensure_jwt_keypair
217
+ ```
218
+
219
+ ## Лицензия
220
+
221
+ MIT