blockauth-sdk 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,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,136 @@
1
+ # BlockAuth SDK (Python)
2
+
3
+ Cliente headless del servicio de autenticación BlockAuth (Socket.IO v4).
4
+ Puerto de referencia de la capa de protocolo de `@block-auth.io/core`
5
+ (monorepo `blockauth-sdk`, patrón *headless core + thin per-framework adapters*).
6
+
7
+ ## Arquitectura
8
+
9
+ Este puerto replica la capa `protocol/` del core TypeScript; la reactividad y
10
+ la UI de los bindings (React/Vue/Angular) quedan fuera de alcance.
11
+
12
+ | Este repo | Upstream (`blockauth-sdk`) |
13
+ | :--- | :--- |
14
+ | `blockauth_sdk/protocol/provider.py` | `packages/core/src/protocol/BlockAuthProvider.ts` |
15
+ | `blockauth_sdk/protocol/ws_client.py` | `packages/core/src/protocol/WsClient.ts` |
16
+ | `blockauth_sdk/protocol/rpc_client.py` | `packages/core/src/protocol/RpcClient.ts` |
17
+ | `blockauth_sdk/protocol/roles.py` | `packages/core/src/protocol/roles.ts` |
18
+ | `blockauth_sdk/protocol/transport.py` | — (abstracción propia del puerto; upstream usa socket.io directo) |
19
+ | `blockauth_sdk/api_bridge.py` | `packages/core/src/utils/apiBridge.ts` (solo la emisión del browser token) |
20
+ | `blockauth_sdk/crypto.py`, `blockauth_sdk/errors.py` | — (infra propia del puerto) |
21
+
22
+ La API pública se re-exporta desde la raíz del paquete (`from blockauth_sdk import
23
+ BlockAuthProvider`), igual que hace el `index.ts` del core.
24
+
25
+ ## Instalación
26
+
27
+ ```bash
28
+ pip install blockauth-sdk
29
+ ```
30
+
31
+ ## Uso
32
+
33
+ Un backend se autentica con su `apiKey` y su `apiSecret`. El `apiSecret` solo
34
+ vive en el backend.
35
+
36
+ ```python
37
+ from blockauth_sdk import BlockAuthProvider
38
+
39
+ provider = BlockAuthProvider(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET")
40
+ provider.connect()
41
+
42
+ result = provider.signin()
43
+ if result["success"]:
44
+ profile = provider.user("0xUserAddress")
45
+ print(provider.role_names)
46
+
47
+ provider.disconnect()
48
+ ```
49
+
50
+ No autentiques un backend con un browser token: el ws-server lo rechaza (exige
51
+ un `Origin` de confianza y su scope no da acceso a `user`, `users`, `roles` ni
52
+ `logs`).
53
+
54
+ ### Login por redirección
55
+
56
+ Para que tus usuarios inicien sesión con BlockAuth, tu backend los manda al
57
+ login alojado y recibe de vuelta un código de un solo uso, que canjea por la
58
+ dirección y los roles del usuario. La dirección la da el ws-server, nunca el
59
+ navegador.
60
+
61
+ ```python
62
+ import json
63
+ import secrets
64
+ import urllib.error
65
+ import urllib.request
66
+
67
+ from blockauth_sdk.api_bridge import build_ws_server_api_url
68
+ from blockauth_sdk.crypto import sha256_hex
69
+
70
+
71
+ def post(path, body):
72
+ request = urllib.request.Request(
73
+ build_ws_server_api_url(path),
74
+ data=json.dumps(body).encode("utf-8"),
75
+ headers={"Content-Type": "application/json"},
76
+ )
77
+ try:
78
+ with urllib.request.urlopen(request) as response:
79
+ data = json.load(response)
80
+ except urllib.error.HTTPError as err: # los errores llegan como 400 con el mismo JSON
81
+ data = json.load(err)
82
+ if not data.get("success"):
83
+ raise RuntimeError(data.get("message"))
84
+ return data["message"]
85
+
86
+
87
+ credentials = {"apiKey": "YOUR_API_KEY", "apiSecret": sha256_hex("YOUR_API_SECRET")}
88
+ redirect_uri = "https://tuapp.com/callback"
89
+
90
+ # 1. Pide el enlace de login y redirige al usuario a él.
91
+ state = secrets.token_hex(16) # guárdalo en la sesión
92
+ link = post("/auth/authorize", {**credentials, "redirectUri": redirect_uri, "state": state})["url"]
93
+
94
+ # 2. El usuario vuelve a redirect_uri?code=...&state=... Comprueba que el state es el de la sesión.
95
+
96
+ # 3. Canjea el código (válido 60 s, un solo uso).
97
+ user = post("/auth/token", {**credentials, "code": code, "redirectUri": redirect_uri})
98
+ address = user["address"] # también: roles, signedExpiredAt
99
+ ```
100
+
101
+ - El `redirect_uri` tiene que ser `https` en el dominio (o el dominio de
102
+ desarrollo) de la app; `http` solo en `localhost`.
103
+ - Para cerrar la sesión del usuario en tu app: `post("/auth/signout",
104
+ {**credentials, "address": address})`.
105
+
106
+ ### Browser token para tu frontend
107
+
108
+ Si tu frontend usa `@block-auth.io/core`, el backend le emite un browser token
109
+ para que el `apiSecret` no salga nunca del backend:
110
+
111
+ ```python
112
+ provider = BlockAuthProvider(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET")
113
+ browser_token = provider.create_browser_token()
114
+ ```
115
+
116
+ El frontend lo usa como `browserToken` en `@block-auth.io/core`.
117
+
118
+ ## API
119
+
120
+ `BlockAuthProvider`: `connect`, `disconnect`, `is_connected`, `signin`, `create_browser_token`, `refresh`,
121
+ `is_address_signed`, `app`, `user`, `users`, `roles`, `logs`, `kyc_issuer`,
122
+ helpers de roles (`has_role`, `has_role_by_slug`, `has_any_role`,
123
+ `has_any_role_by_slug`) y getters (`current_user`, `role_names`, `ui_config`, …).
124
+
125
+ ## Tests
126
+
127
+ ```bash
128
+ pip install -e ".[dev]"
129
+ pytest
130
+ ```
131
+
132
+ ## Smoke test contra un servidor real
133
+
134
+ Los tests usan un transporte simulado. Para probar el SDK contra el ws-server
135
+ de PRE (transporte real, login por redirección y canje del código), usa la app
136
+ de prueba de [`examples/test_app/`](https://gitlab.com/blockauthio/blockauth-sdk-python/-/tree/main/examples/test_app).
@@ -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 {}
@@ -0,0 +1,9 @@
1
+ import hashlib
2
+
3
+
4
+ def sha256_hex(text: str) -> str:
5
+ """SHA256 del texto (UTF-8) como hex en minúsculas.
6
+
7
+ Equivalente a crypto-js `SHA256(text).toString()` usado por el SDK TS.
8
+ """
9
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
@@ -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)