Fix async genesis hash bug, add 7 missing methods, document exports
- Fix AsyncElydoraClient _prev_chain_hash from empty string to GENESIS_CHAIN_HASH - Add list_agents, unfreeze_agent, delete_agent, get_me, issue_token, download_export, health - Add methods to both sync and async clients - Add corresponding response types to types.py - Export new types from __init__.py - Document all previously undocumented exports in README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
3c77b4a31d
commit
eef18a8520
5 files changed
+214
-1
No files matched your search
@@ -117,6 +117,12 @@ reg = ElydoraClient.register(base_url, email, password, display_name=None, org_n
|
|||||||
|
|
||||||
# Login and receive a JWT
|
# Login and receive a JWT
|
||||||
auth = ElydoraClient.login(base_url, email, password)
|
auth = ElydoraClient.login(base_url, email, password)
|
||||||
|
|
||||||
|
# Get current authenticated user profile
|
||||||
|
me = client.get_me()
|
||||||
|
|
||||||
|
# Issue a new API token (with optional TTL in seconds)
|
||||||
|
token_resp = client.issue_token(ttl_seconds=3600)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Operations
|
### Operations
|
||||||
@@ -159,6 +165,15 @@ client.freeze_agent(agent_id, reason="security review")
|
|||||||
|
|
||||||
# Revoke a key
|
# Revoke a key
|
||||||
client.revoke_key(agent_id, kid, reason="key rotation")
|
client.revoke_key(agent_id, kid, reason="key rotation")
|
||||||
|
|
||||||
|
# List all agents in the organization
|
||||||
|
agents_resp = client.list_agents()
|
||||||
|
|
||||||
|
# Unfreeze a previously frozen agent
|
||||||
|
client.unfreeze_agent(agent_id, reason="review complete")
|
||||||
|
|
||||||
|
# Delete an agent permanently
|
||||||
|
deleted_resp = client.delete_agent(agent_id)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Audit
|
### Audit
|
||||||
@@ -193,6 +208,9 @@ export = client.create_export(
|
|||||||
|
|
||||||
exports = client.list_exports()
|
exports = client.list_exports()
|
||||||
detail = client.get_export(export_id)
|
detail = client.get_export(export_id)
|
||||||
|
|
||||||
|
# Download export file data
|
||||||
|
data = client.download_export(export_id)
|
||||||
```
|
```
|
||||||
|
|
||||||
### JWKS
|
### JWKS
|
||||||
@@ -201,6 +219,67 @@ detail = client.get_export(export_id)
|
|||||||
jwks = client.get_jwks()
|
jwks = client.get_jwks()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Health
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Check API health (no authentication required)
|
||||||
|
health = client.health()
|
||||||
|
# health["status"], health["version"], health["protocol_version"], health["timestamp"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Crypto Functions
|
||||||
|
|
||||||
|
The SDK exports low-level cryptographic primitives for advanced use:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from elydora import (
|
||||||
|
jcs_canonicalize, # RFC 8785 JSON Canonicalization
|
||||||
|
sha256_base64url, # SHA-256 hash as base64url
|
||||||
|
compute_chain_hash, # Chain hash computation
|
||||||
|
compute_payload_hash, # Payload hash (SHA-256 of JCS-canonicalized payload)
|
||||||
|
sign_ed25519, # Ed25519 signing
|
||||||
|
sign_eor, # Sign an EOR dict
|
||||||
|
get_public_key_base64url, # Derive public key from private seed
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Utility Functions
|
||||||
|
|
||||||
|
```python
|
||||||
|
from elydora import (
|
||||||
|
base64url_encode, # Encode bytes to base64url (no padding)
|
||||||
|
base64url_decode, # Decode base64url string to bytes
|
||||||
|
generate_nonce, # Generate 16-byte random nonce (base64url)
|
||||||
|
generate_uuidv7, # Generate UUIDv7 (time-ordered, RFC 9562)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Definitions
|
||||||
|
|
||||||
|
All types are `TypedDict` classes for structural typing:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from elydora import (
|
||||||
|
# Protocol types
|
||||||
|
EOR, # Elydora Operation Record
|
||||||
|
EAR, # Elydora Acknowledgment Receipt
|
||||||
|
|
||||||
|
# Entity types
|
||||||
|
Agent, AgentKey, Operation, Receipt, Epoch, Export, Organization, User,
|
||||||
|
|
||||||
|
# API response types
|
||||||
|
RegisterAgentResponse, GetAgentResponse, ListAgentsResponse,
|
||||||
|
DeleteAgentResponse, SubmitOperationResponse, GetOperationResponse,
|
||||||
|
VerifyOperationResponse, AuditQueryResponse, GetEpochResponse,
|
||||||
|
ListEpochsResponse, CreateExportResponse, GetExportResponse,
|
||||||
|
ListExportsResponse, JWKSResponse, AuthRegisterResponse,
|
||||||
|
AuthLoginResponse, GetMeResponse, IssueTokenResponse, HealthResponse,
|
||||||
|
|
||||||
|
# Request types
|
||||||
|
RegisterAgentRequest,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ from .types import (
|
|||||||
RegisterAgentRequest,
|
RegisterAgentRequest,
|
||||||
RegisterAgentResponse,
|
RegisterAgentResponse,
|
||||||
GetAgentResponse,
|
GetAgentResponse,
|
||||||
|
ListAgentsResponse,
|
||||||
|
DeleteAgentResponse,
|
||||||
SubmitOperationResponse,
|
SubmitOperationResponse,
|
||||||
GetOperationResponse,
|
GetOperationResponse,
|
||||||
VerifyOperationResponse,
|
VerifyOperationResponse,
|
||||||
@@ -39,6 +41,9 @@ from .types import (
|
|||||||
JWKSResponse,
|
JWKSResponse,
|
||||||
AuthRegisterResponse,
|
AuthRegisterResponse,
|
||||||
AuthLoginResponse,
|
AuthLoginResponse,
|
||||||
|
GetMeResponse,
|
||||||
|
IssueTokenResponse,
|
||||||
|
HealthResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
__version__ = "1.0.0"
|
||||||
@@ -71,6 +76,8 @@ __all__ = [
|
|||||||
"RegisterAgentRequest",
|
"RegisterAgentRequest",
|
||||||
"RegisterAgentResponse",
|
"RegisterAgentResponse",
|
||||||
"GetAgentResponse",
|
"GetAgentResponse",
|
||||||
|
"ListAgentsResponse",
|
||||||
|
"DeleteAgentResponse",
|
||||||
"SubmitOperationResponse",
|
"SubmitOperationResponse",
|
||||||
"GetOperationResponse",
|
"GetOperationResponse",
|
||||||
"VerifyOperationResponse",
|
"VerifyOperationResponse",
|
||||||
@@ -83,4 +90,7 @@ __all__ = [
|
|||||||
"JWKSResponse",
|
"JWKSResponse",
|
||||||
"AuthRegisterResponse",
|
"AuthRegisterResponse",
|
||||||
"AuthLoginResponse",
|
"AuthLoginResponse",
|
||||||
|
"GetMeResponse",
|
||||||
|
"IssueTokenResponse",
|
||||||
|
"HealthResponse",
|
||||||
]
|
]
|
||||||
+55
-1
@@ -15,12 +15,17 @@ from .types import (
|
|||||||
AuthLoginResponse,
|
AuthLoginResponse,
|
||||||
AuthRegisterResponse,
|
AuthRegisterResponse,
|
||||||
CreateExportResponse,
|
CreateExportResponse,
|
||||||
|
DeleteAgentResponse,
|
||||||
EOR,
|
EOR,
|
||||||
GetAgentResponse,
|
GetAgentResponse,
|
||||||
GetEpochResponse,
|
GetEpochResponse,
|
||||||
GetExportResponse,
|
GetExportResponse,
|
||||||
|
GetMeResponse,
|
||||||
GetOperationResponse,
|
GetOperationResponse,
|
||||||
|
HealthResponse,
|
||||||
|
IssueTokenResponse,
|
||||||
JWKSResponse,
|
JWKSResponse,
|
||||||
|
ListAgentsResponse,
|
||||||
ListEpochsResponse,
|
ListEpochsResponse,
|
||||||
ListExportsResponse,
|
ListExportsResponse,
|
||||||
RegisterAgentRequest,
|
RegisterAgentRequest,
|
||||||
@@ -30,6 +35,10 @@ from .types import (
|
|||||||
)
|
)
|
||||||
from .utils import generate_nonce, generate_uuidv7
|
from .utils import generate_nonce, generate_uuidv7
|
||||||
|
|
||||||
|
# The genesis chain hash: base64url encoding of 32 zero bytes.
|
||||||
|
# Must match the backend's GENESIS_CHAIN_HASH constant exactly.
|
||||||
|
GENESIS_CHAIN_HASH = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||||
|
|
||||||
|
|
||||||
class AsyncElydoraClient:
|
class AsyncElydoraClient:
|
||||||
"""Asynchronous client for the Elydora API.
|
"""Asynchronous client for the Elydora API.
|
||||||
@@ -63,7 +72,7 @@ class AsyncElydoraClient:
|
|||||||
self.max_retries = max_retries
|
self.max_retries = max_retries
|
||||||
self.token = token
|
self.token = token
|
||||||
|
|
||||||
self._prev_chain_hash = ""
|
self._prev_chain_hash = GENESIS_CHAIN_HASH
|
||||||
self._kid = ""
|
self._kid = ""
|
||||||
self._session: Optional[aiohttp.ClientSession] = None
|
self._session: Optional[aiohttp.ClientSession] = None
|
||||||
|
|
||||||
@@ -186,6 +195,17 @@ class AsyncElydoraClient:
|
|||||||
) as resp:
|
) as resp:
|
||||||
return await AsyncElydoraClient._handle_response(resp)
|
return await AsyncElydoraClient._handle_response(resp)
|
||||||
|
|
||||||
|
async def get_me(self) -> GetMeResponse:
|
||||||
|
"""Get the current user's profile."""
|
||||||
|
return await self._request("GET", "/v1/auth/me")
|
||||||
|
|
||||||
|
async def issue_token(self, ttl_seconds: Optional[int] = None) -> IssueTokenResponse:
|
||||||
|
"""Issue an API token with an optional TTL."""
|
||||||
|
body: Dict[str, Any] = {}
|
||||||
|
if ttl_seconds is not None:
|
||||||
|
body["ttl_seconds"] = ttl_seconds
|
||||||
|
return await self._request("POST", "/v1/auth/token", json_body=body)
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
# Agent management
|
# Agent management
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
@@ -202,6 +222,18 @@ class AsyncElydoraClient:
|
|||||||
"""Freeze an agent."""
|
"""Freeze an agent."""
|
||||||
await self._request("POST", f"/v1/agents/{agent_id}/freeze", json_body={"reason": reason})
|
await self._request("POST", f"/v1/agents/{agent_id}/freeze", json_body={"reason": reason})
|
||||||
|
|
||||||
|
async def list_agents(self) -> ListAgentsResponse:
|
||||||
|
"""List all agents for the organization."""
|
||||||
|
return await self._request("GET", "/v1/agents")
|
||||||
|
|
||||||
|
async def unfreeze_agent(self, agent_id: str, reason: str) -> None:
|
||||||
|
"""Unfreeze an agent."""
|
||||||
|
await self._request("POST", f"/v1/agents/{agent_id}/unfreeze", json_body={"reason": reason})
|
||||||
|
|
||||||
|
async def delete_agent(self, agent_id: str) -> DeleteAgentResponse:
|
||||||
|
"""Delete an agent."""
|
||||||
|
return await self._request("DELETE", f"/v1/agents/{agent_id}")
|
||||||
|
|
||||||
async def revoke_key(self, agent_id: str, kid: str, reason: str) -> None:
|
async def revoke_key(self, agent_id: str, kid: str, reason: str) -> None:
|
||||||
"""Revoke an agent's key."""
|
"""Revoke an agent's key."""
|
||||||
await self._request(
|
await self._request(
|
||||||
@@ -347,6 +379,17 @@ class AsyncElydoraClient:
|
|||||||
"""Retrieve export status and download URL."""
|
"""Retrieve export status and download URL."""
|
||||||
return await self._request("GET", f"/v1/exports/{export_id}")
|
return await self._request("GET", f"/v1/exports/{export_id}")
|
||||||
|
|
||||||
|
async def download_export(self, export_id: str) -> bytes:
|
||||||
|
"""Download an export file as raw bytes."""
|
||||||
|
url = f"{self.base_url}/v1/exports/{export_id}/download"
|
||||||
|
session = await self._get_session()
|
||||||
|
async with session.get(
|
||||||
|
url, headers=self._headers(), timeout=aiohttp.ClientTimeout(total=30)
|
||||||
|
) as resp:
|
||||||
|
if resp.status >= 400:
|
||||||
|
await self._handle_response(resp)
|
||||||
|
return await resp.read()
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
# JWKS
|
# JWKS
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
@@ -357,3 +400,14 @@ class AsyncElydoraClient:
|
|||||||
session = await self._get_session()
|
session = await self._get_session()
|
||||||
async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
||||||
return await self._handle_response(resp)
|
return await self._handle_response(resp)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Health
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
|
||||||
|
async def health(self) -> HealthResponse:
|
||||||
|
"""Check API health (public, no auth required)."""
|
||||||
|
url = f"{self.base_url}/v1/health"
|
||||||
|
session = await self._get_session()
|
||||||
|
async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
||||||
|
return await self._handle_response(resp)
|
||||||
@@ -14,12 +14,17 @@ from .types import (
|
|||||||
AuthLoginResponse,
|
AuthLoginResponse,
|
||||||
AuthRegisterResponse,
|
AuthRegisterResponse,
|
||||||
CreateExportResponse,
|
CreateExportResponse,
|
||||||
|
DeleteAgentResponse,
|
||||||
EOR,
|
EOR,
|
||||||
GetAgentResponse,
|
GetAgentResponse,
|
||||||
GetEpochResponse,
|
GetEpochResponse,
|
||||||
GetExportResponse,
|
GetExportResponse,
|
||||||
|
GetMeResponse,
|
||||||
GetOperationResponse,
|
GetOperationResponse,
|
||||||
|
HealthResponse,
|
||||||
|
IssueTokenResponse,
|
||||||
JWKSResponse,
|
JWKSResponse,
|
||||||
|
ListAgentsResponse,
|
||||||
ListEpochsResponse,
|
ListEpochsResponse,
|
||||||
ListExportsResponse,
|
ListExportsResponse,
|
||||||
RegisterAgentRequest,
|
RegisterAgentRequest,
|
||||||
@@ -181,6 +186,17 @@ class ElydoraClient:
|
|||||||
resp = requests.post(url, json=body, headers={"Content-Type": "application/json"}, timeout=30)
|
resp = requests.post(url, json=body, headers={"Content-Type": "application/json"}, timeout=30)
|
||||||
return ElydoraClient._handle_response(resp)
|
return ElydoraClient._handle_response(resp)
|
||||||
|
|
||||||
|
def get_me(self) -> GetMeResponse:
|
||||||
|
"""Get the current user's profile."""
|
||||||
|
return self._request("GET", "/v1/auth/me")
|
||||||
|
|
||||||
|
def issue_token(self, ttl_seconds: Optional[int] = None) -> IssueTokenResponse:
|
||||||
|
"""Issue an API token with an optional TTL."""
|
||||||
|
body: Dict[str, Any] = {}
|
||||||
|
if ttl_seconds is not None:
|
||||||
|
body["ttl_seconds"] = ttl_seconds
|
||||||
|
return self._request("POST", "/v1/auth/token", json_body=body)
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
# Agent management
|
# Agent management
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
@@ -197,6 +213,18 @@ class ElydoraClient:
|
|||||||
"""Freeze an agent."""
|
"""Freeze an agent."""
|
||||||
self._request("POST", f"/v1/agents/{agent_id}/freeze", json_body={"reason": reason})
|
self._request("POST", f"/v1/agents/{agent_id}/freeze", json_body={"reason": reason})
|
||||||
|
|
||||||
|
def list_agents(self) -> ListAgentsResponse:
|
||||||
|
"""List all agents for the organization."""
|
||||||
|
return self._request("GET", "/v1/agents")
|
||||||
|
|
||||||
|
def unfreeze_agent(self, agent_id: str, reason: str) -> None:
|
||||||
|
"""Unfreeze an agent."""
|
||||||
|
self._request("POST", f"/v1/agents/{agent_id}/unfreeze", json_body={"reason": reason})
|
||||||
|
|
||||||
|
def delete_agent(self, agent_id: str) -> DeleteAgentResponse:
|
||||||
|
"""Delete an agent."""
|
||||||
|
return self._request("DELETE", f"/v1/agents/{agent_id}")
|
||||||
|
|
||||||
def revoke_key(self, agent_id: str, kid: str, reason: str) -> None:
|
def revoke_key(self, agent_id: str, kid: str, reason: str) -> None:
|
||||||
"""Revoke an agent's key."""
|
"""Revoke an agent's key."""
|
||||||
self._request(
|
self._request(
|
||||||
@@ -344,6 +372,14 @@ class ElydoraClient:
|
|||||||
"""Retrieve export status and download URL."""
|
"""Retrieve export status and download URL."""
|
||||||
return self._request("GET", f"/v1/exports/{export_id}")
|
return self._request("GET", f"/v1/exports/{export_id}")
|
||||||
|
|
||||||
|
def download_export(self, export_id: str) -> bytes:
|
||||||
|
"""Download an export file as raw bytes."""
|
||||||
|
url = f"{self.base_url}/v1/exports/{export_id}/download"
|
||||||
|
resp = self._session.get(url, headers=self._headers(), timeout=30)
|
||||||
|
if resp.status_code >= 400:
|
||||||
|
self._handle_response(resp)
|
||||||
|
return resp.content
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
# JWKS
|
# JWKS
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
@@ -353,3 +389,13 @@ class ElydoraClient:
|
|||||||
url = f"{self.base_url}/.well-known/elydora/jwks.json"
|
url = f"{self.base_url}/.well-known/elydora/jwks.json"
|
||||||
resp = self._session.get(url, timeout=30)
|
resp = self._session.get(url, timeout=30)
|
||||||
return self._handle_response(resp)
|
return self._handle_response(resp)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Health
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
|
||||||
|
def health(self) -> HealthResponse:
|
||||||
|
"""Check API health (public, no auth required)."""
|
||||||
|
url = f"{self.base_url}/v1/health"
|
||||||
|
resp = self._session.get(url, timeout=30)
|
||||||
|
return self._handle_response(resp)
|
||||||
@@ -291,3 +291,27 @@ class AuthRegisterResponse(TypedDict):
|
|||||||
class AuthLoginResponse(TypedDict):
|
class AuthLoginResponse(TypedDict):
|
||||||
user: User
|
user: User
|
||||||
token: str
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
class ListAgentsResponse(TypedDict):
|
||||||
|
agents: List[Agent]
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteAgentResponse(TypedDict):
|
||||||
|
deleted: bool
|
||||||
|
|
||||||
|
|
||||||
|
class GetMeResponse(TypedDict):
|
||||||
|
user: User
|
||||||
|
|
||||||
|
|
||||||
|
class IssueTokenResponse(TypedDict):
|
||||||
|
token: str
|
||||||
|
expires_at: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
|
class HealthResponse(TypedDict):
|
||||||
|
status: str
|
||||||
|
version: str
|
||||||
|
protocol_version: str
|
||||||
|
timestamp: int
|
||||||
Reference in new issue
Block a user