diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 336 | ||||
| -rw-r--r-- | docs/MESHBAY_HTTP_API.md | 8 | ||||
| -rw-r--r-- | docs/MESHBAY_NODE_PROTOCOL.md | 41 | ||||
| -rw-r--r-- | docs/PACKAGING-GUIDE.md | 17 | ||||
| -rw-r--r-- | docs/QUICKSTART.md | 5 | ||||
| -rw-r--r-- | docs/USERGUIDE.md | 134 | ||||
| -rw-r--r-- | docs/WINDOWS-PORT.md | 10 |
7 files changed, 436 insertions, 115 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 1f6c96d..4e99994 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -16,8 +16,8 @@ > them — it names the invariant that holds today, not the incident that produced > it. §13 is the register of those labels. > -> Wire versions at the time of writing: **MNP 6.2** (oldest peer accepted 4.0), -> **MHP 0.1**, packages **0.19.0**. The normative source for the wire format is +> Wire versions at the time of writing: **MNP 6.3** (oldest peer accepted 4.0), +> **MHP 0.1**, packages **0.20.0**. The normative source for the wire format is > `MESHBAY_NODE_PROTOCOL.md`; this document states the design the protocol > serves, not its byte layout. @@ -249,7 +249,8 @@ derived for that node, so one that leaks opens nothing on another. Two consequences fall out and both are wanted. Two operators cannot tell they host the same person by comparing keys. And **the hub stores and publishes no user keys at all** — `users.pk_ed25519` / `pk_x25519` are dropped, `PUT /me/keys` -does not exist, and `/pubkeys` returns an account id and the node's linking key. +does not exist, and `/pubkeys` returns an account id — and, to the account itself +only, its latest linked node key, which installed desktop applications read. There is no directory to substitute from, which is what **H3** was. Two further rules follow directly: @@ -283,7 +284,11 @@ new_pk_x25519)`. The approving device asks the node for this account's pending requests **with their stored hashes** and recomputes the hash for each until one matches. **An approval answers a pending request**: it names the request's hash, and the node admits the keys only if that request was filed by those same keys. A -countersignature alone — however it was obtained — admits nothing. Retiring a +countersignature alone — however it was obtained — admits nothing. **A code that +matches more than one pending request approves none**: filing needs only the +account's token, so somebody holding the passphrase who saw the code could file +their own keys under it, and the first match would then be whoever filed first. +The new device shows a new code instead. Retiring a device is signed under a prefix of its own, never the admission transcript: a signature given to retire a key would otherwise admit it. @@ -302,6 +307,7 @@ comes from the hash binding: | Bound | Value | |---|---| | Request TTL | 1 h, `[node] device_request_ttl_minutes` | +| Pending requests per account per node | 10; filing one more drops the oldest | | Devices per account per node | 5 | | Attempts per connection | 5, audited on exhaustion | | Filing a request | requires the account to have at least one pinned identity already | @@ -410,6 +416,11 @@ Four properties, each load-bearing: accepted, not after. Nothing to rotate then — they never held the key, and `member_revoke` says so by returning no reminder rather than by leaving the caller to work it out. +6. **Every removal is in `audit.db`, whichever door made it.** The signed MNP + door records its own; the loopback one (Node page, CLI) records + `member_revoke` and `member_unpin` with the node's account as the actor and + `127.0.0.1` as the address. Otherwise a person's next connection shows an + `auth_failed` with nothing before it to explain it. Node authority is established the same way, once per node: `meshbay-node operator pair` prints a code, the operator types it into their own browser, and the node @@ -563,6 +574,13 @@ application needs no fallback: it holds its identities, keeps the new `M` aside until the hub has accepted the change, and re-seals a node's bundle the next time it connects to it — it records which key sealed each one. Every refresh-token family is revoked. +The command-line client (`account passphrase`) carries each copy as a browser +does — opened under the old `M`, the same keys sealed under the new one, never +its own keys in their place — and sets them back if the hub then refuses. It +keeps the `M` it changed away from, so a copy on a node that was out of reach is +carried at its next connection there; a copy that opens under no `M` it knows is +never rewritten, because it may be sealed under a passphrase set elsewhere and is +then the one the browsers open. **Recovering a lost passphrase** splits into what each key can reach: @@ -580,10 +598,15 @@ browser writes an identity bundle to a node and holds the recovery key, a **seco copy** is written beside it sealed under `recovery_key` (`bundle_enc_recovery`, additive on the wire), in the same format and bound to the same account and node. The desktop application writes one when the recovery key is entered on the -Profile page, for an account with browser access. `R` is a pass-through: shown once -on screen at registration and mailed with the verification code only if the person -ticks the box for it (unticked by default), never written to any database, never -logged. +Profile page, for an account with browser access. **`R` never reaches the hub.** +It is shown once on screen at registration, and the person may mail it to +themselves from their own mail application (a `mailto:` link, which the desktop +and Android shells hand to the system), so it never passes through the hub's +mail, is never written to any database and is never logged. The hub refuses a +registration that carries one: an older client that still offers to have it +e-mailed is told so rather than left to say it was sent. A mailbox holding `R` +is, with the reset code that reaches the same mailbox, enough to recover the +account's identities — that is the person's choice to make, and the link says so. The reset endpoints are built to leak nothing. `POST /v1/users/password/reset-request` requires **the username and the email on file as a pair**, checked against a blind @@ -697,8 +720,9 @@ groups, and it is made **in the desktop application, never in a browser**: The hub keeps a **mirror** of the setting as a preference (`browser_access`), written by the application at each start and change. It grants nothing — no node reads it — and exists so a browser can say why a group will not open and name the -way in: turn it on from the application, or approve this browser from it (device -linking, §3.3), which gives the browser identities of its own. Were a browser able +way in: turn it on from the application. A browser is not linked as a device (§3.3): +it keeps no identity between connections, so a key approved for it would be gone by +the next one. Were a browser able to turn it on, somebody holding only the passphrase could sign in on the web, flip it, and have the owner's own application upload the identities at its next start — under the pepper that same sign-in hands out. @@ -1105,15 +1129,22 @@ legitimate rotation. > complete a handshake with. It is unverified at that point and is never a > substitute for the ack. -**The pre-proof window is three messages, and that is a bound to defend.** Only -the two bundle fetches and `join_request` are served before the proof, because -each is something a caller needs *in order to* prove possession of the group key: -a returning browser has to recover its identity, and a new member has no key to -prove with. Nothing else qualifies. Device messages are **authenticated-only** — -a device request is countersigned later by a device already pinned, so requiring -the caller to finish its own handshake first costs nothing and keeps the surface -at three. The bundle fetches are additionally bounded (4 per connection) and -audited, because that window is the neighbourhood **C4** and **C5b** came from. +**The pre-proof window is four messages, and that is a bound to defend.** Only +the two bundle fetches, `join_request` and `device_add_request` are served before +the proof, because each is something a caller needs *in order to* prove possession +of the group key: a returning browser has to recover its identity, a new member +has no key to prove with, and a new device of a known member has none either — the +group key is wrapped for an identity the node pinned, and a machine that keeps no +bundle there (the desktop application, the command-line client) holds no such +identity until another device approves it. +Nothing else qualifies. Filing a device request is **inert**: nothing is admitted +until a device already pinned countersigns it, the code is bound to the new keys, +the request is bounded per connection (5) and per account (10 pending), and it is +audited. Every other device message — looking up, approving, listing, revoking, +saying hello — is sent by a device that can finish its own handshake, and is +**authenticated-only**. The bundle fetches are additionally bounded (4 per +connection) and audited, because that window is the neighbourhood **C4** and +**C5b** came from. **Pinning is defence in depth, not the primary control.** A substituted node already fails the key proof. Pinning covers the case where an attacker *holds* the @@ -2030,8 +2061,8 @@ message may cost, and **60 a minute per account per group** for how often one member may impose it. Both are checked before anything is stored or relayed, and a refusal names itself (`chat_too_large`, `chat_rate_limited`) and is audited. -The numbers are meant to be invisible. The sealed payload is the text, a thread -id, a display name and a timestamp — an attachment is a file on a root and +The numbers are meant to be invisible. The sealed payload is the text, a display +name and a timestamp — an attachment is a file on a root and travels as a reference (§4.5) — so 64 KB is some sixty thousand characters, and sixty a minute is far above a person typing. The rate is keyed by **account**, not by connection: a second tab does not make anyone type faster, and keying on @@ -2226,6 +2257,18 @@ moderator route, even when the account behind it holds a hub role: what a node m is its operator's roster pin, and a hub role is a person's, exercised from a client. The operator manages groups from a client (**NS7**). +**An account links one node key per machine it runs a node on** (`node_keys`), up to +the number of node keys it may announce, ten. `/v1/nodes/auth` accepts a signature +from any of them, and the token names the key that signed (`nk`); on the socket it +registers that key's node and no other. Linking is a person's act, from a client +session. A node's own token may unlink its own key and nothing else, so a node can let +go of itself (`meshbay-node setup --reset`, the Node page) and never of a sibling. +**Unlinking ends that machine at once and leaves the others alone**: the hub closes +its socket, refuses every token naming the key, and answers its next sign-in +"No node key", which is what the daemon reads to wait for its link rather than for an +account (`waiting_for_node_key`). The list is the account's own (`GET /v1/users/me/node_keys`): no route names another account's node +keys, so nobody learns which machines someone runs. + Signaling is rate-limited, SDP-size bounded, capped per user, and **the caller must share an active group with the target node**. Otherwise any authenticated user could make a third party's machine allocate peer connections on demand (**H6**). @@ -2501,7 +2544,7 @@ A user can delete their own account from Settings behind a **passphrase re-entry radius is proof of the passphrase. An admin can delete one too. The row is **tombstoned rather than dropped**: username released, email and password -hash cleared, bundle pepper cleared, node linking key dropped, memberships, notifications, refresh tokens, +hash cleared, bundle pepper cleared, node linking keys dropped, memberships, notifications, refresh tokens, node registrations and device keys removed, active tokens refused at once by a status check rather than left to expire. Device keys go because the desktop client keeps its half: left on the tombstone, the key would @@ -2533,9 +2576,25 @@ Two things survive on purpose: the nodes refuse the account and close the groups' sessions now. A node that is offline misses it. -Registration is gated by a CAPTCHA whenever one is configured — **unconditionally**, +**Registration and the passphrase-reset request are gated by a proof of work** +(`captcha.py`), on unless `[captcha] enabled = false`, and **unconditionally** — not only when some other field is absent, or the real client's ordinary request -skips it. The desktop client renders the widget too. +skips it. The hub signs a challenge (the SHA-256 of a nonce, an expiry and a +number below `max_number`) with a key derived from its own private key; the page +finds the number while the form is being filled in and sends it with the form. +An answer is checked against the hub's signature, its expiry, and the answers +already spent, so one solve is one form. **No third party is involved**: no +script, cookie or visitor address leaves for anyone else, which is what keeps the +sign-up form free of a consent it could not ask for freely. What it buys is cost +per attempt, not a verdict on who is human; the bound that holds against a +caller with machines to spare is the mail budget (§7.4), since these two routes +are what make the hub send mail. + +**The sign-up form states what the hub keeps**, as a notice and never a checkbox: +the account rests on the terms of use and the connection log on a legal +obligation, so there is no consent to collect, and one that blocks sign-up would +be neither free nor withdrawable. The notice links to the legal pages of the hub +in use (`/legal/`), which are its operator's to write. **Sessions end three ways, all admin settings in hours** (`session.*`, §7.4): @@ -2594,16 +2653,16 @@ The rules that make this safe: ## 8. Clients -### 8.1 Two clients, deliberately +### 8.1 The clients, deliberately different -| | Hub-served web SPA | Desktop client | -|---|---|---| -| Distribution | served by the hub | installed, signed release | -| Code integrity | **T3 accepted** — the hub can inject | detectable *if* reproducible builds ship | -| Key storage | IndexedDB / sessionStorage, plus a bundle on each node | OS-protected local storage; keys never bundled | -| Transport | WebRTC | WebRTC **+ QUIC** (sidecar, for hub-less `group://`) | -| Downloads | to disk where the engine allows it | native, streamed, unlimited | -| Positioning | **convenience tier** — zero install | recommended for sensitive use | +| | Hub-served web SPA | Desktop client | Command-line client (§8.6) | +|---|---|---|---| +| Distribution | served by the hub | installed, signed release | installed: its own `.deb`/`.rpm`, inside Windows Full and Store | +| Code integrity | **T3 accepted** — the hub can inject | detectable *if* reproducible builds ship | as the desktop client: the hub serves it no code | +| Key storage | IndexedDB / sessionStorage, plus a bundle on each node | OS-protected local storage; keys never bundled | OS key storage, else a 0600 file; opens a node's bundle, never writes one | +| Transport | WebRTC | WebRTC **+ QUIC** (sidecar, for hub-less `group://`) | WebRTC | +| Downloads | to disk where the engine allows it | native, streamed, unlimited | native, resumable, unlimited | +| Positioning | **convenience tier** — zero install | recommended for sensitive use | a member's files from a terminal, scripts and headless machines | **The SPA is not deprecated and stays.** It is the zero-install path, and the objective is explicit: **a native client must not prevent web use.** It must be @@ -2979,6 +3038,153 @@ MSE string) fed to a source buffer, with the node holding one slot per viewer. --- +### 8.6 The command-line client + +`meshbay-cli` (`packages/meshbay-cli`) is a member's groups and files from a +terminal: `login`, `logout`, `account`, `groups`, `invitations`, `join`, `decline`, +`leave`, `ls`, `search`, `get`, `stream`, `put`, `rm`, `chat`, `devices`, `trust`, +`browser-access`, `mkdir`, `completion` (bash and PowerShell), `help` (what `--help` prints), and with no argument an interactive two-pane view (Textual) +over the same library. It is not an operator tool — a node is administered +through `meshbay-node` and its loopback API (§6.7) — and it plays nothing +itself: `stream` hands a file to a media player. English only. + +**The same protocol as the other clients, end to end.** The hub signs it in, +issues the per-node token (E10) and relays the WebRTC offer and answer; from the +DataChannel on, everything goes to the node and is sealed under the group key +(§5.1, §5.2), and transfers take leases like any client's (§5.5). Nothing +data-related passes through the hub. + +**Identity on a node, in this order:** the identity this machine kept from an +earlier run; the one waiting for a device approval; the bundle the node keeps +for browsers, opened with the passphrase (§3.7) and then kept locally; failing +all three, a new key pair — admitted with an invitation's code by `join`, or +filed as a device request (§3.3): the code is printed, an already linked device +approves it, and the client joins with its own keys. That is the desktop +client's position: an installed program, no hub-served code (T3 does not +apply), so device linking is admitted for it as for the application, and it +approves other devices as the application does (`devices approve`, exactly one +waiting request per code). + +**Joining and leaving** follow the application's two halves (§3.4): the hub +makes the account a member (an invitation accepted, a link's ticket redeemed), +then the node admits the key made here with the invitation's code; a link's +code goes to the node the link names and to no other. `leave`, like +`decline` and `devices revoke`, asks first on a terminal and refuses without +`--yes` where there is none. + +**Search** (`search`) dials every group through the pool and matches each +index on the client, on the Search page's fields (name, folder, title, artist, +album), every word required. A group with `search_listed` off in its sealed ack +is skipped before its index is asked for (§9.11). A file several groups hold is +one result, keyed by its content hash, and listed from the reader's own node +first; the others are kept as places. Like Files, and unlike the media views, it +groups nothing into units. + +**Chat** (`chat`) reads the last messages (ten, or `-N`, paging back through +`chat_hist`) and posts one, with the application's construction (§4.5): every +epoch key from `chat_keys_req`, a message sealed under the current epoch through +the sending device's derived subkey and signed with its key on the node, which +must be the device `device_hello` named. A stored message is read signature +first, then key, then content, and one that fails any of them is shown as +unreadable, never left out. The name shown is the one sealed by the sender, and +the signing device is checked against the group's roster as the application +checks it (Tier 2, §3.3): 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 — and only "changed" is said, under +the message. `chat GROUP --follow` then prints what the node pushes, pings the +connection every half minute, opens it again when it dies and reads what was +said meanwhile from the history, each message printed once (a device and nonce +pair is stored once). `chat` alone opens the interactive chat: the groups, one group's +conversation IRC-style with what the node pushes arriving live, and back. A +right-click copies from the conversation (the mouse selection, else the line +under the pointer) and pastes into the line written in. `/put FILE` uploads +one local file into the chat's folder and posts the application's attachment +message naming it; `/get NAME` saves a file of that folder into the current +directory. The chat's folder follows the page's rule: the operator's choice +while its root is writable and present, else the first such root. + +**Streaming** (`stream`) serves one video or sound file over HTTP on +127.0.0.1 to a media player (VLC, mpv; `--player` starts one), as the node +stores it: the player decodes it, so the node runs no ffmpeg and holds no +stream slot (§8.5 is the browser's path). Each byte range the player asks for +is answered from `file_req` chunks decrypted in memory, read without a lease as +the application's players read, eight chunks ahead and the last twenty-four +kept. Decrypted content on a local port is what is guarded: loopback only, a +random token in the address, a request whose `Host` is not that address +refused (DNS rebinding), an audio or video type with `nosniff`. The connection +to the node is pinged every half minute and opened again when it dies; a +refusal by the node stops the stream (exit 5); Ctrl+C, or closing the player +`--player` started, ends it, and the address with it. + +**Deleting** (`rm`) is `file_delete` signed with the machine's key on the node +(§5.4): the node accepts the uploading account's devices and the operator's. +The client checks the same rule against the index first (`uploader_id`, and the +`role` the join result gives its key) and refuses the whole run, before asking +anything, when one file falls outside it. `rm -r` takes a folder with all it +holds: the files first, then the folders deepest first, each an empty +`dir_delete` — the node never deletes recursively, and only its operator removes +a folder — so the run is the operator's, a group's root is never in it, and the +one confirmation counts every file and its size. `mkdir` is `dir_create`, which +any member may send where the root is writable; `--parents`, not `-p`, which is +the passphrase in every action. + +**Tab completion** asks `meshbay-cli __complete` for the word under the cursor: +actions, verbs and options from the parser, and remote paths from the hub and +the node for the account this machine remembers. It asks nothing — no +passphrase, no device approval — and opens a node the way that never makes +keys, as a passphrase change's connections do; what a group holds is cached two +minutes, so Tab after Tab is instant. Anything that fails is no candidate. + +The join result names `operator` for a key whose account holds the node-wide +operator row, whatever its row in the group says: the client offers what the +node will accept, and the node still checks every admin operation's signature. + +**Browser access is the application's setting, kept per machine** (§3.7): +`browser-access on` leaves a copy of the identity sealed under `M` on each node +it opens a group on, when the node holds none or an outdated one; `off` +withdraws it; never set, it writes and withdraws nothing. It is set on the +command line and nowhere else: the hub's mirror, which anyone holding a session +can write, is read only to tell a person joining that a browser will not open +the group — never to upload a key. + +**What it keeps.** `state.json` (hub, account, pinned node keys, browser +access) and `secrets.enc` (AES-GCM: the refresh token, the device hub key, the +node identities, an invitation's code waiting for its node) in the user's data directory (`$XDG_DATA_HOME/meshbay-cli`, +`%LOCALAPPDATA%\meshbay-cli`). The key that opens `secrets.enc` is in the OS +key storage; where there is none, in a 0600 file, which `login` says once. The +passphrase is never stored: a remembered session renews with the refresh +token, else with the device hub key, and asks for the passphrase only when both +fail. Two runs renewing at once take a file lock, so a rotated refresh token is +never spent twice. + +**Node keys are pinned at first contact.** A node presenting another key is +refused before anything is sent (exit 6) until `meshbay-cli trust reset NODE` +— the deliberate way out, for a rotation the operator announced. + +**Text from the network is data.** Every name a node or a member chose is +printed with control and bidi characters escaped, and drawn as plain text, +never markup, in the interactive view. Local names are made writable on the +machine and kept inside the destination; on Windows a download carries the +Mark of the Web. + +**Remote paths** are `group[@owner]/root/folder/file`: `@owner` only when two +groups share a name, names compared without case, `%2F` and `%25` for a `/` or +a `%` in a name, and a link copied from the application accepted as it is. + +**Concurrency.** At most six WebRTC negotiations in flight, one connection per +group; a group whose node is away is reported without stopping the others +(exit 4). + +**Exit codes** are a contract for scripts: 1 usage (or a confirmation not +given), 2 sign-in, 3 not found, 4 partial, 5 node unreachable, waiting for +device approval, or asking for an invitation's code, 6 node key changed, +7 version, 8 refused before anything was sent, 130 interrupted. + +**Packaging.** Its own `.deb`/`.rpm` (`meshbay-cli`, with the packages only it +uses, on top of `meshbay-common`'s venv) and a man page generated from its +argument parser; on Windows, `meshbay-cli.exe` beside `meshbay-node.exe` in +the Full installer and the Store package, not in Light. + ## 9. Group applications ### 9.1 The plug-in architecture @@ -3177,7 +3383,7 @@ each removable root. Entries from unavailable roots are filtered out. ### 9.6 Chat -Messages, threads, attachments, and link previews (§6.5). The composer gates on the +Messages, attachments, and link previews (§6.5). The composer gates on the connection having identified a device (§3.3) and on a writable root existing for attachments; the attachment directory is a single writable folder chosen by the operator, and the paperclip is disabled with a stated reason when it is not usable. @@ -3537,13 +3743,13 @@ object — absent, not refusing (§11.3). under a **Phone** heading of the side menu (`android-sync-page.js`), shown only where `platform.phoneSync` exists. Settings holds what the account prefers on every client; this page holds what one phone sends: photos, contacts (§9.13), -and the kinds to come (messages, calendar, files). +the calendar, chosen folders and WhatsApp's. **The personal profile only.** Every backup is of the phone's personal profile, never of a work profile: a copy of the application installed inside a work profile is offered none of them (`Profiles.kt`; the bridge objects are absent, so the page shows no Android Sync). In the personal profile each -source reads that profile alone, since MediaStore and the contacts, SMS and +source reads that profile alone, since MediaStore and the contacts and calendar providers answer for the profile they are asked from; none of the cross-profile `ENTERPRISE_*` URIs is used. An account added to the personal profile is part of it, whatever its use. @@ -3627,7 +3833,7 @@ only once the node has that file, so a run cut short sends them with the next one. Restoring puts each photo back into its album and each file back under its own name and date; merging onto a phone that already has some of them compares hashes without downloading anything. Photos, videos and files have -one; contacts, calendars and messages are each one file that describes +one; contacts and calendars are each one file that describes itself. What the manifest cannot bring back is a photo's location, removed before it left the phone. @@ -3650,10 +3856,10 @@ foreground service (`BackupService`) and the WebView reported visible, like a cast (§11.3). Android 15 limits that type to six hours a day; the service stops when told and the run carries on at the next opening. -### 9.13 Contacts, calendar, messages, files and WhatsApp backup (Android) +### 9.13 Contacts, calendar, files and WhatsApp backup (Android) -The Android application sends a copy of the phone's **contacts**, its -**calendars** and its **text messages** to a folder of a group, on the photo backup's terms (§9.12): a client +The Android application sends a copy of the phone's **contacts** and its +**calendars** to a folder of a group, on the photo backup's terms (§9.12): a client feature over the upload path, additive, once a day, the page doing the sending and the phone handing one file over by token (`/phonesync/<token>`, `phonesync/` in the Android package, `phone-sync.js` on the page). Each kind is @@ -3683,16 +3889,6 @@ organizer and attendees come along; each event names its calendar in `X-MESHBAY-CALENDAR`, which importers ignore. Like contacts it is sent when its SHA-256, taken without the DTSTAMP lines, differs from the last copy. -**Messages.** `<folder>/<account>-messages/YYYY/sms-2026-10-10-0900.xml`: the -SMS added since the last file the node took (the marker is the highest -provider `_id` sent; ids only grow, so a message restored onto the phone later -is sent too), the first file holding the whole history. The format is the -`<smses>` XML that SMS backup applications restore, so a copy goes back onto a -phone with tools that exist; `SmsXml.kt` writes it, keeping line breaks as -character references and leaving out the characters XML 1.0 cannot carry. -A contact name is filled in only when the person also allowed contacts. MMS -are not included. - **Files.** `<folder>/<account>-drive/<chosen folder>/…`: the files of the folders the person chose through the system's picker (`ACTION_OPEN_DOCUMENT_TREE`, a persisted read grant per folder, no storage @@ -3728,17 +3924,15 @@ increments: once anything in that set was sent, the manifest gets a `restore-set` line naming, for each file of the set as it is now, the copy on the node that goes with it. -**Two builds.** Play's policy gives `READ_SMS` to the default SMS application -only, so the APK has a `store` dimension: `full`, distributed directly, and -`play`, which has neither the permission (`src/full/AndroidManifest.xml`) nor -the code that reads messages (`SmsSource.kt` is in `src/full`; `Flavor.kt` -answers null in `src/play`). The bridge offers `messageSync` only where the -shell has a message source (`MESSAGES` in its prelude), so the Play build shows -no Messages section rather than one that refuses. +**No text messages.** They are not backed up. Play gives `READ_SMS` to the +default SMS application only, and Android keeps it behind "restricted +settings" for an application installed outside a store, which the person must +lift by hand in the system's settings; for an occasional restore that was not +worth a second build of the application. -**Unlike photos**, these run on any network: a contacts or calendar file is small, and a -history of text messages is megabytes, not the gigabytes of a camera roll. -`READ_CONTACTS`, `READ_CALENDAR` and `READ_SMS` are asked for when the person +**Unlike photos**, contacts and calendar run on any network: their files are +small, not the gigabytes of a camera roll. `READ_CONTACTS` and +`READ_CALENDAR` are asked for when the person turns each on, never at start. ## 10. Filesystem portability @@ -3752,7 +3946,7 @@ requirements, not compatibility notes. | **Case-insensitive, case-preserving** | The index needs a canonical identity and a **case-folding collision check** at scan time, reported to the operator rather than resolved silently. One directory indexed as two roots is the same problem one level up | | **Unicode normalization** | A name written on one platform in decomposed form and on another in composed form are different byte strings. **Normalize to NFC for identity and comparison; preserve the original bytes for display and for opening the file** | | **Reserved names and characters** | A group indexed on one platform can hold names another cannot create. The client sanitises on save **and says so**; the upload allowlist is the intersection across platforms, or some files are simply undownloadable | -| **Path length limits** | Use extended-length paths on Windows, in the node and the client alike | +| **Path length limits** | Windows refuses a path of 260 characters or more unless `LongPathsEnabled` is set, which it is not by default and which takes administrator rights to change. **The node hands every call that touches a group's disk the extended form** (`paths.long_path`, `\\?\`) and keeps the plain path in the index, the hash cache and the log: a directory is listed under its long form and its entries named from the plain one (`indexer.walk_files`, `wire._walk_dirs`). ffmpeg extends a long path itself and is given the plain one. `test_long_paths.py` holds indexing, serving, uploads, folders and enrichment with the setting off. The command-line client does the same for what it saves and sends (`get -R` into a deep folder, the resume file beside it, `put -R` of a deep tree; `test_get_and_put_past_max_path`). What the desktop application saves is not measured | | **Coarse timestamps, local time** | mtime alone is not a change detector. Size plus mtime with tolerance, and rehash when in doubt | | **Watcher reliability** | Change notification drops events under load on Windows, and inotify is unreliable on a FUSE mount. **Periodic reconciliation is mandatory on both platforms** | | **No symlinks, no POSIX permissions** | Simplifications: nothing to defend against, and the node runs as the user anyway | @@ -3889,7 +4083,12 @@ narrow bridge — and Android has all three: which Android stretches under Doze. The secret is the only credential the background holds and it reads notification lines and nothing else — not a session, so nothing renewing in the background can collide with the page's - rotating refresh token, and a sign-out, which deletes the row, ends it. **When + rotating refresh token. It is a credential of its own, so whatever ends the + account's sessions ends it too: turning it off, signing out everywhere (what + someone who lost a phone presses), a passphrase change or reset, a revocation + and an erasure all delete the row, and a revoked or deleted account's poll is + refused regardless. A suspended one still reads, the line saying so being one + of its notifications. **When a UnifiedPush distributor is already installed** (ntfy, or an application carrying one), the row also gets its endpoint and P-256 key and each notification is sent there at once as one RFC 8291 record, so the push server @@ -4471,7 +4670,10 @@ phone's photo backup (§9.12), the operator CLI and loopback control API, the desktop client through its identity and download stages, account recovery, the Windows port through packaging, and the Android client — the shell, native keys, downloads and uploads, casting, -and notifications while it is closed, fetched or pushed (§11.3). +and notifications while it is closed, fetched or pushed (§11.3) — and the +command-line client: groups, joining and leaving them, listing, search across +groups, downloads and uploads with resume, deleting, reading and posting chat, device linking and approval, browser access, and an +interactive two-pane view (§8.6). The packages install: a machine has been taken from the built artefacts to a running hub and node on **Ubuntu 26.04 (`.deb`), Fedora 44 (`.rpm`) and @@ -4502,6 +4704,7 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows. | — | **HEIC/HEIF in Photos.** The indexer does not classify `.heic`/`.heif` as images and no browser but Safari draws them, so a phone that shoots HEIC has its backed-up photos stored and listed in Files but absent from Photos. Needs `pillow-heif` in every package and a JPEG rendition for the viewer (§9.12) | | — | **Photo backup with the application closed**, and **"remove what this phone sent"** (§9.12). A closed application has no page and so no transport; it runs at the next opening | | — | **Android: phone behaviour and release** — the back button driving the page, recovery from a network handover, keeping a download alive with the screen off, lock-screen media controls, and an update channel (§11.3) | +| — | **Command-line client: cross-group search and chat** (§8.6). Not run on Windows yet; its transfer windows are not yet measured against a node on another machine | | — | **Federation between two hubs.** The protocol is written and switched off in the code (§7.6); what is not built is one run between two machines | ### 15.3 Open, and why each is where it is @@ -4510,7 +4713,6 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows. |---|---| | **A disk that stops answering still stalls what is not the scan** | The indexer's own reads are bounded (§6.3). Serving a chunk, the availability check and the directory deletions go through the roots' I/O thread, which has no such limit, and the tag and photo readers leave a thread in the shared pool at each timeout | | **A file that gave no answer is asked again at every start** | The list of unread files is memory. With three or more such files in a row the root is paused again three minutes into each start, until the operator resumes it or moves them | -| **Paths longer than 260 characters are not indexed on Windows** | With the system's default setting the walk cannot list them; it now says so once per directory instead of nothing. The reads already use the long form (`paths.long_path`), the walk does not | | **The desktop cast relay drops whole segments** | `cast-relay.js` drops a fragment once 8 MB wait for a receiver, and a fragment is a segment, 5 to 10 MB at a film's bitrate: the picture freezes for its length. Its backlog is bounded in fragments only. The Android relay spools a receiver's lead to disk and bounds its backlog in bytes (§11.4); the desktop has neither | | **The desktop cast relay takes the first chunk for the whole header** | The node's first chunk can be the ftyp alone, the moov in the next; the receiver then gives up. Fixed in the Android relay (§11.4), not in `cast-relay.js` | | **A cast restart may pull far ahead** | Seen once on an emulator with a synthetic film: after the restart's reinit the node reported `duration=None`, and the page pulled most of the film at network speed. Not reproduced on a real film; the suspicion is a read-ahead budget computed without a duration | @@ -4520,7 +4722,6 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows. | **`connection_processes` has no CLI verb** | Set from the Node page or `node.toml`; `meshbay-node` has no command for it, unlike the transfer caps | | **A connection process does not slow a peer the node cannot keep up with** | What a process receives is forwarded to the node unbounded. An upload is bounded by its own rate, 5 to 6 MB/s a connection, so it would take a node stalled for seconds to matter | | **The home page says "No groups yet" when the hub cannot be reached** | An unreachable hub reads as an account with no groups, rather than as an error | -| **`meshbay-node init` says "Settings → Link Node"** | The control is on the Profile page, as QUICKSTART says | | **C4** for accounts with browser access | Closed against operators by the pepper; **open against an active hub**, which holds the pepper and can fetch a bundle with a token it mints — the adversary T3 already concedes for browsers (§3.7) | | **A desktop that never held an identity cannot open its recovery copy** | The application opens a node's passphrase copy, not the recovery copy: after a reset, an identity it never held is recovered from a browser (§3.6). And an identity the application mints gets a recovery copy only when the recovery key is entered on its Profile page with browser access on | | **T3** for browser users | **Accepted permanently.** Removed for native clients, and that removal's value depends on reproducible builds | @@ -4534,9 +4735,8 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows. | **The reconnect backoff only wakes on `visibilitychange`** | So a tab that stays visible through an outage — which is what a screen wake lock guarantees while a film is playing — waits out the full backoff, up to 30 s, after the network is already back. Nothing listens for `online` | | **Per-device revocation has no CLI** | A device is revoked over MNP (`roster.revoke_device`), from a device the node has already pinned. On a headless node the operator's only lever is `member unpin`, which removes **every** device of that account — so the per-device control the roster is built around is reachable from an interface and from nowhere else. §6.7 listed a `meshbay-node member device list\|revoke` verb that was never written, and that listing is how this was found: `USERGUIDE.md` was the first document written by reading the CLI rather than this specification, and the verb it copied out did not run | | **Migrations run on SQLite only** | The chain reaches head and agrees with the models there (§12), which is not where it ships. **The exposure is one revision deep, not the whole chain**: every revision behind the first packaged release was development that no installation ever ran, so nothing replays them on PostgreSQL. What is unguarded is the *next* migration — a default, an index type or a constraint PostgreSQL refuses reaches a deploy without the suite saying so | -| **The loopback path removes access without writing an audit entry** | `ops.revoke_member` and `ops.unpin_member` log to the daemon's log and nothing to `audit.db`; the MNP `member_revoke` handler doing the same work audits it (and `member_unpin` did, until it left MNP in 6.0). So a removal made from the node page or the CLI — the two doors an operator sitting at their own machine actually uses — leaves the journal showing an admission and then, whenever that person next connects, an `auth_failed` ("not admitted by the roster") with nothing in between to explain it. §5.4's signed transcript is not what is missing: a loopback caller is authorized by being on localhost with the run token and signs nothing, so the gap is the record, not the authority. Found by reading a node's audit log for a refusal whose cause was six hours earlier and unrecorded | | **The Create group wizard calls two hooks after an early return** | `CreateGroupWizard` (`create-group-page.js`) returns during node detection, before its `useRef`/`useEffect` for provisioning, so the hook count changes between renders. Preact tolerates a list that grows, and nothing is known to break; `test_hook_ordering.py` checks declaration order, not this. Found while tracing the frozen-fields report, which had another cause (`ask.js`) | -| **A node key is read from the terminal or the desktop client, never a browser** | **Accepted.** `meshbay-node status` on the node's own machine and Node → Overview in the desktop client are the two places the key can be read; the Node page is Electron-only, because `platform.node` resolves to "not available" without the bridge, and no hub route exposes the key. The create-group wizard links it automatically over that same bridge, so the manual paste in **Profile → Link Node** exists for the operator who runs the node from a terminal and the hub from a browser — who has a terminal by definition. Anyone linking a node is already at a shell prompt, so a browser-reachable copy would buy nothing and widen what the hub knows about the node | +| **A node key is read from the terminal or the desktop client, never a browser** | **Accepted.** `meshbay-node status` on the node's own machine and Node → Overview in the desktop client are the two places the key can be read; the Node page is Electron-only, because `platform.node` resolves to "not available" without the bridge, and no hub route exposes the key. The create-group wizard links it automatically over that same bridge, so the manual paste in **Profile → Linked nodes** exists for the operator who runs the node from a terminal and the hub from a browser — who has a terminal by definition. Anyone linking a node is already at a shell prompt, so a browser-reachable copy would buy nothing and widen what the hub knows about the node | | **A loopback eject or plug reaches open pages late** | `ops.eject_root` and `ops.plug_root` flip the live set and tell nobody; over MNP the broadcast `root_eject_ack` / `root_plug_ack` is what moves every open table. So an eject made from the desktop application or the CLI shows on members' pages only with the next index push — for a plug, the end of its rescan; for an eject, whatever changes next. `ops.update_root` had the same silence and now calls `DirectoryIndexer.publish_roots`; the same call belongs in these two. Found while moving the `writable`/`removable` switches to the loopback door (MNP 6.0) | | **A group key rotated from the Node page leaves the chat key where it was** | `ops.set_gek(rotate=True)` replaces the group key and opens no chat epoch; the MNP `gek_rotate` handler opened one itself (`_new_chat_epoch`), and it was the only door that did — but no client ever sent it, and it is gone since 6.0. The removals that matter (revoke, unpin, device revoke) open an epoch in `ops` for every door, so what is missing is the follow-through for an operator who rotates by hand: §4.5's "rotate after a removal" means it for chat too. The fix is the `_after_removal` shape — `open_chat_epoch` inside `ops.set_gek` when `rotated`. Found while removing the MNP message | | **iPhone playback is untested, and the player ignores what ManagedMediaSource asks of it** | An iPhone has no `MediaSource`, only `ManagedMediaSource` (iOS 17.1+), which `video-player.js` now falls back to; below 17.1 nothing plays. Nobody has watched a film on one yet. The player listens for neither `startstreaming`/`endstreaming` nor `bufferedchange`, and the browser may evict buffered ranges on its own, ahead of the playhead included: the read-ahead (`pump()`, `currentRange()`) assumes a buffer only it shrinks. AirPlay is switched off on that path, because the source never opens otherwise | @@ -4577,7 +4777,7 @@ superseded document kept under `docs/`, a note somebody holds elsewhere. | `draft-v5 §6.2`–`§6.4` — node registration, signaling, client addresses | §7.2 | | `draft-v5 §7` — cryptography | §4.1–§4.4, §4.6 | | `draft-v5 §7.1` — the keypair bundle and C4 | §3.7 | -| `draft-v5 §8.1`, `§8.2`, `desktop-client-v1.md §2` — the two clients, the shell | §8.1, §8.2 | +| `draft-v5 §8.1`, `§8.2`, `desktop-client-v1.md §2` — the clients, the shell | §8.1, §8.2 | | `draft-v5 §9`, `draft-v6 §5` — open items | §15.3 | | `draft-v5 §10` — testing posture | §12 | | `draft-v6 §2.2`, `desktop-client-v1.md §4`, `§4.5`, `§4.6` — device linking | §3.3 | diff --git a/docs/MESHBAY_HTTP_API.md b/docs/MESHBAY_HTTP_API.md index 73d079d..70bcefe 100644 --- a/docs/MESHBAY_HTTP_API.md +++ b/docs/MESHBAY_HTTP_API.md @@ -40,6 +40,7 @@ hub also serves the web application at `/` and `/app/`, which are not listed. | Method | Path | Auth | What | |---|---|---|---| +| GET | `/v1/users/captcha` | none | A proof-of-work challenge for the register and reset-request forms. | | POST | `/v1/users/register` | none | Create an account. | | POST | `/v1/users/verify-email` | none | Verify a registration email with the code received by mail. | | POST | `/v1/users/me/bundle-pepper` | user | The pepper, for a session that has just been given the passphrase again. | @@ -60,10 +61,11 @@ hub also serves the web application at `/` and `/app/`, which are not listed. | GET | `/v1/users/me/preferences` | user or node | The account's stored interface preferences. | | PUT | `/v1/users/me/preferences/{key:path}` | user | Store one interface preference. | | DELETE | `/v1/users/me/preferences/{key:path}` | user | Remove one interface preference. | -| PUT | `/v1/users/me/node_key` | user | Link a node daemon's Ed25519 public key to the operator's account. | -| DELETE | `/v1/users/me/node_key` | user or node | Remove the linked node key from the operator's account. | +| GET | `/v1/users/me/node_keys` | user | The node keys linked to your account, oldest first, with what the hub saw of each node. | +| PUT | `/v1/users/me/node_key` | user | Link a node daemon's Ed25519 public key to your account, alongside any already linked. | +| DELETE | `/v1/users/me/node_key` | user or node | Unlink one node key from your account. | | DELETE | `/v1/users/me` | user | Erase your own account. | -| GET | `/v1/users/{username}/pubkeys` | user or node | Resolve a username to its account id, and its node's linking key. | +| GET | `/v1/users/{username}/pubkeys` | user or node | Resolve a username to its account id. | ### Nodes diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md index 207d640..04860cd 100644 --- a/docs/MESHBAY_NODE_PROTOCOL.md +++ b/docs/MESHBAY_NODE_PROTOCOL.md @@ -625,20 +625,21 @@ operator's real choice (I8). ## 7. The pre-proof window Between `handshake_challenge` and a valid `handshake_response` the peer is -*authorized* but not *authenticated*. Three message families are served there, each +*authorized* but not *authenticated*. Four message families are served there, each because the peer provably cannot compute a proof without it. **Everything else is answered `Handshake required`**: the dispatcher's authenticated branch begins -immediately after these three, so the table below is the window, exhaustively. +immediately after these four, so the table below is the window, exhaustively. | Message | Why it must precede the proof | Bound | |---|---|---| | `keypair_bundle_fetch` | the client's own identity keys for this node live in an encrypted bundle stored on it | counts against `MAX_PRE_PROOF_FETCHES` = 4; audited | | `gek_bundle_fetch` | the wrapped group key is what the proof is computed with | same counter | | `join_request` | a first-time member holds no group key at all. Accepted after the proof as well — an operator pairing a browser is already connected — because its authority comes from the pairing code and the signature, never from the session state | 5 attempts per connection; wrong codes: 5 per account and 20 node-wide per 600 s, consulted only when a code is tried | +| `device_add_request` | a new device of a known account holds no identity this node pinned, so the node wraps it no group key (§9). Inert: it admits nothing until a pinned device countersigns. Filed for the account the token names. Accepted after the proof as well | the device budget, 5 attempts per connection; 10 pending requests per account, the oldest dropped; audited | -Device linking (§9) is **not** in this window. `device_add_request` and every message -after it are answered only on an authenticated session, and the device budget of 5 -attempts per connection applies there. +The rest of device linking (§9) is **not** in this window: `device_lookup`, +`device_add`, `device_list`, `device_revoke` and `device_hello` are answered only on +an authenticated session, by the device budget of 5 attempts per connection. Exceeding the fetch budget is audited as `pre-proof fetch flood` and answered `Too many requests`. Every fetch in this window is written to the audit log with the @@ -960,9 +961,12 @@ two keys on the same node. A second device is admitted by **a key the node alrea pinned** — never by the hub, which stores no user keys and therefore cannot countersign anything. -Every message in this section is answered on an **authenticated** session only (§7): -both the device filing a request and the device approving it have completed a handshake -on their own connection. +The new device files its request in the **pre-proof window** (§7): it holds no +identity the node pinned, so it cannot complete a handshake until the request is +approved. Every other message in this section is answered on an **authenticated** +session only — the approving device has completed a handshake on its own connection. +Once approved, the new device's next `join_request`, with no code, is recognised and +answered with the group key, like any pinned device's. ### 9.1 Exchange @@ -990,6 +994,7 @@ on their own connection. | | A recomputes sha256(code||keys) | | for each candidate and keeps the match. | | No match -> refuse before signing. + | | More than one -> refuse: approve none. | | | | |<--- device_add --------------| | | {pk_ed25519, pk_x25519, | @@ -1002,6 +1007,12 @@ on their own connection. | | {pk_ed25519} | ``` +A request is filed with the account's token alone, before any proof (§7). So +anyone holding the passphrase who has seen a code on D's screen can file a second +request with that code and keys of their own, and both hashes verify. That is why +A approves only a code that matches **exactly one** pending request; D then shows +a new code. + ### 9.2 Transcripts ``` @@ -1891,22 +1902,22 @@ read, and receivers include devices that were not connected when it was sent. | | |-- chat_msg {v, format: 1, epoch, device, | | nonce, ct, sig, sender_name, | - | [thread_id], [iteration]} -------------------->| + | [iteration]} --------------------------------->| | | envelope checks (below) | | persist to THIS group's store | | broadcast ------------------->| | | {chat_msg, sender_id, | | sender_name, format, epoch, | | device, nonce, ct, sig, - | | thread_id, timestamp} + | | timestamp} | | notify the hub: group id and | | sender id only |<- ack {v} -----------------------------------------| | |-- chat_hist {v, [before], [limit <= 200]} -------->| |<- chat_hist_resp {v, has_more, messages: [{id, | - | sender_id, sender_name, timestamp, thread_id, | - | format, epoch, device, nonce, ct, sig}]} ------| + | sender_id, sender_name, timestamp, format, | + | epoch, device, nonce, ct, sig}]} --------------| | |<= chat_epoch_ack {v, epoch} =======================| pushed: a new epoch is open ``` @@ -2187,7 +2198,7 @@ message: ## 13. Versioning and compatibility -MNP versions independently of the package version. Current: **`6.2`**; oldest peer +MNP versions independently of the package version. Current: **`6.3`**; oldest peer accepted: **`4.0`**. 4.0 is the floor: a member presents a short-lived node-audience token bound to one node @@ -2217,6 +2228,10 @@ answer, as for any unknown type, and nothing else changes. the roots table, additively: a 6.1 node gives no answer to either message, and a client that ignores `paused` shows a paused root as an ordinary one. +6.3 serves `device_add_request` in the pre-proof window (§7, §9), additively: a 6.2 +node answers it `Handshake required` there, which a client reads as "this node cannot +link a new device", and nothing else changes. + The two numbers are separate on purpose. `MNP_VERSION` says what this build speaks; `MNP_MIN_SUPPORTED` says what it will talk to, and moving the second is a decision about whether an older peer can still do anything useful: diff --git a/docs/PACKAGING-GUIDE.md b/docs/PACKAGING-GUIDE.md index 5fbf7a7..e79fcd6 100644 --- a/docs/PACKAGING-GUIDE.md +++ b/docs/PACKAGING-GUIDE.md @@ -1,16 +1,19 @@ # MeshBay — Installation Guide -Four packages, all installed under `/opt/`: +Five packages, all installed under `/opt/`: | Package | What it does | |---|---| | `meshbay-common` | Shared Python venv with all dependencies | | `meshbay-hub` | Identity authority and group registry (server) | | `meshbay-node` | Local file host, streaming, chat daemon | +| `meshbay-cli` | Command-line client: your groups' files from a terminal (`man meshbay-cli`) | | `meshbay-client` | Desktop app (Electron) | Pick what you need: a desktop user installs **common + node + client**. -A server running the hub installs **common + hub**. +A server running the hub installs **common + hub**. **common + cli** gives a +machine — a server, one with no desktop — a member's access to their groups' +files from a terminal. --- @@ -23,6 +26,7 @@ sudo dpkg -i meshbay-common_0.9.0_amd64.deb sudo dpkg -i meshbay-node_0.9.0_amd64.deb # desktop machine sudo dpkg -i meshbay-hub_0.9.0_amd64.deb # server only sudo dpkg -i meshbay-client_0.9.0_amd64.deb # desktop machine +sudo dpkg -i meshbay-cli_0.9.0_amd64.deb # optional: the command-line client ``` If dpkg complains about missing dependencies: @@ -34,7 +38,7 @@ sudo apt-get install -f ### Uninstall ```bash -sudo dpkg --remove meshbay-client meshbay-hub meshbay-node meshbay-common +sudo dpkg --remove meshbay-client meshbay-cli meshbay-hub meshbay-node meshbay-common sudo rm -rf /opt/meshbay-* ``` @@ -49,12 +53,13 @@ sudo rpm -ivh meshbay-common-0.9.0-1.fc44.x86_64.rpm sudo rpm -ivh meshbay-node-0.9.0-1.fc44.noarch.rpm # desktop machine sudo rpm -ivh meshbay-hub-0.9.0-1.fc44.noarch.rpm # server only sudo rpm -ivh meshbay-client-0.9.0-1.fc44.x86_64.rpm # desktop machine +sudo rpm -ivh meshbay-cli-0.9.0-1.fc44.x86_64.rpm # optional: the command-line client ``` ### Uninstall ```bash -sudo rpm -e meshbay-client meshbay-hub meshbay-node meshbay-common +sudo rpm -e meshbay-client meshbay-cli meshbay-hub meshbay-node meshbay-common ``` --- @@ -69,7 +74,9 @@ node (with `meshbay-common` inside it). There is no Windows hub. Run the installer. It is **per-user** and lands in `%LOCALAPPDATA%\Programs\MeshBay\` without needing admin rights. The node daemon ships beside the app at `resources\node-runtime\meshbay-node.exe`; -the client finds it automatically. +the client finds it automatically. The command-line client, `meshbay-cli.exe`, +is beside it, and works in any new terminal (the light installer does not +carry it). It asks one thing — **when the node runs**: diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index cfb0743..dc263b8 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -148,8 +148,9 @@ and prints something like: Node key: 7mK2p...base64...= ``` -**Copy that key.** In your browser, on meshbay.org: **Profile → Link Node**, -paste it, submit. It is on the Profile page, not Settings — next to your +**Copy that key.** In your browser, on meshbay.org: **Profile → Linked nodes**, +paste it, **Link node**. Each machine you run a node on has its own key, and +each is linked the same way; linking one leaves the others as they are. It is on the Profile page, not Settings — next to your passphrase and recovery phrase, which is where everything tied to your identity rather than to this browser lives. diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index bab2074..1808598 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -212,10 +212,14 @@ a browser can open your groups with your passphrase. With it off — the default an account created in the application — your keys stay on this computer and nothing of them is left on anybody's node. With it on, the application leaves a locked copy on each node, as a browser would. The change reaches each group the -next time it opens. A browser can also be approved from the application for -itself, group by group: the browser shows a linking code (*Get a linking code*), -and you enter it in the application under *Your devices on this node*. That gives -the browser keys of its own without turning browser access on. +next time it opens. With it off, only the application (and the command-line +client) opens your groups: a browser keeps no keys between visits, so it cannot be +linked as a device of its own. + +On another computer, the application opens a group with keys of its own once a +device already linked approves it: the group shows a linking code (*Get a linking +code*), you enter it on that device under *Your devices on this node*, and the +group opens by itself. The application asks in a small window of its own, not in the page, before it hosts a group on this computer, shares a folder you did not pick in its folder @@ -224,6 +228,101 @@ points the node at another account. A folder you pick in the folder dialog is no twice. That window belongs to the application, so nothing displayed in the page — which shows content from other people's nodes — can answer it for you. +### The command-line client + +`meshbay-cli` reaches your groups and their files from a terminal — a server, +a script, a machine with no desktop. It is a package of its own on Linux (`meshbay-cli`, +with a manual page: `man meshbay-cli`), and comes with the desktop +application's full installer on Windows (not with the light one). + +```bash +meshbay-cli login -u alice # asks for the passphrase +meshbay-cli groups # your groups, their node, your role +meshbay-cli ls -l Family/photos # a group, a folder of it +meshbay-cli search beach 2024 # every group at once; paths get takes +meshbay-cli chat Family # the last 10 messages; -100 for 100 +meshbay-cli chat Family -m "On my way" # post one (-m - reads it from stdin) +meshbay-cli chat Family --follow # then each new message, until Ctrl+C +meshbay-cli chat # interactive: pick a group, talk, Esc to leave +meshbay-cli get -R Family/photos/2024 # download, here; -o elsewhere +meshbay-cli stream --player vlc 'Family/films/Night Drive.mkv' # play it; Ctrl+C stops +meshbay-cli put -R ~/scans Family/documents +meshbay-cli rm Family/documents/old-draft.odt # asks first; --yes in a script +meshbay-cli mkdir --parents Family/documents/2026/taxes +meshbay-cli rm -r Family/documents/2019 # a folder and all it holds +meshbay-cli logout +``` + +A group is named as the GROUP column of `meshbay-cli groups` prints it; add +`@owner` only when two of your groups share a name. Quote wildcards +(`'Family/music/*.flac'`): they are matched against the group, not against +your disk. An interrupted download resumes where it stopped the next time you +run the same `get`. `stream` plays a film or a song without downloading it: +it prints an address to open in VLC or mpv (or starts the player with +`--player`), which works on this computer only and until you press Ctrl+C or +close the player; skipping ahead in the player works. `rm` deletes what you uploaded, from any of your devices; +the node's operator can delete anything, and only the operator removes a +folder (`rm -r`, which says how many files go before it asks). Run `meshbay-cli` with nothing after it for a two-pane +view: this computer on the left, your groups on the right, F5 to copy the +selection across, Delete to delete it from the group (what `rm` may delete). + +`login` remembers the session, so later runs need no passphrase; `logout` +forgets it and everything else stored on this computer. What is kept is +encrypted, under a key held by the system's key storage — or, on a machine +that has none (a server with no desktop session), in a file only your user +can read, which `login` tells you. + +The first time it opens a group, the client may print a linking code instead +of the files: the node holds no copy of your keys (Browser access is off), so +this computer asks to be added as one of your devices. Enter the code in the +application under *Your devices on this node*, or run +`meshbay-cli devices approve GROUP CODE` on a computer already linked; the +client goes on by itself. + +Joining and leaving work as in the application (§4): + +```bash +meshbay-cli invitations # what you were invited to +meshbay-cli join Family@bob --code K7P2-9WQX # accept, then give the node its code +meshbay-cli join 'https://meshbay.org/#/invite?v=1&...' # a link, pasted as sent +meshbay-cli decline Club@carol +meshbay-cli leave Club@carol # asks first; --yes in a script +``` + +`meshbay-cli devices list GROUP` shows your devices on the node serving a group; +`devices approve` admits a new one by its code, `devices revoke` retires one. + +Like the application, the client keeps your keys to itself: a browser cannot +open, with your passphrase alone, a group you joined from it. To allow that, as +*Browser access* does in the application, run `meshbay-cli browser-access on` +on that computer; `off` takes the copy back. It is that computer's setting. + +Your account, from the client: + +```bash +meshbay-cli account # name, e-mail address, status +meshbay-cli account register -u alice --email alice@example.org +meshbay-cli account passphrase # change it; asks the current one, then the new +meshbay-cli account reset -u alice --email alice@example.org # forgotten: a code by e-mail +meshbay-cli account email new@example.org +meshbay-cli account sign-out-everywhere +meshbay-cli account delete +``` + +`register` prints your recovery key once: keep it apart from your passphrase, +it is what gives your groups back after a `reset`. Changing the passphrase +carries the copy of your keys each node keeps for browsers over to the new one +first; a node out of reach is carried the next time this computer connects to it. + +Tab completes actions, options and the paths in your groups once the shell +knows how. In bash, add `eval "$(meshbay-cli completion bash)"` to `~/.bashrc` +(the Linux packages install it already); in PowerShell, add +`meshbay-cli completion powershell | Out-String | Invoke-Expression` to your +`$PROFILE`. It never asks for anything: on a computer that is not signed in, +it completes actions and options only. + +Managing a node is not the client's job: that is `meshbay-node` (§6). + --- ## 4. Joining a group @@ -389,8 +488,8 @@ that matters to you. ### Backing up your phone On the Android application, **Android Sync** in the menu under your avatar, -top right, backs up your phone's photos, contacts, calendar, text messages and -the files of folders you choose to a group. +top right, backs up your phone's photos, contacts, calendar and the files of +folders you choose to a group. Text messages are not backed up. Only your **personal profile** is backed up. If your phone has a work profile, nothing in it is sent, and MeshBay installed inside the work profile offers no @@ -455,18 +554,6 @@ backup at all. (`calendar-2026-10-10-0900.ics`). Any calendar application can import it; repeating events, their exceptions and reminders come along. -#### Text messages - -- **Turn on** asks for access to your text messages. The first copy holds them - all; then, once a day, a new file holds only the ones that arrived or were - sent since. -- The files go by year (`alice-messages/2026/sms-2026-10-10-0900.xml`), in the - XML format SMS backup applications can restore onto a phone. Senders are - named when contacts backup is also allowed. Multimedia messages (MMS) are - not included. -- The version of the application from the Play Store has no text messages - backup: Play allows reading them only to the phone's messaging application. - #### Files - **Add a folder** opens Android's folder picker: choose `Documents`, a folder @@ -1001,9 +1088,10 @@ anything missing. **"No nodes are currently online for this group."** The node is not running, is not reachable, or has not linked its key to the hub. On the node: `meshbay-node status`, then Step 4 and 5 of the quickstart. -In the desktop app, the **Node** page says when your account is linked to the -node of another machine (**Link this node instead**); **Profile → Unlink** -lets go of a node from anywhere, for a machine that is gone. +In the desktop app, the **Node** page says when this machine's node is not +linked to your account (**Link this node**). **Profile → Linked nodes** lists +every node your account has linked, one per machine, and **Unlink** lets go of +one from anywhere — for a machine that is gone — without touching the others. **A member sees the group but no files.** They never redeemed an invitation code, or the group has no key @@ -1034,7 +1122,7 @@ fresh invitation code. **"Browser access is off for this account."** The account keeps its keys in the desktop application. Turn browser access on -there (Profile), or approve this browser from it. +there (Profile). **Video says the codec is not supported.** The source codec has no decoder in that browser and transcoding is off, or @@ -1198,6 +1286,8 @@ Better to know now than to go looking for it: phones take JPEG unless set otherwise. - **Hubs do not talk to each other yet.** Everyone in a group needs an account on the same hub. +- **The command-line client lists, downloads and uploads.** Searching across + groups and chat are not in it yet. - **Casting reaches Chromecast devices** from the desktop and Android applications, for films, photos and music. Support for other TV protocols is designed but not built. diff --git a/docs/WINDOWS-PORT.md b/docs/WINDOWS-PORT.md index 840f22a..cd0c89b 100644 --- a/docs/WINDOWS-PORT.md +++ b/docs/WINDOWS-PORT.md @@ -729,8 +729,14 @@ real bug found while testing W4, not from the original plan. verify `safeStorage`/DPAPI works, verify Task Scheduler autostart. - **Electron:** visual check of the UI, native save dialog, node management panel (once W3 is done). -- **Long paths:** a library with a path exceeding 260 characters on NTFS - with long paths enabled. +- **Long paths:** done for the node (2026-10-14), with `LongPathsEnabled` + off, the default: `test_long_paths.py` indexes, serves, deletes, uploads + into, lists and enriches files whose path is past 260 characters, and + fails without the change. Before it, the walk could not list such a + directory and its files never reached the members. Checked by hand in the + installed app the same day, files at 312 and 380 characters: indexed, + listed, streamed, downloaded byte for byte, uploaded into, deleted; tags, + the cover beside the track and the video and photo thumbnails all there. - **Case collisions:** a group indexed on ext4 containing `Film.mkv` and `film.mkv`, opened from a Windows client. |