diff --git a/pyproject.toml b/pyproject.toml index 087051f..5323c56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ "python-multipart>=0.0.27", "jinja2>=3.1", "py-key-value-aio[disk]", + "authlib>=1.7.2", "pyjwt>=2.12.1", "base58>=2.1.1", ] diff --git a/src/authsome/auth/flows/base.py b/src/authsome/auth/flows/base.py index 3d04e45..d8b4a06 100644 --- a/src/authsome/auth/flows/base.py +++ b/src/authsome/auth/flows/base.py @@ -7,9 +7,9 @@ from datetime import timedelta from typing import TYPE_CHECKING, Any -import requests as http_client from loguru import logger +from authsome.auth.flows.oauth2_client import refresh_oauth_token, revoke_oauth_token from authsome.auth.models.connection import ConnectionRecord, ProviderClientRecord from authsome.auth.models.enums import ConnectionStatus from authsome.auth.models.provider import ProviderDefinition @@ -92,27 +92,23 @@ async def revoke( if not revocation_url: return - def _do_revoke(token: str, token_type: str) -> None: - payload = {"token": token} - if client_id: - payload["client_id"] = client_id - if client_secret: - payload["client_secret"] = client_secret - + def _do_revoke(token: str, token_type: str, token_type_hint: str) -> None: try: - http_client.post( - revocation_url, - data=payload, - timeout=15, + revoke_oauth_token( + provider=provider, + token=token, + token_type_hint=token_type_hint, + client_id=client_id, + client_secret=client_secret, ) except Exception as exc: logger.warning(f"{token_type.capitalize()} token revocation failed (continuing): {{}}", exc) if record.access_token: - _do_revoke(record.access_token, "access") + _do_revoke(record.access_token, "access", "access_token") if record.refresh_token: - _do_revoke(record.refresh_token, "refresh") + _do_revoke(record.refresh_token, "refresh", "refresh_token") def refresh( self, @@ -134,22 +130,12 @@ def refresh( if not client_id: raise RefreshFailedError("No client_id available for refresh", provider=provider.name) - payload: dict[str, str] = { - "grant_type": "refresh_token", - "refresh_token": record.refresh_token, - "client_id": client_id, - } - if client_secret: - payload["client_secret"] = client_secret - - resp = http_client.post( - provider.oauth.token_url, - data=payload, - headers={"Accept": "application/json"}, - timeout=30, + token = refresh_oauth_token( + provider=provider, + refresh_token=record.refresh_token, + client_id=client_id, + client_secret=client_secret, ) - resp.raise_for_status() - token = resp.json() now = utc_now() record.access_token = token["access_token"] diff --git a/src/authsome/auth/flows/dcr_pkce.py b/src/authsome/auth/flows/dcr_pkce.py index e785975..21f18a4 100644 --- a/src/authsome/auth/flows/dcr_pkce.py +++ b/src/authsome/auth/flows/dcr_pkce.py @@ -3,7 +3,6 @@ from __future__ import annotations import json -import secrets import urllib.parse from datetime import timedelta from typing import TYPE_CHECKING, Any @@ -11,10 +10,11 @@ import requests as http_client from authsome.auth.flows.base import AuthFlow, FlowResult +from authsome.auth.flows.oauth2_client import create_pkce_authorization, exchange_authorization_code from authsome.auth.models.connection import AccountInfo, ConnectionRecord, ProviderClientRecord from authsome.auth.models.enums import AuthType, ConnectionStatus from authsome.auth.models.provider import ProviderDefinition -from authsome.auth.utils import generate_pkce, resolve_callback_url +from authsome.auth.utils import resolve_callback_url from authsome.errors import AuthenticationFailedError, DiscoveryError from authsome.utils import utc_now @@ -51,21 +51,13 @@ async def begin( assert client_id is not None # guaranteed: either passed in or registered above - code_verifier, code_challenge = generate_pkce() - - state = secrets.token_urlsafe(32) - auth_params: dict[str, str] = { - "response_type": "code", - "client_id": client_id, - "redirect_uri": redirect_uri, - "state": state, - "code_challenge": code_challenge, - "code_challenge_method": "S256", - } - if effective_scopes: - auth_params["scope"] = " ".join(effective_scopes) - - auth_url = f"{provider.oauth.authorization_url}?{urllib.parse.urlencode(auth_params)}" + auth_url, state, code_verifier = create_pkce_authorization( + provider=provider, + client_id=client_id, + client_secret=client_secret, + redirect_uri=redirect_uri, + scopes=effective_scopes, + ) runtime_session.state = "waiting_for_user" runtime_session.payload["auth_url"] = auth_url @@ -99,10 +91,10 @@ async def resume( if not auth_code: raise AuthenticationFailedError("Authorization timed out or no code received", provider=provider.name) - returned_state = callback_data.get("state") + returned_state = callback_data.get("state", "") expected_state = runtime_session.payload.get("internal_state") - if returned_state != expected_state: - raise AuthenticationFailedError("OAuth state mismatch — potential CSRF attack", provider=provider.name) + if not expected_state: + raise AuthenticationFailedError("OAuth state missing from session", provider=provider.name) # If DCR registered a client, it's stored in payload if "internal_client_id" in runtime_session.payload: @@ -119,9 +111,11 @@ async def resume( redirect_uri = runtime_session.payload.get("callback_url", "") effective_scopes = json.loads(runtime_session.payload.get("internal_scopes", "[]")) - token_data = await self._exchange_code( + token_data = exchange_authorization_code( provider=provider, auth_code=auth_code, + expected_state=expected_state, + returned_state=returned_state, redirect_uri=redirect_uri, client_id=client_id, client_secret=client_secret, @@ -223,42 +217,3 @@ async def _register_client( if not client_id: raise AuthenticationFailedError("DCR response missing client_id", provider=provider.name) return client_id, reg_data.get("client_secret") - - @staticmethod - async def _exchange_code( - *, - provider: ProviderDefinition, - auth_code: str, - redirect_uri: str, - client_id: str, - client_secret: str | None, - code_verifier: str, - ) -> dict[str, Any]: - assert provider.oauth is not None - payload: dict[str, str] = { - "grant_type": "authorization_code", - "code": auth_code, - "redirect_uri": redirect_uri, - "client_id": client_id, - "code_verifier": code_verifier, - } - if client_secret: - payload["client_secret"] = client_secret - - try: - resp = http_client.post( - provider.oauth.token_url, data=payload, headers={"Accept": "application/json"}, timeout=30 - ) - resp.raise_for_status() - data = resp.json() - except http_client.RequestException as exc: - raise AuthenticationFailedError(f"Token exchange failed: {exc}", provider=provider.name) from exc - except json.JSONDecodeError as exc: - raise AuthenticationFailedError("Token response was not valid JSON", provider=provider.name) from exc - - if "access_token" not in data: - raise AuthenticationFailedError( - f"Token exchange error: {data.get('error', '')} — {data.get('error_description', 'Unknown error')}", - provider=provider.name, - ) - return data diff --git a/src/authsome/auth/flows/device_code.py b/src/authsome/auth/flows/device_code.py index 6b65744..5361f5e 100644 --- a/src/authsome/auth/flows/device_code.py +++ b/src/authsome/auth/flows/device_code.py @@ -8,9 +8,12 @@ from typing import TYPE_CHECKING, Any import requests +from authlib.integrations.base_client.errors import OAuthError +from authlib.oauth2 import OAuth2Error from loguru import logger from authsome.auth.flows.base import AuthFlow, FlowResult +from authsome.auth.flows.oauth2_client import fetch_device_token from authsome.auth.models.connection import AccountInfo, ConnectionRecord from authsome.auth.models.enums import AuthType, ConnectionStatus from authsome.auth.models.provider import ProviderDefinition @@ -183,57 +186,39 @@ async def poll_for_token( effective_expires_in = min(expires_in, 300) deadline = time.monotonic() + effective_expires_in - use_json = provider.oauth.device_token_request == "json" - while time.monotonic() < deadline: await asyncio.sleep(poll_interval) try: - if use_json: - resp = requests.post( - provider.oauth.token_url, - json={"device_code": device_code}, - headers={"Accept": "application/json", "Content-Type": "application/json"}, - timeout=30, - ) - else: - payload: dict[str, str] = { - "grant_type": "urn:ietf:params:oauth:grant-type:device_code", - "device_code": device_code, - } - if client_id: - payload["client_id"] = client_id - if client_secret: - payload["client_secret"] = client_secret - resp = requests.post( - provider.oauth.token_url, data=payload, headers={"Accept": "application/json"}, timeout=30 - ) + return fetch_device_token( + provider=provider, + device_code=device_code, + client_id=client_id, + client_secret=client_secret, + ) except requests.RequestException as exc: logger.warning("Token poll request failed: {}, retrying...", exc) continue - - try: - data = resp.json() except json.JSONDecodeError: logger.warning("Token poll response was not JSON, retrying...") continue - - if resp.status_code == 200 and "access_token" in data: - return data - - error = data.get("error", "") - if error == "authorization_pending": - continue - elif error == "slow_down": - poll_interval += 5 - elif error == "access_denied": - raise AuthenticationFailedError("User denied the authorization request", provider=provider.name) - elif error == "expired_token": - raise AuthenticationFailedError("Device code has expired. Please try again.", provider=provider.name) - else: + except (OAuthError, OAuth2Error) as exc: + error = getattr(exc, "error", "") + if error == "authorization_pending": + continue + if error == "slow_down": + poll_interval += 5 + continue + if error == "access_denied": + raise AuthenticationFailedError("User denied the authorization request", provider=provider.name) + if error == "expired_token": + raise AuthenticationFailedError( + "Device code has expired. Please try again.", provider=provider.name + ) + description = getattr(exc, "description", None) raise AuthenticationFailedError( - f"Token endpoint error: {data.get('error_description', error or 'Unknown error')}", + f"Token endpoint error: {description or error or 'Unknown error'}", provider=provider.name, - ) + ) from exc raise AuthenticationFailedError("Device authorization timed out.", provider=provider.name) diff --git a/src/authsome/auth/flows/oauth2_client.py b/src/authsome/auth/flows/oauth2_client.py new file mode 100644 index 0000000..d924d25 --- /dev/null +++ b/src/authsome/auth/flows/oauth2_client.py @@ -0,0 +1,186 @@ +"""Small Authlib-backed helpers for OAuth2 client flows.""" + +from __future__ import annotations + +import urllib.parse +from typing import Any + +import requests as http_client +from authlib.common.security import generate_token +from authlib.integrations.base_client.errors import OAuthError +from authlib.integrations.requests_client import OAuth2Session +from authlib.oauth2 import OAuth2Error + +from authsome.auth.models.provider import ProviderDefinition +from authsome.errors import AuthenticationFailedError, RefreshFailedError + +_PKCE_VERIFIER_LENGTH = 64 +_DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code" + + +def create_pkce_authorization( + *, + provider: ProviderDefinition, + client_id: str, + client_secret: str | None, + redirect_uri: str, + scopes: list[str], +) -> tuple[str, str, str]: + """Create an authorization URL and state using Authlib's PKCE support.""" + assert provider.oauth is not None + + session = OAuth2Session( + client_id=client_id, + client_secret=client_secret, + scope=" ".join(scopes) if scopes else None, + redirect_uri=redirect_uri, + code_challenge_method="S256", + token_endpoint_auth_method=_token_endpoint_auth_method(client_secret), + ) + code_verifier = generate_token(_PKCE_VERIFIER_LENGTH) + authorization_url, state = session.create_authorization_url( + provider.oauth.authorization_url, + code_verifier=code_verifier, + ) + return authorization_url, state, code_verifier + + +def exchange_authorization_code( + *, + provider: ProviderDefinition, + auth_code: str, + expected_state: str, + returned_state: str, + redirect_uri: str, + client_id: str, + client_secret: str | None, + code_verifier: str, +) -> dict[str, Any]: + """Exchange an authorization code for tokens using Authlib.""" + assert provider.oauth is not None + + session = OAuth2Session( + client_id=client_id, + client_secret=client_secret, + state=expected_state, + redirect_uri=redirect_uri, + token_endpoint_auth_method=_token_endpoint_auth_method(client_secret), + ) + authorization_response = _authorization_response_url( + redirect_uri=redirect_uri, + code=auth_code, + state=returned_state, + ) + + try: + token = session.fetch_token( + provider.oauth.token_url, + authorization_response=authorization_response, + code_verifier=code_verifier, + timeout=30, + ) + except (OAuthError, OAuth2Error, http_client.RequestException, ValueError) as exc: + raise AuthenticationFailedError(f"Token exchange failed: {exc}", provider=provider.name) from exc + + if "access_token" not in token: + error = token.get("error", "") + error_desc = token.get("error_description", "Unknown error") + raise AuthenticationFailedError(f"Token exchange error: {error} - {error_desc}", provider=provider.name) + + return dict(token) + + +def refresh_oauth_token( + *, + provider: ProviderDefinition, + refresh_token: str, + client_id: str, + client_secret: str | None, +) -> dict[str, Any]: + """Refresh an OAuth access token using Authlib.""" + assert provider.oauth is not None + + session = OAuth2Session( + client_id=client_id, + client_secret=client_secret, + token_endpoint_auth_method=_token_endpoint_auth_method(client_secret), + ) + try: + token = session.refresh_token( + provider.oauth.token_url, + refresh_token=refresh_token, + timeout=30, + ) + except (OAuthError, OAuth2Error, http_client.RequestException, ValueError) as exc: + raise RefreshFailedError(f"Token refresh failed: {exc}", provider=provider.name) from exc + + return dict(token) + + +def revoke_oauth_token( + *, + provider: ProviderDefinition, + token: str, + token_type_hint: str, + client_id: str | None, + client_secret: str | None, +) -> None: + """Revoke an OAuth token using Authlib.""" + assert provider.oauth is not None + assert provider.oauth.revocation_url is not None + + session = OAuth2Session( + client_id=client_id, + client_secret=client_secret, + revocation_endpoint_auth_method=_token_endpoint_auth_method(client_secret), + ) + session.revoke_token( + provider.oauth.revocation_url, + token=token, + token_type_hint=token_type_hint, + timeout=15, + ) + + +def fetch_device_token( + *, + provider: ProviderDefinition, + device_code: str, + client_id: str | None, + client_secret: str | None, +) -> dict[str, Any]: + """Poll a device-code token endpoint once using Authlib response handling.""" + assert provider.oauth is not None + + session = OAuth2Session( + client_id=client_id, + client_secret=client_secret, + token_endpoint_auth_method=_token_endpoint_auth_method(client_secret), + ) + + if provider.oauth.device_token_request == "json": + response = http_client.post( + provider.oauth.token_url, + json={"device_code": device_code}, + headers={"Accept": "application/json", "Content-Type": "application/json"}, + timeout=30, + ) + return dict(session.parse_response_token(response)) + + token = session.fetch_token( + provider.oauth.token_url, + grant_type=_DEVICE_CODE_GRANT, + device_code=device_code, + timeout=30, + ) + return dict(token) + + +def _token_endpoint_auth_method(client_secret: str | None) -> str: + return "client_secret_post" if client_secret else "none" + + +def _authorization_response_url(*, redirect_uri: str, code: str, state: str) -> str: + parsed = urllib.parse.urlsplit(redirect_uri) + query = urllib.parse.urlencode({"code": code, "state": state}) + return urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, parsed.path, query, parsed.fragment)) diff --git a/src/authsome/auth/flows/pkce.py b/src/authsome/auth/flows/pkce.py index db25a33..6e35909 100644 --- a/src/authsome/auth/flows/pkce.py +++ b/src/authsome/auth/flows/pkce.py @@ -3,18 +3,15 @@ from __future__ import annotations import json -import secrets -import urllib.parse from datetime import timedelta from typing import TYPE_CHECKING, Any -import requests as http_client - from authsome.auth.flows.base import AuthFlow, FlowResult +from authsome.auth.flows.oauth2_client import create_pkce_authorization, exchange_authorization_code from authsome.auth.models.connection import AccountInfo, ConnectionRecord from authsome.auth.models.enums import AuthType, ConnectionStatus from authsome.auth.models.provider import ProviderDefinition -from authsome.auth.utils import generate_pkce, resolve_callback_url +from authsome.auth.utils import resolve_callback_url from authsome.errors import AuthenticationFailedError from authsome.utils import utc_now @@ -44,23 +41,16 @@ async def begin( raise AuthenticationFailedError("PKCE flow requires a client_id.", provider=provider.name) effective_scopes = scopes or provider.oauth.scopes or [] - code_verifier, code_challenge = generate_pkce() redirect_uri = resolve_callback_url(runtime_session) - state = secrets.token_urlsafe(32) - auth_params: dict[str, str] = { - "response_type": "code", - "client_id": client_id, - "redirect_uri": redirect_uri, - "state": state, - "code_challenge": code_challenge, - "code_challenge_method": "S256", - } - if effective_scopes: - auth_params["scope"] = " ".join(effective_scopes) - - auth_url = f"{provider.oauth.authorization_url}?{urllib.parse.urlencode(auth_params)}" + auth_url, state, code_verifier = create_pkce_authorization( + provider=provider, + client_id=client_id, + client_secret=client_secret, + redirect_uri=redirect_uri, + scopes=effective_scopes, + ) runtime_session.state = "waiting_for_user" runtime_session.payload["auth_url"] = auth_url @@ -92,18 +82,20 @@ async def resume( if not auth_code: raise AuthenticationFailedError("Authorization timed out or no code received", provider=provider.name) - returned_state = callback_data.get("state") + returned_state = callback_data.get("state", "") expected_state = runtime_session.payload.get("internal_state") - if returned_state != expected_state: - raise AuthenticationFailedError("OAuth state mismatch — potential CSRF attack", provider=provider.name) + if not expected_state: + raise AuthenticationFailedError("OAuth state missing from session", provider=provider.name) code_verifier = runtime_session.payload.get("internal_code_verifier", "") redirect_uri = runtime_session.payload.get("callback_url", "") effective_scopes = json.loads(runtime_session.payload.get("internal_scopes", "[]")) - token_data = await self._exchange_code( + token_data = exchange_authorization_code( provider=provider, auth_code=auth_code, + expected_state=expected_state, + returned_state=returned_state, redirect_uri=redirect_uri, client_id=client_id, client_secret=client_secret, @@ -135,47 +127,3 @@ async def resume( metadata=metadata, ) ) - - @staticmethod - async def _exchange_code( - *, - provider: ProviderDefinition, - auth_code: str, - redirect_uri: str, - client_id: str, - client_secret: str | None, - code_verifier: str, - ) -> dict[str, Any]: - assert provider.oauth is not None - payload: dict[str, str] = { - "grant_type": "authorization_code", - "code": auth_code, - "redirect_uri": redirect_uri, - "client_id": client_id, - "code_verifier": code_verifier, - } - if client_secret: - payload["client_secret"] = client_secret - - try: - resp = http_client.post( - provider.oauth.token_url, - data=payload, - headers={"Accept": "application/json"}, - timeout=30, - ) - resp.raise_for_status() - except http_client.RequestException as exc: - raise AuthenticationFailedError(f"Token exchange failed: {exc}", provider=provider.name) from exc - - try: - data = resp.json() - except json.JSONDecodeError as exc: - raise AuthenticationFailedError("Token response was not valid JSON", provider=provider.name) from exc - - if "access_token" not in data: - error = data.get("error", "") - error_desc = data.get("error_description", "Unknown error") - raise AuthenticationFailedError(f"Token exchange error: {error} — {error_desc}", provider=provider.name) - - return data diff --git a/src/authsome/auth/utils.py b/src/authsome/auth/utils.py index 01173d6..9ba8246 100644 --- a/src/authsome/auth/utils.py +++ b/src/authsome/auth/utils.py @@ -2,10 +2,7 @@ from __future__ import annotations -import hashlib import re -import secrets -from base64 import urlsafe_b64encode from typing import TYPE_CHECKING from urllib.parse import urlsplit, urlunsplit @@ -17,14 +14,6 @@ _DEFAULT_CALLBACK_URL = build_callback_url(DEFAULT_SERVER_BASE_URL) -def generate_pkce() -> tuple[str, str]: - """Generate code verifier and challenge for PKCE.""" - code_verifier = secrets.token_urlsafe(64)[:128] - digest = hashlib.sha256(code_verifier.encode("ascii")).digest() - code_challenge = urlsafe_b64encode(digest).rstrip(b"=").decode("ascii") - return code_verifier, code_challenge - - def resolve_callback_url(runtime_session: AuthSession) -> str: """Resolve the callback URL.""" callback_override = runtime_session.payload.get("callback_url_override") diff --git a/tests/auth/test_oauth2_client.py b/tests/auth/test_oauth2_client.py new file mode 100644 index 0000000..0d2dd4f --- /dev/null +++ b/tests/auth/test_oauth2_client.py @@ -0,0 +1,248 @@ +"""Tests for Authlib-backed OAuth2 flow helpers.""" + +from __future__ import annotations + +import urllib.parse +from typing import Any + +import pytest +from requests import Session + +from authsome.auth.flows.oauth2_client import ( + create_pkce_authorization, + exchange_authorization_code, + fetch_device_token, + refresh_oauth_token, + revoke_oauth_token, +) +from authsome.auth.flows.pkce import PkceFlow +from authsome.auth.models.enums import AuthType, FlowType +from authsome.auth.models.provider import OAuthConfig, ProviderDefinition +from authsome.errors import AuthenticationFailedError + + +class _TokenResponse: + status_code = 200 + + def json(self) -> dict[str, Any]: + return { + "access_token": "access-token", + "refresh_token": "refresh-token", + "token_type": "Bearer", + "expires_in": 3600, + } + + +def _make_oauth_provider() -> ProviderDefinition: + return ProviderDefinition( + name="oauth-test", + display_name="OAuth Test", + auth_type=AuthType.OAUTH2, + flow=FlowType.PKCE, + oauth=OAuthConfig( + authorization_url="https://example.com/oauth/authorize", + token_url="https://example.com/oauth/token", + revocation_url="https://example.com/oauth/revoke", + scopes=["read"], + ), + ) + + +def test_create_pkce_authorization_uses_authlib_pkce_params() -> None: + provider = _make_oauth_provider() + + auth_url, state, code_verifier = create_pkce_authorization( + provider=provider, + client_id="client-id", + client_secret=None, + redirect_uri="http://127.0.0.1:7999/auth/callback", + scopes=["read", "write"], + ) + + parsed = urllib.parse.urlsplit(auth_url) + params = urllib.parse.parse_qs(parsed.query) + + assert parsed.scheme == "https" + assert parsed.netloc == "example.com" + assert parsed.path == "/oauth/authorize" + assert params["client_id"] == ["client-id"] + assert params["scope"] == ["read write"] + assert params["state"] == [state] + assert params["code_challenge_method"] == ["S256"] + assert params["code_challenge"][0] + assert code_verifier + + +def test_exchange_authorization_code_uses_authlib_token_request(monkeypatch: pytest.MonkeyPatch) -> None: + provider = _make_oauth_provider() + captured: dict[str, Any] = {} + + def fake_post(self: Session, url: str, data: dict[str, str], **kwargs: Any) -> _TokenResponse: + captured["url"] = url + captured["data"] = data + captured["headers"] = kwargs.get("headers") + captured["auth"] = kwargs.get("auth") + return _TokenResponse() + + monkeypatch.setattr(Session, "post", fake_post) + + token = exchange_authorization_code( + provider=provider, + auth_code="auth-code", + expected_state="expected-state", + returned_state="expected-state", + redirect_uri="http://127.0.0.1:7999/auth/callback", + client_id="client-id", + client_secret=None, + code_verifier="verifier", + ) + + assert token["access_token"] == "access-token" + assert captured["url"] == "https://example.com/oauth/token" + assert captured["data"]["grant_type"] == "authorization_code" + assert captured["data"]["code"] == "auth-code" + assert captured["data"]["code_verifier"] == "verifier" + assert captured["auth"] is not None + + +def test_exchange_authorization_code_rejects_state_mismatch() -> None: + provider = _make_oauth_provider() + + with pytest.raises(AuthenticationFailedError, match="Token exchange failed"): + exchange_authorization_code( + provider=provider, + auth_code="auth-code", + expected_state="expected-state", + returned_state="wrong-state", + redirect_uri="http://127.0.0.1:7999/auth/callback", + client_id="client-id", + client_secret=None, + code_verifier="verifier", + ) + + +def test_refresh_oauth_token_uses_authlib_refresh_request(monkeypatch: pytest.MonkeyPatch) -> None: + provider = _make_oauth_provider() + captured: dict[str, Any] = {} + + def fake_post(self: Session, url: str, data: dict[str, str], **kwargs: Any) -> _TokenResponse: + captured["url"] = url + captured["data"] = data + captured["auth"] = kwargs.get("auth") + return _TokenResponse() + + monkeypatch.setattr(Session, "post", fake_post) + + token = refresh_oauth_token( + provider=provider, + refresh_token="old-refresh-token", + client_id="client-id", + client_secret="client-secret", + ) + + assert token["access_token"] == "access-token" + assert captured["url"] == "https://example.com/oauth/token" + assert captured["data"]["grant_type"] == "refresh_token" + assert captured["data"]["refresh_token"] == "old-refresh-token" + assert captured["auth"] is not None + + +def test_revoke_oauth_token_uses_authlib_revocation_request(monkeypatch: pytest.MonkeyPatch) -> None: + provider = _make_oauth_provider() + captured: dict[str, Any] = {} + + def fake_post(self: Session, url: str, data: dict[str, str], **kwargs: Any) -> _TokenResponse: + captured["url"] = url + captured["data"] = data + captured["auth"] = kwargs.get("auth") + return _TokenResponse() + + monkeypatch.setattr(Session, "post", fake_post) + + revoke_oauth_token( + provider=provider, + token="access-token", + token_type_hint="access_token", + client_id="client-id", + client_secret="client-secret", + ) + + assert captured["url"] == "https://example.com/oauth/revoke" + assert captured["data"]["token"] == "access-token" + assert captured["data"]["token_type_hint"] == "access_token" + assert captured["auth"] is not None + + +def test_fetch_device_token_uses_authlib_form_request(monkeypatch: pytest.MonkeyPatch) -> None: + provider = _make_oauth_provider() + captured: dict[str, Any] = {} + + def fake_post(self: Session, url: str, data: dict[str, str], **kwargs: Any) -> _TokenResponse: + captured["url"] = url + captured["data"] = data + captured["auth"] = kwargs.get("auth") + return _TokenResponse() + + monkeypatch.setattr(Session, "post", fake_post) + + token = fetch_device_token( + provider=provider, + device_code="device-code", + client_id="client-id", + client_secret=None, + ) + + assert token["access_token"] == "access-token" + assert captured["url"] == "https://example.com/oauth/token" + assert captured["data"]["grant_type"] == "urn:ietf:params:oauth:grant-type:device_code" + assert captured["data"]["device_code"] == "device-code" + assert captured["auth"] is not None + + +def test_fetch_device_token_keeps_json_variant(monkeypatch: pytest.MonkeyPatch) -> None: + provider = _make_oauth_provider() + assert provider.oauth is not None + provider.oauth.device_token_request = "json" + captured: dict[str, Any] = {} + + def fake_post(url: str, json: dict[str, str], **kwargs: Any) -> _TokenResponse: + captured["url"] = url + captured["json"] = json + captured["headers"] = kwargs.get("headers") + return _TokenResponse() + + monkeypatch.setattr("authsome.auth.flows.oauth2_client.http_client.post", fake_post) + + token = fetch_device_token( + provider=provider, + device_code="device-code", + client_id="client-id", + client_secret="client-secret", + ) + + assert token["access_token"] == "access-token" + assert captured["url"] == "https://example.com/oauth/token" + assert captured["json"] == {"device_code": "device-code"} + assert captured["headers"] == {"Accept": "application/json", "Content-Type": "application/json"} + + +@pytest.mark.asyncio +async def test_pkce_flow_begin_stores_authlib_authorization_state() -> None: + from unittest.mock import Mock + + provider = _make_oauth_provider() + session = Mock() + session.payload = {} + + await PkceFlow().begin( + provider=provider, + identity="default", + connection_name="default", + runtime_session=session, + client_id="client-id", + ) + + assert session.state == "waiting_for_user" + assert session.payload["auth_url"].startswith("https://example.com/oauth/authorize?") + assert session.payload["internal_state"] + assert session.payload["internal_code_verifier"] diff --git a/uv.lock b/uv.lock index a7a61f3..c1c7fb8 100644 --- a/uv.lock +++ b/uv.lock @@ -103,11 +103,11 @@ wheels = [ [[package]] name = "asgiref" -version = "3.11.0" +version = "3.11.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, ] [[package]] @@ -119,11 +119,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "authlib" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "joserfc" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" }, +] + [[package]] name = "authsome" version = "0.3.0" source = { editable = "." } dependencies = [ + { name = "authlib" }, { name = "base58" }, { name = "click" }, { name = "cryptography" }, @@ -152,6 +166,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "authlib", specifier = ">=1.7.2" }, { name = "base58", specifier = ">=2.1.1" }, { name = "click", specifier = ">=8.0" }, { name = "cryptography", specifier = ">=41.0" }, @@ -499,55 +514,55 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.7" +version = "48.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, - { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, - { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, - { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, - { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, - { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, - { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, - { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, - { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, - { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, - { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, - { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, - { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, - { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, - { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, - { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, - { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, - { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, - { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, - { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, - { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, ] [[package]] @@ -662,11 +677,11 @@ wheels = [ [[package]] name = "idna" -version = "3.14" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/b1/efac073e0c297ecf2fb33c346989a529d4e19164f1759102dee5953ee17e/idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3", size = 198272, upload-time = "2026-05-10T20:32:15.935Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/3c/3f62dee257eb3d6b2c1ef2a09d36d9793c7111156a73b5654d2c2305e5ce/idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69", size = 72184, upload-time = "2026-05-10T20:32:14.295Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] @@ -741,6 +756,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "joserfc" +version = "1.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/dc/5f768c2e391e9afabe5d18e3221346deb5fb6338565f1ccc9e7c6d7befdd/joserfc-1.6.5.tar.gz", hash = "sha256:1482a7db78fb4602e44ed89e51b599d052e091288c7c532c5b694e20149dec48", size = 231881, upload-time = "2026-05-06T04:58:13.408Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/3b/ad1cb22e75c963b1f07c8a2329bf47227ce7e4361df5eb2fb101b2ce33ef/joserfc-1.6.5-py3-none-any.whl", hash = "sha256:e9878a0f8243fe7b95e11fdda81374ca9f7a689e302751579d3dfdeec559675e", size = 70464, upload-time = "2026-05-06T04:58:11.668Z" }, +] + [[package]] name = "kaitaistruct" version = "0.11" @@ -846,7 +873,7 @@ wheels = [ [[package]] name = "mitmproxy" -version = "12.2.2" +version = "12.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aioquic" }, @@ -877,7 +904,7 @@ dependencies = [ { name = "zstandard" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/14/32b4c3a9c3380e0cf1bed1976951e70510c06f1ba7c0dc46f9e69e3965ae/mitmproxy-12.2.2-py3-none-any.whl", hash = "sha256:7b111ba3b83b34b0d9b653044685db7c3f5fbc63b39b8f06439642da83910713", size = 1651385, upload-time = "2026-04-12T21:49:54.312Z" }, + { url = "https://files.pythonhosted.org/packages/e0/56/0df365a56624472c397b45788b64a5c10ecabf0de3858bf538b42875a268/mitmproxy-12.2.3-py3-none-any.whl", hash = "sha256:df75ccd15ccb39ab55ce9dd4130312270e8ba208eb927a7cbe50cb52678ec722", size = 1652028, upload-time = "2026-05-12T14:09:49.694Z" }, ] [[package]] @@ -1171,14 +1198,14 @@ wheels = [ [[package]] name = "pyopenssl" -version = "25.3.0" +version = "26.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, + { url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" }, ] [[package]] @@ -1261,7 +1288,7 @@ wheels = [ [[package]] name = "requests" -version = "2.33.1" +version = "2.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1269,9 +1296,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/b8/7a707d60fea4c49094e40262cc0e2ca6c768cca21587e34d3f705afec47e/requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a", size = 142436, upload-time = "2026-05-11T19:29:51.717Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60", size = 73021, upload-time = "2026-05-11T19:29:49.923Z" }, ] [[package]]