ciphermesh 2.0.0 → 2.1.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/README.md +33 -6
- package/README.pt-BR.md +33 -6
- package/bin/ciphermesh.js +5 -0
- package/docs/ARCHITECTURE.md +41 -1
- package/docs/PLUGINS.md +69 -0
- package/examples/plugins/poll.js +28 -0
- package/examples/plugins/roll.js +24 -0
- package/package.json +2 -1
- package/src/client/ChatController.js +385 -9
- package/src/client/UI.js +128 -4
- package/src/client/index.js +50 -7
- package/src/crypto/RoomKey.js +162 -0
- package/src/crypto/TrustStore.js +47 -0
- package/src/p2p/P2PChatController.js +9 -1
- package/src/protocol/messages.js +31 -4
- package/src/protocol/validators.js +22 -1
- package/src/server/SessionManager.js +26 -2
- package/src/server/WebSocketServer.js +138 -1
- package/src/shared/AuditLog.js +3 -0
- package/src/shared/PluginManager.js +9 -6
- package/src/shared/config.js +27 -2
- package/src/shared/constants.js +8 -0
- package/src/shared/lastSession.js +55 -0
- package/src/shared/onboarding.js +111 -0
package/README.md
CHANGED
|
@@ -53,7 +53,9 @@ forwarding, survives CGNAT).
|
|
|
53
53
|
| 💬 | **Modern chat feel** | Right-aligned own messages, per-user emoji avatars, replies with quotes, `:fire:` → 🔥 |
|
|
54
54
|
| 🎞️ | **Animated UI** | Splash intro, reconnect spinner, live transfer bars (shimmer + ETA), a lock-closing handshake on connect, and a pulsing "new messages ↓" pill |
|
|
55
55
|
| 👻 | **Deniable & ephemeral** | Symmetric-crypto deniable mode; ephemeral messages *burn away* char-by-char when they expire |
|
|
56
|
+
| 🔒 | **Private rooms** | `/create <room> <password>` — zero-knowledge: the password never leaves your machine (Argon2id → Ed25519 challenge-response) and room content gets an extra symmetric layer the relay can't fake its way into |
|
|
56
57
|
| 🛰️ | **Serverless P2P mode** | mDNS peer discovery on the LAN — no relay at all |
|
|
58
|
+
| 🧩 | **Plugins** | Drop a JS file in `~/.ciphermesh/plugins` and get new slash-commands — `/roll` and `/poll` examples included ([Plugin API](docs/PLUGINS.md)) |
|
|
57
59
|
|
|
58
60
|
## 🚀 Quick start
|
|
59
61
|
|
|
@@ -131,12 +133,21 @@ QR code) to whoever you want to pull in.
|
|
|
131
133
|
|
|
132
134
|
| Command | Description |
|
|
133
135
|
|---------|-------------|
|
|
134
|
-
| `/join <room
|
|
135
|
-
| `/
|
|
136
|
+
| `/join <room> [password]` | Enter/create a room (password if it's private) |
|
|
137
|
+
| `/create <room> <password>` | Create a **private room** 🔒 — see below |
|
|
138
|
+
| `/rooms` | List rooms (🔒 marks private ones) |
|
|
136
139
|
| `/room` | Current room |
|
|
137
140
|
| `/owner` | Room owner |
|
|
138
141
|
| `/kick` `/mute` `/ban` | Owner moderation |
|
|
139
142
|
|
|
143
|
+
**Private rooms** are zero-knowledge: the password never leaves your machine.
|
|
144
|
+
Joining derives an Ed25519 key from the password (Argon2id) and answers a
|
|
145
|
+
server challenge with a signature — the server stores only a verifier, in
|
|
146
|
+
memory, and it dies when the last member leaves (rooms are always ephemeral).
|
|
147
|
+
On top of that, everything said in a private room carries an extra symmetric
|
|
148
|
+
layer keyed by the password, so even a malicious relay that let someone in
|
|
149
|
+
without verifying couldn't read a word. Share the password out-of-band.
|
|
150
|
+
|
|
140
151
|
</details>
|
|
141
152
|
|
|
142
153
|
<details>
|
|
@@ -148,8 +159,10 @@ QR code) to whoever you want to pull in.
|
|
|
148
159
|
| `/verify <nick>` | SAS code (~40-bit) + QR + key randomart for out-of-band verification |
|
|
149
160
|
| `/verify-confirm <nick>` | Mark peer as verified |
|
|
150
161
|
| `/trust <nick>` / `/trustlist` | Accept new key / trust status |
|
|
162
|
+
| `/contacts [add\|remove\|all]` | Contact book — persistent aliases on trust records ("this fingerprint is João"); shows in `/users`, rides along in identity backups |
|
|
151
163
|
| `/backup [path]` | Encrypted backup of identity + verified peers (restore at startup) |
|
|
152
164
|
| `/deniable [on\|off]` | Plausible-deniability mode |
|
|
165
|
+
| `/lock` / `/autolock <min\|off>` | Lock the screen behind the session passphrase — manually or after idle time (`autoLock` in config). Privacy for the "stepped away" moment; `/panic` is for the worst one |
|
|
153
166
|
| `/panic [yes]` | Duress wipe — securely erase all on-disk secrets (session, history, trust, keys) and exit |
|
|
154
167
|
| `/cover [on\|constant\|off]` | Cover traffic — `on` = jittered decoys, `constant` = steady-rate paced channel |
|
|
155
168
|
| `/theme [name]` | Nick colour theme: neon, matrix, mono, sunset, ocean |
|
|
@@ -183,7 +196,8 @@ A green **✓** next to a name marks a SAS-verified peer; a red **✗** flags a
|
|
|
183
196
|
|
|
184
197
|
| Command | Description |
|
|
185
198
|
|---------|-------------|
|
|
186
|
-
| `/away [reason]` / `/back` | Mark yourself away |
|
|
199
|
+
| `/away [reason]` / `/back` | Mark yourself away — while away, unreads are counted (`[away · N new]`) and `/back` shows a summary |
|
|
200
|
+
| `/mentions [n]` | Recent mentions of you this session (who, where, when) |
|
|
187
201
|
| `/status <text\|off>` | Free-form status — emojis welcome (`/status :fire: coding`) |
|
|
188
202
|
| `/react <emoji>` | React to the last message |
|
|
189
203
|
| `/edit` `/delete` | Edit/delete your last message |
|
|
@@ -194,11 +208,23 @@ A green **✓** next to a name marks a SAS-verified peer; a red **✗** flags a
|
|
|
194
208
|
|
|
195
209
|
</details>
|
|
196
210
|
|
|
197
|
-
Typing `:fire:` anywhere becomes 🔥 (Tab autocompletes shortcodes). **Ctrl+K** opens a fuzzy command palette, **Ctrl+E** an emoji picker. PageUp/PageDown scroll the history. **Alt+Enter** (or Shift+Enter where the terminal supports it, plus Ctrl+J) inserts a newline for multi-line messages; Enter sends. Markdown works: \`code\`, **bold**, *italic*, links, plus fenced \`\`\` code blocks and | tables |. Received images preview inline (half-blocks) and render full-res with `/img` on kitty/iTerm2. Day separators and message grouping keep the log clean.
|
|
211
|
+
Typing `:fire:` anywhere becomes 🔥 (Tab autocompletes shortcodes). **Ctrl+K** opens a fuzzy command palette, **Ctrl+E** an emoji picker. PageUp/PageDown scroll the history. **Alt+Enter** (or Shift+Enter where the terminal supports it, plus Ctrl+J) inserts a newline for multi-line messages; Enter sends. Pasting multi-line text (code included) keeps its line breaks — paste, check, Enter. Markdown works: \`code\`, **bold**, *italic*, links, plus fenced \`\`\` code blocks and | tables |. Received images preview inline (half-blocks) and render full-res with `/img` on kitty/iTerm2. Day separators and message grouping keep the log clean.
|
|
212
|
+
|
|
213
|
+
### First run & config file
|
|
214
|
+
|
|
215
|
+
On your very first run, a **30-second setup wizard** walks you through nickname,
|
|
216
|
+
colour theme and default server (with a 3-line crash course on how the
|
|
217
|
+
encryption works), then saves everything so later runs go straight to the chat.
|
|
218
|
+
Re-run it anytime with `ciphermesh --setup`; skip it in scripts/CI with
|
|
219
|
+
`--no-onboard`.
|
|
198
220
|
|
|
199
|
-
|
|
221
|
+
CipherMesh also remembers your **last session**: the server prompt defaults to
|
|
222
|
+
where you were, and after connecting you land back in your last room
|
|
223
|
+
automatically (private rooms excluded — their names never touch disk). Start
|
|
224
|
+
clean with `ciphermesh --fresh`.
|
|
200
225
|
|
|
201
|
-
|
|
226
|
+
The wizard writes `~/.ciphermesh/config.json` — you can also edit it by hand.
|
|
227
|
+
All keys are optional (unknown keys are ignored):
|
|
202
228
|
|
|
203
229
|
```json
|
|
204
230
|
{
|
|
@@ -211,6 +237,7 @@ Drop a `~/.ciphermesh/config.json` to set defaults and skip the prompts. All key
|
|
|
211
237
|
"cover": "constant",
|
|
212
238
|
"theme": "matrix",
|
|
213
239
|
"autoAway": 10,
|
|
240
|
+
"autoLock": 5,
|
|
214
241
|
"dnd": "22:00-08:00"
|
|
215
242
|
}
|
|
216
243
|
```
|
package/README.pt-BR.md
CHANGED
|
@@ -53,7 +53,9 @@ forwarding, imune a CGNAT).
|
|
|
53
53
|
| 💬 | **Cara de app moderno** | Suas mensagens à direita, avatar de emoji por usuário, reply com citação, `:fire:` → 🔥 |
|
|
54
54
|
| 🎞️ | **Interface animada** | Splash na abertura, spinner de reconexão, barra de transferência viva (shimmer + ETA), cadeado fechando no handshake e um selo pulsante "novas mensagens ↓" |
|
|
55
55
|
| 👻 | **Deniable e efêmeras** | Modo de negação plausível (crypto simétrica); mensagens efêmeras *queimam* caractere a caractere ao expirar |
|
|
56
|
+
| 🔒 | **Salas privadas** | `/create <sala> <senha>` — zero-knowledge: a senha nunca sai da sua máquina (Argon2id → challenge-response Ed25519) e o conteúdo da sala ganha uma camada simétrica extra que nem um relay malicioso atravessa |
|
|
56
57
|
| 🛰️ | **Modo P2P sem servidor** | Descoberta de peers via mDNS na LAN — sem relay nenhum |
|
|
58
|
+
| 🧩 | **Plugins** | Solta um arquivo JS em `~/.ciphermesh/plugins` e ganha comandos novos — exemplos `/roll` e `/poll` inclusos ([API de plugins](docs/PLUGINS.md)) |
|
|
57
59
|
|
|
58
60
|
## 🚀 Começando
|
|
59
61
|
|
|
@@ -131,12 +133,21 @@ quem você quiser puxar pra conversa.
|
|
|
131
133
|
|
|
132
134
|
| Comando | Descrição |
|
|
133
135
|
|---------|-----------|
|
|
134
|
-
| `/join <sala
|
|
135
|
-
| `/
|
|
136
|
+
| `/join <sala> [senha]` | Entra/cria uma sala (senha se for privada) |
|
|
137
|
+
| `/create <sala> <senha>` | Cria uma **sala privada** 🔒 — veja abaixo |
|
|
138
|
+
| `/rooms` | Lista salas (🔒 marca as privadas) |
|
|
136
139
|
| `/room` | Sala atual |
|
|
137
140
|
| `/owner` | Dono da sala |
|
|
138
141
|
| `/kick` `/mute` `/ban` | Moderação (dono da sala) |
|
|
139
142
|
|
|
143
|
+
**Salas privadas** são zero-knowledge: a senha nunca sai da sua máquina. Ao
|
|
144
|
+
entrar, o cliente deriva uma chave Ed25519 da senha (Argon2id) e responde um
|
|
145
|
+
desafio do servidor com uma assinatura — o servidor guarda só um verificador,
|
|
146
|
+
em memória, que morre quando a última pessoa sai (salas são sempre efêmeras).
|
|
147
|
+
Além disso, tudo que é dito numa sala privada carrega uma camada simétrica
|
|
148
|
+
extra derivada da senha: mesmo um relay malicioso que deixasse alguém entrar
|
|
149
|
+
sem verificar não leria uma palavra. Combine a senha por outro canal.
|
|
150
|
+
|
|
140
151
|
</details>
|
|
141
152
|
|
|
142
153
|
<details>
|
|
@@ -149,7 +160,9 @@ quem você quiser puxar pra conversa.
|
|
|
149
160
|
| `/verify-confirm <nick>` | Marca o peer como verificado |
|
|
150
161
|
| `/backup [caminho]` | Backup cifrado da identidade + peers verificados (restaura no startup) |
|
|
151
162
|
| `/trust <nick>` / `/trustlist` | Aceita chave nova / status de confiança |
|
|
163
|
+
| `/contacts [add\|remove\|all]` | Agenda — apelidos persistentes nos registros de confiança ("esse fingerprint é o João"); aparece no `/users` e viaja no backup de identidade |
|
|
152
164
|
| `/deniable [on\|off]` | Modo de negação plausível |
|
|
165
|
+
| `/lock` / `/autolock <min\|off>` | Trava a tela atrás da passphrase da sessão — na mão ou após inatividade (`autoLock` no config). Privacidade para o "saí um minuto"; o `/panic` é para o pior minuto |
|
|
153
166
|
| `/panic [sim]` | Wipe de coação — apaga com segurança todos os segredos do disco (sessão, histórico, confiança, chaves) e sai |
|
|
154
167
|
| `/cover [on\|constant\|off]` | Cover traffic — `on` = iscas com jitter, `constant` = canal de taxa constante |
|
|
155
168
|
| `/theme [nome]` | Tema de cores dos nicks: neon, matrix, mono, sunset, ocean |
|
|
@@ -183,7 +196,8 @@ Um **✓** verde ao lado de um nome indica um peer verificado por SAS; um **✗*
|
|
|
183
196
|
|
|
184
197
|
| Comando | Descrição |
|
|
185
198
|
|---------|-----------|
|
|
186
|
-
| `/away [motivo]` / `/back` | Marca/remove ausência |
|
|
199
|
+
| `/away [motivo]` / `/back` | Marca/remove ausência — enquanto ausente, não-lidas são contadas (`[away · N new]`) e o `/back` mostra um resumo |
|
|
200
|
+
| `/mentions [n]` | Menções recentes a você na sessão (quem, onde, quando) |
|
|
187
201
|
| `/status <texto\|off>` | Status livre — emoji à vontade (`/status :fire: codando`) |
|
|
188
202
|
| `/react <emoji>` | Reage à última mensagem |
|
|
189
203
|
| `/edit` `/delete` | Edita/apaga sua última mensagem |
|
|
@@ -195,11 +209,23 @@ Um **✓** verde ao lado de um nome indica um peer verificado por SAS; um **✗*
|
|
|
195
209
|
</details>
|
|
196
210
|
|
|
197
211
|
Digitar `:fire:` em qualquer lugar vira 🔥 (Tab autocompleta shortcodes).
|
|
198
|
-
**Ctrl+K** abre uma paleta de comandos fuzzy, **Ctrl+E** um seletor de emoji. PageUp/PageDown rolam o histórico. **Alt+Enter** (ou Shift+Enter onde o terminal suporta, além de Ctrl+J) insere uma nova linha para mensagens de várias linhas; Enter envia. Markdown funciona: \`código\`, **negrito**, *itálico*, links, além de blocos de código \`\`\` e | tabelas |. Imagens recebidas têm preview inline (half-blocks) e renderizam em alta resolução com `/img` no kitty/iTerm2. Separadores de dia e agrupamento de mensagens deixam o log limpo.
|
|
212
|
+
**Ctrl+K** abre uma paleta de comandos fuzzy, **Ctrl+E** um seletor de emoji. PageUp/PageDown rolam o histórico. **Alt+Enter** (ou Shift+Enter onde o terminal suporta, além de Ctrl+J) insere uma nova linha para mensagens de várias linhas; Enter envia. Colar texto multi-linha (código incluso) preserva as quebras — cola, confere, Enter. Markdown funciona: \`código\`, **negrito**, *itálico*, links, além de blocos de código \`\`\` e | tabelas |. Imagens recebidas têm preview inline (half-blocks) e renderizam em alta resolução com `/img` no kitty/iTerm2. Separadores de dia e agrupamento de mensagens deixam o log limpo.
|
|
213
|
+
|
|
214
|
+
### Primeira execução & arquivo de config
|
|
215
|
+
|
|
216
|
+
Na primeiríssima execução, um **wizard de 30 segundos** te guia por nickname,
|
|
217
|
+
tema de cores e servidor padrão (com um mini-curso de 3 linhas de como a
|
|
218
|
+
criptografia funciona) e salva tudo — das próximas vezes você cai direto no
|
|
219
|
+
chat. Refaça quando quiser com `ciphermesh --setup`; pule em scripts/CI com
|
|
220
|
+
`--no-onboard`.
|
|
199
221
|
|
|
200
|
-
|
|
222
|
+
O CipherMesh também lembra a sua **última sessão**: o prompt de servidor passa
|
|
223
|
+
a ter como padrão onde você estava, e depois de conectar você volta sozinho
|
|
224
|
+
para a última sala (salas privadas ficam de fora — o nome delas nunca toca o
|
|
225
|
+
disco). Para começar do zero: `ciphermesh --fresh`.
|
|
201
226
|
|
|
202
|
-
|
|
227
|
+
O wizard grava o `~/.ciphermesh/config.json` — que você também pode editar na
|
|
228
|
+
mão. Todas as chaves são opcionais (chaves desconhecidas são ignoradas):
|
|
203
229
|
|
|
204
230
|
```json
|
|
205
231
|
{
|
|
@@ -212,6 +238,7 @@ Crie um `~/.ciphermesh/config.json` para definir padrões e pular os prompts. To
|
|
|
212
238
|
"cover": "constant",
|
|
213
239
|
"theme": "matrix",
|
|
214
240
|
"autoAway": 10,
|
|
241
|
+
"autoLock": 5,
|
|
215
242
|
"dnd": "22:00-08:00"
|
|
216
243
|
}
|
|
217
244
|
```
|
package/bin/ciphermesh.js
CHANGED
|
@@ -19,6 +19,11 @@ Usage:
|
|
|
19
19
|
Options:
|
|
20
20
|
-h, --help show this help
|
|
21
21
|
-v, --version show the version
|
|
22
|
+
|
|
23
|
+
Client options:
|
|
24
|
+
--setup re-run the first-run setup wizard
|
|
25
|
+
--no-onboard skip the first-run wizard (scripts/CI)
|
|
26
|
+
--fresh forget the last session (server/room) and start clean
|
|
22
27
|
`;
|
|
23
28
|
|
|
24
29
|
const arg = process.argv[2];
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -448,7 +448,12 @@ Types:
|
|
|
448
448
|
| `peer_left` | Server -> Clients | A peer left |
|
|
449
449
|
| `key_exchange` | Client -> Server -> Client | Public key exchange between peers |
|
|
450
450
|
| `encrypted_message` | Client -> Server -> Client | Encrypted message |
|
|
451
|
-
| `
|
|
451
|
+
| `change_room` | Client -> Server | Enter/create a room (+ optional `roomAuthPk` verifier when creating a private room) |
|
|
452
|
+
| `room_changed` | Server -> Client | Room switch confirmed (+ `private` flag) |
|
|
453
|
+
| `list_rooms` / `room_list` | Client <-> Server | Room list (each room carries a `private` flag) |
|
|
454
|
+
| `room_challenge` | Server -> Client | Target room is private — sign this nonce (see 6.9) |
|
|
455
|
+
| `room_auth` | Client -> Server | Ed25519 signature proving password knowledge (see 6.9) |
|
|
456
|
+
| `error` | Server -> Client | Error (duplicate nickname, wrong room password, etc.) |
|
|
452
457
|
| `ping` / `pong` | Bidirectional | Heartbeat |
|
|
453
458
|
|
|
454
459
|
#### `src/protocol/validators.js` — Validation
|
|
@@ -733,6 +738,41 @@ fingerprint = SHA-256(publicKey)
|
|
|
733
738
|
= "A1B2:C3D4:E5F6:7890"
|
|
734
739
|
```
|
|
735
740
|
|
|
741
|
+
### 6.9 Private Rooms (password-protected, zero-knowledge)
|
|
742
|
+
|
|
743
|
+
`/create <room> <password>` creates a room the server can gate **without ever
|
|
744
|
+
seeing the password** (`src/crypto/RoomKey.js`):
|
|
745
|
+
|
|
746
|
+
```
|
|
747
|
+
seed(64B) = Argon2id(password, salt = BLAKE2b("ciphermesh/room-v1:" + room))
|
|
748
|
+
├── authSeed (32B) → Ed25519 keypair (join authentication)
|
|
749
|
+
└── roomKey (32B) → secretbox key (room content layer)
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
**Join authentication (challenge-response):**
|
|
753
|
+
```
|
|
754
|
+
1. Creator sends change_room + roomAuthPk (the Ed25519 PUBLIC key).
|
|
755
|
+
The server stores it as the room's verifier — in memory only.
|
|
756
|
+
2. A joiner sends change_room → server replies room_challenge { nonce }.
|
|
757
|
+
3. The joiner signs "ciphermesh/room-auth-v1:room:nonce:sessionId" with the
|
|
758
|
+
password-derived secret key → room_auth { signature }.
|
|
759
|
+
4. The server verifies against the stored verifier. Wrong password ⇒ a
|
|
760
|
+
different keypair ⇒ invalid signature ⇒ ROOM_AUTH_FAILED.
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
The signature binds room, nonce **and** sessionId, so it can't be replayed by
|
|
764
|
+
another connection. Wrong-password attempts are throttled per connection, and
|
|
765
|
+
each guess costs the attacker a full Argon2id derivation client-side. When the
|
|
766
|
+
last member leaves, the room dies and the verifier with it — same ephemeral
|
|
767
|
+
lifecycle as public rooms.
|
|
768
|
+
|
|
769
|
+
**Content layer:** every payload sent in a private room is wrapped as
|
|
770
|
+
`{ rk: 1, n, c }` — `crypto_secretbox(payload, roomKey)` — *before* the normal
|
|
771
|
+
pairwise encryption (ratchet/sealed sender). So even a malicious relay that
|
|
772
|
+
skipped verification and injected a member could not read the room: reading
|
|
773
|
+
requires the password. The outer layer already pads ciphertexts, so the
|
|
774
|
+
wrapper adds no metadata leak.
|
|
775
|
+
|
|
736
776
|
---
|
|
737
777
|
|
|
738
778
|
## 7. Handshake Protocol
|
package/docs/PLUGINS.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# CipherMesh Plugin API
|
|
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.
|
|
5
|
+
|
|
6
|
+
## Quick start
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
mkdir -p ~/.ciphermesh/plugins
|
|
10
|
+
cp examples/plugins/roll.js examples/plugins/poll.js ~/.ciphermesh/plugins/
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Restart the client — `/plugins` lists what loaded, and `/roll 2d20+3` /
|
|
14
|
+
`/poll Pizza tonight? | yes | obviously` just work.
|
|
15
|
+
|
|
16
|
+
## Plugin format
|
|
17
|
+
|
|
18
|
+
A plugin is an ES module whose **default export** is:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
export default {
|
|
22
|
+
name: 'roll', // required, unique
|
|
23
|
+
description: 'Roll dice', // optional, shown by /plugins
|
|
24
|
+
commands: {
|
|
25
|
+
// key = command name (with or without the leading slash)
|
|
26
|
+
roll(args) {
|
|
27
|
+
// args: string[] — everything typed after the command, split on spaces
|
|
28
|
+
return { send: '🎲 4' };
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Files that fail to import, or lack `name`/`commands`, are skipped silently.
|
|
35
|
+
|
|
36
|
+
## Handler return values
|
|
37
|
+
|
|
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*. |
|
|
44
|
+
|
|
45
|
+
Handlers are synchronous — return the final value directly.
|
|
46
|
+
|
|
47
|
+
## Precedence and naming
|
|
48
|
+
|
|
49
|
+
Built-in commands always win: a plugin command named `/help` or `/join` is
|
|
50
|
+
never reached. Plugin commands are only tried when no built-in matches. If two
|
|
51
|
+
plugins register the same command, the one loaded last wins (load order is the
|
|
52
|
+
directory listing).
|
|
53
|
+
|
|
54
|
+
## Security model — read this
|
|
55
|
+
|
|
56
|
+
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.
|
|
58
|
+
|
|
59
|
+
- Only install plugins you wrote or read line-by-line.
|
|
60
|
+
- Treat a plugin file like you treat `curl | sh`.
|
|
61
|
+
- Plugins are never synced, auto-updated or downloaded by CipherMesh — the
|
|
62
|
+
only way code gets into `~/.ciphermesh/plugins/` is you putting it there.
|
|
63
|
+
|
|
64
|
+
## Included examples
|
|
65
|
+
|
|
66
|
+
- [`examples/plugins/roll.js`](../examples/plugins/roll.js) — dice roller,
|
|
67
|
+
D&D notation (`/roll 2d20+3`), result goes to the room.
|
|
68
|
+
- [`examples/plugins/poll.js`](../examples/plugins/poll.js) — quick poll
|
|
69
|
+
(`/poll question | opt A | opt B`), voted with the built-in `/react`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// CipherMesh example plugin: quick poll voted with reactions.
|
|
2
|
+
// Install: cp poll.js ~/.ciphermesh/plugins/
|
|
3
|
+
const NUMBERS = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣'];
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
name: 'poll',
|
|
7
|
+
description: 'Create a poll: /poll <question> | <option> | <option> [| …]',
|
|
8
|
+
commands: {
|
|
9
|
+
poll(args) {
|
|
10
|
+
const raw = args.join(' ');
|
|
11
|
+
if (!raw.includes('|')) {
|
|
12
|
+
return { info: 'Usage: /poll <question> | <option> | <option> [| …]' };
|
|
13
|
+
}
|
|
14
|
+
const [question, ...options] = raw
|
|
15
|
+
.split('|')
|
|
16
|
+
.map((s) => s.trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
if (!question || options.length < 2 || options.length > NUMBERS.length) {
|
|
19
|
+
return { info: `A poll needs a question and 2-${NUMBERS.length} options.` };
|
|
20
|
+
}
|
|
21
|
+
const lines = [`📊 **${question}**`];
|
|
22
|
+
options.forEach((opt, i) => lines.push(`${NUMBERS[i]} ${opt}`));
|
|
23
|
+
lines.push(`Vote by reacting — e.g. /react ${NUMBERS[0]}`);
|
|
24
|
+
// `send` → delivered to the whole room as a normal E2EE message.
|
|
25
|
+
return { send: lines.join('\n') };
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// CipherMesh example plugin: dice roller (D&D notation).
|
|
2
|
+
// Install: cp roll.js ~/.ciphermesh/plugins/
|
|
3
|
+
export default {
|
|
4
|
+
name: 'roll',
|
|
5
|
+
description: 'Roll dice: /roll [NdM+K] — e.g. /roll, /roll d20, /roll 2d20+3',
|
|
6
|
+
commands: {
|
|
7
|
+
roll(args) {
|
|
8
|
+
const spec = (args[0] || 'd6').toLowerCase();
|
|
9
|
+
const m = spec.match(/^(\d{0,2})d(\d{1,4})([+-]\d{1,4})?$/);
|
|
10
|
+
if (!m) {
|
|
11
|
+
return { info: 'Usage: /roll [NdM(+K)] — e.g. /roll 2d20+3' };
|
|
12
|
+
}
|
|
13
|
+
const count = Math.min(parseInt(m[1] || '1', 10) || 1, 20);
|
|
14
|
+
const sides = Math.max(2, parseInt(m[2], 10));
|
|
15
|
+
const modifier = parseInt(m[3] || '0', 10);
|
|
16
|
+
const rolls = Array.from({ length: count }, () => 1 + Math.floor(Math.random() * sides));
|
|
17
|
+
const total = rolls.reduce((a, b) => a + b, 0) + modifier;
|
|
18
|
+
const detail =
|
|
19
|
+
count > 1 || modifier ? ` (${rolls.join(' + ')}${modifier ? ` ${m[3]}` : ''})` : '';
|
|
20
|
+
// `send` → delivered to the whole room as a normal E2EE message.
|
|
21
|
+
return { send: `🎲 rolled ${spec}: **${total}**${detail}` };
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ciphermesh",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Secure terminal chat for the local network (LAN) with real end-to-end encryption (E2EE) using libsodium",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/client/index.js",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"bin",
|
|
12
12
|
"src",
|
|
13
13
|
"docs",
|
|
14
|
+
"examples",
|
|
14
15
|
"README.md",
|
|
15
16
|
"README.pt-BR.md",
|
|
16
17
|
"LICENSE"
|