truthid-sdk 0.1.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,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ build/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 masterlxz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: truthid-sdk
3
+ Version: 0.1.0
4
+ Summary: TruthID passwordless, decentralized authentication SDK for Python
5
+ Project-URL: Homepage, https://github.com/masterlxz/truthid/tree/main/sdk/python#readme
6
+ Project-URL: Repository, https://github.com/masterlxz/truthid
7
+ Project-URL: Issues, https://github.com/masterlxz/truthid/issues
8
+ Author: masterlxz
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: auth,authentication,blockchain,decentralized,passwordless,web3
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: web3>=6.0.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # truthid-sdk
21
+
22
+ TruthID passwordless, decentralized authentication SDK for Python.
23
+
24
+ Integrate passwordless, decentralized authentication into your app in minutes — no TruthID-operated server, no passwords, no third-party login.
25
+
26
+ ```bash
27
+ pip install truthid-sdk
28
+ ```
29
+
30
+ Full documentation, how it works, and usage examples: [github.com/masterlxz/truthid/tree/main/sdk](https://github.com/masterlxz/truthid/tree/main/sdk#readme)
31
+
32
+ ## License
33
+
34
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,15 @@
1
+ # truthid-sdk
2
+
3
+ TruthID passwordless, decentralized authentication SDK for Python.
4
+
5
+ Integrate passwordless, decentralized authentication into your app in minutes — no TruthID-operated server, no passwords, no third-party login.
6
+
7
+ ```bash
8
+ pip install truthid-sdk
9
+ ```
10
+
11
+ Full documentation, how it works, and usage examples: [github.com/masterlxz/truthid/tree/main/sdk](https://github.com/masterlxz/truthid/tree/main/sdk#readme)
12
+
13
+ ## License
14
+
15
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "truthid-sdk"
7
+ version = "0.1.0"
8
+ description = "TruthID passwordless, decentralized authentication SDK for Python"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "masterlxz" },
14
+ ]
15
+ keywords = ["authentication", "passwordless", "decentralized", "blockchain", "web3", "auth"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+ dependencies = [
21
+ "web3>=6.0.0",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ dev = ["pytest"]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/masterlxz/truthid/tree/main/sdk/python#readme"
29
+ Repository = "https://github.com/masterlxz/truthid"
30
+ Issues = "https://github.com/masterlxz/truthid/issues"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["truthid"]
@@ -0,0 +1,11 @@
1
+ from .client import TruthIDClient
2
+ from .types import AuthChallenge, AuthResponse, VerifyAuthResult, SessionInfo, DeviceStatus
3
+
4
+ __all__ = [
5
+ "TruthIDClient",
6
+ "AuthChallenge",
7
+ "AuthResponse",
8
+ "VerifyAuthResult",
9
+ "SessionInfo",
10
+ "DeviceStatus",
11
+ ]
@@ -0,0 +1,126 @@
1
+ import json
2
+ import time
3
+ import uuid
4
+ from dataclasses import asdict
5
+ from datetime import datetime, timezone
6
+ from typing import Optional
7
+
8
+ from eth_account import Account
9
+ from eth_account.messages import encode_defunct
10
+ from web3 import Web3
11
+
12
+ from .contracts import (
13
+ DEVICE_REGISTRY_ADDRESSES,
14
+ DEVICE_REGISTRY_ABI,
15
+ SESSION_REGISTRY_ADDRESSES,
16
+ SESSION_REGISTRY_ABI,
17
+ )
18
+ from .types import AuthChallenge, AuthResponse, DeviceStatus, SessionInfo, VerifyAuthResult
19
+
20
+ _RPC_URLS = {
21
+ "base-sepolia": "https://sepolia.base.org",
22
+ "base-mainnet": "https://mainnet.base.org",
23
+ }
24
+
25
+
26
+ class TruthIDClient:
27
+ def __init__(self, network: str = "base-mainnet", rpc_url: Optional[str] = None):
28
+ url = rpc_url or _RPC_URLS[network]
29
+ self._w3 = Web3(Web3.HTTPProvider(url))
30
+ self._devices = self._w3.eth.contract(
31
+ address=Web3.to_checksum_address(DEVICE_REGISTRY_ADDRESSES[network]),
32
+ abi=DEVICE_REGISTRY_ABI,
33
+ )
34
+ self._sessions = self._w3.eth.contract(
35
+ address=Web3.to_checksum_address(SESSION_REGISTRY_ADDRESSES[network]),
36
+ abi=SESSION_REGISTRY_ABI,
37
+ )
38
+
39
+ def create_challenge(self, origin: str) -> AuthChallenge:
40
+ return AuthChallenge(
41
+ type="challenge",
42
+ nonce=str(uuid.uuid4()),
43
+ issuedAt=int(time.time() * 1000),
44
+ origin=origin,
45
+ )
46
+
47
+ def verify_auth_response(
48
+ self,
49
+ challenge: AuthChallenge,
50
+ response: AuthResponse,
51
+ ttl_ms: int = 30_000,
52
+ ) -> VerifyAuthResult:
53
+ # 1. Usuário recusou
54
+ if not response.approved:
55
+ return VerifyAuthResult(valid=False, reason="User rejected the login request")
56
+
57
+ # 2. TTL expirado
58
+ now_ms = int(time.time() * 1000)
59
+ if now_ms - challenge.issuedAt > ttl_ms:
60
+ return VerifyAuthResult(valid=False, reason="Challenge expired")
61
+
62
+ # 3. Nonce bate com o challenge original
63
+ if challenge.nonce != response.nonce:
64
+ return VerifyAuthResult(valid=False, reason="Nonce mismatch")
65
+
66
+ # 4. Verificar assinatura
67
+ # separators=(',', ':') → JSON compacto, igual ao jsonEncode() do Dart e JSON.stringify() do JS
68
+ message = json.dumps(asdict(challenge), separators=(",", ":"))
69
+ msg = encode_defunct(text=message) # adiciona o prefixo Ethereum personal_sign
70
+ try:
71
+ signer = Account.recover_message(msg, signature=response.signature)
72
+ except Exception:
73
+ return VerifyAuthResult(valid=False, reason="Invalid signature format")
74
+
75
+ if signer.lower() != response.deviceAddress.lower():
76
+ return VerifyAuthResult(valid=False, reason="Signature does not match device address")
77
+
78
+ # 5. Device ativo na blockchain
79
+ checksum_addr = Web3.to_checksum_address(response.deviceAddress)
80
+ is_active = self._devices.functions.isDeviceActive(checksum_addr).call()
81
+ if not is_active:
82
+ return VerifyAuthResult(valid=False, reason="Device is not active or has been revoked")
83
+
84
+ # 6. Buscar identityId do device
85
+ device = self._devices.functions.getDevice(checksum_addr).call()
86
+
87
+ return VerifyAuthResult(
88
+ valid=True,
89
+ identity_id=device[0], # identityId (uint256)
90
+ device_address=response.deviceAddress,
91
+ )
92
+
93
+ def verify_session(self, session_hash: str) -> SessionInfo:
94
+ hash_bytes = bytes.fromhex(session_hash.removeprefix("0x"))
95
+
96
+ session = self._sessions.functions.getSession(hash_bytes).call()
97
+ if not session[4]: # exists
98
+ return SessionInfo(exists=False, revoked=False)
99
+
100
+ revoked = self._sessions.functions.isSessionRevoked(hash_bytes).call()
101
+ created_at = datetime.fromtimestamp(session[2], tz=timezone.utc) # createdAt (uint256 segundos)
102
+
103
+ return SessionInfo(
104
+ exists=True,
105
+ revoked=revoked,
106
+ identity_id=session[0], # identityId
107
+ device_pub_key=session[1], # devicePubKey
108
+ created_at=created_at,
109
+ )
110
+
111
+ def check_device_status(self, device_pub_key: str) -> DeviceStatus:
112
+ checksum_addr = Web3.to_checksum_address(device_pub_key)
113
+ device = self._devices.functions.getDevice(checksum_addr).call()
114
+
115
+ if not device[5]: # exists
116
+ return DeviceStatus(exists=False, active=False)
117
+
118
+ added_at = datetime.fromtimestamp(device[3], tz=timezone.utc) # addedAt
119
+
120
+ return DeviceStatus(
121
+ exists=True,
122
+ active=not device[4], # revoked → active é o inverso
123
+ label=device[2],
124
+ identity_id=device[0],
125
+ added_at=added_at,
126
+ )
@@ -0,0 +1,71 @@
1
+ IDENTITY_REGISTRY_ADDRESSES = {
2
+ "base-sepolia": "0x35D21c65980cBd2dAE7576e1bf6b8e46C9e180BF",
3
+ "base-mainnet": "0xbf097EC74d0Cc9b16D3d94EaCa62060d89A63b17",
4
+ }
5
+
6
+ DEVICE_REGISTRY_ADDRESSES = {
7
+ "base-sepolia": "0x225c67a98c9D675fE595ae05a2F9249C34d9C60a",
8
+ "base-mainnet": "0x4A7a307cb6872bde24BAf3E9de2BeC3Ddd03e144",
9
+ }
10
+ DEVICE_REGISTRY_ABI = [
11
+ {
12
+ "type": "function",
13
+ "name": "isDeviceActive",
14
+ "inputs": [{"name": "devicePubKey", "type": "address"}],
15
+ "outputs": [{"name": "", "type": "bool"}],
16
+ "stateMutability": "view",
17
+ },
18
+ {
19
+ "type": "function",
20
+ "name": "getDevice",
21
+ "inputs": [{"name": "devicePubKey", "type": "address"}],
22
+ "outputs": [
23
+ {
24
+ "name": "",
25
+ "type": "tuple",
26
+ "components": [
27
+ {"name": "identityId", "type": "uint256"},
28
+ {"name": "pubKey", "type": "address"},
29
+ {"name": "label", "type": "string"},
30
+ {"name": "addedAt", "type": "uint256"},
31
+ {"name": "revoked", "type": "bool"},
32
+ {"name": "exists", "type": "bool"},
33
+ ],
34
+ }
35
+ ],
36
+ "stateMutability": "view",
37
+ },
38
+ ]
39
+
40
+ SESSION_REGISTRY_ADDRESSES = {
41
+ "base-sepolia": "0xdeD2Ad865069CA6546172926540D3A3Aa73C1CA6",
42
+ "base-mainnet": "0x24074587a2aFB3aa5491361BB0a5eBee90797D1B",
43
+ }
44
+ SESSION_REGISTRY_ABI = [
45
+ {
46
+ "type": "function",
47
+ "name": "isSessionRevoked",
48
+ "inputs": [{"name": "hash", "type": "bytes32"}],
49
+ "outputs": [{"name": "", "type": "bool"}],
50
+ "stateMutability": "view",
51
+ },
52
+ {
53
+ "type": "function",
54
+ "name": "getSession",
55
+ "inputs": [{"name": "hash", "type": "bytes32"}],
56
+ "outputs": [
57
+ {
58
+ "name": "",
59
+ "type": "tuple",
60
+ "components": [
61
+ {"name": "identityId", "type": "uint256"},
62
+ {"name": "devicePubKey", "type": "address"},
63
+ {"name": "createdAt", "type": "uint256"},
64
+ {"name": "revoked", "type": "bool"},
65
+ {"name": "exists", "type": "bool"},
66
+ ],
67
+ }
68
+ ],
69
+ "stateMutability": "view",
70
+ },
71
+ ]
@@ -0,0 +1,46 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from typing import Optional
4
+
5
+
6
+ # Campos em camelCase: mapeiam diretamente para o protocolo JSON do mobile
7
+ @dataclass
8
+ class AuthChallenge:
9
+ type: str
10
+ nonce: str
11
+ issuedAt: int # timestamp Unix em ms
12
+ origin: str
13
+
14
+
15
+ @dataclass
16
+ class AuthResponse:
17
+ approved: bool
18
+ nonce: str
19
+ signature: str # assinatura secp256k1 em hex ("0x...")
20
+ deviceAddress: str # endereço Ethereum da chave do device
21
+
22
+
23
+ @dataclass
24
+ class VerifyAuthResult:
25
+ valid: bool
26
+ identity_id: Optional[int] = None
27
+ device_address: Optional[str] = None
28
+ reason: Optional[str] = None
29
+
30
+
31
+ @dataclass
32
+ class SessionInfo:
33
+ exists: bool
34
+ revoked: bool
35
+ identity_id: Optional[int] = None
36
+ device_pub_key: Optional[str] = None
37
+ created_at: Optional[datetime] = None
38
+
39
+
40
+ @dataclass
41
+ class DeviceStatus:
42
+ exists: bool
43
+ active: bool
44
+ label: Optional[str] = None
45
+ identity_id: Optional[int] = None
46
+ added_at: Optional[datetime] = None