<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src, 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>fix(hub): adjust no-group prompt and post-create wizard step</title>
<updated>2026-09-01T13:30:53Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T13:18:27Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=5c25e5a4f42f2eb05c525a284cd584786324b6e1'/>
<id>urn:sha1:5c25e5a4f42f2eb05c525a284cd584786324b6e1</id>
<content type='text'>
No-group home/explore message: when the hub offers no public groups,
drop the "browse public groups" invitation and just ask to be invited
by an admin. New home.invite_only key added to all ten catalogues.

Create-group wizard done step: reword the message to point at inviting
members, and send the button to the group's Settings tab (where the
invite form lives) instead of a stale /groups/&lt;id&gt; path that never
matched the router. The landing tab is a one-shot session hint, so the
usual per-user default-tab preference is left untouched.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01QNPfgH6VWcRzJDZGuzy1jJ
</content>
</entry>
<entry>
<title>fix(hub): stop Search-view video posters flickering to spinners</title>
<updated>2026-09-01T12:48:13Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T12:48:13Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b83803a91753b38435eb4d3be2e683daae9b4de9'/>
<id>urn:sha1:b83803a91753b38435eb4d3be2e683daae9b4de9</id>
<content type='text'>
The cross-group "Search files" view caps its WebRTC connection pool at
MAX_POOL_SIZE but pre-connected every indexed group, so any account with
more groups than the cap thrashed the pool. An evicted transport was
never removed from SearchPage's own groupConns map, so every tile of that
group kept a `_tRef` pointing at a closed transport; MediaThumb and
useMediaMeta bail on a disconnected transport with no retry, so posters
rendered for a moment then fell back to a loading spinner for good. Every
connect also fired the module-wide bumpMediaMetaGeneration(), clearing
every mounted tile's metadata across all groups and flickering the whole
grid through the warm-up walk. A one-group account (grenet) never hit it;
a many-group account (cbesson) always did.

- ConnectionPool takes an onEvict callback; SearchPage prunes groupConns
  and rebuilds the entry lists when a connection is evicted or closed.
- MAX_POOL_SIZE 3 -&gt; 12; the pre-connect walk is capped to it. Groups
  past the cap connect lazily when a tile scrolls into view (onNeedConn).
- connectGroup() no longer fires the module-wide meta/thumb generation
  bumps on a normal connect (kept for operator TMDB override/rematch).
  Per-group gen counter + concurrent-caller guard: a group's tiles
  refetch once per (re)connect, not once per mounting tile.
- MediaThumb/useMediaMeta take an optional reloadKey; each group's
  `_connGen` is threaded through so a tile refetches when its group
  reconnects instead of staying stuck on a spinner.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01W4Yj8EuXkjjYxbeS5kPd3U
</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(hub): remove "shown only once, hub never sees it" from recovery intro</title>
<updated>2026-09-01T10:46:43Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T10:46:43Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=38ce775e33e7773e8cc5232b0e11746ab05fb2ba'/>
<id>urn:sha1:38ce775e33e7773e8cc5232b0e11746ab05fb2ba</id>
<content type='text'>
The clause was redundant with the surrounding UI context.
Removed from all 10 locale files.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</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>feat(hub): reCAPTCHA v2 on Register and Password Reset pages</title>
<updated>2026-09-01T09:06:47Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T09:06:47Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b6c15f35d570d4f54901b811654991847502ca82'/>
<id>urn:sha1:b6c15f35d570d4f54901b811654991847502ca82</id>
<content type='text'>
Server-side verification module, CaptchaConfig in hub.toml,
captcha_site_key exposed via /v1/hub/info, useCaptcha() hook
in the SPA with stable DOM rendering (strength bar always present
to avoid Preact re-ordering the captcha widget). Native clients
(auth_key path) skip captcha. All 10 locales updated.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</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: email verification for registration, email change, and invitations</title>
<updated>2026-08-31T15:19:17Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-31T15:19:17Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=c6fd7ea89b6e0a96eb1d81989de891b4768b1044'/>
<id>urn:sha1:c6fd7ea89b6e0a96eb1d81989de891b4768b1044</id>
<content type='text'>
Registration now creates a pending account and sends a 6-digit code via
email; the account activates only after verification. Email changes on
the profile page follow the same flow. Group invitations send a
notification email to the invitee (without revealing their address to
the inviter) containing the invite code and hub link.

Backend: blind HMAC-SHA256 email index for uniqueness without decryption,
mail.py for localhost Postfix delivery, verification endpoints, cleanup
of expired codes and stale pending accounts, startup backfill of
email_hash for existing users.

Frontend: 3-phase register page, inline email change verification on
profile, invite-notify call with status display. All 10 locales updated.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</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>
</feed>
