<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/static/i18n.js, branch 0.2</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.2</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.2'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-08-14T10:42:51Z</updated>
<entry>
<title>feat(client): Argon2id for the keypair bundle, and remove the backup toggle</title>
<updated>2026-08-14T10:42:51Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T10:42:51Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=86563d5db0ae19fc58336b71a5c29a8712973590'/>
<id>urn:sha1:86563d5db0ae19fc58336b71a5c29a8712973590</id>
<content type='text'>
Two corrections to yesterday's judgement, in the order they matter.

**The toggle is gone.** Asked to make the remote key backup optional, I shipped a
setting whose "off" position meant: no second browser, ever, and clearing your
storage destroys the account. I wrote the warning that says so without drawing
the conclusion. A control whose only effect is to break the ordinary case is not
a control, and removing an exposure by removing the feature is not a fix. Every
browser backs its keys up again, unconditionally.

**The exposure is fixed where it actually lives: the KDF.** The keypair bundle
rests on every node whose group its owner joins, protected by the passphrase
alone (finding C4). It used PBKDF2-SHA512 at 600k — compute-only, which is
exactly what a GPU eats. Measured on this machine: PBKDF2 600k costs 241 ms and
Argon2id 64 MB/t=3 costs 322 ms, near enough the same honest work, except only
one of them forces an attacker to find 64 MB per guess.

So the bundle key is now Argon2id 64 MB / t=3 / p=1, via a vendored WebAssembly
build (no external host — the CSP forbids one, and 12.2 will tighten it further).
Parameters chosen by measurement through that build: 19 MB is OWASP's floor at
118 ms, 256 MB is 1.3 s and too slow for a phone, 64 MB sits where a login should.

What this buys, stated honestly: cracking a bundle yields the owner's identity
keys, and with them content on OTHER nodes and the ability to sign as them — not
the content on the operator's own node, which they host in the clear by design.
Argon2id raises that price steeply; it does not remove it, and a weak passphrase
still loses. Hence the floor raised to 12 characters and ~60 bits in the same
breath, which can only be enforced client-side: with the password split (T1) the
hub never sees a passphrase.

Migration is automatic and invisible. Bundles carry an "MBK2" marker; the old
form is still readable, and is re-encrypted the first time a browser backs it up.
Both keys are derived at sign-in, because which one a bundle needs is only known
once it is read and the passphrase is deliberately not kept around.

Two implementations of the KDF now exist — the browser's WASM and argon2-cffi in
QE — so a parity test holds them byte-identical. A disagreement would not look
like an error; it would look like an account nobody can open.

keypair_bundle_delete stays, without a UI. It is the mechanism behind withdrawing
your data from a node, exercised end to end, and it will belong to a deliberate
"forget me on this node" action rather than a setting that quietly disables
multi-device.

Verified against the live deployment: the full workflow passes, including
recovering keys on a second client from the passphrase alone.

Tests: 341.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(client): make the key backup a choice, and raise the passphrase floor</title>
<updated>2026-08-14T10:06:31Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T10:06:31Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=2caa93dbc06161b5d3f776a204ac8d921df92126'/>
<id>urn:sha1:2caa93dbc06161b5d3f776a204ac8d921df92126</id>
<content type='text'>
Two things the multi-browser story made obvious.

**The backup is now opt-out.** Keys are kept, encrypted with the passphrase, on
every node whose group you join — that is what lets a second browser recover
them, and it is finding C4: a PBKDF2-protected blob on other people's disks,
attackable offline at the speed of PBKDF2, which is memory-light and therefore
cheap on a GPU. Until now everybody paid that cost, including people who will
only ever use one browser and get nothing back for it.

Settings → "Use this account on other devices". Turning it off does not merely
stop future uploads: the next connection to each node withdraws what that node
already holds (new keypair_bundle_delete, which only ever deletes the caller's
own, taken from the authenticated session and never from the message). The
warning says plainly what it costs — clearing the browser then loses everything
encrypted for that account, with no recovery, which is the point of choosing it.

Default is on. Silent, unrecoverable key loss is worse for an ordinary user than
an exposure the roadmap already tracks, but that is a judgement call and it is
now visible and reversible instead of implicit.

**Passphrase floor 8 → 12 characters, plus a strength estimate** shown while
typing, with a refusal below ~60 bits. This number matters more here than in
most applications: it is what stands between a node operator and your identity
keys. It has to live in the client — with the password split (T1) the hub never
sees a password and cannot enforce anything about one — so the UI says why it
is asking, rather than nagging.

The estimator is deliberately conservative and dependency-free: character
classes and length, penalised for repetition and for the handful of patterns
everyone tries.

Verified against the live deployment: withdrawing the backup leaves a second
browser unable to recover anything, which is exactly what it promises, and
re-enabling restores it.

Tests: 338.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(node)!: the node wraps the group key — closes H3 and M3</title>
<updated>2026-08-13T23:27:21Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-13T23:27:21Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f15efd23f66c521ca9206789482bb38e7326eeb4'/>
<id>urn:sha1:f15efd23f66c521ca9206789482bb38e7326eeb4</id>
<content type='text'>
The invite flow fetched the invitee's pk_x25519 from the hub and wrapped the
GEK for whatever came back (app.js:1466, and gek-init did the same server-side).
The hub is the key directory, so a hub answering with its own key was handed the
group key by an honest member following the protocol exactly. No forgery, no
injection, nothing for the client to notice. That was H3.

The fix is not safety numbers. Nobody reads the directory any more:

  - the node holds the GEK and wraps it itself, on every connection, for the
    X25519 key the joiner signed with their Ed25519 identity in one transcript
    (meshbay:join:v1), so the identity key vouches for the encryption key;
  - identities are bound to accounts by a one-time code the hub never sees —
    40 bits, single use, one account, bounded per connection AND node-wide;
  - the node's own roster decides who may receive the key. Hub membership lets
    someone reach a node; it no longer gets them anything. A hub that invents an
    account and mints it a token is answered not_authorized_for_group.

Safety numbers would have made substitution detectable by a human who checks, at
the moment there is nothing to check against — first contact. Removing the lookup
makes it impossible, and costs the user one code to pass along.

M3 falls out of the same work. The daemon auto-pinned its own keystore key as
admin_pk_ed25519 while the browser signs with the user identity key, so every
privileged operation failed closed with a signature error that looked like a bug
somewhere else; the demo only worked because a deploy script overwrote the value.
Authority now comes from the roster, established locally by `operator pair`.
Asking the hub for the operator's key — the obvious-looking fix — would have let
the hub install itself as node administrator.

BREAKING: gek_bundle_store is deleted, not gated. No member hands the node key
material at all, so C5b becomes structural rather than an authorization to check.
Existing stored bundles are still served, so current deployments keep working.

Also:
  - join_policy (invite|open) is read from node.toml, never from the hub — a hub
    able to declare a group open would be handed its key. Unknown group ⇒ invite.
  - admin signatures are verified against the roster on every check, so unpinning
    takes effect without a restart. admin_pk_ed25519 stays readable as legacy.
  - two C5b tests were rewritten, deliberately: they asserted that
    gek_bundle_store demanded an operator signature, and the message is gone. They
    now assert the stronger property. The file says not to fix these tests, so
    this is the record of why they changed.
  - a slice-1 bug found while writing slice 2: connect() never passed skEdB64, so
    pairing would have failed at runtime with no test able to catch it.

Tests: 152 node+common here, including an end-to-end DataChannel run where a
member who has never held the group key redeems a code in the pre-proof window
and receives the key wrapped for a key only they can open.

Design: docs/invite-pairing-v1.md

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(client): pin node identities on first use — closes 11.5.8</title>
<updated>2026-08-13T12:32:47Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-13T12:32:47Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=38ad6c54f8c8a9180f0f7522dead38e57760d55e'/>
<id>urn:sha1:38ad6c54f8c8a9180f0f7522dead38e57760d55e</id>
<content type='text'>
The client verified the node's Ed25519 signature but did not remember which key
it had seen, so a substituted node was caught only by its lack of the GEK.

Trust On First Use: the node's public key is recorded per node_id on the first
successful handshake and compared on every later one. A change is refused
outright — strict, per operator decision. A warning users can click through is
decorative, and this is the SSH known-hosts tradeoff taken deliberately.

Scope, stated honestly: with C6 closed this is defence in depth, not the primary
control. A substituted node already fails the GEK proof. Pinning covers the case
where an attacker HAS the group key — an ex-member, or a leaked GEK — and swaps
the node underneath, which the proof alone cannot distinguish from the real one.

Strict refusal needs an escape hatch or it is a dead end: a node operator who
reinstalls and loses their keystore generates a new pk_node and would otherwise
lock out every member. Settings gains a "Node identities" section showing the
pin count and clearing them, with copy telling the user to verify out of band
first. Also exposed as MeshBayTransport.clearNodePin() for the native client.

Tests: hub+common green; all five static JS files syntax-checked.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat: Phase 12 — P2P crypto material, password split, node Ed25519 auth</title>
<updated>2026-08-13T01:56:30Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-13T01:56:30Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f0248975908ad670fa8a820f865bf22ea8d0172d'/>
<id>urn:sha1:f0248975908ad670fa8a820f865bf22ea8d0172d</id>
<content type='text'>
Baseline commit capturing in-progress Phase 12 work that was already present
in the working tree (uncommitted) before the Phase 11.5 security remediation
begins. Committed as-is, without review or modification, so that remediation
changes arrive as a separable diff.

Contents: BundleStore (P2P GEK + keypair bundles), password split
(auth_key / bundle_key), node Ed25519 auth (POST /v1/nodes/auth, node-scoped
JWT), GEK-HMAC handshake proof with DTLS channel binding, Ed25519 admin
challenge-response, node local admin UI rewrite, browser key persistence.

Not authored in this session — captured to establish a baseline.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat: Phase 10c — MSE video streaming (real-time playback)</title>
<updated>2026-08-11T14:47:01Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-11T14:47:01Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=e608b95bf1fe225915eaeafca2a933f687844733'/>
<id>urn:sha1:e608b95bf1fe225915eaeafca2a933f687844733</id>
<content type='text'>
Replace download-then-play VideoPlayer with MSE (MediaSource Extensions)
streaming. Node remuxes to fMP4 via ffmpeg, probes codecs with ffprobe,
and sends encrypted segments over DataChannel. Browser decrypts and
appends to SourceBuffer — playback starts within seconds.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(ui): upload chunk size, cached file display, chat names, file delete, inline thumbnails</title>
<updated>2026-08-11T14:12:16Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-11T14:12:16Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b50750466622dd6cda0fc084d39dfce000ad0081'/>
<id>urn:sha1:b50750466622dd6cda0fc084d39dfce000ad0081</id>
<content type='text'>
- Upload chunks capped at 64KB to avoid WebRTC DataChannel max-message-size
- Show cached files immediately while WebRTC connects (tabs visible during connection)
- Persist sender_name in chat store (SQLite) — no more UUID display in history
- File delete action in menu (node admin only, enforced server-side)
- FILE_DELETE / FILE_DELETE_ACK MNP message types
- Inline image thumbnails in chat attachments (download+decrypt, Signal-style)
- Member panel: "Owner" label instead of "Group admin" to avoid hub/group admin confusion
- Create group page: hint about needing a node
- Refresh index after chat file attachment upload

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(ui): group creation error, chat UUIDs, file preview, action menu</title>
<updated>2026-08-11T13:39:09Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-11T13:39:09Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=3ee7f817d532208df1d95ecd86def12e5bd53a18'/>
<id>urn:sha1:3ee7f817d532208df1d95ecd86def12e5bd53a18</id>
<content type='text'>
Bug fixes:
- Group creation "[object Object]" error: removed dead pkcs8 import code
  that threw before GEK wrapping, added array detail handling in hubFetch
- Chat shows usernames instead of UUIDs (sender_name passed through node)
- Join button: navigate to group on "Already a member" instead of error

UI improvements:
- Loading spinner animation for async states (connecting, fetching)
- File action menu (3-dot dropdown: View, Download, Play)
- Click filename to preview inline (images, text/code files)
- FilePreview overlay for images and text files
- Chat file attachment button (upload to node + structured message)
- Chat attachment display (icon, filename, size)
- Member panel: "Group admin" badge instead of plain "Admin" text

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat: Phase 10b — Self-service UI (group create/join, upload, IndexedDB, search)</title>
<updated>2026-08-11T12:55:52Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-11T12:55:52Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=cc90dc943fcd0e7bbf52674fb3f95ff097026f4a'/>
<id>urn:sha1:cc90dc943fcd0e7bbf52674fb3f95ff097026f4a</id>
<content type='text'>
Six self-service features for the web SPA:
- Group creation UI with GEK auto-generation (AES-256-GCM ECIES)
- Member management + invite by username (GEK wrapping for invitee)
- Open group self-join flow (POST /v1/groups/{id}/join)
- File upload client→node (FILE_UPLOAD MNP type, .uploads/ staging)
- IndexedDB caching of group file indexes (instant display on revisit)
- Cross-group file search (SearchPage, pure client-side on cached indexes)

11 new tests (166 total): 8 group self-service + 3 AES GEK wrap/unwrap.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(hub): Phase 10.5–10.8, 10.10 — notifications, settings, search, version</title>
<updated>2026-08-11T10:40:13Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-11T10:40:13Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=edde9e441fb6b84e9d56215d6e2a8d9338b8f962'/>
<id>urn:sha1:edde9e441fb6b84e9d56215d6e2a8d9338b8f962</id>
<content type='text'>
- 10.5: Notification model + CRUD API (list, mark read, mark all read)
  Triggered on: group invite, role change, suspend/unsuspend
- 10.6: SettingsPage shows role, per-group notification mute (localStorage)
- 10.7: GET /v1/groups?q= search filter (ilike on name)
- 10.8: NotificationFeed on home page + bell with unread badge in navbar
- 10.10: GET /v1/hub/version endpoint for client update checks
- 8 new tests (test_notifications.py), 155 total

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