| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Electron 42 / Chromium 148, launched under xvfb. The packaged interface mounts
over `app://` with a secure context, `crypto.subtle` present, Argon2 WASM
loaded, and no console errors. Three statements in the design were wrong, and
only launching it found them.
**A CSP in a `<meta>` tag silently drops `frame-ancestors`.** Chromium says so
in the console. A policy carrying a directive that does nothing is worse than
one without it, so the policy is sent as a header by the protocol handler —
which is also the only thing serving the interface, so one source instead of
two.
**`secure: true` is not what makes the service worker register.** Chromium
refuses a worker on a custom scheme whatever its privileges: "The URL protocol
of the current origin ('app://meshbay') is not supported". The application has
no service worker and needs none — it saves through a native dialog, which is
the better of the two paths. `sw.js` stays in the package because the same files
serve the browser, where it is one of only three ways to write a large file.
What `secure: true` is actually for was measured at the same time: without it
**the whole of `crypto.subtle` is undefined**. The first probe loaded a `data:`
URL and every algorithm failed with TypeError, AES-GCM included — which is why
the probe was rewritten before believing its answer. X25519 and Ed25519 are both
present on Chromium 148, settling the version floor left open as O6.
**The renderer cannot call the hub.** Its origin is `app://meshbay` and CORS
refuses it. The hub has *no CORS middleware at all* — its API is reachable from
no web origin whatever — and that is worth keeping. Widening it for
`app://meshbay` would be worse than it looks: that origin is not a credential,
since any Electron application can claim the same scheme and host name.
So every hub call leaves from the main process, exactly as saving a file does,
and it refuses any origin that is not the hub the user signed in to.
`platform.apiFetch()` is `fetch` in a browser and the bridge in the application,
so no caller has to know which it got. `transport.js` reaches it through a
global because it is a classic script, not a module — the alternative was a
second fetch path, which is how two callers of one hub start disagreeing about
how to reach it.
Verified from inside Electron: the main process gets 200 from
/v1/hub/version, the renderer is refused by CORS, and **a script served by the
hub is refused by the policy** — T3's mitigation demonstrated rather than
asserted.
Build note, written into the README because it will bite the next person:
**Ubuntu 24.04's nodejs 18 cannot install Electron at all** — the download
script `require()`s an ESM module, which Node gained in 22. Node 24 LTS,
checksum-verified against nodejs.org, is what this was built with.
package-lock.json is committed; builds use `npm ci`, not `npm install`.
799 tests pass, e2e.py still passes end to end. The session harness needed a
platform stub: it lifts `hubFetch` out of app.js as text and runs it, so the
adapter is now part of the environment it models.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Stage D, and the honest half of it.
D1 — the seam (done, and verified)
----------------------------------
`static/platform.js`. `HUB` becomes `platform.hubBase()` and the transport is
built with the same base, so one address has one source. In a browser it returns
'' and every path stays relative to the origin that served the page — the
acceptance criterion for this split was "the browser SPA behaves identically",
and it does. `platform.js` joins `_ASSETS`, or a change to it would not move the
content hash and a cached browser would never ask for it.
D2 — the shell (written, never launched)
-----------------------------------------
**There is no npm on this machine. Electron was never installed and
`packages/meshbay-client/` has not been run once.** That is stated here rather
than discovered later.
What is there: a main process serving the packaged interface over a privileged
`app://` scheme (`secure` and `standard` are not cosmetic — without them the
service worker refuses to register and streamed downloads break silently), a
preload exposing an enumerated bridge that never passes a filesystem path, a
window with `sandbox`, `contextIsolation` and no node integration, navigation
away from the package refused, and a CSP where the hub is reachable over
connect-src and is not a script source. The hub address arrives as a process
argument because `platform.hubBase()` runs before anything can await.
`test_desktop_shell.py` pins each of those by reading the source — the treatment
`test_downloads.py` already gives the three browser save paths. It catches a
property being removed and proves nothing about the application running. Two
were checked by breaking them.
The interface is *copied* into the package by `build/sync-ui.js` from the hub's
static directory, and `ui/` is gitignored: a silent fork is the only real way to
end up maintaining the interface twice.
D3 — partial
------------
The bridge, and the part worth having now: safeStorage's backend is reported
rather than assumed. On Linux it falls back to a fixed key when no keyring is
running, silently — someone who believes the OS is holding their keys is told
when it is not. The native key lifecycle belongs with D4 and needs a running
application to mean anything.
D8 — partial, and a real defect found
--------------------------------------
`meshbay-node.spec` installed the SYSTEM template — the one carrying `User=%i` —
into `%{_userunitdir}`. A user unit already runs as its owner and cannot carry
`User=`; systemd refuses the file, so the packaged unit could never have
started. Nothing noticed because nobody had built and installed the RPM.
Two units now: the template to `%{_unitdir}`, and a new `meshbay-node-user.service`
that a person enables themselves without a password — which is what lets the
desktop client install a node without asking for one. It carries ExecReload, so
`meshbay-node reload` does not have to stop a service somebody is streaming from,
and documents the drop-in for a drive outside the home, RequiresMountsFor
included.
798 tests pass; e2e.py still passes end to end. Nothing here was built or
launched: no npm, no rpmbuild.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Stage C. Identity keys are per node, so a browser and a desktop client are two
keys on one account there — and the node refused the second where it accepted
the first. Without this, an account created natively could never be opened in a
browser without an operator code per node, and "a native client must not prevent
web use" would have been dead on arrival.
Device linking (node)
---------------------
`identities` is keyed by `(user_id, pk_ed25519)` instead of `user_id` alone. The
old shape did `INSERT OR REPLACE`, so a second device overwrote the first
silently; SQLite cannot change a primary key in place, so the table is rebuilt.
Existing pins are carried over — verified against a live roster with 10 of them,
nobody re-pairs.
A new device files a request bound by `sha256(code ‖ its own keys)`, and a key
the node **already pinned** countersigns it. The hub cannot: it has stored no
user keys since 2026-08-14, which is what makes this safe to do without an
operator in the loop.
**The code never reaches the node.** It lists this account's pending requests
with their stored hashes; the approver recomputes and keeps the match. A node
offering fabricated keys would have to produce a hash over a code it has never
seen. Nothing rests on a human comparing digits — that ritual was dropped in
12.1 as "correct, unusable as the default" and must not return by the back door.
The design document had the approver look a request up *by* its hash, which is
circular: computing it needs the keys being asked about. Corrected in both.
Revocation marks rather than deletes, because a deleted row is a key the node
would happily pin again — which is the laptop somebody just reported lost. Your
last device cannot be revoked: coming back would need an operator's code.
Hub — the only change in the whole plan
---------------------------------------
`POST /v1/users/auth` signs in with a device Ed25519 key, on the same pattern as
`/v1/nodes/auth`, plus `/v1/users/devices` to register, list and retire. New
`user_devices` table with an Alembic migration, because `create_all()` is not
one.
This is **not** the key directory that was H3, and the tests say so: nothing
reads it but the hub, no group key is ever wrapped for one, and it is a
different key from the per-node identities. What it does cost is metadata — the
hub now knows how many devices an account has and when each last signed in.
Also `client.minimum` / `client.recommended` in `GET /v1/hub/version`: an
installed client meets a newer hub the day the interface ships in a package, and
that is cheap now and awkward to retrofit.
Browser
-------
The `key_changed` refusal becomes `unknown_device` and offers a linking code
instead of telling someone to find their operator. The Members panel lists this
account's devices here, approves one by code, and retires one.
773 tests pass. `e2e.py` gained a step that links a device end to end against
the live deployment — file, list, recompute, countersign, then open the group
with the new keys and no code — and it also gained `recv_type`, because a step
that assumes the next message is its own answer reads an ack left by the step
before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
A design discussion on 2026-08-17 settled Phase 13 and, in doing so, changed
four things the spec states. v6 restates only those; v5 stays authoritative for
everything it does not touch, per the convention v5 itself used with v4.
What changed:
* The native shell is **Electron**, not pywebview — structural decision 18
reversed. The SPA depends on Chromium-class APIs (WebRTC, WebCrypto
X25519/Ed25519, MSE, Service Workers), so keeping Chromium keeps
transport.js, crypto.js, keyderive.js, downloads.js and sw.js *as the
client*. A system webview meant reimplementing ~2500-3000 lines. The old
"69 % reused" figure was measured against an app.js of ~2600 lines; it is
4586.
* A group's content is **several named roots**, not one directory, because
the planned video and audio libraries will not live in one folder on one
disk.
* **Device linking**: one person may hold several devices on a node,
admitted by a key the node already pinned and bound by a one-time code the
new device generates. Without it a native client is refused where a browser
is not, and an account created natively could never be opened in a browser.
* **Authorship is authenticated, not asserted** — chat senders sign, uploads
have a provable owner, and delete authorization moves from the uploading
key to the account.
And one rule v5 assumed without writing down: **group-related server state
lives on the node.** Verified for multi-root — SwarmSource carries hashes and
endpoints, no paths.
Also here: the Caddy configuration, which was a snippet in the roadmap that
would have broken the SPA (it predates /a/<hash>/ asset versioning and would
have 404ed /sw.js, silently killing streamed downloads on Firefox and Safari);
and downloads.html, which becomes a security page once a release key exists.
Phase 15 was re-read against device linking and is wrong as written: a sender
key must be per **device**, never per person, or two devices sharing a chain
produce key and nonce reuse — C1 again, one level down. senderkeys.py already
fails this silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|