dfns-sdk 0.0.0.dev0__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.
Files changed (69) hide show
  1. dfns_sdk/__init__.py +24 -0
  2. dfns_sdk/_internal/__init__.py +5 -0
  3. dfns_sdk/_internal/http_client.py +395 -0
  4. dfns_sdk/auth.py +137 -0
  5. dfns_sdk/base_auth_api.py +129 -0
  6. dfns_sdk/client.py +81 -0
  7. dfns_sdk/delegated_client.py +97 -0
  8. dfns_sdk/generated/__init__.py +47 -0
  9. dfns_sdk/generated/agreements/__init__.py +7 -0
  10. dfns_sdk/generated/agreements/client.py +55 -0
  11. dfns_sdk/generated/agreements/delegated_client.py +95 -0
  12. dfns_sdk/generated/agreements/types.py +20 -0
  13. dfns_sdk/generated/allocations/__init__.py +7 -0
  14. dfns_sdk/generated/allocations/client.py +118 -0
  15. dfns_sdk/generated/allocations/delegated_client.py +187 -0
  16. dfns_sdk/generated/allocations/types.py +69 -0
  17. dfns_sdk/generated/auth/__init__.py +7 -0
  18. dfns_sdk/generated/auth/client.py +1215 -0
  19. dfns_sdk/generated/auth/delegated_client.py +1825 -0
  20. dfns_sdk/generated/auth/types.py +702 -0
  21. dfns_sdk/generated/exchanges/__init__.py +7 -0
  22. dfns_sdk/generated/exchanges/client.py +209 -0
  23. dfns_sdk/generated/exchanges/delegated_client.py +341 -0
  24. dfns_sdk/generated/exchanges/types.py +96 -0
  25. dfns_sdk/generated/fee_sponsors/__init__.py +7 -0
  26. dfns_sdk/generated/fee_sponsors/client.py +161 -0
  27. dfns_sdk/generated/fee_sponsors/delegated_client.py +287 -0
  28. dfns_sdk/generated/fee_sponsors/types.py +89 -0
  29. dfns_sdk/generated/keys/__init__.py +7 -0
  30. dfns_sdk/generated/keys/client.py +310 -0
  31. dfns_sdk/generated/keys/delegated_client.py +518 -0
  32. dfns_sdk/generated/keys/types.py +229 -0
  33. dfns_sdk/generated/networks/__init__.py +7 -0
  34. dfns_sdk/generated/networks/client.py +176 -0
  35. dfns_sdk/generated/networks/delegated_client.py +274 -0
  36. dfns_sdk/generated/networks/types.py +77 -0
  37. dfns_sdk/generated/permissions/__init__.py +7 -0
  38. dfns_sdk/generated/permissions/client.py +185 -0
  39. dfns_sdk/generated/permissions/delegated_client.py +344 -0
  40. dfns_sdk/generated/permissions/types.py +107 -0
  41. dfns_sdk/generated/policies/__init__.py +7 -0
  42. dfns_sdk/generated/policies/client.py +187 -0
  43. dfns_sdk/generated/policies/delegated_client.py +311 -0
  44. dfns_sdk/generated/policies/types.py +70 -0
  45. dfns_sdk/generated/signers/__init__.py +7 -0
  46. dfns_sdk/generated/signers/client.py +45 -0
  47. dfns_sdk/generated/signers/delegated_client.py +56 -0
  48. dfns_sdk/generated/signers/types.py +13 -0
  49. dfns_sdk/generated/staking/__init__.py +7 -0
  50. dfns_sdk/generated/staking/client.py +142 -0
  51. dfns_sdk/generated/staking/delegated_client.py +211 -0
  52. dfns_sdk/generated/staking/types.py +64 -0
  53. dfns_sdk/generated/swaps/__init__.py +7 -0
  54. dfns_sdk/generated/swaps/client.py +118 -0
  55. dfns_sdk/generated/swaps/delegated_client.py +157 -0
  56. dfns_sdk/generated/swaps/types.py +99 -0
  57. dfns_sdk/generated/wallets/__init__.py +7 -0
  58. dfns_sdk/generated/wallets/client.py +805 -0
  59. dfns_sdk/generated/wallets/delegated_client.py +1231 -0
  60. dfns_sdk/generated/wallets/types.py +534 -0
  61. dfns_sdk/generated/webhooks/__init__.py +7 -0
  62. dfns_sdk/generated/webhooks/client.py +193 -0
  63. dfns_sdk/generated/webhooks/delegated_client.py +320 -0
  64. dfns_sdk/generated/webhooks/types.py +101 -0
  65. dfns_sdk/types.py +72 -0
  66. dfns_sdk-0.0.0.dev0.dist-info/METADATA +191 -0
  67. dfns_sdk-0.0.0.dev0.dist-info/RECORD +69 -0
  68. dfns_sdk-0.0.0.dev0.dist-info/WHEEL +4 -0
  69. dfns_sdk-0.0.0.dev0.dist-info/licenses/LICENSE +21 -0
dfns_sdk/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ """Dfns Python SDK - Auto-generated from OpenAPI specification."""
2
+
3
+ from .client import DfnsClient
4
+ from .delegated_client import DfnsDelegatedClient
5
+ from .types import DfnsClientConfig, DfnsDelegatedClientConfig, DfnsError
6
+ from .auth import Signer, KeySigner
7
+ from .base_auth_api import (
8
+ BaseAuthApi,
9
+ UserActionChallengeResponse,
10
+ SignUserActionChallengeRequest,
11
+ )
12
+
13
+ __all__ = [
14
+ "DfnsClient",
15
+ "DfnsDelegatedClient",
16
+ "DfnsClientConfig",
17
+ "DfnsDelegatedClientConfig",
18
+ "DfnsError",
19
+ "Signer",
20
+ "KeySigner",
21
+ "BaseAuthApi",
22
+ "UserActionChallengeResponse",
23
+ "SignUserActionChallengeRequest",
24
+ ]
@@ -0,0 +1,5 @@
1
+ """Internal modules."""
2
+
3
+ from .http_client import HttpClient, AsyncHttpClient
4
+
5
+ __all__ = ["HttpClient", "AsyncHttpClient"]
@@ -0,0 +1,395 @@
1
+ """HTTP client for making API requests."""
2
+
3
+ import json
4
+ from typing import Any, Optional, TypeVar, TYPE_CHECKING
5
+ from urllib.parse import urlencode
6
+
7
+ import httpx
8
+
9
+ from dfns_sdk.types import DfnsClientConfig, DfnsDelegatedClientConfig, DfnsError
10
+
11
+ if TYPE_CHECKING:
12
+ from dfns_sdk.auth import Signer
13
+
14
+ T = TypeVar("T")
15
+
16
+
17
+
18
+ class HttpClient:
19
+ """HTTP client for Dfns API requests."""
20
+
21
+ def __init__(self, config: "DfnsClientConfig | DfnsDelegatedClientConfig"):
22
+ self.config = config
23
+ self._client = httpx.Client(
24
+ base_url=config.base_url,
25
+ timeout=30.0,
26
+ )
27
+
28
+ def _build_headers(self, user_action_token: Optional[str] = None) -> dict[str, str]:
29
+ """Build request headers."""
30
+ headers = {
31
+ "Content-Type": "application/json",
32
+ "Authorization": f"Bearer {self.config.auth_token}",
33
+ **self.config.headers,
34
+ }
35
+
36
+ if user_action_token:
37
+ headers["X-DFNS-USERACTION"] = user_action_token
38
+
39
+ return headers
40
+
41
+ def _build_url(
42
+ self,
43
+ path: str,
44
+ path_params: Optional[dict[str, Any]] = None,
45
+ query_params: Optional[dict[str, Any]] = None,
46
+ ) -> str:
47
+ """Build the full URL with path and query parameters."""
48
+ url = path
49
+
50
+ if path_params:
51
+ for key, value in path_params.items():
52
+ url = url.replace(f"{{{key}}}", str(value))
53
+
54
+ if query_params:
55
+ filtered_params = {k: v for k, v in query_params.items() if v is not None}
56
+ if filtered_params:
57
+ url = f"{url}?{urlencode(filtered_params)}"
58
+
59
+ return url
60
+
61
+ def _handle_response(self, response: httpx.Response) -> Any:
62
+ """Handle API response and raise errors if needed."""
63
+ if response.status_code >= 400:
64
+ try:
65
+ error_data = response.json()
66
+ raise DfnsError(
67
+ message=error_data.get("message", "Unknown error"),
68
+ status_code=response.status_code,
69
+ error_code=error_data.get("error"),
70
+ details=error_data.get("details"),
71
+ )
72
+ except json.JSONDecodeError:
73
+ raise DfnsError(
74
+ message=response.text or "Unknown error",
75
+ status_code=response.status_code,
76
+ )
77
+
78
+ if response.status_code == 204 or not response.content:
79
+ return None
80
+
81
+ return response.json()
82
+
83
+ def _get_user_action_token(
84
+ self,
85
+ method: str,
86
+ path: str,
87
+ body: Optional[Any] = None,
88
+ ) -> str:
89
+ """
90
+ Get a user action token by creating and signing a challenge.
91
+
92
+ Args:
93
+ method: HTTP method.
94
+ path: Request path.
95
+ body: Request body.
96
+
97
+ Returns:
98
+ The user action token to include in the request header.
99
+
100
+ Raises:
101
+ DfnsError: If no signer is configured or signing fails.
102
+ """
103
+ if not self.config.signer:
104
+ raise DfnsError(
105
+ message="Signer required for this operation. Configure a signer in DfnsClientConfig.",
106
+ status_code=None,
107
+ error_code="SIGNER_REQUIRED",
108
+ )
109
+
110
+ # Step 1: Create user action challenge
111
+ challenge_body = {
112
+ "userActionPayload": json.dumps(body, separators=(",", ":")) if body else "",
113
+ "userActionHttpMethod": method,
114
+ "userActionHttpPath": path,
115
+ "userActionServerKind": "Api",
116
+ }
117
+
118
+ challenge_response = self._client.request(
119
+ method="POST",
120
+ url="/auth/action/init",
121
+ headers=self._build_headers(),
122
+ json=challenge_body,
123
+ )
124
+ challenge = self._handle_response(challenge_response)
125
+
126
+ # Step 2: Sign the challenge
127
+ assertion = self.config.signer.sign(challenge)
128
+
129
+ # Step 3: Submit signed challenge to get user action token
130
+ signature_body = {
131
+ "challengeIdentifier": challenge["challengeIdentifier"],
132
+ "firstFactor": assertion,
133
+ }
134
+
135
+ signature_response = self._client.request(
136
+ method="POST",
137
+ url="/auth/action",
138
+ headers=self._build_headers(),
139
+ json=signature_body,
140
+ )
141
+ result = self._handle_response(signature_response)
142
+
143
+ return result["userAction"]
144
+
145
+ def request(
146
+ self,
147
+ method: str,
148
+ path: str,
149
+ path_params: Optional[dict[str, Any]] = None,
150
+ query_params: Optional[dict[str, Any]] = None,
151
+ body: Optional[Any] = None,
152
+ requires_signature: bool = False,
153
+ ) -> Any:
154
+ """Make an HTTP request to the API."""
155
+ url = self._build_url(path, path_params, query_params)
156
+
157
+ # Get user action token if required
158
+ user_action_token = None
159
+ if requires_signature:
160
+ # Use the path with params substituted for signing
161
+ signing_path = path
162
+ if path_params:
163
+ for key, value in path_params.items():
164
+ signing_path = signing_path.replace(f"{{{key}}}", str(value))
165
+ user_action_token = self._get_user_action_token(method, signing_path, body)
166
+
167
+ headers = self._build_headers(user_action_token)
168
+
169
+ response = self._client.request(
170
+ method=method,
171
+ url=url,
172
+ headers=headers,
173
+ json=body if body is not None else None,
174
+ )
175
+
176
+ return self._handle_response(response)
177
+
178
+ def request_with_user_action(
179
+ self,
180
+ method: str,
181
+ path: str,
182
+ path_params: Optional[dict[str, Any]] = None,
183
+ query_params: Optional[dict[str, Any]] = None,
184
+ body: Optional[Any] = None,
185
+ user_action: str = "",
186
+ ) -> Any:
187
+ """
188
+ Make an HTTP request with a pre-signed user action token.
189
+
190
+ This method is used by delegated clients where signing is handled externally.
191
+
192
+ Args:
193
+ method: HTTP method.
194
+ path: Request path.
195
+ path_params: Path parameters to substitute.
196
+ query_params: Query parameters.
197
+ body: Request body.
198
+ user_action: Pre-signed user action token.
199
+
200
+ Returns:
201
+ The API response.
202
+ """
203
+ url = self._build_url(path, path_params, query_params)
204
+ headers = self._build_headers(user_action if user_action else None)
205
+
206
+ response = self._client.request(
207
+ method=method,
208
+ url=url,
209
+ headers=headers,
210
+ json=body if body is not None else None,
211
+ )
212
+
213
+ return self._handle_response(response)
214
+
215
+ def close(self) -> None:
216
+ """Close the HTTP client."""
217
+ self._client.close()
218
+
219
+ def __enter__(self) -> "HttpClient":
220
+ return self
221
+
222
+ def __exit__(self, *args: Any) -> None:
223
+ self.close()
224
+
225
+
226
+
227
+ class AsyncHttpClient:
228
+ """Async HTTP client for Dfns API requests."""
229
+
230
+ def __init__(self, config: DfnsClientConfig):
231
+ self.config = config
232
+ self._client = httpx.AsyncClient(
233
+ base_url=config.base_url,
234
+ timeout=30.0,
235
+ )
236
+
237
+ def _build_headers(self, user_action_token: Optional[str] = None) -> dict[str, str]:
238
+ """Build request headers."""
239
+ headers = {
240
+ "Content-Type": "application/json",
241
+ "Authorization": f"Bearer {self.config.auth_token}",
242
+ **self.config.headers,
243
+ }
244
+
245
+ if user_action_token:
246
+ headers["X-DFNS-USERACTION"] = user_action_token
247
+
248
+ return headers
249
+
250
+ def _build_url(
251
+ self,
252
+ path: str,
253
+ path_params: Optional[dict[str, Any]] = None,
254
+ query_params: Optional[dict[str, Any]] = None,
255
+ ) -> str:
256
+ """Build the full URL with path and query parameters."""
257
+ url = path
258
+
259
+ if path_params:
260
+ for key, value in path_params.items():
261
+ url = url.replace(f"{{{key}}}", str(value))
262
+
263
+ if query_params:
264
+ filtered_params = {k: v for k, v in query_params.items() if v is not None}
265
+ if filtered_params:
266
+ url = f"{url}?{urlencode(filtered_params)}"
267
+
268
+ return url
269
+
270
+ def _handle_response(self, response: httpx.Response) -> Any:
271
+ """Handle API response and raise errors if needed."""
272
+ if response.status_code >= 400:
273
+ try:
274
+ error_data = response.json()
275
+ raise DfnsError(
276
+ message=error_data.get("message", "Unknown error"),
277
+ status_code=response.status_code,
278
+ error_code=error_data.get("error"),
279
+ details=error_data.get("details"),
280
+ )
281
+ except json.JSONDecodeError:
282
+ raise DfnsError(
283
+ message=response.text or "Unknown error",
284
+ status_code=response.status_code,
285
+ )
286
+
287
+ if response.status_code == 204 or not response.content:
288
+ return None
289
+
290
+ return response.json()
291
+
292
+ async def _get_user_action_token(
293
+ self,
294
+ method: str,
295
+ path: str,
296
+ body: Optional[Any] = None,
297
+ ) -> str:
298
+ """
299
+ Get a user action token by creating and signing a challenge.
300
+
301
+ Args:
302
+ method: HTTP method.
303
+ path: Request path.
304
+ body: Request body.
305
+
306
+ Returns:
307
+ The user action token to include in the request header.
308
+
309
+ Raises:
310
+ DfnsError: If no signer is configured or signing fails.
311
+ """
312
+ if not self.config.signer:
313
+ raise DfnsError(
314
+ message="Signer required for this operation. Configure a signer in DfnsClientConfig.",
315
+ status_code=None,
316
+ error_code="SIGNER_REQUIRED",
317
+ )
318
+
319
+ # Step 1: Create user action challenge
320
+ challenge_body = {
321
+ "userActionPayload": json.dumps(body, separators=(",", ":")) if body else "",
322
+ "userActionHttpMethod": method,
323
+ "userActionHttpPath": path,
324
+ "userActionServerKind": "Api",
325
+ }
326
+
327
+ challenge_response = await self._client.request(
328
+ method="POST",
329
+ url="/auth/action/init",
330
+ headers=self._build_headers(),
331
+ json=challenge_body,
332
+ )
333
+ challenge = self._handle_response(challenge_response)
334
+
335
+ # Step 2: Sign the challenge
336
+ assertion = self.config.signer.sign(challenge)
337
+
338
+ # Step 3: Submit signed challenge to get user action token
339
+ signature_body = {
340
+ "challengeIdentifier": challenge["challengeIdentifier"],
341
+ "firstFactor": assertion,
342
+ }
343
+
344
+ signature_response = await self._client.request(
345
+ method="POST",
346
+ url="/auth/action",
347
+ headers=self._build_headers(),
348
+ json=signature_body,
349
+ )
350
+ result = self._handle_response(signature_response)
351
+
352
+ return result["userAction"]
353
+
354
+ async def request(
355
+ self,
356
+ method: str,
357
+ path: str,
358
+ path_params: Optional[dict[str, Any]] = None,
359
+ query_params: Optional[dict[str, Any]] = None,
360
+ body: Optional[Any] = None,
361
+ requires_signature: bool = False,
362
+ ) -> Any:
363
+ """Make an async HTTP request to the API."""
364
+ url = self._build_url(path, path_params, query_params)
365
+
366
+ # Get user action token if required
367
+ user_action_token = None
368
+ if requires_signature:
369
+ # Use the path with params substituted for signing
370
+ signing_path = path
371
+ if path_params:
372
+ for key, value in path_params.items():
373
+ signing_path = signing_path.replace(f"{{{key}}}", str(value))
374
+ user_action_token = await self._get_user_action_token(method, signing_path, body)
375
+
376
+ headers = self._build_headers(user_action_token)
377
+
378
+ response = await self._client.request(
379
+ method=method,
380
+ url=url,
381
+ headers=headers,
382
+ json=body if body is not None else None,
383
+ )
384
+
385
+ return self._handle_response(response)
386
+
387
+ async def close(self) -> None:
388
+ """Close the HTTP client."""
389
+ await self._client.aclose()
390
+
391
+ async def __aenter__(self) -> "AsyncHttpClient":
392
+ return self
393
+
394
+ async def __aexit__(self, *args: Any) -> None:
395
+ await self.close()
dfns_sdk/auth.py ADDED
@@ -0,0 +1,137 @@
1
+ """Authentication utilities for the Dfns SDK."""
2
+
3
+ import base64
4
+ import hashlib
5
+ import json
6
+ from typing import Any, Optional, Protocol, TypedDict
7
+
8
+ from cryptography.hazmat.primitives import hashes, serialization
9
+ from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding
10
+
11
+
12
+ class UserActionChallenge(TypedDict):
13
+ """Challenge returned by the server for user action signing."""
14
+
15
+ challenge: str
16
+ challengeIdentifier: str
17
+ supportedCredentialKinds: list[dict[str, Any]]
18
+ allowCredentials: dict[str, Any]
19
+ rp: dict[str, str]
20
+ externalAuthenticationUrl: str
21
+
22
+
23
+ class CredentialAssertion(TypedDict):
24
+ """Credential assertion for signing."""
25
+
26
+ kind: str
27
+ credentialAssertion: dict[str, str]
28
+
29
+
30
+ class Signer(Protocol):
31
+ """Protocol for signing user action challenges."""
32
+
33
+ def sign(self, challenge: UserActionChallenge) -> CredentialAssertion:
34
+ """
35
+ Sign a user action challenge.
36
+
37
+ Args:
38
+ challenge: The challenge from the server.
39
+
40
+ Returns:
41
+ The credential assertion to submit to the server.
42
+ """
43
+ ...
44
+
45
+
46
+ def base64url_encode(data: bytes) -> str:
47
+ """Encode bytes to base64url string without padding."""
48
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
49
+
50
+
51
+ def base64url_decode(data: str) -> bytes:
52
+ """Decode base64url string to bytes."""
53
+ padding = 4 - len(data) % 4
54
+ if padding != 4:
55
+ data += "=" * padding
56
+ return base64.urlsafe_b64decode(data)
57
+
58
+
59
+ class KeySigner:
60
+ """
61
+ Signer implementation using a private key credential.
62
+
63
+ Supports Ed25519, ECDSA (secp256k1, P-256), and RSA keys.
64
+
65
+ Example:
66
+ >>> from dfns_sdk.auth import KeySigner
67
+ >>> signer = KeySigner(
68
+ ... credential_id="cr-xxx-xxx",
69
+ ... private_key=open("private_key.pem").read(),
70
+ ... app_origin="https://app.dfns.io",
71
+ ... )
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ credential_id: str,
77
+ private_key: str,
78
+ app_origin: str = "https://app.dfns.io",
79
+ ):
80
+ """
81
+ Initialize the key signer.
82
+
83
+ Args:
84
+ credential_id: The credential ID (cr-xxx-xxx format).
85
+ private_key: The private key in PEM format.
86
+ app_origin: The application origin for the client data.
87
+ """
88
+ self.credential_id = credential_id
89
+ self.app_origin = app_origin
90
+ self._private_key = serialization.load_pem_private_key(
91
+ private_key.encode() if isinstance(private_key, str) else private_key,
92
+ password=None,
93
+ )
94
+
95
+ def sign(self, challenge: UserActionChallenge) -> CredentialAssertion:
96
+ """
97
+ Sign a user action challenge.
98
+
99
+ Args:
100
+ challenge: The challenge from the server.
101
+
102
+ Returns:
103
+ The credential assertion to submit to the server.
104
+ """
105
+ client_data = {
106
+ "type": "key.get",
107
+ "challenge": challenge["challenge"],
108
+ "origin": self.app_origin,
109
+ "crossOrigin": False,
110
+ }
111
+ client_data_json = json.dumps(client_data, separators=(",", ":"))
112
+ client_data_bytes = client_data_json.encode("utf-8")
113
+
114
+ signature = self._sign_bytes(client_data_bytes)
115
+
116
+ return {
117
+ "kind": "Key",
118
+ "credentialAssertion": {
119
+ "credId": self.credential_id,
120
+ "clientData": base64url_encode(client_data_bytes),
121
+ "signature": base64url_encode(signature),
122
+ },
123
+ }
124
+
125
+ def _sign_bytes(self, data: bytes) -> bytes:
126
+ """Sign bytes with the private key."""
127
+ if isinstance(self._private_key, ed25519.Ed25519PrivateKey):
128
+ return self._private_key.sign(data)
129
+ elif isinstance(self._private_key, ec.EllipticCurvePrivateKey):
130
+ return self._private_key.sign(data, ec.ECDSA(hashes.SHA256()))
131
+ else:
132
+ # RSA
133
+ return self._private_key.sign(
134
+ data,
135
+ padding.PKCS1v15(),
136
+ hashes.SHA256(),
137
+ )
@@ -0,0 +1,129 @@
1
+ """Base authentication API for delegated client operations."""
2
+
3
+ import json
4
+ from typing import Any, TypedDict, Optional
5
+
6
+ from ._internal import HttpClient
7
+
8
+
9
+ class AllowCredential(TypedDict):
10
+ """Allowed credential for signing."""
11
+
12
+ type: str
13
+ id: str
14
+ transports: list[str]
15
+
16
+
17
+ class SupportedCredentialKind(TypedDict):
18
+ """Supported credential kind."""
19
+
20
+ kind: str
21
+ factor: str
22
+ requiresSecondFactor: bool
23
+
24
+
25
+ class UserActionChallengeResponse(TypedDict):
26
+ """Response from creating a user action challenge."""
27
+
28
+ challengeIdentifier: str
29
+ challenge: str
30
+ allowCredentials: list[AllowCredential]
31
+ supportedCredentialKinds: list[SupportedCredentialKind]
32
+ userVerification: str
33
+ rp: dict[str, str]
34
+ externalAuthenticationUrl: str
35
+
36
+
37
+ class SignUserActionChallengeRequest(TypedDict, total=False):
38
+ """Request to complete a user action challenge."""
39
+
40
+ challengeIdentifier: str
41
+ firstFactor: dict[str, Any]
42
+ secondFactor: dict[str, Any]
43
+
44
+
45
+ class BaseAuthApi:
46
+ """
47
+ Helper class for delegated client user action signing.
48
+
49
+ This class provides static methods for the two-phase user action signing flow:
50
+ 1. create_user_action_challenge() - Creates a challenge for external signing
51
+ 2. sign_user_action_challenge() - Completes signing with the signed challenge
52
+
53
+ Example:
54
+ >>> # Step 1: Create challenge
55
+ >>> challenge = BaseAuthApi.create_user_action_challenge(
56
+ ... http_client,
57
+ ... user_action_http_method="POST",
58
+ ... user_action_http_path="/wallets",
59
+ ... user_action_payload='{"network":"EthereumSepolia"}',
60
+ ... )
61
+ >>>
62
+ >>> # Step 2: Sign externally (your signing system)
63
+ >>> signed = your_external_signer.sign(challenge)
64
+ >>>
65
+ >>> # Step 3: Complete with signed challenge
66
+ >>> result = BaseAuthApi.sign_user_action_challenge(
67
+ ... http_client,
68
+ ... signed_challenge={
69
+ ... "challengeIdentifier": challenge["challengeIdentifier"],
70
+ ... "firstFactor": signed,
71
+ ... },
72
+ ... )
73
+ """
74
+
75
+ @staticmethod
76
+ def create_user_action_challenge(
77
+ http_client: HttpClient,
78
+ user_action_http_method: str,
79
+ user_action_http_path: str,
80
+ user_action_payload: str,
81
+ user_action_server_kind: str = "Api",
82
+ ) -> UserActionChallengeResponse:
83
+ """
84
+ Create a user action challenge for signing.
85
+
86
+ Args:
87
+ http_client: The HTTP client to use.
88
+ user_action_http_method: The HTTP method of the action (GET, POST, etc.).
89
+ user_action_http_path: The path of the action endpoint.
90
+ user_action_payload: The JSON-serialized request body.
91
+ user_action_server_kind: The server kind (default: "Api").
92
+
93
+ Returns:
94
+ The challenge response containing the challenge to sign.
95
+ """
96
+ return http_client.request(
97
+ method="POST",
98
+ path="/auth/action/init",
99
+ body={
100
+ "userActionHttpMethod": user_action_http_method,
101
+ "userActionHttpPath": user_action_http_path,
102
+ "userActionPayload": user_action_payload,
103
+ "userActionServerKind": user_action_server_kind,
104
+ },
105
+ requires_signature=False,
106
+ )
107
+
108
+ @staticmethod
109
+ def sign_user_action_challenge(
110
+ http_client: HttpClient,
111
+ signed_challenge: SignUserActionChallengeRequest,
112
+ ) -> dict[str, Any]:
113
+ """
114
+ Complete user action signing with a signed challenge.
115
+
116
+ Args:
117
+ http_client: The HTTP client to use.
118
+ signed_challenge: The signed challenge containing challengeIdentifier
119
+ and firstFactor (and optional secondFactor).
120
+
121
+ Returns:
122
+ Dictionary containing the userAction token.
123
+ """
124
+ return http_client.request(
125
+ method="POST",
126
+ path="/auth/action",
127
+ body=signed_challenge,
128
+ requires_signature=False,
129
+ )