xache 5.0.0__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.
- xache/__init__.py +142 -0
- xache/client.py +331 -0
- xache/crypto/__init__.py +17 -0
- xache/crypto/signing.py +244 -0
- xache/crypto/wallet.py +240 -0
- xache/errors.py +184 -0
- xache/payment/__init__.py +5 -0
- xache/payment/handler.py +244 -0
- xache/services/__init__.py +29 -0
- xache/services/budget.py +285 -0
- xache/services/collective.py +174 -0
- xache/services/extraction.py +173 -0
- xache/services/facilitator.py +296 -0
- xache/services/identity.py +415 -0
- xache/services/memory.py +401 -0
- xache/services/owner.py +293 -0
- xache/services/receipts.py +202 -0
- xache/services/reputation.py +274 -0
- xache/services/royalty.py +290 -0
- xache/services/sessions.py +268 -0
- xache/services/workspaces.py +447 -0
- xache/types.py +399 -0
- xache/utils/__init__.py +5 -0
- xache/utils/cache.py +214 -0
- xache/utils/http.py +209 -0
- xache/utils/retry.py +101 -0
- xache-5.0.0.dist-info/METADATA +337 -0
- xache-5.0.0.dist-info/RECORD +30 -0
- xache-5.0.0.dist-info/WHEEL +5 -0
- xache-5.0.0.dist-info/top_level.txt +1 -0
xache/services/memory.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""Memory Service - Store, retrieve, delete encrypted memories per LLD §2.4"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import hashlib
|
|
5
|
+
from typing import Dict, Any, Optional, List
|
|
6
|
+
|
|
7
|
+
import nacl.secret
|
|
8
|
+
import nacl.utils
|
|
9
|
+
import nacl.hash
|
|
10
|
+
|
|
11
|
+
from ..types import (
|
|
12
|
+
StoreMemoryRequest,
|
|
13
|
+
StoreMemoryResponse,
|
|
14
|
+
RetrieveMemoryResponse,
|
|
15
|
+
BatchStoreMemoryRequest,
|
|
16
|
+
BatchStoreMemoryResponse,
|
|
17
|
+
BatchStoreMemoryResult,
|
|
18
|
+
BatchRetrieveMemoryRequest,
|
|
19
|
+
BatchRetrieveMemoryResponse,
|
|
20
|
+
BatchRetrieveMemoryResult,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MemoryService:
|
|
25
|
+
"""Memory service for encrypted data storage"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, client):
|
|
28
|
+
self.client = client
|
|
29
|
+
self._encryption_key: Optional[bytes] = None
|
|
30
|
+
|
|
31
|
+
async def store(
|
|
32
|
+
self,
|
|
33
|
+
data: Dict[str, Any],
|
|
34
|
+
storage_tier: str,
|
|
35
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
36
|
+
) -> StoreMemoryResponse:
|
|
37
|
+
"""
|
|
38
|
+
Store encrypted memory per LLD §2.4
|
|
39
|
+
Cost: $0.01 (automatic 402 payment)
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
```python
|
|
43
|
+
memory = await client.memory.store(
|
|
44
|
+
data={"key": "value", "nested": {"data": 123}},
|
|
45
|
+
storage_tier="hot",
|
|
46
|
+
metadata={"tags": ["important"]},
|
|
47
|
+
)
|
|
48
|
+
print(f"Memory ID: {memory.memory_id}")
|
|
49
|
+
```
|
|
50
|
+
"""
|
|
51
|
+
# Validate request
|
|
52
|
+
self._validate_store_request(data, storage_tier)
|
|
53
|
+
|
|
54
|
+
# Get encryption key
|
|
55
|
+
key = await self._get_encryption_key()
|
|
56
|
+
|
|
57
|
+
# Encrypt data client-side using PyNaCl
|
|
58
|
+
encrypted_data = self._encrypt_data(data, key)
|
|
59
|
+
|
|
60
|
+
# Make API request with automatic 402 payment
|
|
61
|
+
response = await self.client.request_with_payment(
|
|
62
|
+
"POST",
|
|
63
|
+
"/v1/memory/store",
|
|
64
|
+
{
|
|
65
|
+
"encryptedData": encrypted_data,
|
|
66
|
+
"storageTier": storage_tier,
|
|
67
|
+
"metadata": metadata,
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if not response.success or not response.data:
|
|
72
|
+
raise Exception("Memory store failed")
|
|
73
|
+
|
|
74
|
+
resp_data = response.data
|
|
75
|
+
return StoreMemoryResponse(
|
|
76
|
+
memory_id=resp_data["memoryId"],
|
|
77
|
+
storage_tier=resp_data["storageTier"],
|
|
78
|
+
size=resp_data["size"],
|
|
79
|
+
receipt_id=resp_data["receiptId"],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
async def retrieve(self, memory_id: str) -> RetrieveMemoryResponse:
|
|
83
|
+
"""
|
|
84
|
+
Retrieve encrypted memory per LLD §2.4
|
|
85
|
+
Cost: $0.005 (automatic 402 payment)
|
|
86
|
+
"""
|
|
87
|
+
if not memory_id:
|
|
88
|
+
raise ValueError("memory_id is required")
|
|
89
|
+
|
|
90
|
+
# Make API request with automatic 402 payment
|
|
91
|
+
response = await self.client.request_with_payment(
|
|
92
|
+
"POST",
|
|
93
|
+
"/v1/memory/retrieve",
|
|
94
|
+
{"memoryId": memory_id},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if not response.success or not response.data:
|
|
98
|
+
raise Exception("Memory retrieve failed")
|
|
99
|
+
|
|
100
|
+
resp_data = response.data
|
|
101
|
+
|
|
102
|
+
# Get encryption key
|
|
103
|
+
key = await self._get_encryption_key()
|
|
104
|
+
|
|
105
|
+
# Decrypt data client-side
|
|
106
|
+
decrypted_data = self._decrypt_data(resp_data["encryptedData"], key)
|
|
107
|
+
|
|
108
|
+
return RetrieveMemoryResponse(
|
|
109
|
+
memory_id=resp_data["memoryId"],
|
|
110
|
+
data=decrypted_data,
|
|
111
|
+
storage_tier=resp_data["storageTier"],
|
|
112
|
+
metadata=resp_data.get("metadata"),
|
|
113
|
+
receipt_id=resp_data["receiptId"],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
async def delete(self, memory_id: str) -> Dict[str, Any]:
|
|
117
|
+
"""Delete memory per LLD §2.4 (free)"""
|
|
118
|
+
if not memory_id:
|
|
119
|
+
raise ValueError("memory_id is required")
|
|
120
|
+
|
|
121
|
+
response = await self.client.request(
|
|
122
|
+
"DELETE",
|
|
123
|
+
f"/v1/memory/{memory_id}",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if not response.success or not response.data:
|
|
127
|
+
raise Exception("Memory delete failed")
|
|
128
|
+
|
|
129
|
+
return response.data
|
|
130
|
+
|
|
131
|
+
async def store_batch(
|
|
132
|
+
self,
|
|
133
|
+
items: List[Dict[str, Any]],
|
|
134
|
+
) -> BatchStoreMemoryResponse:
|
|
135
|
+
"""
|
|
136
|
+
Batch store encrypted memories per PRD FR-010, LLD §2.3
|
|
137
|
+
Max 100 items per batch
|
|
138
|
+
Cost: Single 402 payment for entire batch
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
```python
|
|
142
|
+
result = await client.memory.store_batch([
|
|
143
|
+
{"data": {"key": "value1"}, "storage_tier": "hot"},
|
|
144
|
+
{"data": {"key": "value2"}, "storage_tier": "warm"},
|
|
145
|
+
{"data": {"key": "value3"}, "storage_tier": "cold"},
|
|
146
|
+
])
|
|
147
|
+
|
|
148
|
+
print(f"Success: {result.success_count}, Failed: {result.failure_count}")
|
|
149
|
+
for r in result.results:
|
|
150
|
+
if r.memory_id:
|
|
151
|
+
print(f"Stored: {r.memory_id}")
|
|
152
|
+
else:
|
|
153
|
+
print(f"Failed: {r.error}")
|
|
154
|
+
```
|
|
155
|
+
"""
|
|
156
|
+
# Validate batch request
|
|
157
|
+
if not isinstance(items, list):
|
|
158
|
+
raise ValueError("items must be a list")
|
|
159
|
+
|
|
160
|
+
if len(items) == 0:
|
|
161
|
+
raise ValueError("items list cannot be empty")
|
|
162
|
+
|
|
163
|
+
if len(items) > 100:
|
|
164
|
+
raise ValueError("batch size exceeds maximum of 100 items")
|
|
165
|
+
|
|
166
|
+
# Validate each item
|
|
167
|
+
for idx, item in enumerate(items):
|
|
168
|
+
try:
|
|
169
|
+
self._validate_store_request(item.get("data"), item.get("storage_tier"))
|
|
170
|
+
except Exception as e:
|
|
171
|
+
raise ValueError(f"Invalid item at index {idx}: {str(e)}")
|
|
172
|
+
|
|
173
|
+
# Get encryption key
|
|
174
|
+
key = await self._get_encryption_key()
|
|
175
|
+
|
|
176
|
+
# Encrypt all items client-side
|
|
177
|
+
encrypted_items = []
|
|
178
|
+
for item in items:
|
|
179
|
+
encrypted_data = self._encrypt_data(item["data"], key)
|
|
180
|
+
encrypted_items.append({
|
|
181
|
+
"encryptedData": encrypted_data,
|
|
182
|
+
"storageTier": item["storage_tier"],
|
|
183
|
+
"metadata": item.get("metadata"),
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
# Make API request with automatic 402 payment
|
|
187
|
+
response = await self.client.request_with_payment(
|
|
188
|
+
"POST",
|
|
189
|
+
"/v1/memory/store/batch",
|
|
190
|
+
{"items": encrypted_items},
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if not response.success or not response.data:
|
|
194
|
+
raise Exception("Batch memory store failed")
|
|
195
|
+
|
|
196
|
+
resp_data = response.data
|
|
197
|
+
|
|
198
|
+
# Convert results to dataclass instances
|
|
199
|
+
results = [
|
|
200
|
+
BatchStoreMemoryResult(
|
|
201
|
+
index=r["index"],
|
|
202
|
+
memory_id=r.get("memoryId"),
|
|
203
|
+
receipt_id=r.get("receiptId"),
|
|
204
|
+
error=r.get("error"),
|
|
205
|
+
)
|
|
206
|
+
for r in resp_data["results"]
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
return BatchStoreMemoryResponse(
|
|
210
|
+
results=results,
|
|
211
|
+
success_count=resp_data["successCount"],
|
|
212
|
+
failure_count=resp_data["failureCount"],
|
|
213
|
+
batch_receipt_id=resp_data["batchReceiptId"],
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
async def retrieve_batch(
|
|
217
|
+
self,
|
|
218
|
+
memory_ids: List[str],
|
|
219
|
+
) -> BatchRetrieveMemoryResponse:
|
|
220
|
+
"""
|
|
221
|
+
Batch retrieve encrypted memories per PRD FR-011, LLD §2.3
|
|
222
|
+
Max 100 items per batch
|
|
223
|
+
Cost: Single 402 payment for entire batch
|
|
224
|
+
|
|
225
|
+
Example:
|
|
226
|
+
```python
|
|
227
|
+
result = await client.memory.retrieve_batch([
|
|
228
|
+
"mem_abc123",
|
|
229
|
+
"mem_def456",
|
|
230
|
+
"mem_ghi789",
|
|
231
|
+
])
|
|
232
|
+
|
|
233
|
+
print(f"Success: {result.success_count}, Failed: {result.failure_count}")
|
|
234
|
+
for r in result.results:
|
|
235
|
+
if r.data:
|
|
236
|
+
print(f"Retrieved: {r.memory_id}, {r.data}")
|
|
237
|
+
else:
|
|
238
|
+
print(f"Failed: {r.error}")
|
|
239
|
+
```
|
|
240
|
+
"""
|
|
241
|
+
# Validate batch request
|
|
242
|
+
if not isinstance(memory_ids, list):
|
|
243
|
+
raise ValueError("memory_ids must be a list")
|
|
244
|
+
|
|
245
|
+
if len(memory_ids) == 0:
|
|
246
|
+
raise ValueError("memory_ids list cannot be empty")
|
|
247
|
+
|
|
248
|
+
if len(memory_ids) > 100:
|
|
249
|
+
raise ValueError("batch size exceeds maximum of 100 items")
|
|
250
|
+
|
|
251
|
+
# Validate each memoryId
|
|
252
|
+
for idx, memory_id in enumerate(memory_ids):
|
|
253
|
+
if not memory_id or not isinstance(memory_id, str):
|
|
254
|
+
raise ValueError(f"Invalid memory_id at index {idx}")
|
|
255
|
+
|
|
256
|
+
# Make API request with automatic 402 payment
|
|
257
|
+
response = await self.client.request_with_payment(
|
|
258
|
+
"POST",
|
|
259
|
+
"/v1/memory/retrieve/batch",
|
|
260
|
+
{"memoryIds": memory_ids},
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if not response.success or not response.data:
|
|
264
|
+
raise Exception("Batch memory retrieve failed")
|
|
265
|
+
|
|
266
|
+
resp_data = response.data
|
|
267
|
+
|
|
268
|
+
# Get encryption key
|
|
269
|
+
key = await self._get_encryption_key()
|
|
270
|
+
|
|
271
|
+
# Decrypt all successfully retrieved items
|
|
272
|
+
results = []
|
|
273
|
+
for result in resp_data["results"]:
|
|
274
|
+
# If retrieval failed, return error as-is
|
|
275
|
+
if result.get("error") or not result.get("encryptedData"):
|
|
276
|
+
results.append(
|
|
277
|
+
BatchRetrieveMemoryResult(
|
|
278
|
+
index=result["index"],
|
|
279
|
+
memory_id=result.get("memoryId"),
|
|
280
|
+
error=result.get("error") or "No data returned",
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
# Decrypt the data
|
|
286
|
+
try:
|
|
287
|
+
decrypted_data = self._decrypt_data(result["encryptedData"], key)
|
|
288
|
+
results.append(
|
|
289
|
+
BatchRetrieveMemoryResult(
|
|
290
|
+
index=result["index"],
|
|
291
|
+
memory_id=result.get("memoryId"),
|
|
292
|
+
data=decrypted_data,
|
|
293
|
+
storage_tier=result.get("storageTier"),
|
|
294
|
+
metadata=result.get("metadata"),
|
|
295
|
+
receipt_id=result.get("receiptId"),
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
except Exception as e:
|
|
299
|
+
results.append(
|
|
300
|
+
BatchRetrieveMemoryResult(
|
|
301
|
+
index=result["index"],
|
|
302
|
+
memory_id=result.get("memoryId"),
|
|
303
|
+
error=f"Decryption failed: {str(e)}",
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return BatchRetrieveMemoryResponse(
|
|
308
|
+
results=results,
|
|
309
|
+
success_count=resp_data["successCount"],
|
|
310
|
+
failure_count=resp_data["failureCount"],
|
|
311
|
+
batch_receipt_id=resp_data["batchReceiptId"],
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def _validate_store_request(self, data: Dict[str, Any], storage_tier: str):
|
|
315
|
+
"""Validate store request"""
|
|
316
|
+
if not isinstance(data, dict):
|
|
317
|
+
raise ValueError("data must be a dictionary")
|
|
318
|
+
|
|
319
|
+
if storage_tier not in ["hot", "warm", "cold"]:
|
|
320
|
+
raise ValueError('storage_tier must be "hot", "warm", or "cold"')
|
|
321
|
+
|
|
322
|
+
# Validate data size
|
|
323
|
+
json_str = json.dumps(data)
|
|
324
|
+
if len(json_str) > 400:
|
|
325
|
+
raise ValueError("data too large (max ~400 characters)")
|
|
326
|
+
|
|
327
|
+
async def _get_encryption_key(self) -> bytes:
|
|
328
|
+
"""Get or derive encryption key"""
|
|
329
|
+
if self._encryption_key is None:
|
|
330
|
+
self._encryption_key = self._derive_encryption_key()
|
|
331
|
+
return self._encryption_key
|
|
332
|
+
|
|
333
|
+
def _derive_encryption_key(self) -> bytes:
|
|
334
|
+
"""
|
|
335
|
+
Derive encryption key from client configuration using PyNaCl
|
|
336
|
+
Uses BLAKE2b hash function for deterministic key derivation
|
|
337
|
+
"""
|
|
338
|
+
key_material = self.client.private_key + self.client.did
|
|
339
|
+
key_material_bytes = key_material.encode('utf-8')
|
|
340
|
+
|
|
341
|
+
# Use BLAKE2b for deterministic key derivation (32 bytes for SecretBox)
|
|
342
|
+
key = nacl.hash.blake2b(
|
|
343
|
+
key_material_bytes,
|
|
344
|
+
digest_size=nacl.secret.SecretBox.KEY_SIZE,
|
|
345
|
+
encoder=nacl.encoding.RawEncoder
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return key
|
|
349
|
+
|
|
350
|
+
def _encrypt_data(self, data: Dict[str, Any], key: bytes) -> str:
|
|
351
|
+
"""
|
|
352
|
+
Encrypt data using PyNaCl SecretBox (XSalsa20-Poly1305)
|
|
353
|
+
"""
|
|
354
|
+
import base64
|
|
355
|
+
|
|
356
|
+
# Convert data to JSON bytes
|
|
357
|
+
json_str = json.dumps(data)
|
|
358
|
+
json_bytes = json_str.encode('utf-8')
|
|
359
|
+
|
|
360
|
+
# Create SecretBox with key
|
|
361
|
+
box = nacl.secret.SecretBox(key)
|
|
362
|
+
|
|
363
|
+
# Encrypt (nonce is automatically generated and prepended)
|
|
364
|
+
encrypted = box.encrypt(json_bytes)
|
|
365
|
+
|
|
366
|
+
# Return base64 encoded ciphertext (includes nonce)
|
|
367
|
+
return base64.b64encode(encrypted).decode('utf-8')
|
|
368
|
+
|
|
369
|
+
def _decrypt_data(self, encrypted_data: str, key: bytes) -> Dict[str, Any]:
|
|
370
|
+
"""
|
|
371
|
+
Decrypt data using PyNaCl SecretBox
|
|
372
|
+
"""
|
|
373
|
+
import base64
|
|
374
|
+
|
|
375
|
+
# Decode from base64
|
|
376
|
+
encrypted_bytes = base64.b64decode(encrypted_data)
|
|
377
|
+
|
|
378
|
+
# Create SecretBox with key
|
|
379
|
+
box = nacl.secret.SecretBox(key)
|
|
380
|
+
|
|
381
|
+
# Decrypt (nonce is automatically extracted from ciphertext)
|
|
382
|
+
decrypted_bytes = box.decrypt(encrypted_bytes)
|
|
383
|
+
|
|
384
|
+
# Convert to JSON
|
|
385
|
+
json_str = decrypted_bytes.decode('utf-8')
|
|
386
|
+
return json.loads(json_str)
|
|
387
|
+
|
|
388
|
+
def set_encryption_key(self, key: bytes):
|
|
389
|
+
"""
|
|
390
|
+
Set custom encryption key
|
|
391
|
+
Useful for testing or using external key management
|
|
392
|
+
"""
|
|
393
|
+
if len(key) != nacl.secret.SecretBox.KEY_SIZE:
|
|
394
|
+
raise ValueError(f"Key must be {nacl.secret.SecretBox.KEY_SIZE} bytes")
|
|
395
|
+
self._encryption_key = key
|
|
396
|
+
|
|
397
|
+
async def get_current_encryption_key(self) -> bytes:
|
|
398
|
+
"""
|
|
399
|
+
Get current encryption key (for backup purposes)
|
|
400
|
+
"""
|
|
401
|
+
return await self._get_encryption_key()
|
xache/services/owner.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Owner Service - Owner identity registration and management
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional, Dict
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from ..types import DID
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Owner:
|
|
12
|
+
"""Owner profile information"""
|
|
13
|
+
owner_did: DID
|
|
14
|
+
wallet_address: str
|
|
15
|
+
wallet_chain: str # 'evm' | 'solana'
|
|
16
|
+
entity_type: str # 'human' | 'organization'
|
|
17
|
+
display_name: Optional[str]
|
|
18
|
+
organization_name: Optional[str]
|
|
19
|
+
email: Optional[str]
|
|
20
|
+
email_verified: bool
|
|
21
|
+
wallet_verified: bool
|
|
22
|
+
created_at: str
|
|
23
|
+
updated_at: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class OwnedAgent:
|
|
28
|
+
"""Owned agent information"""
|
|
29
|
+
agent_did: DID
|
|
30
|
+
wallet_address: str
|
|
31
|
+
chain: str
|
|
32
|
+
claim_type: str # 'individual' | 'workspace'
|
|
33
|
+
workspace_id: Optional[str]
|
|
34
|
+
workspace_name: Optional[str]
|
|
35
|
+
role: Optional[str] # 'admin' | 'member'
|
|
36
|
+
added_at: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class OwnerService:
|
|
40
|
+
"""
|
|
41
|
+
Owner service for identity registration and management
|
|
42
|
+
|
|
43
|
+
Register and manage owner identities for enterprise fleet management.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, client):
|
|
47
|
+
self.client = client
|
|
48
|
+
|
|
49
|
+
async def register(
|
|
50
|
+
self,
|
|
51
|
+
wallet_address: str,
|
|
52
|
+
wallet_chain: str,
|
|
53
|
+
entity_type: str,
|
|
54
|
+
display_name: Optional[str] = None,
|
|
55
|
+
organization_name: Optional[str] = None,
|
|
56
|
+
email: Optional[str] = None,
|
|
57
|
+
supabase_user_id: Optional[str] = None,
|
|
58
|
+
) -> Owner:
|
|
59
|
+
"""
|
|
60
|
+
Register a new owner (human or organization)
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
wallet_address: Wallet address
|
|
64
|
+
wallet_chain: Wallet chain ('evm' or 'solana')
|
|
65
|
+
entity_type: Entity type ('human' or 'organization')
|
|
66
|
+
display_name: Optional display name
|
|
67
|
+
organization_name: Optional organization name
|
|
68
|
+
email: Optional email address
|
|
69
|
+
supabase_user_id: Optional Supabase user ID
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Registered owner
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
```python
|
|
76
|
+
owner = await client.owner.register(
|
|
77
|
+
wallet_address="0x1234...",
|
|
78
|
+
wallet_chain="evm",
|
|
79
|
+
entity_type="human",
|
|
80
|
+
display_name="John Doe",
|
|
81
|
+
email="john@example.com"
|
|
82
|
+
)
|
|
83
|
+
print(f"Registered: {owner.owner_did}")
|
|
84
|
+
```
|
|
85
|
+
"""
|
|
86
|
+
body = {
|
|
87
|
+
"walletAddress": wallet_address,
|
|
88
|
+
"walletChain": wallet_chain,
|
|
89
|
+
"entityType": entity_type,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if display_name:
|
|
93
|
+
body["displayName"] = display_name
|
|
94
|
+
if organization_name:
|
|
95
|
+
body["organizationName"] = organization_name
|
|
96
|
+
if email:
|
|
97
|
+
body["email"] = email
|
|
98
|
+
if supabase_user_id:
|
|
99
|
+
body["supabaseUserId"] = supabase_user_id
|
|
100
|
+
|
|
101
|
+
response = await self.client.request(
|
|
102
|
+
"POST", "/v1/owners/register", body, skip_auth=True
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if not response.success or not response.data:
|
|
106
|
+
raise Exception(
|
|
107
|
+
response.error.get("message", "Failed to register owner")
|
|
108
|
+
if response.error
|
|
109
|
+
else "Failed to register owner"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return self._parse_owner(response.data.get("owner", response.data))
|
|
113
|
+
|
|
114
|
+
async def verify_wallet(
|
|
115
|
+
self,
|
|
116
|
+
owner_did: DID,
|
|
117
|
+
message: str,
|
|
118
|
+
signature: str,
|
|
119
|
+
timestamp: int,
|
|
120
|
+
) -> bool:
|
|
121
|
+
"""
|
|
122
|
+
Verify wallet ownership with cryptographic signature
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
owner_did: Owner DID
|
|
126
|
+
message: Message that was signed
|
|
127
|
+
signature: Signature
|
|
128
|
+
timestamp: Timestamp of signature
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
True if verified
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
```python
|
|
135
|
+
verified = await client.owner.verify_wallet(
|
|
136
|
+
owner_did="did:owner:evm:0x1234...",
|
|
137
|
+
message="Verify wallet ownership",
|
|
138
|
+
signature="0x...",
|
|
139
|
+
timestamp=1699900000
|
|
140
|
+
)
|
|
141
|
+
print(f"Verified: {verified}")
|
|
142
|
+
```
|
|
143
|
+
"""
|
|
144
|
+
body = {
|
|
145
|
+
"ownerDID": owner_did,
|
|
146
|
+
"message": message,
|
|
147
|
+
"signature": signature,
|
|
148
|
+
"timestamp": timestamp,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
response = await self.client.request(
|
|
152
|
+
"POST", "/v1/owners/verify-wallet", body, skip_auth=True
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if not response.success or response.data is None:
|
|
156
|
+
raise Exception(
|
|
157
|
+
response.error.get("message", "Failed to verify wallet")
|
|
158
|
+
if response.error
|
|
159
|
+
else "Failed to verify wallet"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return response.data.get("verified", False)
|
|
163
|
+
|
|
164
|
+
async def get_profile(self) -> Optional[Owner]:
|
|
165
|
+
"""
|
|
166
|
+
Get the authenticated owner's profile
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Owner profile or None if not found
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
```python
|
|
173
|
+
profile = await client.owner.get_profile()
|
|
174
|
+
if profile:
|
|
175
|
+
print(f"Display name: {profile.display_name}")
|
|
176
|
+
```
|
|
177
|
+
"""
|
|
178
|
+
response = await self.client.request("GET", "/v1/owners/me")
|
|
179
|
+
|
|
180
|
+
if not response.success:
|
|
181
|
+
raise Exception(
|
|
182
|
+
response.error.get("message", "Failed to get profile")
|
|
183
|
+
if response.error
|
|
184
|
+
else "Failed to get profile"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if not response.data or not response.data.get("owner"):
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
return self._parse_owner(response.data["owner"])
|
|
191
|
+
|
|
192
|
+
async def update_profile(
|
|
193
|
+
self,
|
|
194
|
+
display_name: Optional[str] = None,
|
|
195
|
+
organization_name: Optional[str] = None,
|
|
196
|
+
email: Optional[str] = None,
|
|
197
|
+
) -> Owner:
|
|
198
|
+
"""
|
|
199
|
+
Update the authenticated owner's profile
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
display_name: Optional new display name
|
|
203
|
+
organization_name: Optional new organization name
|
|
204
|
+
email: Optional new email
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Updated owner profile
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
```python
|
|
211
|
+
updated = await client.owner.update_profile(
|
|
212
|
+
display_name="Jane Doe",
|
|
213
|
+
organization_name="Acme Corp"
|
|
214
|
+
)
|
|
215
|
+
print(f"Updated: {updated.display_name}")
|
|
216
|
+
```
|
|
217
|
+
"""
|
|
218
|
+
body = {}
|
|
219
|
+
if display_name is not None:
|
|
220
|
+
body["displayName"] = display_name
|
|
221
|
+
if organization_name is not None:
|
|
222
|
+
body["organizationName"] = organization_name
|
|
223
|
+
if email is not None:
|
|
224
|
+
body["email"] = email
|
|
225
|
+
|
|
226
|
+
response = await self.client.request("PUT", "/v1/owners/me", body)
|
|
227
|
+
|
|
228
|
+
if not response.success or not response.data:
|
|
229
|
+
raise Exception(
|
|
230
|
+
response.error.get("message", "Failed to update profile")
|
|
231
|
+
if response.error
|
|
232
|
+
else "Failed to update profile"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return self._parse_owner(response.data.get("owner", response.data))
|
|
236
|
+
|
|
237
|
+
async def get_owned_agents(self) -> Dict:
|
|
238
|
+
"""
|
|
239
|
+
Get all agents owned by the authenticated owner
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Dictionary with agents and count
|
|
243
|
+
|
|
244
|
+
Example:
|
|
245
|
+
```python
|
|
246
|
+
result = await client.owner.get_owned_agents()
|
|
247
|
+
print(f"Total owned agents: {result['count']}")
|
|
248
|
+
|
|
249
|
+
for agent in result['agents']:
|
|
250
|
+
print(f"{agent.agent_did} - {agent.claim_type}")
|
|
251
|
+
```
|
|
252
|
+
"""
|
|
253
|
+
response = await self.client.request("GET", "/v1/owners/me/agents")
|
|
254
|
+
|
|
255
|
+
if not response.success or not response.data:
|
|
256
|
+
raise Exception(
|
|
257
|
+
response.error.get("message", "Failed to get owned agents")
|
|
258
|
+
if response.error
|
|
259
|
+
else "Failed to get owned agents"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
"agents": [
|
|
264
|
+
OwnedAgent(
|
|
265
|
+
agent_did=a["agentDID"],
|
|
266
|
+
wallet_address=a["walletAddress"],
|
|
267
|
+
chain=a["chain"],
|
|
268
|
+
claim_type=a["claimType"],
|
|
269
|
+
workspace_id=a.get("workspaceId"),
|
|
270
|
+
workspace_name=a.get("workspaceName"),
|
|
271
|
+
role=a.get("role"),
|
|
272
|
+
added_at=a["addedAt"],
|
|
273
|
+
)
|
|
274
|
+
for a in response.data.get("agents", [])
|
|
275
|
+
],
|
|
276
|
+
"count": response.data.get("count", 0),
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
def _parse_owner(self, data: dict) -> Owner:
|
|
280
|
+
"""Parse owner data into Owner object"""
|
|
281
|
+
return Owner(
|
|
282
|
+
owner_did=data["ownerDID"],
|
|
283
|
+
wallet_address=data["walletAddress"],
|
|
284
|
+
wallet_chain=data["walletChain"],
|
|
285
|
+
entity_type=data["entityType"],
|
|
286
|
+
display_name=data.get("displayName"),
|
|
287
|
+
organization_name=data.get("organizationName"),
|
|
288
|
+
email=data.get("email"),
|
|
289
|
+
email_verified=data.get("emailVerified", False),
|
|
290
|
+
wallet_verified=data.get("walletVerified", False),
|
|
291
|
+
created_at=data["createdAt"],
|
|
292
|
+
updated_at=data["updatedAt"],
|
|
293
|
+
)
|