blockauth-sdk 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blockauth_sdk/__init__.py +27 -0
- blockauth_sdk/api_bridge.py +45 -0
- blockauth_sdk/crypto.py +9 -0
- blockauth_sdk/errors.py +9 -0
- blockauth_sdk/protocol/__init__.py +29 -0
- blockauth_sdk/protocol/provider.py +195 -0
- blockauth_sdk/protocol/roles.py +55 -0
- blockauth_sdk/protocol/rpc_client.py +9 -0
- blockauth_sdk/protocol/transport.py +92 -0
- blockauth_sdk/protocol/ws_client.py +220 -0
- blockauth_sdk-0.5.0.dist-info/METADATA +161 -0
- blockauth_sdk-0.5.0.dist-info/RECORD +15 -0
- blockauth_sdk-0.5.0.dist-info/WHEEL +5 -0
- blockauth_sdk-0.5.0.dist-info/licenses/LICENSE +15 -0
- blockauth_sdk-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .errors import BlockAuthError, NotConnectedError
|
|
2
|
+
from .protocol import BlockAuthProvider, RpcClient, WsClient
|
|
3
|
+
from .protocol.roles import (
|
|
4
|
+
get_user_role_names,
|
|
5
|
+
get_user_role_slugs,
|
|
6
|
+
get_user_roles,
|
|
7
|
+
has_any_role,
|
|
8
|
+
has_any_role_by_slug,
|
|
9
|
+
has_role,
|
|
10
|
+
has_role_by_slug,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BlockAuthProvider",
|
|
15
|
+
"RpcClient",
|
|
16
|
+
"WsClient",
|
|
17
|
+
"BlockAuthError",
|
|
18
|
+
"NotConnectedError",
|
|
19
|
+
"get_user_roles",
|
|
20
|
+
"get_user_role_names",
|
|
21
|
+
"get_user_role_slugs",
|
|
22
|
+
"has_role",
|
|
23
|
+
"has_role_by_slug",
|
|
24
|
+
"has_any_role",
|
|
25
|
+
"has_any_role_by_slug",
|
|
26
|
+
]
|
|
27
|
+
__version__ = "0.5.0"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import urllib.error
|
|
3
|
+
import urllib.parse
|
|
4
|
+
import urllib.request
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .errors import BlockAuthError
|
|
8
|
+
|
|
9
|
+
DEFAULT_WS_URL = "wss://wss.block-auth.io"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_ws_server_api_url(path: str, ws_url: str = "") -> str:
|
|
13
|
+
parsed = urllib.parse.urlsplit(ws_url or DEFAULT_WS_URL)
|
|
14
|
+
scheme = "https" if parsed.scheme == "wss" else "http"
|
|
15
|
+
return urllib.parse.urlunsplit((scheme, parsed.netloc, "/api" + path, "", ""))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_browser_token(api_key: str, api_secret_hash: str, ws_url: str = "", timeout: float = 10.0) -> str:
|
|
19
|
+
body = json.dumps({"apiKey": api_key, "apiSecret": api_secret_hash}).encode("utf-8")
|
|
20
|
+
request = urllib.request.Request(
|
|
21
|
+
build_ws_server_api_url("/browser-token", ws_url),
|
|
22
|
+
data=body,
|
|
23
|
+
headers={"Content-Type": "application/json"},
|
|
24
|
+
method="POST",
|
|
25
|
+
)
|
|
26
|
+
try:
|
|
27
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
28
|
+
payload = _read_json(response)
|
|
29
|
+
except urllib.error.HTTPError as err:
|
|
30
|
+
payload = _read_json(err)
|
|
31
|
+
except (urllib.error.URLError, OSError) as err:
|
|
32
|
+
raise BlockAuthError(f"Browser token request failed: {err}") from err
|
|
33
|
+
|
|
34
|
+
message = payload.get("message")
|
|
35
|
+
if not payload.get("success") or not isinstance(message, dict) or not message.get("accessToken"):
|
|
36
|
+
raise BlockAuthError(str(message or "Failed to create browser token"))
|
|
37
|
+
return message["accessToken"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _read_json(response: Any) -> dict:
|
|
41
|
+
try:
|
|
42
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
43
|
+
except (ValueError, OSError):
|
|
44
|
+
return {}
|
|
45
|
+
return data if isinstance(data, dict) else {}
|
blockauth_sdk/crypto.py
ADDED
blockauth_sdk/errors.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class BlockAuthError(Exception):
|
|
2
|
+
"""Base error for the BlockAuth SDK."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class NotConnectedError(BlockAuthError):
|
|
6
|
+
"""Raised when an operation needs an active WebSocket connection."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, message: str = "Not connected") -> None:
|
|
9
|
+
super().__init__(message)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Espejo de packages/core/src/protocol del SDK TypeScript (@block-auth.io/core).
|
|
2
|
+
from .provider import BlockAuthProvider
|
|
3
|
+
from .roles import (
|
|
4
|
+
get_user_role_names,
|
|
5
|
+
get_user_role_slugs,
|
|
6
|
+
get_user_roles,
|
|
7
|
+
has_any_role,
|
|
8
|
+
has_any_role_by_slug,
|
|
9
|
+
has_role,
|
|
10
|
+
has_role_by_slug,
|
|
11
|
+
)
|
|
12
|
+
from .rpc_client import RpcClient
|
|
13
|
+
from .transport import SocketIOTransport, Transport
|
|
14
|
+
from .ws_client import WsClient
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"BlockAuthProvider",
|
|
18
|
+
"RpcClient",
|
|
19
|
+
"WsClient",
|
|
20
|
+
"Transport",
|
|
21
|
+
"SocketIOTransport",
|
|
22
|
+
"get_user_roles",
|
|
23
|
+
"get_user_role_names",
|
|
24
|
+
"get_user_role_slugs",
|
|
25
|
+
"has_role",
|
|
26
|
+
"has_role_by_slug",
|
|
27
|
+
"has_any_role",
|
|
28
|
+
"has_any_role_by_slug",
|
|
29
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from ..api_bridge import create_browser_token
|
|
4
|
+
from ..crypto import sha256_hex
|
|
5
|
+
from ..errors import BlockAuthError, NotConnectedError
|
|
6
|
+
from .rpc_client import RpcClient
|
|
7
|
+
from .ws_client import WsClient
|
|
8
|
+
from .roles import (
|
|
9
|
+
get_user_role_names,
|
|
10
|
+
get_user_role_slugs,
|
|
11
|
+
has_any_role,
|
|
12
|
+
has_any_role_by_slug,
|
|
13
|
+
has_role,
|
|
14
|
+
has_role_by_slug,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BlockAuthProvider:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
api_key: str = "",
|
|
22
|
+
api_secret: str = "",
|
|
23
|
+
ws_url: str = "wss://wss.block-auth.io",
|
|
24
|
+
browser_token: str = "",
|
|
25
|
+
transport: Optional[Any] = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self._rpc = RpcClient()
|
|
28
|
+
self._wsc = WsClient(ws_url, True, transport=transport)
|
|
29
|
+
self._api_key = api_key
|
|
30
|
+
self._api_secret = api_secret
|
|
31
|
+
self._browser_token = browser_token or ""
|
|
32
|
+
self._ws_url = ws_url
|
|
33
|
+
|
|
34
|
+
# --- browser token ---
|
|
35
|
+
@property
|
|
36
|
+
def browser_token(self) -> str:
|
|
37
|
+
return self._browser_token
|
|
38
|
+
|
|
39
|
+
@browser_token.setter
|
|
40
|
+
def browser_token(self, token: str) -> None:
|
|
41
|
+
self._browser_token = token or ""
|
|
42
|
+
|
|
43
|
+
def create_browser_token(self) -> str:
|
|
44
|
+
if not self._api_secret:
|
|
45
|
+
raise BlockAuthError("API secret required to create a browser token")
|
|
46
|
+
return create_browser_token(self._api_key, sha256_hex(self._api_secret), self._ws_url)
|
|
47
|
+
|
|
48
|
+
# --- conexión ---
|
|
49
|
+
def connect(self, wait_ms: int = 1000, url: str = "", retries: int = 3) -> None:
|
|
50
|
+
self._wsc.connect(url, wait_ms, retries)
|
|
51
|
+
|
|
52
|
+
def disconnect(self) -> None:
|
|
53
|
+
self._wsc.close()
|
|
54
|
+
|
|
55
|
+
def is_connected(self) -> bool:
|
|
56
|
+
return self._wsc.is_connected()
|
|
57
|
+
|
|
58
|
+
def _require_connection(self) -> None:
|
|
59
|
+
if not self.is_connected():
|
|
60
|
+
raise NotConnectedError()
|
|
61
|
+
|
|
62
|
+
# --- métodos del protocolo ---
|
|
63
|
+
def signin(self) -> Any:
|
|
64
|
+
self._require_connection()
|
|
65
|
+
credentials = {"apiKey": self._api_key}
|
|
66
|
+
if self._browser_token:
|
|
67
|
+
credentials["browserToken"] = self._browser_token
|
|
68
|
+
elif self._api_secret:
|
|
69
|
+
credentials["apiSecret"] = sha256_hex(self._api_secret)
|
|
70
|
+
data = self._wsc.request("sdk:signin", "sdk:signin", credentials)
|
|
71
|
+
return {"success": data.get("success"), "message": data.get("message")}
|
|
72
|
+
|
|
73
|
+
def refresh(self, refresh_token: Optional[str] = None) -> Any:
|
|
74
|
+
self._require_connection()
|
|
75
|
+
token = refresh_token or self._wsc.refresh_token
|
|
76
|
+
if not token:
|
|
77
|
+
raise BlockAuthError("No refresh token provided")
|
|
78
|
+
data = self._wsc.request("sdk:refresh", "sdk:refresh", {"refreshToken": token})
|
|
79
|
+
if not data.get("success"):
|
|
80
|
+
raise BlockAuthError(str(data.get("message")))
|
|
81
|
+
return data.get("message")
|
|
82
|
+
|
|
83
|
+
def is_address_signed(self, address: str, token: Optional[str] = None) -> Any:
|
|
84
|
+
self._require_connection()
|
|
85
|
+
_token = token or self._wsc.access_token
|
|
86
|
+
data = self._wsc.request("sdk:sign", "sdk:sign", {"token": _token, "address": address})
|
|
87
|
+
return {"success": data.get("success"), "message": data.get("message")}
|
|
88
|
+
|
|
89
|
+
def app(self, token: Optional[str] = None) -> Any:
|
|
90
|
+
self._require_connection()
|
|
91
|
+
_token = token or self._wsc.access_token
|
|
92
|
+
data = self._wsc.request("sdk:app", "sdk:app", {"token": _token})
|
|
93
|
+
if not data.get("success"):
|
|
94
|
+
raise BlockAuthError(str(data.get("message")))
|
|
95
|
+
return data.get("message")
|
|
96
|
+
|
|
97
|
+
def user(self, address: str, token: Optional[str] = None) -> Any:
|
|
98
|
+
self._require_connection()
|
|
99
|
+
_token = token or self._wsc.access_token
|
|
100
|
+
data = self._wsc.request("sdk:user", "sdk:user", {"token": _token, "address": address})
|
|
101
|
+
return {"success": data.get("success"), "message": data.get("message")}
|
|
102
|
+
|
|
103
|
+
def users(self, token: Optional[str] = None, data: Optional[dict] = None) -> Any:
|
|
104
|
+
self._require_connection()
|
|
105
|
+
_token = token or self._wsc.access_token
|
|
106
|
+
payload = {"token": _token, **(data or {})}
|
|
107
|
+
resp = self._wsc.request("sdk:users", "sdk:users", payload)
|
|
108
|
+
if not resp.get("success"):
|
|
109
|
+
raise BlockAuthError(str(resp.get("message")))
|
|
110
|
+
return resp.get("message")
|
|
111
|
+
|
|
112
|
+
def roles(self, token: Optional[str] = None, data: Optional[dict] = None) -> Any:
|
|
113
|
+
self._require_connection()
|
|
114
|
+
_token = token or self._wsc.access_token
|
|
115
|
+
payload = {"token": _token, **(data or {})}
|
|
116
|
+
resp = self._wsc.request("sdk:roles", "sdk:roles", payload)
|
|
117
|
+
if not resp.get("success"):
|
|
118
|
+
raise BlockAuthError(str(resp.get("message")))
|
|
119
|
+
return resp.get("message")
|
|
120
|
+
|
|
121
|
+
def logs(self, token: Optional[str] = None, data: Optional[dict] = None) -> Any:
|
|
122
|
+
self._require_connection()
|
|
123
|
+
_token = token or self._wsc.access_token
|
|
124
|
+
payload = {"token": _token, **(data or {})}
|
|
125
|
+
resp = self._wsc.request("sdk:logs", "sdk:logs", payload)
|
|
126
|
+
if not resp.get("success"):
|
|
127
|
+
raise BlockAuthError(str(resp.get("message")))
|
|
128
|
+
return resp.get("message")
|
|
129
|
+
|
|
130
|
+
def kyc_issuer(self, address: str, data: Optional[dict] = None, token: Optional[str] = None) -> Any:
|
|
131
|
+
self._require_connection()
|
|
132
|
+
_token = token or self._wsc.access_token
|
|
133
|
+
payload = {"token": _token, "address": address, "details": {**(data or {})}}
|
|
134
|
+
resp = self._wsc.request("sdk:kyc:issuer", "sdk:kyc:issuer", payload)
|
|
135
|
+
if not resp.get("success"):
|
|
136
|
+
raise BlockAuthError(str(resp.get("message")))
|
|
137
|
+
return resp.get("message")
|
|
138
|
+
|
|
139
|
+
# --- getters ---
|
|
140
|
+
@property
|
|
141
|
+
def rpc(self) -> RpcClient:
|
|
142
|
+
return self._rpc
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def wsc(self) -> WsClient:
|
|
146
|
+
return self._wsc
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def socket(self) -> Any:
|
|
150
|
+
return self._wsc.ws
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def ui_config(self) -> dict:
|
|
154
|
+
return self._wsc.ui_config
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def app_users(self) -> list:
|
|
158
|
+
return self._wsc.users
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def app_roles(self) -> list:
|
|
162
|
+
return self._wsc.roles
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def app_logs(self) -> list:
|
|
166
|
+
return self._wsc.logs
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def app_kyc_issuer_by_address(self) -> dict:
|
|
170
|
+
return self._wsc.kyc_issuer_by_address
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def current_user(self) -> dict:
|
|
174
|
+
return self._wsc.user
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def role_names(self) -> list:
|
|
178
|
+
return get_user_role_names(self.current_user)
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def role_slugs(self) -> list:
|
|
182
|
+
return get_user_role_slugs(self.current_user)
|
|
183
|
+
|
|
184
|
+
# --- helpers de roles ---
|
|
185
|
+
def has_role(self, role_name: str) -> bool:
|
|
186
|
+
return has_role(self.current_user, role_name)
|
|
187
|
+
|
|
188
|
+
def has_role_by_slug(self, role_slug: str) -> bool:
|
|
189
|
+
return has_role_by_slug(self.current_user, role_slug)
|
|
190
|
+
|
|
191
|
+
def has_any_role(self, role_names: list) -> bool:
|
|
192
|
+
return has_any_role(self.current_user, role_names)
|
|
193
|
+
|
|
194
|
+
def has_any_role_by_slug(self, role_slugs: list) -> bool:
|
|
195
|
+
return has_any_role_by_slug(self.current_user, role_slugs)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _normalize(value: Any) -> str:
|
|
5
|
+
return str(value if value is not None else "").strip().lower()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _is_valid_role(role: Any) -> bool:
|
|
9
|
+
return (
|
|
10
|
+
isinstance(role, dict)
|
|
11
|
+
and isinstance(role.get("name"), str)
|
|
12
|
+
and role["name"].strip() != ""
|
|
13
|
+
and isinstance(role.get("slug"), str)
|
|
14
|
+
and role["slug"].strip() != ""
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_user_roles(user: Optional[dict]) -> list:
|
|
19
|
+
if not user or not isinstance(user.get("roles"), list):
|
|
20
|
+
return []
|
|
21
|
+
return [role for role in user["roles"] if _is_valid_role(role)]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_user_role_names(user: Optional[dict]) -> list:
|
|
25
|
+
return [role["name"] for role in get_user_roles(user)]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_user_role_slugs(user: Optional[dict]) -> list:
|
|
29
|
+
return [role["slug"] for role in get_user_roles(user) if role["slug"]]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def has_role(user: Optional[dict], role_name: str) -> bool:
|
|
33
|
+
target = _normalize(role_name)
|
|
34
|
+
if not target:
|
|
35
|
+
return False
|
|
36
|
+
return any(_normalize(name) == target for name in get_user_role_names(user))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def has_role_by_slug(user: Optional[dict], role_slug: str) -> bool:
|
|
40
|
+
target = _normalize(role_slug)
|
|
41
|
+
if not target:
|
|
42
|
+
return False
|
|
43
|
+
return any(_normalize(slug) == target for slug in get_user_role_slugs(user))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def has_any_role(user: Optional[dict], role_names: list) -> bool:
|
|
47
|
+
if not isinstance(role_names, list) or len(role_names) == 0:
|
|
48
|
+
return False
|
|
49
|
+
return any(has_role(user, name) for name in role_names)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def has_any_role_by_slug(user: Optional[dict], role_slugs: list) -> bool:
|
|
53
|
+
if not isinstance(role_slugs, list) or len(role_slugs) == 0:
|
|
54
|
+
return False
|
|
55
|
+
return any(has_role_by_slug(user, slug) for slug in role_slugs)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from typing import Protocol, runtime_checkable
|
|
5
|
+
except ImportError: # pragma: no cover
|
|
6
|
+
from typing_extensions import Protocol, runtime_checkable # type: ignore
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@runtime_checkable
|
|
10
|
+
class Transport(Protocol):
|
|
11
|
+
@property
|
|
12
|
+
def connected(self) -> bool: ...
|
|
13
|
+
def connect(self, url: str, config: dict, wait_ms: int = 5000) -> None: ...
|
|
14
|
+
def on(self, event: str, handler: Callable) -> None: ...
|
|
15
|
+
def once(self, event: str, handler: Callable) -> None: ...
|
|
16
|
+
def off(self, event: str, handler: Optional[Callable] = None) -> None: ...
|
|
17
|
+
def emit(self, event: str, data: Any) -> None: ...
|
|
18
|
+
def has_listeners(self, event: str) -> bool: ...
|
|
19
|
+
def disconnect(self) -> None: ...
|
|
20
|
+
def close(self) -> None: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SocketIOTransport:
|
|
24
|
+
"""Transporte real sobre python-socketio (Socket.IO v4).
|
|
25
|
+
|
|
26
|
+
python-socketio no ofrece `once` ni `has_listeners`, así que los
|
|
27
|
+
implementamos con un registro propio de listeners.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
import socketio # import perezoso: solo si se usa el transporte real
|
|
32
|
+
|
|
33
|
+
self._sio = socketio.Client(reconnection=True)
|
|
34
|
+
self._listeners: Dict[str, List[Callable]] = {}
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def connected(self) -> bool:
|
|
38
|
+
return bool(getattr(self._sio, "connected", False))
|
|
39
|
+
|
|
40
|
+
def connect(self, url: str, config: dict, wait_ms: int = 5000) -> None:
|
|
41
|
+
transports = config.get("transports", ["websocket"])
|
|
42
|
+
# wait_ms (ms) -> wait_timeout de socketio (segundos, mínimo 1).
|
|
43
|
+
self._sio.connect(
|
|
44
|
+
url,
|
|
45
|
+
transports=transports,
|
|
46
|
+
wait=True,
|
|
47
|
+
wait_timeout=max(1, wait_ms // 1000),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def _register(self, event: str) -> None:
|
|
51
|
+
# Registra un único dispatcher en socketio por evento; reenvía a
|
|
52
|
+
# nuestros listeners (permite múltiples handlers + once).
|
|
53
|
+
if event in self._listeners:
|
|
54
|
+
return
|
|
55
|
+
self._listeners[event] = []
|
|
56
|
+
|
|
57
|
+
def dispatcher(data=None):
|
|
58
|
+
for handler in list(self._listeners.get(event, [])):
|
|
59
|
+
handler(data)
|
|
60
|
+
|
|
61
|
+
self._sio.on(event, dispatcher)
|
|
62
|
+
|
|
63
|
+
def on(self, event: str, handler: Callable) -> None:
|
|
64
|
+
self._register(event)
|
|
65
|
+
self._listeners[event].append(handler)
|
|
66
|
+
|
|
67
|
+
def once(self, event: str, handler: Callable) -> None:
|
|
68
|
+
def wrapper(data=None):
|
|
69
|
+
self.off(event, wrapper)
|
|
70
|
+
handler(data)
|
|
71
|
+
|
|
72
|
+
self.on(event, wrapper)
|
|
73
|
+
|
|
74
|
+
def off(self, event: str, handler: Optional[Callable] = None) -> None:
|
|
75
|
+
if event not in self._listeners:
|
|
76
|
+
return
|
|
77
|
+
if handler is None:
|
|
78
|
+
self._listeners[event] = []
|
|
79
|
+
elif handler in self._listeners[event]:
|
|
80
|
+
self._listeners[event].remove(handler)
|
|
81
|
+
|
|
82
|
+
def emit(self, event: str, data: Any) -> None:
|
|
83
|
+
self._sio.emit(event, data)
|
|
84
|
+
|
|
85
|
+
def has_listeners(self, event: str) -> bool:
|
|
86
|
+
return len(self._listeners.get(event, [])) > 0
|
|
87
|
+
|
|
88
|
+
def disconnect(self) -> None:
|
|
89
|
+
self._sio.disconnect()
|
|
90
|
+
|
|
91
|
+
def close(self) -> None:
|
|
92
|
+
self._sio.disconnect()
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from typing import Any, Callable, List, Optional
|
|
3
|
+
|
|
4
|
+
from ..errors import BlockAuthError
|
|
5
|
+
from .transport import SocketIOTransport, Transport
|
|
6
|
+
|
|
7
|
+
DEFAULT_WS_URL = "wss://wss.block-auth.io/"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WsClient:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
url: str = "",
|
|
14
|
+
reconnect: bool = False,
|
|
15
|
+
transport: Optional[Transport] = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
self._ws_url = url or DEFAULT_WS_URL
|
|
18
|
+
self._ws_config = {
|
|
19
|
+
"transports": ["websocket"],
|
|
20
|
+
"reconnection": reconnect,
|
|
21
|
+
"reconnectionDelay": 100,
|
|
22
|
+
"reconnectionDelayMax": 3000,
|
|
23
|
+
"reconnectionAttempts": 30,
|
|
24
|
+
}
|
|
25
|
+
self._transport: Transport = transport or SocketIOTransport()
|
|
26
|
+
self._handlers_wired = False
|
|
27
|
+
self._access_token = ""
|
|
28
|
+
self._refresh_token = ""
|
|
29
|
+
self._ui_config: dict = {}
|
|
30
|
+
self._user: dict = {}
|
|
31
|
+
self._users: List[Any] = []
|
|
32
|
+
self._roles: List[Any] = []
|
|
33
|
+
self._logs: List[Any] = []
|
|
34
|
+
self._kyc_issuer_by_address: dict = {}
|
|
35
|
+
|
|
36
|
+
# --- conexión ---
|
|
37
|
+
def is_connected(self) -> bool:
|
|
38
|
+
return bool(self._transport) and bool(self._transport.connected)
|
|
39
|
+
|
|
40
|
+
def connect(self, url: str = "", wait_ms: int = 5000, retries: int = 3) -> None:
|
|
41
|
+
target = url or self._ws_url
|
|
42
|
+
attempts = max(1, retries)
|
|
43
|
+
for attempt in range(attempts):
|
|
44
|
+
if self._transport.connected:
|
|
45
|
+
break
|
|
46
|
+
try:
|
|
47
|
+
self._transport.connect(target, self._ws_config, wait_ms)
|
|
48
|
+
except Exception:
|
|
49
|
+
# Reintenta hasta agotar `retries`; en el último intento propaga.
|
|
50
|
+
if attempt >= attempts - 1:
|
|
51
|
+
raise
|
|
52
|
+
continue
|
|
53
|
+
if self._transport.connected:
|
|
54
|
+
break
|
|
55
|
+
self._wire_handlers()
|
|
56
|
+
|
|
57
|
+
def _wire_handlers(self) -> None:
|
|
58
|
+
if self._handlers_wired:
|
|
59
|
+
return
|
|
60
|
+
self._handlers_wired = True
|
|
61
|
+
handlers = {
|
|
62
|
+
"sdk:signin": self.on_sdk_signin,
|
|
63
|
+
"sdk:refresh": self.on_sdk_refresh,
|
|
64
|
+
"sdk:sign": self.on_sdk_sign,
|
|
65
|
+
"sdk:app": self.on_sdk_app,
|
|
66
|
+
"sdk:user": self.on_sdk_user,
|
|
67
|
+
"sdk:users": self.on_sdk_users,
|
|
68
|
+
"sdk:roles": self.on_sdk_roles,
|
|
69
|
+
"sdk:logs": self.on_sdk_logs,
|
|
70
|
+
"sdk:kyc:issuer": self.on_sdk_kyc_issuer,
|
|
71
|
+
}
|
|
72
|
+
for event, handler in handlers.items():
|
|
73
|
+
self._transport.on(event, handler)
|
|
74
|
+
|
|
75
|
+
def on(self, event: str, handler: Callable) -> None:
|
|
76
|
+
self._transport.on(event, handler)
|
|
77
|
+
|
|
78
|
+
def off(self, event: str, handler: Callable) -> None:
|
|
79
|
+
self._transport.off(event, handler)
|
|
80
|
+
|
|
81
|
+
def emit(self, event: str, data: Any) -> None:
|
|
82
|
+
self._transport.emit(event, data)
|
|
83
|
+
|
|
84
|
+
def request(
|
|
85
|
+
self,
|
|
86
|
+
emit_event: str,
|
|
87
|
+
response_event: str,
|
|
88
|
+
payload: dict,
|
|
89
|
+
timeout: float = 10.0,
|
|
90
|
+
) -> Any:
|
|
91
|
+
holder: dict = {}
|
|
92
|
+
done = threading.Event()
|
|
93
|
+
|
|
94
|
+
# Se registra con `on` (no `once`) para que el callable registrado sea
|
|
95
|
+
# exactamente `handler`: así `off(handler)` lo elimina de verdad tanto en
|
|
96
|
+
# éxito como en timeout. (El `once` del transporte envuelve el handler en
|
|
97
|
+
# un wrapper interno que `off(handler)` no encontraría → fuga de listener.)
|
|
98
|
+
def handler(data=None):
|
|
99
|
+
holder["data"] = data
|
|
100
|
+
self._transport.off(response_event, handler)
|
|
101
|
+
done.set()
|
|
102
|
+
|
|
103
|
+
self._transport.on(response_event, handler)
|
|
104
|
+
self._transport.emit(emit_event, payload)
|
|
105
|
+
if not done.wait(timeout):
|
|
106
|
+
self._transport.off(response_event, handler)
|
|
107
|
+
raise BlockAuthError(f"Timeout waiting for {response_event}")
|
|
108
|
+
return holder.get("data")
|
|
109
|
+
|
|
110
|
+
def close(self) -> None:
|
|
111
|
+
if self.is_connected():
|
|
112
|
+
self._transport.close()
|
|
113
|
+
self._access_token = ""
|
|
114
|
+
self._refresh_token = ""
|
|
115
|
+
|
|
116
|
+
# --- handlers de estado (solo mutan estado; nunca lanzan) ---
|
|
117
|
+
def on_sdk_signin(self, data: Any) -> None:
|
|
118
|
+
if not isinstance(data, dict) or not data.get("success"):
|
|
119
|
+
return
|
|
120
|
+
message = data.get("message") or {}
|
|
121
|
+
self._access_token = message.get("accessToken", self._access_token)
|
|
122
|
+
self._refresh_token = message.get("refreshToken", self._refresh_token)
|
|
123
|
+
|
|
124
|
+
def on_sdk_refresh(self, data: Any) -> None:
|
|
125
|
+
if not isinstance(data, dict) or not data.get("success"):
|
|
126
|
+
return
|
|
127
|
+
message = data.get("message") or {}
|
|
128
|
+
access = message.get("accessToken")
|
|
129
|
+
refresh = message.get("refreshToken")
|
|
130
|
+
# Guardia anti-replay (fiel al TS): solo se actualiza si AMBOS tokens
|
|
131
|
+
# cambian; si cualquiera coincide con el actual, se ignora (no-op).
|
|
132
|
+
if access and refresh and access != self._access_token and refresh != self._refresh_token:
|
|
133
|
+
self._access_token = access
|
|
134
|
+
self._refresh_token = refresh
|
|
135
|
+
|
|
136
|
+
def on_sdk_sign(self, data: Any) -> None:
|
|
137
|
+
if isinstance(data, dict) and not data.get("success") and data.get("message") == "Bad sign":
|
|
138
|
+
self._transport.emit("sdk:refresh", {"refreshToken": self._refresh_token})
|
|
139
|
+
|
|
140
|
+
def on_sdk_app(self, data: Any) -> None:
|
|
141
|
+
if not isinstance(data, dict) or not data.get("success"):
|
|
142
|
+
return
|
|
143
|
+
message = data.get("message") or {}
|
|
144
|
+
config = message.get("config") or {}
|
|
145
|
+
self._ui_config = {
|
|
146
|
+
"domain": message.get("domain"),
|
|
147
|
+
"signinCallbackUrl": message.get("signinCallbackUrl"),
|
|
148
|
+
**config,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
def on_sdk_user(self, data: Any) -> None:
|
|
152
|
+
if not isinstance(data, dict) or not data.get("success") or not data.get("message"):
|
|
153
|
+
self._user = {}
|
|
154
|
+
return
|
|
155
|
+
message = data["message"]
|
|
156
|
+
items = message.get("items") if isinstance(message, dict) else None
|
|
157
|
+
# `items` solo se usa si es una lista no vacía (un items no-lista, p.ej. dict,
|
|
158
|
+
# haría fallar items[0]); si no, se usa el propio message.
|
|
159
|
+
user = items[0] if isinstance(items, list) and items else message
|
|
160
|
+
# Si el usuario resuelto no es un dict (mensaje malformado), no se propaga
|
|
161
|
+
# ni se lanza: se resetea a {} (los handlers nunca lanzan por control de flujo).
|
|
162
|
+
if not isinstance(user, dict):
|
|
163
|
+
self._user = {}
|
|
164
|
+
return
|
|
165
|
+
roles = user.get("roles")
|
|
166
|
+
self._user = {**user, "roles": roles if isinstance(roles, list) else []}
|
|
167
|
+
|
|
168
|
+
def on_sdk_users(self, data: Any) -> None:
|
|
169
|
+
if isinstance(data, dict) and data.get("success"):
|
|
170
|
+
self._users = list((data.get("message") or {}).get("items", []))
|
|
171
|
+
|
|
172
|
+
def on_sdk_roles(self, data: Any) -> None:
|
|
173
|
+
if isinstance(data, dict) and data.get("success"):
|
|
174
|
+
self._roles = list((data.get("message") or {}).get("items", []))
|
|
175
|
+
|
|
176
|
+
def on_sdk_logs(self, data: Any) -> None:
|
|
177
|
+
if isinstance(data, dict) and data.get("success"):
|
|
178
|
+
self._logs = list((data.get("message") or {}).get("items", []))
|
|
179
|
+
|
|
180
|
+
def on_sdk_kyc_issuer(self, data: Any) -> None:
|
|
181
|
+
if isinstance(data, dict) and data.get("success"):
|
|
182
|
+
items = (data.get("message") or {}).get("items", [])
|
|
183
|
+
self._kyc_issuer_by_address = dict(items[0]) if items else {}
|
|
184
|
+
|
|
185
|
+
# --- propiedades ---
|
|
186
|
+
@property
|
|
187
|
+
def ws(self) -> Transport:
|
|
188
|
+
return self._transport
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def access_token(self) -> str:
|
|
192
|
+
return self._access_token
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def refresh_token(self) -> str:
|
|
196
|
+
return self._refresh_token
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def ui_config(self) -> dict:
|
|
200
|
+
return self._ui_config
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def user(self) -> dict:
|
|
204
|
+
return self._user
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def users(self) -> list:
|
|
208
|
+
return self._users
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def roles(self) -> list:
|
|
212
|
+
return self._roles
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def logs(self) -> list:
|
|
216
|
+
return self._logs
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def kyc_issuer_by_address(self) -> dict:
|
|
220
|
+
return self._kyc_issuer_by_address
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blockauth-sdk
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: BlockAuth Auth SDK (Python) — blockchain-based authentication client.
|
|
5
|
+
Author: Block-Auth.io Team
|
|
6
|
+
License-Expression: ISC
|
|
7
|
+
Project-URL: Homepage, https://block-auth.io
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/blockauthio/blockauth-sdk-python
|
|
9
|
+
Project-URL: Issues, https://gitlab.com/blockauthio/blockauth-sdk-python/-/issues
|
|
10
|
+
Keywords: blockauth,authentication,blockchain,web3,socket.io,sdk
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: python-socketio[client]<6,>=5.11
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# BlockAuth SDK (Python)
|
|
27
|
+
|
|
28
|
+
Cliente headless del servicio de autenticación BlockAuth (Socket.IO v4).
|
|
29
|
+
Puerto de referencia de la capa de protocolo de `@block-auth.io/core`
|
|
30
|
+
(monorepo `blockauth-sdk`, patrón *headless core + thin per-framework adapters*).
|
|
31
|
+
|
|
32
|
+
## Arquitectura
|
|
33
|
+
|
|
34
|
+
Este puerto replica la capa `protocol/` del core TypeScript; la reactividad y
|
|
35
|
+
la UI de los bindings (React/Vue/Angular) quedan fuera de alcance.
|
|
36
|
+
|
|
37
|
+
| Este repo | Upstream (`blockauth-sdk`) |
|
|
38
|
+
| :--- | :--- |
|
|
39
|
+
| `blockauth_sdk/protocol/provider.py` | `packages/core/src/protocol/BlockAuthProvider.ts` |
|
|
40
|
+
| `blockauth_sdk/protocol/ws_client.py` | `packages/core/src/protocol/WsClient.ts` |
|
|
41
|
+
| `blockauth_sdk/protocol/rpc_client.py` | `packages/core/src/protocol/RpcClient.ts` |
|
|
42
|
+
| `blockauth_sdk/protocol/roles.py` | `packages/core/src/protocol/roles.ts` |
|
|
43
|
+
| `blockauth_sdk/protocol/transport.py` | — (abstracción propia del puerto; upstream usa socket.io directo) |
|
|
44
|
+
| `blockauth_sdk/api_bridge.py` | `packages/core/src/utils/apiBridge.ts` (solo la emisión del browser token) |
|
|
45
|
+
| `blockauth_sdk/crypto.py`, `blockauth_sdk/errors.py` | — (infra propia del puerto) |
|
|
46
|
+
|
|
47
|
+
La API pública se re-exporta desde la raíz del paquete (`from blockauth_sdk import
|
|
48
|
+
BlockAuthProvider`), igual que hace el `index.ts` del core.
|
|
49
|
+
|
|
50
|
+
## Instalación
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install blockauth-sdk
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Uso
|
|
57
|
+
|
|
58
|
+
Un backend se autentica con su `apiKey` y su `apiSecret`. El `apiSecret` solo
|
|
59
|
+
vive en el backend.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from blockauth_sdk import BlockAuthProvider
|
|
63
|
+
|
|
64
|
+
provider = BlockAuthProvider(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET")
|
|
65
|
+
provider.connect()
|
|
66
|
+
|
|
67
|
+
result = provider.signin()
|
|
68
|
+
if result["success"]:
|
|
69
|
+
profile = provider.user("0xUserAddress")
|
|
70
|
+
print(provider.role_names)
|
|
71
|
+
|
|
72
|
+
provider.disconnect()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
No autentiques un backend con un browser token: el ws-server lo rechaza (exige
|
|
76
|
+
un `Origin` de confianza y su scope no da acceso a `user`, `users`, `roles` ni
|
|
77
|
+
`logs`).
|
|
78
|
+
|
|
79
|
+
### Login por redirección
|
|
80
|
+
|
|
81
|
+
Para que tus usuarios inicien sesión con BlockAuth, tu backend los manda al
|
|
82
|
+
login alojado y recibe de vuelta un código de un solo uso, que canjea por la
|
|
83
|
+
dirección y los roles del usuario. La dirección la da el ws-server, nunca el
|
|
84
|
+
navegador.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import json
|
|
88
|
+
import secrets
|
|
89
|
+
import urllib.error
|
|
90
|
+
import urllib.request
|
|
91
|
+
|
|
92
|
+
from blockauth_sdk.api_bridge import build_ws_server_api_url
|
|
93
|
+
from blockauth_sdk.crypto import sha256_hex
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def post(path, body):
|
|
97
|
+
request = urllib.request.Request(
|
|
98
|
+
build_ws_server_api_url(path),
|
|
99
|
+
data=json.dumps(body).encode("utf-8"),
|
|
100
|
+
headers={"Content-Type": "application/json"},
|
|
101
|
+
)
|
|
102
|
+
try:
|
|
103
|
+
with urllib.request.urlopen(request) as response:
|
|
104
|
+
data = json.load(response)
|
|
105
|
+
except urllib.error.HTTPError as err: # los errores llegan como 400 con el mismo JSON
|
|
106
|
+
data = json.load(err)
|
|
107
|
+
if not data.get("success"):
|
|
108
|
+
raise RuntimeError(data.get("message"))
|
|
109
|
+
return data["message"]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
credentials = {"apiKey": "YOUR_API_KEY", "apiSecret": sha256_hex("YOUR_API_SECRET")}
|
|
113
|
+
redirect_uri = "https://tuapp.com/callback"
|
|
114
|
+
|
|
115
|
+
# 1. Pide el enlace de login y redirige al usuario a él.
|
|
116
|
+
state = secrets.token_hex(16) # guárdalo en la sesión
|
|
117
|
+
link = post("/auth/authorize", {**credentials, "redirectUri": redirect_uri, "state": state})["url"]
|
|
118
|
+
|
|
119
|
+
# 2. El usuario vuelve a redirect_uri?code=...&state=... Comprueba que el state es el de la sesión.
|
|
120
|
+
|
|
121
|
+
# 3. Canjea el código (válido 60 s, un solo uso).
|
|
122
|
+
user = post("/auth/token", {**credentials, "code": code, "redirectUri": redirect_uri})
|
|
123
|
+
address = user["address"] # también: roles, signedExpiredAt
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- El `redirect_uri` tiene que ser `https` en el dominio (o el dominio de
|
|
127
|
+
desarrollo) de la app; `http` solo en `localhost`.
|
|
128
|
+
- Para cerrar la sesión del usuario en tu app: `post("/auth/signout",
|
|
129
|
+
{**credentials, "address": address})`.
|
|
130
|
+
|
|
131
|
+
### Browser token para tu frontend
|
|
132
|
+
|
|
133
|
+
Si tu frontend usa `@block-auth.io/core`, el backend le emite un browser token
|
|
134
|
+
para que el `apiSecret` no salga nunca del backend:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
provider = BlockAuthProvider(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET")
|
|
138
|
+
browser_token = provider.create_browser_token()
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
El frontend lo usa como `browserToken` en `@block-auth.io/core`.
|
|
142
|
+
|
|
143
|
+
## API
|
|
144
|
+
|
|
145
|
+
`BlockAuthProvider`: `connect`, `disconnect`, `is_connected`, `signin`, `create_browser_token`, `refresh`,
|
|
146
|
+
`is_address_signed`, `app`, `user`, `users`, `roles`, `logs`, `kyc_issuer`,
|
|
147
|
+
helpers de roles (`has_role`, `has_role_by_slug`, `has_any_role`,
|
|
148
|
+
`has_any_role_by_slug`) y getters (`current_user`, `role_names`, `ui_config`, …).
|
|
149
|
+
|
|
150
|
+
## Tests
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pip install -e ".[dev]"
|
|
154
|
+
pytest
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Smoke test contra un servidor real
|
|
158
|
+
|
|
159
|
+
Los tests usan un transporte simulado. Para probar el SDK contra el ws-server
|
|
160
|
+
de PRE (transporte real, login por redirección y canje del código), usa la app
|
|
161
|
+
de prueba de [`examples/test_app/`](https://gitlab.com/blockauthio/blockauth-sdk-python/-/tree/main/examples/test_app).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
blockauth_sdk/__init__.py,sha256=eJEHy0xkg0Jb3H8vUu8jEHjXzJQTQke0sDxFmxlt20I,603
|
|
2
|
+
blockauth_sdk/api_bridge.py,sha256=Gc5os7anyrPaZFm8RoM1Vkcb114qc-LVJDcIPLLcKqk,1644
|
|
3
|
+
blockauth_sdk/crypto.py,sha256=IMc-xCQ7pUv42sTDw83jThKckwQCpdNa7448QxR4lA0,252
|
|
4
|
+
blockauth_sdk/errors.py,sha256=oqDMg7QWZSp8_sbrBck10dYpqv2aavHZZxO3gp--q6Q,292
|
|
5
|
+
blockauth_sdk/protocol/__init__.py,sha256=dbUSL8v46WwfpXOFpA0GGbv33TlJV5vbykoZPAkF744,691
|
|
6
|
+
blockauth_sdk/protocol/provider.py,sha256=XDo2Xg9F0BEp3BfH_ravBSRnJoBMvVTb4DerOdlO7rU,6958
|
|
7
|
+
blockauth_sdk/protocol/roles.py,sha256=VIh5dSsAp-a_lCXUUWa7zvYfYdlxCERGf2_Dr8LSZAI,1732
|
|
8
|
+
blockauth_sdk/protocol/rpc_client.py,sha256=txDuuv92x3w9yO6wsQBsE3QhlxRmsaVYPepR0dBldqY,253
|
|
9
|
+
blockauth_sdk/protocol/transport.py,sha256=H84yps5kplv3ZciqEGtyrEbh-tZJsjHxrndLt2yiG78,3116
|
|
10
|
+
blockauth_sdk/protocol/ws_client.py,sha256=tM8gYEtPapPK4N4SkW8jYO74BjSdssRksUKpbcBAgrE,8009
|
|
11
|
+
blockauth_sdk-0.5.0.dist-info/licenses/LICENSE,sha256=mXOK634s7KQHauR30-L3jKdDt6dF2MTQUQAklLpSbmU,757
|
|
12
|
+
blockauth_sdk-0.5.0.dist-info/METADATA,sha256=XWVrk717r05CX4NqQZkjOf8oJfge6UD9XkQXSMuknMc,5899
|
|
13
|
+
blockauth_sdk-0.5.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
14
|
+
blockauth_sdk-0.5.0.dist-info/top_level.txt,sha256=300jLVbaYrygxtD8YWV8RpjDR3pmNeUKCk-RqPMbvk4,14
|
|
15
|
+
blockauth_sdk-0.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Block-Auth (block-auth.io)
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blockauth_sdk
|