hawkapi-auth 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,32 @@
1
+ """hawkapi-auth — JWT auth (access + refresh) + password hashing for HawkAPI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from hawkapi_auth._deps import requires_claims, requires_scopes, requires_user
8
+ from hawkapi_auth._passwords import hash_password, needs_rehash, verify_password
9
+ from hawkapi_auth._plugin import init_auth
10
+ from hawkapi_auth._tokens import (
11
+ JWTConfig,
12
+ RevocationList,
13
+ TokenError,
14
+ TokenIssuer,
15
+ random_secret,
16
+ )
17
+
18
+ __all__ = [
19
+ "JWTConfig",
20
+ "RevocationList",
21
+ "TokenError",
22
+ "TokenIssuer",
23
+ "__version__",
24
+ "hash_password",
25
+ "init_auth",
26
+ "needs_rehash",
27
+ "random_secret",
28
+ "requires_claims",
29
+ "requires_scopes",
30
+ "requires_user",
31
+ "verify_password",
32
+ ]
hawkapi_auth/_deps.py ADDED
@@ -0,0 +1,102 @@
1
+ """DI dependencies — extract + verify access tokens from incoming requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from hawkapi.exceptions import HTTPException
8
+ from hawkapi.requests.request import Request
9
+
10
+ from hawkapi_auth._tokens import TokenError
11
+
12
+
13
+ def _bearer_token(request: Request) -> str | None:
14
+ auth = request.headers.get("authorization")
15
+ if not auth:
16
+ return None
17
+ parts = auth.split(" ", 1)
18
+ if len(parts) != 2 or parts[0].lower() != "bearer":
19
+ return None
20
+ return parts[1].strip() or None
21
+
22
+
23
+ def _issuer_from_request(request: Request) -> Any:
24
+ from hawkapi_auth._plugin import resolve_issuer # noqa: PLC0415
25
+
26
+ return resolve_issuer(request.scope.get("app"))
27
+
28
+
29
+ async def requires_user(request: Request) -> str:
30
+ """DI helper: validate the ``Authorization: Bearer <jwt>`` header.
31
+
32
+ Returns the ``sub`` claim of the access token. Raises ``HTTPException(401)``
33
+ on missing / invalid / revoked tokens.
34
+
35
+ Usage:
36
+ @app.get("/me")
37
+ async def me(user_id: str = Depends(requires_user)):
38
+ return await db.fetch_user(user_id)
39
+ """
40
+ token = _bearer_token(request)
41
+ if token is None:
42
+ raise HTTPException(
43
+ 401, detail="Missing bearer token", headers={"WWW-Authenticate": "Bearer"}
44
+ )
45
+ issuer = _issuer_from_request(request)
46
+ if issuer is None:
47
+ raise HTTPException(500, detail="hawkapi-auth not initialised")
48
+ try:
49
+ claims = issuer.verify_access(token)
50
+ except TokenError as exc:
51
+ raise HTTPException(401, detail=str(exc), headers={"WWW-Authenticate": "Bearer"}) from exc
52
+ sub = claims.get("sub")
53
+ if not isinstance(sub, str):
54
+ raise HTTPException(401, detail="Token has no subject")
55
+ return sub
56
+
57
+
58
+ async def requires_claims(request: Request) -> dict[str, Any]:
59
+ """DI helper that returns the full set of verified claims, not just ``sub``."""
60
+ token = _bearer_token(request)
61
+ if token is None:
62
+ raise HTTPException(
63
+ 401, detail="Missing bearer token", headers={"WWW-Authenticate": "Bearer"}
64
+ )
65
+ issuer = _issuer_from_request(request)
66
+ if issuer is None:
67
+ raise HTTPException(500, detail="hawkapi-auth not initialised")
68
+ try:
69
+ return issuer.verify_access(token)
70
+ except TokenError as exc:
71
+ raise HTTPException(401, detail=str(exc), headers={"WWW-Authenticate": "Bearer"}) from exc
72
+
73
+
74
+ def requires_scopes(*required: str):
75
+ """Build a DI dependency that gates on the ``scope`` claim.
76
+
77
+ The access token must carry a ``scope`` claim — either a space-separated
78
+ string (OAuth2 style) or a list of strings. All entries in *required* must
79
+ be present.
80
+
81
+ Usage:
82
+ @app.get("/admin", dependencies=[Depends(requires_scopes("admin"))])
83
+ async def admin(): ...
84
+ """
85
+
86
+ async def _dep(request: Request) -> dict[str, Any]:
87
+ claims = await requires_claims(request)
88
+ raw = claims.get("scope") or claims.get("scopes") or []
89
+ granted: set[str] = set()
90
+ if isinstance(raw, str):
91
+ granted = set(raw.split())
92
+ elif isinstance(raw, list):
93
+ granted = {s for s in raw if isinstance(s, str)}
94
+ missing = [s for s in required if s not in granted]
95
+ if missing:
96
+ raise HTTPException(403, detail=f"Missing scopes: {', '.join(missing)}")
97
+ return claims
98
+
99
+ return _dep
100
+
101
+
102
+ __all__ = ["requires_user", "requires_claims", "requires_scopes"]
@@ -0,0 +1,40 @@
1
+ """Password hashing helpers — argon2id by default."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from argon2 import PasswordHasher
6
+ from argon2.exceptions import VerifyMismatchError
7
+
8
+ _hasher = PasswordHasher()
9
+
10
+
11
+ def hash_password(password: str) -> str:
12
+ """Return an argon2id hash of *password* (PHC string format)."""
13
+ return _hasher.hash(password)
14
+
15
+
16
+ def verify_password(password: str, stored_hash: str) -> bool:
17
+ """Constant-time check of *password* against a stored argon2 hash.
18
+
19
+ Returns False on mismatch or invalid hash format. Never raises so it is
20
+ safe to use directly in handler logic.
21
+ """
22
+ try:
23
+ return _hasher.verify(stored_hash, password)
24
+ except (VerifyMismatchError, Exception):
25
+ return False
26
+
27
+
28
+ def needs_rehash(stored_hash: str) -> bool:
29
+ """True when *stored_hash* should be re-hashed with current parameters.
30
+
31
+ Call this after a successful login and re-hash the password if it returns
32
+ True — protects against algorithm / parameter upgrades over time.
33
+ """
34
+ try:
35
+ return _hasher.check_needs_rehash(stored_hash)
36
+ except Exception:
37
+ return False
38
+
39
+
40
+ __all__ = ["hash_password", "verify_password", "needs_rehash"]
@@ -0,0 +1,64 @@
1
+ """``init_auth`` — wires a JWT issuer into the HawkAPI app."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from hawkapi_auth._tokens import JWTConfig, RevocationList, TokenIssuer
8
+
9
+
10
+ class _StateNamespace:
11
+ """Lightweight attribute bag for ``app.state`` when none exists."""
12
+
13
+ auth: Any # set by init_auth
14
+
15
+
16
+ _ACTIVE_ISSUERS: dict[int, TokenIssuer] = {}
17
+ _LAST_ISSUER: list[TokenIssuer | None] = [None]
18
+
19
+
20
+ def resolve_issuer(app: Any) -> TokenIssuer | None:
21
+ """Look up the active TokenIssuer for *app*, or the last-attached as a fallback.
22
+
23
+ Mirrors the lookup pattern from hawkapi-cache: TestClient does not populate
24
+ ``scope["app"]``, so the DI dependency uses a module-level registry.
25
+ """
26
+ if app is not None:
27
+ issuer = _ACTIVE_ISSUERS.get(id(app))
28
+ if issuer is not None:
29
+ return issuer
30
+ return _LAST_ISSUER[0]
31
+
32
+
33
+ def init_auth(
34
+ app: Any,
35
+ *,
36
+ config: JWTConfig,
37
+ revocation: RevocationList | None = None,
38
+ ) -> TokenIssuer:
39
+ """Mount a :class:`TokenIssuer` on ``app.state.auth`` and register lookup.
40
+
41
+ ```python
42
+ from hawkapi import HawkAPI
43
+ from hawkapi_auth import init_auth, JWTConfig, random_secret
44
+
45
+ app = HawkAPI()
46
+ init_auth(app, config=JWTConfig(secret=random_secret()))
47
+ ```
48
+
49
+ After this call:
50
+
51
+ * ``app.state.auth`` is the :class:`TokenIssuer`.
52
+ * ``Depends(requires_user)`` resolves the ``sub`` claim from
53
+ ``Authorization: Bearer …`` headers.
54
+ """
55
+ issuer = TokenIssuer(config=config, revocation=revocation)
56
+ if getattr(app, "state", None) is None:
57
+ app.state = _StateNamespace()
58
+ app.state.auth = issuer
59
+ _ACTIVE_ISSUERS[id(app)] = issuer
60
+ _LAST_ISSUER[0] = issuer
61
+ return issuer
62
+
63
+
64
+ __all__ = ["init_auth", "resolve_issuer"]
@@ -0,0 +1,168 @@
1
+ """JWT access + refresh token issue/verify."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import secrets
6
+ import time
7
+ import uuid
8
+ from dataclasses import dataclass, field
9
+ from typing import Any
10
+
11
+ import jwt
12
+ from jwt.exceptions import InvalidTokenError
13
+
14
+
15
+ class TokenError(Exception):
16
+ """Raised when a token fails validation."""
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class JWTConfig:
21
+ """JWT signing configuration.
22
+
23
+ Attributes:
24
+ secret: HMAC secret for symmetric algorithms (HS256/384/512). For RS*
25
+ / ES* use ``private_key`` + ``public_key`` instead.
26
+ algorithm: Signing algorithm. ``HS256`` by default.
27
+ access_ttl_seconds: Lifetime of issued access tokens. 15 minutes default.
28
+ refresh_ttl_seconds: Lifetime of issued refresh tokens. 30 days default.
29
+ issuer: Optional ``iss`` claim.
30
+ audience: Optional ``aud`` claim (string or list of strings).
31
+ private_key: PEM-encoded private key when using asymmetric algorithms.
32
+ public_key: PEM-encoded public key when using asymmetric algorithms.
33
+ """
34
+
35
+ secret: str = ""
36
+ algorithm: str = "HS256"
37
+ access_ttl_seconds: int = 15 * 60
38
+ refresh_ttl_seconds: int = 30 * 24 * 60 * 60
39
+ issuer: str | None = None
40
+ audience: str | list[str] | None = None
41
+ private_key: str = ""
42
+ public_key: str = ""
43
+ # Token-type claim values — exposed so consumers can match on them.
44
+ access_type: str = "access"
45
+ refresh_type: str = "refresh"
46
+
47
+
48
+ @dataclass
49
+ class TokenIssuer:
50
+ """Issue + verify JWTs against a :class:`JWTConfig`.
51
+
52
+ Stateless. For refresh-token revocation use :class:`RevocationList`.
53
+ """
54
+
55
+ config: JWTConfig
56
+ revocation: RevocationList | None = None
57
+
58
+ def issue_access(self, subject: str, **extra_claims: Any) -> str:
59
+ return self._issue(
60
+ subject, self.config.access_type, self.config.access_ttl_seconds, extra_claims
61
+ )
62
+
63
+ def issue_refresh(self, subject: str, **extra_claims: Any) -> str:
64
+ return self._issue(
65
+ subject, self.config.refresh_type, self.config.refresh_ttl_seconds, extra_claims
66
+ )
67
+
68
+ def verify_access(self, token: str) -> dict[str, Any]:
69
+ return self._verify(token, expected_type=self.config.access_type)
70
+
71
+ def verify_refresh(self, token: str) -> dict[str, Any]:
72
+ return self._verify(token, expected_type=self.config.refresh_type)
73
+
74
+ def revoke_refresh(self, token: str) -> None:
75
+ if self.revocation is None:
76
+ raise TokenError("no RevocationList configured")
77
+ claims = self._verify(token, expected_type=self.config.refresh_type, allow_revoked=True)
78
+ jti = claims.get("jti")
79
+ if not isinstance(jti, str):
80
+ raise TokenError("token has no jti claim")
81
+ exp = int(claims.get("exp", 0))
82
+ self.revocation.revoke(jti, exp)
83
+
84
+ def _issue(self, subject: str, token_type: str, ttl: int, extra: dict[str, Any]) -> str:
85
+ now = int(time.time())
86
+ payload: dict[str, Any] = {
87
+ "sub": subject,
88
+ "iat": now,
89
+ "exp": now + ttl,
90
+ "jti": uuid.uuid4().hex,
91
+ "type": token_type,
92
+ **extra,
93
+ }
94
+ if self.config.issuer:
95
+ payload["iss"] = self.config.issuer
96
+ if self.config.audience:
97
+ payload["aud"] = self.config.audience
98
+ key = self.config.private_key or self.config.secret
99
+ if not key:
100
+ raise TokenError("JWTConfig has no secret or private_key")
101
+ return jwt.encode(payload, key, algorithm=self.config.algorithm)
102
+
103
+ def _verify(
104
+ self, token: str, *, expected_type: str, allow_revoked: bool = False
105
+ ) -> dict[str, Any]:
106
+ key = self.config.public_key or self.config.secret
107
+ if not key:
108
+ raise TokenError("JWTConfig has no secret or public_key")
109
+ options: dict[str, Any] = {}
110
+ if self.config.audience is None:
111
+ options["verify_aud"] = False
112
+ try:
113
+ claims: dict[str, Any] = jwt.decode(
114
+ token,
115
+ key,
116
+ algorithms=[self.config.algorithm],
117
+ audience=self.config.audience,
118
+ issuer=self.config.issuer,
119
+ options=options, # type: ignore[arg-type]
120
+ )
121
+ except InvalidTokenError as exc:
122
+ raise TokenError(str(exc)) from exc
123
+ if claims.get("type") != expected_type:
124
+ raise TokenError(f"expected {expected_type!r} token, got {claims.get('type')!r}")
125
+ if not allow_revoked and self.revocation is not None:
126
+ jti = claims.get("jti")
127
+ if isinstance(jti, str) and self.revocation.is_revoked(jti):
128
+ raise TokenError("token revoked")
129
+ return claims
130
+
131
+
132
+ @dataclass
133
+ class RevocationList:
134
+ """In-memory revoked-token registry. Keys are JTI claims; values are expiry.
135
+
136
+ Expired entries are dropped lazily on every access. For multi-process
137
+ deployments use a Redis-backed implementation (TODO v0.2.0).
138
+ """
139
+
140
+ _store: dict[str, int] = field(default_factory=dict)
141
+
142
+ def revoke(self, jti: str, exp_timestamp: int) -> None:
143
+ self._sweep()
144
+ self._store[jti] = exp_timestamp
145
+
146
+ def is_revoked(self, jti: str) -> bool:
147
+ self._sweep()
148
+ return jti in self._store
149
+
150
+ def _sweep(self) -> None:
151
+ now = int(time.time())
152
+ dead = [k for k, exp in self._store.items() if exp <= now]
153
+ for k in dead:
154
+ self._store.pop(k, None)
155
+
156
+
157
+ def random_secret(length: int = 64) -> str:
158
+ """Return a URL-safe random secret suitable for JWTConfig.secret."""
159
+ return secrets.token_urlsafe(length)
160
+
161
+
162
+ __all__ = [
163
+ "JWTConfig",
164
+ "RevocationList",
165
+ "TokenError",
166
+ "TokenIssuer",
167
+ "random_secret",
168
+ ]
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: hawkapi-auth
3
+ Version: 0.1.0
4
+ Summary: JWT auth for HawkAPI — access + refresh tokens, password hashing, DI guards
5
+ Project-URL: Homepage, https://pypi.org/project/hawkapi-auth/
6
+ Project-URL: Repository, https://github.com/ashimov/hawkapi-auth
7
+ Project-URL: Issues, https://github.com/ashimov/hawkapi-auth/issues
8
+ Author-email: HawkAPI Contributors <hawkapi@users.noreply.github.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 HawkAPI Contributors
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: argon2,auth,authentication,bcrypt,hawkapi,jwt
32
+ Classifier: Development Status :: 5 - Production/Stable
33
+ Classifier: Framework :: AsyncIO
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Classifier: Topic :: Internet :: WWW/HTTP
40
+ Classifier: Topic :: Security
41
+ Classifier: Typing :: Typed
42
+ Requires-Python: >=3.12
43
+ Requires-Dist: argon2-cffi>=23.1
44
+ Requires-Dist: hawkapi>=0.1.7
45
+ Requires-Dist: pyjwt>=2.8
46
+ Provides-Extra: dev
47
+ Requires-Dist: httpx>=0.27; extra == 'dev'
48
+ Requires-Dist: pyright>=1.1; extra == 'dev'
49
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
50
+ Requires-Dist: pytest>=8.0; extra == 'dev'
51
+ Requires-Dist: ruff>=0.8; extra == 'dev'
52
+ Description-Content-Type: text/markdown
53
+
54
+ # hawkapi-auth
55
+
56
+ JWT auth for [HawkAPI](https://github.com/ashimov/HawkAPI). Access + refresh tokens, argon2id password hashing, DI guards, scope-based access control.
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ pip install hawkapi-auth
62
+ ```
63
+
64
+ ## Quickstart
65
+
66
+ ```python
67
+ from hawkapi import Depends, HawkAPI, HTTPException
68
+ from hawkapi_auth import (
69
+ JWTConfig,
70
+ hash_password,
71
+ init_auth,
72
+ random_secret,
73
+ requires_user,
74
+ verify_password,
75
+ )
76
+
77
+ app = HawkAPI()
78
+ init_auth(app, config=JWTConfig(secret=random_secret()))
79
+
80
+
81
+ @app.post("/register")
82
+ async def register(email: str, password: str):
83
+ await db.create_user(email=email, password_hash=hash_password(password))
84
+ return {"ok": True}
85
+
86
+
87
+ @app.post("/login")
88
+ async def login(email: str, password: str):
89
+ user = await db.find_user(email)
90
+ if not user or not verify_password(password, user.password_hash):
91
+ raise HTTPException(401, detail="Invalid credentials")
92
+ issuer = app.state.auth
93
+ return {
94
+ "access_token": issuer.issue_access(user.id),
95
+ "refresh_token": issuer.issue_refresh(user.id),
96
+ }
97
+
98
+
99
+ @app.get("/me")
100
+ async def me(user_id: str = Depends(requires_user)):
101
+ return await db.fetch_user(user_id)
102
+ ```
103
+
104
+ ## Token issue / verify
105
+
106
+ ```python
107
+ issuer = app.state.auth # TokenIssuer
108
+
109
+ access = issuer.issue_access("user-1", role="admin", scope="read write")
110
+ refresh = issuer.issue_refresh("user-1")
111
+
112
+ claims = issuer.verify_access(access) # raises TokenError on bad token
113
+ claims = issuer.verify_refresh(refresh) # ditto, plus checks the token type
114
+ ```
115
+
116
+ `issue_access` / `issue_refresh` accept arbitrary keyword claims (`role`, `scope`, anything JSON-serialisable).
117
+
118
+ ## JWTConfig
119
+
120
+ ```python
121
+ JWTConfig(
122
+ secret="…", # HMAC secret for HS256/384/512
123
+ algorithm="HS256",
124
+ access_ttl_seconds=15 * 60,
125
+ refresh_ttl_seconds=30 * 24 * 60 * 60,
126
+ issuer="my-service", # optional iss claim
127
+ audience="my-api", # optional aud claim
128
+ private_key="", # RS*/ES* — PEM
129
+ public_key="",
130
+ )
131
+ ```
132
+
133
+ Use `random_secret()` to mint one. Store it outside of git.
134
+
135
+ ## DI guards
136
+
137
+ ```python
138
+ from hawkapi_auth import requires_user, requires_claims, requires_scopes
139
+
140
+ @app.get("/me")
141
+ async def me(user_id: str = Depends(requires_user)):
142
+ ...
143
+
144
+ @app.get("/dump")
145
+ async def dump(claims: dict = Depends(requires_claims)):
146
+ ...
147
+
148
+ @app.get("/admin", dependencies=[Depends(requires_scopes("admin"))])
149
+ async def admin():
150
+ ...
151
+ ```
152
+
153
+ `requires_scopes(*scopes)` expects either a space-separated `scope` claim or a list under `scope` / `scopes`. Missing scopes → 403.
154
+
155
+ ## Refresh + revocation
156
+
157
+ ```python
158
+ from hawkapi_auth import RevocationList
159
+
160
+ rev = RevocationList()
161
+ init_auth(app, config=JWTConfig(secret=...), revocation=rev)
162
+
163
+ @app.post("/refresh")
164
+ async def refresh(refresh_token: str):
165
+ issuer = app.state.auth
166
+ claims = issuer.verify_refresh(refresh_token)
167
+ return {"access_token": issuer.issue_access(claims["sub"])}
168
+
169
+ @app.post("/logout")
170
+ async def logout(refresh_token: str):
171
+ app.state.auth.revoke_refresh(refresh_token)
172
+ return {"ok": True}
173
+ ```
174
+
175
+ `RevocationList` is in-memory only. For multi-process deployments, swap in a Redis-backed implementation (planned in v0.2.0).
176
+
177
+ ## Password hashing
178
+
179
+ ```python
180
+ from hawkapi_auth import hash_password, verify_password, needs_rehash
181
+
182
+ h = hash_password("hunter2") # argon2id
183
+ ok = verify_password("hunter2", h) # constant-time, returns bool
184
+ if needs_rehash(h):
185
+ h = hash_password("hunter2") # re-hash after a successful login
186
+ ```
187
+
188
+ `verify_password` never raises — safe to use directly in handler bodies.
189
+
190
+ ## What's not included (v0.2.0 roadmap)
191
+
192
+ * Social OAuth providers (Google / GitHub / Discord / Microsoft).
193
+ * Email-based password reset + verification flows.
194
+ * Pre-built user model and storage.
195
+ * Redis-backed `RevocationList`.
196
+
197
+ ## Development
198
+
199
+ ```bash
200
+ git clone https://github.com/ashimov/hawkapi-auth.git
201
+ cd hawkapi-auth
202
+ uv sync --extra dev
203
+ uv run pytest -q
204
+ uv run ruff check . && uv run ruff format --check .
205
+ uv run pyright src/
206
+ ```
207
+
208
+ ## License
209
+
210
+ MIT.
@@ -0,0 +1,9 @@
1
+ hawkapi_auth/__init__.py,sha256=wszL8wfaZObC3F8X8Y92uuGPr8ypWVvrNF5amuQUsWU,749
2
+ hawkapi_auth/_deps.py,sha256=IgHC0bVXfqGRGgchWDLqmjMPb-bgzi25csN6OQVZxPc,3479
3
+ hawkapi_auth/_passwords.py,sha256=5mTy64MP3icz3hRwufLgWVOpLp5XY3v3OM_pnikdZds,1195
4
+ hawkapi_auth/_plugin.py,sha256=3d3ybjllXf6sUVzZvS8VS523HfG1DUHCkpTQ1ZSVnZw,1783
5
+ hawkapi_auth/_tokens.py,sha256=9HtRxbqgQm1-dQqfFugeraeepvkiavw3d_HetlrhNo8,5755
6
+ hawkapi_auth-0.1.0.dist-info/METADATA,sha256=JlK9jvSD1H8lfuyP69F_ZD27EmXDomu9a7LmbvmZwsw,6654
7
+ hawkapi_auth-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
8
+ hawkapi_auth-0.1.0.dist-info/licenses/LICENSE,sha256=_RpjhvsfLqqeG_gv2cRatjIxCTGXTpXhKU9jqLZXYa4,1077
9
+ hawkapi_auth-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HawkAPI Contributors
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.