aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node
Commit message (Collapse)AuthorAgeFilesLines
* test: make both suites pass on WindowsChristophe Besson18 hours20-49/+74
| | | | | | | | | | | | | | | | | | | | | | | | | | | Most of these failed on Windows for reasons that had nothing to do with the code under test, which is how real Windows defects hid among them: - Read and write files as UTF-8, and talk to Node in UTF-8. read_text(), write_text() and subprocess text=True use the locale codepage, cp1252 on Windows: "é", "—" and "→" arrived as "?" or crashed, some sixty tests. Calls to PowerShell and schtasks are left alone -- they answer in the console codepage. - Import ESM harness modules by file URL (as_uri): a raw "C:\..." path is not a module specifier. - test_cli_golden: mask the tmp path in its JSON-escaped form, spell it the POSIX way, record on Linux, mask the protocol version (the recording had failed everywhere since the MNP 4.0 bump) and argparse's version-dependent quoting; point USERPROFILE at the tmp home, or `member invite` and `operator pair` wrote their codes into the developer's profile. - test_disk_io_off_loop: expect what a free loop can reach on the platform's timer, 15.6 ms on Windows, not an assumed 5 ms. - test_root_paths_are_operator_only: expect the OS's spelling of the path. - test_audio_meta_cache: find ffprobe with shutil.which. Node suite on Windows: 1489 passed, none failed. Hub suite: 3 failures left, all older than this change (two SQLite concurrency tests, one transfer resume). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: Windows installer and desktop app start and stop the node one wayChristophe Besson18 hours1-69/+403
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A 0.16 upgrade in service mode left the previous node running: setup's unelevated taskkill cannot reach session 0, and it ran in customInstall, which electron-builder inserts after the files are copied. The locked exe was not replaced, and the new app talked to the old node ("started but could not link", "No operator paired"). Installer (build/installer.nsh, build/stop-node.ps1): - customCheckAppRunning, which runs before uninstallOldVersion and extraction, stops the node with an embedded stop-node.ps1: control API, then schtasks /end, then Stop-Process, and refuses to half-upgrade if one survives. - An upgrade keeps the mode it finds (task, launcher, previous install), restores the sign-in launcher the old uninstaller deletes, and restarts the node the way that mode runs it. A silent upgrade of an "at sign-in" install used to end with no autostart and no node. - The uninstaller removes the task and firewall rules only on a real uninstall, not on an update. Desktop app (src/main.js): - Start, Stop, Restart and node:start go through the CLI's lifecycle verbs instead of a second implementation; a child spawned by Electron also held Electron's sockets after the app quit. - "Only while MeshBay is open" is a real mode: the app starts a provisioned node at launch and stops the one it started when it quits. - Switching modes stops the node first -- deleting a task does not end its instance, and a new service found the port taken -- keeps the firewall rules every mode needs, and starts the node again. A declined or unanswered UAC prompt restores the node instead of leaving it stopped, and says that nothing changed. - waiting_for_hub counts as a node that is up; linking waits for a node that answers, with a longer deadline, and reports a version mismatch. Packaging (packaging/win): - The service task gets no 72-hour limit, runs on battery and ignores a second start; service.ps1 status reports a stale registration so setup re-registers it; remove ends the running instance before deleting the task. - build-node-runtime.ps1 starts the frozen daemon in a throwaway profile (smoke-node-runtime.ps1) instead of only asking for --help. The mode that was "Off (start manually)" is labelled "Only while MeshBay is open" in all ten catalogues. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): a Windows daemon that stops properly, starts honestly and runs onceChristophe Besson18 hours10-215/+948
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Found by installing the builds and driving every startup mode live: - Stop through the node's own control API first (POST /api/shutdown, loopback and per-run token): the one channel that reaches a daemon in any session without elevation -- a service node runs in session 0 -- and the one that runs its shutdown. Then Task Scheduler, then a forced stop. Nine stops in a row used to log no shutdown at all: each was a TerminateProcess. - The forced stop spares the command running it. The frozen meshbay-node.exe is the daemon and every CLI verb, so `taskkill /IM meshbay-node.exe` killed `autostart stop` and `restart-daemon` themselves: exit 1, no output, and no node after a restart. It excludes its own pid and its parent's, and /T takes a venv launcher's python child and a daemon's ffmpeg children with it. - Start and restart report the version that answered, never "started" about a node nobody asked; `service start` says so when no node answered, and where the log is. - A second instance fails before it touches anything. The daemon wrote ui-token, then failed to bind inside uvicorn's task and exited with the reason on a hidden console; the node still running then refused every stop and status, its token file naming a dead process. The control port is now bound first (exclusively on Windows, where SO_REUSEADDR would share it), and a refusal is logged and exits 2. Linux had the same order. - The daemon logs to %LOCALAPPDATA%\meshbay\state\node.log: Task Scheduler discards its stderr. Only the daemon run opens it, never a CLI verb. - Hub sign-in waits are interruptible, a stop requested before the node is up is honoured, and a hub that answers 429 or restarts leaves the node in waiting_for_hub rather than looking dead. - operator_paired is null until the roster is read, instead of a false that showed "No operator paired" about a node whose pairing was intact. The node test conftest also points HOME, USERPROFILE, LOCALAPPDATA and APPDATA at a throwaway directory for every test, and keeps log_file() away from the developer's own node: redirecting HOME alone isolates nothing on Windows, and the CLI tests had been writing invite and pairing codes into the real profile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node,client): query TMDB only once a language is chosen, in that languageChristophe Besson2 days7-1/+262
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | TMDB fiches are fetched lazily, on browse, and the fetch used to run whatever the moment it was first triggered — routinely before the operator had opened settings and picked a language, so it queried in TMDB's English default. Then the fiche was cached by tmdb_id alone, with no note of language and a 30-day TTL, so switching to the intended language afterwards changed nothing: the English fiche was served until it expired. The operator's only recourse was to find and wipe the cache by hand (found live 2026-09-26: a whole library indexed in English although "Français" had been chosen). Two rules now, both there to make the first fetch the right language rather than English-then-corrected, and to stop the doubled requests that eventually get a node rate-limited: - No language configured, no query. media_meta_req/season_meta_req answer confidence 0 and make no TMDB call while tmdb_language is unset; the fetch waits for the operator's choice, so the first (and only) query is in it. English is now a first-class choice (en-US), not the default of skipping the setting. - Changing the language wipes the metadata cache (ops.set_tmdb_config), so the new language takes effect on an already-browsed library. The file->tmdb matches are language-independent and kept. The client refetches on the tmdb_config_ack that carries the new language, so the grid updates without a page reload. docs/MESHBAY_DESIGN.md §9.7 states both rules; tests cover the gate and the cache wipe, and two existing handler harnesses now declare a language. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(node): read the packaged TMDB token in placeChristophe Besson2 days3-74/+58
| | | | | | | | A node onboarded by the desktop client never ran `init`, so default.env was never copied to node.env; and default.env was 0600 root, unreadable to a per-user node anyway. The daemon now loads it beneath node.env, 0644. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(protocol): bind the MNP token to the node it is for (E10)Christophe Besson3 days2-0/+2
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | The audience split stopped a member's node credential from opening the hub API. It did not stop the credential being *replayed to another node*: the MNP token carried the member's whole group set and named no node, so a token handed to node A's operator could be presented to node B the member also belongs to. That does not read content on B — the handshake still requires proving node B's group key, which the operator lacks — but it reaches B's pre-proof window and fetches the member's *encrypted* keypair bundle for B (offline-attackable, bounded, audited): a disclosure §2.4 says should not follow from hosting a member on A. The token now names the node it is minted for (a `node` claim = that node's Ed25519 key), and authorize_token refuses one that names a different key. The client already knows the target node's key (from /v1/groups/{id}/nodes) and asks for a token bound to it: POST /v1/nodes/mnp-token takes node_pk, and transport.connect threads it (group-page, the connection pool and rewrap pass n.pk_node; reconnect preserves it). A token that names no node is still accepted, because the hub only mints one for the authenticated requester, so an unbound token grants nothing across accounts — which also keeps non-binding callers working with no churn. Done before deploy, so it folds into the MNP 4.0 flag day rather than needing its own. Docs: §5.2, register E10, MESHBAY_NODE_PROTOCOL.md §6.3. test_handshake.py and test_mnp_token.py hold the binding (a token for node A is refused by node B, accepted by node A; an unbound token still works); red before, green after. common/node/hub suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(node,client): survive a transient hub state on login, and surface a ↵Christophe Besson3 days2-0/+126
| | | | | | | | | | | | | | | | | | | | | | | | | | | | failed node-key link Two defensive gaps turned a routine reset-and-reonboard into "impossible de démarrer le node": 1. daemon._login_with_retry retried a 401 (node key not linked yet) but `raise`d on every other status, so a 429 — the daemon's own 5s retries hitting the sign-in rate limit — or a 502/503 while the hub restarts during a deploy killed the process, and systemd crash-looped it. Those statuses (429, 5xx) are now retried with a back-off that respects Retry-After, so a freshly reset node stays alive (the operator needs it up to read its key) instead of dying. A genuine 4xx (400/422) still raises. 2. create-group's linkNodeKey swallowed every error as "already linked or same key" — but PUT /me/node_key is idempotent and returns 200 on a re-link, so there was no benign error to hide: the catch only ever hid a real failure (a rejected session, a bad key), letting the wizard proceed against a node that looked linked but was not, which then could not authenticate. The link failure now surfaces (detectNode shows it). test_login_retry_is_resilient.py holds the retry behaviour (429/5xx retried, Retry-After honoured, 401 stays alive, 400 still raises); red before, green after. common/node/hub suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(protocol): MNP 4.0 flag day for the node-audience token (B2)Christophe Besson3 days1-56/+56
| | | | | | | | | | | | | | | | | | | | | | | | | | | The node-audience token (previous commit) is a change to what a peer must present, so it is a MAJOR per the versioning rule (§5.6): a pre-4.0 client presents its hub session token and a 4.0 node refuses it, and there is no compatibility branch, because leaving one would keep a hub credential reachable by every node (C6's lesson). So the floor moves with the version. - MNP_VERSION 3.4 -> 4.0 and MNP_MIN_SUPPORTED 3.0 -> 4.0 (meshbay_common); transport.js MNP_V/MNP_V_MIN -> 4.0 to match. - MIN_CLIENT_VERSION 0.13.0 -> 0.16.0 so a stale desktop client is told to update before connecting rather than meeting a handshake refusal it cannot read; the browser reloads this build from the hub. - Regenerate tests/golden/dispatch.json: the only change is the `v` the node stamps on outbound messages, 3.4 -> 4.0 (56 cases, v field only). - Document the split and the flag day: MESHBAY_DESIGN.md §5.2 (the handshake token is the MNP-audience token), §5.6 (the 4.0 flag day), register E10 and decision 23; MESHBAY_NODE_PROTOCOL.md §6.3 (authorize_token binds MNP_AUD) and the wire-version banner. Deploy is coordinated and atomic (common+hub+node+SPA together); a live browser-to-node validation and the deploy itself remain. common (173), node (1489, the pre-existing test_cli_golden argparse/prog artifact aside) and hub (1471) suites all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(hub): present a node-audience token in the handshake, not the hub ↵Christophe Besson3 days5-6/+18
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | session token A member authenticated to a node in the MNP handshake with its hub *session* token — scope=user, valid at the hub API for hours. A node operator is in the threat model, so this handed them a live hub credential for the member: enough to enumerate the member's other groups, act as them, and (before the previous commit closed it) take the account over. The node genuinely needs a hub-signed membership assertion, so the fix is to make that a separate credential that opens nothing at the hub API. Two audiences signed by the one hub key (meshbay_common/tokens.py): - HUB_API_AUD — session tokens (login, device-auth, node-auth, refresh), used for hub calls and signaling. decode_access_token now binds this audience, so an MNP token cannot be replayed against the hub API. - MNP_AUD — a short-lived token a member presents to a node and nothing else, from POST /v1/nodes/mnp-token. authorize_token now binds this audience, so a session token presented to a node is refused. This closes the disclosure. The node's own self-decode (hub_client.py) reads its node token with audience=HUB_API_AUD. The client fetches the MNP token inside transport.connect() (and on every reconnect) using the session token, so callers are unchanged and signaling keeps using the session token. No regression to a long session: the MNP token is checked once, at the handshake, before any proof — a film already playing is not re-authenticated, so a 15-minute token does not interrupt a 4-hour film; reconnects refetch a fresh one. Denylist and membership checks are unchanged (the MNP token carries sub/jti/groups). Tests: authorize_token refuses a session/no-audience token and accepts an MNP token; the hub API refuses an MNP token; POST /v1/nodes/mnp-token is minted only for a member's own session. Verified red-before/green-after; common, node and hub suites green (the pre-existing test_cli_golden failure is an argparse/pytest prog artifact unrelated to this change). Still to do before deploy (B2): bump the MNP version and client.minimum so a stale desktop client is told to update rather than getting a handshake refusal, update docs/MESHBAY_DESIGN.md and MESHBAY_NODE_PROTOCOL.md, and validate against a real node locally, then deploy hub+node+SPA atomically. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: invitation links no longer bound to an e-mail addressChristophe Besson3 days8-36/+60
| | | | | | | | A link is redeemable by whoever opens it first, so it can be sent by any messaging app. The address is optional (mail + label only); a link lives 7 days, fixed. Adds a Share button; see MESHBAY_DESIGN.md §3.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: bump version to 0.16.0Christophe Besson3 days2-2/+2
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): split ops.py into the ops packageChristophe Besson4 days16-2060/+2285
| | | | | | | | | Each section of ops.py becomes a module of meshbay_node/ops/ (core, node_toml, members, chat, groups, roots, files, settings, apps), cut as text; ops/__init__.py keeps the docstring and re-exports every name, so `ops.<name>` is unchanged for every caller. Logger name unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(node): read ops wherever it is splitChristophe Besson4 days2-6/+26
| | | | | | | | The tests that read ops.py for an absence (fastapi, a TOML path without as_posix, the reference app's name) and the one counting operations by module now take every file of ops.py or the ops package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move per-app enrichment out of NodeDaemonChristophe Besson4 days5-294/+305
| | | | | | | | The _enrich_*/_reenrich_* methods and _on_enriched become EnrichmentMixin in meshbay_node/enrichment.py, with the two directory helpers only they use. APP_DIR_KEYS stays on NodeDaemon. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move the CLI out of daemon.py into cli/Christophe Besson4 days17-1427/+1600
| | | | | | | | | | Each `if args.command == ...` branch of main() becomes a function in cli/ (one module per family of verbs); the parser, the process setup and a VERBS table go to cli/parser.py and cli/dispatch.py. daemon.main runs the verb or starts the daemon. Tests patch the CLI through conftest.patch_cli; the CLI golden is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(node): record what every CLI verb printsChristophe Besson4 days2-0/+1001
| | | | | | | | A golden master of `meshbay-node`: help text, every verb, the prompts answered "no", each verb's usage fallback. Exit code, output, loopback calls with their bodies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(node): keep the CLI dispatch test off the developer's nodeChristophe Besson4 days1-0/+8
| | | | | | | | The stub left HOME and config.DEFAULT_CONFIG_PATH real: every run wrote TEST-CODE over ~/.local/share/meshbay/invite-code and pair-code, and `status` read the real keystore and asked the running node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(node): read the daemon's source wherever it is splitChristophe Besson4 days9-59/+138
| | | | | | | | Tests that read daemon.py now take their text from node_source (daemon.py, NodeDaemon's bases, the cli package once it exists). The CLI tests that sliced main() run the CLI instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): Peers tab answered 500 after the webrtc splitChristophe Besson4 days3-1/+70
| | | | | | | | /api/peers imported _get_remote_ip, inside the function, from webrtc_server, which no longer has it. A test now resolves every meshbay import in the node's source, function bodies included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): run signed operations through a table of executorsChristophe Besson4 days1-85/+37
| | | | | | | | _do_admin_response looked each operation up in a 28-branch elif, every branch the same call. The admin cases of the dispatch golden are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): dispatch MNP messages through a tableChristophe Besson4 days3-196/+166
| | | | | | | | | | The pre-authentication guards stay explicit code, in the same order and text. After the handshake, a table maps each type to its handler and to whether it runs as a task, the choice each branch made; the three inline blocks become StreamingMixin methods, unchanged. The dispatch golden is identical, including types that are not strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(node): the dispatch golden covers a type field that is not a stringChristophe Besson4 days2-0/+258
| | | | | | | | Missing, None, an int, a list, a dict, bytes: recorded on the elif chain before it becomes a table, since a dict lookup would raise on the unhashable ones where the chain found no match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move _dispatch_message out of webrtc_server, unchangedChristophe Besson4 days3-244/+254
| | | | | | | | DispatchMixin in transport/webrtc/dispatch.py, with the pre-proof fetch bound it enforces. The elif chain moves as it is; turning it into a table is the next commit, on its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move the session core out of webrtc_serverChristophe Besson4 days3-322/+342
| | | | | | | | SessionCore in transport/webrtc/core.py, last among the bases: state, the data channel, task ownership, the peer registry, sending and teardown. The facade keeps _dispatch_message and WebRTCTransport. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move operator authority and the admin response out of ↵Christophe Besson4 days2-307/+318
| | | | | | | | | webrtc_server AdminMixin in transport/webrtc/admin.py: who the operator is, the signed challenge, signature checks, and _do_admin_response moved unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move the operator's group controls out of webrtc_serverChristophe Besson4 days3-368/+383
| | | | | | | | GroupOpsMixin in transport/webrtc/group_ops.py: member revocation and unpinning, the group key rotation, apps and their directories with the allow-list, and Search listing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move the operator's node controls out of webrtc_serverChristophe Besson4 days2-563/+588
| | | | | | | | NodeOpsMixin in transport/webrtc/node_ops.py: status and settings, roster and denylist, roots, hosted groups, reload, scan pacing and transfer limits, with their class-level bounds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): section headers follow the code they introduceChristophe Besson4 days3-21/+16
| | | | | | | | Three headers stayed in webrtc_server when their sections moved; they now sit above the same methods in blobs.py and admission.py. Two that were only a title over what is now a whole module are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move the MNP handshake out of webrtc_serverChristophe Besson4 days3-355/+373
| | | | | | | HandshakeMixin in transport/webrtc/handshake.py: challenge, proof, channel binding, and the sealed configuration a peer receives once admitted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(node): two stale comments in webrtc_serverChristophe Besson4 days1-6/+2
| | | | | | | | The peer-session ceiling no longer quotes the hub's old per-account number, and the note on a single uploads/ directory goes: its constant left with f3fb449, when an upload started landing in the folder it was sent to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move invitations, joins and device linking out of webrtc_serverChristophe Besson4 days2-847/+864
| | | | | | | AdmissionMixin in transport/webrtc/admission.py, with the invite handle pattern and the join attempt bounds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move uploads out of webrtc_serverChristophe Besson4 days7-416/+422
| | | | | | | UploadMixin in transport/webrtc/upload_handlers.py, with the upload cap; _upload_chunk moved unchanged, _append_chunk joins webrtc/disk.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move transfer slots and leases out of webrtc_serverChristophe Besson4 days3-262/+285
| | | | | | | TransferMixin in transport/webrtc/transfer_handlers.py, with the sweep interval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move file browsing and folder/file ops out of webrtc_serverChristophe Besson4 days7-473/+502
| | | | | | | FilesMixin in transport/webrtc/files.py; the blocking disk helpers join _locate in webrtc/disk.py, and the lease states go to webrtc/limits.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move group chat out of webrtc_serverChristophe Besson4 days4-654/+670
| | | | | | | | ChatMixin in transport/webrtc/chat.py: sealed messages, history, epoch keys, link previews with their cache and rate bounds, and the operator's chat ops. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move per-account blobs and key bundles out of webrtc_serverChristophe Besson4 days3-244/+262
| | | | | | | BlobsMixin in transport/webrtc/blobs.py, with the blob caps and the kind pattern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move video streaming out of webrtc_serverChristophe Besson4 days12-652/+663
| | | | | | | StreamingMixin in transport/webrtc/apps/streaming.py: stream credit, handover, the transcode slots and _stream_video_inner, moved unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move the Videos catalogue handlers out of webrtc_serverChristophe Besson4 days3-716/+739
| | | | | | | | VideoMetaMixin in transport/webrtc/apps/video_meta.py: media and season meta, TMDB search and its per-member bound, posters, and the operator's TMDB ops. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move the MusicBrainz switch to the Music mixinChristophe Besson4 days2-38/+40
| | | | | | An operator op that exists for one app lives with that app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move the Music handlers out of webrtc_serverChristophe Besson4 days3-192/+212
| | | | | | | MusicMixin in transport/webrtc/apps/music.py: tags and cover art, and the audio transcode with the extension list that gates it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move the subtitle handler out of webrtc_serverChristophe Besson4 days5-176/+199
| | | | | | | SubtitlesMixin in transport/webrtc/apps/subtitles.py; _locate, which it shares with the files, music and streaming handlers, in webrtc/disk.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move framing, shared limits and ffmpeg jobs out of webrtc_serverChristophe Besson4 days6-343/+383
| | | | | | | transport/webrtc/channel.py, limits.py and media_tools.py, cut from webrtc_server.py as text; the facade imports them back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(node): record what the session does with every message it can be sentChristophe Besson4 days2-0/+24943
| | | | | | | | A characterisation test over every MNP type, peer state and message shape, and every signed operation's answer: the reply, the audit, the handler started. It pins dispatch order and the admin table so they can be moved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(node): a patched module attribute must be read where it is patchedChristophe Besson4 days1-0/+118
| | | | | | | A function reads its globals from the module that defines it, so patching a name a module only re-exports changes nothing and the test passes anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: read the WebRTC transport's source as a set of filesChristophe Besson4 days11-93/+160
| | | | | | | | | Source-reading tests take their text from node_source (node) and node_tree (hub): webrtc_server.py plus anything under transport/webrtc/, so a check for something's absence keeps reading the code it guards if that code moves. test_node_source_scope holds the boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): a connection that ends leaves its group's peer setChristophe Besson4 days2-0/+70
| | | | | | | | | | connectionstatechange dropped a closed or failed session from the transport and stopped its tasks, but only close() took it out of the group's peer set, and nothing on that path called it. Every broadcast then went to closed channels, and each reconnect left a dead session held until restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(hub): offer ceilings that bound a member without failing an ordinary oneChristophe Besson5 days1-2/+3
| | | | | | | | | | | | | | | | | | | | | | | | | | | A phone on 4G found one of five groups missing from Search on half its visits, and all but one on a fifth of them. Every offer that reached the node was answered in about a second; the hub refused the others with 429 — 93 of them in half an hour on meshbay.org, all from that phone, all to nodes that were up. Two ceilings did it. Three pending offers per account was exactly what one Search page dialled at once, so the first tile or reconnection beside the sweep was refused. Thirty offers a minute per address and per node was spent by four or five reloads of a page whose groups share one node. Sized now against an account with twenty groups on three devices: - 32 offers pending per account, across devices and nodes. - A budget per account and per node, a burst of 120 refilled at two a second. This is what bounds a member's cost to one machine (H6), and it leaves their other nodes alone. Counted by account, because a mobile carrier puts many subscribers behind one IPv4 address. - 600 a minute per address and per node, as a coarse guard in front of authentication only. Both refusals carry Retry-After, which the browser now honours. The node's own ceiling on peer sessions is unchanged; its comment no longer quotes the old per-account number. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node): member invite --link and member cancel in the CLIChristophe Besson5 days6-6/+274
| | | | | | | | | The CLI makes both halves itself — the node's code, then the hub's ticket bound to the address — and prints the link; a refused ticket takes the code back, and cancel takes back both. The CLI never asks the hub to mail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: drop the invitation-link plan from the treeChristophe Besson5 days5-5/+5
| | | | | | | | The plan was a working document; what it decided lives in MESHBAY_DESIGN.md and MESHBAY_NODE_PROTOCOL.md. Code and tests now cite those instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(node): invitation links — a code bound to no account until redeemedChristophe Besson5 days7-25/+695
| | | | | | | | | | New invite kind "link": member of one group, once, never operator, not spendable by an active member, capped at 20 per group, cancellable by handle. Signed ops invite_link_create / invite_cancel, loopback routes, and the known-device join path now accepts a link code. Adds the plan, docs/invite-links.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>