<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-node/src/meshbay_node/transport, branch 0.11</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.11</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.11'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-09-04T12:51:08Z</updated>
<entry>
<title>feat(node): log the host candidates the WebRTC answer offers</title>
<updated>2026-09-04T12:51:08Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-04T12:51:08Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b53298e339b47c6f0b53ab9fd0cfa3fb7741fc28'/>
<id>urn:sha1:b53298e339b47c6f0b53ab9fd0cfa3fb7741fc28</id>
<content type='text'>
"DataChannel closed" from a peer and a clean node log look identical: the
answer-ready line reported only the srflx count, not the host addresses.
On a NAT'd host or a VM the sole host candidate is an address no other
machine can route to, and that is exactly the case you cannot see. The
line now reads `... host: 192.168.200.173, 1 srflx`, so "did the node
offer anything routable" is answerable from the journal.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(node): make ice_interfaces match adapters on Windows (W9)</title>
<updated>2026-09-04T11:44:12Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-04T11:44:12Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=8288714853952aca6b3511268b9d772f1b7f489f'/>
<id>urn:sha1:8288714853952aca6b3511268b9d772f1b7f489f</id>
<content type='text'>
`ice_interfaces` compared the operator's entry against ifaddr's
`adapter.name` only -- the kernel name on Linux (`wlp3s0f0`), but the adapter
GUID on Windows (`{846EE342-...}`). A setting written on Linux, or copied
into a Windows node's node.toml, matched no adapter at all.

The failure was silent and total rather than partial: aioice binds one socket
per host address, so an empty list means no sockets, no host candidates, and
an SDP offering only a reflexive address. The settings field is free text
with no picker, and on Windows the operator sees neither the GUID nor the
description -- `ipconfig` shows the connection name -- so an entry now
matches the adapter name, the device description, or one of the adapter's own
IPv4 addresses, case-insensitively. An address is the one identifier visible
on every platform.

A filter that matches nothing now falls back to the unfiltered list with a
warning. Losing the 5 s timeout saving is a regression; being silently
unconnectable is a defect.

Also fixes IPv4/IPv6 discrimination in the same loop: the two were told apart
by falling through to an `elif` that index-probed `ip.ip[0]` and `ip.ip[2]`,
which on an IPv4 str yields characters that compared unequal by luck rather
than by design. Now discriminated by isinstance.

WINDOWS-PORT.md claimed Transport had "no platform dependency"; it does.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01DtfG7z6wHWj8RKHCvxQtY1
</content>
</entry>
<entry>
<title>fix(node): pin utf-8 (and LF) on every text file the node reads or writes</title>
<updated>2026-09-04T00:20:48Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-04T00:20:48Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=7a4b905ddb64bdc92b7f9acf2ccde9bd84d7a6f3'/>
<id>urn:sha1:7a4b905ddb64bdc92b7f9acf2ccde9bd84d7a6f3</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>refactor(node): platform abstraction for Windows portability (W1-W2-W5-W6-W7)</title>
<updated>2026-09-03T14:20:28Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-03T14:20:23Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=753653b4df62723b82d32799825135822886eac7'/>
<id>urn:sha1:753653b4df62723b82d32799825135822886eac7</id>
<content type='text'>
Platform directories, signal handling, chmod guards, ffmpeg discovery,
and platform-conditional CLI messages — all testable on Linux.

See docs/WINDOWS-PORT.md §5 for the plan these implement.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat!: MNP 1.0 — seal index and handshake_ack under the group key</title>
<updated>2026-09-03T14:16:55Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-03T14:16:55Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=675beed6ff688733a9598f9d82d41578f48316be'/>
<id>urn:sha1:675beed6ff688733a9598f9d82d41578f48316be</id>
<content type='text'>
`index_sync`, `index_delta` and the `handshake_ack` config payload now travel
sealed under a GEK-derived subkey (`meshbay_common/groupbox.py`, mirrored by
`sealGroup`/`openGroup` in `crypto.js`). Only `type`, `v`, `group_id` and the
ack's `node_pk`/`proof`/`sig` stay in clear — a receiver must route and
authenticate before it would trust a decryption. Verify, then decrypt.

The ack line is integrity, not confidentiality: the signed handshake transcript
names no ack field, so `is_node_admin`, `enabled_apps`, `video_root` and the
rest were authenticated by the DTLS channel alone. The index line is defence in
depth against a repeat of C1/C6 — a peer served before the handshake completes
now gets ciphertext, not filenames. Nothing against an observer, the hub, or a
member; that is the whole claim. `index_progress` stays clear (D3, counters
only). Chat is out of scope.

Failure is fatal: a payload that does not open ends the session naming the
message type — never an empty index or an empty `enabled_apps`, both of which
are legitimate states.

Version negotiation ships here too (phase 15.6, brought forward): `v` + `v_min`
on `handshake` and `handshake_challenge`, refused with `version_too_old` /
`version_too_new` / `version_unreadable`. The flag day was already being paid
for; the next breaking change now costs a refusal message.

BREAKING CHANGE: breaks the WebRTC wire every deployed client speaks. Hub and
every node must deploy together; the SPA is served by the hub, so a browser
picks up the new client on reload. See MESHBAY_NODE_PROTOCOL.md §11.1a, §13.1.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HkzbhmMmK8PqQBtGz5zCvY
</content>
</entry>
<entry>
<title>refactor!: one file_chunk and index_sync encoder for every transport</title>
<updated>2026-09-03T13:20:40Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-03T13:20:40Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=c1be7571973c3d0b671ed4db2da41266ae3099d8'/>
<id>urn:sha1:c1be7571973c3d0b671ed4db2da41266ae3099d8</id>
<content type='text'>
`file_chunk` and `index_sync` were each built twice, once per transport, and
the two copies did not agree. WebRTC sent binary, unsigned chunks carrying a
`file_id`; QUIC sent base64 fields, two BLAKE3 hashes, a per-chunk Ed25519
signature and no `file_id`. `index_sync` was plain entries on one transport
and a `GroupIndex.serialize()` envelope on the other. One message type, two
shapes, one consumer each, and nothing that failed when they drifted — finding
C6 one size down, in the two places the handshake unification did not reach.

Phase 9.15 moved WebRTC to the binary format and dropped the per-chunk
signature; the QUIC encoder was never brought along. It is dropped here rather
than reintroduced: the AES-GCM tag authenticates the ciphertext under a
GEK-derived key, and since C3 the node authenticates itself once in the
handshake instead of once per megabyte.

`meshbay_common.protocol` now owns the chunk codec (`chunk_ciphertext`,
`file_chunk_wire`, `file_chunk_plaintext`) and `meshbay_node/transport/wire.py`
the index builder, which also absorbs the delta the daemon used to hand-build.
`test_transport_wire_parity.py` fails if either server grows its own copy back.

`ChunkRequest`/`ChunkResponse` are deleted. `ChunkResponse` described the QUIC
half while reading like the contract for both, which is what made the fork hard
to see at all.

BREAKING CHANGE: MNP 0.15 changes the encoding of `file_chunk` and `index_sync`
on the QUIC transport. The WebRTC shapes are byte for byte unchanged and no
QUIC client ships, which is why this is a MINOR bump; a deployed QUIC peer
would have made it MAJOR.

Also fixes a test fixture that put a `Path` where the daemon puts a `RootSet`.
Nothing caught it: the old QUIC index handler never touched `roots`, and
`entry_abs_path` fell through `Path.resolve(strict=...)`, reading the virtual
path as a truthy flag and returning the right file by accident.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
</content>
</entry>
<entry>
<title>fix(hub): steady the show detail modal, and give a series its director</title>
<updated>2026-09-02T01:23:46Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-02T01:13:53Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=12b6dc4dd3e009f2e844d87181800aa12d07a3a6'/>
<id>urn:sha1:12b6dc4dd3e009f2e844d87181800aa12d07a3a6</id>
<content type='text'>
Opening a different season of the same show moved everything under the
synopsis, which is where the season control and the episode list are, so the
thing just clicked was no longer under the pointer.

- The synopsis is exactly three lines for a multi-season show, with a "read
  more" link floated into the third line box (-webkit-line-clamp only ever
  puts its ellipsis at the end of the last line and leaves no room after it).
  Clamped from above and pinned from below to the same number: a constant,
  not a range — a season summary runs two lines and the next one twelve, and
  a band still reads as a jump. Whether three lines is all of it depends on
  the modal's width, so it is measured in the browser and re-measured on a
  resize.
- The cast is clamped to two lines.
- SeasonMenu replaces SeasonTabs: the tab row scrolled sideways once a show
  had more seasons than fit, which is close to unusable on a phone. One
  trigger reading "Season 5 · 1997" and a menu of every season with its
  episode count, one row high whatever the season count.
- media_meta_resp.director was filled from the credits crew's job ==
  "Director", a movie shape. TMDB's aggregate tv_credits crew is routinely
  empty and never carries that job, so every show answered null and the modal
  dropped the line. It now comes from created_by on the show details. Cached
  show metadata keeps its null until TMDB_META_TTL_SECS expires or an
  operator re-matches.

The facts line is joined rather than concatenated (a title with no rating
used to open with " · ") and carries the show's own year next to the
director; the selected season's air year moved onto the picker.

test_video_detail_measured.py asserts rectangles through layout_probe.py, not
declarations: the picker's offset inside its own modal body is the same pixel
either way, the synopsis and cast heights, where the read-more link lands,
and the open menu at 320 px. Each measured block sits in a whole-pixel-height
container, or two identical layouts an eighth of a pixel apart round to tops
one pixel apart. test_tmdb_show_director.py covers the credit.
docs/mediacenter.md §10.4.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_014UtzVrzM7e2tG9fSpkR9ML
</content>
</entry>
<entry>
<title>fix(node): bound and tighten the chat link-preview SSRF surface</title>
<updated>2026-09-01T17:09:20Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T17:09:20Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=0808d594a371e7caea54f45a71db586160ae75ad'/>
<id>urn:sha1:0808d594a371e7caea54f45a71db586160ae75ad</id>
<content type='text'>
The link-preview fetch is an outbound request to an address a member
chose. safe_url() already blocked non-public addresses and re-checked
each redirect hop; this adds the parts that were missing:

- Rate limit. `_do_link_preview_request` was reachable by any member
  with no ceiling, so a member — or a hub minting tokens for many
  accounts — could drive unbounded outbound HTTP from the operator's
  machine (amplification / DoS / on-demand IP disclosure to arbitrary
  hosts). Now bounded per connection (15) and node-wide (60) over a
  60 s window; only a real fetch counts, a cache hit is free, and over
  the ceiling the reply is a plain `ok: false` (bare link), not cached.

- Port allowlist. safe_url() passed `parts.port` straight through, so
  a member could aim the node at `http://&lt;public-host&gt;:&lt;any-port&gt;`.
  Restricted to {80, 443, 8080, 8443} — every real OpenGraph page,
  none of SSH / mail / DB / cache / search / admin ports.

- DNS rebinding. The connection's actual peer address is now
  re-checked against the public-address rule (`_reject_if_rebound`),
  so a name that resolves clean and then to something internal does
  not get its body read. Best-effort (no `network_stream` extension,
  no check); a full literal-pin is noted as remaining hardening.

- Decompression bomb. `_downscale` now refuses an image whose header
  dimensions exceed ~40 MP before convert()/thumbnail() decode it.

Third security review, finding M3.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_011pG75yGK3NthNfyjH74omG
</content>
</entry>
<entry>
<title>fix(node): bring the QUIC chat and stream handlers to WebRTC parity</title>
<updated>2026-09-01T16:56:12Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T16:56:12Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f8598f57222028ba5600c9be507394dbcbb6f459'/>
<id>urn:sha1:f8598f57222028ba5600c9be507394dbcbb6f459</id>
<content type='text'>
The unified handshake reached QUIC in Phase 11.5, but the chat and
stream handlers did not get the authorization rules the WebRTC path
gained at the same time:

- _do_chat_message_sync took `sender_id` from the wire, so an
  authenticated peer could post as anyone (NS6 / M2a). It is now the
  authenticated session's id, always.
- chat used a connection-global peer registry and read chat_store from
  the top-level context, so on a multi-group node a message from one
  group fanned out to peers of the others (M2b / H1). Both are now
  resolved per group via _peer_registry() / _group_ctx(), mirroring
  the WebRTC path. The QUIC peer set is kept separate from the WebRTC
  one in the same group context — the two session types have
  different _send signatures and no cross-transport fan-out is wired.
- _do_stream_segment_sync ran `subprocess.run(timeout=30)` on the
  event loop with no concurrency cap, so one request stalled the whole
  node and any member could fork-bomb it with ffmpeg (M2c). Extraction
  now runs in a thread behind a small semaphore, spawned as a tracked
  task (cancelled on connection_lost).

Also corrects the stale docstring claiming C6 is still open here — the
GEK proof has been enforced on this transport since 11.5.

No behaviour change for shipping clients: none speak QUIC, and the
listener is off by default (previous commit).

Third security review, finding M2.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_011pG75yGK3NthNfyjH74omG
</content>
</entry>
<entry>
<title>fix(node): CLI member invite now registers hub membership and enforces code</title>
<updated>2026-09-01T09:07:05Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T09:07:05Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=0443cf8aaab58a54c73d28cafca5d5d8a52605e6'/>
<id>urn:sha1:0443cf8aaab58a54c73d28cafca5d5d8a52605e6</id>
<content type='text'>
Two bugs fixed:

1. `meshbay-node member invite &lt;user&gt;` created a local roster invite
   but never told the hub to add the user to group_members, so the
   group was invisible in the SPA. The node now calls
   POST /v1/groups/{id}/members/{username} after creating the invite,
   and the hub endpoint accepts node-scoped tokens (the admin_id
   check is the real authorization guard).

2. The WebRTC handshake let a previously-pinned user reconnect without
   a code even when a new invite was pending (e.g. after leave + re-invite).
   Now any pending invite forces code entry, regardless of existing
   member/pin status.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
