<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-node, branch 0.10</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.10</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.10'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-09-01T13:34:22Z</updated>
<entry>
<title>chore: release 0.10.0</title>
<updated>2026-09-01T13:34:22Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T13:34:22Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=cea3020534b320435226c8a8d99886dfb310f8e9'/>
<id>urn:sha1:cea3020534b320435226c8a8d99886dfb310f8e9</id>
<content type='text'>
All three packages (common, hub, node) bump 0.9.0 -&gt; 0.10.0 together.
Highlights since v0.9.0: email verification for registration, email
change and invitations; passphrase change and account recovery;
reCAPTCHA v2 on Register and Password Reset; node JSON-only control API
with the Node page absorbing the admin dashboard; WebRTC STUN fallback
fix; assorted hub UI fixes.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01QNPfgH6VWcRzJDZGuzy1jJ
</content>
</entry>
<entry>
<title>refactor(node): JSON-only control API, Node page absorbs the admin dashboard</title>
<updated>2026-09-01T12:08:32Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T12:08:32Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=cfc91e0a424163869c64d30e55d55a53f18a3dbf'/>
<id>urn:sha1:cfc91e0a424163869c64d30e55d55a53f18a3dbf</id>
<content type='text'>
Remove the node daemon's server-rendered admin UI (GET / and /audit, the
_render_* helpers and inline templates) and the `meshbay-node ui` CLI verb.
The loopback control API stays; it is now JSON only, ruff-clean, and 453
lines (was 1074). Also drop three never-wired endpoints (/api/config,
/api/chat/history, /ws/chat, plus broadcast_chat) and the pointless
18000/tcp firewall profiles.

The desktop client's Node page (static/node-page.js) takes over what the
dashboard showed, reorganised into six tabs (Overview, Groups, Roster,
Peers, Audit, Settings):
- Overview: version, node id, QUIC port, hub, index-cache maintenance
- Roster: node-wide view with unpin
- Peers and Audit: auto-load on open, no Load button
- Audit: real usernames and group names (resolved from the roster and
  node.toml), Previous/Next pagination newest-first, Export CSV of every
  matching row
- Settings: node settings, STUN, ICE, denylist, then Unlink from hub

Backend: audit.get_entries gains `offset`; /api/audit and /api/peers
resolve ids to names via a new _display_names helper; CSP tightened to
default-src 'none' now that no HTML is served. draft-v6 sections 2.11 and
2.12 corrected -- the Node page uses the loopback API, not MNP.

One capability is intentionally dropped: browser-based admin on a headless
server. The CLI covers every operation there.

See docs/refactor-node-ui.md.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01MQCaZnde4Bjjdu84dhSuF5
</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>
<entry>
<title>fix(node): make WebRTC STUN fallback actually use every configured server</title>
<updated>2026-08-31T23:13:43Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-31T23:13:43Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b3b48a43a33f936be18a3daa22d307c144b60e22'/>
<id>urn:sha1:b3b48a43a33f936be18a3daa22d307c144b60e22</id>
<content type='text'>
aiortc's connection_kwargs() keeps only the first STUN URI from
RTCConfiguration.iceServers ("only a single STUN server is supported"),
and aioice.ice.Connection has a single stun_server field. So the node's
four default STUN servers -- and anything added on the Node page or with
`meshbay-node stun add` -- collapsed to stun:stun.l.google.com:19302. When
that one server was slow or unreachable from the node, ICE gathering
(get_component_candidates, timeout=5) burned its full 5 s with no
server-reflexive candidate, adding seconds to every browser connection.
The multi-server fallback of draft-v6 s2.12 was configuration only.

transport/stun_multi patches aioice.ice.server_reflexive_candidate (same
monkey-patch technique ice_filter.py uses on get_host_addresses) so a
single ICE gather races the STUN binding request against every configured
server on the one bound socket and takes the first answer. One reachable
server anywhere in the list now yields a reflexive candidate in one RTT.

- daemon: install_stun_multi() alongside install_ice_filter()
- ops.set_node_settings: push the list to stun_multi.set_servers() so the
  CLI / Node-page hot-swap takes effect without a restart
- webrtc_server.handle_offer: log ICE gather time and srflx count
- test_stun_multi.py: fan-out, first-answer-wins, all-fail, empty-list
  fallback, DNS failure

Verified end to end with a real RTCPeerConnection: with a black-hole STUN
server first in the list, gathering still completes in ~0.07 s with full
srflx candidates (previously a 5 s stall).

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01BSsQhfxEAhwi4nqc4hASmq
</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>chore: bump version to 0.9.0</title>
<updated>2026-08-31T10:05:24Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-31T10:05:24Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f4c6628c8e85513d9fd110ead95682368a15a0fd'/>
<id>urn:sha1:f4c6628c8e85513d9fd110ead95682368a15a0fd</id>
<content type='text'>
Packaging system complete and verified on Ubuntu 26.04 and Fedora 44.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(ui): configurable ICE interfaces in the Node page</title>
<updated>2026-08-30T20:36:56Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-30T20:36:56Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=2d6c276734fd19f80edf84cdb95f91a929df6365'/>
<id>urn:sha1:2d6c276734fd19f80edf84cdb95f91a929df6365</id>
<content type='text'>
The ice_interfaces setting (auto-exclude vs explicit whitelist) is now
editable from the Node page, persisted via the settings API and roster,
and hot-swapped at runtime by re-installing the aioice filter.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</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>chore: scrub copyrighted names from tests, comments and docs</title>
<updated>2026-08-30T15:05:15Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-30T15:05:15Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=d5fc45ff907e618d884a8d2d91c1f2b45e6898d1'/>
<id>urn:sha1:d5fc45ff907e618d884a8d2d91c1f2b45e6898d1</id>
<content type='text'>
Real franchise / show / release-group names had crept back into test
fixtures, code comments, a docstring and docs/mediacenter.md while fixing
the saga-match and misclassification bugs. Replace them all with invented
placeholders ("Some Saga", "A Different Show") and shape descriptions
("a franchise-origin film", "a 3-season show"). Behaviour and assertions
unchanged; 738 node tests still pass.

Record the rule in CLAUDE.md so it stops recurring.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
</content>
</entry>
<entry>
<title>fix(node): stop a numbered saga all matching its first film</title>
<updated>2026-08-30T14:31:34Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-30T14:31:34Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=854b76338072979526ce36fbc176c82f6fc5d0bd'/>
<id>urn:sha1:854b76338072979526ce36fbc176c82f6fc5d0bd</id>
<content type='text'>
Every "&lt;Saga&gt; Episode &lt;N&gt; - &lt;subtitle&gt;" file in a numbered franchise
resolved to the series' first entry (a real, older film). `sequel_variants`
stripped "Episode &lt;N&gt;" and offered the bare "&lt;Saga&gt;" as a candidate query;
that matches the first film's original_title at ratio 1.0 and beat PASS 1's
correct-but-lower hit. A franchise's bare name is very often a real,
different film.

When a Part/Episode/Chapitre/… keyword carried the index, sequel_variants
no longer emits the bare base — only "&lt;base&gt; &lt;digit&gt;" and "&lt;base&gt; &lt;roman&gt;".
Without a keyword ("&lt;Franchise&gt; 3") the bare base is still offered, so that
fix is untouched. Verified live against TMDB: the franchise's episodes each
resolve to their own entry; the earlier numbered-sequel, two-part-film and
franchise-subtitle regressions all hold.

docs/mediacenter.md §10.3.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
</content>
</entry>
</feed>
