Split the 657-line operation service into eor, submit, and verify modules, add the AGENT_REVOKED and KEY_RETIRED codes the Backend already emits, and condense every multi-line comment. Mirror Node, Python, and Go at 2.1.0 with the deep-health 503, non-idempotent retry, rotation grace, and path-encoding fixes. Document which routes and response fields the self-hosted server does not provide. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
116 lines
3.3 KiB
Python
116 lines
3.3 KiB
Python
"""Request and response building shared by the sync and async clients."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from typing import Any, Dict, Optional, Tuple, Union
|
|
|
|
from .crypto import compute_chain_hash, compute_payload_hash, sign_eor
|
|
from .errors import ElydoraError
|
|
from .types import EOR
|
|
from .utils import generate_nonce, generate_uuidv7
|
|
|
|
# 32 zero bytes; equals the Backend GENESIS_CHAIN_HASH.
|
|
GENESIS_CHAIN_HASH = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
DEFAULT_BASE_URL = "https://api.elydora.com"
|
|
REQUEST_TIMEOUT_SECONDS = 30
|
|
|
|
|
|
def build_headers(token: Optional[str]) -> Dict[str, str]:
|
|
headers = {"Content-Type": "application/json"}
|
|
if token:
|
|
headers["Authorization"] = f"Bearer {token}"
|
|
return headers
|
|
|
|
|
|
def error_from_response(status_code: int, body: Any, text: str) -> ElydoraError:
|
|
error = body.get("error", {}) if isinstance(body, dict) else {}
|
|
return ElydoraError(
|
|
code=error.get("code", "INTERNAL_ERROR"),
|
|
message=error.get("message") or text or "Unknown error",
|
|
request_id=error.get("request_id", ""),
|
|
details=error.get("details"),
|
|
status_code=status_code,
|
|
)
|
|
|
|
|
|
def without_none(values: Dict[str, Any]) -> Dict[str, Any]:
|
|
return {key: value for key, value in values.items() if value is not None}
|
|
|
|
|
|
def register_body(
|
|
email: str,
|
|
password: str,
|
|
display_name: Optional[str],
|
|
org_name: Optional[str],
|
|
) -> Dict[str, Any]:
|
|
return without_none({
|
|
"email": email,
|
|
"password": password,
|
|
"display_name": display_name,
|
|
"org_name": org_name,
|
|
})
|
|
|
|
|
|
def api_token_body(ttl_seconds: Optional[int]) -> Dict[str, Any]:
|
|
return without_none({"ttl_seconds": ttl_seconds})
|
|
|
|
|
|
def export_body(
|
|
start_time: int,
|
|
end_time: int,
|
|
format: str,
|
|
agent_id: Optional[str],
|
|
operation_type: Optional[str],
|
|
) -> Dict[str, Any]:
|
|
return without_none({
|
|
"start_time": start_time,
|
|
"end_time": end_time,
|
|
"format": format,
|
|
"agent_id": agent_id,
|
|
"operation_type": operation_type,
|
|
})
|
|
|
|
|
|
def admin_events_params(limit: Optional[int]) -> Dict[str, str]:
|
|
return {} if limit is None else {"limit": str(limit)}
|
|
|
|
|
|
def build_eor(
|
|
*,
|
|
org_id: str,
|
|
agent_id: str,
|
|
private_key: str,
|
|
kid: str,
|
|
ttl_ms: int,
|
|
prev_chain_hash: str,
|
|
operation_type: str,
|
|
subject: Dict[str, Any],
|
|
action: Dict[str, Any],
|
|
payload: Union[Dict[str, Any], str, None],
|
|
) -> Tuple[EOR, str]:
|
|
"""Build and sign one EOR; returns the record and its chain hash."""
|
|
operation_id = generate_uuidv7()
|
|
issued_at = int(time.time() * 1000)
|
|
payload_hash = compute_payload_hash(payload)
|
|
chain_hash = compute_chain_hash(prev_chain_hash, payload_hash, operation_id, issued_at)
|
|
eor: Dict[str, Any] = {
|
|
"op_version": "1.0",
|
|
"operation_id": operation_id,
|
|
"org_id": org_id,
|
|
"agent_id": agent_id,
|
|
"issued_at": issued_at,
|
|
"ttl_ms": ttl_ms,
|
|
"nonce": generate_nonce(),
|
|
"operation_type": operation_type,
|
|
"subject": subject,
|
|
"action": action,
|
|
"payload": payload,
|
|
"payload_hash": payload_hash,
|
|
"prev_chain_hash": prev_chain_hash,
|
|
"agent_pubkey_kid": kid,
|
|
"signature": "",
|
|
}
|
|
eor["signature"] = sign_eor(eor, private_key)
|
|
return eor, chain_hash # type: ignore[return-value]
|