<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/config.py, branch 0.11</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.11</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.11'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-09-02T09:54:22Z</updated>
<entry>
<title>fix(hub): a desktop solve reports no hostname at all, not "meshbay"</title>
<updated>2026-09-02T09:54:22Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-02T09:54:22Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=2d8c6bc449e343a80569e45d4ceae0423616cedd'/>
<id>urn:sha1:2d8c6bc449e343a80569e45d4ceae0423616cedd</id>
<content type='text'>
Registering from the native client failed with `captcha_failed` while the
checkbox was green — a worse symptom than the one being fixed, because the
widget now looked fine and only the hub's own log said otherwise:

    captcha solved on an unexpected host ''; allowed: ['localhost', 'meshbay', 'meshbay.org']

The previous commit assumed Google would report the host component of the
origin, so `app://meshbay` would come back as `meshbay` and could sit in
`allowed_hosts`. It does not. A solve Google cannot attribute to a domain
reports an **empty** hostname, and no allowlist entry can match that. An empty
entry is not the answer either: a blank in a TOML list is a typo far more
often than an intention, and `load_config` drops blanks for that reason —
`captcha.allow_unattributed_host` is a named flag instead, so the trade is
stated where it is made.

What it admits, plainly: every non-web client, not only ours. A file:// page
or somebody else's Electron application look identical from here. That is the
same bar the client's own origin would have been — main.js already records
that `app://meshbay` is not a credential — and it is a bar: the captcha still
has to be solved, per token, in something that can render it. What is given up
is the origin restriction for non-web clients, not the captcha. Off by
default, and a hub without the desktop client should leave it off.

The refusal now names which of the two it is, since they need different
answers: an unexpected host names the host, an unattributed one says to set
the flag.

docs/captcha.md §6 said `meshbay` was the value and told operators to add it;
it now records what was measured and why the guess was wrong. The packaged
example config carries the flag with the same warning.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_014UtzVrzM7e2tG9fSpkR9ML
</content>
</entry>
<entry>
<title>fix(hub): check the captcha's origin here, so the desktop client can pass one</title>
<updated>2026-09-02T08:35:17Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-02T08:35:17Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=9c1b622611bb0b21852d3c9c11e1d8674aa35654'/>
<id>urn:sha1:9c1b622611bb0b21852d3c9c11e1d8674aa35654</id>
<content type='text'>
Reported from the native client: the reCAPTCHA box renders
"ERROR for site owner: Invalid domain for site key". The web browser is fine.

It is not a client restriction, and the CSP was never what refused — the
script loads, which is why the widget appears at all to say so. reCAPTCHA
validates the hostname of the page the widget is rendered in against the
domain list on the site key, and the desktop client's interface ships inside
the package and is served from `app://meshbay` (main.js: `win.loadURL`). Not a
preference: file:// breaks ES modules and IndexedDB, and the hub must never
become the document origin. So the hostname Google sees is `meshbay`, it is
not on the key's list, and it never can be — the check runs on Google's
servers and nothing client-side reaches it.

The fix turns that check off on the key and does it on the hub instead:

    [captcha]
    allowed_hosts = ["meshbay.org", "localhost", "meshbay"]

`verify_captcha` refuses a solve whose hostname is not in the list. The
hostname comes from `siteverify` — what Google observed, not what the caller
asserts — so it is a real check against what turning the console setting off
opens, which is a bot rendering the public site key on a page of its own.
Empty (the default) skips it, so an existing hub upgrades unchanged with
reCAPTCHA still doing the origin check. The two settings go together, and
docs/captcha.md §6 says so.

The `meshbay` entry is the weak one and the doc says that too: any Electron
application can claim the same scheme and host, as main.js already records.
What it still costs is a captcha solve per token inside a real Chromium
instead of a token farmed from any web page.

docs/captcha.md §6 replaced. It documented a design that was superseded twice
— an `auth_key`-keyed carve-out that turned out to disable the gate for
everyone, and "works in the Electron client too, both run Chromium", which is
the assumption this bug is made of: reCAPTCHA validates the domain, not the
rendering engine.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_014UtzVrzM7e2tG9fSpkR9ML
</content>
</entry>
<entry>
<title>feat(hub): reCAPTCHA v2 on Register and Password Reset pages</title>
<updated>2026-09-01T09:06:47Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T09:06:47Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b6c15f35d570d4f54901b811654991847502ca82'/>
<id>urn:sha1:b6c15f35d570d4f54901b811654991847502ca82</id>
<content type='text'>
Server-side verification module, CaptchaConfig in hub.toml,
captcha_site_key exposed via /v1/hub/info, useCaptcha() hook
in the SPA with stable DOM rendering (strength bar always present
to avoid Preact re-ordering the captcha widget). Native clients
(auth_key path) skip captcha. All 10 locales updated.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(hub): a session that renews itself</title>
<updated>2026-08-17T15:38:00Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-17T15:38:00Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=6562665c80f96f30fef95af83a0abcf71f41795f'/>
<id>urn:sha1:6562665c80f96f30fef95af83a0abcf71f41795f</id>
<content type='text'>
Reported: after an hour of watching a film, every action answers "token expired
or invalid", with signing out and back in as the only way on. Reopening the tab
the next day did the same.

The access token lasts an hour and the refresh token thirty days, and nothing
used the second one. `hubFetch` reported a 401 like any other error, and
watching a film is precisely an hour in which the hub hears nothing at all,
because the video travels over WebRTC. So the token aged out with no request to
notice, and a tab reopened the next morning presented a stale token with a
perfectly good refresh token sitting beside it in localStorage.

Underneath was the reason it could not be recovered from. The hub *rotates*:
the refresh endpoint revokes the token presented, returns a replacement, and
treats a revoked one presented again as theft, revoking the whole family. The
client kept only the access token out of that response. So the refresh token was
spent on first use and the second attempt did not merely fail — it destroyed the
family. Which is exactly the reported symptom.

Renewal now happens on a margin, on returning to the tab, on mount, and on a 401
with the request replayed. Concurrent renewals share one request: two 401s
racing would otherwise present the same refresh token twice, and the hub cannot
tell that from theft, so the remedy would have been worse than the fault. A
refusal signs out cleanly rather than leaving a session that fails every call
while looking signed in.

The lifetime goes to four hours, which is not what makes long sessions work —
renewal is — but is what someone has to notice by if renewal itself breaks. An
hour was less than a feature film. Twelve was considered and declined: it widens
the window in which a leaked token cannot be turned off, and it lets the renewal
path go a whole day between uses, which is how it came to be broken here without
anyone noticing. Production sets this in its own hub.toml, so both moved.

The tests run the shipped code against a hub that enforces rotation, because a
stub that accepted the same refresh token twice would have passed against the
broken client. Checked that dropping the rotated token reproduces the revoked
family, so the guard is guarding something.

Also widens the orphan-setter rule to ignore `setX` functions declared in the
module: `setAuth` is not a hook setter, and a rule that cries wolf is one
somebody eventually silences. Verified it still catches a real orphan.
</content>
</entry>
<entry>
<title>feat(hub): Phase 8 — Hub v2 security hardening + production readiness</title>
<updated>2026-08-10T01:57:55Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-10T01:57:55Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=1a53eb4cc404ec94658fde0ae04cfe2ccf1810dc'/>
<id>urn:sha1:1a53eb4cc404ec94658fde0ae04cfe2ccf1810dc</id>
<content type='text'>
8.1  Config-based admin authz (require_admin on all admin endpoints)
8.2  Email encrypted at rest (AES-256-GCM, HKDF from hub Ed25519 key)
8.3  Refresh token rotation with family-based reuse detection
8.4  Federation persistence (HubPeer model replaces in-memory dict)
8.5  Federation token verification now async (DB-backed)
8.6  CSAM hash check wired into swarm registration flow
8.7  Rate limiting on auth endpoints (5/10/20 per minute)
8.8  Healthcheck endpoint (GET /v1/health, no auth)
8.9  IP log cleanup background task (365-day retention)
8.10 Argon2id params bumped to 256 MB (pw_version, rehash on login)

Deployed to meshbay.org — schema migrated, existing emails encrypted.
117 tests pass (29 hub, 88 common+node).

Resolves security review items S1, S2, S5.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(hub): add production hub — config, auth, API routers, tests</title>
<updated>2026-08-09T02:39:34Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-09T02:39:34Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=fb91c4545c757711e1b5fd354ca4b311c89fd2c0'/>
<id>urn:sha1:fb91c4545c757711e1b5fd354ca4b311c89fd2c0</id>
<content type='text'>
config.py: TOML + env var priority. auth.py: Argon2id passwords,
JWT EdDSA with jti, refresh token hashed (blake3). Routers:
hub (info/pubkey), users (register/login/refresh/pubkeys),
nodes (announce/get), groups (create/gek-bundle/gek-retrieve).
Rate limiting via slowapi. app.py factory with lifespan.

All 40 tests pass (SQLite in-memory, no PostgreSQL required).
Fix: remove tests/__init__.py to resolve namespace conflicts.

Co-Authored-By: Claude Sonnet 4.6 (1M context) &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
