memwal 0.2.1.dev0__py3-none-any.whl → 0.2.2.dev0__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.
- memwal/__init__.py +1 -1
- memwal/client.py +170 -23
- memwal/mock.py +10 -3
- memwal/types.py +7 -4
- memwal/utils.py +23 -10
- {memwal-0.2.1.dev0.dist-info → memwal-0.2.2.dev0.dist-info}/METADATA +11 -1
- memwal-0.2.2.dev0.dist-info/RECORD +10 -0
- memwal-0.2.1.dev0.dist-info/RECORD +0 -10
- {memwal-0.2.1.dev0.dist-info → memwal-0.2.2.dev0.dist-info}/WHEEL +0 -0
memwal/__init__.py
CHANGED
memwal/client.py
CHANGED
|
@@ -437,6 +437,12 @@ def _check_vector(vector: Any) -> None:
|
|
|
437
437
|
):
|
|
438
438
|
raise ValueError("vector must be a non-empty list of finite numbers")
|
|
439
439
|
|
|
440
|
+
def _destroyed_client_error() -> "MemWalError":
|
|
441
|
+
return MemWalError(
|
|
442
|
+
"Walrus Memory client was destroyed; its keys are zeroed. Create a new one."
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
|
|
440
446
|
class MemWal:
|
|
441
447
|
"""Async-native Walrus Memory client.
|
|
442
448
|
|
|
@@ -445,8 +451,13 @@ class MemWal:
|
|
|
445
451
|
"""
|
|
446
452
|
|
|
447
453
|
def __init__(self, config: MemWalConfig) -> None:
|
|
448
|
-
self.
|
|
449
|
-
self.
|
|
454
|
+
self._destroyed: bool = False
|
|
455
|
+
self._private_key_hex: str = normalize_private_key(config.key)
|
|
456
|
+
signing_key = build_signing_key(self._private_key_hex)
|
|
457
|
+
self._signing_key: Optional[nacl.signing.SigningKey] = signing_key
|
|
458
|
+
# Mutable copy of the 32-byte seed so destroy() can zero-fill it in
|
|
459
|
+
# place without mutating any caller-supplied bytearray (#975).
|
|
460
|
+
self._private_key_bytes: bytearray = bytearray(bytes(signing_key))
|
|
450
461
|
self._account_id = config.account_id
|
|
451
462
|
self._server_url = normalize_server_url(config.server_url)
|
|
452
463
|
self._namespace = config.namespace
|
|
@@ -473,7 +484,7 @@ class MemWal:
|
|
|
473
484
|
@classmethod
|
|
474
485
|
def create(
|
|
475
486
|
cls,
|
|
476
|
-
key: str,
|
|
487
|
+
key: Union[str, bytes, bytearray],
|
|
477
488
|
account_id: str,
|
|
478
489
|
server_url: Optional[str] = None,
|
|
479
490
|
namespace: str = "default",
|
|
@@ -482,7 +493,10 @@ class MemWal:
|
|
|
482
493
|
"""Create a new Walrus Memory client instance.
|
|
483
494
|
|
|
484
495
|
Args:
|
|
485
|
-
key: Ed25519 private key hex string
|
|
496
|
+
key: Ed25519 private key (hex string, ``suiprivkey1...``, or 32-byte
|
|
497
|
+
``bytes`` / ``bytearray``). When passed as ``bytearray``, the
|
|
498
|
+
bytes are copied into an internal buffer so :meth:`destroy`
|
|
499
|
+
zeroes only the client's copy and leaves the caller's buffer intact.
|
|
486
500
|
account_id: Walrus Memory account object ID on Sui.
|
|
487
501
|
server_url: Server URL (default: ``https://relayer.memory.walrus.xyz``).
|
|
488
502
|
namespace: Default namespace for memory isolation (default: ``"default"``).
|
|
@@ -510,8 +524,65 @@ class MemWal:
|
|
|
510
524
|
self._client = httpx.AsyncClient(timeout=60.0)
|
|
511
525
|
return self._client
|
|
512
526
|
|
|
513
|
-
|
|
514
|
-
"""
|
|
527
|
+
def destroy(self) -> None:
|
|
528
|
+
"""Wipe internal key buffers, drop signing credentials, and invalidate the client.
|
|
529
|
+
|
|
530
|
+
Provides parity with the TypeScript SDK's ``destroy()`` method (#975):
|
|
531
|
+
|
|
532
|
+
- Zeroes ``_private_key_bytes`` (the client's internal ``bytearray`` seed
|
|
533
|
+
copy) in place.
|
|
534
|
+
- Clears ``_private_key_hex``, drops the ``_signing_key`` reference, and
|
|
535
|
+
clears cached SEAL session tokens, server config, owner address, and
|
|
536
|
+
pending idempotency keys.
|
|
537
|
+
- Marks the client as destroyed so any subsequent or in-flight operation
|
|
538
|
+
raises :class:`MemWalError`.
|
|
539
|
+
|
|
540
|
+
Note on CPython memory semantics:
|
|
541
|
+
Python ``str`` and ``bytes`` objects (and PyNaCl's internal C-level
|
|
542
|
+
``SigningKey`` buffers) are immutable in CPython and cannot be zeroed
|
|
543
|
+
in place from pure Python. ``destroy()`` zeroes the client's mutable
|
|
544
|
+
``bytearray`` seed copy in place and drops all references to immutable
|
|
545
|
+
key objects so they become eligible for garbage collection once the
|
|
546
|
+
caller also releases its references. Callers that want to minimize
|
|
547
|
+
long-lived plaintext ``str`` keys in memory should pass ``key`` as a
|
|
548
|
+
32-byte ``bytearray`` and zero their own buffer after ``create()``.
|
|
549
|
+
"""
|
|
550
|
+
self._destroyed = True
|
|
551
|
+
if self._private_key_bytes:
|
|
552
|
+
self._private_key_bytes[:] = b"\x00" * len(self._private_key_bytes)
|
|
553
|
+
self._private_key_hex = ""
|
|
554
|
+
self._signing_key = None
|
|
555
|
+
self._session_cache = None
|
|
556
|
+
self._server_config = None
|
|
557
|
+
self._relayer_version_metadata = None
|
|
558
|
+
self._owner_address = None
|
|
559
|
+
self._pending_remember_keys.clear()
|
|
560
|
+
for attr in ("_session_build_task", "_owner_task"):
|
|
561
|
+
task = getattr(self, attr, None)
|
|
562
|
+
setattr(self, attr, None)
|
|
563
|
+
if task is not None and not task.done():
|
|
564
|
+
task.cancel()
|
|
565
|
+
|
|
566
|
+
async def close(self, clear_keys: bool = False) -> None:
|
|
567
|
+
"""Close the underlying HTTP client and cancel any in-flight initialization tasks.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
clear_keys: If ``True``, also call :meth:`destroy` to wipe internal
|
|
571
|
+
key buffers, drop signing credentials, and invalidate the client.
|
|
572
|
+
"""
|
|
573
|
+
for attr in ("_session_build_task", "_owner_task"):
|
|
574
|
+
task = getattr(self, attr, None)
|
|
575
|
+
setattr(self, attr, None)
|
|
576
|
+
if task is not None and not task.done():
|
|
577
|
+
task.cancel()
|
|
578
|
+
try:
|
|
579
|
+
await task
|
|
580
|
+
except (asyncio.CancelledError, Exception):
|
|
581
|
+
# Ignore cancellation or transport errors from in-flight
|
|
582
|
+
# session tasks during teardown.
|
|
583
|
+
pass
|
|
584
|
+
if clear_keys:
|
|
585
|
+
self.destroy()
|
|
515
586
|
if self._client is not None and not self._client.is_closed:
|
|
516
587
|
await self._client.aclose()
|
|
517
588
|
self._client = None
|
|
@@ -1443,7 +1514,11 @@ class MemWal:
|
|
|
1443
1514
|
Returns:
|
|
1444
1515
|
:class:`HealthResult` with status and version.
|
|
1445
1516
|
"""
|
|
1517
|
+
if self._destroyed:
|
|
1518
|
+
raise _destroyed_client_error()
|
|
1446
1519
|
response = await self._http.get(f"{self._server_url}/health")
|
|
1520
|
+
if self._destroyed:
|
|
1521
|
+
raise _destroyed_client_error()
|
|
1447
1522
|
if response.status_code != 200:
|
|
1448
1523
|
raise MemWalError(f"Health check failed: {response.status_code}")
|
|
1449
1524
|
data = response.json()
|
|
@@ -1462,7 +1537,8 @@ class MemWal:
|
|
|
1462
1537
|
|
|
1463
1538
|
async def compatibility(self) -> Dict[str, Any]:
|
|
1464
1539
|
"""Fetch and validate the relayer compatibility contract."""
|
|
1465
|
-
|
|
1540
|
+
if self._destroyed:
|
|
1541
|
+
raise _destroyed_client_error()
|
|
1466
1542
|
return await self._ensure_compatible_relayer()
|
|
1467
1543
|
|
|
1468
1544
|
# ============================================================
|
|
@@ -1545,6 +1621,8 @@ class MemWal:
|
|
|
1545
1621
|
Returns:
|
|
1546
1622
|
Public key hex string.
|
|
1547
1623
|
"""
|
|
1624
|
+
if self._destroyed or self._signing_key is None:
|
|
1625
|
+
raise _destroyed_client_error()
|
|
1548
1626
|
return bytes_to_hex(bytes(self._signing_key.verify_key))
|
|
1549
1627
|
|
|
1550
1628
|
# ============================================================
|
|
@@ -1552,6 +1630,8 @@ class MemWal:
|
|
|
1552
1630
|
# ============================================================
|
|
1553
1631
|
|
|
1554
1632
|
async def _ensure_compatible_relayer(self) -> Dict[str, Any]:
|
|
1633
|
+
if self._destroyed:
|
|
1634
|
+
raise _destroyed_client_error()
|
|
1555
1635
|
if self._relayer_version_metadata is not None:
|
|
1556
1636
|
return self._relayer_version_metadata
|
|
1557
1637
|
|
|
@@ -1559,14 +1639,20 @@ class MemWal:
|
|
|
1559
1639
|
self._compatibility_lock = asyncio.Lock()
|
|
1560
1640
|
|
|
1561
1641
|
async with self._compatibility_lock:
|
|
1642
|
+
if self._destroyed:
|
|
1643
|
+
raise _destroyed_client_error()
|
|
1562
1644
|
if self._relayer_version_metadata is not None:
|
|
1563
1645
|
return self._relayer_version_metadata
|
|
1564
1646
|
|
|
1565
1647
|
version_response = await self._http.get(f"{self._server_url}/version")
|
|
1648
|
+
if self._destroyed:
|
|
1649
|
+
raise _destroyed_client_error()
|
|
1566
1650
|
if version_response.status_code == 200:
|
|
1567
1651
|
metadata = version_response.json()
|
|
1568
1652
|
elif version_response.status_code in (404, 405):
|
|
1569
1653
|
health_response = await self._http.get(f"{self._server_url}/health")
|
|
1654
|
+
if self._destroyed:
|
|
1655
|
+
raise _destroyed_client_error()
|
|
1570
1656
|
if health_response.status_code != 200:
|
|
1571
1657
|
raise MemWalError(
|
|
1572
1658
|
"Walrus Memory compatibility check failed: "
|
|
@@ -1684,10 +1770,16 @@ class MemWal:
|
|
|
1684
1770
|
)
|
|
1685
1771
|
|
|
1686
1772
|
async def _build_seal_session_inner(self) -> str:
|
|
1773
|
+
if self._destroyed or self._signing_key is None:
|
|
1774
|
+
raise _destroyed_client_error()
|
|
1687
1775
|
cfg = await self._fetch_server_config()
|
|
1776
|
+
if self._destroyed or self._signing_key is None:
|
|
1777
|
+
raise _destroyed_client_error()
|
|
1688
1778
|
await self._assert_first_package_version(
|
|
1689
1779
|
cfg["network"], cfg["suiRpcUrl"], cfg["packageId"]
|
|
1690
1780
|
)
|
|
1781
|
+
if self._destroyed or self._signing_key is None:
|
|
1782
|
+
raise _destroyed_client_error()
|
|
1691
1783
|
|
|
1692
1784
|
session_signing_key = nacl.signing.SigningKey.generate()
|
|
1693
1785
|
session_public_key = bytes(session_signing_key.verify_key)
|
|
@@ -1716,6 +1808,8 @@ class MemWal:
|
|
|
1716
1808
|
separators=(",", ":"),
|
|
1717
1809
|
)
|
|
1718
1810
|
session_bytes = base64.b64encode(json_str.encode("utf-8")).decode("utf-8")
|
|
1811
|
+
if self._destroyed:
|
|
1812
|
+
raise _destroyed_client_error()
|
|
1719
1813
|
self._session_cache = (
|
|
1720
1814
|
session_bytes,
|
|
1721
1815
|
int(time.time() * 1000) + SEAL_SESSION_TTL_MIN * 60_000 - SEAL_SESSION_SAFETY_MARGIN_MS,
|
|
@@ -1723,22 +1817,40 @@ class MemWal:
|
|
|
1723
1817
|
return session_bytes
|
|
1724
1818
|
|
|
1725
1819
|
async def _build_seal_session(self) -> str:
|
|
1820
|
+
if self._destroyed:
|
|
1821
|
+
raise _destroyed_client_error()
|
|
1726
1822
|
now_ms = int(time.time() * 1000)
|
|
1727
1823
|
if self._session_cache is not None:
|
|
1728
1824
|
cached_bytes, expires_at_ms = self._session_cache
|
|
1729
1825
|
if now_ms < expires_at_ms:
|
|
1730
1826
|
return cached_bytes
|
|
1731
1827
|
|
|
1732
|
-
|
|
1733
|
-
|
|
1828
|
+
task = self._session_build_task
|
|
1829
|
+
if task is None or task.done():
|
|
1830
|
+
task = asyncio.create_task(self._build_seal_session_inner())
|
|
1831
|
+
self._session_build_task = task
|
|
1832
|
+
|
|
1833
|
+
def _on_session_build_done(done_task: "asyncio.Task[str]") -> None:
|
|
1834
|
+
if self._session_build_task is done_task:
|
|
1835
|
+
self._session_build_task = None
|
|
1836
|
+
if not done_task.cancelled():
|
|
1837
|
+
done_task.exception()
|
|
1838
|
+
|
|
1839
|
+
task.add_done_callback(_on_session_build_done)
|
|
1734
1840
|
|
|
1735
|
-
self._session_build_task = asyncio.create_task(self._build_seal_session_inner())
|
|
1736
1841
|
try:
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
self.
|
|
1842
|
+
result = await asyncio.shield(task)
|
|
1843
|
+
except asyncio.CancelledError:
|
|
1844
|
+
if self._destroyed:
|
|
1845
|
+
raise _destroyed_client_error() from None
|
|
1846
|
+
raise
|
|
1847
|
+
if self._destroyed:
|
|
1848
|
+
raise _destroyed_client_error()
|
|
1849
|
+
return result
|
|
1740
1850
|
|
|
1741
1851
|
async def _resolve_owner_inner(self) -> str:
|
|
1852
|
+
if self._destroyed:
|
|
1853
|
+
raise _destroyed_client_error()
|
|
1742
1854
|
# POST /api/stats authenticates with the same delegate scheme and
|
|
1743
1855
|
# returns the owner the relayer resolved from our key. Same approach
|
|
1744
1856
|
# as the TypeScript SDK's resolveOwner().
|
|
@@ -1748,6 +1860,8 @@ class MemWal:
|
|
|
1748
1860
|
{"namespace": self._namespace},
|
|
1749
1861
|
include_seal_session=False,
|
|
1750
1862
|
)
|
|
1863
|
+
if self._destroyed:
|
|
1864
|
+
raise _destroyed_client_error()
|
|
1751
1865
|
owner = data.get("owner")
|
|
1752
1866
|
if not owner:
|
|
1753
1867
|
raise MemWalError(
|
|
@@ -1763,17 +1877,33 @@ class MemWal:
|
|
|
1763
1877
|
The owner-scoped read routes take the address in the path, but the
|
|
1764
1878
|
client is configured with only a delegate key and account id.
|
|
1765
1879
|
"""
|
|
1880
|
+
if self._destroyed:
|
|
1881
|
+
raise _destroyed_client_error()
|
|
1766
1882
|
if self._owner_address is not None:
|
|
1767
1883
|
return self._owner_address
|
|
1768
1884
|
|
|
1769
|
-
|
|
1770
|
-
|
|
1885
|
+
task = self._owner_task
|
|
1886
|
+
if task is None or task.done():
|
|
1887
|
+
task = asyncio.create_task(self._resolve_owner_inner())
|
|
1888
|
+
self._owner_task = task
|
|
1889
|
+
|
|
1890
|
+
def _on_owner_done(done_task: "asyncio.Task[str]") -> None:
|
|
1891
|
+
if self._owner_task is done_task:
|
|
1892
|
+
self._owner_task = None
|
|
1893
|
+
if not done_task.cancelled():
|
|
1894
|
+
done_task.exception()
|
|
1895
|
+
|
|
1896
|
+
task.add_done_callback(_on_owner_done)
|
|
1771
1897
|
|
|
1772
|
-
self._owner_task = asyncio.create_task(self._resolve_owner_inner())
|
|
1773
1898
|
try:
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
self.
|
|
1899
|
+
result = await asyncio.shield(task)
|
|
1900
|
+
except asyncio.CancelledError:
|
|
1901
|
+
if self._destroyed:
|
|
1902
|
+
raise _destroyed_client_error() from None
|
|
1903
|
+
raise
|
|
1904
|
+
if self._destroyed:
|
|
1905
|
+
raise _destroyed_client_error()
|
|
1906
|
+
return result
|
|
1777
1907
|
|
|
1778
1908
|
async def _signed_request(
|
|
1779
1909
|
self,
|
|
@@ -1803,8 +1933,12 @@ class MemWal:
|
|
|
1803
1933
|
- ``x-account-id``: Walrus Memory account object ID
|
|
1804
1934
|
- ``Content-Type``: application/json
|
|
1805
1935
|
"""
|
|
1936
|
+
if self._destroyed or self._signing_key is None:
|
|
1937
|
+
raise _destroyed_client_error()
|
|
1806
1938
|
|
|
1807
1939
|
await self._ensure_compatible_relayer()
|
|
1940
|
+
if self._destroyed or self._signing_key is None:
|
|
1941
|
+
raise _destroyed_client_error()
|
|
1808
1942
|
|
|
1809
1943
|
method_upper = method.upper()
|
|
1810
1944
|
timestamp = str(int(time.time()))
|
|
@@ -1838,6 +1972,8 @@ class MemWal:
|
|
|
1838
1972
|
}
|
|
1839
1973
|
if include_seal_session:
|
|
1840
1974
|
headers["x-seal-session"] = await self._build_seal_session()
|
|
1975
|
+
if self._destroyed or self._signing_key is None:
|
|
1976
|
+
raise _destroyed_client_error()
|
|
1841
1977
|
|
|
1842
1978
|
response = await self._http.request(
|
|
1843
1979
|
method=method_upper,
|
|
@@ -1846,6 +1982,8 @@ class MemWal:
|
|
|
1846
1982
|
content=None if method_upper == "GET" else body_str,
|
|
1847
1983
|
timeout=timeout,
|
|
1848
1984
|
)
|
|
1985
|
+
if self._destroyed or self._signing_key is None:
|
|
1986
|
+
raise _destroyed_client_error()
|
|
1849
1987
|
|
|
1850
1988
|
if response.status_code not in accepted_statuses:
|
|
1851
1989
|
err_text = response.text
|
|
@@ -2073,7 +2211,7 @@ class MemWalSync:
|
|
|
2073
2211
|
@classmethod
|
|
2074
2212
|
def create(
|
|
2075
2213
|
cls,
|
|
2076
|
-
key: str,
|
|
2214
|
+
key: Union[str, bytes, bytearray],
|
|
2077
2215
|
account_id: str,
|
|
2078
2216
|
server_url: Optional[str] = None,
|
|
2079
2217
|
namespace: str = "default",
|
|
@@ -2278,9 +2416,18 @@ class MemWalSync:
|
|
|
2278
2416
|
"""Synchronous version of :meth:`MemWal.get_public_key_hex`."""
|
|
2279
2417
|
return self._run(self._inner.get_public_key_hex())
|
|
2280
2418
|
|
|
2281
|
-
def
|
|
2282
|
-
"""
|
|
2283
|
-
self.
|
|
2419
|
+
def destroy(self) -> None:
|
|
2420
|
+
"""Synchronous version of :meth:`MemWal.destroy`."""
|
|
2421
|
+
self._inner.destroy()
|
|
2422
|
+
|
|
2423
|
+
def close(self, clear_keys: bool = False) -> None:
|
|
2424
|
+
"""Close the underlying HTTP client.
|
|
2425
|
+
|
|
2426
|
+
Args:
|
|
2427
|
+
clear_keys: If ``True``, also call :meth:`destroy` to wipe internal
|
|
2428
|
+
key buffers, drop signing credentials, and invalidate the client.
|
|
2429
|
+
"""
|
|
2430
|
+
self._run(self._inner.close(clear_keys=clear_keys))
|
|
2284
2431
|
|
|
2285
2432
|
def __enter__(self) -> "MemWalSync":
|
|
2286
2433
|
return self
|
memwal/mock.py
CHANGED
|
@@ -129,9 +129,13 @@ class MemWalMock:
|
|
|
129
129
|
) -> "MemWalMock":
|
|
130
130
|
return cls(namespace, owner, initial_memories)
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
def destroy(self) -> None:
|
|
133
133
|
"""No-op for API parity with :class:`MemWal`."""
|
|
134
134
|
|
|
135
|
+
async def close(self, clear_keys: bool = False) -> None:
|
|
136
|
+
"""No-op for API parity with :class:`MemWal`."""
|
|
137
|
+
del clear_keys
|
|
138
|
+
|
|
135
139
|
async def __aenter__(self) -> "MemWalMock":
|
|
136
140
|
return self
|
|
137
141
|
|
|
@@ -714,8 +718,11 @@ class MemWalMockSync:
|
|
|
714
718
|
def clear(self, namespace: Optional[str] = None) -> int:
|
|
715
719
|
return self._inner.clear(namespace)
|
|
716
720
|
|
|
717
|
-
def
|
|
718
|
-
self.
|
|
721
|
+
def destroy(self) -> None:
|
|
722
|
+
self._inner.destroy()
|
|
723
|
+
|
|
724
|
+
def close(self, clear_keys: bool = False) -> None:
|
|
725
|
+
self._run(self._inner.close(clear_keys=clear_keys))
|
|
719
726
|
|
|
720
727
|
def __enter__(self) -> "MemWalMockSync":
|
|
721
728
|
return self
|
memwal/types.py
CHANGED
|
@@ -9,7 +9,7 @@ the Walrus Memory Rust server (TEE).
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
from dataclasses import dataclass
|
|
12
|
-
from typing import Any, Dict, List, Optional
|
|
12
|
+
from typing import Any, Dict, List, Optional, Union
|
|
13
13
|
|
|
14
14
|
# ============================================================
|
|
15
15
|
# Config
|
|
@@ -34,8 +34,11 @@ class MemWalConfig:
|
|
|
34
34
|
"""Configuration for creating a Walrus Memory client.
|
|
35
35
|
|
|
36
36
|
Attributes:
|
|
37
|
-
key: Ed25519 private key (hex string
|
|
38
|
-
|
|
37
|
+
key: Ed25519 private key (hex string, ``suiprivkey1...``, or 32-byte
|
|
38
|
+
``bytes`` / ``bytearray``). This is the delegate key from the
|
|
39
|
+
Walrus Memory dashboard. When passed as ``bytearray``, the client
|
|
40
|
+
copies the bytes into its own internal buffer so :meth:`MemWal.destroy`
|
|
41
|
+
zeroes only the client's copy.
|
|
39
42
|
account_id: Walrus Memory account object ID on Sui.
|
|
40
43
|
server_url: Server URL. ``None`` means omitted. Any explicit URL,
|
|
41
44
|
including the hosted default, wins over ``env``.
|
|
@@ -46,7 +49,7 @@ class MemWalConfig:
|
|
|
46
49
|
> ``DEFAULT_SERVER_URL``.
|
|
47
50
|
"""
|
|
48
51
|
|
|
49
|
-
key: str
|
|
52
|
+
key: Union[str, bytes, bytearray]
|
|
50
53
|
account_id: str
|
|
51
54
|
server_url: Optional[str] = None
|
|
52
55
|
namespace: str = "default"
|
memwal/utils.py
CHANGED
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
import base64
|
|
11
11
|
import hashlib
|
|
12
12
|
from datetime import datetime, timezone
|
|
13
|
-
from typing import Tuple
|
|
13
|
+
from typing import Tuple, Union
|
|
14
14
|
|
|
15
15
|
import nacl.signing
|
|
16
16
|
|
|
@@ -51,7 +51,7 @@ def hex_to_bytes(hex_str: str) -> bytes:
|
|
|
51
51
|
return bytes.fromhex(clean)
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
def bytes_to_hex(b: bytes) -> str:
|
|
54
|
+
def bytes_to_hex(b: Union[bytes, bytearray]) -> str:
|
|
55
55
|
"""Convert bytes to a lowercase hex string (no ``0x`` prefix).
|
|
56
56
|
|
|
57
57
|
Args:
|
|
@@ -75,11 +75,14 @@ def sha256_hex(data: str) -> str:
|
|
|
75
75
|
return hashlib.sha256(data.encode("utf-8")).hexdigest()
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def build_signing_key(
|
|
79
|
-
|
|
78
|
+
def build_signing_key(
|
|
79
|
+
private_key: Union[str, bytes, bytearray]
|
|
80
|
+
) -> nacl.signing.SigningKey:
|
|
81
|
+
"""Build a PyNaCl ``SigningKey`` from an Ed25519 private key hex string or 32-byte buffer.
|
|
80
82
|
|
|
81
83
|
Args:
|
|
82
|
-
|
|
84
|
+
private_key: 32-byte Ed25519 seed as hex (64 hex chars, optionally ``0x``-prefixed)
|
|
85
|
+
or raw 32-byte ``bytes`` / ``bytearray``.
|
|
83
86
|
|
|
84
87
|
Returns:
|
|
85
88
|
A ``nacl.signing.SigningKey`` instance.
|
|
@@ -87,7 +90,10 @@ def build_signing_key(private_key_hex: str) -> nacl.signing.SigningKey:
|
|
|
87
90
|
Raises:
|
|
88
91
|
ValueError: If the decoded seed is not exactly 32 bytes.
|
|
89
92
|
"""
|
|
90
|
-
|
|
93
|
+
if isinstance(private_key, (bytes, bytearray)):
|
|
94
|
+
seed_bytes = bytes(private_key)
|
|
95
|
+
else:
|
|
96
|
+
seed_bytes = hex_to_bytes(private_key)
|
|
91
97
|
if len(seed_bytes) != 32:
|
|
92
98
|
raise ValueError(
|
|
93
99
|
f"Ed25519 seed must be exactly 32 bytes, got {len(seed_bytes)}"
|
|
@@ -290,12 +296,19 @@ def decode_sui_private_key(encoded: str) -> bytes:
|
|
|
290
296
|
return seed
|
|
291
297
|
|
|
292
298
|
|
|
293
|
-
def normalize_private_key(key: str) -> str:
|
|
294
|
-
"""Accept
|
|
299
|
+
def normalize_private_key(key: Union[str, bytes, bytearray]) -> str:
|
|
300
|
+
"""Accept a hex seed, ``suiprivkey1...`` string, or 32-byte ``bytes``/``bytearray``; return hex.
|
|
295
301
|
|
|
296
|
-
The TypeScript SDK
|
|
297
|
-
so hex-only input would reject a key a user reasonably expects to work.
|
|
302
|
+
The TypeScript SDK accepts hex, ``suiprivkey1...``, and 32-byte ``Uint8Array``.
|
|
298
303
|
"""
|
|
304
|
+
if isinstance(key, (bytes, bytearray)):
|
|
305
|
+
if len(key) != 32:
|
|
306
|
+
raise ValueError(f"Ed25519 seed must be exactly 32 bytes, got {len(key)}")
|
|
307
|
+
return bytes_to_hex(key)
|
|
308
|
+
if not isinstance(key, str):
|
|
309
|
+
raise TypeError(
|
|
310
|
+
f"key must be str, bytes, or bytearray, got {type(key).__name__}"
|
|
311
|
+
)
|
|
299
312
|
candidate = key.strip()
|
|
300
313
|
if candidate.lower().startswith("suiprivkey1"):
|
|
301
314
|
return bytes_to_hex(decode_sui_private_key(candidate))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: memwal
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2.dev0
|
|
4
4
|
Summary: Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing
|
|
5
5
|
Project-URL: Homepage, https://memory.walrus.xyz
|
|
6
6
|
Project-URL: Documentation, https://memory.walrus.xyz
|
|
@@ -259,6 +259,16 @@ Create a new async client.
|
|
|
259
259
|
| `await remember_manual(opts)` | Store encrypted payload + pre-computed vector |
|
|
260
260
|
| `await recall_manual(opts)` | Search with pre-computed vector |
|
|
261
261
|
| `await get_public_key_hex()` | Get Ed25519 public key |
|
|
262
|
+
| `destroy()` | Zero internal key seed buffer, drop signing credentials/session cache, and permanently invalidate the client |
|
|
263
|
+
| `await close(clear_keys=False)` | Close the underlying HTTP client (pass `clear_keys=True` to also run `destroy()`) |
|
|
264
|
+
|
|
265
|
+
### Key Lifecycle & `destroy()`
|
|
266
|
+
|
|
267
|
+
For parity with the TypeScript SDK's `destroy()`, `MemWal` and `MemWalSync` expose `destroy()` and `close(clear_keys=True)`:
|
|
268
|
+
|
|
269
|
+
- `key` may be passed as a hex string, `suiprivkey1...` string, or a 32-byte `bytes` / `bytearray`. When passed as `bytearray`, the SDK copies the bytes into its own internal buffer so `destroy()` zeroes only the client's copy and leaves the caller's `bytearray` untouched.
|
|
270
|
+
- Calling `destroy()` (or `await client.close(clear_keys=True)` / `sync_client.close(clear_keys=True)`) zero-fills the client's internal `bytearray` seed copy in place, clears `_private_key_hex`, drops the `_signing_key` and cached SEAL session references, cancels any in-flight initialization tasks, and causes subsequent or in-flight calls to raise `MemWalError`.
|
|
271
|
+
- **CPython memory semantics**: Python `str` and `bytes` objects (and PyNaCl's internal C-level `SigningKey` buffers) are immutable in CPython and cannot be overwritten in place from pure Python. `destroy()` zeroes the mutable `bytearray` copy and drops all references to immutable key objects so they become eligible for garbage collection once the caller also releases its references. To minimize plaintext `str` copies in process memory, pass a 32-byte `bytearray` to `MemWal.create()` and zero your own buffer after creation.
|
|
262
272
|
|
|
263
273
|
## Authentication
|
|
264
274
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
memwal/__init__.py,sha256=G_2AYOMiAWMZ_G89OKlHPYV63KXxp--oMGC0cYV9r1c,3142
|
|
2
|
+
memwal/client.py,sha256=l_V6fcUyXq9r1qV8sHw8xlSc8-Q50ebTtDuP4rscq8c,95076
|
|
3
|
+
memwal/compatibility.py,sha256=FsKApCcHOFZeIeNOph3YydWVnZtMJdT0KCt0e6jylho,2799
|
|
4
|
+
memwal/middleware.py,sha256=z2xdbvaobBg4byJXnrX7amWdVYE4jfeO4t31DoDBv_o,38413
|
|
5
|
+
memwal/mock.py,sha256=cB-mu2yqycvMCVMmgqTTsIj_pGSkHufCyLNbfkPdHEE,24262
|
|
6
|
+
memwal/types.py,sha256=fPvxU4k14qmj577sMtGZyVaGhwXxORtyXL-TxD0P6LI,14002
|
|
7
|
+
memwal/utils.py,sha256=YyLuxPjKB0RksL1ZGPORpW7QrSID0vQQABq3L_64zKw,12596
|
|
8
|
+
memwal-0.2.2.dev0.dist-info/METADATA,sha256=11TlxEsj6VTIT3FxMNJBr9UqstdqvYlIp8jduI83LV4,11923
|
|
9
|
+
memwal-0.2.2.dev0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
10
|
+
memwal-0.2.2.dev0.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
memwal/__init__.py,sha256=IAJWdWRnUVTD5bZN7EVWQ6Q511j27npZgD5aG_DMltk,3142
|
|
2
|
-
memwal/client.py,sha256=GryxHSCyGjDmlWOTBSJp7J7ItopwUs0D1JdynrgjYTk,88398
|
|
3
|
-
memwal/compatibility.py,sha256=FsKApCcHOFZeIeNOph3YydWVnZtMJdT0KCt0e6jylho,2799
|
|
4
|
-
memwal/middleware.py,sha256=z2xdbvaobBg4byJXnrX7amWdVYE4jfeO4t31DoDBv_o,38413
|
|
5
|
-
memwal/mock.py,sha256=5bXVej1Xs1a3o1tLL0Vvr1QB9IXyRUTBM7qooTEtysQ,24015
|
|
6
|
-
memwal/types.py,sha256=xvs9BqFLXDsfkHp3-8Bd_Pv5RhDDWOKr0-bUsvic1P4,13732
|
|
7
|
-
memwal/utils.py,sha256=Dyv-DWIMwivtYLxruHXlNXE9i8hZu8QDv1TapVKMeOQ,12064
|
|
8
|
-
memwal-0.2.1.dev0.dist-info/METADATA,sha256=OiHVIIM8Tbpf4Mj0dsKObxQfxZD_1NqshTsKGthp9hE,10346
|
|
9
|
-
memwal-0.2.1.dev0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
10
|
-
memwal-0.2.1.dev0.dist-info/RECORD,,
|
|
File without changes
|