summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/MESHBAY_DESIGN.md49
-rw-r--r--docs/MESHBAY_NODE_PROTOCOL.md64
-rw-r--r--docs/transfers-v1.md5
-rw-r--r--packages/meshbay-client/src/transcripts.js11
-rw-r--r--packages/meshbay-common/src/meshbay_common/__init__.py5
-rw-r--r--packages/meshbay-common/src/meshbay_common/adminop.py24
-rw-r--r--packages/meshbay-common/src/meshbay_common/groupbox.py2
-rw-r--r--packages/meshbay-common/src/meshbay_common/protocol.py27
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport-admin.js75
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport-chat.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js3
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops/chat.py2
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops/settings.py6
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc/admin.py49
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py6
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py10
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc/group_ops.py84
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc/node_ops.py210
-rw-r--r--packages/meshbay-node/src/meshbay_node/ui/app.py7
-rw-r--r--packages/meshbay-node/tests/golden/dispatch.json3437
-rw-r--r--packages/meshbay-node/tests/test_admin_ops_mnp.py174
-rw-r--r--packages/meshbay-node/tests/test_node_status.py236
-rw-r--r--packages/meshbay-node/tests/test_security_regressions.py97
23 files changed, 183 insertions, 4402 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md
index 49bd99b..189dbc5 100644
--- a/docs/MESHBAY_DESIGN.md
+++ b/docs/MESHBAY_DESIGN.md
@@ -786,10 +786,10 @@ Three properties are why this shape:
The node produces every copy of the key itself, from its own CSPRNG. **Nothing
arriving over MNP can activate a group key** (**C5b**). Read that precisely: it
-targets *key material arriving from outside*, not the instruction. An
-operator-signed `gek_rotate` where the node generates the key is a different shape
-and is allowed. The initial `gek-init` stays local, because with no key there is
-no completed session to carry a signed op.
+targets *key material arriving from outside*, not the instruction: a key the node
+generates itself on an operator's instruction is a different shape. Initialising
+and rotating the group key are local (loopback API, CLI); the signed `chat_epoch`
+is the MNP instance of that shape.
### 4.3 On-the-fly encryption
@@ -898,7 +898,7 @@ signature refuses it.
**Epochs.** A new epoch is opened when, and only when, the set of devices that may
read *future* messages shrinks: `member revoke`, `member unpin`, `revoke_device`,
-`gek_rotate`, or an explicit `chat rotate`. Epoch 1 is opened at group load — a
+or an explicit `chat rotate`. Epoch 1 is opened at group load — a
group with no epoch is a group nobody can speak in.
**Old epochs are kept and still delivered.** That is what keeps history readable
@@ -1233,9 +1233,9 @@ from anything in the response.
| `dir_delete` | the operator alone, and only on an empty directory |
| `invite_create` | the operator (or a delegate, when delegation ships) |
| `invite_link_create`, `invite_cancel` | the operator |
-| `gek_rotate` | operator-signed; the node generates the key itself |
-| initial `gek-init` | **local admin API or CLI only** |
-| root add/remove/update/eject/plug, `apps_enabled`, app directories, transfer limits | operator-signed |
+| group key init and rotation | **local admin API or CLI only**; the node generates the key itself |
+| root add, root `writable`/`removable`, hosting a group, transfer limits | **local admin API or CLI only** (MNP 6.0) |
+| root remove/eject/plug, `apps_enabled`, app directories | operator-signed |
| ~~`gek_bundle_store`~~ | **the message does not exist.** No member ever hands the node key material |
`gek_bundle_store` was deleted rather than gated. The operator's X25519 public key
@@ -1899,26 +1899,24 @@ issues invitations and reads the audit log. There is no server-rendered dashboar
the desktop client's Node page and the CLI are the two consumers, and each
operation endpoint is one `_op(...)` line onto `ops` (§5.4).
-**Over MNP, the node's own controls need a proved operator device.** The
-node-wide surface — `node_status`, which lists every group on the machine with
-each root's absolute path, plus `node_settings_set`, `roster_read`,
-`denylist_read`, `denylist_clear` and `node_reload` — is reachable when two
-things hold: the account is the one the node belongs to, *and* the device on the
-connection has proved (`device_hello`, §3.3) a key the roster holds as an
-operator. The first alone is a claim in a token the hub issued, and **NS4** does
-not allow it to be authority: a hub that can name the operator is a hub that can
-be one. The second is what it cannot forge, since it holds no user keys and
-cannot countersign a device — the same property device linking rests on. A
-browser that has never been paired therefore reads nothing here, exactly as it
-can already sign nothing (§5.4).
+**The node's own controls are not on MNP.** Its status — which lists every group
+on the machine with each root's absolute path — settings, roster, denylist and
+reload were MNP messages gated on a proved operator device (`device_hello`, §3.3),
+because the account id in a token is the hub's to choose (**NS4**). No client ever
+sent them, and MNP 6.0 removed them with the four signed ops in the same position
+(`gek_rotate`, `member_unpin`, `transfer_limits`, `group_detach`): the desktop
+client's Node page and the CLI do this work over loopback. A door nobody calls is
+an untested way in, and one that does not exist needs no gate.
**The accepted cost, recorded as a choice:** on a headless server the only admin
path is the CLI. The CLI covers every operation, so this is acceptable — but it is
a real capability reduction, not an oversight.
-> **MNP is the path that must exist; loopback is the fallback.** The operator of a
-> node is not necessarily sitting at it. Any operator-facing control needs its MNP
-> route first, or it renders for nobody on the web.
+> **What the operator must see needs an MNP route; what widens the node does not
+> get one.** The operator of a node is not necessarily sitting at it, so a view that
+> only loopback can fill renders for nobody on the web — which is why the roots
+> table rides in the index. But sharing a folder, opening it to writes and the
+> node's own controls are decided at the node (§6.2, MNP 6.0).
### 6.8 Node settings
@@ -3621,7 +3619,7 @@ had already been asked.
| **AV15** | **A hash is checked for shape before it is a key lookup**, on every blocklist endpoint, the administrator's included |
| **AV20** | **Chat is bounded in size and in rate, like every other member-supplied write** (§6.6). A message is a row on the operator's disk that nothing expires, a relayed copy for every connected member and a notification for every member of the group; the only ceiling was the frame size. Uploads had carried four protections and a cap since C5a because somebody asked what one member costs the others on that path, and nobody had asked it on this one |
| **AV21** | **A lease is what the node granted, not what the client called it** (§5.5). `tr` was read as a boolean, so any non-empty string skipped the leaseless ceiling and every cap behind it, and a queued transfer was held back only by the honesty of the client waiting in the queue |
-| **AV22** | **The node's own controls take no authority from a hub token** (§6.7). `node_status`, `node_settings_set`, `roster_read`, `denylist_read`, `denylist_clear` and `node_reload` were gated on the account id in the JWT, which is the hub's to choose — NS4 and M3 with the check written the other way round. The gate is a proved operator device, which a hub holding no user keys cannot produce |
+| **AV22** | **The node's own controls take no authority from a hub token** (§6.7). `node_status`, `node_settings_set`, `roster_read`, `denylist_read`, `denylist_clear` and `node_reload` were gated on the account id in the JWT, which is the hub's to choose — NS4 and M3 with the check written the other way round. The gate became a proved operator device, which a hub holding no user keys cannot produce; since MNP 6.0 the messages are gone and these controls are loopback and CLI only |
| **AV23** | **An upload's owner is recorded when the upload ends and applied when the entry is created**, which are different moments (§5.4). Written against the index at the end of the upload it matched nothing, every time, and left every uploaded file owned by nobody — so no member could delete what they had sent |
| **AV24** | **A node registered for no group is refused signaling, not exempted from it** (§7.2). The membership check was written as "if the node claims any group", so it skipped itself — membership, group status and the public-group gate together — for the node AV1 made commonplace: the unconfigured one, which is also the one least able to absorb the work |
| **AV25** | **Which nodes host a group is answered to its members** (§7.3). Only the public case checked, so a private group told any authenticated account that knew its id which machines hosted it — and an ex-member knows that id for ever |
@@ -3807,11 +3805,12 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows.
| **The reconnect backoff only wakes on `visibilitychange`** | So a tab that stays visible through an outage — which is what a screen wake lock guarantees while a film is playing — waits out the full backoff, up to 30 s, after the network is already back. Nothing listens for `online` |
| **Per-device revocation has no CLI** | A device is revoked over MNP (`roster.revoke_device`), from a device the node has already pinned. On a headless node the operator's only lever is `member unpin`, which removes **every** device of that account — so the per-device control the roster is built around is reachable from an interface and from nowhere else. §6.7 listed a `meshbay-node member device list\|revoke` verb that was never written, and that listing is how this was found: `USERGUIDE.md` was the first document written by reading the CLI rather than this specification, and the verb it copied out did not run |
| **Migrations run on SQLite only** | The chain reaches head and agrees with the models there (§12), which is not where it ships. **The exposure is one revision deep, not the whole chain**: every revision behind the first packaged release was development that no installation ever ran, so nothing replays them on PostgreSQL. What is unguarded is the *next* migration — a default, an index type or a constraint PostgreSQL refuses reaches a deploy without the suite saying so |
-| **The loopback path removes access without writing an audit entry** | `ops.revoke_member` and `ops.unpin_member` log to the daemon's log and nothing to `audit.db`; the MNP admin handlers doing the same work audit `member_revoke` and `member_unpin`. So a removal made from the node page or the CLI — the two doors an operator sitting at their own machine actually uses — leaves the journal showing an admission and then, whenever that person next connects, an `auth_failed` ("not admitted by the roster") with nothing in between to explain it. §5.4's signed transcript is not what is missing: a loopback caller is authorized by being on localhost with the run token and signs nothing, so the gap is the record, not the authority. Found by reading a node's audit log for a refusal whose cause was six hours earlier and unrecorded |
+| **The loopback path removes access without writing an audit entry** | `ops.revoke_member` and `ops.unpin_member` log to the daemon's log and nothing to `audit.db`; the MNP `member_revoke` handler doing the same work audits it (and `member_unpin` did, until it left MNP in 6.0). So a removal made from the node page or the CLI — the two doors an operator sitting at their own machine actually uses — leaves the journal showing an admission and then, whenever that person next connects, an `auth_failed` ("not admitted by the roster") with nothing in between to explain it. §5.4's signed transcript is not what is missing: a loopback caller is authorized by being on localhost with the run token and signs nothing, so the gap is the record, not the authority. Found by reading a node's audit log for a refusal whose cause was six hours earlier and unrecorded |
| **The Create group wizard calls two hooks after an early return** | `CreateGroupWizard` (`create-group-page.js`) returns during node detection, before its `useRef`/`useEffect` for provisioning, so the hook count changes between renders. Preact tolerates a list that grows, and nothing is known to break; `test_hook_ordering.py` checks declaration order, not this. Found while tracing the frozen-fields report, which had another cause (`ask.js`) |
| **A node key is read from the terminal or the desktop client, never a browser** | **Accepted.** `meshbay-node status` on the node's own machine and Node → Overview in the desktop client are the two places the key can be read; the Node page is Electron-only, because `platform.node` resolves to "not available" without the bridge, and no hub route exposes the key. The create-group wizard links it automatically over that same bridge, so the manual paste in **Profile → Link Node** exists for the operator who runs the node from a terminal and the hub from a browser — who has a terminal by definition. Anyone linking a node is already at a shell prompt, so a browser-reachable copy would buy nothing and widen what the hub knows about the node |
| **Listing a group's folders walks every root on the event loop** | `index_sync_message` (`transport/wire.py`) builds its `dirs` field with `list_dirs`, an `rglob("*")` over every root, and nothing sends it off the loop: the WebRTC `index_sync` handler, the daemon's index push and QUIC all call it inline. So each index request from any member is a directory walk of the whole library that every other peer on the node waits behind. `test_disk_io_off_loop.py` never saw it, because it reads the transport's own modules and the walk is one call away in `wire.py`. Found by widening what that test reads, not by a symptom |
| **A loopback eject or plug reaches open pages late** | `ops.eject_root` and `ops.plug_root` flip the live set and tell nobody; over MNP the broadcast `root_eject_ack` / `root_plug_ack` is what moves every open table. So an eject made from the desktop application or the CLI shows on members' pages only with the next index push — for a plug, the end of its rescan; for an eject, whatever changes next. `ops.update_root` had the same silence and now calls `DirectoryIndexer.publish_roots`; the same call belongs in these two. Found while moving the `writable`/`removable` switches to the loopback door (MNP 6.0) |
+| **A group key rotated from the Node page leaves the chat key where it was** | `ops.set_gek(rotate=True)` replaces the group key and opens no chat epoch; the MNP `gek_rotate` handler opened one itself (`_new_chat_epoch`), and it was the only door that did — but no client ever sent it, and it is gone since 6.0. The removals that matter (revoke, unpin, device revoke) open an epoch in `ops` for every door, so what is missing is the follow-through for an operator who rotates by hand: §4.5's "rotate after a removal" means it for chat too. The fix is the `_after_removal` shape — `open_chat_epoch` inside `ops.set_gek` when `rotated`. Found while removing the MNP message |
| **The transcoded-seek test passes without transcoding** | `test_a_transcoded_video_keeps_accurate_seeking` (`test_stream_seek_audio_alignment.py`) forces the re-encode branch by swapping the module's `BROWSER_INCOMPATIBLE_VIDEO_CODECS`, then checks only that the result has no audio gap. The copy path also leaves no gap on that clip, so pointing the swap at a module the streaming code does not read still passes: the test cannot tell that the branch it is named after never ran. It should assert the re-encode happened (the `re-encoding` log line, or the encoder in the ffmpeg argv). Found by breaking the swap on purpose while moving the streaming code |
---
diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md
index cdcc2c0..0b5213c 100644
--- a/docs/MESHBAY_NODE_PROTOCOL.md
+++ b/docs/MESHBAY_NODE_PROTOCOL.md
@@ -162,7 +162,7 @@ Codes in use:
| Upload | `upload_not_sealed`, `no_group_key`, `lease_not_granted`, `upload_incomplete`, `bad_chunk_encoding`, `bad_chunk_index`, `invalid_filename`, `no_roots`, `no_such_root`, `no_writable_root`, `root_read_only`, `root_unavailable`, `no_such_directory`, `already_exists`, `not_started`, `too_large` (§11.4) |
| Directories | `root_read_only`, `root_unavailable` (§11.5) |
| Chat | `chat_too_large`, `chat_rate_limited` (§11.7) |
-| Operator controls | `not_operator`, `too_many_pending`, `too_large` (§10.4) |
+| Operator controls | `too_many_pending`, `too_large` (§10.4) |
| Metadata | `transcode_not_applicable`, `tmdb_search_rate_limited` (§11.9) |
| Moderation | `content_blocked` — a file the hub's content blocklist names, in a public group (§11.3) |
@@ -1156,8 +1156,6 @@ broadcast, every connected peer in the group learns the change without reconnect
| `invite_link_create` | `link:<group_id>`, the session's group | operator only | `invite_link_result{code, invite_id, expires_at, group_id}` | no — the code is shown once |
| `invite_cancel` | `invite_id` (32 hex) | operator only | `ack{detail: "invite_cancelled", invite_id}` | no |
| `member_revoke` | `user_id` | operator | `member_revoke_ack` | no |
-| `member_unpin` | `user_id` | operator | `member_unpin_ack{user_id}` | no |
-| `gek_rotate` | `group_id` | operator | `gek_rotate_ack{group_id, authorized_members, note}` | no |
| `apps_enabled` | the app set | operator | `apps_enabled_ack{apps}` | yes |
| `set_scan_settings` | the interval/debounce pair | operator | `set_scan_settings_ack{...}` | yes |
| `tmdb_config` | `{token, language}` — `token` is `null` (unchanged), `""` (clear) or `sha256:<hex>` of the token, never the token | operator | `tmdb_config_ack{token_customized, language}` | yes (never the token) |
@@ -1171,9 +1169,7 @@ broadcast, every connected peer in the group learns the change without reconnect
| `chat_directory` | the path | operator | `chat_directory_ack{path}` | yes |
| `chat_link_preview` | `on\|off` | operator | `chat_link_preview_ack{enabled}` | yes |
| `search_listed` | `on\|off` | operator | `search_listed_ack{listed}` | yes |
-| `transfer_limits` | `d=<n>,u=<n>` | operator | `transfer_limits_ack{limits}` | yes |
| `chat_epoch` | `group_id` | operator | `chat_epoch_ack{epoch}` | yes |
-| `group_detach` | the group name | operator | `group_detach_ack` | no |
**Upload policy is not in this table**, and that is the design: whether a member may
write is a property of each root (its `writable` flag), not a switch over the group. A single
@@ -1212,18 +1208,14 @@ Their *storage* keys survive on the node — `Roster.LEGACY_DIR_KEYS` still read
operator's disk rather than on the wire, and a node upgraded into this has to find its
own configuration.
-A second family of operator messages is **not** signed: `node_status`, `roster_read`,
-`denylist_read`, `denylist_clear`, `node_settings_set`, `node_reload`. These are gated
-by `_operator_device()`, which asks two things: the authenticated session's `user_id`
-is the account the node records as its own (`node_user_id`, `is_node_admin()`), **and**
-the device on this connection has proved, with `device_hello` (§9.4), a key the roster
-holds as an operator. Anything else is refused with code `not_operator`. The first
-alone would be a claim in a token the hub issued, and a hub that can name the operator
-is a hub that can be one; the second is what it cannot forge, since it holds no user
-keys. Three of them only read; the other three run through the same `ops` entry points
-as the CLI and the loopback admin API. The distinction from the signed table above:
-a signed op proves possession of an operator key for *this* operation, while these
-prove it once per connection, through the device the connection identified.
+**The node's own controls are not MNP messages.** Its status (every group, with each
+root's absolute path), settings, roster, denylist and reload; rotating a group key;
+forgetting a pinned identity; the per-member transfer caps; no longer hosting a group —
+the Node page in the desktop application and the CLI do these over the loopback API, on
+the operator's own machine. Until 6.0 MNP carried them too (`node_status`,
+`node_settings_set`, `roster_read`, `denylist_read`, `denylist_clear`, `node_reload`,
+`gek_rotate`, `member_unpin`, `transfer_limits`, `group_detach`), and no client ever
+sent one: a door nobody calls is an untested way in (§10.5).
**The subject covers everything the node acts on.** The signature covers `op`, the
node, the group, the subject, the nonce and the time — nothing else of the request —
@@ -1242,16 +1234,14 @@ operations (`too_many_pending` beyond), each at most 64 KiB of subject and paylo
Rules that hold across the table:
* **No MNP message can activate a group key.** The rule (I2) targets key material
- arriving from outside, not the instruction: `gek_rotate` and `chat_epoch` are allowed
- precisely because the node generates the new key itself with its own CSPRNG. Initial
- `gek-init` stays local — with no group key there is no completed session to carry a
- signed op anyway.
+ arriving from outside, not the instruction: `chat_epoch` is allowed precisely
+ because the node generates the new key itself with its own CSPRNG. Initialising and
+ rotating the group key stay local (loopback, CLI).
* **There is no operation by which key material reaches the node** (§12). The node
wraps for a key the recipient has proved possession of, so no such message is needed —
and a path that does not exist cannot be mis-authorized, which is I10 applied to a
message instead of a version.
-* An operator cannot revoke or unpin **themselves** over the connection their pin
- authorizes.
+* An operator cannot revoke **themselves** over the connection their pin authorizes.
* Rotation is what actually removes a revoked member's access. Revocation stops the
node serving the *next* key; the ex-member still holds the current one, and content
they already downloaded stays readable. The ack says so in words.
@@ -1419,7 +1409,7 @@ The control plane is not covered either — see §14.2.
**Nonce collision, since `upload` is the first purpose with volume.** One subkey per
purpose and a fresh 96-bit random nonce per message: at one message per 48 KiB chunk,
2³² chunks is 200 TB uploaded under a single GEK before the collision probability
-reaches 2⁻³², and `gek_rotate` exists. Deriving the nonce from the payload instead
+reaches 2⁻³², and the group key can be rotated. Deriving the nonce from the payload instead
would be worse, not better — two chunks of identical bytes are ordinary in a file.
**Failure is fatal, never degraded** (I8). A client that cannot open an index message
@@ -1477,9 +1467,9 @@ The **lease** is that object, and every transfer runs under one.
**Caps.** Node-wide, 8 concurrent per kind by default; per member per group, 2 by
default. A group with no value of its own gets the default, never "unlimited": reading
an absent setting as no limit would leave the node-wide cap as the only control, which
-is the situation leases exist to end. The per-group value is a signed operator
-operation (`transfer_limits`, §10.4, bounded to 1–32; zero is refused, because a member
-who may not transfer at all is a member the operator revokes). The node-wide values are
+is the situation leases exist to end. The per-group value is set on the node's machine
+(`ops.set_transfer_limits`, loopback and CLI; bounded to 1–32, zero is refused, because
+a member who may not transfer at all is a member the operator revokes). The node-wide values are
daemon settings. A member's own cap rides on every `transfer_state`, so the interface can
say "2 of your 2 slots are busy" rather than draw a spinner that explains nothing.
@@ -1946,7 +1936,7 @@ collide immediately. The design degrades correctly into the deployment that exis
chain-based one would not have.
**Epochs.** A new epoch is opened when the set of devices that may read *future*
-messages shrinks — member revoke, member unpin, device revoke, `gek_rotate` — and by
+messages shrinks — member revoke, member unpin, device revoke — and by
hand with the signed `chat_epoch` op (§10.4). Old epochs are kept and still delivered to
current members, which is what keeps history readable to the people who could already
read it. The epoch key is wrapped under the group key **at delivery**, never stored
@@ -2132,15 +2122,12 @@ it back (§3.5).
| `admin_response` | C→N | auth | the operator's signature over that transcript |
| `client_diag` | C→N | auth | the video player's own view of a stream, written to the node's log beside its own (a stream event at INFO, the periodic state at DEBUG); the node acts on none of it and sends no reply |
| `member_revoke` / `_ack` | C→N / N→C | signed | stop serving the key to someone |
-| `member_unpin` / `_ack` | C→N / N→C | signed | forget a pinned identity |
-| `transfer_limits` / `_ack` | C→N / N⇒C | signed | per-member transfer caps for this group |
| `chat_epoch` / `_ack` | C→N / N⇒C | signed | open a new chat epoch by hand |
| `app_directories` / `_ack` | C→N / N⇒C | signed | one application's folders, keyed by app name |
| `chat_directory` / `_ack` | C→N / N⇒C | signed | where chat attachments are written |
| `chat_link_preview` / `_ack` | C→N / N⇒C | signed | whether the node unfurls posted links |
| `search_listed` / `_ack` | C→N / N⇒C | signed | whether members' cross-group Search lists this group |
| `root_eject` / `_ack`, `root_plug` / `_ack` | C→N / N⇒C | signed | take a removable root offline, put it back |
-| `gek_rotate` / `_ack` | C→N / N→C | signed | node generates a new group key |
| `apps_enabled` / `_ack` | C→N / N⇒C | signed | which group apps are shown |
| `set_scan_settings` / `_ack` | C→N / N⇒C | signed | reconcile interval and debounce |
| `tmdb_config` / `_ack` | C→N / N⇒C | signed | node-wide TMDB token and language |
@@ -2149,13 +2136,6 @@ it back (§3.5).
| `tmdb_rematch` / `_ack` | C→N / N⇒C | signed | drop one file's cached match |
| `musicbrainz_enabled` / `_ack` | C→N / N⇒C | signed | per-group MusicBrainz on/off |
| `root_remove` / `_ack` | C→N / N→C | signed | remove a shared directory |
-| `group_detach` / `_ack` | C→N / N→C | signed | stop hosting a group |
-| `node_status` / `_ack` | C→N / N→C | auth (operator) | all groups, roots, daemon state |
-| `roster_read` / `_ack` | C→N / N→C | auth (operator) | pinned identities and members |
-| `denylist_read` / `_ack` | C→N / N→C | auth (operator) | current refusals |
-| `denylist_clear` / `_ack` | C→N / N→C | auth (operator) | remove entries |
-| `node_settings_set` / `_ack` | C→N / N→C | auth (operator) | change daemon settings |
-| `node_reload` / `_ack` | C→N / N→C | auth (operator) | re-read `node.toml` |
| `error` | N→C | any | refusal, with `detail` and optionally `code`, `req_id`, and the `upload_id` / `tr` / `file_id` it is about |
| `ack` | N→C | auth | generic acknowledgement (chat, keypair bundle store) |
@@ -2210,7 +2190,8 @@ so nothing is left unsigned on either side. That is why the floor did not move.
6.0 is a MAJOR that removes three signed operations — `root_add`, `root_update`,
`group_attach` — rather than changing any (§10.4: nothing over MNP widens what a node
shares). A 5.x client that sends one gets no answer, as for any unknown type, and
-everything else it does still works; the floor stays at 4.0. The desktop application
+everything else it does still works; the floor stays at 4.0. The same version removes
+ten operator messages no client ever sent (§10.4, "The node's own controls"). The desktop application
also refuses to sign them, so a node older than 6.0 cannot be driven into them by a
script in its page either.
@@ -2353,9 +2334,8 @@ walks through the gate meant to stop it.
file chunks, stream segments, uploads, the chat keys and the group roster. It does not
cover the admin and configuration acks (`app_directories_ack`, `root_*_ack` and the
rest), which carry the same folder names the sealed index carries; the media-metadata replies (`media_meta_resp`, `music_meta_resp`,
- `link_preview_resp`), which carry titles, artists and synopses; `node_status_ack`,
- which carries absolute paths on the operator's disk to an operator session; the
- identity replies (`roster_read_ack`, `device_list_result`); or `invite_result` and
+ `link_preview_resp`), which carry titles, artists and synopses; the identity reply
+ `device_list_result`; or `invite_result` and
`invite_link_result`, which carry a pairing code. All are inside DTLS/TLS and none reaches the hub, but none is
behind the group key.
* **Transfer messages are in clear on purpose**, and that is a deliberate line rather
diff --git a/docs/transfers-v1.md b/docs/transfers-v1.md
index a07d7d6..b87e07c 100644
--- a/docs/transfers-v1.md
+++ b/docs/transfers-v1.md
@@ -607,6 +607,11 @@ signs names the outcome. `_do_transfer_limits` +
`transfer_limits_ack` to the group's peers, surfaced in the group Settings tab
as a section beside the scan settings.
+> **Since MNP 6.0 the signed op and its ack are gone.** No client ever sent it —
+> the Settings section was not built — so the value is set on the node's machine:
+> `PUT /api/groups/{id}/transfer-limits` on loopback, and
+> `meshbay-node transfers set`. `ops.set_transfer_limits` is unchanged.
+
**Absent means the default (2), not unlimited.** Deliberately unlike
`member_upload`'s "absent means allowed": a group that predates the setting and
came back unlimited would leave the node-wide cap as the only control, which is
diff --git a/packages/meshbay-client/src/transcripts.js b/packages/meshbay-client/src/transcripts.js
index 63f87b2..8b0f6ef 100644
--- a/packages/meshbay-client/src/transcripts.js
+++ b/packages/meshbay-client/src/transcripts.js
@@ -37,12 +37,11 @@ function lenPrefixed(prefix, parts) {
// script in the page either.
const ADMIN_OPS = new Set([
'file_delete', 'dir_delete', 'invite_create', 'invite_link_create',
- 'invite_cancel', 'member_revoke', 'member_unpin', 'gek_rotate',
- 'apps_enabled', 'set_scan_settings', 'transfer_limits', 'tmdb_config',
- 'tmdb_enabled', 'tmdb_override', 'tmdb_rematch', 'musicbrainz_enabled',
- 'root_remove', 'root_eject', 'root_plug', 'app_directories',
- 'chat_directory', 'chat_link_preview', 'search_listed', 'chat_epoch',
- 'group_detach',
+ 'invite_cancel', 'member_revoke', 'apps_enabled', 'set_scan_settings',
+ 'tmdb_config', 'tmdb_enabled', 'tmdb_override', 'tmdb_rematch',
+ 'musicbrainz_enabled', 'root_remove', 'root_eject', 'root_plug',
+ 'app_directories', 'chat_directory', 'chat_link_preview', 'search_listed',
+ 'chat_epoch',
]);
// ── Field checks ──────────────────────────────────────────────────────────
diff --git a/packages/meshbay-common/src/meshbay_common/__init__.py b/packages/meshbay-common/src/meshbay_common/__init__.py
index cb84e2c..2a58326 100644
--- a/packages/meshbay-common/src/meshbay_common/__init__.py
+++ b/packages/meshbay-common/src/meshbay_common/__init__.py
@@ -267,5 +267,10 @@ __version__ = "0.17.0"
# desktop application over loopback, or the CLI), never
# over the wire. A 5.x client that sends one gets no answer, as for any unknown
# type; everything else it does still works, so the floor stays at 4.0.
+# The same version removes ten operator messages no client ever sent —
+# `gek_rotate`, `member_unpin`, `transfer_limits`, `group_detach` (signed) and
+# `node_status`, `node_settings_set`, `roster_read`, `denylist_read`,
+# `denylist_clear`, `node_reload` — whose work the Node page and the CLI do over
+# loopback.
MNP_VERSION = "6.0"
MHP_VERSION = "0.1"
diff --git a/packages/meshbay-common/src/meshbay_common/adminop.py b/packages/meshbay-common/src/meshbay_common/adminop.py
index b8915a9..bd7a439 100644
--- a/packages/meshbay-common/src/meshbay_common/adminop.py
+++ b/packages/meshbay-common/src/meshbay_common/adminop.py
@@ -47,14 +47,6 @@ OP_INVITE_LINK_CREATE = "invite_link_create"
# Taking back an unredeemed link, by the handle it was issued with.
OP_INVITE_CANCEL = "invite_cancel"
OP_MEMBER_REVOKE = "member_revoke"
-# Rotating the group key is what actually takes it away from a revoked member:
-# revocation stops the node serving the *next* key, and they still hold the
-# current one. The node generates the new key itself with its own CSPRNG, so
-# nothing arriving over MNP contributes key material — the C5b rule is about
-# key material from outside, not about the instruction.
-OP_GEK_ROTATE = "gek_rotate"
-# Forgetting a pinned identity, so someone can pair again after losing a device.
-OP_MEMBER_UNPIN = "member_unpin"
# Which group "applications" (Chat, Files, and whatever registers later) are
# shown to members. Signed like the rest: it decides what a member sees, not
# anything about key material, but an unsigned toggle would let any member
@@ -67,11 +59,6 @@ OP_APPS_ENABLED = "apps_enabled"
# security property in itself, but the pattern (every operator setting is
# signed) is what keeps the authorization model simple to reason about.
OP_SET_SCAN_SETTINGS = "set_scan_settings"
-# How many transfers one member may run at once in this group. Signed like the
-# rest: an unsigned cap is one any member can raise for themselves, which makes
-# the control a suggestion. The subject is "d=2,u=2" so what the operator is
-# shown before signing names the outcome and not the operation.
-OP_TRANSFER_LIMITS = "transfer_limits"
# Whether the node uses the operator's own API token/language instead of the
# shipped default — node-wide (docs/MESHBAY_DESIGN.md §9.7), one credential
# shared by every group. Signed like the rest: it turns on outbound
@@ -114,16 +101,19 @@ OP_CHAT_LINK_PREVIEW = "chat_link_preview"
# operator's, signed like the other per-group switches.
OP_SEARCH_LISTED = "search_listed"
# Open a new chat epoch for a group, by hand. The removals that matter open one
-# by themselves (member revoke/unpin, device revoke, gek_rotate); this is the
-# operator saying "do it anyway", which is the same shape as `gek_rotate` and
-# signed for the same reason.
+# by themselves (member revoke/unpin, device revoke, group key rotation); this is the
+# operator saying "do it anyway", and is signed like the rest.
#
# There is no op for *enabling* chat encryption. It is not a setting — MNP 2.0
# has no plaintext chat to fall back to.
OP_CHAT_EPOCH = "chat_epoch"
OP_ROOT_EJECT = "root_eject"
OP_ROOT_PLUG = "root_plug"
-OP_GROUP_DETACH = "group_detach"
+# Also gone with 6.0, because no client ever sent them: `gek_rotate`,
+# `member_unpin`, `transfer_limits` and `group_detach`. Rotating the group key,
+# forgetting an identity, the per-member transfer caps and no longer hosting a
+# group are done on the node's machine — the desktop application's Node page
+# or the CLI. A door nobody uses is an untested way in.
# OP_ROOT_ADD, OP_ROOT_UPDATE and OP_GROUP_ATTACH are gone (MNP 6.0). Each one
# chose what of the operator's disk is shared and who may write there, and a
# signature proves only that the operator's key signed — in a browser, through
diff --git a/packages/meshbay-common/src/meshbay_common/groupbox.py b/packages/meshbay-common/src/meshbay_common/groupbox.py
index 260e362..ae79fcc 100644
--- a/packages/meshbay-common/src/meshbay_common/groupbox.py
+++ b/packages/meshbay-common/src/meshbay_common/groupbox.py
@@ -74,7 +74,7 @@ _INFO = {
# the bound that matters is birthday collision under `PURPOSE_UPLOAD` — the only
# purpose with real volume, one message per 48 KiB chunk. 2**32 chunks is 200 TB
# uploaded under a single GEK before the collision probability reaches 2**-32,
-# and `gek_rotate` exists. Deriving the nonce from the payload instead would be
+# and the group key can be rotated. Deriving the nonce from the payload instead would be
# worse, not better: two chunks of identical bytes are ordinary in a file.
NONCE_LEN = 12 # 96-bit, the WebCrypto AES-GCM standard
diff --git a/packages/meshbay-common/src/meshbay_common/protocol.py b/packages/meshbay-common/src/meshbay_common/protocol.py
index 2c7e301..f8e56ec 100644
--- a/packages/meshbay-common/src/meshbay_common/protocol.py
+++ b/packages/meshbay-common/src/meshbay_common/protocol.py
@@ -154,12 +154,8 @@ class MNP:
INVITE_CANCEL = "invite_cancel" # operator → node: take back an unredeemed link
MEMBER_REVOKE = "member_revoke" # operator → node: stop serving the key
MEMBER_REVOKE_ACK = "member_revoke_ack"
- MEMBER_UNPIN = "member_unpin" # operator → node: forget an identity
- MEMBER_UNPIN_ACK = "member_unpin_ack"
APPS_ENABLED = "apps_enabled" # operator → node: which group apps to show
APPS_ENABLED_ACK = "apps_enabled_ack"
- TRANSFER_LIMITS = "transfer_limits" # operator → node: per-member caps here
- TRANSFER_LIMITS_ACK = "transfer_limits_ack" # node → this group: the new caps
SET_SCAN_SETTINGS = "set_scan_settings" # operator → node: reconcile/debounce timing
SET_SCAN_SETTINGS_ACK = "set_scan_settings_ack"
MEDIA_META_REQ = "media_meta_req" # client → node: TMDB metadata for a path
@@ -216,14 +212,8 @@ class MNP:
# where it was.
DEVICE_HELLO = "device_hello" # device → node: this is me
DEVICE_HELLO_ACK = "device_hello_ack"
- # Rotation is the half of revocation that revocation cannot do: the node
- # generates a fresh key itself, so no key material crosses the wire.
- GEK_ROTATE = "gek_rotate" # operator → node: new group key
- GEK_ROTATE_ACK = "gek_rotate_ack"
INVITE_RESULT = "invite_result" # node → operator: the code, once
INVITE_LINK_RESULT = "invite_link_result" # node → operator: link code + handle, once
- NODE_STATUS = "node_status" # operator → node: list all groups + roots
- NODE_STATUS_ACK = "node_status_ack" # node → operator: full status
ROOT_REMOVE = "root_remove" # operator → node: remove a root from a group
ROOT_REMOVE_ACK = "root_remove_ack" # node → operator: confirmed
# One message for every application's directories, keyed by the app's own
@@ -259,25 +249,12 @@ class MNP:
ROOT_PLUG = "root_plug" # operator → node: re-enable an ejected root
ROOT_PLUG_ACK = "root_plug_ack"
# Member → node: who is in this group and which device keys they hold, with
- # the countersignature that admitted each one. Distinct from ROSTER_READ
- # below, which is the operator's view of the whole node: this is scoped to
- # one group and answers any member of it, because the point is that a member
+ # the countersignature that admitted each one. Scoped to one group and
+ # answers any member of it, because the point is that a member
# verifies another member's device *for themselves* rather than trusting the
# node's `sender_id` (Tier 2, docs/MESHBAY_DESIGN.md §3.3).
GROUP_ROSTER_REQ = "group_roster_req"
GROUP_ROSTER_RESP = "group_roster_resp"
- ROSTER_READ = "roster_read" # operator → node: identities + members
- ROSTER_READ_ACK = "roster_read_ack"
- DENYLIST_READ = "denylist_read" # operator → node: show denylist entries
- DENYLIST_READ_ACK = "denylist_read_ack"
- DENYLIST_CLEAR = "denylist_clear" # operator → node: remove denylist entry(ies)
- DENYLIST_CLEAR_ACK = "denylist_clear_ack"
- GROUP_DETACH = "group_detach" # operator → node: stop hosting a group
- GROUP_DETACH_ACK = "group_detach_ack"
- NODE_SETTINGS_SET = "node_settings_set" # operator → node: change daemon settings
- NODE_SETTINGS_SET_ACK = "node_settings_set_ack"
- NODE_RELOAD = "node_reload" # operator → node: re-read node.toml
- NODE_RELOAD_ACK = "node_reload_ack"
# ── Index entry ───────────────────────────────────────────────────────────────
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport-admin.js b/packages/meshbay-hub/src/meshbay_hub/static/transport-admin.js
index 32307c5..63cb644 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport-admin.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport-admin.js
@@ -241,20 +241,6 @@ extendTransport(class {
// ── Node management (D5) ───────────────────────────────────────────────
- async fetchNodeStatus() {
- const msg = await this._sendAndWait({ type: 'node_status', v: '0.1' });
- if (msg.type === 'error') throw new Error(msg.detail);
- return msg;
- }
-
- async updateNodeSettings(settings) {
- const msg = await this._sendAndWait({
- type: 'node_settings_set', v: '0.1', settings,
- });
- if (msg.type === 'error') throw new Error(msg.detail);
- return msg;
- }
-
async removeRoot(groupId, rootName, signFn) {
const msg = await this._sendAndWait({
type: 'root_remove', v: '0.1',
@@ -291,67 +277,6 @@ extendTransport(class {
return msg;
}
- async unpinMember(userId, signFn) {
- const msg = await this._sendAndWait({
- type: 'member_unpin', v: '0.1', user_id: userId,
- });
- if (msg.type === 'error') throw new Error(msg.detail);
- if (msg.type === 'admin_challenge') {
- return this._authorizeAdminOp(msg, 'member_unpin', userId, signFn);
- }
- return msg;
- }
-
- async rotateGek(groupId, signFn) {
- const msg = await this._sendAndWait({
- type: 'gek_rotate', v: '0.1', group_id: groupId,
- });
- if (msg.type === 'error') throw new Error(msg.detail);
- if (msg.type === 'admin_challenge') {
- return this._authorizeAdminOp(msg, 'gek_rotate', groupId, signFn);
- }
- return msg;
- }
-
- async fetchRoster(groupId) {
- const msg = await this._sendAndWait({
- type: 'roster_read', v: '0.1', group_id: groupId || '',
- });
- if (msg.type === 'error') throw new Error(msg.detail);
- return msg;
- }
-
- async fetchDenylist() {
- const msg = await this._sendAndWait({ type: 'denylist_read', v: '0.1' });
- if (msg.type === 'error') throw new Error(msg.detail);
- return msg;
- }
-
- async clearDenylist(subject) {
- const msg = await this._sendAndWait({
- type: 'denylist_clear', v: '0.1', subject: subject || '',
- });
- if (msg.type === 'error') throw new Error(msg.detail);
- return msg;
- }
-
- async detachGroup(name, signFn) {
- const msg = await this._sendAndWait({
- type: 'group_detach', v: '0.1', name,
- });
- if (msg.type === 'error') throw new Error(msg.detail);
- if (msg.type === 'admin_challenge') {
- return this._authorizeAdminOp(msg, 'group_detach', name, signFn);
- }
- return msg;
- }
-
- async reloadConfig() {
- const msg = await this._sendAndWait({ type: 'node_reload', v: '0.1' });
- if (msg.type === 'error') throw new Error(msg.detail);
- return msg;
- }
-
/**
* Ask the node for a one-time pairing code admitting `userId` to this group.
*
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport-chat.js b/packages/meshbay-hub/src/meshbay_hub/static/transport-chat.js
index e49eb4c..b734d44 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport-chat.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport-chat.js
@@ -57,7 +57,7 @@ extendTransport(class {
*
* Not a switch — there is nothing to turn on. The removals that matter open
* an epoch by themselves; this is the operator saying "move the key anyway",
- * the same instruction as `rotateGek` and signed for the same reason.
+ * signed like every operator instruction.
*/
async rotateChatEpoch(signFn) {
// This connection's own group, not a parameter. Every settings pane takes
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index e60f673..279396a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -221,8 +221,7 @@ const ADMIN_OP_TYPES = new Set([
'apps_enabled', 'set_scan_settings', 'member_revoke',
'root_remove', 'root_eject', 'root_plug',
'app_directories', 'chat_directory', 'chat_link_preview', 'chat_epoch',
- 'search_listed', 'member_unpin', 'gek_rotate',
- 'group_detach', 'invite_create', 'invite_link_create', 'invite_cancel',
+ 'search_listed', 'invite_create', 'invite_link_create', 'invite_cancel',
]);
// ── Diagnostic trace (opt-in, off by default) ───────────────────────────────
diff --git a/packages/meshbay-node/src/meshbay_node/ops/chat.py b/packages/meshbay-node/src/meshbay_node/ops/chat.py
index 469e907..539284b 100644
--- a/packages/meshbay-node/src/meshbay_node/ops/chat.py
+++ b/packages/meshbay-node/src/meshbay_node/ops/chat.py
@@ -17,7 +17,7 @@ log = logging.getLogger("meshbay_node.ops")
#
# The key a group's chat archive is encrypted under. Generated here, by the
# node, and never by a member — the C5b rule is about key material arriving from
-# outside, and this is the same rule that lets `gek_rotate` be a signed
+# outside, and this is the same rule that lets a group key rotation be an
# instruction rather than a delivery.
#
# An *epoch* rather than a rotation, and the distinction is the whole design:
diff --git a/packages/meshbay-node/src/meshbay_node/ops/settings.py b/packages/meshbay-node/src/meshbay_node/ops/settings.py
index eade6ca..906cd03 100644
--- a/packages/meshbay-node/src/meshbay_node/ops/settings.py
+++ b/packages/meshbay-node/src/meshbay_node/ops/settings.py
@@ -182,9 +182,9 @@ async def set_transfer_limits(state: dict, group_id: str,
Same shape as every other operator setting: lives on the node (roster.db,
not the hub and not node.toml, for the reason change 5 gives — a hub that
- decided this would have authority over someone else's machine), signed
- (webrtc_server checks the caller's admin authority before this runs), and
- live, so the pools are updated in place rather than at the next restart.
+ decided this would have authority over someone else's machine), set on the
+ node's own machine (loopback API, CLI), and live, so the pools are updated
+ in place rather than at the next restart.
"""
roster = _roster(state)
ctx = _group_ctx(state, group_id)
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/admin.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/admin.py
index 738dbce..9a6cbd1 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/admin.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/admin.py
@@ -17,13 +17,10 @@ from meshbay_common.adminop import (
OP_CHAT_LINK_PREVIEW,
OP_DIR_DELETE,
OP_FILE_DELETE,
- OP_GEK_ROTATE,
- OP_GROUP_DETACH,
OP_INVITE_CANCEL,
OP_INVITE_CREATE,
OP_INVITE_LINK_CREATE,
OP_MEMBER_REVOKE,
- OP_MEMBER_UNPIN,
OP_MUSICBRAINZ_ENABLED,
OP_ROOT_EJECT,
OP_ROOT_PLUG,
@@ -34,7 +31,6 @@ from meshbay_common.adminop import (
OP_TMDB_ENABLED,
OP_TMDB_OVERRIDE,
OP_TMDB_REMATCH,
- OP_TRANSFER_LIMITS,
admin_transcript,
)
from meshbay_common.crypto import pk_to_b64
@@ -59,10 +55,7 @@ _ADMIN_EXECUTORS = {
OP_INVITE_CREATE: "_admin_exec_invite_create",
OP_INVITE_LINK_CREATE: "_admin_exec_invite_link_create",
OP_INVITE_CANCEL: "_admin_exec_invite_cancel",
- OP_GEK_ROTATE: "_admin_exec_gek_rotate",
- OP_MEMBER_UNPIN: "_admin_exec_member_unpin",
OP_APPS_ENABLED: "_admin_exec_apps_enabled",
- OP_TRANSFER_LIMITS: "_admin_exec_transfer_limits",
OP_SET_SCAN_SETTINGS: "_admin_exec_set_scan_settings",
OP_TMDB_CONFIG: "_admin_exec_tmdb_config",
OP_TMDB_ENABLED: "_admin_exec_tmdb_enabled",
@@ -77,7 +70,6 @@ _ADMIN_EXECUTORS = {
OP_CHAT_EPOCH: "_admin_exec_chat_epoch",
OP_ROOT_EJECT: "_admin_exec_root_eject",
OP_ROOT_PLUG: "_admin_exec_root_plug",
- OP_GROUP_DETACH: "_admin_exec_group_detach",
}
@@ -174,49 +166,12 @@ class AdminMixin:
says "the hub says you are the owner", which is the one thing NS4 and
M3 rule out: a hub that can name the operator can install itself as
node administrator. It rides the handshake ack so a client knows whether
- to offer the Node page at all, and every operation is gated on
- `_operator_device()` below.
+ to offer operator controls at all; every operation is gated on a
+ signature (`_verify_admin_sig`).
"""
node_user_id = self._ctx.get("node_user_id")
return bool(node_user_id and self._user_id == node_user_id)
- async def _operator_device(self) -> bool:
- """
- Whether this connection may run the node's own controls.
-
- Two things, and the second is the one that cannot be forged:
-
- - the account is the one this node belongs to (`_is_node_admin`), which
- is what keeps node-wide controls with the machine's owner rather than
- with every paired operator of every group on it; and
- - **the device on this connection proved a key the node pinned as an
- operator**. `device_hello` is signed over a transcript naming this
- node, this group and this connection's nonce, and `operator_pks()` is
- rebuilt from the roster on each call, so an unpinned browser and a
- revoked one are both refused at once.
-
- The second clause is the fix for the door this used to leave open.
- `node_status`, `node_settings_set`, `roster_read`, `denylist_read`,
- `denylist_clear` and `node_reload` were gated on the account id alone —
- a value the hub chooses. An active hub that can also reach the group key
- (which §3.5 concedes it can in an open-join group) could therefore mint
- a token for the owner's account and read `node_status`, which lists
- every group on the node with the operator's **absolute paths**, or clear
- the denylist, which is the persisted revocation H4 exists to keep.
-
- It holds no user keys and cannot countersign anything, so it cannot
- produce a `device_hello` — which is the same property device linking
- rests on (§3.3), applied to the node's own surface.
- """
- if not self._is_node_admin():
- return False
- if not self._device_confirmed or not self._pinned_pk:
- return False
- roster = self._ctx.get("roster")
- if roster is None:
- return False
- return self._pinned_pk in await roster.operator_pks()
-
def _has_admin_authority(self) -> bool:
"""
Cheap synchronous pre-check: is there anyone who could authorize this?
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py
index 493d7f0..dc43ae2 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py
@@ -198,9 +198,9 @@ class ChatMixin:
There is no switch to turn chat encryption on: MNP 2.0 has no plaintext
chat to fall back to. What an operator may want to do deliberately is
- move the key on — the same instruction as `gek_rotate`, and signed for
- the same reason. The removals that matter (member revoke, member unpin,
- device revoke, `gek_rotate`) already open one by themselves.
+ move the key on, which is signed like the rest. The removals that matter
+ (member revoke, member unpin, device revoke, a group key rotation)
+ already open one by themselves.
"""
group_id = str(msg.get("group_id", "")).strip() or self._group_id
if not group_id:
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py
index 1ba5445..4bf9dbb 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py
@@ -48,7 +48,6 @@ _HANDLERS = {
MNP.DEVICE_REVOKE: ("_do_device_revoke", SPAWNED),
MNP.DEVICE_HELLO: ("_do_device_hello", SPAWNED),
MNP.APPS_ENABLED: ("_do_apps_enabled", INLINE),
- MNP.TRANSFER_LIMITS: ("_do_transfer_limits", INLINE),
MNP.SET_SCAN_SETTINGS: ("_do_set_scan_settings", INLINE),
MNP.TMDB_CONFIG: ("_do_tmdb_config", INLINE),
MNP.TMDB_ENABLED: ("_do_tmdb_enabled", INLINE),
@@ -68,18 +67,9 @@ _HANDLERS = {
MNP.MUSIC_META_REQ: ("_do_music_meta_request", SPAWNED),
MNP.AUDIO_TRANSCODE_REQ: ("_do_audio_transcode_request", SPAWNED),
MNP.SUBTITLE_REQ: ("_do_subtitle_request", SPAWNED),
- MNP.MEMBER_UNPIN: ("_do_member_unpin", INLINE),
- MNP.GEK_ROTATE: ("_do_gek_rotate", INLINE),
- MNP.NODE_STATUS: ("_do_node_status", SPAWNED),
MNP.ROOT_REMOVE: ("_do_root_remove", INLINE),
MNP.ROOT_EJECT: ("_do_root_eject", INLINE),
MNP.ROOT_PLUG: ("_do_root_plug", INLINE),
- MNP.ROSTER_READ: ("_do_roster_read", SPAWNED),
- MNP.DENYLIST_READ: ("_do_denylist_read", SPAWNED),
- MNP.DENYLIST_CLEAR: ("_do_denylist_clear", SPAWNED),
- MNP.GROUP_DETACH: ("_do_group_detach", INLINE),
- MNP.NODE_SETTINGS_SET: ("_do_node_settings_set", SPAWNED),
- MNP.NODE_RELOAD: ("_do_node_reload", SPAWNED),
MNP.KEYPAIR_BUNDLE_STORE: ("_do_keypair_bundle_store", SPAWNED),
MNP.KEYPAIR_BUNDLE_DELETE: ("_do_keypair_bundle_delete", SPAWNED),
MNP.USER_BLOB_STORE: ("_do_user_blob_store", SPAWNED),
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/group_ops.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/group_ops.py
index 3756eac..a94e2aa 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/group_ops.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/group_ops.py
@@ -5,9 +5,7 @@ from meshbay_common import MNP_VERSION
from meshbay_common.adminop import (
OP_APP_DIRECTORIES,
OP_APPS_ENABLED,
- OP_GEK_ROTATE,
OP_MEMBER_REVOKE,
- OP_MEMBER_UNPIN,
OP_SEARCH_LISTED,
)
from meshbay_common.groupbox import PURPOSE_ROSTER, seal
@@ -41,88 +39,6 @@ class GroupOpsMixin:
return
self._issue_admin_challenge(OP_MEMBER_REVOKE, user_id)
- def _do_gek_rotate(self, msg: dict) -> None:
- """
- Ask for a new group key. Operator only, and signed.
-
- This is what actually removes a revoked member's access: revocation
- stops the node serving the *next* key, and they still hold the current
- one. The node generates the replacement itself — nothing arriving here
- contributes key material, which is what the C5b rule is about.
- """
- group_id = str(msg.get("group_id", "")).strip() or self._group_id
- if not group_id:
- self._send({"type": "error", "detail": "No group on this connection"})
- return
- if not self._has_admin_authority():
- self._send({"type": "error", "detail": "No authorized key for this"})
- return
- self._issue_admin_challenge(OP_GEK_ROTATE, group_id, group_id=group_id)
-
- async def _admin_exec_gek_rotate(
- self, pending: dict, transcript: bytes, sig: bytes,
- ) -> None:
- if not await self._verify_admin_sig(transcript, sig):
- self._send({"type": "error", "detail": "Signature verification failed"})
- self._audit("admin_auth_failed", f"gek_rotate:{pending['subject'][:8]}")
- return
- try:
- result = await self._run_op(
- ops.set_gek, pending["subject"], rotate=True)
- except ops.OpError as e:
- self._send({"type": "error", "detail": e.message})
- return
- # The operator is rotating because somebody left, and the chat archive
- # key is not derived from the group key — so rotating that one does not
- # move this one. Doing both here is what makes "rotate after a removal"
- # mean the same thing for chat as it does for files.
- await self._new_chat_epoch(pending["subject"], "gek_rotate")
- self._audit("gek_rotate", pending["subject"])
- self._send({
- "type": MNP.GEK_ROTATE_ACK, "v": MNP_VERSION,
- "group_id": pending["subject"],
- "authorized_members": result.get("authorized_members", 0),
- # Said plainly, because rotating is the step people skip: content
- # already downloaded stays readable to whoever holds it.
- "note": "members re-receive the key on their next connect; content "
- "already downloaded is unaffected",
- })
-
- def _do_member_unpin(self, msg: dict) -> None:
- """Forget a pinned identity, so someone can pair again with a new key."""
- user_id = str(msg.get("user_id", "")).strip()
- if not user_id:
- self._send({"type": "error", "detail": "Missing user_id"})
- return
- if user_id == self._user_id:
- # Unpinning yourself over the connection your pin authorizes would
- # end that connection's authority mid-operation.
- self._send({"type": "error", "detail": "Cannot unpin yourself"})
- return
- if not self._has_admin_authority():
- self._send({"type": "error", "detail": "No authorized key for this"})
- return
- self._issue_admin_challenge(OP_MEMBER_UNPIN, user_id)
-
- async def _admin_exec_member_unpin(
- self, pending: dict, transcript: bytes, sig: bytes,
- ) -> None:
- user_id = pending["subject"]
- if not await self._verify_admin_sig(transcript, sig):
- self._send({"type": "error", "detail": "Signature verification failed"})
- self._audit("admin_auth_failed", f"member_unpin:{user_id[:8]}")
- return
- try:
- # The new chat epochs and the closed sessions are the op's own
- # (`ops.members._after_removal`), for every door alike.
- await self._run_op(ops.unpin_member, user_id)
- except ops.OpError as e:
- self._send({"type": "error", "detail": e.message})
- return
- self._audit("member_unpin", user_id)
- self._send({"type": MNP.MEMBER_UNPIN_ACK, "v": MNP_VERSION,
- "user_id": user_id})
-
# Every "application" a group can show. Photos joins this set (and
# apps.js's registry, client-side) when it lands; nothing else about
# this handler changes. DEFAULT_APPS (roster.py) deliberately does not
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/node_ops.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/node_ops.py
index acaa33f..237a359 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/node_ops.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/node_ops.py
@@ -1,16 +1,16 @@
-"""The operator's controls over the node itself: status and settings, roster
-and denylist, roots, hosted groups, reload, scan pacing and transfer limits."""
+"""The operator's controls over a group's roots and scan pacing that MNP carries:
+removing, ejecting and plugging a root. What widens the sharing, and the node's
+own status, settings, roster and denylist, are the loopback API's and the CLI's
+(MNP 6.0)."""
import logging
from meshbay_common import MNP_VERSION
from meshbay_common.adminop import (
- OP_GROUP_DETACH,
OP_ROOT_EJECT,
OP_ROOT_PLUG,
OP_ROOT_REMOVE,
OP_SET_SCAN_SETTINGS,
- OP_TRANSFER_LIMITS,
)
from meshbay_common.protocol import MNP
@@ -55,64 +55,6 @@ class NodeOpsMixin:
self._issue_admin_challenge(
OP_SET_SCAN_SETTINGS, f"{reconcile:g},{debounce:g}")
- MIN_TRANSFER_LIMIT = 1
- MAX_TRANSFER_LIMIT = 32
-
- def _do_transfer_limits(self, msg: dict) -> None:
- """How many transfers one member may run at once in this group.
-
- Zero is not "unlimited" and is refused: a member who may not transfer at
- all is a member the operator revokes, and reading 0 as no-limit would
- make the most dangerous value the easiest to type by accident.
- """
- try:
- downloads = int(msg.get("downloads"))
- uploads = int(msg.get("uploads"))
- except (TypeError, ValueError):
- self._send({"type": "error", "detail": "Invalid transfer limits"})
- return
- for value in (downloads, uploads):
- if not (self.MIN_TRANSFER_LIMIT <= value <= self.MAX_TRANSFER_LIMIT):
- self._send({"type": "error",
- "detail": f"transfer limits must be between "
- f"{self.MIN_TRANSFER_LIMIT} and "
- f"{self.MAX_TRANSFER_LIMIT}"})
- return
- if not self._has_admin_authority():
- self._send({"type": "error", "detail": "No authorized key for this"})
- return
- self._issue_admin_challenge(OP_TRANSFER_LIMITS,
- f"d={downloads},u={uploads}")
-
- async def _admin_exec_transfer_limits(
- self, pending: dict, transcript: bytes, sig: bytes,
- ) -> None:
- try:
- parts = dict(p.split("=") for p in pending["subject"].split(","))
- downloads, uploads = int(parts["d"]), int(parts["u"])
- except (ValueError, KeyError):
- self._send({"type": "error", "detail": "Invalid transfer limits"})
- return
- if not await self._verify_admin_sig(transcript, sig):
- self._send({"type": "error", "detail": "Signature verification failed"})
- self._audit("admin_auth_failed", f"transfer_limits:{pending['subject']}")
- return
- try:
- result = await self._run_op(
- ops.set_transfer_limits, self._group_id or "", downloads, uploads)
- except ops.OpError as e:
- self._send({"type": "error", "detail": e.message})
- return
- self._audit("transfer_limits", pending["subject"])
-
- notice = {"type": MNP.TRANSFER_LIMITS_ACK, "v": MNP_VERSION,
- "limits": result["limits"]}
- for session in list(self._peer_registry().values()):
- try:
- session._send(notice)
- except Exception:
- pass
-
async def _admin_exec_set_scan_settings(
self, pending: dict, transcript: bytes, sig: bytes,
) -> None:
@@ -141,150 +83,6 @@ class NodeOpsMixin:
except Exception:
pass
- # ── Node management (D5) ─────────────────────────────────────────────────
-
- async def _do_node_status(self, msg: dict) -> None:
- """All groups, roots, peers — the operator's overview.
-
- Including every root's absolute path, which is why this is gated on a
- proved operator device and not on an account the hub named.
- """
- node_uid = self._ctx.get("node_user_id")
- log.info("node_status: user=%s node_user=%s owner=%s device=%s",
- self._user_id, node_uid, self._is_node_admin(),
- "confirmed" if self._device_confirmed else "unidentified")
- if not await self._operator_device():
- self._send({"type": "error", "detail": "Not the node operator",
- "code": "not_operator"})
- return
- try:
- result = await self._run_op(ops.list_groups)
- self._send({"type": MNP.NODE_STATUS_ACK, "v": MNP_VERSION, **result})
- except ops.OpError as e:
- self._send({"type": "error", "detail": e.message})
- except Exception as e:
- log.error("node_status failed: %s", e, exc_info=True)
- self._send({"type": "error", "detail": "Internal error"})
-
- async def _do_node_settings_set(self, msg: dict) -> None:
- if not await self._operator_device():
- self._send({"type": "error", "detail": "Not the node operator",
- "code": "not_operator"})
- return
- settings = msg.get("settings", {})
- if not settings:
- self._send({"type": "error", "detail": "No settings provided"})
- return
- try:
- result = await self._run_op(ops.set_node_settings, settings)
- self._send({"type": MNP.NODE_SETTINGS_SET_ACK, "v": MNP_VERSION,
- **result})
- except ops.OpError as e:
- self._send({"type": "error", "detail": e.message})
- except Exception as e:
- log.error("node_settings_set failed: %s", e, exc_info=True)
- self._send({"type": "error", "detail": "Internal error"})
-
- async def _do_roster_read(self, msg: dict) -> None:
- if not await self._operator_device():
- self._send({"type": "error", "detail": "Not the node operator",
- "code": "not_operator"})
- return
- group_id = str(msg.get("group_id", "")).strip()
- try:
- result = await self._run_op(ops.read_roster, group_id)
- self._send({"type": MNP.ROSTER_READ_ACK, "v": MNP_VERSION, **result})
- except ops.OpError as e:
- self._send({"type": "error", "detail": e.message})
- except Exception as e:
- log.error("roster_read failed: %s", e, exc_info=True)
- self._send({"type": "error", "detail": "Internal error"})
-
- async def _do_denylist_read(self, msg: dict) -> None:
- if not await self._operator_device():
- self._send({"type": "error", "detail": "Not the node operator",
- "code": "not_operator"})
- return
- try:
- result = await self._run_op(ops.read_denylist)
- self._send({"type": MNP.DENYLIST_READ_ACK, "v": MNP_VERSION, **result})
- except ops.OpError as e:
- self._send({"type": "error", "detail": e.message})
- except Exception as e:
- log.error("denylist_read failed: %s", e, exc_info=True)
- self._send({"type": "error", "detail": "Internal error"})
-
- async def _do_denylist_clear(self, msg: dict) -> None:
- if not await self._operator_device():
- self._send({"type": "error", "detail": "Not the node operator",
- "code": "not_operator"})
- return
- subject = str(msg.get("subject", "")).strip()
- try:
- result = await self._run_op(ops.clear_denylist, subject=subject)
- self._audit("denylist_clear", subject or "all")
- self._send({"type": MNP.DENYLIST_CLEAR_ACK, "v": MNP_VERSION, **result})
- except ops.OpError as e:
- self._send({"type": "error", "detail": e.message})
- except Exception as e:
- log.error("denylist_clear failed: %s", e, exc_info=True)
- self._send({"type": "error", "detail": "Internal error"})
-
- def _do_group_detach(self, msg: dict) -> None:
- name = str(msg.get("name", "")).strip()
- if not name:
- self._send({"type": "error", "detail": "Missing group name or id"})
- return
- if not self._has_admin_authority():
- self._send({"type": "error", "detail": "No authorized key for this"})
- return
- self._issue_admin_challenge(
- OP_GROUP_DETACH, name,
- payload={"name": name},
- group_id="")
-
- async def _admin_exec_group_detach(
- self, pending: dict, transcript: bytes, sig: bytes,
- ) -> None:
- if not await self._verify_admin_sig(transcript, sig):
- self._send({"type": "error", "detail": "Signature verification failed"})
- self._audit("admin_auth_failed",
- f"group_detach:{pending['subject'][:16]}")
- return
- p = pending.get("payload") or {}
- try:
- result = await self._run_op(ops.detach_group, p["name"])
- except ops.OpError as e:
- self._send({"type": "error", "detail": e.message})
- return
- self._audit("group_detach", pending["subject"])
- self._send({"type": MNP.GROUP_DETACH_ACK, "v": MNP_VERSION, **result})
- state = self._ctx.get("daemon_state")
- reload_fn = state.get("reload_fn") if state else None
- if reload_fn:
- try:
- await reload_fn()
- except Exception as e:
- log.error("Reload after group_detach failed: %s", e)
-
- async def _do_node_reload(self, msg: dict) -> None:
- if not await self._operator_device():
- self._send({"type": "error", "detail": "Not the node operator",
- "code": "not_operator"})
- return
- state = self._ctx.get("daemon_state")
- reload_fn = state.get("reload_fn") if state else None
- if not reload_fn:
- self._send({"type": "error", "detail": "Reload not available"})
- return
- try:
- await reload_fn()
- self._send({"type": MNP.NODE_RELOAD_ACK, "v": MNP_VERSION,
- "status": "reloaded"})
- except Exception as e:
- log.error("node_reload failed: %s", e, exc_info=True)
- self._send({"type": "error", "detail": "Reload failed"})
-
def _do_root_remove(self, msg: dict) -> None:
target_group = str(msg.get("group_id", "")).strip()
root_name = str(msg.get("root_name", "")).strip()
diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py
index f810903..db11a09 100644
--- a/packages/meshbay-node/src/meshbay_node/ui/app.py
+++ b/packages/meshbay-node/src/meshbay_node/ui/app.py
@@ -535,10 +535,9 @@ def create_ui_app(state: dict) -> FastAPI:
@app.put("/api/groups/{group_id}/transfer-limits")
async def set_transfer_limits(group_id: str, payload: dict):
- # The same `ops.set_transfer_limits` the signed MNP handler calls. The
- # op existed with only that one door, and nothing anywhere opened it —
- # so the per-member cap sat at its default of 2 with no way to change
- # it, which from outside is indistinguishable from a hardcoded 2.
+ # The only door to `ops.set_transfer_limits` (the CLI uses it). It once
+ # had only a signed MNP message, which nothing anywhere sent — so the
+ # per-member cap sat at its default of 2 with no way to change it.
return await _op(lambda: ops.set_transfer_limits(
state, group_id,
int(payload.get("downloads", 0)), int(payload.get("uploads", 0))))
diff --git a/packages/meshbay-node/tests/golden/dispatch.json b/packages/meshbay-node/tests/golden/dispatch.json
index 345b077..61bbc45 100644
--- a/packages/meshbay-node/tests/golden/dispatch.json
+++ b/packages/meshbay-node/tests/golden/dispatch.json
@@ -76,22 +76,6 @@
"_admin_exec_file_delete"
]
},
- "gek_rotate": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_admin_exec_gek_rotate"
- ]
- },
- "group_detach": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_admin_exec_group_detach"
- ]
- },
"invite_cancel": {
"audit": [],
"log": [],
@@ -124,14 +108,6 @@
"_admin_exec_member_revoke"
]
},
- "member_unpin": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_admin_exec_member_unpin"
- ]
- },
"musicbrainz_enabled": {
"audit": [],
"log": [],
@@ -223,14 +199,6 @@
"spawned": [
"_admin_exec_tmdb_rematch"
]
- },
- "transfer_limits": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_admin_exec_transfer_limits"
- ]
}
},
"dispatch": {
@@ -4005,646 +3973,6 @@
"sent": [],
"spawned": []
},
- "denylist_clear | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear | member | bare": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_clear"
- ]
- },
- "denylist_clear | member | lists": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_clear"
- ]
- },
- "denylist_clear | member | numbers": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_clear"
- ]
- },
- "denylist_clear | member | strings": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_clear"
- ]
- },
- "denylist_clear | operator | bare": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_clear"
- ]
- },
- "denylist_clear | operator | lists": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_clear"
- ]
- },
- "denylist_clear | operator | numbers": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_clear"
- ]
- },
- "denylist_clear | operator | strings": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_clear"
- ]
- },
- "denylist_clear_ack | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear_ack | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear_ack | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear_ack | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear_ack | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear_ack | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear_ack | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear_ack | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_clear_ack | member | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_clear_ack | member | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_clear_ack | member | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_clear_ack | member | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_clear_ack | operator | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_clear_ack | operator | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_clear_ack | operator | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_clear_ack | operator | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_read | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read | member | bare": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_read"
- ]
- },
- "denylist_read | member | lists": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_read"
- ]
- },
- "denylist_read | member | numbers": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_read"
- ]
- },
- "denylist_read | member | strings": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_read"
- ]
- },
- "denylist_read | operator | bare": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_read"
- ]
- },
- "denylist_read | operator | lists": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_read"
- ]
- },
- "denylist_read | operator | numbers": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_read"
- ]
- },
- "denylist_read | operator | strings": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_denylist_read"
- ]
- },
- "denylist_read_ack | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read_ack | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read_ack | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read_ack | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read_ack | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read_ack | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read_ack | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read_ack | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "denylist_read_ack | member | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_read_ack | member | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_read_ack | member | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_read_ack | member | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_read_ack | operator | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_read_ack | operator | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_read_ack | operator | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "denylist_read_ack | operator | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
"device_add | challenged | bare": {
"audit": [],
"log": [],
@@ -8353,759 +7681,6 @@
"sent": [],
"spawned": []
},
- "gek_rotate | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate | member | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate | member | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate | member | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate | member | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate | operator | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "gggggggggggggggggggggggggggggggg",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "gek_rotate",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "gggggggggggggggggggggggggggggggg",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "6.0"
- }
- ],
- "spawned": []
- },
- "gek_rotate | operator | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "['x']",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "gek_rotate",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "['x']",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "6.0"
- }
- ],
- "spawned": []
- },
- "gek_rotate | operator | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "7",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "gek_rotate",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "7",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "6.0"
- }
- ],
- "spawned": []
- },
- "gek_rotate | operator | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "x",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "gek_rotate",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "x",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "6.0"
- }
- ],
- "spawned": []
- },
- "gek_rotate_ack | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate_ack | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate_ack | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate_ack | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate_ack | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate_ack | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate_ack | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate_ack | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "gek_rotate_ack | member | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "gek_rotate_ack | member | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "gek_rotate_ack | member | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "gek_rotate_ack | member | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "gek_rotate_ack | operator | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "gek_rotate_ack | operator | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "gek_rotate_ack | operator | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "gek_rotate_ack | operator | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_detach | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach | member | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Missing group name or id",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach | member | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach | member | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach | member | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach | operator | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Missing group name or id",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach | operator | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "group_detach",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "['x']",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "6.0"
- }
- ],
- "spawned": []
- },
- "group_detach | operator | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "group_detach",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "7",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "6.0"
- }
- ],
- "spawned": []
- },
- "group_detach | operator | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "group_detach",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "x",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "6.0"
- }
- ],
- "spawned": []
- },
- "group_detach_ack | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach_ack | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach_ack | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach_ack | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach_ack | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach_ack | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach_ack | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach_ack | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_detach_ack | member | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_detach_ack | member | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_detach_ack | member | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_detach_ack | member | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_detach_ack | operator | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_detach_ack | operator | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_detach_ack | operator | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_detach_ack | operator | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
"group_roster_req | challenged | bare": {
"audit": [],
"log": [],
@@ -13546,379 +12121,6 @@
"sent": [],
"spawned": []
},
- "member_unpin | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin | member | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Missing user_id",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin | member | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin | member | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin | member | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin | operator | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Missing user_id",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin | operator | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "gggggggggggggggggggggggggggggggg",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "member_unpin",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "['x']",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "6.0"
- }
- ],
- "spawned": []
- },
- "member_unpin | operator | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "gggggggggggggggggggggggggggggggg",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "member_unpin",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "7",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "6.0"
- }
- ],
- "spawned": []
- },
- "member_unpin | operator | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "gggggggggggggggggggggggggggggggg",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "member_unpin",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "x",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "6.0"
- }
- ],
- "spawned": []
- },
- "member_unpin_ack | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin_ack | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin_ack | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin_ack | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin_ack | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin_ack | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin_ack | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin_ack | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "member_unpin_ack | member | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "member_unpin_ack | member | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "member_unpin_ack | member | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "member_unpin_ack | member | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "member_unpin_ack | operator | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "member_unpin_ack | operator | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "member_unpin_ack | operator | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "member_unpin_ack | operator | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
"music_meta_req | challenged | bare": {
"audit": [],
"log": [],
@@ -14751,966 +12953,6 @@
"sent": [],
"spawned": []
},
- "node_reload | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload | member | bare": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_reload"
- ]
- },
- "node_reload | member | lists": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_reload"
- ]
- },
- "node_reload | member | numbers": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_reload"
- ]
- },
- "node_reload | member | strings": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_reload"
- ]
- },
- "node_reload | operator | bare": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_reload"
- ]
- },
- "node_reload | operator | lists": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_reload"
- ]
- },
- "node_reload | operator | numbers": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_reload"
- ]
- },
- "node_reload | operator | strings": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_reload"
- ]
- },
- "node_reload_ack | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload_ack | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload_ack | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload_ack | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload_ack | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload_ack | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload_ack | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload_ack | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_reload_ack | member | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_reload_ack | member | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_reload_ack | member | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_reload_ack | member | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_reload_ack | operator | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_reload_ack | operator | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_reload_ack | operator | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_reload_ack | operator | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_settings_set | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set | member | bare": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_settings_set"
- ]
- },
- "node_settings_set | member | lists": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_settings_set"
- ]
- },
- "node_settings_set | member | numbers": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_settings_set"
- ]
- },
- "node_settings_set | member | strings": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_settings_set"
- ]
- },
- "node_settings_set | operator | bare": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_settings_set"
- ]
- },
- "node_settings_set | operator | lists": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_settings_set"
- ]
- },
- "node_settings_set | operator | numbers": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_settings_set"
- ]
- },
- "node_settings_set | operator | strings": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_settings_set"
- ]
- },
- "node_settings_set_ack | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set_ack | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set_ack | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set_ack | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set_ack | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set_ack | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set_ack | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set_ack | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_settings_set_ack | member | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_settings_set_ack | member | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_settings_set_ack | member | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_settings_set_ack | member | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_settings_set_ack | operator | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_settings_set_ack | operator | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_settings_set_ack | operator | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_settings_set_ack | operator | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_status | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status | member | bare": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_status"
- ]
- },
- "node_status | member | lists": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_status"
- ]
- },
- "node_status | member | numbers": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_status"
- ]
- },
- "node_status | member | strings": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_status"
- ]
- },
- "node_status | operator | bare": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_status"
- ]
- },
- "node_status | operator | lists": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_status"
- ]
- },
- "node_status | operator | numbers": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_status"
- ]
- },
- "node_status | operator | strings": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_node_status"
- ]
- },
- "node_status_ack | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status_ack | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status_ack | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status_ack | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status_ack | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status_ack | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status_ack | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status_ack | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "node_status_ack | member | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_status_ack | member | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_status_ack | member | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_status_ack | member | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_status_ack | operator | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_status_ack | operator | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_status_ack | operator | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "node_status_ack | operator | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
"ping | challenged | bare": {
"audit": [],
"log": [],
@@ -17194,326 +14436,6 @@
"sent": [],
"spawned": []
},
- "roster_read | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read | member | bare": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_roster_read"
- ]
- },
- "roster_read | member | lists": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_roster_read"
- ]
- },
- "roster_read | member | numbers": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_roster_read"
- ]
- },
- "roster_read | member | strings": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_roster_read"
- ]
- },
- "roster_read | operator | bare": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_roster_read"
- ]
- },
- "roster_read | operator | lists": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_roster_read"
- ]
- },
- "roster_read | operator | numbers": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_roster_read"
- ]
- },
- "roster_read | operator | strings": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_do_roster_read"
- ]
- },
- "roster_read_ack | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read_ack | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read_ack | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read_ack | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read_ack | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read_ack | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read_ack | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read_ack | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "roster_read_ack | member | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "roster_read_ack | member | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "roster_read_ack | member | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "roster_read_ack | member | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "roster_read_ack | operator | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "roster_read_ack | operator | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "roster_read_ack | operator | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "roster_read_ack | operator | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
"search_listed | challenged | bare": {
"audit": [],
"log": [],
@@ -21724,365 +18646,6 @@
"sent": [],
"spawned": []
},
- "transfer_limits | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits | member | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Invalid transfer limits",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits | member | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Invalid transfer limits",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits | member | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits | member | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Invalid transfer limits",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits | operator | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Invalid transfer limits",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits | operator | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Invalid transfer limits",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits | operator | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "gggggggggggggggggggggggggggggggg",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "transfer_limits",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "d=7,u=7",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "6.0"
- }
- ],
- "spawned": []
- },
- "transfer_limits | operator | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Invalid transfer limits",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits_ack | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits_ack | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits_ack | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits_ack | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits_ack | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits_ack | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits_ack | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits_ack | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "transfer_limits_ack | member | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "transfer_limits_ack | member | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "transfer_limits_ack | member | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "transfer_limits_ack | member | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "transfer_limits_ack | operator | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "transfer_limits_ack | operator | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "transfer_limits_ack | operator | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "transfer_limits_ack | operator | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
"transfer_open | challenged | bare": {
"audit": [],
"log": [],
diff --git a/packages/meshbay-node/tests/test_admin_ops_mnp.py b/packages/meshbay-node/tests/test_admin_ops_mnp.py
index 7fd1c2b..898228e 100644
--- a/packages/meshbay-node/tests/test_admin_ops_mnp.py
+++ b/packages/meshbay-node/tests/test_admin_ops_mnp.py
@@ -1,17 +1,14 @@
"""
-`gek_rotate` and `member_unpin` over MNP.
+Rotating the group key and forgetting a pinned identity — `ops.set_gek` and
+`ops.unpin_member`, which the Node page and the CLI reach over loopback — and
+the H5 rule every signed MNP operation lives under.
-Both are destructive and both are new, so the tests are negative assertions:
-nobody without the operator's pinned key can reach them, a signature over the
-wrong transcript does not count, and the operation cannot be triggered by the
-request message alone.
-
-The rule these live under is worth restating, because it is easy to read
-draft-v5 §5.1 as forbidding them: **"nothing arriving over MNP can activate a
-GEK" is about key material arriving from outside** (C5b — a member handing the
-node a key of their choosing). An operator-signed instruction where the node
-generates the key with its own CSPRNG is a different shape, and it is the only
-thing that finishes a revocation: the ex-member still holds the current key.
+`gek_rotate` and `member_unpin` were MNP messages until 6.0. No client sent
+them, so they went; what they did is still tested here, at the door that is
+used. **"Nothing arriving over MNP can activate a GEK" is about key material
+arriving from outside** (C5b): the node generates the new key with its own
+CSPRNG, which is the only thing that finishes a revocation — the ex-member
+still holds the current key.
"""
import base64
@@ -19,14 +16,10 @@ from pathlib import Path
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
-from meshbay_common.adminop import (
- OP_GEK_ROTATE,
- OP_MEMBER_UNPIN,
- admin_transcript,
-)
+from meshbay_common.adminop import OP_CHAT_EPOCH, admin_transcript
from meshbay_common.crypto import pk_to_b64
from meshbay_common.join import ROLE_MEMBER, ROLE_OPERATOR
-from meshbay_common.protocol import MNP
+from meshbay_node import ops
from meshbay_node.indexer.group_index import GroupIndex
from meshbay_node.roster import open_roster
from meshbay_node.transport.webrtc_server import WebRTCPeerSession
@@ -127,58 +120,7 @@ async def _drain(session):
session.spawned.clear()
-async def _sign_and_exec(session, op: str, subject: str, sk, exec_fn):
- challenge = _last(session)
- assert challenge["type"] == "admin_challenge", challenge
- transcript = admin_transcript(
- op=op, node_pk_b64=session._node_pk_b64(), group_id=GROUP,
- subject=subject, nonce=base64.b64decode(challenge["nonce"]),
- ts=challenge["ts"])
- pending = session._admin_ops.get(challenge["op_id"]) or {
- "op": op, "subject": subject}
- await exec_fn(pending, transcript, sk.sign(transcript))
-
-
-# ── gek_rotate ───────────────────────────────────────────────────────────────
-
-async def test_rotation_needs_an_operator(tmp_path, roster):
- """Without a paired operator there is nobody who could sign, so the node
- fails closed and says why rather than issuing a challenge nobody can meet."""
- session = await _session(tmp_path, roster, operator=False)
-
- session._do_gek_rotate({})
-
- assert _last(session)["type"] == "error"
- assert "authorized key" in _last(session)["detail"]
- assert not session._admin_ops
-
-
-async def test_the_request_alone_rotates_nothing(tmp_path, roster):
- """The message asks; only a signature acts. A node that rotated here would
- let any member lock the group out."""
- session = await _session(tmp_path, roster, operator=True)
- before = session._ctx["groups"][GROUP]["gek"]
-
- session._do_gek_rotate({})
-
- assert _last(session)["type"] == "admin_challenge"
- assert session._ctx["groups"][GROUP]["gek"] == before
-
-
-async def test_a_members_signature_does_not_rotate(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=True)
- sk_mallory, pk_mallory = _keypair()
- await roster.pin_identity("mallory", "mallory", pk_mallory, pk_mallory, "code")
- await roster.set_member(GROUP, "mallory", ROLE_MEMBER, "active", "grenet")
- before = session._ctx["groups"][GROUP]["gek"]
-
- session._do_gek_rotate({})
- await _sign_and_exec(session, OP_GEK_ROTATE, GROUP, sk_mallory,
- session._admin_exec_gek_rotate)
-
- assert _last(session)["type"] == "error"
- assert session._ctx["groups"][GROUP]["gek"] == before
-
+# ── H5: a signature is for one operation ─────────────────────────────────────
async def test_a_signature_over_another_operation_does_not_count(tmp_path, roster):
"""
@@ -191,15 +133,18 @@ async def test_a_signature_over_another_operation_does_not_count(tmp_path, roste
control, and the test would pass while proving nothing.
"""
session = await _session(tmp_path, roster, operator=True)
- before = session._ctx["groups"][GROUP]["gek"]
+ _, pk_bob = _keypair()
+ await roster.pin_identity("bob", "bob", pk_bob, pk_bob, "code")
+ await roster.set_member(GROUP, "bob", ROLE_MEMBER, "active", "code")
- session._do_gek_rotate({})
+ session._do_member_revoke({"user_id": "bob"})
challenge = _last(session)
+ assert challenge["type"] == "admin_challenge", challenge
- # Signed over member_unpin, presented against the pending gek_rotate.
+ # Signed over chat_epoch, presented against the pending member_revoke.
wrong = admin_transcript(
- op=OP_MEMBER_UNPIN, node_pk_b64=session._node_pk_b64(), group_id=GROUP,
- subject=GROUP, nonce=base64.b64decode(challenge["nonce"]),
+ op=OP_CHAT_EPOCH, node_pk_b64=session._node_pk_b64(), group_id=GROUP,
+ subject="bob", nonce=base64.b64decode(challenge["nonce"]),
ts=challenge["ts"])
session._do_admin_response({
"op_id": challenge["op_id"],
@@ -208,19 +153,18 @@ async def test_a_signature_over_another_operation_does_not_count(tmp_path, roste
await _drain(session)
assert _last(session)["type"] == "error"
- assert session.state["groups_ctx"][GROUP]["gek"] == before
+ assert (await roster.get_member(GROUP, "bob"))["status"] == "active"
-async def test_the_operator_rotates_and_the_node_makes_the_key(tmp_path, roster):
+# ── Rotating the group key ───────────────────────────────────────────────────
+
+async def test_rotating_makes_a_new_key_on_the_node(tmp_path, roster):
session = await _session(tmp_path, roster, operator=True)
- before = session._ctx["groups"][GROUP]["gek"]
+ before = session.state["groups_ctx"][GROUP]["gek"]
- session._do_gek_rotate({})
- await _sign_and_exec(session, OP_GEK_ROTATE, GROUP, session.sk_op,
- session._admin_exec_gek_rotate)
+ result = await ops.set_gek(session.state, GROUP, rotate=True)
- ack = _last(session)
- assert ack["type"] == MNP.GEK_ROTATE_ACK, ack
+ assert result["rotated"] is True
after = session.state["groups_ctx"][GROUP]["gek"]
assert after != before, "the key did not change"
assert len(after) == 32
@@ -234,54 +178,21 @@ async def test_rotation_reaches_the_index(tmp_path, roster):
serve members a listing they cannot open."""
session = await _session(tmp_path, roster, operator=True)
- session._do_gek_rotate({})
- await _sign_and_exec(session, OP_GEK_ROTATE, GROUP, session.sk_op,
- session._admin_exec_gek_rotate)
+ await ops.set_gek(session.state, GROUP, rotate=True)
assert session.state["indexes"][GROUP].gek == \
session.state["groups_ctx"][GROUP]["gek"]
-# ── member_unpin ─────────────────────────────────────────────────────────────
-
-async def test_unpinning_needs_an_operator(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=False)
- session._do_member_unpin({"user_id": "bob"})
- assert _last(session)["type"] == "error"
-
-
-async def test_unpinning_yourself_is_refused(tmp_path, roster):
- """It would end the authority of the connection performing the operation,
- halfway through it."""
- session = await _session(tmp_path, roster, operator=True)
- session._do_member_unpin({"user_id": "grenet"})
- assert _last(session)["detail"] == "Cannot unpin yourself"
-
-
-async def test_a_members_signature_does_not_unpin(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=True)
- sk_bob, pk_bob = _keypair()
- await roster.pin_identity("bob", "bob", pk_bob, pk_bob, "code")
-
- session._do_member_unpin({"user_id": "bob"})
- await _sign_and_exec(session, OP_MEMBER_UNPIN, "bob", sk_bob,
- session._admin_exec_member_unpin)
-
- assert _last(session)["type"] == "error"
- assert await roster.get_identity("bob") is not None, (
- "a member removed their own pin — only the operator may")
-
+# ── Forgetting a pinned identity ─────────────────────────────────────────────
-async def test_the_operator_unpins(tmp_path, roster):
+async def test_unpinning_forgets_the_identity_and_its_bundle(tmp_path, roster):
session = await _session(tmp_path, roster, operator=True)
_, pk_bob = _keypair()
await roster.pin_identity("bob", "bob", pk_bob, pk_bob, "code")
- session._do_member_unpin({"user_id": "bob"})
- await _sign_and_exec(session, OP_MEMBER_UNPIN, "bob", session.sk_op,
- session._admin_exec_member_unpin)
+ await ops.unpin_member(session.state, "bob")
- assert _last(session)["type"] == MNP.MEMBER_UNPIN_ACK
assert await roster.get_identity("bob") is None
# The stored keypair bundle goes too — left behind it blocks the re-join
# the unpin exists to enable.
@@ -290,8 +201,21 @@ async def test_the_operator_unpins(tmp_path, roster):
async def test_unpinning_someone_unknown_says_so(tmp_path, roster):
session = await _session(tmp_path, roster, operator=True)
- session._do_member_unpin({"user_id": "nobody"})
- await _sign_and_exec(session, OP_MEMBER_UNPIN, "nobody", session.sk_op,
- session._admin_exec_member_unpin)
- assert _last(session)["type"] == "error"
- assert "No such pinned identity" in _last(session)["detail"]
+ with pytest.raises(ops.OpError, match="No such pinned identity"):
+ await ops.unpin_member(session.state, "nobody")
+
+
+# ── What MNP no longer carries ───────────────────────────────────────────────
+
+@pytest.mark.parametrize("op", ["gek_rotate", "member_unpin", "transfer_limits",
+ "group_detach"])
+def test_operations_no_client_sent_are_not_signed_ops_any_more(op):
+ """Gone with MNP 6.0: a door nobody uses is an untested way in. The Node
+ page and the CLI do the same work over loopback."""
+ from meshbay_common import adminop
+ from meshbay_node.transport.webrtc.admin import _ADMIN_EXECUTORS
+ from meshbay_node.transport.webrtc.dispatch import _HANDLERS
+
+ assert op not in _HANDLERS
+ assert op not in _ADMIN_EXECUTORS
+ assert op not in vars(adminop).values()
diff --git a/packages/meshbay-node/tests/test_node_status.py b/packages/meshbay-node/tests/test_node_status.py
index 257c60c..075d11f 100644
--- a/packages/meshbay-node/tests/test_node_status.py
+++ b/packages/meshbay-node/tests/test_node_status.py
@@ -1,25 +1,19 @@
"""
-node_status and root_remove over MNP.
+The node management panel's server-side behaviour.
-These test the D5 node management panel's server-side behaviour: the admin
-identity check on node_status, the list_groups operation, and the root
-removal flow through the MNP handlers. Adding a root is not an MNP message
-(MNP 6.0); `ops.add_root` is still tested here, as the loopback and CLI use it.
+The `_is_node_admin` hint, root removal over MNP, and the operations the Node
+page and the CLI reach over loopback: listing groups, adding and updating
+roots, the roster, the denylist and unpinning. Their own MNP messages
+(`node_status`, `root_add`, `roster_read`, …) are gone since MNP 6.0.
"""
-import base64
from dataclasses import asdict
from pathlib import Path
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
-from meshbay_common.adminop import (
- OP_MEMBER_UNPIN,
- admin_transcript,
-)
from meshbay_common.crypto import pk_to_b64
from meshbay_common.join import ROLE_OPERATOR
-from meshbay_common.protocol import MNP
from meshbay_node import ops
from meshbay_node.indexer.group_index import GroupIndex
from meshbay_node.roots import RootSet
@@ -134,19 +128,6 @@ async def _session(
return session
-async def _sign_and_exec(session, op: str, subject: str, sk, exec_fn,
- group_id: str = GROUP):
- challenge = _last(session)
- assert challenge["type"] == "admin_challenge", challenge
- transcript = admin_transcript(
- op=op, node_pk_b64=session._node_pk_b64(), group_id=group_id,
- subject=subject, nonce=base64.b64decode(challenge["nonce"]),
- ts=challenge["ts"])
- pending = session._admin_ops.get(challenge["op_id"]) or {
- "op": op, "subject": subject}
- await exec_fn(pending, transcript, sk.sign(transcript))
-
-
# ── _is_node_admin ──────────────────────────────────────────────────────────
async def test_is_node_admin_matches_user_id(tmp_path, roster):
@@ -168,62 +149,6 @@ async def test_is_node_admin_rejects_missing_node_user_id(tmp_path, roster):
assert not session._is_node_admin()
-# ── node_status ─────────────────────────────────────────────────────────────
-
-async def test_node_status_returns_groups_for_admin(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=True,
- node_user_id="grenet")
- session._spawn(session._do_node_status({}))
- await _drain(session)
- msg = _last(session)
- assert msg["type"] == MNP.NODE_STATUS_ACK
- assert len(msg["groups"]) == 1
- assert msg["groups"][0]["id"] == GROUP
-
-
-async def test_node_status_refused_for_non_admin(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=False,
- node_user_id="grenet")
- session._spawn(session._do_node_status({}))
- await _drain(session)
- msg = _last(session)
- assert msg["type"] == "error"
- assert "operator" in msg["detail"].lower()
-
-
-async def test_node_status_refused_when_user_is_operator_but_ids_mismatch(
- tmp_path, roster,
-):
- """A paired operator who is not the node owner cannot see node_status."""
- session = await _session(tmp_path, roster, operator=True,
- node_user_id="someone-else")
- session._spawn(session._do_node_status({}))
- await _drain(session)
- msg = _last(session)
- assert msg["type"] == "error"
-
-
-async def test_node_status_catches_send_failure(tmp_path, roster):
- """If _send itself throws (e.g. msgpack encoding fails), the error must
- not silently vanish — it used to, because _send was outside the try block."""
- session = await _session(tmp_path, roster, operator=True,
- node_user_id="grenet")
- sent = []
- call_count = [0]
-
- def _exploding_send(msg):
- call_count[0] += 1
- if msg.get("type") == "node_status_ack":
- raise TypeError("msgpack cannot encode this")
- sent.append(msg)
-
- session._send = _exploding_send
- session._spawn(session._do_node_status({}))
- await _drain(session)
- # The try/except around _send should catch the error and send an error reply
- assert any(m.get("type") == "error" for m in sent)
-
-
# ── ops.list_groups ─────────────────────────────────────────────────────────
async def test_list_groups_returns_group_metadata(tmp_path):
@@ -504,52 +429,32 @@ async def test_root_remove_refuses_missing_fields(tmp_path, roster):
assert "Missing" in _last(session)["detail"]
-# ── roster_read MNP handler ──────────────────────────────────────────────
-
-async def test_roster_read_returns_members_for_admin(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=True,
- node_user_id="grenet")
- session._spawn(session._do_roster_read({"group_id": GROUP}))
- await _drain(session)
- msg = _last(session)
- assert msg["type"] == "roster_read_ack"
- assert "members" in msg
- assert "identities" in msg
-
-
-async def test_roster_read_refused_for_non_admin(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=False,
- node_user_id="grenet")
- session._spawn(session._do_roster_read({"group_id": GROUP}))
- await _drain(session)
- msg = _last(session)
- assert msg["type"] == "error"
-
+# ── The roster, the denylist and unpinning (loopback, CLI) ──────────────────
+#
+# Their MNP messages are gone (MNP 6.0, no client sent them); the operations are
+# what the Node page and the CLI call.
-async def test_roster_read_filters_ghost_members(tmp_path, roster):
+async def test_read_roster_filters_ghost_members(tmp_path, roster):
"""Members whose identity was deleted (revoked then unpinned) are filtered
out by read_roster — the LEFT JOIN returns them with pk_ed25519 = NULL but
they should never reach the UI."""
session = await _session(tmp_path, roster, operator=True,
node_user_id="grenet")
- sk2, pk2 = _keypair()
+ _, pk2 = _keypair()
await roster.pin_identity("ghost", "ghost", pk2, pk2, "code")
await roster.set_member(GROUP, "ghost", "member", "revoked", "local-cli")
await roster._db.execute("DELETE FROM identities WHERE user_id = 'ghost'")
await roster._db.commit()
# Also add a real member so the roster isn't empty
- sk3, pk3 = _keypair()
+ _, pk3 = _keypair()
await roster.pin_identity("real", "real", pk3, pk3, "code")
await roster.set_member(GROUP, "real", "member", "active", "local-cli")
- session._spawn(session._do_roster_read({"group_id": GROUP}))
- await _drain(session)
- msg = _last(session)
- assert msg["type"] == "roster_read_ack"
- ghost = [m for m in msg["members"] if m["user_id"] == "ghost"]
- assert len(ghost) == 0, "ghost members must be filtered out"
- real = [m for m in msg["members"] if m["user_id"] == "real"]
- assert len(real) == 1
+ result = await ops.read_roster(session.state, GROUP)
+ assert "identities" in result
+ assert not [m for m in result["members"] if m["user_id"] == "ghost"], (
+ "ghost members must be filtered out")
+ assert len([m for m in result["members"] if m["user_id"] == "real"]) == 1
async def test_unpin_fails_for_ghost_member(tmp_path, roster):
@@ -557,118 +462,35 @@ async def test_unpin_fails_for_ghost_member(tmp_path, roster):
session = await _session(tmp_path, roster, operator=True,
node_user_id="grenet")
await roster.set_member(GROUP, "ghost", "member", "revoked", "local-cli")
- # ghost has no identity row
- session._do_member_unpin({"user_id": "ghost"})
- challenge = _last(session)
- assert challenge["type"] == "admin_challenge"
+ with pytest.raises(ops.OpError, match="No such pinned identity"):
+ await ops.unpin_member(session.state, "ghost")
- await _sign_and_exec(session, OP_MEMBER_UNPIN, "ghost",
- session.sk_op, session._admin_exec_member_unpin)
- msg = _last(session)
- assert msg["type"] == "error"
- assert "No such pinned identity" in msg["detail"]
-
-
-async def test_unpin_succeeds_for_real_identity(tmp_path, roster):
- """Full unpin flow: challenge → sign → exec → identity deleted."""
- session = await _session(tmp_path, roster, operator=True,
- node_user_id="grenet")
- sk2, pk2 = _keypair()
- await roster.pin_identity("target", "target", pk2, pk2, "code")
- await roster.set_member(GROUP, "target", "member", "active", "local-cli")
-
- session._do_member_unpin({"user_id": "target"})
- challenge = _last(session)
- assert challenge["type"] == "admin_challenge"
-
- await _sign_and_exec(session, OP_MEMBER_UNPIN, "target",
- session.sk_op, session._admin_exec_member_unpin)
- msg = _last(session)
- assert msg["type"] == "member_unpin_ack"
- assert msg["user_id"] == "target"
-
- idents = await roster.list_identities()
- assert not any(i["user_id"] == "target" for i in idents)
-
-# ── denylist_read MNP handler ────────────────────────────────────────────
-
-async def test_denylist_read_returns_entries_for_admin(tmp_path, roster):
+async def test_read_denylist_lists_entries(tmp_path, roster):
session = await _session(tmp_path, roster, operator=True,
node_user_id="grenet")
session.denylist.deny_user("bad-user")
- session._spawn(session._do_denylist_read({}))
- await _drain(session)
- msg = _last(session)
- assert msg["type"] == "denylist_read_ack"
- assert msg["count"] == 1
- assert "bad-user" in msg["users"]
-
-
-async def test_denylist_read_refused_for_non_admin(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=False,
- node_user_id="grenet")
- session._spawn(session._do_denylist_read({}))
- await _drain(session)
- msg = _last(session)
- assert msg["type"] == "error"
-
+ result = await ops.read_denylist(session.state)
+ assert result["count"] == 1
+ assert "bad-user" in result["users"]
-# ── denylist_clear MNP handler ───────────────────────────────────────────
-async def test_denylist_clear_removes_entry_for_admin(tmp_path, roster):
+async def test_clear_denylist_removes_one_entry(tmp_path, roster):
session = await _session(tmp_path, roster, operator=True,
node_user_id="grenet")
session.denylist.deny_user("bad-user")
session.denylist.deny_user("other-user")
- session._spawn(session._do_denylist_clear({"subject": "bad-user"}))
- await _drain(session)
- msg = _last(session)
- assert msg["type"] == "denylist_clear_ack"
- assert msg["removed"] == 1
+ result = await ops.clear_denylist(session.state, subject="bad-user")
+ assert result["removed"] == 1
assert "bad-user" not in session.denylist.entries()["users"]
assert "other-user" in session.denylist.entries()["users"]
-async def test_denylist_clear_all_for_admin(tmp_path, roster):
+async def test_clear_denylist_all(tmp_path, roster):
session = await _session(tmp_path, roster, operator=True,
node_user_id="grenet")
session.denylist.deny_user("a")
session.denylist.deny_user("b")
session.denylist.deny_jti("j")
- session._spawn(session._do_denylist_clear({"subject": ""}))
- await _drain(session)
- msg = _last(session)
- assert msg["type"] == "denylist_clear_ack"
- assert msg["removed"] == 3
-
-
-async def test_denylist_clear_refused_for_non_admin(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=False,
- node_user_id="grenet")
- session._spawn(session._do_denylist_clear({"subject": "bad-user"}))
- await _drain(session)
- msg = _last(session)
- assert msg["type"] == "error"
-
-
-# ── node_reload MNP handler ─────────────────────────────────────────────
-
-async def test_node_reload_runs_for_admin(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=True,
- node_user_id="grenet")
- session._spawn(session._do_node_reload({}))
- await _drain(session)
- msg = _last(session)
- assert msg["type"] == "node_reload_ack"
- assert msg["status"] == "reloaded"
- assert len(session.reload_called) == 1
-
-
-async def test_node_reload_refused_for_non_admin(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=False,
- node_user_id="grenet")
- session._spawn(session._do_node_reload({}))
- await _drain(session)
- msg = _last(session)
- assert msg["type"] == "error"
+ result = await ops.clear_denylist(session.state, subject="")
+ assert result["removed"] == 3
diff --git a/packages/meshbay-node/tests/test_security_regressions.py b/packages/meshbay-node/tests/test_security_regressions.py
index 43e88c2..26a3b1d 100644
--- a/packages/meshbay-node/tests/test_security_regressions.py
+++ b/packages/meshbay-node/tests/test_security_regressions.py
@@ -842,14 +842,11 @@ def test_node_control_api_serves_no_html():
# ── NS4 / M3: the node's own controls take no authority from the hub ─────────
-async def _owner_session(tmp_path, roster, *, device: str = "",
- confirmed: bool = False):
+async def _owner_session(tmp_path, roster):
"""A session whose token says it is the account this node belongs to.
Which is all a hub can decide: `_user_id` is the `sub` of a JWT it issued,
- so this is what an active hub forging a token arrives holding. Whether the
- *device* on the connection is one the node pinned as an operator is the
- other half, and no token can assert it.
+ so this is what an active hub forging a token arrives holding.
"""
from meshbay_node.indexer.group_index import GroupIndex
@@ -865,9 +862,6 @@ async def _owner_session(tmp_path, roster, *, device: str = "",
}
session._group_id = "g" * 32
session._user_id = "the-owner"
- session._username = "the-owner"
- session._pinned_pk = device
- session._device_confirmed = confirmed
session._pk_user = ""
session.sent = []
session._send = session.sent.append
@@ -876,18 +870,18 @@ async def _owner_session(tmp_path, roster, *, device: str = "",
@pytest.mark.asyncio
-async def test_a_token_naming_the_owner_is_not_node_authority(tmp_path):
+@pytest.mark.parametrize("mtype", ["node_status", "node_settings_set", "roster_read",
+ "denylist_read", "denylist_clear", "node_reload"])
+async def test_a_token_naming_the_owner_reaches_no_node_control(tmp_path, mtype):
"""
- The hub holds no user keys, so it cannot countersign a device — but it does
- choose what a token says. Six node-wide controls used to be gated on the
- account id alone, which is the hub's to decide: `node_status` (every group
- on the machine, with the operator's absolute paths), `node_settings_set`,
- `roster_read`, `denylist_read`, `denylist_clear` (the persisted revocation
- H4 exists to keep) and `node_reload`.
-
- An active hub reaches a completed handshake wherever it can also obtain the
- group key, which §3.5 concedes it can in an open-join group. From there,
- "the hub says you are the owner" was the whole of the check.
+ The hub holds no user keys, but it does choose what a token says. Six
+ node-wide controls were once gated on the account id alone: `node_status`
+ (every group on the machine, with the operator's absolute paths),
+ `node_settings_set`, `roster_read`, `denylist_read`, `denylist_clear` (the
+ persisted revocation H4 exists to keep) and `node_reload`. A device proof
+ closed that; MNP 6.0 removed the messages, since no client sent them. They
+ are the loopback API's and the CLI's — on the operator's own machine — and a
+ peer naming one over MNP is answered by nobody.
"""
from meshbay_node.roster import Roster
@@ -895,67 +889,8 @@ async def test_a_token_naming_the_owner_is_not_node_authority(tmp_path):
await roster.open()
try:
forged = await _owner_session(tmp_path, roster)
- await forged._do_node_status({})
- assert forged.sent[-1]["type"] == "error"
- assert forged.sent[-1]["code"] == "not_operator"
-
- forged.sent.clear()
- await forged._do_denylist_read({})
- assert forged.sent[-1]["type"] == "error"
-
- forged.sent.clear()
- await forged._do_denylist_clear({"subject": ""})
- assert forged.sent[-1]["type"] == "error", (
- "clearing the denylist undoes a revocation every node enforces")
- finally:
- await roster.close()
-
-
-@pytest.mark.asyncio
-async def test_an_identified_device_that_is_not_an_operator_is_refused(tmp_path):
- """Being a pinned member of the group is not being the node's operator.
-
- The device proof is real here; what it proves is an ordinary member's key,
- and `operator_pks()` is rebuilt from the roster on every call so a revoked
- one stops working at once.
- """
- from meshbay_common.crypto import pk_to_b64
- from meshbay_node.roster import Roster
-
- roster = Roster(db_path=tmp_path / "roster.db")
- await roster.open()
- try:
- sk = Ed25519PrivateKey.generate()
- member_pk = pk_to_b64(sk.public_key())
- await roster.pin_identity("the-owner", "the-owner", member_pk,
- member_pk, "code")
- session = await _owner_session(tmp_path, roster, device=member_pk,
- confirmed=True)
- await session._do_node_status({})
- assert session.sent[-1]["type"] == "error"
- finally:
- await roster.close()
-
-
-@pytest.mark.asyncio
-async def test_a_paired_operator_device_is_what_opens_it(tmp_path):
- """The positive case, so the test above is about authority and not about
- everything being refused."""
- from meshbay_common.crypto import pk_to_b64
- from meshbay_common.join import ROLE_OPERATOR
- from meshbay_node.roster import Roster
-
- roster = Roster(db_path=tmp_path / "roster.db")
- await roster.open()
- try:
- sk = Ed25519PrivateKey.generate()
- pk = pk_to_b64(sk.public_key())
- await roster.pin_identity("the-owner", "the-owner", pk, pk, "code")
- await roster.set_member("", "the-owner", ROLE_OPERATOR, "active",
- "local-cli")
- session = await _owner_session(tmp_path, roster, device=pk,
- confirmed=True)
- await session._do_denylist_read({})
- assert session.sent[-1]["type"] != "error", session.sent[-1]
+ forged._dispatch_message({"type": mtype, "subject": "", "group_id": "g" * 32,
+ "settings": {"max_peers": 1}})
+ assert forged.sent == [], f"{mtype} answered over MNP: {forged.sent}"
finally:
await roster.close()