Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 31 additions & 9 deletions dfns_sdk/_internal/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,35 @@
import json
from collections.abc import Mapping
from typing import Any, cast
from urllib.parse import urlencode
from urllib.parse import urlencode, urlsplit, urlunsplit

import httpx

from dfns_sdk.types import DfnsClientConfig, DfnsDelegatedClientConfig, DfnsError


def _normalize_base_url(base_url: str) -> str:
"""Validate and normalize a complete API transport base URL."""
parsed = urlsplit(base_url)
if not parsed.scheme or not parsed.netloc:
raise ValueError("base_url must be an absolute URL")
if "?" in base_url:
raise ValueError("base_url must not include a query")
if "#" in base_url:
raise ValueError("base_url must not include a fragment")

normalized_path = parsed.path.rstrip("/")
return urlunsplit((parsed.scheme, parsed.netloc, normalized_path, "", ""))


class HttpClient:
"""HTTP client for Dfns API requests."""

def __init__(self, config: "DfnsClientConfig | DfnsDelegatedClientConfig"):
self.config = config
self._base_url = _normalize_base_url(config.base_url)
self._client = httpx.Client(
base_url=config.base_url,
base_url=self._base_url,
timeout=30.0,
)

Expand All @@ -41,7 +56,10 @@ def _build_url(
query_params: Mapping[str, Any] | None = None,
) -> str:
"""Build the full URL with path and query parameters."""
url = path
if not path.startswith("/") or path.startswith("//"):
raise ValueError("request path must be root-relative")

url = f"{self._base_url}/{path.removeprefix('/')}"

if path_params:
for key, value in path_params.items():
Expand Down Expand Up @@ -114,7 +132,7 @@ def _get_user_action_token(

challenge_response = self._client.request(
method="POST",
url="/auth/action/init",
url=self._build_url("/auth/action/init"),
headers=self._build_headers(),
json=challenge_body,
)
Expand All @@ -131,7 +149,7 @@ def _get_user_action_token(

signature_response = self._client.request(
method="POST",
url="/auth/action",
url=self._build_url("/auth/action"),
headers=self._build_headers(),
json=signature_body,
)
Expand Down Expand Up @@ -249,8 +267,9 @@ class AsyncHttpClient:

def __init__(self, config: DfnsClientConfig):
self.config = config
self._base_url = _normalize_base_url(config.base_url)
self._client = httpx.AsyncClient(
base_url=config.base_url,
base_url=self._base_url,
timeout=30.0,
)

Expand All @@ -274,7 +293,10 @@ def _build_url(
query_params: Mapping[str, Any] | None = None,
) -> str:
"""Build the full URL with path and query parameters."""
url = path
if not path.startswith("/") or path.startswith("//"):
raise ValueError("request path must be root-relative")

url = f"{self._base_url}/{path.removeprefix('/')}"

if path_params:
for key, value in path_params.items():
Expand Down Expand Up @@ -347,7 +369,7 @@ async def _get_user_action_token(

challenge_response = await self._client.request(
method="POST",
url="/auth/action/init",
url=self._build_url("/auth/action/init"),
headers=self._build_headers(),
json=challenge_body,
)
Expand All @@ -364,7 +386,7 @@ async def _get_user_action_token(

signature_response = await self._client.request(
method="POST",
url="/auth/action",
url=self._build_url("/auth/action"),
headers=self._build_headers(),
json=signature_body,
)
Expand Down
44 changes: 44 additions & 0 deletions dfns_sdk/generated/auth/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,50 @@ def logout(self, body: T.LogoutRequest) -> T.LogoutResponse:
)
return cast(T.LogoutResponse, response)

def complete_oidc_login(self, body: T.CompleteOidcLoginRequest) -> dict[str, Any]:
"""
Complete OIDC Login.

Completes the OIDC login process by exchanging the authorization code obtained from the identity provider. If the verified user has no active first-factor credential yet, it returns a registration challenge to complete via [Complete User Registration](/api-reference/auth/complete-user-registration); otherwise it returns the user's authentication token.

Args:
body: Request body.

Returns:
dict[str, Any]: The API response.
""" # noqa: E501
response = self._http.request(
method="POST",
path="/auth/login/oidc",
path_params={},
query_params=None,
body=body,
requires_signature=False,
)
return cast(dict[str, Any], response)

def initiate_oidc_login(self, body: T.InitiateOidcLoginRequest) -> T.InitiateOidcLoginResponse:
"""
Initiate OIDC Login.

Initialize the OIDC login process by returning the identity provider authorization URL to redirect the user to.

Args:
body: Request body.

Returns:
T.InitiateOidcLoginResponse: The API response.
""" # noqa: E501
response = self._http.request(
method="POST",
path="/auth/login/oidc/init",
path_params={},
query_params=None,
body=body,
requires_signature=False,
)
return cast(T.InitiateOidcLoginResponse, response)

def send_login_code(self, body: T.SendLoginCodeRequest) -> T.SendLoginCodeResponse:
"""
Send Login Code.
Expand Down
44 changes: 44 additions & 0 deletions dfns_sdk/generated/auth/delegated_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,50 @@ def logout(self, body: T.LogoutRequest) -> T.LogoutResponse:
)
return cast(T.LogoutResponse, response)

def complete_oidc_login(self, body: T.CompleteOidcLoginRequest) -> dict[str, Any]:
"""
Complete OIDC Login.

Completes the OIDC login process by exchanging the authorization code obtained from the identity provider. If the verified user has no active first-factor credential yet, it returns a registration challenge to complete via [Complete User Registration](/api-reference/auth/complete-user-registration); otherwise it returns the user's authentication token.

Args:
body: Request body.

Returns:
dict[str, Any]: The API response.
""" # noqa: E501
response = self._http.request(
method="POST",
path="/auth/login/oidc",
path_params={},
query_params=None,
body=body,
requires_signature=False,
)
return cast(dict[str, Any], response)

def initiate_oidc_login(self, body: T.InitiateOidcLoginRequest) -> T.InitiateOidcLoginResponse:
"""
Initiate OIDC Login.

Initialize the OIDC login process by returning the identity provider authorization URL to redirect the user to.

Args:
body: Request body.

Returns:
T.InitiateOidcLoginResponse: The API response.
""" # noqa: E501
response = self._http.request(
method="POST",
path="/auth/login/oidc/init",
path_params={},
query_params=None,
body=body,
requires_signature=False,
)
return cast(T.InitiateOidcLoginResponse, response)

def send_login_code(self, body: T.SendLoginCodeRequest) -> T.SendLoginCodeResponse:
"""
Send Login Code.
Expand Down
21 changes: 21 additions & 0 deletions dfns_sdk/generated/auth/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,27 @@ class LogoutResponse(TypedDict, total=False):
message: str


class CompleteOidcLoginRequest(TypedDict, total=False):
"""completeOidcLogin request body."""

code: str
state: str


class InitiateOidcLoginRequest(TypedDict, total=False):
"""initiateOidcLogin request body."""

org_id: NotRequired[str]
tenant_id: NotRequired[str]
redirect_uri: str


class InitiateOidcLoginResponse(TypedDict, total=False):
"""initiateOidcLogin response."""

redirect_url: str


class SendLoginCodeRequest(TypedDict, total=False):
"""sendLoginCode request body."""

Expand Down
4 changes: 4 additions & 0 deletions dfns_sdk/generated/permissions/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ class CreatePermissionRequest(TypedDict, total=False):
"Vaults:Tags:Delete",
"Vaults:Addresses:Create",
"Vaults:Quarantines:Release",
"Vaults:Locks:Create",
"Vaults:Locks:Delete",
"Vaults:Transfers:Create",
"Webhooks:Create",
"Webhooks:Read",
Expand Down Expand Up @@ -385,6 +387,8 @@ class UpdatePermissionRequest(TypedDict, total=False):
"Vaults:Tags:Delete",
"Vaults:Addresses:Create",
"Vaults:Quarantines:Release",
"Vaults:Locks:Create",
"Vaults:Locks:Delete",
"Vaults:Transfers:Create",
"Webhooks:Create",
"Webhooks:Read",
Expand Down
92 changes: 92 additions & 0 deletions dfns_sdk/generated/vaults/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,52 @@ def create_vault_address(self, vault_id: str, body: T.CreateVaultAddressRequest)
)
return cast(T.CreateVaultAddressResponse, response)

def list_vault_locks(self, vault_id: str, query: T.ListVaultLocksQuery | None = None) -> T.ListVaultLocksResponse:
"""
List Vault Locks.

Lists a vault's locks, active and released.

Args:
vault_id: Vault id.
query: Query parameters.

Returns:
T.ListVaultLocksResponse: The API response.
""" # noqa: E501
response = self._http.request(
method="GET",
path="/vaults/{vaultId}/locks",
path_params={"vaultId": vault_id},
query_params=query,
body=None,
requires_signature=False,
)
return cast(T.ListVaultLocksResponse, response)

def create_vault_lock(self, vault_id: str, body: T.CreateVaultLockRequest) -> T.CreateVaultLockResponse:
"""
Create Vault Lock.

Locks funds from the vault's available balance for off-chain settlement or escrow.

Args:
vault_id: Vault id.
body: Request body.

Returns:
T.CreateVaultLockResponse: The API response.
""" # noqa: E501
response = self._http.request(
method="POST",
path="/vaults/{vaultId}/locks",
path_params={"vaultId": vault_id},
query_params=None,
body=body,
requires_signature=True,
)
return cast(T.CreateVaultLockResponse, response)

def create_vault_transfer(self, vault_id: str, body: T.CreateVaultTransferRequest) -> T.CreateVaultTransferResponse:
"""
Create Vault Transfer.
Expand All @@ -102,6 +148,52 @@ def create_vault_transfer(self, vault_id: str, body: T.CreateVaultTransferReques
)
return cast(T.CreateVaultTransferResponse, response)

def get_vault_lock(self, vault_id: str, lock_id: str) -> T.GetVaultLockResponse:
"""
Get Vault Lock.

Retrieves a vault lock by its ID.

Args:
vault_id: Vault id.
lock_id: The lock to retrieve.

Returns:
T.GetVaultLockResponse: The API response.
""" # noqa: E501
response = self._http.request(
method="GET",
path="/vaults/{vaultId}/locks/{lockId}",
path_params={"vaultId": vault_id, "lockId": lock_id},
query_params=None,
body=None,
requires_signature=False,
)
return cast(T.GetVaultLockResponse, response)

def delete_vault_lock(self, vault_id: str, lock_id: str) -> T.DeleteVaultLockResponse:
"""
Delete Vault Lock.

Releases a lock, returning the locked funds to the vault's available balance. Owner only.

Args:
vault_id: Vault id.
lock_id: Vault lock id.

Returns:
T.DeleteVaultLockResponse: The API response.
""" # noqa: E501
response = self._http.request(
method="DELETE",
path="/vaults/{vaultId}/locks/{lockId}",
path_params={"vaultId": vault_id, "lockId": lock_id},
query_params=None,
body=None,
requires_signature=True,
)
return cast(T.DeleteVaultLockResponse, response)

def get_vault(self, vault_id: str) -> T.GetVaultResponse:
"""
Get Vault.
Expand Down
Loading