openkitx403-client 0.1.2__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.
@@ -0,0 +1,242 @@
1
+ """OpenKitx403 Python Client - Manual wallet authentication for scripts and agents"""
2
+ import json
3
+ import base64
4
+ import secrets
5
+ import re
6
+ from datetime import datetime
7
+ from typing import Dict, Optional, Any
8
+ from urllib.parse import urlparse
9
+
10
+
11
+ import requests
12
+ import base58
13
+ from solders.keypair import Keypair # ✅ CHANGED: solana → solders
14
+ from nacl.signing import SigningKey
15
+
16
+
17
+
18
+ class OpenKit403ClientError(Exception):
19
+ """Base exception for OpenKit403 client errors"""
20
+ pass
21
+
22
+
23
+
24
+ class OpenKit403Client:
25
+ """
26
+ Python client for OpenKitx403 wallet authentication.
27
+
28
+ This client is designed for server-side Python applications and scripts
29
+ that need to authenticate with OpenKitx403-protected APIs using a Solana keypair.
30
+
31
+ Example:
32
+ from solders.keypair import Keypair # ✅ CHANGED
33
+ from openkitx403_client import OpenKit403Client
34
+
35
+ keypair = Keypair.generate()
36
+ client = OpenKit403Client(keypair)
37
+
38
+ response = client.authenticate('https://api.example.com/protected')
39
+ print(response.json())
40
+ """
41
+
42
+ def __init__(self, keypair: Keypair):
43
+ """
44
+ Initialize client with a Solana keypair.
45
+
46
+ Args:
47
+ keypair: Solana keypair for signing challenges
48
+ """
49
+ self.keypair = keypair
50
+ self.address = str(keypair.pubkey()) # ✅ CHANGED: public_key → pubkey()
51
+
52
+ def authenticate(
53
+ self,
54
+ url: str,
55
+ method: str = 'GET',
56
+ headers: Optional[Dict[str, str]] = None,
57
+ data: Optional[Dict[str, Any]] = None,
58
+ json_data: Optional[Dict[str, Any]] = None
59
+ ) -> requests.Response:
60
+ """
61
+ Authenticate with an OpenKitx403-protected endpoint.
62
+
63
+ This method handles the complete authentication flow:
64
+ 1. Makes initial request
65
+ 2. If 403, extracts challenge
66
+ 3. Signs challenge with keypair
67
+ 4. Retries request with Authorization header
68
+
69
+ Args:
70
+ url: API endpoint URL
71
+ method: HTTP method (GET, POST, PUT, DELETE, etc.)
72
+ headers: Additional headers to send
73
+ data: Form data to send (for POST/PUT)
74
+ json_data: JSON data to send (for POST/PUT)
75
+
76
+ Returns:
77
+ requests.Response object from the authenticated request
78
+
79
+ Raises:
80
+ OpenKit403ClientError: If authentication fails
81
+ """
82
+ headers = headers or {}
83
+
84
+ # Step 1: Initial request
85
+ response = self._make_request(url, method, headers, data, json_data)
86
+
87
+ # Step 2: Check if we got a 403 challenge
88
+ if response.status_code == 403:
89
+ www_auth = response.headers.get('WWW-Authenticate', '')
90
+
91
+ if not www_auth.startswith('OpenKitx403'):
92
+ raise OpenKit403ClientError(
93
+ f"Expected OpenKitx403 challenge, got: {www_auth[:50]}"
94
+ )
95
+
96
+ # Extract challenge
97
+ challenge_b64 = self._extract_challenge(www_auth)
98
+ if not challenge_b64:
99
+ raise OpenKit403ClientError("No challenge found in WWW-Authenticate header")
100
+
101
+ # Step 3: Sign challenge
102
+ signature_b58 = self._sign_challenge(challenge_b64)
103
+
104
+ # Step 4: Build Authorization header
105
+ auth_header = self._build_authorization(
106
+ challenge_b64,
107
+ signature_b58,
108
+ method,
109
+ urlparse(url).path
110
+ )
111
+
112
+ # Step 5: Retry with Authorization
113
+ headers['Authorization'] = auth_header
114
+ response = self._make_request(url, method, headers, data, json_data)
115
+
116
+ return response
117
+
118
+ def _make_request(
119
+ self,
120
+ url: str,
121
+ method: str,
122
+ headers: Dict[str, str],
123
+ data: Optional[Dict[str, Any]],
124
+ json_data: Optional[Dict[str, Any]]
125
+ ) -> requests.Response:
126
+ """Make HTTP request"""
127
+ return requests.request(
128
+ method=method,
129
+ url=url,
130
+ headers=headers,
131
+ data=data,
132
+ json=json_data
133
+ )
134
+
135
+ def _extract_challenge(self, www_authenticate: str) -> Optional[str]:
136
+ """Extract base64url-encoded challenge from WWW-Authenticate header"""
137
+ match = re.search(r'challenge="([^"]+)"', www_authenticate)
138
+ return match.group(1) if match else None
139
+
140
+ def _sign_challenge(self, challenge_b64: str) -> str:
141
+ """
142
+ Sign a challenge and return base58-encoded signature.
143
+
144
+ Args:
145
+ challenge_b64: Base64url-encoded challenge from server
146
+
147
+ Returns:
148
+ Base58-encoded signature
149
+ """
150
+ # Decode challenge
151
+ challenge_json = self._base64url_decode(challenge_b64)
152
+ challenge = json.loads(challenge_json)
153
+
154
+ # Build signing string
155
+ signing_string = self._build_signing_string(challenge)
156
+
157
+ # Sign with keypair
158
+ message = signing_string.encode('utf-8')
159
+ signing_key = SigningKey(bytes(self.keypair.secret())) # ✅ CHANGED: secret_key[:32] → secret()
160
+ signed = signing_key.sign(message)
161
+
162
+ # Return base58-encoded signature
163
+ return base58.b58encode(signed.signature).decode('ascii')
164
+
165
+ def _build_signing_string(self, challenge: Dict[str, Any]) -> str:
166
+ """Build canonical signing string from challenge"""
167
+ # Sort challenge keys for deterministic JSON
168
+ payload = json.dumps(challenge, sort_keys=True)
169
+
170
+ lines = [
171
+ 'OpenKitx403 Challenge',
172
+ '',
173
+ f'domain: {challenge["aud"]}',
174
+ f'server: {challenge["serverId"]}',
175
+ f'nonce: {challenge["nonce"]}',
176
+ f'ts: {challenge["ts"]}',
177
+ f'method: {challenge["method"]}',
178
+ f'path: {challenge["path"]}',
179
+ '',
180
+ f'payload: {payload}'
181
+ ]
182
+
183
+ return '\n'.join(lines)
184
+
185
+ def _build_authorization(
186
+ self,
187
+ challenge_b64: str,
188
+ signature_b58: str,
189
+ method: str,
190
+ path: str
191
+ ) -> str:
192
+ """Build Authorization header value"""
193
+ nonce = self._generate_nonce()
194
+ ts = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
195
+ bind = f'{method}:{path}'
196
+
197
+ return (
198
+ f'OpenKitx403 '
199
+ f'addr="{self.address}", '
200
+ f'sig="{signature_b58}", '
201
+ f'challenge="{challenge_b64}", '
202
+ f'ts="{ts}", '
203
+ f'nonce="{nonce}", '
204
+ f'bind="{bind}"'
205
+ )
206
+
207
+ @staticmethod
208
+ def _generate_nonce() -> str:
209
+ """Generate cryptographically random nonce"""
210
+ return base58.b58encode(secrets.token_bytes(16)).decode('ascii')
211
+
212
+ @staticmethod
213
+ def _base64url_decode(s: str) -> str:
214
+ """Decode base64url string"""
215
+ # Add padding
216
+ padding = (4 - len(s) % 4) % 4
217
+ s_padded = s + '=' * padding
218
+
219
+ decoded = base64.urlsafe_b64decode(s_padded)
220
+ return decoded.decode('utf-8')
221
+
222
+
223
+
224
+ def create_client(keypair: Keypair) -> OpenKit403Client:
225
+ """
226
+ Factory function to create an OpenKit403Client.
227
+
228
+ Args:
229
+ keypair: Solana keypair for authentication
230
+
231
+ Returns:
232
+ Configured OpenKit403Client instance
233
+ """
234
+ return OpenKit403Client(keypair)
235
+
236
+
237
+
238
+ __all__ = [
239
+ 'OpenKit403Client',
240
+ 'OpenKit403ClientError',
241
+ 'create_client'
242
+ ]
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: openkitx403-client
3
+ Version: 0.1.2
4
+ Summary: Python client SDK for OpenKitx403 wallet authentication
5
+ License: MIT
6
+ Author: OpenKitx403 Contributors
7
+ Author-email: team@openkitx403.dev
8
+ Requires-Python: >=3.8,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Dist: base58 (>=2.1.1,<3.0.0)
19
+ Requires-Dist: pynacl (>=1.5.0,<2.0.0)
20
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
21
+ Requires-Dist: solders (>=0.18.1,<0.19.0)
22
+ Project-URL: Documentation, https://openkitx403.github.io/openkitx403-docs
23
+ Project-URL: Homepage, https://www.openkitx403.dev
24
+ Project-URL: Repository, https://github.com/openkitx403/openkitx403
25
+ Description-Content-Type: text/markdown
26
+
27
+ # openkitx403-client (Python)
28
+
29
+ Python client SDK for OpenKitx403 wallet authentication.
30
+
31
+ ## Installation
32
+
33
+ \`\`\`bash
34
+ pip install openkitx403-client
35
+ \`\`\`
36
+
37
+ ## Usage
38
+
39
+ \`\`\`python
40
+ from solana.keypair import Keypair
41
+ from openkitx403_client import OpenKit403Client
42
+
43
+ keypair = Keypair.generate()
44
+ client = OpenKit403Client(keypair)
45
+
46
+ response = client.authenticate('https://api.example.com/protected')
47
+ print(response.json())
48
+ \`\`\`
49
+
50
+ See USAGE_EXAMPLES.md for complete examples.
51
+
@@ -0,0 +1,4 @@
1
+ openkitx403_client/__init__.py,sha256=0MbpgQgGzlZk2L3ZhvAwpEjtPNK5oVgRFN1cTGTmytM,7566
2
+ openkitx403_client-0.1.2.dist-info/METADATA,sha256=PSmBGti8TuWswwWc9iAV9b_BUZdy88rrt-dxAOjXYz0,1560
3
+ openkitx403_client-0.1.2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
4
+ openkitx403_client-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any