diff --git a/README.md b/README.md index 50a1700..dc7231d 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,19 @@ with AxmeClient(config) as client: print(approval["approval"]["decision"]) capabilities = client.get_capabilities() print(capabilities["supported_intent_types"]) + invite = client.create_invite( + {"owner_agent": "agent://example/receiver", "recipient_hint": "Partner A", "ttl_seconds": 3600}, + idempotency_key="invite-create-001", + ) + print(invite["token"]) + invite_details = client.get_invite(invite["token"]) + print(invite_details["status"]) + accepted = client.accept_invite( + invite["token"], + {"nick": "@PartnerA.User", "display_name": "Partner A"}, + idempotency_key="invite-accept-001", + ) + print(accepted["public_address"]) subscription = client.upsert_webhook_subscription( { "callback_url": "https://integrator.example/webhooks/axme", diff --git a/axme_sdk/client.py b/axme_sdk/client.py index 8d206ac..1bc6867 100644 --- a/axme_sdk/client.py +++ b/axme_sdk/client.py @@ -162,6 +162,42 @@ def decide_approval( def get_capabilities(self, *, trace_id: str | None = None) -> dict[str, Any]: return self._request_json("GET", "/v1/capabilities", trace_id=trace_id, retryable=True) + def create_invite( + self, + payload: dict[str, Any], + *, + idempotency_key: str | None = None, + trace_id: str | None = None, + ) -> dict[str, Any]: + return self._request_json( + "POST", + "/v1/invites/create", + json_body=payload, + idempotency_key=idempotency_key, + trace_id=trace_id, + retryable=idempotency_key is not None, + ) + + def get_invite(self, token: str, *, trace_id: str | None = None) -> dict[str, Any]: + return self._request_json("GET", f"/v1/invites/{token}", trace_id=trace_id, retryable=True) + + def accept_invite( + self, + token: str, + payload: dict[str, Any], + *, + idempotency_key: str | None = None, + trace_id: str | None = None, + ) -> dict[str, Any]: + return self._request_json( + "POST", + f"/v1/invites/{token}/accept", + json_body=payload, + idempotency_key=idempotency_key, + trace_id=trace_id, + retryable=idempotency_key is not None, + ) + def upsert_webhook_subscription( self, payload: dict[str, Any], diff --git a/tests/test_client.py b/tests/test_client.py index 426c2ad..9cf9908 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -278,6 +278,92 @@ def handler(request: httpx.Request) -> httpx.Response: assert client.get_capabilities()["ok"] is True +def test_create_invite_success() -> None: + token = "invite-token-0001" + payload = {"owner_agent": "agent://owner", "recipient_hint": "receiver", "ttl_seconds": 3600} + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert request.url.path == "/v1/invites/create" + assert request.headers["idempotency-key"] == "invite-create-1" + body = json.loads(request.read().decode("utf-8")) + assert body == payload + return httpx.Response( + 200, + json={ + "ok": True, + "token": token, + "invite_url": f"https://invite.example/{token}", + "owner_agent": "agent://owner", + "recipient_hint": "receiver", + "status": "pending", + "created_at": "2026-02-28T00:00:00Z", + "expires_at": "2026-03-01T00:00:00Z", + }, + ) + + client = _client(handler) + assert client.create_invite(payload, idempotency_key="invite-create-1")["token"] == token + + +def test_get_invite_success() -> None: + token = "invite-token-0002" + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "GET" + assert request.url.path == f"/v1/invites/{token}" + return httpx.Response( + 200, + json={ + "ok": True, + "token": token, + "owner_agent": "agent://owner", + "recipient_hint": "receiver", + "status": "pending", + "created_at": "2026-02-28T00:00:00Z", + "expires_at": "2026-03-01T00:00:00Z", + "accepted_at": None, + "accepted_owner_agent": None, + "nick": None, + "public_address": None, + }, + ) + + client = _client(handler) + assert client.get_invite(token)["status"] == "pending" + + +def test_accept_invite_success() -> None: + token = "invite-token-0003" + payload = {"nick": "@Invite.User", "display_name": "Invite User"} + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert request.url.path == f"/v1/invites/{token}/accept" + assert request.headers["idempotency-key"] == "invite-accept-1" + body = json.loads(request.read().decode("utf-8")) + assert body == payload + return httpx.Response( + 200, + json={ + "ok": True, + "token": token, + "status": "accepted", + "invite_owner_agent": "agent://owner", + "user_id": "66666666-6666-4666-8666-666666666666", + "owner_agent": "agent://accepted", + "nick": "@Invite.User", + "public_address": "invite.user@ax", + "display_name": "Invite User", + "accepted_at": "2026-02-28T00:00:10Z", + "registry_bind_status": "propagated", + }, + ) + + client = _client(handler) + assert client.accept_invite(token, payload, idempotency_key="invite-accept-1")["status"] == "accepted" + + @pytest.mark.parametrize( ("status_code", "expected_exception"), [