| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
| |
pipelinedDownload kept every chunk's resolved promise until the end; a 2 GB
download held 2 GB in the page. Each is released once read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
Keystore-wrapped store, a Kotlin port of keyring.js and transcripts.js held
to the shared vectors, the same keys/device/secrets bridge as the desktop,
and a native confirmation before browser access is widened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
Gated on capabilities.nodeAdmin rather than on any bridge, so a phone with no
groups sees its invitations and the join link.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
WebView over the packaged UI (copied from hub/static at build time), the
desktop CSP as a header, a bridge answering our top-level document only,
hub calls from native to the signed-in hub. Keys stay in the page for now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
One file every bundle/transcript implementation must reproduce, generated
from the desktop keyring; checked against it and against the specification.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
Removes confirmFolder (addRoot, attachGroup) and the writable confirmation
added in e4f6177, with their two catalogue keys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
root_add, root_update and group_attach leave MNP: adding a directory and
switching writable/removable go through the loopback API (native dialog in
the desktop app) or the CLI. The operator's Settings tab still lists the
roots from any browser, read-only. The desktop app refuses to sign those
ops; a loopback flag change now reaches open pages (publish_roots).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
The scan still runs six seconds, but the picker polls what it has found
and shows each receiver immediately. A rescan no longer has its timer
cut short by the scan it replaced.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
Rotation narrows rather than widens: members still connected receive the
new key, and nothing already shared changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
Registration refuses a name that differs from an existing one only by case;
accounts that already do keep their names, and a pending retry needs the exact
name (F-26).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
An offer's IP-log row (kept a year) was written before any check, for any
string named as a node; it is written once the offer goes to a node. The ICE
list is capped (64 candidates, 32 KiB). A node's update_groups, a database read
each, is budgeted like chat_notify and claims at most 1000 groups (F-22).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
A cell starting with = + - @ (or a tab or carriage return) gets a leading
apostrophe; the export carries text members chose (F-29).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Transitional. The Argon2 run that makes M makes A, the key MBK2 bundles were
sealed under; a session keeps it as a decrypt-only key (IndexedDB in a browser,
the key storage in the desktop app). A client meeting an MBK2 bundle opens it —
or its recovery copy — and stores the same identity as MBK3 once connected; the
desktop app reseals or withdraws it as browser access says. A session without
A asks for the passphrase once. Older formats stay refused by name. Replaces
the unpin-and-reinvite step the 0.17 flag day required on every node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
The desktop app writes the Mark-of-the-Web on each file it saves on Windows, as
a browser does. Bidirectional controls are reserved characters in a saved name
(portable-name.js and paths.sanitize_for_download, and again in the main
process), so a name cannot display one extension and carry another. The node
refuses uploads of files Windows Explorer acts on by itself: desktop.ini,
.lnk, .url, .scf, .library-ms, .searchConnector-ms (F-19).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
A sign-in from a browser that presented no token is answered with one
(known_browser, kept hashed, twenty per account); a later sign-in presenting it
counts failures on its own row, which nobody else can spend. Passphrase checks
inside an open session (change, e-mail, deletion, device, pepper) count on the
account's own row, so a locked name no longer stops its owner there either; /me
reports that row. Reset and erasure forget the browsers (F-15).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
Over 128 characters was a 500 on PostgreSQL; line breaks, C0/C1 controls and
bidi overrides are refused (joiners stay, for emoji). The creation form caps
the field at 128 (F-13, what remains of it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
| |
Sealed at rest and bound to the account; returned by sign-in, device sign-in,
a passphrase change and GET /me/bundle-pepper, never by a refresh, to a node
token, in a token or in a log. Erasure clears it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
| |
connect-src drops https: and wss: in both policies. Checked against Google's
reCAPTCHA test keys: in Chrome widget, token and registration unchanged; in
Firefox the widget loads; no violation reported in either.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
| |
Each name in admin_usernames is pinned to the first active account seen
holding it (admin_pins), so a name freed by a deletion grants nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
cleanup
- node: only a wrong code counts towards the join lock, now per account
(5) as well as node-wide (20), and it is consulted only when a code is
tried. Every member reconnecting gets the group key through join_request,
so a lock checked before recognition let one member refuse it to everyone.
- node: link previews read the body as a stream and stop at the cap,
counted on decoded bytes; a declared oversized image is not read; 15 s
total deadline; image decoding off the loop. `client.get` had buffered
the whole (decompressed) response before the caps looked at it.
- hub: the daily purge of never-verified accounts detaches their IP-log
rows (keeping the name) and clears every other reference first, and each
cleanup step runs on its own. On PostgreSQL the bare DELETE violated the
ip_logs foreign key and stopped every purge behind it for good.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
A descriptive <title>, a two-line meta description apart from the
short one messengers get, "/" canonical for every shell path, and a
<noscript> pitch with the download and doc links for crawlers that
do not run app.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
| |
Every /v1/relays route answered 503 and nothing called them; no TURN
relay is needed. The proof-of-possession rule it carried stays as AV6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
| |
A node hosting a public group syncs the hub's blocklist on every
connection (paged, node token only) and applies pushed changes. A
blocked file leaves the index and is refused (content_blocked); private
groups are untouched. The unused per-hash check route is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
Nodes registered the hashes of their public groups on the hub and nothing
ever read them back. Routes, model and node registration removed; a
migration drops swarm_sources. No node sends the hub a content hash now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
| |
Behind Caddy every request's TCP peer is loopback, and the limiter was keyed
on that peer (slowapi's get_remote_address), so each limit was one bucket for
the whole internet: ten node sign-ins a minute shared by every node. A node
starting while others signed in got 429 and sat in waiting_for_hub, which the
desktop app took for no node at all. Key it on client_ip, which already
resolves X-Forwarded-For from a trusted proxy and was written for this.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
- 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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
POST /v1/nodes/{id}/incoming checked only the caller's own address, then revealed
whether the node was connected (404 vs 504) and, with QUIC on, made it punch —
so any authenticated account could poll it for a node's liveness or make a
stranger's node emit a UDP probe. The membership gate webrtc_offer did inline is
now require_shared_active_group() in api/signaling.py, called by both routes; in
notify_incoming it runs before anything depends on the node's connection state,
so a non-member gets one uniform 403 whether the node is up or not.
test_incoming_membership.py holds it (a non-member is refused with a membership
403 whether the node is connected or not; a member passes the gate); red before,
green after. The offer relay is unchanged in behaviour (it now calls the shared
helper); signaling/availability suites pass. Design §7.2 updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|