ciphermesh 2.9.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,132 @@
3
3
  Notable changes per release. Older versions are reconstructed from the git
4
4
  history — the commit bodies and pull requests remain the fuller record.
5
5
 
6
+ ## 2.11.0
7
+
8
+ Groundwork for sender keys on the relay. **Nothing sends a group message yet** —
9
+ this release is the half that has to be in the field first, and the reason is in
10
+ "Added" below.
11
+
12
+ ### Added
13
+
14
+ - **Capability negotiation in `JOIN`.** `PROTOCOL_VERSION` is checked for exact
15
+ equality, so it can only say "same" or "refuse to talk". It cannot express
16
+ _newer, but still willing to speak the old way_, which is the only thing that
17
+ makes a protocol change survivable on a public hub where people upgrade
18
+ whenever they upgrade.
19
+
20
+ A client now lists what it can do in its `JOIN`; the relay validates the list,
21
+ stores it, and hands it on verbatim with the peer list, without ever acting on
22
+ it. A feature turns on only when **every member of the room** advertises it, so
23
+ one peer on an older build holds the whole room on the old path — which is the
24
+ outcome that keeps a half-upgraded room readable.
25
+
26
+ The relay advertises its own abilities separately, in `join_ack.serverCaps`. No
27
+ client can promise those on the relay's behalf, and some features need the
28
+ relay to play along, so a capable room sitting on an older hub is still not a
29
+ room that can switch paths.
30
+
31
+ - **Receiving group messages, and the relay fan-out that carries them.** The
32
+ relay path seals one envelope per peer, so a line in a room of fifty costs
33
+ fifty encryptions and fifty envelopes — against a 1 MiB/s byte budget, a padded
34
+ message to fifty people throttles the sender for saying one thing. Sender keys
35
+ make it one of each.
36
+
37
+ Reading ships a release ahead of writing on purpose. The switch is _every
38
+ member agrees_: if both arrived together it would only ever be true in rooms
39
+ where everybody upgraded in the same moment.
40
+
41
+ A room-addressed message carries no `to`, because there is no single recipient,
42
+ and no `from`, because the relay must not become the one place on this wire
43
+ that asserts who is speaking. A packet names its _chain_ instead — an opaque
44
+ label members resolve through the sender key they were handed over the pairwise
45
+ channel, and which the relay cannot invert.
46
+
47
+ - **Per-sender signatures on group messages.** A sender chain is symmetric: every
48
+ member holds the key that decrypts a given sender, and can therefore also
49
+ produce ciphertext on it. Without something asymmetric on top, "Alice said
50
+ this" only ever meant "somebody in this room said this" — tolerable in a small
51
+ P2P mesh, not on a public hub where `general` has no owner and no admission
52
+ control.
53
+
54
+ Each sender now holds an Ed25519 keypair for the life of its chain, rotated
55
+ with it. Recipients verify before touching the ratchet, so an unauthenticated
56
+ packet carrying a large counter cannot make them derive and cache a thousand
57
+ message keys.
58
+
59
+ - **`docs/PROTOCOL.md`.** The wire format field by field, with the limits, the
60
+ failure modes, and the reason attached wherever the relay is forbidden to do
61
+ something. It also states what the relay _does_ learn, because a specification
62
+ that lists only the protections is misleading.
63
+
64
+ ### Changed
65
+
66
+ - **Room-addressed messages are never queued for absent members**, unlike
67
+ unicast. Not policy — arithmetic: a sender key handed over on someone's return
68
+ serialises the chain at its current counter, so the backlog is unreadable to
69
+ them whatever the relay does with it. Queueing would store ciphertext on the
70
+ relay that provably nobody can open.
71
+
72
+ ### Internal
73
+
74
+ - **Frozen test vectors for sender keys.** The previous tests round-tripped a
75
+ sender against a receiver built from that same sender — a mirror, not a
76
+ reference, which agrees with itself however the KDF is defined. Swapping the
77
+ two domain tags left the old suite fully green while making two clients unable
78
+ to read each other. The vectors pin the absolute values, and moving them is a
79
+ protocol version bump rather than a regeneration.
80
+
81
+ - **A guarantees test that lives apart from the features it checks.** In August a
82
+ squash reverted the entire plugin-approval control and nothing failed, because
83
+ that pull request's tests were reverted in the same commit. `test/guarantees.test.js`
84
+ asserts what the project promises — through the surfaces a user goes through,
85
+ and deliberately not next to any feature — so deleting a feature now leaves its
86
+ proof behind, failing. Verified by mutation rather than assumed.
87
+
88
+ - **Branch protection and CODEOWNERS.** `dev` and `master` now require CI, and
89
+ refuse force pushes and deletions.
90
+
91
+ ## 2.10.0
92
+
93
+ ### Added
94
+
95
+ - **\`--check\` for operators.** The deploy guide already explained every
96
+ footgun here, which is the problem: a document is read once, by whoever set
97
+ the machine up, while the misconfiguration lasts as long as the machine does.
98
+ The worst of them — \`TRUST_PROXY\` left off behind a reverse proxy — is
99
+ invisible from outside, because everything works and the per-IP cap, the rate
100
+ limit and the banlist simply apply to the proxy and protect nobody.
101
+
102
+ \`ciphermesh-server --check\` validates and exits without opening a socket, so
103
+ it is safe against a live host, and exits non-zero on an error so a deploy
104
+ script can gate on it. The same findings print at every startup, because a
105
+ warning you have to ask for is a warning nobody sees.
106
+
107
+ - **Connection-rate limiting.** The relay capped how many sockets one address
108
+ could hold, and how many messages a session could send, but nothing capped how
109
+ fast an address could _open_ connections. Connect, run the hybrid handshake,
110
+ disconnect, repeat: the concurrency cap never trips because the sockets are
111
+ never held, and every attempt costs the relay an X25519 and an ML-KEM-768
112
+ operation while costing the client almost nothing. That asymmetry was the one
113
+ real denial-of-service route into a public hub.
114
+
115
+ An address that exceeds `CONNECTION_RATE_PER_MINUTE` (60 by default, still
116
+ LAN-friendly) is refused for a minute, then five, then thirty. An hour of
117
+ behaving clears the record, so a shared NAT gateway cannot accumulate strikes
118
+ forever, and the refusal says how long to wait so a well-behaved client backs
119
+ off instead of extending its own ban.
120
+
121
+ - **A byte budget per connection** (`MAX_BYTES_PER_SECOND`,
122
+ `MAX_BYTES_BURST`). The message limit counts messages, and messages are padded
123
+ into buckets of up to 32 KiB, so a session sitting at the limit is a
124
+ multi-megabit stream. Bytes are the resource that runs out. The burst
125
+ allowance keeps a legitimate file transfer from looking like an attack.
126
+
127
+ Both are continuously refilling token buckets rather than fixed windows: a
128
+ fixed window lets a caller spend its whole allowance at the end of one window
129
+ and again at the start of the next, which is twice the intended rate at
130
+ exactly the moment an attacker aims for.
131
+
6
132
  ## 2.9.0
7
133
 
8
134
  ### Added
package/README.md CHANGED
@@ -19,7 +19,7 @@
19
19
 
20
20
  **[ciphermesh.de](https://ciphermesh.de)** — the website, and the public hub anyone can join.
21
21
 
22
- **[🇧🇷 Leia em Português](README.pt-BR.md)** · [Setup Guide](docs/SETUP.md) · [Architecture](docs/ARCHITECTURE.md) · [Security Policy](SECURITY.md) · [Hub Terms](TERMS.md) · [Contributing](CONTRIBUTING.md)
22
+ **[🇧🇷 Leia em Português](README.pt-BR.md)** · [Setup Guide](docs/SETUP.md) · [Architecture](docs/ARCHITECTURE.md) · [Wire Protocol](docs/PROTOCOL.md) · [Security Policy](SECURITY.md) · [Hub Terms](TERMS.md) · [Contributing](CONTRIBUTING.md)
23
23
 
24
24
  <img src="docs/demo.svg" alt="CipherMesh terminal demo" width="720">
25
25
 
@@ -204,10 +204,11 @@ software was built for.
204
204
  | ----------------------------- | ----------------------------------------------------------------- |
205
205
  | `/help` | All commands |
206
206
  | `/tips` | Show a rotating security/UX tip |
207
+ | `/plugins` | List loaded plugins and the commands they add |
207
208
  | `/users` | Who's online (with away/status) |
208
209
  | `/msg <nick> <text>` | Private message (DM) |
209
210
  | `/reply <text>` | Reply quoting the last received message |
210
- | `/me <action>` | Third-person action — _«felipe is compiling»_ |
211
+ | `/me <action>` | Third-person action — _«ana is compiling»_ |
211
212
  | `/watch [add\|remove\|clear]` | Alert on a keyword in **any** room, like a mention |
212
213
  | `/invite [host:port]` | Generate a `ciphermesh://` invite + QR code |
213
214
  | `/nick <new>` | Change nickname (before joining — recovers from "nickname taken") |
@@ -294,6 +295,7 @@ A green **✓** next to a name marks a SAS-verified peer; a red **✗** flags a
294
295
  | Command | Description |
295
296
  | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
296
297
  | `/away [reason]` / `/back` | Mark yourself away — while away, unreads are counted (`[away · N new]`) and `/back` shows a summary |
298
+ | `/autoaway <minutes\|off>` | Go away by itself after that long without typing (1–240 minutes) |
297
299
  | `/mentions [n]` | Recent mentions of you this session (who, where, when) |
298
300
  | `/status <text\|off>` | Free-form status — emojis welcome (`/status :fire: coding`) |
299
301
  | `/react <emoji>` | React to the last message — the emoji lands **on the message**, with a count when several people react |
@@ -325,7 +327,7 @@ All keys are optional (unknown keys are ignored):
325
327
 
326
328
  ```json
327
329
  {
328
- "nickname": "felipe",
330
+ "nickname": "ana",
329
331
  "server": "wss://100.x.y.z:3600",
330
332
  "sound": false,
331
333
  "notify": true,
package/README.pt-BR.md CHANGED
@@ -209,7 +209,7 @@ aquela para a qual este software foi feito.
209
209
  | `/users` | Quem está online (com away/status) |
210
210
  | `/msg <nick> <texto>` | Mensagem privada (DM) |
211
211
  | `/reply <texto>` | Responde citando a última mensagem recebida |
212
- | `/me <ação>` | Ação em terceira pessoa — _«felipe está compilando»_ |
212
+ | `/me <ação>` | Ação em terceira pessoa — _«ana está compilando»_ |
213
213
  | `/watch [add\|remove\|clear]` | Alerta quando uma palavra aparece em **qualquer** sala, como uma menção |
214
214
  | `/invite [host:porta]` | Gera convite `ciphermesh://` + QR code |
215
215
  | `/nick <novo>` | Troca de apelido (antes de entrar — recupera de "apelido em uso") |
@@ -328,7 +328,7 @@ mão. Todas as chaves são opcionais (chaves desconhecidas são ignoradas):
328
328
 
329
329
  ```json
330
330
  {
331
- "nickname": "felipe",
331
+ "nickname": "ana",
332
332
  "server": "wss://100.x.y.z:3600",
333
333
  "sound": false,
334
334
  "notify": true,
@@ -439,15 +439,21 @@ Defines the protocol's message types. All messages have:
439
439
  }
440
440
  ```
441
441
 
442
+ > The normative description of the wire format lives in
443
+ > **[docs/PROTOCOL.md](PROTOCOL.md)** — field by field, with the limits, the
444
+ > failure modes and what the relay is forbidden to do. This chapter is the tour;
445
+ > that document is the specification.
446
+
442
447
  Types:
443
448
  | Type | Direction | Description |
444
449
  |------|---------|-----------|
445
- | `join` | Client -> Server | The client wants to join (nickname + publicKey) |
450
+ | `join` | Client -> Server | The client wants to join (nickname + publicKey + optional `caps`, see 6.10) |
446
451
  | `join_ack` | Server -> Client | The server confirms the join (sessionId + peer list) |
447
452
  | `peer_joined` | Server -> Clients | A new peer joined (nickname + publicKey) |
448
453
  | `peer_left` | Server -> Clients | A peer left |
449
454
  | `key_exchange` | Client -> Server -> Client | Public key exchange between peers |
450
455
  | `encrypted_message` | Client -> Server -> Client | Encrypted message |
456
+ | `group_message` | Client -> Server -> Room | One ciphertext fanned out to a room's members (sender keys, see 6.11). No `to`, no `from` |
451
457
  | `change_room` | Client -> Server | Legacy single-room switch: leave every room, enter one (+ optional `roomAuthPk` when creating a private room) |
452
458
  | `room_changed` | Server -> Client | Room switch confirmed (+ `private` flag) |
453
459
  | `join_room` | Client -> Server | Multi-room: join an ADDITIONAL room, keeping current ones (same `roomAuthPk` option) |
@@ -514,10 +520,15 @@ export const FILE_CHUNK_SIZE = 49152; // 48KB
514
520
  "version": 1,
515
521
  "timestamp": 1739800000000,
516
522
  "nickname": "Alice",
517
- "publicKey": "base64(32 bytes da chave publica Curve25519)"
523
+ "publicKey": "base64(32 bytes da chave publica Curve25519)",
524
+ "caps": ["sk1"]
518
525
  }
519
526
  ```
520
527
 
528
+ `caps` is optional (see 6.10). It is omitted entirely when the client has
529
+ nothing to advertise, so the message is byte-identical to a pre-capability
530
+ client's.
531
+
521
532
  ### 5.2 Join ACK (server -> client)
522
533
 
523
534
  ```json
@@ -530,12 +541,17 @@ export const FILE_CHUNK_SIZE = 49152; // 48KB
530
541
  {
531
542
  "sessionId": "660e8400-e29b-41d4-a716-446655440001",
532
543
  "nickname": "Bob",
533
- "publicKey": "base64(chave publica do Bob)"
544
+ "publicKey": "base64(chave publica do Bob)",
545
+ "caps": ["sk1"]
534
546
  }
535
- ]
547
+ ],
548
+ "serverCaps": ["sk1"]
536
549
  }
537
550
  ```
538
551
 
552
+ `caps` is what each peer can do; `serverCaps` is what the relay itself can do.
553
+ Both are optional and both matter — see 6.11.
554
+
539
555
  ### 5.3 Encrypted Message (client -> server -> client)
540
556
 
541
557
  ```json
@@ -550,6 +566,63 @@ export const FILE_CHUNK_SIZE = 49152; // 48KB
550
566
 
551
567
  **Note (sealed sender, v2)**: The relay sees only `to` (for routing) and an opaque `sealed` blob. The sender (`from`) *and* the whole already-E2E-encrypted `payload` are sealed to the recipient's public key with libsodium `crypto_box_seal` — an anonymous box only the recipient can open. The relay therefore learns **neither who sent the message nor what's inside**, and it never stamps, stores, or logs a sender. The recipient opens the seal to recover `{ from, payload }`, then decrypts the payload as before. (See §10 for the exact — honest — guarantee and its limits.)
552
568
 
569
+ ### 5.3b Group Message (client -> server -> room)
570
+
571
+ ```json
572
+ {
573
+ "type": "group_message",
574
+ "version": 2,
575
+ "timestamp": 1739800001000,
576
+ "room": "general",
577
+ "keyId": "base64(16 random bytes labelling the sender's chain)",
578
+ "counter": 7,
579
+ "ciphertext": "base64(...)",
580
+ "nonce": "base64(24 bytes)",
581
+ "signature": "base64(64 bytes, Ed25519 over keyId|counter|ciphertext|nonce)"
582
+ }
583
+ ```
584
+
585
+ One ciphertext for the whole room, fanned out by the relay to every member
586
+ except the sender. Note what is **not** here: no `to`, because there is no single
587
+ recipient, and no `from`, because the relay must not be the thing that says who
588
+ is speaking — on this wire, identity only ever travels sealed.
589
+
590
+ Recipients pick the right sender chain with `keyId`, which they learned from the
591
+ `sk_dist` payload that member sent them over the pairwise channel. That envelope
592
+ authenticates the sender; the fan-out does not, and is not asked to. To the relay
593
+ `keyId` is a random string, and it already knows which socket sent the frame, so
594
+ it learns nothing from it.
595
+
596
+ **Why the signature is not optional.** A sender chain is symmetric: every member
597
+ holds the key that decrypts a given sender, which means every member can also
598
+ *produce* ciphertext on it. Without something asymmetric on top, "Alice said
599
+ this" only ever means "somebody in this room said this" — and `general` on a
600
+ public hub has no owner and no admission control, so that is a very wide set of
601
+ somebodies. Each sender therefore also holds an Ed25519 keypair for the life of
602
+ its chain; the public half travels in the distribution, over the pairwise channel
603
+ that already authenticates who sent it. Recipients verify **before** touching the
604
+ ratchet, so an unauthenticated packet carrying a large counter cannot make them
605
+ derive and cache a thousand message keys.
606
+
607
+ The relay checks only that the sender is a member of `room` — without that, one
608
+ connection could inject into every room on the hub at once, which the unicast
609
+ path cannot do because it needs a `sessionId` it could only have been told. It
610
+ cannot verify the signature and does not try: it holds no signing keys, and that
611
+ is the recipient's job.
612
+
613
+ **Not queued for absent members**, unlike the unicast path. A member who was away
614
+ could not read it anyway — a sender key handed over on their return serialises
615
+ the chain at its *current* counter, so anything sent while they were gone stays
616
+ shut. Queueing would store ciphertext on the relay that provably nobody can open.
617
+ The unicast queue survives because an envelope addressed to a peer *is* still
618
+ openable when they come back with the same key; that difference is what decides
619
+ it.
620
+
621
+ **Not yet sent by anything.** As of this release clients can *receive* a group
622
+ message; `#broadcastPayload` still seals one envelope per peer. Receive and
623
+ fan-out ship first so that when the send path lands, the rooms it negotiates
624
+ with can already read it.
625
+
553
626
  ### 5.4 Decrypted content (never travels in cleartext)
554
627
 
555
628
  After decrypting `payload.ciphertext`, the result is:
@@ -814,6 +887,56 @@ wrapper adds no metadata leak.
814
887
 
815
888
  ---
816
889
 
890
+ ### 6.11 Capability Negotiation
891
+
892
+ `PROTOCOL_VERSION` is checked for exact equality, so it can only say "same" or
893
+ "refuse to talk". It cannot express a client that is newer but still willing to
894
+ speak the old way — which is what rolling a protocol change through a public hub
895
+ needs, because people upgrade whenever they upgrade.
896
+
897
+ Capabilities fill that gap:
898
+
899
+ 1. A client lists what it can do in its `JOIN` (`caps: ["sk1"]`).
900
+ 2. The relay validates the list, stores it, and hands it on **verbatim** with
901
+ the peer list (`join_ack`) and with `peer_joined`. The relay never acts on a
902
+ capability itself.
903
+ 3. The relay advertises **its own** abilities separately, in `join_ack.serverCaps`.
904
+ No client can promise these on the relay's behalf, and some features need the
905
+ relay to play along — the group fan-out is its job, not a peer's. A capable
906
+ room sitting on an older hub is still not a room that can switch paths.
907
+ 4. A feature turns on only when **every** member of the room advertises it
908
+ (`roomSupports()` in `src/protocol/capabilities.js`) **and** the relay does
909
+ (`relaySupportsCapability()`). One peer on an older build, or one older hub,
910
+ holds the whole room on the old path.
911
+
912
+ **Compatibility:** the field is optional and omitted when empty, so a
913
+ pre-capability client's `JOIN` is unchanged, and an older relay that does not
914
+ know the field simply drops it — peers then see no capabilities and fall back,
915
+ which is the safe direction.
916
+
917
+ **Bounds:** the list arrives from a public hub, so it is capped at 16 entries of
918
+ up to 24 lowercase characters each. A malformed list gets the whole `JOIN`
919
+ rejected rather than filtered: the relay hands this to other clients, and
920
+ forwarding the good half of a bad list would make a peer look capable of
921
+ something it never claimed.
922
+
923
+ **What a hostile relay can do with it:** stripping capabilities forces the room
924
+ back onto the per-peer path, which is the status quo and reveals nothing new.
925
+ Adding one a peer never claimed makes senders encrypt in a form that peer cannot
926
+ read — denial of service, immediately visible, never a way to read plaintext.
927
+ The all-members rule is what keeps the damage on that side of the line.
928
+
929
+ | Capability | Advertised by | Meaning |
930
+ |---|---|---|
931
+ | `sk1` | client | I can **receive** a group message: I accept a sender key over the pairwise channel and can decrypt what that chain produces. |
932
+ | `sk1` | relay | I can fan a room-addressed `group_message` out to a room's members. |
933
+
934
+ Neither means "I send group messages" — nothing does yet. Receive and fan-out
935
+ are deliberately a release ahead of send, because the switch is *every member
936
+ agrees*: if the ability to read arrived with the ability to write, the switch
937
+ would only ever be true in rooms where everybody upgraded at the same moment,
938
+ which on a public hub is close to never. See `docs/design/sender-keys-on-relay.md`.
939
+
817
940
  ## 7. Handshake Protocol
818
941
 
819
942
  ### 7.1 Full Diagram
package/docs/PLUGINS.md CHANGED
@@ -1,7 +1,12 @@
1
1
  # CipherMesh Plugin API
2
2
 
3
- CipherMesh loads user plugins at startup from `~/.ciphermesh/plugins/*.js` and
4
- routes unknown slash-commands to them. Plugins work in both relay and P2P mode.
3
+ CipherMesh loads user plugins from `~/.ciphermesh/plugins/*.js` and routes
4
+ unknown slash-commands to them. Plugins work in both relay and P2P mode.
5
+
6
+ **Nothing there runs until you say so.** A file in that directory is found, not
7
+ loaded. Run `/plugins` to see what is waiting and `/plugins allow <file>` to
8
+ approve it — the approval is remembered in `~/.ciphermesh/config.json` under
9
+ `pluginsAllowed`, so you are asked once per file.
5
10
 
6
11
  ## Quick start
7
12
 
@@ -10,8 +15,15 @@ mkdir -p ~/.ciphermesh/plugins
10
15
  cp examples/plugins/roll.js examples/plugins/poll.js ~/.ciphermesh/plugins/
11
16
  ```
12
17
 
13
- Restart the client — `/plugins` lists what loaded, and `/roll 2d20+3` /
14
- `/poll Pizza tonight? | yes | obviously` just work.
18
+ Then in the client:
19
+
20
+ ```
21
+ /plugins → shows roll.js and poll.js waiting
22
+ /plugins allow roll → approved, loaded, and remembered
23
+ /plugins allow poll
24
+ ```
25
+
26
+ Now `/roll 2d20+3` and `/poll Pizza tonight? | yes | obviously` work.
15
27
 
16
28
  ## Plugin format
17
29
 
@@ -19,8 +31,8 @@ A plugin is an ES module whose **default export** is:
19
31
 
20
32
  ```js
21
33
  export default {
22
- name: 'roll', // required, unique
23
- description: 'Roll dice', // optional, shown by /plugins
34
+ name: 'roll', // required, unique
35
+ description: 'Roll dice', // optional, shown by /plugins
24
36
  commands: {
25
37
  // key = command name (with or without the leading slash)
26
38
  roll(args) {
@@ -31,16 +43,18 @@ export default {
31
43
  };
32
44
  ```
33
45
 
34
- Files that fail to import, or lack `name`/`commands`, are skipped silently.
46
+ A file that fails to import, or lacks `name`/`commands`, is skipped — but if you
47
+ approved it, `/plugins` says so. A plugin you asked for and did not get should
48
+ not disappear without a word.
35
49
 
36
50
  ## Handler return values
37
51
 
38
- | Return | Effect |
39
- |--------|--------|
40
- | `{ send: '<text>' }` | The text is **sent to the current room** as a normal end-to-end-encrypted message (and echoed locally). Markdown and multi-line text work. |
41
- | `{ info: '<text>' }` | Shown **only locally** as an info line. |
42
- | `'<text>'` (plain string) | Same as `{ info }` — the original API, still supported. |
43
- | `null` / `undefined` / throw | Treated as "not handled": the user sees *Unknown command*. |
52
+ | Return | Effect |
53
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
54
+ | `{ send: '<text>' }` | The text is **sent to the current room** as a normal end-to-end-encrypted message (and echoed locally). Markdown and multi-line text work. |
55
+ | `{ info: '<text>' }` | Shown **only locally** as an info line. |
56
+ | `'<text>'` (plain string) | Same as `{ info }` — the original API, still supported. |
57
+ | `null` / `undefined` / throw | Treated as "not handled": the user sees _Unknown command_. |
44
58
 
45
59
  Handlers are synchronous — return the final value directly.
46
60
 
@@ -54,13 +68,38 @@ directory listing).
54
68
  ## Security model — read this
55
69
 
56
70
  A plugin is **arbitrary JavaScript running inside your chat process**, with
57
- your privileges and full access to your keys in memory. There is no sandbox.
71
+ your privileges and full access to your keys in memory. There is no sandbox,
72
+ and approving one does not create one.
58
73
 
59
74
  - Only install plugins you wrote or read line-by-line.
60
75
  - Treat a plugin file like you treat `curl | sh`.
61
76
  - Plugins are never synced, auto-updated or downloaded by CipherMesh — the
62
77
  only way code gets into `~/.ciphermesh/plugins/` is you putting it there.
63
78
 
79
+ ### What approval actually buys
80
+
81
+ Before, any `.js` file appearing in that directory ran at the next start. The
82
+ warning above protected only the people who had already read it, and anything
83
+ able to write one file into a known path had code execution.
84
+
85
+ Now the file is listed and left alone until you approve it. That is the whole
86
+ guarantee, and it is worth being precise about its edges:
87
+
88
+ - **It is a consent step, not a sandbox.** An approved plugin can do everything
89
+ the client can do. Approve for the same reasons you would run a script.
90
+ - **Approval is per _file name_, not per plugin name.** That is forced, not
91
+ chosen: a plugin's own `name` lives inside the module, and reading it means
92
+ importing the module, and importing it is already running it. The check has
93
+ to work from the directory listing alone.
94
+ - **Replacing an approved file is not a new decision.** `roll.js` stays
95
+ approved even if its contents change completely. If you did not put the new
96
+ contents there, you have a bigger problem than plugins — but do not read the
97
+ approval as a promise about what the file contains.
98
+
99
+ There is no capability system, deliberately. Declaring what a plugin may do
100
+ without being able to enforce it would make the risk look bounded when it is
101
+ not, which is worse than the plain warning above.
102
+
64
103
  ## Included examples
65
104
 
66
105
  - [`examples/plugins/roll.js`](../examples/plugins/roll.js) — dice roller,