meshsocket 0.2.0__tar.gz → 0.4.0__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: meshsocket
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Lightweight WebSocket mesh networking — broadcast, direct routing, and request/response over a simple JSON wire protocol.
5
5
  Author: Carter Beaudoin
6
6
  License-Expression: MIT
@@ -7,4 +7,4 @@ so existing code keeps running while it migrates to the package name.
7
7
  from socketCore import MeshSocket, LogColors, sanitize_identity, valid_ident, IDENT_RE, NAMESPACED_RE
8
8
 
9
9
  __all__ = ["MeshSocket", "LogColors", "sanitize_identity", "valid_ident", "IDENT_RE", "NAMESPACED_RE"]
10
- __version__ = "0.2.0"
10
+ __version__ = "0.4.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshsocket
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Lightweight WebSocket mesh networking — broadcast, direct routing, and request/response over a simple JSON wire protocol.
5
5
  Author: Carter Beaudoin
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshsocket"
7
- version = "0.2.0"
7
+ version = "0.4.0"
8
8
  description = "Lightweight WebSocket mesh networking — broadcast, direct routing, and request/response over a simple JSON wire protocol."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -532,7 +532,7 @@ class MeshSocket:
532
532
  client itself produced). Anything else was not written by us."""
533
533
  try:
534
534
  frame = json.loads(line)
535
- except (ValueError, TypeError):
535
+ except (ValueError, TypeError, RecursionError):
536
536
  return False
537
537
  return (isinstance(frame, dict) and isinstance(frame.get("type"), str)
538
538
  and isinstance(frame.get("id"), str)
@@ -620,7 +620,10 @@ class MeshSocket:
620
620
 
621
621
  try:
622
622
  data = json.loads(message)
623
- except (json.JSONDecodeError, TypeError, ValueError):
623
+ except (json.JSONDecodeError, TypeError, ValueError, RecursionError):
624
+ # RecursionError: a frame nested deeper than the parser's C
625
+ # recursion limit. Drop it like any bad frame instead of letting
626
+ # it escape and tear the connection down (peer-forced reconnects).
624
627
  continue
625
628
  await inflight.acquire()
626
629
  asyncio.create_task(run(data))
@@ -19,6 +19,26 @@ class MeshServerConfigError(RuntimeError):
19
19
  """The server refused to start because its configuration is unsafe."""
20
20
 
21
21
 
22
+ # The key the relay writes on every routed payload (0.4.0): the sender's
23
+ # authenticated name, as this relay registered it at identify (through the
24
+ # carter-relay gateway that is the namespaced `<account>.<principal>_<name>`).
25
+ SENDER_KEY = "_from"
26
+
27
+
28
+ def stamp_sender(payload: Any, sender_name: str) -> Any:
29
+ """`payload` with `SENDER_KEY` set to `sender_name`, overwriting any value the
30
+ sender wrote. Only a dict payload is stamped. On an E2EE envelope
31
+ (`{"e2ee": 2, "s", "n", "ct"}`) the key lands on the outer, unsealed object
32
+ beside the ciphertext, which is where receivers read the relay's stamp; the
33
+ sealed body is never touched. Other payloads (lists, strings, null) pass
34
+ unchanged. Returns a copy; the caller's dict is not modified."""
35
+ if not isinstance(payload, dict):
36
+ return payload
37
+ stamped = dict(payload)
38
+ stamped[SENDER_KEY] = sender_name
39
+ return stamped
40
+
41
+
22
42
  def _env_int(name: str, default: int) -> int:
23
43
  raw = os.getenv(name)
24
44
  if raw is None or not raw.strip():
@@ -459,7 +479,8 @@ class MeshServer:
459
479
  return {"error": "malformed route", "status": "failed"}
460
480
  target_id = payload.get('target_id')
461
481
  msg_type = payload.get('type')
462
- data = payload.get('payload')
482
+ # The target learns who sent this from the relay, not the sender.
483
+ data = stamp_sender(payload.get('payload'), client.name)
463
484
  if not isinstance(target_id, str) or not isinstance(msg_type, str):
464
485
  return {"error": "malformed route", "status": "failed"}
465
486
 
@@ -499,7 +520,7 @@ class MeshServer:
499
520
  return {"error": "malformed route", "status": "failed"}
500
521
  target_name = payload.get('target_name')
501
522
  msg_type = payload.get('type')
502
- data = payload.get('payload')
523
+ data = stamp_sender(payload.get('payload'), client.name)
503
524
  if not isinstance(target_name, str) or not isinstance(msg_type, str):
504
525
  return {"error": "malformed route", "status": "failed"}
505
526
 
@@ -608,6 +629,12 @@ class MeshServer:
608
629
  await self._fan_out(sends, targets, "roster")
609
630
 
610
631
  async def broadcast(self, type: str, payload: dict, sender: MeshSocket | None = None):
632
+ # Broadcasts are NOT stamped (only routed payloads are; see stamp_sender).
633
+ # Peers already put their own `_from` on broadcasts and match replies
634
+ # against it: CAR-TER writes its bare name on an ack'd command and drops a
635
+ # `command_ack` whose `to` differs, and carterkit acks to the outer `_from`
636
+ # of a sealed frame when there is one. Behind the gateway the relay name is
637
+ # namespaced, so stamping broadcasts would break those acks.
611
638
  if not self.clients:
612
639
  return
613
640
  sends, targets = [], []
File without changes
File without changes
File without changes