diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-25 17:43:28 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-25 17:43:28 +0200 |
| commit | 3f3c67a4aff7b800c271e88e2bc5e5294b010fb9 (patch) | |
| tree | 7280157a5921605b95fb8d1640f4b656067c8136 | |
| parent | bce962c39fcb2d124506e33f77c3ca9082f145dd (diff) | |
| download | meshbay-3f3c67a4aff7b800c271e88e2bc5e5294b010fb9.tar.gz | |
feat(protocol): MNP 4.0 flag day for the node-audience token (B2)
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>
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 31 | ||||
| -rw-r--r-- | docs/MESHBAY_NODE_PROTOCOL.md | 3 | ||||
| -rw-r--r-- | packages/meshbay-common/src/meshbay_common/__init__.py | 10 | ||||
| -rw-r--r-- | packages/meshbay-common/src/meshbay_common/handshake.py | 6 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/hub.py | 9 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/transport.js | 19 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/golden/dispatch.json | 112 |
7 files changed, 118 insertions, 72 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index c6d3cc7..394dfb5 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -16,7 +16,7 @@ > them — it names the invariant that holds today, not the incident that produced > it. §13 is the register of those labels. > -> Wire versions at the time of writing: **MNP 3.4** (oldest peer accepted 3.0), +> Wire versions at the time of writing: **MNP 4.0** (oldest peer accepted 4.0), > **MHP 0.1**, packages **0.16.0**. The normative source for the wire format is > `MESHBAY_NODE_PROTOCOL.md`; this document states the design the protocol > serves, not its byte layout. @@ -992,12 +992,29 @@ already fails the key proof. Pinning covers the case where an attacker *holds* t group key — an ex-member, a leaked key — and swaps the node underneath, which the proof alone cannot distinguish from the genuine node. +**The token the member presents is a node-audience token, never the hub session +token (E10).** A member hands whatever it presents here to the node operator, +who is in the threat model, so the credential must open nothing at the hub. The +hub signs two audiences with its one key: a session token (`aud` = the hub API) +for `hubFetch` and signaling, and a short-lived **MNP token** (`aud = MNP_AUD`, +from `POST /v1/nodes/mnp-token`) that carries the member's `sub`, `groups` and +`jti` and is the only thing presented in the handshake. The node binds `MNP_AUD` +when it decodes, so a session token is refused here; the hub API binds its own +audience, so an MNP token captured by an operator is refused there. It is +checked once, before the proof, so its short life never interrupts a transfer or +a film already playing — a reconnect fetches a fresh one. `meshbay_common/tokens.py` +holds the two audience strings, shared by the hub that issues and the node that +verifies. + **Authorization rules:** - `group_id` is **mandatory**. Omitting it once skipped the membership check and fell back to the node's first group (**M1**). - `scope == "user"` is enforced by default; node-scoped daemon tokens are refused on the client path (**M9**, **NS7**). +- **The audience must be `MNP_AUD`.** A hub session token, or one with no + audience, is refused — a member's credential to a node is not a credential to + the hub (**E10**). - The denylist is consulted for user, `jti` **and** group. - The node **refuses connections when it holds no group key** — there is no `gek_required: false` bypass (**NS8**). @@ -1229,6 +1246,16 @@ client on reload; the desktop client ships its own UI, which is why connecting and says "this version can no longer connect" rather than showing a handshake refusal nobody can act on. +**MNP 4.0 (2026-09-25) is the worked example of a MAJOR that had to be one.** A +member now presents a node-audience token in the handshake, not its hub session +token (§5.2, **E10**), so a pre-4.0 client — which presents the session token — +cannot authenticate to a 4.0 node, and a 4.0 client cannot authenticate to a +pre-4.0 node, which expected the session token. Leaving a branch that still +accepted the session token would keep the very disclosure the change closes +reachable on every node, which is exactly what "no compatibility switch" forbids. +So the floor moved to 4.0, `client.minimum` moved to the release that carries the +new client, and the hub, node and SPA deploy together. + **Every *requirement* is true of every peer the client can reach.** The floor moves with each MAJOR, so `check_version` refuses at the handshake any peer that cannot meet one: an upload is sealed or it is not sent; a transfer has a real lease or it @@ -3210,6 +3237,7 @@ had already been asked. | **E7** | **Several named roots** per group, forming one virtual root (§6.2) | | **E8** | **exFAT/NTFS and Windows are the common case.** Linux ships first; that is build order, not population (§10) | | **E9** | **Group-related server state lives on the node. Always** (§1.3) | +| **E10** | **The credential a member presents to a node is not the one the hub API accepts.** A member hands its handshake token to the node operator, who is in the threat model, so it is a short-lived node-audience token (`aud = MNP_AUD`, §5.2), useless at the hub API; the session token (`aud` = hub API) is never disclosed to a node. Before this the member presented its hub session token, so an operator held a live hub credential for the member — enough to enumerate their other groups and act as them at the hub. Closed by the audience split and the MNP 4.0 flag day (§5.6) | | **O1** | Initial key setup in the pre-proof window — deferred; that window is where C4 and C5b came from | | **O2** | A LAN enrolment door — one endpoint, bounded window, one-time code, closing permanently on success | | **O3** | `device_policy {allow_bundle: false}`, signed by a pinned key — **the mechanism that actually closes C4** (§3.7) | @@ -3251,6 +3279,7 @@ had already been asked. 20. **Private keys never leave the device on native clients.** Qualified: a browser has no durable storage of its own and still needs a bundle on each node, so C4 closes for an *account* only when it opts out of browser use. 21. **Hub minimisation is enforced by an acceptance test, not by policy.** The hub must be *unable* to see keys, content or file listings. 22. **No new code exchanges between people.** Safety numbers are refused for identity verification, permanently. The device-linking code is between a person's own devices and is unaffected. The total user-visible cost of the whole authorship story is **one notice**: *"this account's key changed"*. +23. **A member's credential to a node is audience-separated from its hub credential** (**E10**, §5.2). The handshake carries a short-lived `aud = MNP_AUD` token, never the hub session token; the hub API accepts only its own audience. A node operator therefore holds nothing that acts at the hub. ### 14.2 Client architecture diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md index 3351793..b3cd6eb 100644 --- a/docs/MESHBAY_NODE_PROTOCOL.md +++ b/docs/MESHBAY_NODE_PROTOCOL.md @@ -1,6 +1,6 @@ # MeshBay Node Protocol (MNP) -**Wire version:** `3.4` — `meshbay_common/__init__.py` (`MNP_VERSION`) +**Wire version:** `4.0` — `meshbay_common/__init__.py` (`MNP_VERSION`) **Oldest peer accepted:** `3.0` — `handshake.py` (`MNP_MIN_SUPPORTED`) **Normative implementation:** `meshbay-common` (`protocol.py`, `handshake.py`, `groupbox.py`, `chatbox.py`, `adminop.py`, `join.py`, `device.py`, `crypto.py`, @@ -462,6 +462,7 @@ absent. `verify_proof` compares with `hmac.compare_digest`. | Rule | Refusal | Rationale | |---|---|---| | JWT verifies under the hub's Ed25519 public key (`EdDSA`) | `Invalid JWT: ...` | | +| `aud == MNP_AUD`, and `exp`/`sub`/`scope` present (MNP 4.0) | `Invalid JWT: ...` | the member presents a short-lived **node-audience** token (`POST /v1/nodes/mnp-token`), not its hub session token — the operator holds whatever is presented, and the session token opens the hub API. The two audience strings are in `meshbay_common/tokens.py` | | `scope == "user"` | `Wrong token scope` | a node-scoped daemon token must not be usable as a client token | | `sub` non-empty | `Token has no subject` | | | `group_id` non-empty | `group_id is required` | an absent group means no membership check to make; there is no default group, and a node's first group is not one | diff --git a/packages/meshbay-common/src/meshbay_common/__init__.py b/packages/meshbay-common/src/meshbay_common/__init__.py index 216a18f..842c52d 100644 --- a/packages/meshbay-common/src/meshbay_common/__init__.py +++ b/packages/meshbay-common/src/meshbay_common/__init__.py @@ -241,5 +241,13 @@ __version__ = "0.16.0" # `invite_link_result` and `invite_cancel`, a code bound to no account until it # is redeemed. Additive in the same way — a 3.3 node answers `unknown message # type`, and no client can hold a link code for a node that could not issue one. -MNP_VERSION = "3.4" +# +# 4.0 (2026-09-25) is a MAJOR — a change to what a peer must *present*, not an +# additive message (§5.6). A member now authenticates to a node with a +# short-lived MNP-audience token (aud=MNP_AUD), never its hub session token: the +# node operator holds whatever is presented, and the session token opens the hub +# API. A pre-4.0 client presents the session token and is refused at the +# handshake — there is no compatibility branch, because leaving one would keep +# the disclosure reachable on every node. So the floor moves with it. +MNP_VERSION = "4.0" MHP_VERSION = "0.1" diff --git a/packages/meshbay-common/src/meshbay_common/handshake.py b/packages/meshbay-common/src/meshbay_common/handshake.py index 2393df1..dedfeb1 100644 --- a/packages/meshbay-common/src/meshbay_common/handshake.py +++ b/packages/meshbay-common/src/meshbay_common/handshake.py @@ -85,7 +85,11 @@ CHALLENGE_PREFIX = b"meshbay:mnp:challenge:v1" # has no vocabulary to understand — or serving it outside every cap the operator # set, which makes the caps decoration. Neither is honest, so it is refused # here, with a code and a sentence. -MNP_MIN_SUPPORTED = "3.0" +# 4.0 (2026-09-25): a member authenticates with an MNP-audience token, not the +# hub session token (MNP_VERSION note). A pre-4.0 peer presents the session +# token, which this node now refuses — so the floor moves to 4.0 rather than +# leaving a branch that would keep a hub credential reachable by every node. +MNP_MIN_SUPPORTED = "4.0" ROLE_CLIENT = "client" ROLE_NODE = "node" diff --git a/packages/meshbay-hub/src/meshbay_hub/api/hub.py b/packages/meshbay-hub/src/meshbay_hub/api/hub.py index cab60c8..efebc4d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/hub.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/hub.py @@ -78,8 +78,13 @@ async def hub_pubkey(): # handshake refusal anyway. The operator is updating every client, node and hub # by hand for this flag day, which is what makes that acceptable exactly once. # The gate is in place for the next one, where it will work as intended. -MIN_CLIENT_VERSION = "0.13.0" -RECOMMENDED_CLIENT_VERSION = "0.13.0" +# +# 0.16.0 is the MNP 4.0 flag day: a client older than this presents its hub +# session token to a node, which a 4.0 node refuses. A desktop client below +# 0.16.0 is told to update *before* it connects rather than meeting a handshake +# refusal it cannot read; the browser reloads this build from the hub. +MIN_CLIENT_VERSION = "0.16.0" +RECOMMENDED_CLIENT_VERSION = "0.16.0" @router.get("/version") diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 27edc4e..f3683eb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -305,16 +305,15 @@ window.addEventListener('hashchange', () => { // The `v: '0.1'` on every other message in this file is the historical value // and is read by nothing; it is left alone deliberately. The range is // negotiated once, at the start, not restated per message. -const MNP_V = '3.1'; -// **Not** raised with it, and that is the whole difference between 3.0 and 3.1. -// 3.0 was a flag day because a node older than it cannot grant the lease this -// client opens for every download and upload, so talking to one would mean -// every transfer failing for a reason the person cannot act on. 3.1 only adds -// `user_blob_*`: a 3.0 node answers "unknown message type" and the client -// stores its playlists on the next node it reaches, keeping its own copy -// meanwhile (docs/playlists.md §6.4). Refusing every 3.0 node over a feature -// that degrades this quietly would be the flag day nobody needed. -const MNP_V_MIN = '3.0'; +const MNP_V = '4.0'; +// Raised with it: 4.0 is a flag day. A member now presents a short-lived +// MNP-audience token in the handshake, not its hub session token — a node +// older than 4.0 expected the session token, and one newer refuses it, so the +// two cannot authenticate across the break. This is the C6 rule: no +// compatibility branch, or the old path (a hub credential handed to a node) +// stays reachable. The desktop client is gated by client.minimum before it +// even connects; the browser picks up this build on reload. +const MNP_V_MIN = '4.0'; // Codes a NODE sends us, in its own vocabulary (meshbay_common/handshake.py's // check_version): `version_too_old` means *we* are too old for it, diff --git a/packages/meshbay-node/tests/golden/dispatch.json b/packages/meshbay-node/tests/golden/dispatch.json index 98911f5..5d15057 100644 --- a/packages/meshbay-node/tests/golden/dispatch.json +++ b/packages/meshbay-node/tests/golden/dispatch.json @@ -2228,7 +2228,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -2551,7 +2551,7 @@ "subject": "gggggggggggggggggggggggggggggggg", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -2570,7 +2570,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -2589,7 +2589,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -2608,7 +2608,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -2878,7 +2878,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -2892,7 +2892,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -2906,7 +2906,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -2920,7 +2920,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -2934,7 +2934,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -2948,7 +2948,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -2962,7 +2962,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -2976,7 +2976,7 @@ "messages": [], "req_id": 4242, "type": "chat_hist_resp", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -8855,7 +8855,7 @@ "subject": "gggggggggggggggggggggggggggggggg", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -8874,7 +8874,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -8893,7 +8893,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -8912,7 +8912,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -9247,7 +9247,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -9266,7 +9266,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -9285,7 +9285,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -9620,7 +9620,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -9639,7 +9639,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -9658,7 +9658,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -11961,7 +11961,7 @@ "subject": "link:gggggggggggggggggggggggggggggggg", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -14060,7 +14060,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -14079,7 +14079,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -14098,7 +14098,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -14433,7 +14433,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -14452,7 +14452,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -14471,7 +14471,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -16532,7 +16532,7 @@ "req_id": 4242, "token": null, "type": "pong", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -16547,7 +16547,7 @@ "x" ], "type": "pong", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -16560,7 +16560,7 @@ "req_id": 4242, "token": 7, "type": "pong", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -16573,7 +16573,7 @@ "req_id": 4242, "token": "x", "type": "pong", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -16586,7 +16586,7 @@ "req_id": 4242, "token": null, "type": "pong", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -16601,7 +16601,7 @@ "x" ], "type": "pong", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -16614,7 +16614,7 @@ "req_id": 4242, "token": 7, "type": "pong", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -16627,7 +16627,7 @@ "req_id": 4242, "token": "x", "type": "pong", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -16962,7 +16962,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -16981,7 +16981,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -17000,7 +17000,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -17335,7 +17335,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -17354,7 +17354,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -17373,7 +17373,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -17708,7 +17708,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -17727,7 +17727,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -17746,7 +17746,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -18081,7 +18081,7 @@ "subject": "['x']", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -18100,7 +18100,7 @@ "subject": "7", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -18119,7 +18119,7 @@ "subject": "x", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -18454,7 +18454,7 @@ "subject": "['x']:rw=on,rem=on", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -18473,7 +18473,7 @@ "subject": "7:rw=on,rem=on", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -18492,7 +18492,7 @@ "subject": "x:rw=on,rem=on", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -21439,7 +21439,7 @@ "subject": "custom_token=no,language=default", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -21482,7 +21482,7 @@ "subject": "custom_token=yes,language=x", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] @@ -23369,7 +23369,7 @@ "subject": "d=7,u=7", "ts": "<volatile>", "type": "admin_challenge", - "v": "3.4" + "v": "4.0" } ], "spawned": [] |