summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-17 17:38:14 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-17 17:38:14 +0200
commitf23e8e51e4b8bc03d6109081fe048af1782e1f4e (patch)
tree29772398ef6cd488d50b032358b468eece0f2add
parent6562665c80f96f30fef95af83a0abcf71f41795f (diff)
downloadmeshbay-0.5.tar.gz
docs: sessions renew themselves, and two faults of the same shape0.5
USERGUIDE said an hour in five places and presented renewal as something the reader does with curl. Both are now wrong: it is four hours, the web app renews for itself, and the endpoint rotates — so anyone driving it by hand has to store the refresh token that comes back, or their next call revokes the family. Also corrects what the token's life actually bounds. It is not how long a revocation takes: the hub reloads the account on every request and refuses a suspended one at once, and it pushes signed revocations to nodes. What remains is a leaked token on an account still in good standing, which is the reason to keep the number small. Two lessons in CLAUDE.md. A rotated refresh token has to be stored or it is spent once. And an effect keyed on a value that used to be constant: the WebRTC dial listed `token` among its dependencies, harmless while a token only ever expired, fatal once the session renewed itself — it tore the connection down mid-handshake and the node waited for ever. That and the hook declared after its own dependency are the same shape, and worth naming as one: code that reads correctly on its own and is wrong against the component lifecycle.
-rw-r--r--CLAUDE.md26
-rw-r--r--docs/USERGUIDE.md37
2 files changed, 54 insertions, 9 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 8e2eafa..76a7aa6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -302,6 +302,30 @@ keypair bundle, or anything that looks like a user's public key.
reproduced the defect in one run. Model the environment, never the code under
test
+- **A refresh token that rotates must be stored, or it is spent once.** The hub
+ revokes the refresh token presented, returns a replacement, and treats a
+ revoked one presented again as theft — revoking the whole family. The SPA kept
+ only the access token out of that response, so renewal worked once and the
+ second attempt destroyed the session, which is why signing out and in was the
+ only cure. Nothing used the path at all: `hubFetch` reported 401 like any
+ other error, and watching a film is an hour in which the hub hears nothing,
+ because the video is WebRTC. Renew on a margin, on returning to the tab, and
+ on a 401 with a replay; coalesce concurrent renewals, or the second presents
+ what the first just spent and looks exactly like theft.
+ `tests/harness/session_harness.mjs` runs it against a hub that enforces
+ rotation — a lax stub would pass the broken client
+
+- **An effect keyed on a value that used to be constant.** The WebRTC dial
+ listed `token` among its dependencies. Harmless while a token only ever
+ expired; once the session renewed itself the string rotated, and the effect
+ tore the connection down and rebuilt it — worst at mount, where a stale token
+ is renewed exactly while ICE is negotiating, so the browser abandoned the
+ handshake and the node sat in `connecting` for ever. Depend on whether there
+ is a token, not which one, and read the live one where it is used. Before
+ making something vary that never varied before, grep the dependency arrays it
+ appears in — this and the hook-ordering fault above are the same shape: code
+ that is correct read on its own and wrong against the component lifecycle
+
- **A stylesheet does not tell you where anything lands.** The responsive tests
pinned numbers out of `style.css` and said in their own docstring that a
layout could not be measured because there was no browser in the suite. There
@@ -509,6 +533,8 @@ SFR residential Fedora 44 → meshbay.org OVH VPS:
| Seeking (node) | `webrtc_server.py` | `start` on `stream_req`; `-ss` **before** `-i` (index seek, not decode-and-discard), clamped away from the end, echoed in `stream_init` |
| Resume position | `static/app.js` | `readResumePosition` / `writeResumePosition` — localStorage, per file, per browser. No protocol, and nothing new learns what you watch |
| Layout, measured | `tests/harness/layout_probe.py` | Renders `style.css` in Chrome at any width and returns bounding boxes. Use it for layout, not `test_layout_responsive.py`, which only pins CSS values |
+| Session renewal (browser) | `static/app.js` | `refreshAccessToken` / `ensureFreshToken` — one writer (`setAuth`), one in-flight renewal, rotated refresh token stored. `hubFetch` renews on 401 and replays |
+| Token lifetimes (hub) | `meshbay_hub.config` | `[jwt] access_token_ttl` 4 h, `refresh_token_ttl` 30 days. **Production sets both in `~/.config/meshbay/hub.toml`** — changing the code default alone does nothing there |
## meshbay.org server (état cible)
diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md
index abfc404..7bd695b 100644
--- a/docs/USERGUIDE.md
+++ b/docs/USERGUIDE.md
@@ -98,10 +98,10 @@ POST /v1/users/register
POST /v1/users/login
{"username": "yourname", "password": "yourpassword"}
→ {
- "access_token": "JWT (Ed25519, 1 hour validity)",
+ "access_token": "JWT (Ed25519, 4 hour validity)",
"refresh_token": "opaque 256-bit token (30 days)",
"token_type": "bearer",
- "expires_in": 3600,
+ "expires_in": 14400,
}
```
@@ -117,12 +117,20 @@ curl -s -X POST https://meshbay.org/v1/users/login \
### Token refresh
-Access tokens are valid for 1 hour. When one expires, use the refresh token to get a new one without re-entering your password:
+Access tokens are valid for 4 hours. **The web app does this for itself** — it
+renews ten minutes before expiry, on returning to the tab, and on any 401, then
+replays the request. Nobody should meet an expired token in the browser; what
+follows is for other clients.
+
+The endpoint **rotates**: it revokes the refresh token you present and returns a
+new one, so store the replacement. Presenting a revoked token is treated as
+theft and revokes the whole family, which is a full sign-out.
```
POST /v1/users/token/refresh
{"refresh_token": "your-refresh-token"}
-→ {"access_token": "new JWT", "token_type": "bearer", "expires_in": 3600}
+→ {"access_token": "new JWT", "refresh_token": "USE THIS NEXT TIME",
+ "token_type": "bearer", "expires_in": 14400}
```
```bash
@@ -131,7 +139,13 @@ curl -s -X POST https://meshbay.org/v1/users/token/refresh \
-d '{"refresh_token":"YOUR_REFRESH_TOKEN"}'
```
-Refresh tokens are valid for 30 days and can be immediately invalidated by the hub on account compromise. Revoking the refresh token means the next access token renewal will fail; any active access token expires within 1 hour at most.
+Refresh tokens are valid for 30 days — that is the session — and can be
+invalidated by the hub at once on account compromise. Revoking one means the
+next renewal fails; an access token already issued keeps working for up to 4
+hours. That window is not the whole story: the hub reloads the account on every
+request and refuses a suspended one immediately, and it pushes signed
+revocations to nodes, so suspending an account or revoking a membership takes
+effect at once regardless of the token's remaining life.
### Access token structure
@@ -146,7 +160,7 @@ The JWT payload contains:
| `groups` | The `group_id`s you are a member of, for node-side authorization |
| `scope` | `user` for a browser, `node` for a daemon |
| `iat` | Issued at (Unix timestamp) |
-| `exp` | Expires at (Unix timestamp, 1 hour from issue) |
+| `exp` | Expires at (Unix timestamp, 4 hours from issue — `[jwt] access_token_ttl`) |
The token carries **no public key of yours**. It used to carry `pk_user`, and a node
recorded that key as the uploader of a file — which meant the party issuing tokens
@@ -166,7 +180,7 @@ What deletion does:
- Releases the username — someone else may register it afterwards
- Clears the email and password hash, and drops the node linking key
- Removes group memberships, notifications and refresh tokens
-- Refuses any access token still in its hour of validity, immediately
+- Refuses any access token still within its validity, immediately
What deletion does **not** do:
@@ -867,7 +881,7 @@ Two layers:
- `jti` (UUID4) is mandatory in every token — prevents replay (Ed25519 signing is deterministic; without `jti`, two tokens issued in the same second are byte-for-byte identical) and enables individual revocation
- Nodes verify JWTs offline using the hub's cached public key — no hub roundtrip, no hub downtime dependency
-- Revocation: hub invalidates refresh token → next access token renewal fails → node access expires within 1 hour. For immediate revocation: hub adds `jti` to a denylist that nodes periodically fetch
+- Revocation: hub invalidates refresh token → next access token renewal fails → node access expires within the access token's life. For immediate revocation: hub adds `jti` to a denylist that nodes periodically fetch
### What node compromise exposes
@@ -961,7 +975,12 @@ These logs are not used for any purpose other than responding to legal requests.
**Symptom:** node returns 401, error says "expired" or "Token signature expired".
-**Fix:** your access token is over 1 hour old. Refresh it:
+In the web app this should not happen: it renews before expiry and retries once
+on a 401. If you see it there, the renewal path itself is broken — check the
+browser console rather than the token.
+
+**Fix (other clients):** your access token is over 4 hours old. Refresh it, and
+**keep the refresh token that comes back** — the one you sent is now revoked:
```bash
curl -s -X POST https://meshbay.org/v1/users/token/refresh \