jamlib 3.2.0rc0__tar.gz → 3.2.0rc2__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.
- {jamlib-3.2.0rc0/src/jamlib.egg-info → jamlib-3.2.0rc2}/PKG-INFO +3 -3
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/README.md +2 -2
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/pyproject.toml +1 -1
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/__init__.py +1 -1
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/__base__.py +8 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/instance.py +21 -9
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/sessions/__base__.py +5 -1
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/ext/flask/extensions.py +2 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/ext/litestar/plugins.py +3 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/ext/starlette/backends.py +2 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jose/__base__.py +16 -11
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jose/jws.py +4 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jose/jwt.py +12 -10
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/paseto/v1.py +4 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/paseto/v2.py +4 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/paseto/v3.py +4 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/paseto/v4.py +4 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/sessions/__base__.py +5 -1
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/tests/clients.py +4 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/utils/config_maker.py +15 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2/src/jamlib.egg-info}/PKG-INFO +3 -3
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/LICENSE.md +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/setup.cfg +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/__base__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/__base_encoder__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/__deprecated__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/jwt/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/oauth2/__base__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/oauth2/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/oauth2/builtin/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/oauth2/builtin/github.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/oauth2/builtin/gitlab.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/oauth2/builtin/google.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/oauth2/builtin/yandex.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/oauth2/client.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/sessions/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/sessions/json.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/aio/sessions/redis.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/cli/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/cli/cli.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/cli/commands/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/cli/commands/keys.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/cli/commands/password.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/encoders.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/exceptions/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/exceptions/base.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/exceptions/jose.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/exceptions/jwt.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/exceptions/oauth2.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/exceptions/paseto.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/exceptions/plugins.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/exceptions/sessions.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/ext/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/ext/fastapi/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/ext/flask/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/ext/flask/objects.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/ext/litestar/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/ext/litestar/middleware.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/ext/litestar/objects.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/ext/starlette/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/ext/starlette/objects.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/instance.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jose/__algorithms__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jose/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jose/jwe.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jose/jwk.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jose/lists/__base__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jose/lists/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jose/lists/json.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jose/lists/memory.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jose/lists/redis.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jose/utils.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jwt/__algorithms__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jwt/__base__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jwt/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jwt/__types__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jwt/lists/__base__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jwt/lists/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jwt/lists/json.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jwt/lists/redis.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jwt/module.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/jwt/utils.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/logger.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/oauth2/__base__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/oauth2/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/oauth2/builtin/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/oauth2/builtin/github.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/oauth2/builtin/gitlab.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/oauth2/builtin/google.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/oauth2/builtin/yandex.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/oauth2/client.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/otp/__base__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/otp/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/otp/hotp.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/otp/totp.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/paseto/__base__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/paseto/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/paseto/utils.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/plugins/__base__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/plugins/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/py.typed +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/sessions/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/sessions/json.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/sessions/redis.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/tests/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/tests/fakers.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/utils/__init__.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/utils/aes.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/utils/await_maybe.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/utils/basic_auth.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/utils/ed.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/utils/otp_keys.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/utils/rsa.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/utils/salt_hash.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/utils/symmetric.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/utils/version_check.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/utils/xchacha20poly1305.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jam/utils/xor.py +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jamlib.egg-info/SOURCES.txt +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jamlib.egg-info/dependency_links.txt +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jamlib.egg-info/entry_points.txt +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jamlib.egg-info/requires.txt +0 -0
- {jamlib-3.2.0rc0 → jamlib-3.2.0rc2}/src/jamlib.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jamlib
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.0rc2
|
|
4
4
|
Summary: Simple and universal library for authorization.
|
|
5
5
|
Author-email: Makridenko Adrian <adrianmakridenko@duck.com>, Ksenia Travnikova <kseniatravnikova@duck.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -77,7 +77,7 @@ from jam import Jam
|
|
|
77
77
|
|
|
78
78
|
jam = Jam(config="config.toml")
|
|
79
79
|
|
|
80
|
-
jwt = jam.
|
|
80
|
+
jwt = jam.jwt_encode(payload={"user": 1})
|
|
81
81
|
session_id = jam.session_create(session_key="username", data={"user": 1})
|
|
82
82
|
otp_code = jam.otp_code(secret="3DB7FOAOFBCI3WFDRE7EPF43CA")
|
|
83
83
|
```
|
|
@@ -85,7 +85,7 @@ otp_code = jam.otp_code(secret="3DB7FOAOFBCI3WFDRE7EPF43CA")
|
|
|
85
85
|
## Why Jam?
|
|
86
86
|
Jam is a library that provides the most popular AUTH* mechanisms right out of the box.
|
|
87
87
|
|
|
88
|
-
* [JOSE](https://jam.makridenko.ru/usage/jose/
|
|
88
|
+
* [JOSE](https://jam.makridenko.ru/usage/jose/)
|
|
89
89
|
* [PASETO](https://jam.makridenko.ru/usage/paseto/)
|
|
90
90
|
* [Server side sessions](https://jam.makridenko.ru/usage/sessions/)
|
|
91
91
|
* [OTP](https://jam.makridenko.ru/usage/otp/)
|
|
@@ -24,7 +24,7 @@ from jam import Jam
|
|
|
24
24
|
|
|
25
25
|
jam = Jam(config="config.toml")
|
|
26
26
|
|
|
27
|
-
jwt = jam.
|
|
27
|
+
jwt = jam.jwt_encode(payload={"user": 1})
|
|
28
28
|
session_id = jam.session_create(session_key="username", data={"user": 1})
|
|
29
29
|
otp_code = jam.otp_code(secret="3DB7FOAOFBCI3WFDRE7EPF43CA")
|
|
30
30
|
```
|
|
@@ -32,7 +32,7 @@ otp_code = jam.otp_code(secret="3DB7FOAOFBCI3WFDRE7EPF43CA")
|
|
|
32
32
|
## Why Jam?
|
|
33
33
|
Jam is a library that provides the most popular AUTH* mechanisms right out of the box.
|
|
34
34
|
|
|
35
|
-
* [JOSE](https://jam.makridenko.ru/usage/jose/
|
|
35
|
+
* [JOSE](https://jam.makridenko.ru/usage/jose/)
|
|
36
36
|
* [PASETO](https://jam.makridenko.ru/usage/paseto/)
|
|
37
37
|
* [Server side sessions](https://jam.makridenko.ru/usage/sessions/)
|
|
38
38
|
* [OTP](https://jam.makridenko.ru/usage/otp/)
|
|
@@ -4,6 +4,7 @@ from abc import abstractmethod
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from jam.__base__ import BaseJam
|
|
7
|
+
from jam.__deprecated__ import deprecated
|
|
7
8
|
from jam.aio.oauth2.__base__ import BaseAsyncOAuth2Client
|
|
8
9
|
from jam.aio.sessions.__base__ import BaseAsyncSessionModule
|
|
9
10
|
from jam.jose.__base__ import BaseJWE, BaseJWS
|
|
@@ -21,11 +22,17 @@ class BaseAsyncJam(BaseJam):
|
|
|
21
22
|
jwe: BaseJWE | None = None # type: ignore[override]
|
|
22
23
|
|
|
23
24
|
@abstractmethod
|
|
25
|
+
@deprecated(
|
|
26
|
+
"This method is deprecated; the JWT payload is generated automatically in accordance with the specification."
|
|
27
|
+
)
|
|
24
28
|
async def jwt_make_payload( # type: ignore[override]
|
|
25
29
|
self, exp: int | None, data: dict[str, Any]
|
|
26
30
|
) -> dict[str, Any]:
|
|
27
31
|
"""Make JWT-specific payload.
|
|
28
32
|
|
|
33
|
+
!!! Deprecated
|
|
34
|
+
This method is deprecated; the JWT payload is generated automatically in accordance with the specification.
|
|
35
|
+
|
|
29
36
|
Args:
|
|
30
37
|
exp (int | None): Token expire, if None -> use default
|
|
31
38
|
data (dict[str, Any]): Data to payload
|
|
@@ -36,6 +43,7 @@ class BaseAsyncJam(BaseJam):
|
|
|
36
43
|
raise NotImplementedError
|
|
37
44
|
|
|
38
45
|
@abstractmethod
|
|
46
|
+
@deprecated("Use jam.jwt_encode")
|
|
39
47
|
async def jwt_create( # type: ignore[override]
|
|
40
48
|
self, payload: dict[str, Any]
|
|
41
49
|
) -> str:
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
|
-
import datetime
|
|
4
3
|
import time
|
|
5
4
|
from typing import Any
|
|
6
5
|
import uuid
|
|
7
6
|
|
|
7
|
+
from jam.__deprecated__ import deprecated
|
|
8
8
|
from jam.aio.__base__ import BaseAsyncJam
|
|
9
9
|
from jam.exceptions import (
|
|
10
10
|
JamConfigurationError,
|
|
@@ -30,11 +30,17 @@ class Jam(BaseAsyncJam):
|
|
|
30
30
|
"otp": "jam.otp.__base__.OTPConfig",
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
@deprecated(
|
|
34
|
+
"This method is deprecated; the JWT payload is generated automatically in accordance with the specification."
|
|
35
|
+
)
|
|
33
36
|
async def jwt_make_payload(
|
|
34
37
|
self, exp: int | None, data: dict[str, Any]
|
|
35
38
|
) -> dict[str, Any]:
|
|
36
39
|
"""Make JWT-specific payload.
|
|
37
40
|
|
|
41
|
+
!!! Deprecated
|
|
42
|
+
This method is deprecated; the JWT payload is generated automatically in accordance with the specification.
|
|
43
|
+
|
|
38
44
|
Args:
|
|
39
45
|
exp (int | None): Token expire
|
|
40
46
|
data (dict[str, Any]): Data to payload
|
|
@@ -42,17 +48,22 @@ class Jam(BaseAsyncJam):
|
|
|
42
48
|
Returns:
|
|
43
49
|
dict[str, Any]: Payload
|
|
44
50
|
"""
|
|
51
|
+
now = time.time()
|
|
45
52
|
payload = {
|
|
46
|
-
"iat":
|
|
47
|
-
"exp": (
|
|
53
|
+
"iat": now,
|
|
54
|
+
"exp": (now + exp) if exp else None,
|
|
48
55
|
"jti": str(uuid.uuid4()),
|
|
49
56
|
}
|
|
50
57
|
payload = payload | data
|
|
51
58
|
return payload
|
|
52
59
|
|
|
60
|
+
@deprecated("Use jam.jwt_encode")
|
|
53
61
|
async def jwt_create(self, payload: dict[str, Any]) -> str:
|
|
54
62
|
"""Create JWT token.
|
|
55
63
|
|
|
64
|
+
!!! Deprecated
|
|
65
|
+
Use Jam.jwt_encode
|
|
66
|
+
|
|
56
67
|
Args:
|
|
57
68
|
payload (dict[str, Any]): Data payload
|
|
58
69
|
|
|
@@ -244,7 +255,10 @@ class Jam(BaseAsyncJam):
|
|
|
244
255
|
"""
|
|
245
256
|
assert self.jwe is not None
|
|
246
257
|
self._logger.debug(f"Encrypting data with JWE, header: {header}")
|
|
247
|
-
token = self.jwe.encrypt(
|
|
258
|
+
token = self.jwe.encrypt(
|
|
259
|
+
self._serializer.dumps(data) if isinstance(data, dict) else data,
|
|
260
|
+
header,
|
|
261
|
+
)
|
|
248
262
|
self._logger.debug(f"JWE token created, length: {len(token)}")
|
|
249
263
|
return token
|
|
250
264
|
|
|
@@ -369,8 +383,8 @@ class Jam(BaseAsyncJam):
|
|
|
369
383
|
async def otp_uri(
|
|
370
384
|
self,
|
|
371
385
|
secret: str,
|
|
372
|
-
name: str
|
|
373
|
-
issuer: str
|
|
386
|
+
name: str,
|
|
387
|
+
issuer: str,
|
|
374
388
|
counter: int | None = None,
|
|
375
389
|
) -> str:
|
|
376
390
|
"""Generates an otpauth:// URI for Google Authenticator.
|
|
@@ -388,9 +402,7 @@ class Jam(BaseAsyncJam):
|
|
|
388
402
|
assert self._otp is not None
|
|
389
403
|
return self._otp(
|
|
390
404
|
secret=secret, digits=self.otp.digits, digest=self.otp.digest
|
|
391
|
-
).provisioning_uri(
|
|
392
|
-
name=name or "", issuer=issuer or "", counter=counter
|
|
393
|
-
)
|
|
405
|
+
).provisioning_uri(name=name, issuer=issuer, counter=counter)
|
|
394
406
|
|
|
395
407
|
async def otp_verify_code(
|
|
396
408
|
self,
|
|
@@ -7,9 +7,11 @@ from uuid import uuid4
|
|
|
7
7
|
|
|
8
8
|
from cryptography.fernet import Fernet
|
|
9
9
|
|
|
10
|
-
from jam.
|
|
10
|
+
from jam.__base_encoder__ import BaseEncoder
|
|
11
|
+
from jam.encoders import JsonEncoder
|
|
11
12
|
from jam.exceptions import JamSessionEmptyAESKey
|
|
12
13
|
from jam.logger import BaseLogger
|
|
14
|
+
from jam.utils.config_maker import __key_loader__
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class BaseAsyncSessionModule(ABC):
|
|
@@ -32,6 +34,8 @@ class BaseAsyncSessionModule(ABC):
|
|
|
32
34
|
raise JamSessionEmptyAESKey
|
|
33
35
|
if is_session_crypt:
|
|
34
36
|
assert session_aes_secret is not None
|
|
37
|
+
if isinstance(session_aes_secret, str):
|
|
38
|
+
session_aes_secret = __key_loader__(session_aes_secret)
|
|
35
39
|
self._code_session_key = Fernet(session_aes_secret)
|
|
36
40
|
|
|
37
41
|
def __encode_session_id__(self, data: str) -> str:
|
|
@@ -89,6 +89,8 @@ class BaseAuthExtension(BaseExtension):
|
|
|
89
89
|
_config: dict[str, Any] | None = (
|
|
90
90
|
__config_maker__(config, pointer) if config else None
|
|
91
91
|
)
|
|
92
|
+
if _config and _config.get("jose", None) and self.MODULE == create_jwt:
|
|
93
|
+
_config = _config["jose"]
|
|
92
94
|
|
|
93
95
|
params = _config.pop(self._CONFIG_KEY) if _config else kwargs
|
|
94
96
|
super().__init__(app=app, **params)
|
|
@@ -76,6 +76,9 @@ class BasePlugin(InitPlugin):
|
|
|
76
76
|
__config_maker__(config, pointer) if config else None
|
|
77
77
|
)
|
|
78
78
|
|
|
79
|
+
# FIXME: Make config wrapper
|
|
80
|
+
if _config and _config.get("jose", None) and self.MODULE == create_jwt:
|
|
81
|
+
_config = _config["jose"]
|
|
79
82
|
params = _config.pop(self._CONFIG_KEY) if _config else kwargs
|
|
80
83
|
self._setup_config(params)
|
|
81
84
|
self._middleware = None
|
|
@@ -81,6 +81,8 @@ class BaseBackend(AuthenticationBackend):
|
|
|
81
81
|
try:
|
|
82
82
|
if config:
|
|
83
83
|
config_ = __config_maker__(config, pointer)[self._CONFIG_KEY]
|
|
84
|
+
if config_.get("jose", None) and self.MODULE == create_jwt:
|
|
85
|
+
config_ = config_["jose"]
|
|
84
86
|
self._auth = self.MODULE(**config_)
|
|
85
87
|
else:
|
|
86
88
|
self._auth = self.MODULE(**kwargs)
|
|
@@ -73,34 +73,39 @@ class BaseJWT(ABC):
|
|
|
73
73
|
@abstractmethod
|
|
74
74
|
def encrypt(
|
|
75
75
|
self,
|
|
76
|
-
|
|
76
|
+
plaintext: bytes | str | dict[str, Any],
|
|
77
77
|
header: dict[str, Any] | None = None,
|
|
78
78
|
) -> str:
|
|
79
|
-
"""Encrypt
|
|
79
|
+
"""Encrypt plaintext.
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
Produces JWE Compact Serialization:
|
|
82
|
+
BASE64URL(header).BASE64URL(encrypted_key).BASE64URL(iv).BASE64URL(ciphertext).BASE64URL(tag)
|
|
82
83
|
|
|
83
84
|
Args:
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
plaintext: Data to encrypt. If str, will be encoded to UTF-8.
|
|
86
|
+
If dict, will be JSON encoded.
|
|
87
|
+
header: JWE header (must include 'alg' and 'enc').
|
|
86
88
|
|
|
87
89
|
Returns:
|
|
88
|
-
|
|
90
|
+
JWE compact serialization string.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
JamJWEEncryptionError: If encryption fails.
|
|
89
94
|
"""
|
|
90
95
|
raise NotImplementedError
|
|
91
96
|
|
|
92
97
|
@abstractmethod
|
|
93
|
-
def decrypt(self, token: str) -> dict[str, Any]:
|
|
94
|
-
"""Decrypt JWE
|
|
98
|
+
def decrypt(self, token: str) -> dict[str, Any] | bytes:
|
|
99
|
+
"""Decrypt JWE token.
|
|
95
100
|
|
|
96
101
|
Args:
|
|
97
|
-
token:
|
|
102
|
+
token: JWE compact serialization string.
|
|
98
103
|
|
|
99
104
|
Returns:
|
|
100
|
-
|
|
105
|
+
bytes: Decrypted plaintext.
|
|
101
106
|
|
|
102
107
|
Raises:
|
|
103
|
-
|
|
108
|
+
JamJWEDecryptionError: If decryption fails.
|
|
104
109
|
"""
|
|
105
110
|
raise NotImplementedError
|
|
106
111
|
|
|
@@ -15,6 +15,7 @@ from jam.jose.__algorithms__ import (
|
|
|
15
15
|
from jam.jose.__base__ import BaseJWS
|
|
16
16
|
from jam.jose.utils import __base64url_decode__, __base64url_encode__
|
|
17
17
|
from jam.logger import BaseLogger, logger
|
|
18
|
+
from jam.utils.config_maker import __key_loader__
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
@@ -46,6 +47,9 @@ class JWS(BaseJWS):
|
|
|
46
47
|
self._alg = alg.upper()
|
|
47
48
|
self._validate_algorithm(self._alg)
|
|
48
49
|
|
|
50
|
+
if isinstance(key, str):
|
|
51
|
+
key = __key_loader__(key)
|
|
52
|
+
|
|
49
53
|
if isinstance(key, JWKClass):
|
|
50
54
|
key = key._to_keylike()
|
|
51
55
|
|
|
@@ -9,12 +9,12 @@ import uuid
|
|
|
9
9
|
from jam.__base_encoder__ import BaseEncoder
|
|
10
10
|
from jam.encoders import JsonEncoder
|
|
11
11
|
from jam.exceptions import (
|
|
12
|
+
JamConfigurationError,
|
|
12
13
|
JamJWTExpired,
|
|
13
14
|
JamJWTNotYetValid,
|
|
14
15
|
JamJWTUnsupportedAlgorithm,
|
|
15
16
|
)
|
|
16
17
|
from jam.exceptions.jose import (
|
|
17
|
-
JamConfigurationError,
|
|
18
18
|
JamInvalidKeyTypeError,
|
|
19
19
|
JamJWSVerificationError,
|
|
20
20
|
)
|
|
@@ -30,6 +30,7 @@ from jam.jose.jwe import JWE
|
|
|
30
30
|
from jam.jose.jws import JWS
|
|
31
31
|
from jam.jose.lists import BaseJWTList
|
|
32
32
|
from jam.logger import BaseLogger, logger
|
|
33
|
+
from jam.utils.config_maker import __key_loader__
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
if TYPE_CHECKING:
|
|
@@ -153,6 +154,7 @@ class JWT(BaseJWT):
|
|
|
153
154
|
if isinstance(key, JWKClass):
|
|
154
155
|
return key._to_keylike()
|
|
155
156
|
if isinstance(key, str):
|
|
157
|
+
key = __key_loader__(key)
|
|
156
158
|
return key.encode() if key else key
|
|
157
159
|
return key
|
|
158
160
|
|
|
@@ -498,7 +500,7 @@ class JWT(BaseJWT):
|
|
|
498
500
|
|
|
499
501
|
def encrypt(
|
|
500
502
|
self,
|
|
501
|
-
|
|
503
|
+
plaintext: bytes | str | dict[str, Any],
|
|
502
504
|
header: dict[str, Any] | None = None,
|
|
503
505
|
) -> str:
|
|
504
506
|
"""Encrypt payload using JWE.
|
|
@@ -508,8 +510,8 @@ class JWT(BaseJWT):
|
|
|
508
510
|
2. Use JWS result as plaintext for JWE (encrypt)
|
|
509
511
|
|
|
510
512
|
Args:
|
|
511
|
-
|
|
512
|
-
header: Additional JWE header.
|
|
513
|
+
plaintext (bytes | str | dict[str, Any]): Data to encrypt. If dict, will be JSON encoded.
|
|
514
|
+
header (dict[str, Any] | None): Additional JWE header.
|
|
513
515
|
|
|
514
516
|
Returns:
|
|
515
517
|
str: Encrypted JWT (JWE or JWS+JWE).
|
|
@@ -522,12 +524,12 @@ class JWT(BaseJWT):
|
|
|
522
524
|
message="JWE not configured. Provide 'enc' parameter."
|
|
523
525
|
)
|
|
524
526
|
|
|
525
|
-
if isinstance(
|
|
526
|
-
payload_bytes = self._serializer.dumps(
|
|
527
|
-
elif isinstance(
|
|
528
|
-
payload_bytes =
|
|
527
|
+
if isinstance(plaintext, dict):
|
|
528
|
+
payload_bytes = self._serializer.dumps(plaintext)
|
|
529
|
+
elif isinstance(plaintext, str):
|
|
530
|
+
payload_bytes = plaintext.encode("utf-8")
|
|
529
531
|
else:
|
|
530
|
-
payload_bytes =
|
|
532
|
+
payload_bytes = plaintext
|
|
531
533
|
|
|
532
534
|
if self.jws and self.jwe:
|
|
533
535
|
_base_header = {"alg": self._alg, "typ": "JWT"}
|
|
@@ -538,7 +540,7 @@ class JWT(BaseJWT):
|
|
|
538
540
|
else:
|
|
539
541
|
return self.jwe.encrypt(payload_bytes, header)
|
|
540
542
|
|
|
541
|
-
def decrypt(self, token: str) -> dict[str, Any]:
|
|
543
|
+
def decrypt(self, token: str) -> dict[str, Any] | bytes:
|
|
542
544
|
"""Decrypt JWE or JWS+JWE token.
|
|
543
545
|
|
|
544
546
|
If token is JWS+JWE (sign then encrypt):
|
|
@@ -29,6 +29,7 @@ from jam.paseto.utils import (
|
|
|
29
29
|
base64url_decode,
|
|
30
30
|
base64url_encode,
|
|
31
31
|
)
|
|
32
|
+
from jam.utils.config_maker import __key_loader__
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
class PASETOv1(BasePASETO):
|
|
@@ -59,6 +60,7 @@ class PASETOv1(BasePASETO):
|
|
|
59
60
|
|
|
60
61
|
if purpose == "local":
|
|
61
62
|
if isinstance(secret_key, str):
|
|
63
|
+
secret_key = __key_loader__(secret_key)
|
|
62
64
|
raw = base64url_decode(secret_key.encode("utf-8"))
|
|
63
65
|
else:
|
|
64
66
|
raw = secret_key
|
|
@@ -75,6 +77,8 @@ class PASETOv1(BasePASETO):
|
|
|
75
77
|
return inst
|
|
76
78
|
|
|
77
79
|
elif purpose == "public":
|
|
80
|
+
if isinstance(secret_key, str):
|
|
81
|
+
secret_key = __key_loader__(secret_key)
|
|
78
82
|
if isinstance(secret_key, RSAPrivateKey):
|
|
79
83
|
inst._secret = secret_key
|
|
80
84
|
inst._public_key = secret_key.public_key()
|
|
@@ -19,6 +19,7 @@ from jam.exceptions import (
|
|
|
19
19
|
)
|
|
20
20
|
from jam.paseto.__base__ import PASETO, BasePASETO
|
|
21
21
|
from jam.paseto.utils import __pae__, base64url_decode, base64url_encode
|
|
22
|
+
from jam.utils.config_maker import __key_loader__
|
|
22
23
|
from jam.utils.xchacha20poly1305 import (
|
|
23
24
|
xchacha20poly1305_decrypt,
|
|
24
25
|
xchacha20poly1305_encrypt,
|
|
@@ -42,6 +43,7 @@ class PASETOv2(BasePASETO):
|
|
|
42
43
|
|
|
43
44
|
if purpose == "local":
|
|
44
45
|
if isinstance(secret_key, str):
|
|
46
|
+
secret_key = __key_loader__(secret_key)
|
|
45
47
|
secret_key = base64.urlsafe_b64decode(secret_key + "==")
|
|
46
48
|
if not isinstance(secret_key, bytes) or len(secret_key) != 32:
|
|
47
49
|
raise ValueError("v2.local key must be 32 bytes")
|
|
@@ -49,6 +51,8 @@ class PASETOv2(BasePASETO):
|
|
|
49
51
|
return k
|
|
50
52
|
|
|
51
53
|
elif purpose == "public":
|
|
54
|
+
if isinstance(secret_key, str):
|
|
55
|
+
secret_key = __key_loader__(secret_key)
|
|
52
56
|
if isinstance(secret_key, Ed25519PrivateKey):
|
|
53
57
|
k._secret = secret_key
|
|
54
58
|
k._public_key = secret_key.public_key()
|
|
@@ -30,6 +30,7 @@ from jam.paseto.utils import (
|
|
|
30
30
|
base64url_decode,
|
|
31
31
|
base64url_encode,
|
|
32
32
|
)
|
|
33
|
+
from jam.utils.config_maker import __key_loader__
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class PASETOv3(BasePASETO):
|
|
@@ -56,6 +57,7 @@ class PASETOv3(BasePASETO):
|
|
|
56
57
|
|
|
57
58
|
if purpose == "local":
|
|
58
59
|
if isinstance(secret_key, str):
|
|
60
|
+
secret_key = __key_loader__(secret_key)
|
|
59
61
|
try:
|
|
60
62
|
raw = base64url_decode(secret_key.encode("utf-8"))
|
|
61
63
|
except Exception:
|
|
@@ -72,6 +74,8 @@ class PASETOv3(BasePASETO):
|
|
|
72
74
|
return inst
|
|
73
75
|
|
|
74
76
|
elif purpose == "public":
|
|
77
|
+
if isinstance(secret_key, str):
|
|
78
|
+
secret_key = __key_loader__(secret_key)
|
|
75
79
|
if hasattr(secret_key, "sign") and isinstance(
|
|
76
80
|
secret_key, ec.EllipticCurvePrivateKey
|
|
77
81
|
):
|
|
@@ -21,6 +21,7 @@ from jam.exceptions import (
|
|
|
21
21
|
)
|
|
22
22
|
from jam.paseto.__base__ import PASETO, BasePASETO
|
|
23
23
|
from jam.paseto.utils import __pae__, base64url_decode, base64url_encode
|
|
24
|
+
from jam.utils.config_maker import __key_loader__
|
|
24
25
|
from jam.utils.xchacha20poly1305 import (
|
|
25
26
|
xchacha20poly1305_decrypt,
|
|
26
27
|
xchacha20poly1305_encrypt,
|
|
@@ -53,6 +54,7 @@ class PASETOv4(BasePASETO):
|
|
|
53
54
|
|
|
54
55
|
if purpose == "local":
|
|
55
56
|
if isinstance(secret_key, str):
|
|
57
|
+
secret_key = __key_loader__(secret_key)
|
|
56
58
|
raw = base64url_decode(secret_key.encode("utf-8"))
|
|
57
59
|
else:
|
|
58
60
|
raw = secret_key
|
|
@@ -65,6 +67,8 @@ class PASETOv4(BasePASETO):
|
|
|
65
67
|
|
|
66
68
|
elif purpose == "public":
|
|
67
69
|
# Ed25519 objects
|
|
70
|
+
if isinstance(secret_key, str):
|
|
71
|
+
secret_key = __key_loader__(secret_key)
|
|
68
72
|
if isinstance(secret_key, Ed25519PrivateKey):
|
|
69
73
|
inst._secret = secret_key
|
|
70
74
|
inst._public_key = secret_key.public_key()
|
|
@@ -7,9 +7,11 @@ from uuid import uuid4
|
|
|
7
7
|
|
|
8
8
|
from cryptography.fernet import Fernet
|
|
9
9
|
|
|
10
|
-
from jam.
|
|
10
|
+
from jam.__base_encoder__ import BaseEncoder
|
|
11
|
+
from jam.encoders import JsonEncoder
|
|
11
12
|
from jam.exceptions import JamSessionEmptyAESKey
|
|
12
13
|
from jam.logger import BaseLogger
|
|
14
|
+
from jam.utils.config_maker import __key_loader__
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class BaseSessionModule(ABC):
|
|
@@ -76,6 +78,8 @@ class BaseSessionModule(ABC):
|
|
|
76
78
|
raise JamSessionEmptyAESKey
|
|
77
79
|
if is_session_crypt:
|
|
78
80
|
assert session_aes_secret is not None
|
|
81
|
+
if isinstance(session_aes_secret, str):
|
|
82
|
+
session_aes_secret = __key_loader__(session_aes_secret)
|
|
79
83
|
self._code_session_key = Fernet(session_aes_secret)
|
|
80
84
|
|
|
81
85
|
def __encode_session_id__(self, data: str) -> str:
|
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
from typing import Any, Union
|
|
6
6
|
import uuid
|
|
7
7
|
|
|
8
|
+
from jam.__deprecated__ import deprecated
|
|
8
9
|
from jam.aio import Jam as AioJam
|
|
9
10
|
from jam.instance import Jam
|
|
10
11
|
from jam.jose.utils import __base64url_decode__ as base64url_decode
|
|
@@ -66,6 +67,9 @@ class TestJam(Jam):
|
|
|
66
67
|
self._sessions: dict[str, dict[str, Any]] = {}
|
|
67
68
|
self._session_keys: dict[str, str] = {}
|
|
68
69
|
|
|
70
|
+
@deprecated(
|
|
71
|
+
"This method is deprecated; the JWT payload is generated automatically in accordance with the specification."
|
|
72
|
+
)
|
|
69
73
|
def jwt_make_payload(
|
|
70
74
|
self, exp: int | None, data: dict[str, Any]
|
|
71
75
|
) -> dict[str, Any]:
|
|
@@ -413,3 +413,18 @@ def __module_loader__(path: str) -> Callable:
|
|
|
413
413
|
module_path, class_name = path.rsplit(".", 1)
|
|
414
414
|
module = import_module(module_path)
|
|
415
415
|
return getattr(module, class_name)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def __key_loader__(key: str) -> str:
|
|
419
|
+
"""Loads a key from file, if `key` is a path to a file.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
key (str): Key to load. If it is a path to a file, the file will be loaded.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
str: Loaded key or original key if not a file path.
|
|
426
|
+
"""
|
|
427
|
+
if os.path.isfile(key):
|
|
428
|
+
with open(key) as f:
|
|
429
|
+
return f.read().strip()
|
|
430
|
+
return key
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jamlib
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.0rc2
|
|
4
4
|
Summary: Simple and universal library for authorization.
|
|
5
5
|
Author-email: Makridenko Adrian <adrianmakridenko@duck.com>, Ksenia Travnikova <kseniatravnikova@duck.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -77,7 +77,7 @@ from jam import Jam
|
|
|
77
77
|
|
|
78
78
|
jam = Jam(config="config.toml")
|
|
79
79
|
|
|
80
|
-
jwt = jam.
|
|
80
|
+
jwt = jam.jwt_encode(payload={"user": 1})
|
|
81
81
|
session_id = jam.session_create(session_key="username", data={"user": 1})
|
|
82
82
|
otp_code = jam.otp_code(secret="3DB7FOAOFBCI3WFDRE7EPF43CA")
|
|
83
83
|
```
|
|
@@ -85,7 +85,7 @@ otp_code = jam.otp_code(secret="3DB7FOAOFBCI3WFDRE7EPF43CA")
|
|
|
85
85
|
## Why Jam?
|
|
86
86
|
Jam is a library that provides the most popular AUTH* mechanisms right out of the box.
|
|
87
87
|
|
|
88
|
-
* [JOSE](https://jam.makridenko.ru/usage/jose/
|
|
88
|
+
* [JOSE](https://jam.makridenko.ru/usage/jose/)
|
|
89
89
|
* [PASETO](https://jam.makridenko.ru/usage/paseto/)
|
|
90
90
|
* [Server side sessions](https://jam.makridenko.ru/usage/sessions/)
|
|
91
91
|
* [OTP](https://jam.makridenko.ru/usage/otp/)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|