memwal 0.1.0.dev0__tar.gz → 0.1.0.dev2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memwal
3
- Version: 0.1.0.dev0
3
+ Version: 0.1.0.dev2
4
4
  Summary: Python SDK for MemWal — Privacy-first AI memory with Ed25519 signing
5
5
  Project-URL: Homepage, https://memwal.ai
6
6
  Project-URL: Documentation, https://docs.memwal.ai
@@ -130,6 +130,32 @@ async with MemWal.create(
130
130
  await memwal.remember("I prefer dark mode")
131
131
  ```
132
132
 
133
+ ## Environment Presets
134
+
135
+ Instead of hardcoding a relayer URL, pass `env` to target a hosted relayer.
136
+ Same shorthand as the TypeScript SDK and MCP package.
137
+
138
+ ```python
139
+ from memwal import MemWal
140
+
141
+ memwal = MemWal.create(
142
+ key=os.environ["MEMWAL_KEY"],
143
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
144
+ env="prod", # prod | dev | staging | local
145
+ )
146
+ ```
147
+
148
+ | `env` | Relayer URL |
149
+ |-------|-------------|
150
+ | `prod` | `https://relayer.memwal.ai` |
151
+ | `dev` | `https://relayer.dev.memwal.ai` |
152
+ | `staging` | `https://relayer.staging.memwal.ai` |
153
+ | `local` | `http://127.0.0.1:8000` |
154
+
155
+ Precedence: an explicit non-default **`server_url` wins over `env`**, which wins
156
+ over the default. An unknown preset raises `ValueError`. `env` is also accepted
157
+ by `MemWalSync.create`, `with_memwal_langchain`, and `with_memwal_openai`.
158
+
133
159
  ## AI Middleware
134
160
 
135
161
  ### LangChain
@@ -201,10 +227,10 @@ Create a new async client.
201
227
  Every request is signed with Ed25519:
202
228
 
203
229
  ```
204
- message = f"{timestamp}.{method}.{path}.{sha256(body)}.{nonce}.{account_id}"
230
+ message = f"{timestamp}.{method}.{path}.{sha256(body)}"
205
231
  ```
206
232
 
207
- Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-nonce`, `x-delegate-key`, `x-account-id`.
233
+ Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-delegate-key`, `x-account-id`.
208
234
 
209
235
  ## License
210
236
 
@@ -91,6 +91,32 @@ async with MemWal.create(
91
91
  await memwal.remember("I prefer dark mode")
92
92
  ```
93
93
 
94
+ ## Environment Presets
95
+
96
+ Instead of hardcoding a relayer URL, pass `env` to target a hosted relayer.
97
+ Same shorthand as the TypeScript SDK and MCP package.
98
+
99
+ ```python
100
+ from memwal import MemWal
101
+
102
+ memwal = MemWal.create(
103
+ key=os.environ["MEMWAL_KEY"],
104
+ account_id=os.environ["MEMWAL_ACCOUNT_ID"],
105
+ env="prod", # prod | dev | staging | local
106
+ )
107
+ ```
108
+
109
+ | `env` | Relayer URL |
110
+ |-------|-------------|
111
+ | `prod` | `https://relayer.memwal.ai` |
112
+ | `dev` | `https://relayer.dev.memwal.ai` |
113
+ | `staging` | `https://relayer.staging.memwal.ai` |
114
+ | `local` | `http://127.0.0.1:8000` |
115
+
116
+ Precedence: an explicit non-default **`server_url` wins over `env`**, which wins
117
+ over the default. An unknown preset raises `ValueError`. `env` is also accepted
118
+ by `MemWalSync.create`, `with_memwal_langchain`, and `with_memwal_openai`.
119
+
94
120
  ## AI Middleware
95
121
 
96
122
  ### LangChain
@@ -162,10 +188,10 @@ Create a new async client.
162
188
  Every request is signed with Ed25519:
163
189
 
164
190
  ```
165
- message = f"{timestamp}.{method}.{path}.{sha256(body)}.{nonce}.{account_id}"
191
+ message = f"{timestamp}.{method}.{path}.{sha256(body)}"
166
192
  ```
167
193
 
168
- Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-nonce`, `x-delegate-key`, `x-account-id`.
194
+ Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-delegate-key`, `x-account-id`.
169
195
 
170
196
  ## License
171
197
 
@@ -25,6 +25,7 @@ import os
25
25
  import sys
26
26
  import time
27
27
  from pathlib import Path
28
+ from typing import Optional
28
29
 
29
30
 
30
31
  def _load_env() -> None:
@@ -49,6 +50,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
49
50
 
50
51
  from memwal import ( # noqa: E402
51
52
  MemWal,
53
+ MemWalRememberJobFailed,
52
54
  RememberBulkItem,
53
55
  RememberBulkOptions,
54
56
  )
@@ -33,6 +33,7 @@ from .client import (
33
33
  from .middleware import with_memwal_langchain, with_memwal_openai
34
34
  from .utils import delegate_key_to_sui_address, delegate_key_to_public_key
35
35
  from .types import (
36
+ ENV_PRESETS,
36
37
  AnalyzedFact,
37
38
  AnalyzeResult,
38
39
  AnalyzeWaitResult,
@@ -81,6 +82,7 @@ __all__ = [
81
82
  "withMemWal",
82
83
  # Types
83
84
  "MemWalConfig",
85
+ "ENV_PRESETS",
84
86
  "AskMemory",
85
87
  "AskResult",
86
88
  "RememberResult",
@@ -108,4 +110,4 @@ __all__ = [
108
110
  "RecallManualResult",
109
111
  ]
110
112
 
111
- __version__ = "0.1.0.dev0"
113
+ __version__ = "0.1.0.dev2"
@@ -29,7 +29,7 @@ import asyncio
29
29
  import json
30
30
  import random
31
31
  import time
32
- from typing import Any, Dict, List, Optional, Sequence, TypeVar
32
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, TypeVar
33
33
 
34
34
  import httpx
35
35
 
@@ -55,6 +55,7 @@ from .types import (
55
55
  RememberBulkResult,
56
56
  RememberBulkStatusItem,
57
57
  RememberBulkStatusResult,
58
+ RememberJobStatus,
58
59
  RememberManualOptions,
59
60
  RememberManualResult,
60
61
  RememberResult,
@@ -128,16 +129,21 @@ class MemWal:
128
129
  cls,
129
130
  key: str,
130
131
  account_id: str,
131
- server_url: str = "https://relayer.memwal.ai",
132
+ server_url: str = "http://localhost:8000",
132
133
  namespace: str = "default",
134
+ env: Optional[str] = None,
133
135
  ) -> "MemWal":
134
136
  """Create a new MemWal client instance.
135
137
 
136
138
  Args:
137
139
  key: Ed25519 private key hex string (the delegate key).
138
140
  account_id: MemWalAccount object ID on Sui.
139
- server_url: Server URL (default: ``https://relayer.memwal.ai``).
141
+ server_url: Server URL (default: ``http://localhost:8000``).
140
142
  namespace: Default namespace for memory isolation (default: ``"default"``).
143
+ env: Optional relayer preset — ``"prod"``, ``"dev"``, ``"staging"``,
144
+ or ``"local"``. Resolves ``server_url`` to the matching hosted
145
+ relayer unless an explicit non-default ``server_url`` is given.
146
+ Precedence: explicit ``server_url`` > ``env`` > default.
141
147
 
142
148
  Returns:
143
149
  A configured :class:`MemWal` instance.
@@ -147,6 +153,7 @@ class MemWal:
147
153
  account_id=account_id,
148
154
  server_url=server_url,
149
155
  namespace=namespace,
156
+ env=env,
150
157
  )
151
158
  return cls(config)
152
159
 
@@ -708,14 +715,12 @@ class MemWal:
708
715
  ) -> Dict[str, Any]:
709
716
  """Make a signed request to the server.
710
717
 
711
- Signature format:
712
- ``{timestamp}.{method}.{path}.{body_sha256}.{nonce}.{account_id}``
718
+ Signature format: ``{timestamp}.{method}.{path}.{body_sha256}``
713
719
 
714
720
  Headers sent:
715
721
  - ``x-public-key``: Ed25519 public key hex
716
722
  - ``x-signature``: Ed25519 signature hex
717
723
  - ``x-timestamp``: Unix seconds string
718
- - ``x-nonce``: UUID v4 replay-protection nonce
719
724
  - ``x-delegate-key``: Private key hex
720
725
  - ``x-account-id``: MemWalAccount object ID
721
726
  - ``Content-Type``: application/json
@@ -842,22 +847,27 @@ class MemWalSync:
842
847
  cls,
843
848
  key: str,
844
849
  account_id: str,
845
- server_url: str = "https://relayer.memwal.ai",
850
+ server_url: str = "http://localhost:8000",
846
851
  namespace: str = "default",
852
+ env: Optional[str] = None,
847
853
  ) -> "MemWalSync":
848
854
  """Create a synchronous MemWal client.
849
855
 
850
- Same parameters as :meth:`MemWal.create`.
856
+ Same parameters as :meth:`MemWal.create` (including the ``env``
857
+ relayer preset).
851
858
  """
852
859
  inner = MemWal.create(
853
860
  key=key,
854
861
  account_id=account_id,
855
862
  server_url=server_url,
856
863
  namespace=namespace,
864
+ env=env,
857
865
  )
858
866
  return cls(inner)
859
867
 
860
868
  def _run(self, coro: Any) -> Any:
869
+ import asyncio
870
+
861
871
  try:
862
872
  loop = asyncio.get_running_loop()
863
873
  except RuntimeError:
@@ -137,12 +137,13 @@ def with_memwal_langchain(
137
137
  llm: "BaseChatModel",
138
138
  key: str,
139
139
  account_id: str,
140
- server_url: str = "https://relayer.memwal.ai",
140
+ server_url: str = "http://localhost:8000",
141
141
  namespace: str = "default",
142
142
  max_memories: int = 5,
143
143
  auto_save: bool = True,
144
144
  min_relevance: float = 0.3,
145
145
  debug: bool = False,
146
+ env: Optional[str] = None,
146
147
  ) -> "BaseChatModel":
147
148
  """Wrap a LangChain ``BaseChatModel`` with MemWal memory management.
148
149
 
@@ -163,6 +164,8 @@ def with_memwal_langchain(
163
164
  auto_save: Auto-save new facts from conversation.
164
165
  min_relevance: Minimum similarity score (0-1) to include a memory.
165
166
  debug: Enable debug logging.
167
+ env: Optional relayer preset (``"prod"`` / ``"dev"`` / ``"staging"`` /
168
+ ``"local"``). Same precedence as :meth:`MemWal.create`.
166
169
 
167
170
  Returns:
168
171
  A wrapped ``BaseChatModel`` that automatically uses MemWal memory.
@@ -181,6 +184,7 @@ def with_memwal_langchain(
181
184
  account_id=account_id,
182
185
  server_url=server_url,
183
186
  namespace=namespace,
187
+ env=env,
184
188
  )
185
189
 
186
190
  log = logger.debug if not debug else logger.warning
@@ -254,6 +258,8 @@ def with_memwal_langchain(
254
258
  messages: List[List[BaseMessage]], *args: Any, **kwargs: Any
255
259
  ) -> ChatResult:
256
260
  # For sync generate, we inject memories synchronously via asyncio.run
261
+ import asyncio
262
+
257
263
  enriched = []
258
264
  for msg_list in messages:
259
265
  try:
@@ -290,12 +296,13 @@ def with_memwal_openai(
290
296
  client: Any,
291
297
  key: str,
292
298
  account_id: str,
293
- server_url: str = "https://relayer.memwal.ai",
299
+ server_url: str = "http://localhost:8000",
294
300
  namespace: str = "default",
295
301
  max_memories: int = 5,
296
302
  auto_save: bool = True,
297
303
  min_relevance: float = 0.3,
298
304
  debug: bool = False,
305
+ env: Optional[str] = None,
299
306
  ) -> Any:
300
307
  """Wrap an OpenAI client with MemWal memory management.
301
308
 
@@ -318,6 +325,8 @@ def with_memwal_openai(
318
325
  auto_save: Auto-save new facts from conversation.
319
326
  min_relevance: Minimum similarity score (0-1) to include a memory.
320
327
  debug: Enable debug logging.
328
+ env: Optional relayer preset (``"prod"`` / ``"dev"`` / ``"staging"`` /
329
+ ``"local"``). Same precedence as :meth:`MemWal.create`.
321
330
 
322
331
  Returns:
323
332
  The same client, with ``chat.completions.create`` wrapped to use MemWal.
@@ -327,6 +336,7 @@ def with_memwal_openai(
327
336
  account_id=account_id,
328
337
  server_url=server_url,
329
338
  namespace=namespace,
339
+ env=env,
330
340
  )
331
341
 
332
342
  log = logger.debug if not debug else logger.warning
@@ -408,6 +418,8 @@ def _wrap_sync_openai(
408
418
  original_create = client.chat.completions.create
409
419
 
410
420
  def patched_create(*args: Any, **kwargs: Any) -> Any:
421
+ import asyncio
422
+
411
423
  messages = kwargs.get("messages") or (args[0] if args else None)
412
424
  if messages is None:
413
425
  return original_create(*args, **kwargs)
@@ -15,6 +15,21 @@ from typing import List, Optional
15
15
  # Config
16
16
  # ============================================================
17
17
 
18
+ #: Default ``server_url`` when neither an explicit URL nor an ``env`` preset
19
+ #: is supplied. Kept as a module constant so ``__post_init__`` can tell an
20
+ #: untouched default apart from an explicitly-passed custom URL.
21
+ DEFAULT_SERVER_URL = "http://localhost:8000"
22
+
23
+ #: Named relayer environments. Mirrors the TypeScript SDK / MCP package
24
+ #: ``--prod`` / ``--dev`` / ``--staging`` / ``--local`` presets so the same
25
+ #: shorthand works across every MemWal client.
26
+ ENV_PRESETS = {
27
+ "prod": "https://relayer.memwal.ai",
28
+ "dev": "https://relayer.dev.memwal.ai",
29
+ "staging": "https://relayer.staging.memwal.ai",
30
+ "local": "http://127.0.0.1:8000",
31
+ }
32
+
18
33
 
19
34
  @dataclass
20
35
  class MemWalConfig:
@@ -23,14 +38,33 @@ class MemWalConfig:
23
38
  Attributes:
24
39
  key: Ed25519 private key (hex string). This is the delegate key from app.memwal.com.
25
40
  account_id: MemWalAccount object ID on Sui.
26
- server_url: Server URL (default: https://relayer.memwal.ai).
41
+ server_url: Server URL (default: http://localhost:8000). An explicit
42
+ non-default value always wins over ``env``.
27
43
  namespace: Default namespace for memory isolation (default: "default").
44
+ env: Optional relayer preset — one of ``"prod"``, ``"dev"``,
45
+ ``"staging"``, ``"local"``. Resolves ``server_url`` to the matching
46
+ hosted relayer when ``server_url`` is left at its default.
47
+ Precedence: explicit ``server_url`` > ``env`` > default.
28
48
  """
29
49
 
30
50
  key: str
31
51
  account_id: str
32
- server_url: str = "https://relayer.memwal.ai"
52
+ server_url: str = DEFAULT_SERVER_URL
33
53
  namespace: str = "default"
54
+ env: Optional[str] = None
55
+
56
+ def __post_init__(self) -> None:
57
+ if self.env is not None:
58
+ preset = ENV_PRESETS.get(self.env)
59
+ if preset is None:
60
+ valid = ", ".join(sorted(ENV_PRESETS))
61
+ raise ValueError(
62
+ f"Unknown env preset {self.env!r}. Valid presets: {valid}"
63
+ )
64
+ # Explicit, non-default server_url takes precedence over the
65
+ # preset; only fill from the preset when server_url is untouched.
66
+ if self.server_url == DEFAULT_SERVER_URL:
67
+ self.server_url = preset
34
68
 
35
69
 
36
70
  # ============================================================
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memwal"
7
- version = "0.1.0.dev0"
7
+ version = "0.1.0.dev2"
8
8
  description = "Python SDK for MemWal — Privacy-first AI memory with Ed25519 signing"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -16,7 +16,7 @@ import respx
16
16
 
17
17
  from memwal.client import MemWal, MemWalError
18
18
  from memwal.types import RecallManualOptions, RememberManualOptions
19
- from memwal.utils import bytes_to_hex, sha256_hex
19
+ from memwal.utils import build_signature_message, bytes_to_hex, sha256_hex
20
20
 
21
21
  # ============================================================
22
22
  # Fixtures
@@ -122,7 +122,14 @@ class TestRemember:
122
122
  timestamp = headers["x-timestamp"]
123
123
  body_hash = sha256_hex(body_str)
124
124
  nonce = headers["x-nonce"]
125
- message = f"{timestamp}.POST./api/remember.{body_hash}.{nonce}.{_TEST_ACCOUNT_ID}"
125
+ message = build_signature_message(
126
+ timestamp=timestamp,
127
+ method="POST",
128
+ path="/api/remember",
129
+ body_sha256=body_hash,
130
+ nonce=nonce,
131
+ account_id=headers["x-account-id"],
132
+ )
126
133
 
127
134
  # Verify signature
128
135
  verify_key = nacl.signing.VerifyKey(bytes.fromhex(headers["x-public-key"]))
@@ -0,0 +1,71 @@
1
+ """Tests for the relayer environment presets (prod/dev/staging/local).
2
+
3
+ Pure config resolution — no network. Mirrors the precedence rule documented
4
+ in the README: explicit non-default ``server_url`` > ``env`` > default.
5
+ """
6
+
7
+ import pytest
8
+
9
+ from memwal import ENV_PRESETS, MemWal, MemWalConfig, MemWalSync
10
+ from memwal.types import DEFAULT_SERVER_URL
11
+
12
+ # A throwaway but structurally valid 32-byte Ed25519 seed (64 hex chars).
13
+ KEY = "11" * 32
14
+ ACCOUNT = "0x" + "ab" * 32
15
+
16
+
17
+ @pytest.mark.parametrize(
18
+ "env,expected",
19
+ [
20
+ ("prod", "https://relayer.memwal.ai"),
21
+ ("dev", "https://relayer.dev.memwal.ai"),
22
+ ("staging", "https://relayer.staging.memwal.ai"),
23
+ ("local", "http://127.0.0.1:8000"),
24
+ ],
25
+ )
26
+ def test_env_preset_resolves(env, expected):
27
+ cfg = MemWalConfig(key=KEY, account_id=ACCOUNT, env=env)
28
+ assert cfg.server_url == expected
29
+ assert ENV_PRESETS[env] == expected
30
+
31
+
32
+ def test_explicit_server_url_overrides_env():
33
+ cfg = MemWalConfig(
34
+ key=KEY,
35
+ account_id=ACCOUNT,
36
+ server_url="https://my.custom.relayer",
37
+ env="prod",
38
+ )
39
+ assert cfg.server_url == "https://my.custom.relayer"
40
+
41
+
42
+ def test_no_env_keeps_default():
43
+ cfg = MemWalConfig(key=KEY, account_id=ACCOUNT)
44
+ assert cfg.server_url == DEFAULT_SERVER_URL
45
+
46
+
47
+ def test_unknown_env_raises():
48
+ with pytest.raises(ValueError, match="Unknown env preset"):
49
+ MemWalConfig(key=KEY, account_id=ACCOUNT, env="prdo")
50
+
51
+
52
+ def test_create_threads_env_through_to_client():
53
+ client = MemWal.create(key=KEY, account_id=ACCOUNT, env="staging")
54
+ assert client._server_url == "https://relayer.staging.memwal.ai"
55
+
56
+
57
+ def test_sync_create_threads_env_through():
58
+ client = MemWalSync.create(key=KEY, account_id=ACCOUNT, env="dev")
59
+ assert client._inner._server_url == "https://relayer.dev.memwal.ai"
60
+
61
+
62
+ def test_explicit_default_url_with_env_still_takes_preset():
63
+ # Passing the default URL explicitly is indistinguishable from not
64
+ # passing it — documented edge: the preset still applies.
65
+ cfg = MemWalConfig(
66
+ key=KEY,
67
+ account_id=ACCOUNT,
68
+ server_url=DEFAULT_SERVER_URL,
69
+ env="prod",
70
+ )
71
+ assert cfg.server_url == "https://relayer.memwal.ai"
@@ -40,12 +40,14 @@ import hashlib
40
40
  import json
41
41
  import os
42
42
  import time
43
+ import uuid
43
44
 
44
45
  import httpx
45
46
  import nacl.signing
46
47
  import pytest
47
48
 
48
49
  from memwal.client import MemWal, MemWalError, MemWalSync
50
+ from memwal.utils import build_signature_message, bytes_to_hex
49
51
 
50
52
  # ── Config ───────────────────────────────────────────────────────────────────
51
53
 
@@ -77,7 +79,15 @@ def _raw_signed_request(
77
79
  body_bytes = json.dumps(body, separators=(",", ":")).encode()
78
80
  body_hash = hashlib.sha256(body_bytes).hexdigest()
79
81
  timestamp = timestamp_override or str(int(time.time()))
80
- message = f"{timestamp}.{method.upper()}.{path}.{body_hash}"
82
+ nonce = str(uuid.uuid4())
83
+ message = build_signature_message(
84
+ timestamp=timestamp,
85
+ method=method.upper(),
86
+ path=path,
87
+ body_sha256=body_hash,
88
+ nonce=nonce,
89
+ account_id=ACCOUNT_ID or "0x0",
90
+ )
81
91
  signed = signing_key.sign(message.encode())
82
92
  signature_hex = signed.signature.hex()
83
93
  pub_key_hex = pub_key_override or signing_key.verify_key.encode().hex()
@@ -92,6 +102,8 @@ def _raw_signed_request(
92
102
  "x-public-key": pub_key_hex,
93
103
  "x-signature": signature_hex,
94
104
  "x-timestamp": timestamp,
105
+ "x-nonce": nonce,
106
+ "x-account-id": ACCOUNT_ID or "0x0",
95
107
  },
96
108
  )
97
109
 
@@ -16,10 +16,13 @@ with ``respx`` and all LLM responses are mocked with ``unittest.mock``.
16
16
  from __future__ import annotations
17
17
 
18
18
  import asyncio
19
- from unittest.mock import AsyncMock, MagicMock
19
+ import json
20
+ from typing import Any, List
21
+ from unittest.mock import AsyncMock, MagicMock, patch
20
22
 
21
23
  import httpx
22
24
  import nacl.signing
25
+ import pytest
23
26
  import respx
24
27
 
25
28
  from memwal.middleware import (
@@ -131,16 +131,16 @@ class TestBuildSignatureMessage:
131
131
  """Tests for build_signature_message -- the exact format the server expects."""
132
132
 
133
133
  def test_format_matches_spec(self) -> None:
134
- """Signature message includes timestamp, method, path, body hash, nonce, and account."""
134
+ """Signature message MUST match the canonical 6-part server format."""
135
135
  result = build_signature_message(
136
136
  timestamp="1700000000",
137
137
  method="POST",
138
138
  path="/api/remember",
139
139
  body_sha256="abc123",
140
- nonce="nonce-1",
141
- account_id="0xabc",
140
+ nonce="550e8400-e29b-41d4-a716-446655440000",
141
+ account_id="0xabc123",
142
142
  )
143
- assert result == "1700000000.POST./api/remember.abc123.nonce-1.0xabc"
143
+ assert result == "1700000000.POST./api/remember.abc123.550e8400-e29b-41d4-a716-446655440000.0xabc123"
144
144
 
145
145
  def test_get_method(self) -> None:
146
146
  result = build_signature_message(
@@ -148,8 +148,8 @@ class TestBuildSignatureMessage:
148
148
  method="GET",
149
149
  path="/health",
150
150
  body_sha256="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
151
- nonce="nonce-2",
152
- account_id="0xdef",
151
+ nonce="550e8400-e29b-41d4-a716-446655440000",
152
+ account_id="0xabc123",
153
153
  )
154
154
  parts = result.split(".")
155
155
  assert parts[0] == "1700000001"
@@ -168,15 +168,10 @@ class TestBuildSignatureMessage:
168
168
  path = "/api/remember"
169
169
  body = json.dumps({"text": "hello", "namespace": "default"}, separators=(",", ":"))
170
170
  body_hash = sha256_hex(body)
171
+ nonce = "550e8400-e29b-41d4-a716-446655440000"
172
+ account_id = "0xabc123"
171
173
 
172
- message = build_signature_message(
173
- timestamp,
174
- method,
175
- path,
176
- body_hash,
177
- nonce="nonce-3",
178
- account_id="0xabc",
179
- )
174
+ message = build_signature_message(timestamp, method, path, body_hash, nonce=nonce, account_id=account_id)
180
175
  sig_hex, pub_hex = sign_message(message, signing_key)
181
176
 
182
177
  # Verify (as the server would)
@@ -194,11 +189,11 @@ class TestBuildSignatureMessage:
194
189
  "POST",
195
190
  "/api/remember",
196
191
  body_hash,
197
- nonce="nonce-4",
198
- account_id="0xabc",
192
+ nonce="550e8400-e29b-41d4-a716-446655440000",
193
+ account_id="0xabc123",
199
194
  )
200
195
 
201
- # Extract the hash from the message
196
+ # Extract the hash from the canonical 6-part message
202
197
  extracted_hash = message.split(".")[3]
203
198
  assert extracted_hash == body_hash
204
199
  assert extracted_hash == hashlib.sha256(body_str.encode("utf-8")).hexdigest()
File without changes
File without changes
File without changes