aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/chat-app-settings.js
Commit message (Collapse)AuthorAgeFilesLines
* feat(chat): encrypt group chat under per-device epoch keys (MNP 2.0)Christophe Besson2026-09-071-1/+15
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Chat messages are sealed with AES-256-GCM under a key derived per group, per epoch, per *device*, and signed over the ciphertext with the device key the node pinned. The node relays and archives; it cannot read a message. There is no switch. MNP goes to 2.0 and MNP_MIN_SUPPORTED moves with it, so a 1.x peer is refused at the handshake with `version_too_old` rather than admitted and then unable to speak. An opt-in flag was designed and rejected: every node is a test node, so it would have bought nothing and left a plaintext branch reachable — C6's lesson one feature later. A test reads the source and refuses any code that consults a `chat_encrypted` setting. Not Sender Keys, and `senderkeys.py` is now documented as unused. With distribution under the group key and a node that serves history to devices which were not present, the node must retain each chain's earliest key, and a chain key at iteration i yields every message key from i on by pure HKDF — forward secrecy is zero either way. What the ratchet was left buying was stateful client code with silent failure modes, three of them reproduced: any member could sign as any other, a second device dropped the first's chain, and the skipped-key cache grew without bound. The reasoning is in docs/chat-sender-keys.md, which is the specification and the decision record. Epochs, not rotation: the epoch key is wrapped under the group key at delivery and never stored under it, so `gek_rotate` is a re-wrap. A group-key-derived archive key would have made every message ever sent unreadable on the first `member unpin`, which is the documented step after removing a member. A new epoch opens on member revoke/unpin, device revoke and `gek_rotate`; old epochs are kept and still delivered, so history stays readable to everyone who could already read it, and nothing anywhere deletes one. Three prerequisites this needed, each a live defect on its own: * The peer registry was keyed by user_id, so one account's second device evicted the first and the broadcast skipped recipients by account — a person's phone never saw what they typed on their laptop. * The handshake authenticated an account, never a device. `device_hello` (additive, signed, refused unless the key is a live device of this account in the node's own roster) is what lets the node refuse a member claiming somebody else's key. * `_admin_exec_file_delete` authorized against the exact uploading key, so device linking had already broken deleting your own file from your other device. It now authorizes against any non-revoked device of `uploader_id`. Found by driving the real panel over the real transport, not by reading source: `chat_keys_resp` was routed by arrival order and handed to an unanswered `media_meta_req` — the original frozen-tab defect in a message type that did not exist when that probe was written. And `_asText` had been deleted with an unrelated helper beside it; its only caller sits inside a promise the panel catches, so every conversation rendered empty with nothing in the console. Existing node data is migrated by QE/migration/migrate_chat_encryption.py (not versioned, per the QE rule), run with the node stopped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZZxYjz8YeWRz13xDi8LJr
* fix(client): drop the "No changes to save" hint beside SaveChristophe Besson2026-09-071-4/+0
| | | | | | | | | | | | | | | | | The hint was added alongside the .app-save control, when an inert Save was indistinguishable from an enabled one and the reader had no way to tell "nothing changed" from "this is broken". The control now reads as disabled on its own, so the sentence beside it is noise. Removing it also removes what only existed to carry it: the .app-save-row wrapper, its two style rules, and settings_app.no_changes in all ten locales. test_the_disabled_state_is_visually_distinct sliced the stylesheet on .app-save-row; it now stops at the closing brace of .app-save:disabled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvMdvLBG92jyhvD5pD6us
* fix(client): Save in the app panes was invisible, and Chat's never landedChristophe Besson2026-09-071-2/+5
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two defects behind one report — "you cannot always click Save, you do not notice, and it does not work". **It did not work, for Chat, systematically.** `_dispatch` resolves an admin ack against the pending request and returns, which is right for an op whose caller already knows the value it chose. Chat's pane calls `transport.setChatDirectory` itself, so nothing told `group-page` anything: the node saved it, every *other* connected client learned it from the broadcast, and the one that asked went on showing an unsaved-looking draft. Clicking Save again just re-sent it. Same shape as the root-ack bug, in a different message — so the set is now `BROADCAST_ACK_TYPES`, named for the property that makes it true, and covers `chat_directory`, `chat_link_preview` and `app_directories`. **You could not notice, because Save was not visibly a button.** It carried `btn btn-small btn-secondary`, and there is no `.btn` rule in the stylesheet at all — so it took `.btn-secondary`: no background, a transparent border, dim grey text. Enabled it already looked like a disabled control; disabled it was the same thing at 40% opacity. Measured on a real page: enabled is now accent on white, disabled is grey text on a plain border, and an inert one says why ("No changes to save") rather than leaving the reader to guess what the pane counts as a change. Videos' second Save — the TMDB one — is the same control, because two Save buttons in one pane that do not look alike is worse than either looking wrong. Verified by driving the real pane in Electron through the whole cycle: inert, pick a folder, live, save, and the node's answer coming back to disable it again. Four earlier readings said the enabled button was transparent; all four were taken inside python's directory-listing page, which the app never runs in — my scaffolding, not the code. Hub suite only: the node package is untouched by this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvMdvLBG92jyhvD5pD6us
* feat(client): Phase 2 — per-app settings panes, folder tree, multi-directoryChristophe Besson2026-09-061-0/+68
Each app's settings were inlined in `group-settings.js` — TMDB, MusicBrainz, and one folder picker per app, each with its own draft state and save handler saying the same thing about a different key. They are one file per app now, reached through the `apps.js` registry, and the page that renders them names no application at all: adding one is a registry entry and a settings file. The line between the two is what makes that true. What every app has — folders — the page does generically, through one `saveDirectories` bound to the app. What one app alone has, its pane does itself with the transport it is handed. An app that only needs directories touches neither `group-settings.js` nor `group-page.js`, which is `test_app_settings_plugin.py`'s subject. `settings-ui.js` exists because a pane importing the page that renders it is a cycle, and ES modules answer that with a temporal-dead-zone ReferenceError at first render — a component that silently does not appear, the fault already recorded in CLAUDE.md about hook ordering. The flat depth-indented `<select>` of every folder in the library becomes a modal tree. It asks the node for nothing: the tree is derived from paths the client already holds, so it shows exactly what the group's index contains and adds no folder-browsing protocol. For Chat's attachment folder — the one directory that is written to rather than read — read-only roots are greyed out, so the node's refusal arrives before the operator picks rather than when somebody sends a file. Videos and Music take a list of folders. A library on two drives could not be described before; the only recourse was pointing the app at a parent containing both, which pulls in everything else under it. The scalar shapes survive on the wire alone, for a node speaking MNP 1.0, and the client reads them as a one-element list. Two things the tests caught that I would not have: `test_asset_versioning` — six new modules were missing from `_ASSETS`. Reached through the registry rather than imported by name, they are exactly the files nothing else would notice changing, and a stale one is served from cache with no version bump. And `node --check foo.js` does **not** reliably report a module syntax error: it accepted `${/* ... */''}` — htm template syntax pasted into a plain object literal — and reported success. A `.mjs` copy forces the module parser and reports it. The suite had no syntax check at all, which is how that reached a file; `test_spa_syntax.py` does it for every module now, and pins that the loose path is not what it uses. Suite: 12 failures, all pre-existing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvMdvLBG92jyhvD5pD6us