<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/static/transport.js, 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>fix: drop the retired Mozilla STUN server from the defaults</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=08b0f8d7c457e5e58a31bea2b2e43c9f398a6923'/>
<id>urn:sha1:08b0f8d7c457e5e58a31bea2b2e43c9f398a6923</id>
<content type='text'>
stun.services.mozilla.com no longer resolves — Mozilla shut the service
down — so every ICE gather waited out a DNS timeout on it. Removed from the
node defaults (config.py), the browser defaults (transport.js) and the Node
page's "reset to defaults" (node-page.js). Google (two endpoints) plus
Cloudflare still give two-provider coverage against a single outage, which
is the §2.12 resilience claim. draft-v6 §2.12 updated.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(client): resolve STUN hostnames in the main process</title>
<updated>2026-09-03T23:47:45Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-03T23:47:45Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=5022f2be149e40ac5bdd5fc362e926f3edcf44f0'/>
<id>urn:sha1:5022f2be149e40ac5bdd5fc362e926f3edcf44f0</id>
<content type='text'>
Chromium's P2P socket manager failed every STUN hostname with
ERR_NAME_NOT_RESOLVED in a restricted-resolver environment (a Windows KVM
guest), even though its own general network stack, the OS resolver and
Node's resolver all resolved the same names -- and mapping the names to IPs
with --host-resolver-rules changed nothing, so it is not ordinary
resolution. WebRTC was left with no server-reflexive candidate.

The desktop client now resolves the STUN hostnames in the main process
(`ice:resolve-stun`, Node's dns.resolve4) and hands `transport.js` the
IP form; a name that will not resolve (the decommissioned Mozilla host) is
dropped. In a browser there is no `meshbay` bridge and the hostnames are
used unchanged -- a browser resolves them fine, so that path is untouched.
Falls back to the hostname form if the bridge call throws.

Also, scoped to win32: disable WebRtcHideLocalIpsWithMdns, so the client
publishes its real local IP instead of a `.local` name the node's ICE stack
cannot resolve across the KVM bridge. Changes nothing on Linux/macOS.
Test-env workaround, revisit before release.

Co-Authored-By: Claude Sonnet 5 &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>fix(hub): route the chat ack to the request that asked for it</title>
<updated>2026-09-03T10:13:41Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-03T10:13:41Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=691c6ba4ef51085c89aeddbcabd5c733861eb56b'/>
<id>urn:sha1:691c6ba4ef51085c89aeddbcabd5c733861eb56b</id>
<content type='text'>
Typing a message froze the Chat tab: the composer stopped taking clicks and
keystrokes, the message never appeared, and it was there all along on the next
visit to the tab.

The node answers a chat message with a bare {"type": "ack"} -- no request id,
no type of its own -- so _dispatch had nothing to match it on and left it to
the arrival-order guess at the end of the function. That guess is wrong the
moment anything else this browser asked for is still waiting: the ack went to
*that* request, and the chat send waited out _sendAndWait's own 30s timeout.
Since the composer is disabled while a send is in flight, that reads as a
frozen tab; the node had stored the message and answered, into somebody else's
promise.

An outstanding request is the ordinary case, not a rare one. The node refuses
an unknown file_id with a bare `error`, which names no request either and so
reaches none, leaving the Videos tab's media_meta_req in _pending for the full
30s. That is the one that was live when this was found.

- `ack` is now matched by request type: chat_msg, or the keypair-bundle store
  and delete, which name themselves in `detail`. A node naming neither still
  has its reply placed rather than dropped.

Every line of chat-app.js is correct and every routed message in transport.js
is routed correctly -- the defect is in the seam, so tests/harness/
chat_send_probe.py drives the two together: the real ChatPanel over the real
MeshBayTransport, with only the DataChannel replaced by a stand-in answering
what the node answers. test_chat_send.py asserts against it, and with the fix
reverted all three of its tests fail on the three visible halves of the defect
-- the composer still disabled, the message absent, and the ack resolving the
unrelated request.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01GFF4BL8VSKrghkSLzCrTVs
</content>
</entry>
<entry>
<title>feat: passphrase change and account recovery (auth-confirm)</title>
<updated>2026-08-31T23:03:43Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-31T23:03:43Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=fe30860c58e0f1b1efd457ff5eb5146d1e592da0'/>
<id>urn:sha1:fe30860c58e0f1b1efd457ff5eb5146d1e592da0</id>
<content type='text'>
The passphrase derives two independent client-side values: auth_key (the
hub verifier) and bundle_key (AES-GCM key for the per-node identity
bundles, which live on nodes and never on the hub). Changing or
recovering a passphrase is therefore two operations — swap the hub
verifier, and re-wrap every reachable node's identity bundle.

Flow A — change a known passphrase (Profile page)
- POST /v1/users/password re-proves the current passphrase, swaps
  pw_hash/salt/version, revokes every refresh token and returns a fresh
  pair so the tab that made the change stays signed in.
- MeshBayTransport.rewrapAllNodes: for every group's online node, connect
  with the old key, read the identity off the handshake, store it back
  under the new key. Returns updated / unreachable / failed so the UI can
  point at the operator-unpin fallback for the gaps. Always-shown
  confirmation dialog listing reachable and unreachable groups.

Recovery key
- keyderive.js generateRecoveryKey (32 random bytes, grouped Base32) and
  deriveRecoveryKey (HKDF-SHA256, domain meshbay:recovery:v1:&lt;username&gt;).
- Every per-node identity gets a second copy wrapped under the recovery
  key: keypair_bundles.bundle_enc_recovery (node-only column, added in
  _SCHEMA_KEYPAIR and via a PRAGMA-guarded ALTER for existing DBs),
  carried on keypair_bundle_store / _resp. MNP 0.13 -&gt; 0.14, additive.
- session.recoveryKey is persisted in IndexedDB (slot rk) and lazy-loaded
  on connect, so a group joined in any later session still leaves a
  recovery copy.
- Shown once at registration; optionally folded into the verification
  e-mail as a pass-through the hub never stores or logs, with an opt-out.
- Profile -&gt; Recovery key re-loads R and backfills every reachable node
  via rewrapAllNodes in bundleKey mode (no passphrase re-entry).

Flow B — recover a lost passphrase (#/reset, linked from sign-in)
- POST /v1/users/password/reset-request {username, email}: both must be
  the pair on file, checked against the blind email_hash (never
  decrypted). A mismatch — wrong e-mail, unknown username, non-active
  account — takes the identical no-op path (no code, no mail, same 200),
  so it reveals nothing and cannot be used to spray reset mail from a
  username alone. 5/min, 1-hour single-use code.
- POST /v1/users/password/reset {username, code, new_auth_key}: same
  expiry / attempts / single-use checks as e-mail verification; revokes
  every session and deletes every registered device key so a stored one
  cannot sign back in past the reset.
- ResetPasswordPage: request code -&gt; code + optional recovery key + new
  passphrase -&gt; reset + sign-in -&gt; fan-out. connect() falls back to the
  recovery-wrapped copy when the passphrase key cannot open bundle_enc.
  Without a recovery key: sign-in is restored and each group needs the
  operator-unpin fallback.

Supporting fixes (found in live testing)
- member unpin now also deletes the keypair bundle; connect() mints a
  fresh identity when handed a bundle it cannot open (unless _rewrapOnly,
  set by rewrapAllNodes), so a rejoin completes instead of dead-ending
  before the invite-code prompt.
- A browser with no bundle key gets a passphrase prompt on the group page
  instead of a "go back to the browser you registered on" message.
- RegisterPage / LoginPage / ResetPasswordPage trim the username so every
  key derivation matches the hub's stored form.

Docs: docs/auth-confirm.md. Locale keys across all ten catalogues.
Tests: test_password_change, test_password_reset, test_recovery_email,
test_recovery_key, test_rewrap_fanout, test_bundle_store_recovery, plus
additions to test_admin_ops_mnp and test_webrtc_transport. Hub suite 492
passed; node suite 741 passed (the lone test_packaging_units failure is a
pre-existing RPM-spec flake, reproducible on main).

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01GGkxJW9br8Y9bhT8ywJ3oc
</content>
</entry>
<entry>
<title>feat: configurable STUN server fallbacks for WebRTC ICE</title>
<updated>2026-08-30T20:17:30Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-30T20:17:30Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=07e6e4271ea3a45e9cd364eb6ec05801a653d9e8'/>
<id>urn:sha1:07e6e4271ea3a45e9cd364eb6ec05801a653d9e8</id>
<content type='text'>
The WebRTC transport relied on a single Google STUN server — if it was
unreachable, ICE gathering waited the full 4s timeout. Now four public
servers are used by default (Google ×2, Cloudflare, Mozilla), configurable
via node.toml, the Node page UI, and the CLI (meshbay-node stun list|add|
remove|reset). Changes are hot-swapped on the live transport.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(node): editable node settings in the Node page (D5)</title>
<updated>2026-08-29T16:52:53Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-29T16:42:43Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=1f8a52484412b48205e5ff6aac506428e2fb77ed'/>
<id>urn:sha1:1f8a52484412b48205e5ff6aac506428e2fb77ed</id>
<content type='text'>
Expose invite_ttl_hours, pair_ttl_hours, device_request_ttl_minutes,
max_concurrent_streams and transcode_incompatible_video in the Node
management panel. Changes are applied immediately via roster.db and
written back to node.toml so they survive a DB wipe. On startup,
roster overrides take precedence over node.toml defaults.

Draft v6 §2.11 documents the design; MNP gains node_settings_set /
node_settings_set_ack for the browser path.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat: V13 — per-card "re-match this one file" (MNP 0.13)</title>
<updated>2026-08-29T13:57:06Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-29T13:57:06Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=4d167e9f958d26c1db920274974ae446426928e3'/>
<id>urn:sha1:4d167e9f958d26c1db920274974ae446426928e3</id>
<content type='text'>
A one-click alternative to the full "Fix match" search-and-pick flow, and
reachable without SSH (`meshbay-node video rematch` clears a whole group).

- MNP 0.13: tmdb_rematch / tmdb_rematch_ack (additive — an older node
  logs "unknown type", the button just does nothing). OP_TMDB_REMATCH,
  signed like tmdb_override (media_cache is shared node-wide).
- media_cache.drop_tmdb_match(file_id): forgets the match AND the
  override marker — deliberately stronger than clear_file_tmdb, since the
  operator is explicitly asking for a fresh resolution.
- webrtc_server: _do_tmdb_rematch / _admin_exec_tmdb_rematch, dispatch +
  admin-response routing, broadcasts tmdb_rematch_ack.
- transport.js: rematchTmdbMatch(fileId, signFn); 'tmdb_rematch' in the
  admin-op allowlist; tmdb_rematch_ack handled like tmdb_override_ack.
- video-app.js: a "Re-match" button beside "Fix match" in the detail
  modal (isNodeAdmin), then bumpMediaMetaGeneration(). video.rematch_one
  key in all ten locales.

docs/mediacenter.md §10.1: V8–V13 marked done.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
</content>
</entry>
<entry>
<title>feat: node workflow redesign — wizard auto-config, reset, MusicBrainz contact</title>
<updated>2026-08-29T09:15:54Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-29T09:15:54Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b7733e812fadd6007976d262bd1d793572a36ba7'/>
<id>urn:sha1:b7733e812fadd6007976d262bd1d793572a36ba7</id>
<content type='text'>
Wizard (Electron):
- Auto-provisions node config (hub URL + username) from logged-in user
- node:start handles both cold start and restart of misconfigured daemon
- Waits for daemon to reach 'running', auto-links node key on hub
- probeNode accepts intermediate states for wizard progress feedback

Reset (meshbay-node reset):
- Unlinks node key from hub (DELETE /me/node_key, best-effort)
- Stops and disables daemon (systemctl --user disable --now)
- Erases ~/.config/meshbay, ~/.local/share/meshbay, ~/.local/state/meshbay

MusicBrainz contact:
- Resolved from owner's hub email instead of per-node roster config
- Removed musicbrainz_contact UI and WebRTC handshake field
- Removed set_musicbrainz_contact/musicbrainz_contact from roster

Node pairing:
- Added operator pairing banner on NodePage
- Added operator_paired flag to list_groups

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(transport): chat_hist_resp dispatch skipped when older pending entries exist</title>
<updated>2026-08-28T15:54:43Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-28T15:54:43Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=09a007937f03fad04a390323f3817f1ae7d7c2a9'/>
<id>urn:sha1:09a007937f03fad04a390323f3817f1ae7d7c2a9</id>
<content type='text'>
chat_hist_resp checked only the single oldest pending entry across all
types. When switching tabs, keyed requests (media_meta_req, file_req)
from Videos/Music/Photos stayed in _pending and were older than the
chat_hist entry, so the response was dropped and the chat appeared empty
on return. Now scans for the first chat_hist entry instead. Same fix
applied to index_sync dispatch for consistency.

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