aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
Commit message (Collapse)AuthorAgeFilesLines
* fix: an identity signs a named kind, and a device approval answers a requestChristophe Besson31 hours5-32/+99
| | | | | | | | | | | The desktop main process builds every transcript itself from fields (transcripts.js) and signs no raw bytes; the page's identity has the same contract (crypto.js transcriptFor). The keyring seals no bundle while browser access is off. On the node, device_add must redeem a pending request filed by the same keys, and device_revoke is signed under its own prefix (meshbay:device_revoke:v1), so a retirement signature admits nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(hub): the pepper and a device key take the passphrase, not a tokenChristophe Besson31 hours4-16/+32
| | | | | | | | | POST /me/bundle-pepper (was GET) and POST /users/devices require auth_key. A refreshed or lifted token could otherwise fetch the pepper, or register a device whose every sign-in carries it. Both callers have just been given the passphrase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: browser access, decided in the desktop applicationChristophe Besson34 hours16-5/+189
| | | | | | | | | Off for an account made there: its identities stay on the device and nothing is left on nodes. Turned on from the Profile page behind a native confirmation; each node is settled when its group next opens. The hub keeps a mirror a browser reads to say why a group will not open; it grants nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(client): the desktop application keeps M and every node identity in its ↵Christophe Besson35 hours19-59/+270
| | | | | | | | | | | | main process keyring.js derives, opens, mints, seals, signs and agrees there; the page gets public keys and a handle. Argon2 comes from the page's own WebAssembly build (Electron's crypto has none). Without OS key storage the page keeps its keys as a browser does. A node's bundle is settled after connecting, re-sealed when the key changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(hub): the transport holds an identity, never a private keyChristophe Besson36 hours10-96/+87
| | | | | | | | Two public keys, sign() and shared(); the apps take transport.signFn. What holds the keys (this page, or the desktop main process) is the identity's business alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: bundles sealed per node under the passphrase and the hub's pepperChristophe Besson37 hours19-197/+273
| | | | | | | | | The session key is M = HKDF(Argon2(passphrase) || pepper, account id); each node's bundle key and the playlist key derive from it. Bundles are MBK3, bound to account and node; MBK1/MBK2 are refused by name, never replaced silently. Playlists move to key v2 and are re-sealed over unreadable node copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(hub): a downloaded file opens in a tab only under a type that runs nothingChristophe Besson38 hours3-20/+56
| | | | | | | Open is offered for PDFs, raster images, audio, video and plain text, typed from the name; HTML, SVG and the rest are not opened in the hub's origin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(client): the page names node operations, and the app confirms what ↵Christophe Besson39 hours16-92/+149
| | | | | | | | | | | widens the node node:call is replaced by named operations with checked arguments; hosting a group, sharing an unpicked folder, key rotation, denylist clearing and a change of node account are confirmed by a native dialog. Every channel checks its sender, secrets:get/set/clear are gone, node:start writes the app's own hub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: only the owner decides who hosts a group, and nobody is made a member ↵Christophe Besson40 hours13-2/+335
| | | | | | | | | | | | | | | | | | | | | | | | | | unasked - hub: a node may host a group only if its account owns it or the owner approved that node (new `group_hosts`). Membership was the ceiling, and every member holds the group key, so any member's node could register as a host and be the one clients kept. A node claiming a group it may not host is recorded as a request; the owner is notified once and approves or refuses it (GET/POST/DELETE /v1/groups/{id}/hosts[/{node_id}]), which takes effect on a connected node at once. - hub: an owner adding a username creates an invitation (new `group_invitations`), accepted or declined by the invitee (/v1/groups/invitations, /{id}/invitation/accept|decline). Until then the group is not listed, not dialled, not searched and not in any token. Invitation links, open joins and group creation still make members directly: they are the account's own act. - hub: the MNP token names only the group it is minted for (group_id is now required), so a node operator no longer learns a member's other groups. - SPA: invitations on the home page; invited people and host requests in the group's settings; the transport sends group_id. Ten catalogues. - Browser probes for both screens, run in Chrome and Firefox. - Design §5.2, §7.2, §7.3, AV32, AV33; protocol §6.3; user guide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(hub): sitemap.xml, named by robots.txtChristophe Besson41 hours2-0/+22
| | | | | | | Home, downloads, and the repository's about page and docs on git.meshbay.org, spelled as the welcome page links them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: keypair_bundle_delete is reserved for device_policy (O3)Christophe Besson3 days1-0/+4
| | | | | | | The node honours it and no interface sends it; offered alone it would strand the next browser that signs in. Stated in both documents. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor: drop the unread transfer_limits field from the handshake ackChristophe Besson3 days1-9/+0
| | | | | | | The interface reads a member's cap from transfer_state and never read the copy on the ack. The transfer probe reads it from transfer_state too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(client): save files under a name every platform can writeChristophe Besson3 days15-6/+113
| | | | | | | | | A node serves the name its disk gave a file; the client now makes it portable at save time (single file, zip entries, zip name) and says so on the transfer row. Same rule as paths.sanitize_for_download, held by a parity test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(hub): reports from public-group members, decided by an administratorChristophe Besson3 days15-5/+435
| | | | | | | | | | A report needs a person's account at least a day old, membership of the public group, and fits a daily allowance per account. Past the threshold a hash is queued and administrators are notified; blocking without review is an instance setting, off by default. Report menu item in public groups, Reports tab and settings in the admin panel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: bound pending admin challenges and sign every value an op acts onChristophe Besson3 days4-15/+62
| | | | | | | | | | Any member could make a node hold unbounded challenge requests; a connection now keeps at most 8, 64 KiB each. root_add, group_attach, invite_create and tmdb_config signed less than they did; their subjects are now canonical JSON of every value (the TMDB token by SHA-256). MNP 5.0, floor kept at 4.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor: remove dead code across packagesChristophe Besson3 days10-112/+9
| | | | | | | | | Unused modules, functions, constants and client helpers with no caller, the unreachable hub:probe IPC handler, and the CSAM hash matching. Behaviour unchanged; the dispatch golden loses only the two removed message types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: refuse an unsigned handshake challengeChristophe Besson3 days1-25/+16
| | | | | | | | Every node the 4.0 floor admits signs its challenge, and one without a channel binding could not complete the proof anyway, so a missing signature is refused like a wrong one (browser and QUIC client). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(hub): new login tagline, dimmer gradient topChristophe Besson4 days11-11/+11
| | | | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(hub): say so when a lazily loaded view cannot be fetched0.16Christophe Besson4 days13-5/+66
| | | | | | | | A tab opened before a hub deploy got 404 for every module it had not loaded yet, and lazy.js kept its spinner for good. It now shows a notice with a Reload button and logs the failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(hub): keep a show's detail modal open under the playerChristophe Besson4 days4-4/+33
| | | | | | | Closing the player lands back on the season being watched, with the episode just started marked. A film's modal still closes on Play. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(hub): the sidebar's Node section follows the node linkChristophe Besson4 days2-8/+35
| | | | | | | | | | | | | | | | | | Reported on a real install: after the first click on Create group, the Node section (Node, Create group) disappeared from the sidebar until a reload, although the group was created and the node ran. hasNodeKey was read once per session change and never again, so a node the wizard linked stayed out of the sidebar; and that read swallowed its errors, so a session blip (a refused renewal, then the desktop app's silent device sign-in) followed by one failed request hid the section for good. The wizard now tells the app when it has linked or started a node, the app asks again then and after a group is created, and a failed read is retried -- never applied to a session that has changed meanwhile. The wizard also starts the node on its own for a node in waiting_for_hub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(hub): say "No operator paired" only when the node says soChristophe Besson4 days1-4/+12
| | | | | | | | | The Node page showed the banner whenever operator_paired was not true, so a node that had not yet read its roster -- one still signing in to the hub -- was reported unpaired while its pairing was intact. The node now answers null until it knows; the page shows the banner for false only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: Windows installer and desktop app start and stop the node one wayChristophe Besson4 days10-10/+10
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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,client): query TMDB only once a language is chosen, in that languageChristophe Besson6 days1-0/+16
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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>
* feat(hub): default email opt-outs and match Register to Login stylingChristophe Besson6 days3-14/+34
| | | | | | | | | | | | | | - Invitation-by-email and recovery-key-by-email boxes now start unchecked; mailing a code/key is opt-in. The invite choice still remembers itself per account once set. - Align the two invite-email checkboxes with their label (center, not flex-start). - Register (and its verify/recovery/done steps) now sits on the same dark gradient backdrop and frosted card as Login, via a shared AuthShell. - Make the gradient's top-left corner very slightly less bright (#86a3c4 -> #809cbc), on both auth pages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(protocol): bind the MNP token to the node it is for (E10)Christophe Besson6 days4-7/+11
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 Besson6 days1-5/+9
| | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 Besson6 days1-10/+9
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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 Besson6 days1-1/+30
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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>
* fix(hub): require the passphrase to change the e-mail on fileChristophe Besson6 days1-6/+22
| | | | | | | | | | | | | | | | | | | | | | | A member hands its hub access token to every node it connects to (the MNP handshake), so a node operator holds a live bearer token for that member. PATCH /v1/users/me {email} needed only that token, and the confirmation code goes to the new address — so an operator could point the account's e-mail at their own inbox, confirm it, and then use the passphrase-reset path to take the account over. This is the immediate mitigation of that chain; the full fix (a node-audience token distinct from the API session token) follows. Changing the address now requires the passphrase-derived auth_key, verified through the same throttle as a passphrase change or an account deletion — the hub still never sees the passphrase. A PATCH that does not change the address is unaffected. The profile page prompts for the passphrase and derives auth_key with the existing MeshBayKeys.deriveAuthKey, as the delete and change-password flows already do. test_email_change_requires_passphrase.py: refused without / with a wrong passphrase, proceeds with the right one, and a no-email PATCH still works; red before, green after. test_mail_is_not_a_relay.py updated to pass the auth_key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: invitation links no longer bound to an e-mail addressChristophe Besson6 days12-71/+97
| | | | | | | | 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>
* feat(hub): friendlier welcome page, link previews, robots.txt and faviconChristophe Besson6 days16-162/+438
| | | | | | | | | | | Welcome page: privacy said once, a three-step "how it works", a documentation box, download (green) and legal links under the sign-in form, on a dark gradient backdrop covering the whole page. Link previews: Open Graph tags in the app shell, rendered for identity.id, with the square icon as image. robots.txt, favicon and touch icon served at the origin root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(client): split transport.js into classic scriptsChristophe Besson7 days10-2145/+2214
| | | | | | | | | | | | transport.js keeps the core (connection, reconnect, leases, dispatch). Chat, media, admin, upload and device methods move, cut as text, into transport-*.js scripts that hand a class of their own to extendTransport, which copies each method onto MeshBayTransport.prototype; the codec, roster checks, node pins and the rewrap fan-out move as they were. Both shells load them after transport.js. Every prototype member, class property and top-level function has the same source text as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(client): fetch the media apps, the player, settings and search on first useChristophe Besson7 days4-12/+69
| | | | | | | | | | | | apps.js registers Videos, Music, Photos and every settings pane through lazy.js; the group page does the same for the video player and the settings panel, and the shell for the search page. The first download goes from 49 modules / 386 KB gzip to 37 / 289 KB; opening Music now fetches music-app.js and media-tiles.js and nothing of Videos. test_first_load_is_lean holds the eager graph; test_spa_imports checks that every on-demand load names an export that exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(client): move the connection pool and the media tiles to modulesChristophe Besson7 days7-369/+392
| | | | | | | | | ConnectionPool (with connectToGroup and its limits) leaves search-page.js for connection-pool.js, and LazyTile/MediaThumb leave video-app.js for media-tiles.js, cut as text. The shell and the Music and Photos apps now reach them without importing the search page or the Videos app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(node): move per-app enrichment out of NodeDaemonChristophe Besson7 days2-2/+2
| | | | | | | | 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 operator authority and the admin response out of ↵Christophe Besson7 days1-2/+2
| | | | | | | | | 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 Besson7 days1-1/+1
| | | | | | | | 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 MNP handshake out of webrtc_serverChristophe Besson7 days1-1/+1
| | | | | | | 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>
* refactor(node): move the Videos catalogue handlers out of webrtc_serverChristophe Besson7 days1-5/+5
| | | | | | | | 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 Besson7 days1-1/+1
| | | | | | 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 Besson7 days1-1/+1
| | | | | | | 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>
* fix(hub): Search reaches each group with one offer, and a busy hub is not a ↵0.15Christophe Besson8 days2-67/+246
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | dead node The other half of the 4G failure. The page negotiated every group twice: the sweep opened a connection, read the index and closed it, then the warm-up opened the same group again. The sweep, the warm-up and the tiles each had a concurrency ceiling of their own, and together they went past what the hub admits per account. Whatever the hub refused was then reported as "node unreachable" and remembered as down, which put that group last next time. - Every connection goes through ConnectionPool, which holds the page's one ceiling (six at once, sized for twenty groups on a phone) and keeps what the sweep opened for the tiles. A visit costs one offer per group. A refresh costs none for a connection that answers a four-second ping, and a connection that died while the phone slept is replaced, not waited on. - A connection whose index is being read is held against eviction. With more groups than the pool keeps, it was otherwise the least recently used one. - Negotiations still under way when the page closes close what they get, and a sweep cut short that way remembers nobody as down. - transport.js sends an offer again on 429, 502 or 503, honouring Retry-After, with jittered waits of about twenty seconds at worst. Search counts each retry as progress. A 404, 403 or 504 still fails at once, so a dead node costs no time. The fan-out tests assumed a ceiling of three and were re-measured: four dead groups of twelve now hold nothing back, even on a first visit. The pool and the retry run as shipped code, lifted as text, against a fake clock. Each guard was checked by removing it and seeing its test fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(hub): a redeemed invitation link leaves the owner's listChristophe Besson8 days11-17/+9
| | | | | | | | | | | | | | | | | | | | The list under "Invite by link" answered every ticket the group had ever minted, so a link that somebody had already used sat there saying "used by <name>" for the thirty days of KEEP_REDEEMED — beside the member row it had just produced, and above the links that still wait for somebody, which are the only ones there is anything to do about. The node's own `member list` had never shown them: it selects `used_at IS NULL`. The listing now selects `redeemed_by IS NULL`, and drops the `redeemed` status and the `redeemed_by` field with it. The row itself still lives for KEEP_REDEEMED, which is what lets a reload or a second tab of the invitation page be answered rather than refused; its comment says that now instead of naming a list it is no longer in. The SPA filters too, because the desktop client's copy of this interface can be newer than the hub it is signed into. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(hub): open, create and join invitation links in the interfaceChristophe Besson8 days18-9/+919
| | | | | | | | | | | #/invite takes the link out of the address on load and keeps it in the tab through registration and sign-in; joining is one click, only the ticket goes to the hub, and the code goes only to the node the link names once it has signed its challenge. Members tab gains "Invite by link" (shared e-mail box, pending list, cancel both halves); home page takes a pasted link. Browser probe drives the real app, signed out and in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(hub): invitation-link tickets bound to a verified addressChristophe Besson8 days11-0/+11
| | | | | | | | | | | group_invite_links holds sha256(ticket) and the invitee's address blind index; redeeming grants membership to that account only. Owner-only create/list/cancel (a node token may create, never mail), 20 outstanding per group, optional mail written by the hub itself and capped at 10 per sender per day (mail.invite_link_daily_cap). MESHBAY_DESIGN.md §3.4 now carries the whole link design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: the node signs its handshake challenge (MNP 3.4)Christophe Besson8 days2-3/+48
| | | | | | | | | node_pk in handshake_challenge is now signed over the channel binding and both nonces, so a client can check the node key before a join rather than only at the ack. Both transports; the browser and the QUIC client refuse a wrong signature and treat an absent one as an older node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(hub): make mailing an invitation a remembered choiceChristophe Besson8 days11-17/+62
| | | | | | | | A "Send the invitation by e-mail" box under the Invite member field, checked by default and stored as the invite_email preference. Unchecked, invite-notify is never called and the hub never sees the code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(hub): right-click menu in the Files tabChristophe Besson9 days11-64/+97
| | | | | | | | The toolbar's actions on the row under the pointer, sharing one action list with the toolbar — which keeps showing what does not apply, disabled, while the menu leaves it out. A count only where more than one item is concerned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: ask in the page instead of native confirm/alertChristophe Besson10 days21-30/+152
| | | | | | | | A native confirm() or alert() leaves the desktop client unable to type until the window is refocused. ask.js draws both in the page; the SPA test now bans all three browser dialogs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>