memwal 0.1.9.dev2__tar.gz → 0.1.9.dev3__tar.gz

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.
Files changed (28) hide show
  1. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/CHANGELOG.md +1 -0
  2. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/PKG-INFO +1 -1
  3. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/memwal/__init__.py +1 -1
  4. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/memwal/client.py +48 -1
  5. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/pyproject.toml +1 -1
  6. memwal-0.1.9.dev3/tests/test_normalize_server_url.py +96 -0
  7. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/.gitignore +0 -0
  8. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/README.md +0 -0
  9. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/examples/.env.example +0 -0
  10. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/examples/.gitignore +0 -0
  11. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/examples/async_remember_demo.py +0 -0
  12. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/examples/interactive_demo.py +0 -0
  13. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/examples/verify_credentials.py +0 -0
  14. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/memwal/compatibility.py +0 -0
  15. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/memwal/middleware.py +0 -0
  16. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/memwal/mock.py +0 -0
  17. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/memwal/types.py +0 -0
  18. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/memwal/utils.py +0 -0
  19. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/notebooks/walrus_memory_python_sdk.ipynb +0 -0
  20. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/run_tests.py +0 -0
  21. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/__init__.py +0 -0
  22. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/test_auth_rejected_message.py +0 -0
  23. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/test_client.py +0 -0
  24. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/test_env_presets.py +0 -0
  25. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/test_integration.py +0 -0
  26. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/test_middleware.py +0 -0
  27. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/test_mock.py +0 -0
  28. {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/test_signing.py +0 -0
@@ -7,6 +7,7 @@
7
7
  - HTTP 503 with `x-auth-error: AUTH_UPSTREAM_UNAVAILABLE` is reported as a retryable credential-verification outage, not a sign-in failure. Other 503s keep the generic sanitized body.
8
8
  - `remember_bulk_async` rejects an empty `items` list before the request and raises when the relayer returns a `job_ids` length that does not match the batch.
9
9
  - restore `truncated` docs now match WALM-431 retryable semantics.
10
+ - Warn when `server_url` uses plaintext `http://` against a non-localhost host, matching the TypeScript SDK `normalizeServerUrl` guard. Localhost, `127.0.0.1`, `::1`, and `*.localhost` are exempt; invalid URLs are left for the HTTP client to surface. The warning logs only scheme, host, and port so URL userinfo is not written to logs.
10
11
 
11
12
  ## 0.1.8
12
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memwal
3
- Version: 0.1.9.dev2
3
+ Version: 0.1.9.dev3
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
@@ -122,4 +122,4 @@ __all__ = [
122
122
  "RecallManualResult",
123
123
  ]
124
124
 
125
- __version__ = "0.1.9.dev2"
125
+ __version__ = "0.1.9.dev3"
@@ -35,6 +35,7 @@ import time
35
35
  import uuid
36
36
  from datetime import datetime, timezone
37
37
  from typing import Any, Dict, List, Optional, Sequence, Tuple, TypeVar, Union
38
+ from urllib.parse import ParseResult, urlparse
38
39
 
39
40
  import httpx
40
41
  import nacl.signing
@@ -107,6 +108,52 @@ UPSTREAM_UNAVAILABLE_MESSAGE = (
107
108
  logger = logging.getLogger("memwal")
108
109
 
109
110
 
111
+ def _server_url_for_log(parsed: ParseResult) -> str:
112
+ """Scheme/host/port only — never userinfo, path, query, or fragment."""
113
+
114
+ host = parsed.hostname or ""
115
+ if ":" in host:
116
+ host = f"[{host}]"
117
+ if parsed.port is not None:
118
+ return f"{parsed.scheme}://{host}:{parsed.port}"
119
+ return f"{parsed.scheme}://{host}"
120
+
121
+
122
+ def normalize_server_url(url: str) -> str:
123
+ """Strip a trailing slash and warn on plaintext HTTP to a remote host.
124
+
125
+ Ports the TypeScript ``normalizeServerUrl`` helper: localhost,
126
+ ``127.0.0.1``, ``::1``, and ``*.localhost`` are exempt. Invalid URLs
127
+ are returned trimmed so the HTTP client can surface the error later.
128
+
129
+ The warning logs only scheme/host/port so URL userinfo (HTTPX
130
+ credentials) and other sensitive components are not written to logs.
131
+ The returned URL is otherwise unchanged and still used for transport.
132
+ """
133
+
134
+ trimmed = url.rstrip("/")
135
+ try:
136
+ parsed = urlparse(trimmed)
137
+ host = (parsed.hostname or "").lower()
138
+ is_local = (
139
+ host == "localhost"
140
+ or host == "127.0.0.1"
141
+ or host == "::1"
142
+ or host.endswith(".localhost")
143
+ )
144
+ if parsed.scheme == "http" and host and not is_local:
145
+ logger.warning(
146
+ '[memwal] serverUrl "%s" uses plaintext HTTP on a non-localhost host. '
147
+ "Signed requests and any bearer material will be visible to the network. "
148
+ "Use https:// in production.",
149
+ _server_url_for_log(parsed),
150
+ )
151
+ except ValueError:
152
+ # invalid URL — let the HTTP call surface the error at request time
153
+ pass
154
+ return trimmed
155
+
156
+
110
157
  # ============================================================
111
158
  # Polling helpers (PR #121 parity with TS SDK)
112
159
  # ============================================================
@@ -234,7 +281,7 @@ class MemWal:
234
281
  self._private_key_hex = normalize_private_key(config.key)
235
282
  self._signing_key = build_signing_key(self._private_key_hex)
236
283
  self._account_id = config.account_id
237
- self._server_url = config.server_url.rstrip("/")
284
+ self._server_url = normalize_server_url(config.server_url)
238
285
  self._namespace = config.namespace
239
286
  self._client: Optional[httpx.AsyncClient] = None
240
287
  self._server_config: Optional[Dict[str, str]] = None
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memwal"
7
- version = "0.1.9.dev2"
7
+ version = "0.1.9.dev3"
8
8
  description = "Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,96 @@
1
+ """Tests for plaintext HTTP server_url guarding (WALM-452 / #748).
2
+
3
+ No network: ``MemWal.create`` only stores config, and
4
+ ``normalize_server_url`` is a pure parse + log helper.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+ import pytest
12
+
13
+ from memwal.client import MemWal, normalize_server_url
14
+
15
+ _KEY = "ab" * 32
16
+ _ACCOUNT = "0xdummy"
17
+
18
+
19
+ def test_plaintext_remote_warns_and_strips(caplog: pytest.LogCaptureFixture) -> None:
20
+ caplog.set_level(logging.WARNING, logger="memwal")
21
+ assert (
22
+ normalize_server_url("http://relayer.example.com/")
23
+ == "http://relayer.example.com"
24
+ )
25
+ assert "plaintext" in caplog.text
26
+
27
+
28
+ def test_https_remote_does_not_warn(caplog: pytest.LogCaptureFixture) -> None:
29
+ caplog.set_level(logging.WARNING, logger="memwal")
30
+ assert (
31
+ normalize_server_url("https://relayer.memory.walrus.xyz")
32
+ == "https://relayer.memory.walrus.xyz"
33
+ )
34
+ assert caplog.text == ""
35
+
36
+
37
+ @pytest.mark.parametrize(
38
+ "url",
39
+ [
40
+ "http://localhost:8000",
41
+ "http://127.0.0.1:8000",
42
+ "http://foo.localhost",
43
+ "http://[::1]:8000",
44
+ ],
45
+ )
46
+ def test_plaintext_local_does_not_warn(
47
+ url: str, caplog: pytest.LogCaptureFixture
48
+ ) -> None:
49
+ caplog.set_level(logging.WARNING, logger="memwal")
50
+ assert normalize_server_url(url) == url
51
+ assert caplog.text == ""
52
+
53
+
54
+ def test_create_warns_on_plaintext_remote(caplog: pytest.LogCaptureFixture) -> None:
55
+ caplog.set_level(logging.WARNING, logger="memwal")
56
+ client = MemWal.create(
57
+ key=_KEY,
58
+ account_id=_ACCOUNT,
59
+ server_url="http://relayer.example.com/",
60
+ )
61
+ assert client._server_url == "http://relayer.example.com"
62
+ assert "plaintext" in caplog.text
63
+
64
+
65
+ def test_plaintext_remote_warning_omits_url_credentials(
66
+ caplog: pytest.LogCaptureFixture,
67
+ ) -> None:
68
+ """HTTPX userinfo must stay on the transport URL and out of the warning."""
69
+
70
+ caplog.set_level(logging.WARNING, logger="memwal")
71
+ url = "http://alice:example-secret@relayer.example.com/?token=super-secret"
72
+ assert (
73
+ normalize_server_url(url)
74
+ == "http://alice:example-secret@relayer.example.com/?token=super-secret"
75
+ )
76
+ assert "plaintext" in caplog.text
77
+ assert "http://relayer.example.com" in caplog.text
78
+ assert "alice" not in caplog.text
79
+ assert "example-secret" not in caplog.text
80
+ assert "super-secret" not in caplog.text
81
+ for rec in caplog.records:
82
+ assert "example-secret" not in rec.getMessage()
83
+ assert "example-secret" not in str(rec.args)
84
+ assert "super-secret" not in rec.getMessage()
85
+ assert "super-secret" not in str(rec.args)
86
+
87
+
88
+ def test_create_preserves_url_credentials_and_redacts_warning(
89
+ caplog: pytest.LogCaptureFixture,
90
+ ) -> None:
91
+ caplog.set_level(logging.WARNING, logger="memwal")
92
+ url = "http://alice:example-secret@relayer.example.com/"
93
+ client = MemWal.create(key=_KEY, account_id=_ACCOUNT, server_url=url)
94
+ assert client._server_url == "http://alice:example-secret@relayer.example.com"
95
+ assert "plaintext" in caplog.text
96
+ assert "example-secret" not in caplog.text
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes