aboutsummaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAgeFilesLines
* chore: bump version to 0.21.0Christophe Besson19 hours11-11/+11
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(spa): purging the chat asks firstChristophe Besson19 hours12-1/+27
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(spa): search hears a refused local network from the connectionChristophe Besson23 hours9-78/+101
| | | | | | | | On a Pixel, Chrome still lists the refused address when search gives up, so nothing tells it from a silent node. The connection given up on now runs to its own verdict and the refusal is reported then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(spa): search names the groups refused the local networkChristophe Besson24 hours22-15/+150
| | | | | | | | Search gives up before ICE does, so the refusal is read from the connection then; the walk moves on to the group's other nodes, and the page lists the refused groups with a link to each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(spa): a reconnect refused the local network says soChristophe Besson24 hours13-22/+210
| | | | | | | | Each failed attempt reaches the group page, which shows the same message and buttons as at opening while the reconnect goes on. Try now gives up an attempt still waiting on ICE and starts the next at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): the answer log names its host addressesChristophe Besson24 hours3-7/+43
| | | | | | | aiortc ends a host candidate line at `typ host`, so the match on " typ host " found none and logged `host: none` for every answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(spa): a reconnection's resume calls the landing, not a numberChristophe Besson28 hours2-3/+19
| | | | | | | A constant in startStream hid resumeAt, so a reconnection with read-ahead left the player waiting and the film froze at the end of its buffer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(spa): a film playing waits for the session after a reconnectChristophe Besson28 hours3-8/+45
| | | | | | | Stream credit sent before the new handshake was refused, and the node's "Handshake required" replaced the film. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: the Android application asks for the local networkChristophe Besson29 hours3-1/+21
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(android): tell the page when the phone changes networkChristophe Besson29 hours2-0/+26
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(android): ask for the local network when a group opensChristophe Besson29 hours20-6/+275
| | | | | | Android 17 makes it a permission for an application targeting SDK 37. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: open items found with the local network refusalChristophe Besson32 hours1-0/+3
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: allowing Chrome on Android 17 to reach the local networkChristophe Besson32 hours2-2/+17
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(spa): follow the device to another networkChristophe Besson32 hours5-8/+480
| | | | | | | | | | | Moving between mobile data and the node's Wi-Fi left the connection on the network it had left until ICE gave up, and a group that had failed was never tried again: the Android application had to be killed. A change of network or an ICE disconnection now pings the session and reconnects one that does not answer, wakes a reconnect backing off, and retries a group page left on an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(spa): say when the device refuses the local networkChristophe Besson33 hours14-5/+373
| | | | | | | | | | Android 17 makes the local network a permission of the application, and Chrome 155 targets it: without it the node's LAN addresses are dropped and a group on the phone's own Wi-Fi failed with "DataChannel closed". The transport now names that refusal and the group page says what to allow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: one datagram per turn of the loop on Windows, and how it hidChristophe Besson2 days2-0/+32
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(cli): e2e waits for the node's hub socket; bash completion where bash isChristophe Besson2 days1-12/+20
| | | | | | | | | The control API answers before the hub socket is up, later still now that the connection processes start first, so attaching the group failed with "Hub not connected". The bash half of the completion test runs only where bash is on PATH, as the PowerShell half already did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): datagrams straight through the socket on Windows' event loopChristophe Besson2 days4-2/+293
| | | | | | | | | | | | | | | | | The ProactorEventLoop's datagram transport keeps one overlapped operation per direction and starts the next from the previous one's completion, on a later turn of the loop: one datagram sent and one received per socket and per turn, however many wait. The busier the node, the slower every connection, with cores to spare. Measured on loopback with the loop busy 0.5 ms a turn: 1,928 datagrams sent a second and 1,954 received, 87 % of the rest dropped by the kernel. A browser's download from a node holding its connections ran at 9 MB/s, the node at 45 % of a core, SCTP round trip 745 ms. Now a datagram goes out with a non-blocking sendto when nothing is queued, and a completed read is followed by whatever the socket holds before the next one is posted. Two-core machine, 400 MB: CLI download 27 -> 33 MB/s, upload 18 -> 22, browser over eight connections 9-14 -> 17-19. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge branch 'main' of meshbay.org:meshbayChristophe Besson2 days23-158/+1380
|\
| * docs: receiver-side SCTP rules, session gating on reconnect, processes defaultChristophe Besson2 days3-10/+81
| | | | | | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| * feat(node): connection processes on by default, one per core up to fourChristophe Besson2 days3-14/+53
| | | | | | | | | | | | | | | | At 1 no process starts and every lane of a download lands on the node's one core. The Node page also sent every setting on each Save, pinning untouched ones at the day's default; it sends only what changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| * fix(cli): apply index updates in place instead of fetching the whole indexChristophe Besson2 days4-2/+126
| | | | | | | | | | | | | | | | The interactive mode fetched and re-parsed the full index on every index_delta, on the loop receiving the download: 67 times in 80 s while a node enriched a library. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| * perf: linear SCTP reassembly, one SACK every second packet, 4 MB buffer in ↵Christophe Besson2 days6-16/+517
| | | | | | | | | | | | | | | | | | | | | | the CLI aiortc walked a message's fragments after each one arrived (n^2/2 steps for a 1 MiB chunk) and acknowledged every packet. Measured on loopback, 400 MB to the CLI: 19 MB/s before, 39 after (49 by the node's count); uploads from the CLI 25 -> 29 MB/s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| * fix(spa): a reconnect no longer fails the download it reconnects forChristophe Besson2 days4-9/+375
| | | | | | | | | | | | | | | | | | | | While a reconnect ran, every request went out, not only its handshake: a chunk retried between the new channel opening and the proof got "Handshake required", which no retry treats as a dropped connection. Only the opening messages pass now; a replaced channel's late close event is ignored, and a closed channel is retried like a lost one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| * fix(hub): scope every :hover rule to devices that can hoverChristophe Besson2 days2-106/+227
| | | | | | | | | | | | | | A tap left buttons in :hover on touchscreens (pager arrows, nav, player...). All rules now sit in @media (hover: hover); test_hover_scoped.py holds it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| * fix(packaging): say so when QE/default.env is missing instead of stopping ↵Christophe Besson2 days1-1/+1
| | | | | | | | | | | | silently Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* | fix(packaging): the command-line client's Store alias on an application of ↵Christophe Besson3 days4-8/+43
|/ | | | | | | | | | | | its own An Application declares one windows.appExecutionAlias extension, and the app's is meshbay-node.exe; the second one added for meshbay-cli.exe made makeappx refuse the package ("must only be declared once") at the very end of the Store build. The client is now an Application of its own, out of the Start menu, carrying its alias. Built: MeshBay-0.20.0.appx with both aliases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(cli): a Chrome that does not come up is not left running0.20Christophe Besson3 days1-1/+5
| | | | | | | | Its constructor gave up with the browser still running and no object to close it: eight headless Chrome processes were found hours after a failed Windows run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: bump version to 0.20.0Christophe Besson3 days11-11/+11
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* build: ignore the Windows build's work directories; a Linux mode check off ↵Christophe Besson3 days2-1/+7
| | | | | | | | | | Windows packages/meshbay-client/build/_pip-audit (and its siblings) is made by the Windows build and was untracked noise. test_packaging_default_env checked a POSIX file mode that Git's bash on Windows cannot set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(packaging): setup ends a command-line client run from its installChristophe Besson3 days3-5/+78
| | | | | | | | | | | | | | | A `meshbay-cli chat --follow` left open holds node-runtime's DLLs, so the previous version's uninstaller failed ("Failed to uninstall old application files"), a silent update included, and left the node it had stopped not running. stop-node.ps1 now ends a meshbay-cli.exe run from this install (-InstallDir, setup's $INSTDIR), and no other copy. Found through CIM, not Get-Process: setup runs the 32-bit PowerShell, which reads no path for a 64-bit process, and the first version of the stop matched nothing. The test runs the script in that PowerShell. Checked live: an update over a running follower now ends it, replaces every file and restarts the node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): make the command-line client work on WindowsChristophe Besson3 days26-107/+428
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Its suite had only run on Linux. On Windows, against a real hub and node and from the installed meshbay-cli.exe: - secrets.enc was written in text mode (os.open without O_BINARY): every 0x0A of the ciphertext became CR LF and the saved session "does not open with its key" on the next run. - isatty() is true for NUL, so a script run with `< NUL` or by the Task Scheduler was asked questions instead of told to pass --yes; output.terminal() checks for a real console. - Ctrl+C waited for the selector loop's next timer, up to half a minute in `chat --follow`; the loop now points the signal wakeup at its self-pipe, as the proactor does. - Right-click copy and paste raised: asyncio cannot start a process on the selector loop; the clipboard tool runs in a thread. - Tab completion of remote paths ran on the default (proactor) loop, which never opens a node, and wrote in the locale encoding; the PowerShell script now reads it as UTF-8. - `get -R` into a folder past MAX_PATH died with a traceback; get, put, the resume file and the interactive mode use the extended form at each disk call, as the node does. - `groups` showed a SQLite hub's naive UTC time as local time. The opt-in end-to-end suite now runs on Windows (16 tests: Chrome found under Program Files and started with the real environment, VLC as the player, Ctrl+C through a console of its own, PowerShell 5.1 completion of an accented name). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(android): no text messages backup, and one build againChristophe Besson3 days36-472/+44
| | | | | | | READ_SMS sits behind restricted settings outside a store and Play keeps it for the default SMS application: not worth a second build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* build: audit the shipped Python dependencies with pip-auditChristophe Besson3 days6-0/+229
| | | | | | | | | | | | | | | | | | | | | No lock file pins the dependencies, so each build installs the newest releases and what ships is only known once it has. packaging/audit_deps.py runs pip-audit then: build-common.sh on the venv before stripping it, build-node-runtime.ps1 on the environment before PyInstaller freezes it. A known vulnerability stops the build, unless its id is listed in packaging/pip-audit-ignore.txt with why it does not apply. No network only skips the audit with a warning, as does a pip-audit that cannot be installed or writes no report. pip-audit runs from a venv of its own, never from the one that ships. Our packages are not audited (not on PyPI), nor pip, setuptools and wheel, which do not ship. Checked with the real build-common.sh (79 packages, none known), with PyJWT 2.14 (the build stops on its two advisories, and goes on once they are listed) and offline (a warning, the build goes on). The PowerShell step was not run here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(node): the event-loop disk guard watches os.stat againChristophe Besson3 days2-6/+23
| | | | | | | | | | | | | | | | | test_a_scan_asks_the_disk_nothing_from_the_event_loop patched Path.stat, and since the indexer learned Windows' long paths it calls os.stat(long_path(p)) and os.scandir instead: the guard saw no call at all, and asserting that the calls ran on the indexer's thread failed on an empty set. It watches os.stat and os.scandir now (Path.stat goes through os.stat too), counting what the root holds and not the root, whose availability check runs on the roots' I/O thread. Nothing runs on the loop; a stat added there makes it fail again. The node CLI's parser names itself: under `python -m pytest`, Python 3.14 named the program after the module, so test_cli_golden saw "python -m pytest" in the help. The installed meshbay-node was not affected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(auth): the sign-up notice links the privacy statement, with a spaceChristophe Besson3 days13-3/+65
| | | | | | | | | | | | | | | The link under the Register button was glued to the sentence before it: htm drops whitespace that holds a line break, so "deleted.Legal information". It also pointed at the hub's /legal/ index. Where an account is created is where GDPR art. 13 owes the person the information about their data, so it now links the privacy statement itself — /legal/confidentialite.fr.txt in French, /legal/privacy-statement.en.txt in every other language — on the hub that served the page, like every legal link. The sidebar's "Legal information" is unchanged. Checked in Chrome and Firefox (headless), in English and French. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(auth): the recovery key never reaches the hubChristophe Besson3 days25-159/+271
| | | | | | | | | | | | | | | | | | | | | | | | | | | | Ticking "also e-mail it to me" at registration sent the recovery key R in the register body, and the hub appended it to the verification e-mail: the hub saw R in clear on its way out, and the resend door — no captcha — let anyone who knew a pending username and address have the hub mail a "recovery key" of their choosing. The box is gone. The recovery screen offers "E-mail it to myself", a mailto: link that opens the person's own mail application with the key filled in; the desktop shell hands mailto: to the system (http, https and mailto only) and the Android shell to a mail application (ACTION_SENDTO). The CLI drops --email-recovery-key and prints the key, as before. The hub mails the code alone, and refuses a register that still carries a recovery_key rather than dropping it, so an older client offering the option cannot tell someone their key was sent when it was not. A mailbox holding R remains, with the reset code that reaches the same mailbox, enough to recover the account's identities; the link says so, and it is the person's choice. Checked in Chrome and Firefox (headless, the real page against a stubbed hub) and by the CLI's end-to-end registration against a local hub. The Android change was not built here, and the desktop link was not clicked in the application. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* build(deps): require PyJWT 2.15Christophe Besson3 days2-2/+2
| | | | | | | | | | | pip-audit reports two advisories against 2.14.0, both fixed in 2.15.0. PYSEC-2026-4141 reaches the hub: federation.py decodes a peer's token without verifying it first, and a deeply nested payload raises a RecursionError there, an unauthenticated 500. PYSEC-2026-4183 does not: no key is imported from a JWK. The builds resolve the newest release anyway; the floor keeps an older environment from satisfying them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): a removal made at the machine is in audit.dbChristophe Besson3 days3-3/+66
| | | | | | | | | | | | The Node page and the CLI remove access through the loopback API, and revoke and unpin there wrote nothing to audit.db; only the signed MNP door did. A removed person's next connection then showed an auth_failed with nothing before it to explain it. Both routes now record member_revoke and member_unpin once the operation has succeeded, with the node's own account as the actor and 127.0.0.1 as the address, which tells the two doors apart. A refusal changes nothing and records nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(hub): a phone's poll secret ends with the account's sessionsChristophe Besson3 days5-7/+107
| | | | | | | | | | | | | | | | The secret a phone polls /v1/push/poll with is a credential of its own, outside any session, and it outlived the ones that should have ended it: "sign out everywhere" — what someone who lost a phone presses — a passphrase change and an admin revocation left every push row in place, so the lost phone went on reading "X posted in <group>" lines. Each now deletes the account's rows, as a reset and an erasure already did, and the poll refuses a revoked or deleted account whatever deleted the row or not. A suspended one still reads: the line telling it so is one of them. A phone still in hand is not affected for long: a refused poll makes it forget the row, and the page registers it again at its next start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(packaging): ship only the TMDB token line of QE/default.envChristophe Besson3 days3-15/+96
| | | | | | | | | | | | | | build-node.sh and build-node-runtime.ps1 copied QE/default.env whole into every deb, rpm and Windows build after checking only its first 30 bytes. The file is unversioned and sits beside other QE notes, so any line added to it later would have gone out publicly. Only its first line ships now, and only once it is exactly MESHBAY_TMDB_DEFAULT_TOKEN=<JWT>; a missing file, a BOM, a token not on the first line or an empty one still stop the build. The Linux step is run by a test against a scratch repo; the PowerShell one has the same rule and was not run here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(chat): no thread_id — chat has no threadsChristophe Besson3 days13-72/+70
| | | | | | | | | | | | | | Nothing ever set it (the page and the CLI sent null) and nothing read it (the node's get_thread had no caller). It is no longer sent by the page or the CLI, in clear or in the sealed payload, nor relayed or served by the node; get_thread and its index are gone, and the protocol and design docs no longer name it. The node still accepts a chat_msg carrying one, from the desktop apps and CLIs already installed, and drops it. The column stays, empty, in chat.db: a node downgraded to an older version names it in every query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): account, rm -r and mkdir, Tab completionChristophe Besson3 days24-75/+2540
| | | | | | | | | | | | | | | | | | | | | | | | | account shows the account and does what the sign-up form and the Profile page do: register (the passphrase floor and the hub's proof of work checked here, the recovery key printed once), verify, change the passphrase, reset a forgotten one, change the e-mail address, sign out everywhere, delete. A new passphrase carries the copy of the account's keys each node keeps for browsers first — opened under the old M, the same keys sealed under the new one — and only then tells the hub; a refusal sets the copies back. A reset restores them from the recovery copy. The connections that do it never make keys nor ask for a device approval. This machine keeps the M it changed away from, so a node out of reach is carried at its next connection; a copy under an M it does not know is no longer rewritten, which locked the browsers out after a passphrase changed elsewhere. rm -r removes a folder and all it holds, from the node's operator key: the files, then the folders deepest first, one confirmation that counts them. mkdir makes folders, --parents the missing ones (not -p, the passphrase). completion bash|powershell prints the script that makes Tab complete actions, verbs, options and the paths of the groups this machine is signed in to, cached two minutes; the deb and rpm packages install the bash one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): an operator who was a member first holds the authority at onceChristophe Besson3 days2-1/+71
| | | | | | | | | | | | | | | A member pinned on the node who then redeems the operator pairing code was written down as the operator but the node kept its "nobody can authorize" flag until it restarted: every admin operation — removing a folder, inviting, the apps' settings — was refused meanwhile. Only a key paired fresh set it. The join result now names `operator` for an account holding the node-wide operator row, whatever its row in the group says. The page and the CLI read that role to know what to offer, and refused before asking deletions the node would have accepted. Nothing is granted by it: every admin operation is still checked against the operators' keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): stream — a film or a song played in a media playerChristophe Besson3 days12-13/+974
| | | | | | | | | | | | | | | `meshbay-cli stream PATH` serves one video or sound file over HTTP on 127.0.0.1 for VLC, mpv or any player that reads HTTP; `--player PROGRAM` starts the player, and closing it stops the stream, as Ctrl+C does. The file goes as the node stores it: no remux, no transcode, no stream slot on the node. Byte ranges are answered from leaseless file_req chunks decrypted in memory, eight ahead and the last 24 kept. Loopback only, a random token in the address, a foreign Host refused (DNS rebinding), an audio or video type with nosniff. The connection is pinged and reopened when it dies; a refusal by the node stops the stream (exit 5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): /put and /get in the interactive chatChristophe Besson3 days8-10/+449
| | | | | | | | | | | | | | | | /put FILE sends one local file, relative to the current directory or absolute, into the chat's folder, then posts the application's attachment message naming it ({"text": "", "attachment": {filename, size, type}}), so the page shows it as its paperclip's. /get NAME saves a file of that folder, named from it, into the current directory, never over a file already there. The chat's folder is the page's rule: the operator's choice while its root is writable and present, else the first such root; a chat_directory_ack moves it. A line above the one written in shows the transfer's progress. Attachment messages, from the page or from /put, read as [file: name, size] in the room and in `chat`, and as an `attachment` field with --json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): help, and right-click copy and paste in the interactive chatChristophe Besson3 days12-13/+385
| | | | | | | | | | | | | | | help prints what --help prints; help ACTION, what ACTION --help prints. In the interactive chat a right-click on the conversation copies the text selected with the mouse, else the line under the pointer: to the terminal (OSC 52) and to the platform's clipboard tool. A right-click on the line written in pastes the system clipboard's text at the cursor, its lines side by side, read with wl-paste, xclip or xsel, pbpaste or PowerShell, else what was copied here last. The conversation is selectable with the mouse, which RichLog alone is not. The deb and rpm packages recommend wl-clipboard or xclip for the paste. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(hub): one account, several linked nodesChristophe Besson3 days42-316/+1430
| | | | | | | | | | | | | | | | | | | | | | | | | An account links up to MAX_NODES_PER_ACCOUNT node keys (node_keys table) instead of the one users.pk_node_ed25519; the migration carries each account's key over and the downgrade keeps the last one linked. /v1/nodes/auth accepts any linked key, and its token names the key that won it (nk). PUT /me/node_key adds a key, idempotently, 409 past the limit; DELETE /me/node_key names the key: a browser session any of its own, a node only its own. Unlinking a key closes that node's socket and refuses its tokens at once, on REST and on the socket; the account's other nodes are untouched. /pubkeys no longer gives a node key to other accounts, and gives the caller its last linked one, which installed desktop apps read. The node reports waiting_for_node_key while its key is unlinked and signs in again as soon as the hub says so, so a key linked again is back within seconds; unlinking itself signs in first to hold a token naming its key. Profile -> Linked nodes lists each node (connected, last seen, IP, linked since, This machine in the desktop app) with Copy and Unlink, and adds a key by hand. The Node page banner, Create group and the desktop app add this machine's key instead of replacing another's. Messages that pointed at Settings -> Link Node point at Profile -> Linked nodes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): chat --followChristophe Besson4 days9-22/+319
| | | | | | | | | | | | chat GROUP --follow prints the last messages, then each one the node pushes, until Ctrl+C (exit 130). The connection is pinged every half minute; one that closes, fails or stops answering is opened again, and what was said meanwhile is read from the history. Each message is printed once, by its (device, nonce) pair, which the node stores once. With --json, one object per line. The man page lists it, with an example, and its FILES now names the device keys kept for the Tier 2 check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): chat — Tier 2 check on the signing device, interactive roomChristophe Besson4 days15-24/+793
| | | | | | | | | | | | | | | | Each message's signing device is checked against the group's roster as the page checks it (a port of transport-roster.js, held to it by the parity test): chains of countersignatures walked from each account's roots, the keys accepted per account and node kept in the store, the roster read again once before a key is called changed. Only "changed" is said, under the message; --json carries every verdict. chat with no group opens the interactive chat: the groups, then one group's conversation IRC-style with pushed messages arriving live and a line to write in; Esc or /part goes back to the groups, /quit or Ctrl+Q quits. Like the bare command, it opens on the sign-in screen when nothing is stored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>