summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-04 02:20:48 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-04 02:20:48 +0200
commit7a4b905ddb64bdc92b7f9acf2ccde9bd84d7a6f3 (patch)
treecc9c7025c2b2d7fc31571b3589ecabcb6f63d8c2
parent5022f2be149e40ac5bdd5fc362e926f3edcf44f0 (diff)
downloadmeshbay-7a4b905ddb64bdc92b7f9acf2ccde9bd84d7a6f3.tar.gz
fix(node): pin utf-8 (and LF) on every text file the node reads or writes
node.toml, the keystore envelope, the unlock key, the loopback UI token, pairing/invite code files and the denylist were all read and written with the platform default encoding and newline translation. On Windows that is cp1252 + CRLF: a node.toml or keystore holding any non-ASCII byte failed to load, and ops.py's line-based node.toml editor round-tripped CRLF in and LF out. Every read is now `encoding="utf-8"`; every write is `encoding="utf-8", newline="\n"` so the files stay LF whatever the OS. No-op where the locale was already UTF-8. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-rw-r--r--packages/meshbay-node/src/meshbay_node/config.py4
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py16
-rw-r--r--packages/meshbay-node/src/meshbay_node/keystore.py6
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops.py18
-rw-r--r--packages/meshbay-node/src/meshbay_node/roster.py2
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/quic_server.py4
6 files changed, 25 insertions, 25 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/config.py b/packages/meshbay-node/src/meshbay_node/config.py
index cfbec50..7be68a4 100644
--- a/packages/meshbay-node/src/meshbay_node/config.py
+++ b/packages/meshbay-node/src/meshbay_node/config.py
@@ -307,7 +307,7 @@ def load_config(path: Path = DEFAULT_CONFIG_PATH) -> Config:
cfg = Config()
if path.exists():
- raw = tomllib.loads(path.read_text())
+ raw = tomllib.loads(path.read_text(encoding="utf-8"))
hub = raw.get("hub", {})
cfg.hub.url = hub.get("url", cfg.hub.url)
@@ -404,4 +404,4 @@ def write_example_config(path: Path = DEFAULT_CONFIG_PATH) -> None:
"""Write an example config file if none exists."""
if not path.exists():
path.parent.mkdir(parents=True, exist_ok=True)
- path.write_text(EXAMPLE_CONFIG)
+ path.write_text(EXAMPLE_CONFIG, encoding="utf-8", newline="\n")
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
index d616b0c..72f7fa4 100644
--- a/packages/meshbay-node/src/meshbay_node/daemon.py
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -219,7 +219,7 @@ class NodeDaemon:
# should ever copy a token out of a log or a terminal.
self._config.data_dir.mkdir(parents=True, exist_ok=True)
self._ui_token_file = self._config.data_dir / "ui-token"
- self._ui_token_file.write_text(ui_token)
+ self._ui_token_file.write_text(ui_token, encoding="utf-8", newline="\n")
chmod_private(self._ui_token_file)
from meshbay_node.ui import create_ui_app
ui_app = create_ui_app(self._state)
@@ -1526,7 +1526,7 @@ def _daemon_api(cfg: Config, path: str, method: str = "GET",
sep = "&" if "?" in path else "?"
url = (f"http://127.0.0.1:{cfg.node.ui_port}{path}"
- f"{sep}t={token_file.read_text().strip()}")
+ f"{sep}t={token_file.read_text(encoding="utf-8").strip()}")
try:
data = _json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(
@@ -1708,7 +1708,7 @@ def main() -> None:
sys.exit(1)
if cfg_path.exists():
- existing = cfg_path.read_text()
+ existing = cfg_path.read_text(encoding="utf-8")
import re as _re
m = _re.search(r'username\s*=\s*"([^"]*)"', existing)
existing_user = m.group(1) if m else ""
@@ -1721,7 +1721,7 @@ def main() -> None:
r'(username\s*=\s*)"[^"]*"', rf'\1"{username}"', existing)
updated = _re.sub(
r'(url\s*=\s*)"[^"]*"', rf'\1"{hub_url}"', updated, count=1)
- cfg_path.write_text(updated)
+ cfg_path.write_text(updated, encoding="utf-8", newline="\n")
print(f"Config updated: username={username}, hub={hub_url}")
else:
print(f"Config already exists: {cfg_path}")
@@ -1741,7 +1741,7 @@ def main() -> None:
f'unlock_file = "{unlock_file}"',
"",
]
- cfg_path.write_text("\n".join(toml_lines) + "\n")
+ cfg_path.write_text("\n".join(toml_lines) + "\n", encoding="utf-8", newline="\n")
chmod_private(cfg_path)
print(f"Config written to {cfg_path}")
@@ -1749,7 +1749,7 @@ def main() -> None:
if not unlock_file.exists():
import secrets
key = secrets.token_urlsafe(32)
- unlock_file.write_text(key + "\n")
+ unlock_file.write_text(key + "\n", encoding="utf-8", newline="\n")
chmod_private(unlock_file)
print(f"Unlock key created: {unlock_file}")
@@ -1815,7 +1815,7 @@ def main() -> None:
if token_file.exists():
try:
cfg = Config(config_dir_ / "node.toml")
- tok = token_file.read_text().strip()
+ tok = token_file.read_text(encoding="utf-8").strip()
url = (f"http://127.0.0.1:{cfg.node.ui_port}"
f"/api/unlink?t={tok}")
req = urllib.request.Request(url, method="DELETE")
@@ -1864,7 +1864,7 @@ def main() -> None:
if token_file.exists():
try:
url = (f"http://127.0.0.1:{cfg.node.ui_port}"
- f"/api/status?t={token_file.read_text().strip()}")
+ f"/api/status?t={token_file.read_text(encoding="utf-8").strip()}")
with urllib.request.urlopen(url, timeout=3) as r:
live = _json.loads(r.read())
except Exception:
diff --git a/packages/meshbay-node/src/meshbay_node/keystore.py b/packages/meshbay-node/src/meshbay_node/keystore.py
index dff443d..00e504e 100644
--- a/packages/meshbay-node/src/meshbay_node/keystore.py
+++ b/packages/meshbay-node/src/meshbay_node/keystore.py
@@ -105,7 +105,7 @@ def _resolve_password(unlock_file: Path | None = None) -> str:
mode, key_file,
)
log.debug("Keystore password from %s", key_file)
- return key_file.read_text().strip()
+ return key_file.read_text(encoding="utf-8").strip()
# 3. Interactive prompt
return getpass.getpass("MeshBay node keystore password: ")
@@ -173,7 +173,7 @@ def load_keystore(
raise FileNotFoundError(f"Keystore not found: {path} — run: meshbay-node init")
pwd = password or _resolve_password(unlock_file)
- envelope = json.loads(path.read_text())
+ envelope = json.loads(path.read_text(encoding="utf-8"))
if envelope.get("version") != KEYSTORE_VERSION:
raise ValueError(f"Unsupported keystore version: {envelope.get('version')}")
@@ -236,7 +236,7 @@ def _write_keystore(path: Path, keys: NodeKeys, password: str) -> None:
"tag_b64": base64.b64encode(tag).decode(),
"ciphertext_b64": base64.b64encode(ct).decode(),
}
- path.write_text(json.dumps(envelope, indent=2))
+ path.write_text(json.dumps(envelope, indent=2), encoding="utf-8", newline="\n")
chmod_private(path)
diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py
index 7a8e8ac..40de755 100644
--- a/packages/meshbay-node/src/meshbay_node/ops.py
+++ b/packages/meshbay-node/src/meshbay_node/ops.py
@@ -453,7 +453,7 @@ async def attach_group(state: dict, name: str, shared_dir: str,
if not separate_upload:
block += f' upload = true\n'
try:
- with conf_path.open("a") as f:
+ with conf_path.open("a", encoding="utf-8", newline="\n") as f:
f.write(block)
except OSError as e:
raise OpError(f"Cannot write {conf_path}: {e}", status=500) from e
@@ -485,7 +485,7 @@ async def detach_group(state: dict, name: str) -> dict:
group = match[0]
conf_path = Path(state.get("config_path") or DEFAULT_CONFIG_PATH)
- text = conf_path.read_text()
+ text = conf_path.read_text(encoding="utf-8")
lines = text.split("\n")
rng = _find_group_range(lines, group.id)
@@ -497,7 +497,7 @@ async def detach_group(state: dict, name: str) -> dict:
end += 1
new_lines = lines[:start] + lines[end:]
- conf_path.write_text("\n".join(new_lines))
+ conf_path.write_text("\n".join(new_lines), encoding="utf-8", newline="\n")
log.info("Group detached: %s (%s) removed from %s", group.name, group.id[:8], conf_path)
return {"group_id": group.id, "name": group.name, "config": str(conf_path),
@@ -534,7 +534,7 @@ def _update_node_toml(conf_path: Path, updates: dict) -> None:
"""
if not conf_path.exists():
return
- text = conf_path.read_text()
+ text = conf_path.read_text(encoding="utf-8")
lines = text.split("\n")
node_start = None
@@ -575,13 +575,13 @@ def _update_node_toml(conf_path: Path, updates: dict) -> None:
lines.insert(node_end, _format_value(key, value))
node_end += 1
- conf_path.write_text("\n".join(lines))
+ conf_path.write_text("\n".join(lines), encoding="utf-8", newline="\n")
def _insert_roots_block(conf_path: Path, group_id: str,
root_block: str) -> None:
"""Append a [[groups.roots]] block inside the matching [[groups]] section."""
- text = conf_path.read_text()
+ text = conf_path.read_text(encoding="utf-8")
lines = text.split("\n")
rng = _find_group_range(lines, group_id)
@@ -597,13 +597,13 @@ def _insert_roots_block(conf_path: Path, group_id: str,
+ [""]
+ root_block.rstrip("\n").split("\n")
+ lines[insert_at:])
- conf_path.write_text("\n".join(new_lines))
+ conf_path.write_text("\n".join(new_lines), encoding="utf-8", newline="\n")
def _remove_roots_block(conf_path: Path, group_id: str,
resolved_path: str) -> None:
"""Remove a [[groups.roots]] block whose resolved path matches."""
- text = conf_path.read_text()
+ text = conf_path.read_text(encoding="utf-8")
lines = text.split("\n")
rng = _find_group_range(lines, group_id)
@@ -631,7 +631,7 @@ def _remove_roots_block(conf_path: Path, group_id: str,
if rm_start > 0 and lines[rm_start - 1].strip() == "":
rm_start -= 1
new_lines = lines[:rm_start] + lines[rs_end:]
- conf_path.write_text("\n".join(new_lines))
+ conf_path.write_text("\n".join(new_lines), encoding="utf-8", newline="\n")
return
raise OpError(f"Root path not found in config", status=404)
diff --git a/packages/meshbay-node/src/meshbay_node/roster.py b/packages/meshbay-node/src/meshbay_node/roster.py
index a50dd3e..c78281b 100644
--- a/packages/meshbay-node/src/meshbay_node/roster.py
+++ b/packages/meshbay-node/src/meshbay_node/roster.py
@@ -915,6 +915,6 @@ def write_code_file(data_dir: Path, code: str, expires_at: str,
path = data_dir / name
path.parent.mkdir(parents=True, exist_ok=True)
from meshbay_node.platform import chmod_private
- path.write_text(f"{code}\nexpires {expires_at}\n")
+ path.write_text(f"{code}\nexpires {expires_at}\n", encoding="utf-8", newline="\n")
chmod_private(path)
return path
diff --git a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py
index 95fccf1..360b9ac 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py
@@ -144,7 +144,7 @@ class Denylist:
return
try:
import json
- data = json.loads(self._path.read_text())
+ data = json.loads(self._path.read_text(encoding="utf-8"))
self.user_ids = set(data.get("users", []))
self.group_ids = set(data.get("groups", []))
self.jtis = set(data.get("jtis", []))
@@ -163,7 +163,7 @@ class Denylist:
"users": sorted(self.user_ids),
"groups": sorted(self.group_ids),
"jtis": sorted(self.jtis),
- }))
+ }), encoding="utf-8", newline="\n")
except Exception as e:
log.warning("Could not persist denylist to %s: %s", self._path, e)