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.
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/CHANGELOG.md +1 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/PKG-INFO +1 -1
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/memwal/__init__.py +1 -1
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/memwal/client.py +48 -1
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/pyproject.toml +1 -1
- memwal-0.1.9.dev3/tests/test_normalize_server_url.py +96 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/.gitignore +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/README.md +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/examples/.env.example +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/examples/.gitignore +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/examples/async_remember_demo.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/examples/interactive_demo.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/examples/verify_credentials.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/memwal/compatibility.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/memwal/middleware.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/memwal/mock.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/memwal/types.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/memwal/utils.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/notebooks/walrus_memory_python_sdk.ipynb +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/run_tests.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/__init__.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/test_auth_rejected_message.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/test_client.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/test_env_presets.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/test_integration.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/test_middleware.py +0 -0
- {memwal-0.1.9.dev2 → memwal-0.1.9.dev3}/tests/test_mock.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|